c++的类(附含explicit关键字,友元,内部类)

简介: 本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。


前言:
c++作为一个面向对象的高级编程语言特点就是:封装,继承,多态。

类就是这三大特点的基础,所以说学好类至关重要。

在c++中的设计中,属性和行为写在一起,表现事物,也就命名为类

语法:class 类名{ 访问权限: 属性/行为 };

也存在struct来写,但是不多,一般都是来用class

就比如我们写一个类,来求一个圆的周长;

include

using namespace std;
class Circle
{
public:
void Init(double r)
{
_r = r;
}
double print()
{
double C = _r 2 3.14;
return C;
}
private:
double _r;
};
int main()
{
Circle s1;
s1.Init(10.0);
cout << s1.print() << endl;
return 0;
}

还是可以到达我们的基础要求的。

这里的private与public全是访问限定符

而访问限定符一共分为三种

1.public 共有 被修饰的成员在类外面还可以访问 ,这一点在上图可以验证

2.protected 被保护 作用范围到下一个结束或到 “}” 结束

3.private 私有 被修饰的成员在类外面不可以访问 以下给出截图验证

确实不能。

类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。

但类里的毕竟全是函数,那么同样也可以做到声明与定义相分离,但是在分离后的定义需要在函数明前加上 类的名字:: 这一点的目的是告诉他去那可以找到声明;

  1. 声明和定义全部放在类体中(需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。)

  2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,(注意:成员函数名前需要加类名::)

就比如说我们把上面这个类进行定义与声明分离;

include"test.h"

void Circle::Init(double r)
{
_r = r;
}
double Circle::print()
{
double C = _r 2 3.14;
return C;
}
int main()
{
Circle s1;
s1.Init(10.0);
cout << s1.print() << endl;
return 0;
}

pragma once

include

using namespace std;
using namespace std;

class Circle
{
public:
void Init(double r);
double print();
private:
double _r;
};

类的大小计算:
就比如我们上面这个类进行代码计算,

发现是8,但是如果我们定义一个结构体,同样也只放一个double类的变量,那么也同样是8;

然后经过多次验证,我们得知,类的大小计算大小是与存相关顺序相同类型的结构体大小是相同的。

特例:一个成员变量也没有,这是为了对于这个类的占位,就会使其大小为默认的1(表示该类存在);

同样只存一个char也是1,但意义完全不同;

this指针
我们观察上面的代码,发现public是作为公共区的,但是如果定义多个同样的类,但不同在空间,同时进行初始化或者打印时,这是又怎么分清时调用的谁呢?

这一点this指针便解决了这一问题,其实类里面函数还有一个隐藏的参数就是this指针,这个指针就是指向的类变量s1(上图的),就在表明init与print就是在调用的s1的成员变量,这一点编译器其实帮我们做了,如果我们自己补充就是如下:

当然百分百会报语法错误,因为编译器已经帮我们存放了一个隐藏参数this指针,我们在添加上,必是重复,

这个截图也在证明确实有隐藏参数this指针;

类的6个默认成员函数

上文就是类的最基础的知识,但是类仅仅做的了这些,那就有点瞧不起c++的祖师爷了;

其实如果我们定义了一个这样的空类:

class A
{
private:
int a;
};
int main()
{
return 0;
}

看似这个类里面什么成员函数也没有,实则不然,

关于如何证明确实存在六个默认成员函数的方法,目前我还是不知道,我查看了反汇编无法证明,如果有的话还请大佬指出,让小弟学习一下

如何查看反汇编
如何查看反汇编这里提一下,毕竟下面还是要通过反汇编来详细讲解六大默认成员函数的;

然后

就调出来反汇编了;

这里先展示六大默认成员函数的名字:

构造函数

构造函数也称为初始化函数;

在我们上面写的一个求圆周长的类里面就有一个初始化

但是c++祖师爷就说,如果这被称为构造函数,那就有点小瞧我的c++了,祖师爷设计的构造函数就是为我们省去了去调用初始化函数,编译器会自动调用

其大致代码实现如下:

