目录
e.g:如何设计一个类,可以查看当前在程序中存在多少个该类的实例
1.访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用
1. public修饰的成员在类外可以直接被访问
2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到 } 即类结束。
5. class的默认访问权限为private,struct为public(因为struct要兼容C)
Q:C++中struct和class的区别是什么?
C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和 class定义类是一样的,c++兼容C struct的用法,同时对struct进行了升级,使用struct也可以定义类:1.结构体名称可以做类型。2.结构体里面可以定义函数
区别是
- 1.class定义的类默认访问权限是 private,struct定义的类默认访问权限是public。(这里指的是对类内成员的访问权限)
- 2.class 继承方式默认是 private 继承,而 struct 继承默认是 public 继承。
- 3.class 可以用在模板参数中(像typename那样),而struct 不能。
2.封装
封装是把客观事物封装成抽象的类,将数据与方法进行结合,隐藏对象的部分属性和实现细节,对外开放一些接口,通过这些接口约束,用户在类外可以合理的访问类内的成员属性。封装本质上是一种更严格的管理,让用户更方便使用类。使用类的人不需要关注类内部的实现细节,使用类提供的接口就行。这实际上就是一种低耦合的体现,使用代码的人和设计代码的人之间的关联程度降低了。
3.类的作用域
类的所有成员都在类的作用域中。在类体外定义成员(就是在类里面声明,在类外面定义)时,需要使用 :: 作用域操作符指明成员属于哪个类域
class Person {
public:
void Print();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定Print是属于Person这个类域
void Person::Print() {
cout << _name << " "<< _gender << " " << _age << endl;
}
4.类对象模型
(1)对象存储方式
每个对象只保存成员变量,成员函数指针(指针)存放在公共的代码段保存
不同的对象,成员变量是不同的,但是成员函数是完全相同的,都是执行的一模一样的函数代码,此时成员函数就不需要存放在对象中,而应该存放在公共代码区。
当对象调用成员函数的时候,是去公共代码区找的,而不是去存放对象的空间里面找。
(2)对象存储大小计算
先看一段代码,和结果
class a1 {
};
class a2 {
public:
void f() {}
};
class a3 {
public:
void f() {}
void f2() {}
private:
int a;
char b;
};
int main() {
a1 a_1;
a2 a_2;
a3 a_3;
cout << "a_1:" << sizeof(a_1) << " byte" << endl;
cout << "a_2:" << sizeof(a_2) << " byte" << endl;
cout << "a_3:" << sizeof(a_3) << " byte" << endl;
}
- 对于a_1对象,没有成员变量的类对象,编译会给他们分配1byte占位,表示对象存在过,不存放实际内容。
- 对于a_2对象同理,也是分配1byte占位,这里也证明了上面所说的,成员函数没有存放在对象中。
- 对于a_3对象同理,计算方式和结构体对齐的方式相同。
这里补充一下结构体对齐的相关知识:
结构体对齐规则
1. 第一个成员在与结构体偏移量为0的地址处。
2. 其他成员变量要对齐到有效对齐数的整数倍的地址处。
注意:有效对齐数 = 编译器默认的一个对齐数 与 该成员所占字节数的较小值。
64位操作系统默认的对齐数为8,32位的为4
3. 结构体总大小为:最大对齐数(所有变量类型所占字节数最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数(该结构体内所有变量类型所占字节数最大者与默认对齐参数取最小)的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
其中编译器默认的对齐数是可以通过#pragma pack (n)来指定对齐数
举例:
struct C{
short a;
double b;
int c;
}C;
printf("%d\n",sizeof(C)); //24
首先第一个变量a放在结构体偏移量为0的地址处,长度是2,存放在0-1的位置
然后看第二个变量b,长度是8,默认对齐参数是8,所以最终对齐就是8,对齐到8*1的位置,存放在8-15的位置
然后看第三个变量c,长度是4,默认对齐参数是8,所以最终对齐数就是4,对齐到4*4的位置,存放在16-20的位置
最后整体对齐,结构体内变量的最大的类型是8,默认对齐参数是8,最终对齐数是8,所以整个结构体的大小就是24
5.this指针
(1).既然调用同一个函数,如何区分不同对象
Date里面有Print函数
void Print() {
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
Date d1, d2;
d1.Init(2022,10,21);
d1.Init(2022,10,22);
d1.Print();
d2.Print();
既然函数都是调用同一个,编译器怎么知道应该使用哪个对象中存放的值呢?
实际上编译器自己给我们在函数上加上了一个指针参数,把当前对象通过这个this指针传过去了。
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
实际上this指针是用const修饰的,是一个指针常量,const修饰的是指针,this指针无法改变,但是this指针指向的值可以改变。
void print(Date* const this);
(2).this指针的特性
1.this指针可以使用,但是不能显示写
2. this指针是* const类型的,不能修改,但是可以被初始化
3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
4.this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
这里看一个问题
1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A {
public:
void Print() {
cout << "Print()" << endl;
}
private:
int _a;
};
int main() {
A* p = nullptr;
p->Print();
return 0;
}
因为上面的第三条,在通过p指针调用成员函数的时候,会隐式的将对象地址作为实参传递给this形参,这里会把p传过去,因为p是对象的地址,只是这里对象的地址是nullptr。
show函数并不是存放在对象里面的,对象里面值保存有成员属性,所以访问show函数的时候并没有解引用,没有通过nullptr去访问对象,所以不会崩溃,能正常运行。
再看一个相似的问题
2.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A {
public:
void PrintA() {
cout << _a << endl;
}
private:
int _a;
};
int main() {
A* p = nullptr;
p->PrintA();
return 0;
}
如果p->a去访问对象里面的成员变量,那程序就会崩溃,因为成员a是存放在对象中的,此时调用会发生解引用,通过nullptr访问对象,自然会崩溃。
this指针存放在哪里
this指针是作为形参传递的,所以是存放在栈里面的。
有些编译器会使用寄存器优化,将this指针存放到寄存器里面。
6.六大默认构造函数
(1)构造函数
1).概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用。构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用
2).特性
1. 函数名与类名相同。
2. 没有返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数(只要定义了就不会自动生成了),一旦用户显式定义编译器将不再生成。
6.无参的构造函数和全缺省的构造函数都称为默认构造函数(可以不用传参的构造函数就是默认构造参数),并且默认构造函数只能有一个(你定义一个无参的,再定义一个全缺省的,能通过编译,但是在调用的时候,不传入参数的情况下就会出现冲突)。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数(默认就是指不用传参就可以调用的构造函数)。
7.构造函数的主要任务并不是开空间创建对象,而是初始化对象。
Q.什么是默认构造函数?自动生成默认构造函数有什么用?
默认构造参数(不用传参就可以调用的构造函数就是默认构造函数)有三种:
- 我们不写,编译器默认生成的构造函数
- 我们写的无参构造函数
- 我们写的全缺省构造函数(全缺省最常用,因为使用的时候既可以传参数也可以不传参数)
C++把类型分成内置类型(基本类型包括指针)和自定义类型。
默认生成构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量才会处理(会调用这个自定义类型成员的默认构造函数)
如果调用了编译器自己生成的默认构造函数,则对内置类型不做处理,对自定义类型会在初始化列表中调用该类型的默认构造函数进行初始化(如果该类型没有默认构造函数,则需要显示地在初始化列表初始化自定义类型,否则编译会报错),如果调用的是自己写的构造函数,则编译器在初始化列表中也会调用该类型的默认构造函数
所以自动生成的默认构造函数的存在还是有必要的,当我们不显示写构造函数的时候,生成默认构造函数的初始化列表会帮助我们默认初始化对象,对内置类型不作处理,对自定义类型会调用自定义类型默认的构造函数
(2)析构函数
1).概念
析构函数(Destructor)也是一种特殊的成员函数,没有返回值,不需要我们显式调用,而是在销毁对象时自动执行,完成对象中资源的清理工作(资源清理并不是笼统的指销毁对象,资源清理指的是如下代码对栈的处理,对指针的空间进行释放,将指针置空,将栈的元素数量置0这种操作)。
~Stack() {
free(_a);
_a = nullptr;
_top = capacity = 0;
}
2).特性
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型(所以自然也就无法重载)。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数(和自动生成的构造函数类似,对内置类型不做处理,自定义类型会去调用它的析构函数)。注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
3). 默认生成的析构函数有什么用?
如果没有自己写的构造函数,会生成默认的构造函数,它对内置类型不做处理,对象生命周期结束的时候,会调用该自定义类型里面默认的构造函数。
如果对象的成员属性中有指针变量,而且在初始化的时候要new一块空间给它的,在这种需要自己直接对资源进行管理的情况下,需要自己重写析构函数对指针指向的空间进行释放,因为指针属于内置类型,而默认生成的析构函数对内置类型是不做处理的。
(3)拷贝构造函数
1).概念
拷贝构造函数:是一种特殊的构造函数,在用对象初始化同一类的对象的时候,编译器会自动调用
2).特性
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用
3.类中如果没有涉及资源申请(没有自己在堆上面new一片连续空间)时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
4.拷贝构造函数典型调用场景:(编译器会自己调用拷贝构造函数来做这些事)
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
3).使用默认生成拷贝构造函数需要注意什么
一般的类,使用默认自动生成拷贝构造就够用了,它对内置类型的成员会完成值拷贝,浅拷贝,对自定义类型的成员,去调用这个成员的拷贝构造。
只有像stack这样类,内部存在内置类型(Mtype*),而且在初始化的时候需要自己new一块空间进行初始化,需要这样自己直接管理资源的类,我们需要自己实现拷贝构造函数完成深拷贝。
默认生成的拷贝构造函数实际上是memcpy按字节拷贝值过去的,属于浅拷贝,所以如果栈里面存放的是内置类型的数组元素,则也不需要自己写拷贝构造,因为数组元素是存放在栈上的,通过memcpy的浅拷贝可以正确实现拷贝。
如果在需要深拷贝的地方使用了默认生成的拷贝构造函数进行了浅拷贝,会出现两个问题:
1.这两个对象的某个指针成员变量会指向同一片内存区域,这两个对象生命周期结束后调用析构函数的时候会进行两次析构函数,会对同一片内存空间进行两次delete操作,这个时候就会报错
2.像栈这样的对象,在插入元素的时候,两个栈共用一块空间,修改数据会互相影响。
(4)赋值运算符重载
1).什么是运算符重载
原来内置类型能够使用的运算符,自定义类型无法使用,运算符重载,就是让原本已经存在的运算符有了新的用法和意义,让自定义类型也能够使用这些运算符。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)——参数列表个数由运算符决定
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数(不能改变内置类型的运算符,写在类里面的,默认有一个this,这里针对的是写在类外面的,至少要有一个类类型参数)类里面的像这样写在类里面的,和内置类型参数进行运算,默认有this
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义(改变含义虽然语法上没错,但是对人的理解和使用会有很大的影响)
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . 注意以上5个运算符不能重载。(点星,域作用限定符,sizeof,三目运算符,对象点)
-
如果operator定义在类外面,则==需要传入两个运算符,并且访问的时候需要考虑对象里面的成员是私有的情况。
-
一般来说,operator定义在类里面,定义在里面的时候,需要少写一个参数,因为有一个是* const this(注意这里左操作数是this) d1==d2实际上是d1.operator(&d1,d2)
2).赋值运算符重载
- 参数类型:const T&,传递引用可以提高传参效率(可以防止原对象被改变)
- 返回值类型:T&,返回引用可以提高返回的效率(因为出了函数作用域以后,对象还在,所以可以用引用返回),有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要符合连续赋值的含义(因为赋值表达式是有返回值的,i=j返回值是i赋值后的值,为了符合i=j=k的写法)
- 赋值运算符只能作为类的成员函数重载( 赋值运算符在类中不显式实现时,编译器会生成一份默认的,此时用户在类外再将赋值运算符重载为全局的,就和编译器生成的默认赋值运算符冲突了,故赋值运算符只能重载成成员函数)
eg.:
右边的才是正确写法,加上返回值,为了符合连续赋值的定义,因为出了函数作用域以后,对象还在,所以可以使用引用返回,提高效率。
但是这里不能加const引用,如果是const那么(i=j)=k的时候会编译报错。注意右边参数内的&是引用,下面if判断里面的&是取地址
3).默认生成的赋值运算符重载
自动默认生成的拷贝构造函数:
1、内置类型的成员会完成值拷贝,浅拷贝。
2、自定义类型的成员,去调用这个成员的拷贝构造
但是如果是用一个对象去初始化另一个对象,即使使用赋值运算符,Date d1=d2,也会识别成d1(d2)这种写法,调用拷贝构造函数。
如果不显示写赋值运算符重载,编译器会为我们自动生成一个,和前面的拷贝构造函数一样,对内置类型会直接赋值,对自定义类型的成员,去调用这个成员的运算符重载。
(5)取地址重载
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容,比如不想让别人拿到真实的地址。
Date* operator&()
{
return this ;
}
(6)const取地址重载
const Date* operator&()const
{
return this ;
}
补充:流提取>>和流插入<<的重载
- cin>> >>是流提取运算符(从缓冲区中提取出来)
- cout<< <<是流插入运算符(将右边的对象插入到输出中)
这两个实际上是等价的。
对于内置类型,可以自己判断类型
cin和cout是全局的对象,本质上是因为库里面给你重载好了,如图:
对于自定义类型,就需要自己写。
第一种方式是将流运算符的重载定义成成员函数,放在类里面,此时外面调用的时候,要将cont放在后面反着调用,因为成员函数最左边的参数默认是this,调用的时候一定是this.operator<<(cout),所以只能把d1放在左边。
class Date
{
public:
void operator<<(std::ostream& out) {
out << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
第二种方式,在类外面定义成全局函数,就可以正常调用了。(注意这里需要使用友元函数)注意这里我们的声明和定义必须分离(即类定义在头文件中,operator<<定义在另外的.cpp文件中)
为什么要定义和声明分离?
但是注意这里的全局函数要是定义在.h头文件里面,会报链接错误。因为.h文件会在多个cpp文件中展开(你在.h头文件里面声明定义,除非你只有一个cpp文件include了这个头文件,只要有多个文件include这个头文件就会报错),所以相当于在多个cpp中都有定义了,通过符号表去找的时候就会链接错误,同个函数有多个链接,所以需要把声明和定义相分离。把声明放在头文件里面,把定义放在别的cpp文件里面。
定义在类里面的函数不会出问题,因为类里面的函数都是内联函数,就算有的函数不展开,它的属性也是inline类型的,不会存到符号表中,链接的时候不会报错。因为也没必要放到符号表中,在.h里面定义了,然后被include到cpp文件中,就算由于函数太长inline函数不展开,此时函数的地址就知道了,不需要链接的时候去找了。
class Date
{
public:
friend std::ostream& operator<<(std::ostream& out, const Date& d);
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
void operator<<(std::ostream& out, const Date& d) {
out << _year << "-" << month << "-" << day << endl;
}
7.初始化列表
Date(int year = 2022, int month = 10, int day = 21) {
_year = year;
_month = month;
_day = day;
}
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值
1.写法
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。如果是自定义类型,可以调用有参的构造函数
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{
_day = 6; //构造函数内部
}
2.注意事项
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2.初始化列表可以认为就是对象的成员变量定义的地方
类中包含以下成员,必须放在初始化列表位置进行初始化:(下面这些变量必须在定义的时候初始化,就算写在类外面,也必须在定义的时候初始化;而内置类型不需要,而构造函数内的赋初始值并不能称为初始化,初始化列表才可以被认为是变量定义并初始化的地方)
- 引用成员变量
- const成员变量
- 自定义类型成员(且该类没有默认构造函数时)
3.尽量使用构造函数的初始化列表初始化,尽量不要在函数体内初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化,然后再进入构造函数的函数体。
4.构造函数的初始化列表一定会有而且只有一个。
5.c++11内置类型可以在定义的时候初始化,这个给的值叫缺省值,实际上是给初始化列表使用的。如果初始化列表中给了值,则给的缺省值无效。
如下,在private中声明处给的缺省值无效,最后_year,_month,_day被初始化为2022,10,21
class Date
{
public:
Date(int year = 2022, int month = 10, int day = 21)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year = 1; // 年
int _month = 1; // 月
int _day = 1; // 日
};
6.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
7.写了拷贝构造函数,编译器也不会生成默认的构造函数了,因为拷贝构造函数也是构造函数。
8.explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
返回值,赋值,传参,甚至计算的时候,都会发生隐式类型转换,凡是能发生隐式类型转换的地方,只要接收方类型有单参数的构造函数,就会自动发生隐式类型转换。
这个d2可以用这种方式初始化
- d1是调用构造函数
- d2是调用构造+拷贝构造函数(进行了隐式类型转换,将int转换成了Date类型,先用2022构造,再进行拷贝构造初始化d2)
Date d1(2022); //构造
Date d2 = 2022; //构造+拷贝构造----->编译器优化 变成只有一次构造
如果我们不想要让第二种语法可以成立,我们就可以在构造函数前面使用explicit关键字。
class Date
{
public:
explicit Date(int year = 2022, int month = 10, int day = 21)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year = 1; // 年
int _month = 1; // 月
int _day = 1; // 日
};
9.静态成员变量
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
特性:
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
2. 静态成员变量必须在类外定义(普通成员不能::这样在类外面定义),定义时不添加static关键字,类中只是声明
3. 类静态成员(函数和变量都可以用)可用 类名::静态成员 或者 对象.静态成员 来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员(因为没有隐藏的this指针)
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
6.静态成员变量不会再初始化列表里面初始化。
7.函数里面的静态,类里面的静态,全局的静态变量生命周期都是全局的,只是前两个能够使用的区域受到限制。
e.g:如何设计一个类,可以查看当前在程序中存在多少个该类的实例
class A
{
public:
A() { ++_count; }
A(const A& t) { ++_count; }
~A() { --_count; }
static int GetACount() { return _count; }
private:
static int _count;
};
int A::_count = 0;
int main()
{
cout << A::GetACount() << endl;
A a1, a2;
cout << A::GetACount() << endl;
}
10.友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
(1).友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
<<,>>运算符重载定义在类内部的话,使用cout输出的时候只能用d1 << cout的方式,因为左操作数一定是this,所以我们要定义在类外,此时就可以借助友元函数。
类内声明:
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
类外定义:
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
用了友元函数相当于突破了访问限定符的限制,就可以通过对象直接访问类内的成员属性了。
特性:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰(成员函数后面加const修饰的是this指针,即const* this)
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用和原理相同,只是突破了类的访问限定(符)
(2).友元类
在类A里面声明一个友元类B(B是A的有元),这样B中的所有成员函数就都是A的友元函数,都可以访问A中的成员。
特性:
1.友元关系是单向的,不具有交换性。
2.友元关系不能传递。
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
11.内部类
tips:成员函数内只要不改变成员变量,建议都加上const(因为const加在this上,加了就不能改变this所指对象的内容了)
1.概念
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
2.特性:
1. 内部类可以定义在外部类的public、protected、private都是可以的
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3. sizeof(外部类)=外部类,和内部类没有任何关系,同时和类内的静态变量也没有关系。
内部类在c++中用的很少,唯一的优势就是内部类是外部类的友元
12.匿名对象
匿名对象,生命周期只有一行,除非使用const引用,可以延长生命周期。
使用场景1:直接调用对象中的某个函数,之后就不需要这个对象了(有时候我们创建一个对象,就是为了调用其中的某个实例方法,调用完就不需要这个对象了,此时就可以使用匿名对象)。
class Zebra {
public:
void print() {
cout << _zebra;
}
private:
int _zebra;
};
int main() {
Zebra z1;
z1.print();
//定义对象,然后调用成员函数
Zebra().print(); //使用匿名对象调用成员函数
}
使用场景2:对象只是为了传参。
void funZ(Zebra zz) {
}
int main() {
Zebra z1;
z1.print();
funZ(z1);//定义对象,然后调用成员函数
funZ(Zebra()); //使用匿名对象调用成员函数
}
感谢阅读!如有意见和想法欢迎评论交流