class A
{
public:
A(int b = 5)//函数名一定要是与类的名一样
{
a = b;
}
private:
int a;
};
int main()
{
A a(2);//在此处进行初始化
return 0;
}

编译器也确实进行了自动调用

那么我们从反汇编来查看呢?

还是可以观察到还是自动调用的;

但是针对于如此的函数,跟c差不了多少,还是无法体现c++的妙处,对此c++的祖师爷又进一步进行了扩展,将构造函数代码模板修改了这样:

这也就是平时所说的初始化列表:

注意:

1:每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

2:类中包含以下成员,必须放在初始化列表位置进行初始化:

     1引用成员变量

      2const成员变量

      3自定义类型成员(且该类没有默认构造函数时)
  1. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使 用初始化列表初始化。

  2. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

class A
{
public:
A(int b = 5,int d)//函数名一定要是与类的名一样
:a(b)
,c(d)
{
}
private:
int a;
int c;
};
int main()
{
A a(2,6);//在此处进行初始化
return 0;
}

要注意的是,第一个是:第二个是,没有加上;

圈红的地方是函数体,函数体内不是初始化,是简单的赋值;

但这个初始化的构造有一个特点,就是:无论构造内的顺序无论如何,他的初始化就是按照private的顺序来进行初始化的

就比如说:

还是按照先进行先初始化a,后初始化c,这就需要我们特别的注意

那么看完这些知识后,练习一个题吧:

设已经有A,B,C,D4个类的定义,程序中A,B,C,D构造函数调用顺序为?
C c;

int main()

{

A a;

B b;

static D d;

return 0;

}
通过观察一个全局对象,一个static修饰的静态对象,还有两个普通对象,从简单的来说肯定是先走a的构造后走b的。因为在走编译过程的时候都是从上而下所以肯定c是最先走构造函数的,最后在世d;所以构造顺序是 c a b d;

默认构造函数(由编译器自动生成):
这时候我们添加上打印,看看打印出来是什么效果

class A
{
public:
void print()
{
cout << "a = " << a << endl;
cout << "c = " << c << endl;
}
private:
int a;
int c;
};
int main()
{
A a;//在此处进行初始化
a.print();
return 0;
}

当然构造函数还是存在无参,全缺省。

在定义是初始化也有点区别的。

析构函数
其实析构函数就是我们在写各种在堆上存储的数据,在使用完后,需要进行free的过程,对于析构的理解还是比较容易的,他跟构造一样还是自动调用,不需要我们特意的去指明要去调用

析构也分为:

默认生成的析构,与我们写的析构

析构的特点:
1.析构函数名为:~ + 类名

2.析构函数无参数,无返回值

3.析构的顺序是先定义的后析构(类的析构函数调用一般按照构造函数调用的相反顺序进行调用)

对于默认生成的析构函数的特点:
1.内置类型成员(内置类型成员就是我们常用的int类型,double类型,float.....类型)不会处理,比如我们在private中定义的int a;就不会析构。

2.自定义类型成员(自定义类型就是我们自己定义的类型,比如类类型)会调用其成员自己的析构

对于此我们在我们上面的类加上一个析构(添加一个数组)

class A
{
public:
A(int b,int d)//函数名一定要是与类的名一样
:c(d)
,a(b)
{
}
void print()
{
cout << "a = " << a << endl;
cout << "c = " << c << endl;
}
~A()
{
free(x);//我没有对x进行初始化,写的不是很完整,主要在体现析构
x = nullptr;
}
private:
int a;
int* x;
int c;
};
int main()
{
A a;//在此处进行初始化
a.print();
return 0;
}

我们看一下反汇编,(再多定义一个类类型的变量)

这也证明了先定义的类类型,后进行析构函数。

那我再将上一个题改一下问法:

设已经有A,B,C,D4个类的定义,程序中A,B,C,D构造函数调用顺序为?
C c;

int main()

{

A a;

B b;

static D d;

return 0;

}

上面已经解释了析构的顺序与构造的恰恰相反,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象。原本的构造顺序是c → a → b → d

所以我们顺序逆过来就是: d b a c,在此基础顺序上只需要注意修改static修饰的静态对象析构的顺序即可,也就是说需注意static改变对象的生存作用域之后,会放在局部 对象之后进行析构,然而全局对象就不一样了,他的作用范围可不仅仅再main的作用域里面,不可以保证出了main以后不用,所以c是再d之后析构。所以最后答案是 b a d c

拷贝构造:
定义:
拷贝构造就是效果如其名,就是进行copy

但是拷贝构造函数只有单个形参,该形参是对本类类型对象的引用(注意:一般常用const修饰,是为了防止改变形参的值),然后再已经存在的类类型中进行创建新的对象进行copy

但实际上拷贝构造是构造函数的函数重载,这也就再说明拷贝构造的模板其实与构造函数没有多大的差别,再参数上一个是将要初始化的目的值,一个是类类型;

class A
{
public:
A(int b,int d)//函数名一定要是与类的名一样
:c(d)
,a(b)
{
}
A(const A& sub)
{
a = sub.a;
c = sub.c;
}
void print()
{
cout << "a = " << a << endl;
cout << "c = " << c << endl;
}
~A()
{
free(x);//我没有对x进行初始化,写的不是很完整,主要在体现析构
x = nullptr;
}
private:
int a;
int* x;
int c;
};
int main()
{
A a(1,2);//在此处进行初始化
A b(a);
a.print();
b.print();
return 0;
}

特点:
1.拷贝构造就是构造函数的一种重载函数,。

2.构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任 务并不是开空间创建对象,而是初始化对象。

3.拷贝构造的参数只有一个,并且这个参数是类类型的引用(使用传值方式编译器直接报错,因为会引发无穷递归调用)是因为传值的话,还会再开辟一个空间去拷贝这个形参,但是这个过程还会进行调用拷贝构造,然后依次类推还会调用拷贝构造,会无限循环。

比如就像这样:

对此还有编译器自动生成的默认拷贝函数:
默认生成的拷贝构造函数就是进行简单的值拷贝,并且还有以下特点:

1.内置类型成员会简单进行值拷贝

2.自定义类型成员会调用在自己的成员的拷贝构造函数

这就会导致以下这个问题(在注意事项中提及)

注意事项:

比如说栈的拷贝:

我们知道栈的实现一般会有三个成员变量:

这三个变量前两个进行值拷贝完全没有问题,但是问题出就出在第三个,第三个如果还是值拷贝的话就会引发下面这个问题:

这就会导致两个类类型的a全指向同一块空间,这就会导致如果对一个类类型对a 进行修改,就会导致对第二个类类型造成影响。显然这样会导致出现很多不必要的问题,对于此,就需要进行深拷贝。

深拷贝:

class stack
{
public:
stack()
{
a = nullptr;
top = capacity = 0;
}
stack(const stack& ps)
{
a = (int)malloc(sizeof(int) (ps.capacity));
if (a == NULL)
{
perror("malloc fail");
return;
}
memcpy(a, ps.a, sizeof(int) * (ps.top));
top = ps.top;
capacity = ps.capacity;
}
void push(int x)
{
//...
}
void StackPop()
{
//...
}
int StackTop()
{
//...
}
int StackEmpty()//空返回1,非空返回0
{

}
~stack()
{
    a = NULL;
    top = capacity = 0;
}

private:
int* a;
int top;
int capacity;
};

赋值重载:
赋值与拷贝就是差不多,但是赋值重载就是纯纯简单的赋值再加一下特殊的像拷贝构造那样的情况。

其实情况与拷贝大差不差。

class A
{
public:
A(int b,int d)//函数名一定要是与类的名一样
:c(d)
,a(b)
{
}
A(const A& sub)
{
a = sub.a;
c = sub.c;
}
A& operator=(const A& d)
{
if (this != &d)
{
a = d.a;
c = d.c;
}
return *this;
}
~A()
{
free(x);//我没有对x进行初始化,写的不是很完整,主要在体现析构
x = nullptr;
}
private:
int a;
int c;
};

特殊案例便不再展示别的

运算符重载
运算符重载是具有特殊函数名的函数 ,可以帮助实现自定义类型的大小比较

(千万要记得这是运算符,不是关键字)

函数名字为:关键字operator后面接需要重载的运算符符号。(如:operator+)
注意事项:

1.只能重载C++中已有的运算符,不能通过连接符号创建新的操作符。例如,operator@ 是非法的,因为 @ 不是C++中的有效运算符。
2.重载操作符必须有一个类类型(或枚举类型)参数,这是为了防止修改内置类型的运算符行为
3.用于内置类型的运算符,其含义不能改变(如:内置类型的 + 不能改变它原有的含义),你不能自己定义一个+,但是效果确实 减 。
4.作为类成员函数重载时,其形参看起来比操作数数目少1(因为成员函数的第一个参数为隐藏的this)

  1. (.*(成员指针访问运算符))(::(作用域解析运算符))(sizeof(大小运算符)(?:(三元条件运算符))(.(成员访问运算符))注意以上5个运算符不能重载(这个经常在笔试选择题中出现)

这里对于运算符的讲解不在多描述,个人还是感觉还是很简单的,

运算符重载又一大特点就是:只要写了一个完整的,就可以通过复用,来实现别的相似作用的运算符重载。

实现起来还是比较容易的,不在说明;

下面展示一下利用日期类实现各种的运算符重载

include"test.h"

date::date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
date::date(const date& d)
{
//cout << "date(date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
bool date::operator<(date& d)
{
return _year < d._year ||
((_year == d._year) && (_month < d._month)) ||
((_year == d._year) && (_month == d._month) && (_day < d._day));
}
bool date::operator==(date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}

bool date::operator!=(date& d)
{
return !(*this == d);
}

bool date::operator<=(date& d)
{
return this == d || this < d;
}

int date::Get_month_day(int year, int month)
{
int month_day[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
return 29;
}
return month_day[month];
}

date date::operator += (int day)
{
if (day < 0)
{
return this -= (-day);
}
_day += day;
while (_day > Get_month_day(_year, _month))
{
_day -= Get_month_day(_year, _month);
++_month;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return
this;
}

date date::operator + (int day)
{
if (day < 0)
{
return this - (-day);
}
date tmp(
this);
tmp += 50;
return tmp;

//date tmp(*this);
//tmp._day += day;
//while (tmp._day > Get_month_day(tmp._year, tmp._month))
//{
//    tmp._day -= Get_month_day(tmp._year, tmp._month);
//    ++tmp._month;
//    if (tmp._month == 13)
//    {
//        tmp._year++;
//        tmp._month = 1;
//    }
//}
//return tmp;

}
date date::operator-=(int day)
{
if (day < 0)
{
return this += (-day);
}
_day -= day;
while (_day < 0)
{
--_month;
_day += Get_month_day(_year, _month);
if (_month == 0)
{
--_year;
_month = 12;
}
}
return
this;
}
date date::operator-(int day)
{
if (day < 0)
{
return this + (-day);
}
date tmp(
this);
tmp -= day;
return tmp;
}

//日期 减 日期

int date::operator-(const date& d)
{
date max = this;
date min = d;
int flag = 1;
if (max < min)
{
flag = -1;
max = d;
min =
this;
}
int n = 0;
while (max != min)
{
++min;
++n;
}
return n * flag;
}

date date::operator++()//前置
{
this += 1;
return
this;
}
date date::operator++(int)//后置
{
date tmp(this); this += 1;
return tmp;
}

date date::operator--()
{
//this->operator-=(1);
this -= 1;//this为什么有时候加,有时候不加?//2
return this;
}
date date::operator--(int)
{
date tmp(
this);
*this -= 1;
return tmp;
}
void date::print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}

class date
{
public:
date(int year = 2, int month = 2, int day = 2);

date(const date& d);

bool operator<(date& d);

bool operator==(date& d);

bool operator!=(date& d);

bool operator<=(date& d);

int Get_month_day(int year, int month);

date operator += (int day);

date operator + (int day);

date operator-=(int day);

date operator-(int day);

//日期 减 日期

int operator-(const date& d);

date operator++();//前置

date operator++(int);//后置

//赋值
date& operator=(const date& d);

date operator--();
date operator--(int);

void print();

private:
int _year;
int _month;
int _day;
};
带有const修饰的成员函数
我们上面已经提到,类成员函数里面每个函数的参数都含有一个隐藏的参数就是this指针,这个指针如果我们需要达到要求,使得我们不可以对其进行修改,那么怎么去修改函数呢,

仔细想想,this指针是隐藏的参数,而且我们自己添加上就会使其重复,导致犯语法错误,这时候c++祖师爷就对此进行了优化,在函数末尾添加const进行修饰,就是表达const在修饰this指针。

取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。

class A
{
public:
A operator&()
{
return this;
}
const A
operator&()const
{
return this;
}
private:
int a;
};

struct B
{
private:
int _b = 4;
};
int main()
{
B b;
cout << &b << endl;
return 0;
}

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需 要重载,比如想让别人获取到指定的内容!

扩充部分:
explicit关键字
explicit关键字在写程序时使用的次数较少,但是仔细观察会发现,在C++标准库中的相关类声明中explicit出现的频率是很高的,那么explicit关键字到底有什么作用呢?接下来我就为大家一一解答.
explicit为清晰的;明确的之意.其作用便是:关键字explicit可以阻止隐式转换的发生。

比如: 单参数构造函数的隐式转换

单参数构造函数不仅可以用于构造对象,还可以用于隐式类型转换。具体来说,当构造函数满足以下条件之一时,它可以用于隐式类型转换:

在这种情况下,编译器可以自动将参数类型转换为类类型。 只有一个参数,那么这个参数承担着两种角色:
1.用于构建单参数的类对象.
2.隐含的类型转换操作符.

例如:一个类A的构造函数A(int i)就是,既可以用来作为构造器,又可以实现隐式转换A a=1;因为1可以通过构造函数A(int i)转换为一个类A的对象。(隐含的类型转换操作符)
如果还是不是很清楚,那么我们从定义与使用比较上来解释:

首先来看隐式转换的定义:

隐式转换是指在编程语言中自动将一种数据类型转换为另一种数据类型的过程,而不需要程序员显式地进行转换操作。隐式转换可以导致意想不到的结果和错误,因此某些编程语言提供了关键字(如explicit)来阻止隐式转换的发生。

例如,在C++中,通过使用关键字explicit来声明构造函数可以阻止隐式转换的发生。下面是一个示例:

include

class MyClass {
public:
explicit MyClass(int value) : m_value(value) {
}

int getValue() {
    return m_value;
}

private:
int m_value;
};

void printValue(MyClass obj) {
std::cout << "Value: " << obj.getValue() << std::endl;
}

int main() {
// MyClass obj1 = 10; // 这行代码会导致编译错误,因为explicit关键字阻止了int到MyClass的隐式转换
MyClass obj2(20); // 显式地创建一个MyClass对象
printValue(obj2);

return 0;

}

在上面的示例中,使用了关键字explicit来声明构造函数,阻止了int到MyClass的隐式转换。如果尝试编写类似于 MyClass obj1 = 10; 的代码,编译器将会报错。通过显式地调用构造函数MyClass obj2(20);,则可以避免隐式转换的发生。

在比如说多参数的情况下:

class Date
{
public:

//  虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转

//换作用
// explicit修饰构造函数,禁止类型转换
explicit Date(int year, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}

Date& operator=(const Date& d)
{
    if (this != &d)
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

    return *this;
}

private:
int _year;
int _month;
int _day;
};

void Test()
{
Date d1(2022);
}
int main()
{
Test();
return 0;
}

走完程序后会将d1的year初始化给定的2022;

当然我们使用了explicit修饰,已经不允许隐式类型转化如果直接 d1=2022,是不允许的,但当explicit删除后,就可以了;

总的来说:用explicit修饰构造函数,将会禁止构造函数的隐式转换

参考文献:

隐式转换的问题
隐式转换虽然方便,但也可能导致意想不到的行为和错误。例如:

void func(const A& a) {
// 一些操作
}

int main() {
func(10); // 隐式转换:将 int 转换为 A 类型
return 0;
}

static成员
概念:
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的 成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化

注意:

1:静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区

2:静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明

3:类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问

4:静态成员函数没有隐藏的this指针,不能访问任何非静态成员

5::静态成员也是类的成员,受public、protected、private 访问限定符的限制

include

using namespace std;class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
static int GetACount() { return _scount; }
private:
static int _scount;
};

int A::_scount = 0;
//或者int MyClass::count = 0; // 在类外定义静态成员变量,不添加 static 关键字

void TestA()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
}
int main()
{
TestA();
return 0;
}

问题一:静态成员函数可以调用非静态成员函数吗?

问题二:非静态成员函数可以调用类的静态成员函数吗?

这里我们用运行结果来验证:

class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
static int GetACount()
{
A(_count);
return _scount;
}
private:
static int _scount;
};

int A::_scount = 0;
//或者int MyClass::count = 0; // 在类外定义静态成员变量,不添加 static 关键字

void TestA()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
}
int main()
{
TestA();
return 0;
}

class A
{
public:
A() { A::GetACount(); }//或者GetACount();都可以
A(const A& t)
{
A::GetACount();
}
~A() { --_scount; }
static int GetACount()
{
++_scount;
return _scount;
}
private:
static int _scount;
};

int A::_scount = 0;
//或者int MyClass::count = 0; // 在类外定义静态成员变量,不添加 static 关键字

void TestA()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
}
int main()
{
TestA();
return 0;
}

友元
定义:
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

分类:
友元分为:友元函数和友元类

友元函数:
当你想为一个类重载 operator<<,使其能用 cout << myObject 这种形式输出对象时,你不能直接将其作为成员函数来实现。 这是因为:

成员函数的第一个隐含参数是 this 指针,指向调用该函数的对象。
对于 cout << myObject,我们希望 cout (一个 ostream 对象) 作为左操作数,myObject 作为右操作数。
如果 operator<< 是成员函数,this 指针会绑定到 myObject,而 cout 则无法成为左操作数。
对于此我们可以使用,全局函数重载: 将 operator<< 重载为全局函数,这样就可以显式地控制两个操作数的顺序。 函数原型通常是 ostream& operator<<(ostream& os, const MyClass& obj)。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}

// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
ostream& operator<<(ostream& _cout)
{
    _cout << _year << "-" << _month << "-" << _day << endl;
    return _cout;
}

private:
int _year;
int _month;
int _day;
};

因为友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

由于全局函数无法直接访问类的私有成员,你需要将这个全局函数声明为类的 友元 函数。 这允许该函数访问类的私有和保护成员,以便格式化输出对象的内容。

添加友元函数后:

class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}

private:
int _year;
int _month;
int _day;
};

ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}

istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}

int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}

注意

1:友元函数可访问类的私有和保护成员,但不是类的成员函数

2:友元函数不能用const修饰

3:友元函数可以在类定义的任何地方声明,不受类访问限定符限制

4:一个函数可以是多个类的友元函数

5:友元函数的调用与普通函数的调用原理相同

友元类:
概念:
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

友元关系是单向的,不具有交换性。( 比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time 类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。)

友元关系不能传递 如果B是A的友元,C是B的友元,则不能说明C时A的友元。

class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成
//员变量
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}

private:
int _hour;
int _minute;
int _second;
};

class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}

void SetTimeOfDate(int hour, int minute, int second)
{
    // 直接访问时间类私有的成员变量
    _t._hour = hour;
    _t._minute = minute;
    _t._second = second;
}

private:
int _year;
int _month;
int _day;
Time _t;
}

内部类
概念:
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外 部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中 的所有成员。但是外部类不是内部类的友元。

简单的来说就是类里面嵌套类;

class A
{
public:
A(int a)
:_a(a)
{}
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << a._a << endl;
}
};
private:
int _a;
};
int main()
{
A::B b;
b.foo(A(1));
return 0;
}

到这里就已经完了,c++已经入门了,可以正常使用类了;

目录
相关文章
|
1月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
47 0
|
3月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
115 12
|
4月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
4月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
4月前
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
|
5月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
4月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
101 16
|
5月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
4月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
237 6
|
6月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
151 19