C++(类与对象)是纸老虎吗?
要努力,但不要着急,繁花锦簇,硕果累累都需要过程!
目录
1.面向过程和面向对象的初步认识
2.类的引入
3.类的定义
4.类的访问限定符及封装
4.1类的访问限定符:
4.2封装
5.类的实例化:
6.类对象模型
6.1如何计算类对象的大小?
6.1.1类对象大小计算特例:
6.2结构体内存对齐规则:
7.this指针
7.1this指针的引出:
7.2this指针的特性:
7.3关于this指针的面试题:
8.类的6个默认成员函数
8.1构造函数:
8.1.1概念:
8.1.2特性:
8.2析构函数:
8.1.1概念:
8.1.2特性:
8.2拷贝构造函数:
8.2.1概念:
8.2.2特性:
9.赋值运算符重载
9.1运算符重载:
9.2赋值重载
9.2.1概念
10.日期类的实现
11.流提取和流插入运算符
12.const成员:
13.取地址和const取地址重载
14.再谈构造函数
14.1构造函数体赋值
14.2初始化列表
14.3explicit关键字
15.static成员
15.1stati成员概念:
15.2static成员特性总结:
16.内部类
16.匿名对象
17.拷贝对象时编译器的优化
1.面向过程和面向对象的初步认识
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用解决问题。
C++语言是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成
通过洗衣服举例:
C语言:
C++语言:
2.类的引入
概念:C++语言中将面向对象这个过程实例化为类
类是通过结构体实现的:
在C语言中结构体中只能定义变量,在C++语言中结构体中不仅能定义变量,还能定义函数
例:实现栈结构
3.类的定义
stuct name
{
//类体:由成员函数和成员变量组成
};
注:虽然struct也能定义类,但是在C++中更喜欢用class这个关键字来定义类
class name
{
//类体:由成员函数和成员变量组成
};
类的两种定义方式:
1.声明和定义全部放在类体中:
class stack { //成员变量 int* a; int size; int capacity; //成员函数 void Init() {} void push() {} };
2.类的声明放在.h中,成员函数的实现放在.cpp中:
class stack { //成员变量 int* a; int size; int capacity; //成员函数 void Init(); void push(); }; //.cpp中 void stack::Init() {} void stack::push() {}
注:在.cpp中实现函数时需要通过类名加访问限定符(::)访问成员函数(指明成员函数属于哪一个类域)
成员变量命名规则:
// 我们看看这个函数,是不是很僵硬? class Date { public: void Init(int year) { // 这里的year到底是成员变量,还是函数形参? year = year; } private: int year; }; // 所以一般都建议这样 class Date { public: void Init(int year) { _year = year; } private: int _year; }; //在成员变量前加"_"
4.类的访问限定符及封装
4.1类的访问限定符:
【访问限定符说明】1. public 修饰的成员在类外可以直接被访问2. protected和private 修饰的成员在类外不能直接被访问(此处protected和private是类似的)3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止4. 如果后面没有访问限定符,作用域就到 }; 即类结束。5. class的默认访问权限为private,struct为public (因为struct要兼容C)
4.2封装
封装是面向对象的三大特性之一,将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
在C++语言中,对成员变量和成员函数都放在同一个类当中,使用者只需要调用成员函数即可,不能改变类中的数据,因此通过封装的方式实现了对数据更好的管理
5.类的实例化:
用类类型创建对象的过程,称为类的实例化
1.类是对对象进行描述的,限定了类有哪些成员,定义出一个类并没有分配实际的空间来进行存储:
2.一个类可以实例化出多个对象,实例化出的对象占用实际物理空间,存储类成员变量:
#include<iostream> using namespace std; class A { public: int a; int size; }; int main() { A a1; a1.a = 10; a1.size = 20; A a2; a2.a = 20; cout << a1.a << endl; cout << a2.a << endl; return 0; }
注:a1,a2就是实例化出的对象,可以通过类名直接定义
6.类对象模型
6.1如何计算类对象的大小?
class A { public: void func() {} private: char _c; }; int main() { cout << sizeof(A) << endl; return 0; }
在C++语言中同样存在结构体内存对齐,因此你可能会说,函数占四个字节,_c变量占一个字节,存在结构体内存对齐,这个类占8个字节,但真的是这样的吗???
通过运行发现,其实这个类只占1个字节,为什么会这样呢?
原因:C++语言起初设计的时候只保存类成员变量,类成员函数放在公共代码区,是因为每个对象中成员变量是不同的,而类成员函数都是一样的,直接调用就可以,不需要单独保存
避免了不必要的空间浪费
6.1.1类对象大小计算特例:
1.类中仅有成员函数:
class A { public: void func() {} };
2.类中什么都没有——空类:
class A1 { public: };
注:类中仅有成员函数和空类的情况,类的大小只占一个字节,不存储有效数据,只是起到一个占位的的作用,用来标识对象的存在
6.2结构体内存对齐规则:
1. 第一个成员在与结构体偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的对齐数为8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。 4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
面试题:
1. 结构体怎么对齐? 为什么要进行内存对齐?
2. 如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐?
详情链接:结构体内存对齐
7.this指针
7.1this指针的引出:
1.为什么不能用类加访问限定符直接调用函数呢?
2.定义一个Data日期类:
class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1, d2; d1.Init(2022, 1, 11); d2.Init(2022, 1, 12); d1.Print(); d2.Print(); return 0; }
对于上述类,有这样一个问题:Data类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函 数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
为了解决上述两个问题,在C++语言中引入了一个指针叫this指针,是C++中的一个关键字,当调用函数的时候,将对象的地址传过去,然后用this指针接受
同样的道理,对于问题1,如果直接用类调用函数,不传对象的地址,就无法区分,所以编译会报错
7.2this指针的特性:
1. this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
2. 只能在“成员函数”的内部使用
3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给 this形参。所以对象中不存储this指针。
4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传 递,不需要用户传递
5.this指针一般不需要显示的去写,而是由编译器隐含的调用;
7.3关于this指针的面试题:
1. this指针存在哪里?
this指针是成员函数第一个隐含的形参,存在栈帧中,一般会由编译器进行优化,通过ecx寄存器进行传递
2. this指针可以为空吗?
this指针由const修饰进行保护,所以不能为空
// 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找Print()函数不会发生解引用,因为成员函数存放在公共代码区中
所以:传参:Print(p) ==> 接受Print(A* const this) 运行正常
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行 class A { public: void PrintA() { cout<<_a<<endl; } private: int _a; }; int main() { A* p = nullptr; p->PrintA(); return 0; }
将空指针传过去用this指针接受,然后通过this指针解引用访问_a,就形成了对空指针的解引用,所以会导致运行崩溃
8.类的6个默认成员函数
如果一个类中什么成员都没有简称空类,但是真的一个成员都没有吗?其实不然,即使类中什么成员都没有,也会默认生成6成员函数:
默认成员函数:用户并没有自己实现,而是由编译器自动生成
8.1构造函数:
8.1.1概念:
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
class Date { public: //构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1(2022, 9, 29); return 0; }
8.1.2特性:
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
class Stack { public: Stack(int capacity = 4) { _a = (int*)malloc(sizeof(int) * capacity); if (_a == nullptr) { printf("malloc fail\n"); exit(-1); } _top = 0; _capacity = capacity; } void push(int x) { _a[_top++] = x; } private: int* _a; int _top; int _capacity; }; int main() { Stack st; st.push(1); st.push(2); return 0; }
4. 构造函数可以重载。
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Date { public: void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1; d1.Print(); return 0; }
通过上图我们发现d1对象调用了编译器生成的默认构造函数,但是d1对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用?? 这与它的第六个特性有关。
6.C++把类型分为内置类型和自定义类型,如果类中没有显示自定义的构造函数,编译器就会调用自己的默认构造函数,初始化自定义类型的数据,对内置类型的数据不做处理,处理方法是一般在声明的时候的给默认值
class Stack { public: Stack(int capacity = 4) { cout << "Stack(int capacity = 4)" << endl; _a = (int*)malloc(sizeof(int)*capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } _top = 0; _capacity = capacity; } private: int* _a; int _top; int _capacity; }; class MyQueue { public: void push(int x) {} private: Stack _pushST; Stack _popST; }; int main() { MyQueue q; return 0; }
通过图示我们可以发现对与这个MyQueue这个类我们并没有进行初始化,但是这个类中的自定义类型的成员编译器默认调用构造函数进行了初始化
class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year = 2022; int _month = 9; int _day = 24; }; int main() { Date d1; return 0; }
内置类型成员可以在声明的时候给默认值(注:这里给的是缺省值,并不是初始化);
7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
class Date { public: Date (int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date() { _year = 1; _month = 1; _day = 1; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; };
代码中的两个构造函数都可以称为默认构造函数,但是不能同时使用:
注:不传参数就可以调用的构造函数称为默认构造函数
总结:对于构造函数是否需要自己写,可以通过面向需求来决定,如果编译器默认生成的就可以满足我们的需求就可以不用自己写,如果不满足,我们就需要自己去显示的写构造函数
8.2析构函数:
8.1.1概念:
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
8.1.2特性:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统自动调用析构函数。class Stack { public: Stack(int capacity = 4) { _a = (int*)malloc(sizeof(int)*capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } _top = 0; _capacity = capacity; } //析构函数 ~Stack() { cout << "~Stack()" << endl; free(_a); _a = nullptr; _top = _capacity = 0; } private: int* _a; int _top; int _capacity; }; int main() { Stack a1; return 0; }
5.对于内置类型成员,销毁时不需要资源清理,对于自定义类型,编译器会调用默认的析构函数
class A { public: ~A() { cout << "调用析构" << endl; } }; class Date { public: Date() { _year = 1; _month = 1; _day = 1; } private: int _year; int _month; int _day; A a1; }; int main() { Date d; return 0; }
总结:对于析构函数,如果类中没有申请资源需要释放时,可以不用自己写析构函数,直接调用
编译器默认的析构函数
8.2拷贝构造函数:
8.2.1概念:
拷贝构造:就是指用一个已经存在的对象创建一个一摸一样的对象
拷贝构造函数:只有一个形参,该形参是本类类型对象的引用(一般用const修饰),在用已存在的类类型创建新对象时由编译器自动调用
8.2.2特性:
1.拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值的方式编译器直接会报错,因为会引发无穷递归调用
class Date { public: Date (int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date(Date d) { _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; }; int main() { //构造 --初始化 Date d1(2022, 9, 24); Date d2(d1); //拷贝构造 -- 拷贝初始化 return 0; }
原因:如果是传值拷贝,形参是实参的一份临时拷贝,又会调用拷贝构造,就会无限递归调用, 因此编译器直接禁止了这个行为,正确的方式应该是用引用,用引用形参是实参的别名,不需要进行拷贝
注:使用引用时一般用const修饰,防止在拷贝的时候将拷贝对象写反了:
3.若显示未定义,则编译器会生成默认的拷贝构造函数,默认的拷贝构造函数是按照字节序的方式进行拷贝,这种拷贝叫浅拷贝:
4.编译器默认生成的拷贝构造函数就可以完成字节序的值拷贝了,是否我们自己就不需要写拷贝构造函数了呢?
class Stack { public: Stack(int capacity = 4) { _a = (int*)malloc(sizeof(int)*capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } _top = 0; _capacity = capacity; } ~Stack() { free(_a); _a = nullptr; _top = _capacity = 0; } private: int* _a; int _top; int _capacity; }; int main() { Stack d1; Stack d2(d1); return 0; }
通过上图我们可以发现调用编译器默认生成的拷贝构造函数就可以完成拷贝,但是在调用析构函数free的时候程序崩溃了,原因是因为,拷贝的时候两个对象指针指向同一块空间,因此同一块空间被free了两次,导致程序崩溃了
解决方案:自己写拷贝构造函数,再申请一块同样大小的一块空间,每个对象各自指向不同的空间,这种拷贝方式称为深拷贝
注:类中如果没有涉及到申请资源时,拷贝构造函数是否写都可以,一旦涉及到资源申请时,则拷贝构造函数一定需要自己写,否则会导致程序崩溃
9.赋值运算符重载
9.1运算符重载:
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)注意:
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this
.* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现例:比较两个日期类:
1.判断两个日期类是否相等:
class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //写在类里面是因为,类成员是私有的,放在外面就访问不到 //==:operator()参数只能有两个,因为类成员函数参数中隐含有一个this指针 //所以不能写成operator(const Date& d1,const Date& d2) bool operator==(const Date& d) { return _year == d._year && _month == d._month && _day == d._day; } private: int _year; int _month; int _day; }; int main() { Date d1(2022, 9, 24); Date d2(2023, 1, 1); cout << (d1 == d2) << endl; //转换成:d1.operator(d2); //也可以显示调用,但一般不这样写: cout << d1.operator==(d2) << endl; return 0; }
9.2赋值重载
9.2.1概念
赋值重载:是默认成员函数,完成对两个已经存在的对象的赋值拷贝
存在的问题:上图的赋值重载无法完成连续赋值
解决方案:利用返回值 :
当我们不去显示的写赋值重载函数的时候,编译器会默认调用自己的函数:
栈类的赋值重载 :
调用编译器默认生成的赋值重载函数:
class Stack { public: Stack(int capacity = 4) { _a = (int*)malloc(sizeof(int) * capacity); if (_a == NULL) { perror("malloc fail"); exit(-1); } _top = 0; _capacity = capacity; } //st2(st1) Stack(const Stack& st) { _a = (int*)malloc(sizeof(int) * st._capacity); if (_a == NULL) { perror("malloc fail"); exit(-1); } memcpy(_a, st._a, sizeof(int) * st._top); _top = st._top; _capacity = st._capacity; } ~Stack() { free(_a); _a = nullptr; _capacity = _top = 0; } void Push(int x) { _a[_top++] = x; } private: int* _a; int _capacity; int _top; };
程序直接崩溃了
原因:
赋值重载前:
赋值重载后:
赋值重载完成之后st1._a也指向了st2._a,不仅st2._a被free了两次造成程序崩溃了,而且原来st1._a指向的空间无法找到,造成了内存泄漏
解决方法:自己显示的去写一个赋值重载函数
注:显示的去写复制重载函数时需要检查是否存在自己给自己赋值的情况
解决方案:
赋值重载特性总结:如果类中没有资源需要进行释放时,可以使用编译器默认生成的赋值重载函数,否则,需要自己去显示的写赋值重载函数
10.日期类的实现
1.比较两个日期是否相等:
2.比较两个日期谁更大:
3.比较日期大于等于:
4.比较日期小于等于:
5.比较日期是否小于:
6.比较日期是否不相等:
8.日期加等上某个天数会变为什么?
获取该年该月的天数
9.日期加上某个天数会变为什么?
10.日期减等上某个天数会变为什么?
11.日期减上某个天数会变为什么?
12. 前置++:
13.后置++:
注:前置++和后置++的区别,后置++在重载的时候加了一个int类型的形参,用来标识,在调用的时候不用传递参数,编译器自动传递
14.前置--:
15.后置--:
注:前置--和后置--与前置++和后置++特性相同
16:如何得到日期之间中间相差的天数
思路:通过从小的日期开始向大的日期自增得到中间相差的天数
11.流提取和流插入运算符
<<:流提取运算符
>>:流插入运算符
在C++语言中可以通过流提取和流插入运算符对内置类型的数据数据进行输入和输出:
那自定义类型该如何进行输入和输出呢?
可以通过运算符重载进行解决,cin和cout的类型:istream和ostream
cout在执行的时候报错
当我们将cout和d1对象调换之后发现运行没有报错,但是这种写法不符合一般的逻辑
但是为什么第一种写法会报错呢?
是因为当我们将成员函数定义类里面之后,函数第一个默认参数是this指针,所以导致错误,因此一般会将流插入和流提取的函数定义在全局,可以很好的控制参数:
此时解决了函数参数的问题,但是内置类型成员定义在类里面默认定义的是私有的,在成员函数外面不能访问,对于这个问题,我们先将私有改为公有,继续测试代码:
运行时发现:运行错误,提示重定义
原因是因为在.h文件中定义全局的函数,两个.cpp文件在编译链接的时候,.h中的函数会进行展开,生成对应的函数地址,在两个.cpp文件的符号表中写入,然后导致重定义
解决方案1:可以使用static修饰
原因:用static修饰全局的函数时,在编译链接的时候,不会生成地址进入符号表,只在当前文件有效,在调用的地方直接进行展开。
解决方案二:声明和定义分离
原因:.h文件中只有声明的时候不会进入符号表
解决方案三:用inline修饰成为内联函数,在调用的时候直接展开,不会进符号表
存在的问题:无法实现链式的流插入
解决方案:带上返回值,流插入和流提取符号的特性是从左往右依次进行
以上我们解决了cout输出自定义类型的代码,存在的问题是,我们将成员函数定义在全局的时候,为了访问私有的内置类型成员,改为了共有,那如何才能既不改变私有的属性又可以访问到呢?
在C++中定义了一个关键字friend,称为友元,在类里面的任何地方声明前用friend关键字修饰,全局函数就可以访问到私有成员了:
自定义类型的cin同上:
用友元在类中修饰函数时该函数称为友元函数,用友元修饰类的时候称为友元类:
class A { //A是B的友元,在B中可以访问A的私有成员 friend class B; public: A(int a = 1, int b = 2) :_a(a) ,_b(b) {} private: int _a; int _b; }; class B { public: //访问A的私有成员 void GetA() { _b1 = _aa._a; _b2 = _aa._b; } private: int _b1; int _b2; A _aa; };
特性:
友元关系是单向的不具有交换性
友元关系不具有传递性
12.const成员:
定义的对象可以用const修饰:
用const修饰完d1之后,去打印d1对象的内容时报错,原因是因为指针的权限被放大了,因为d1在调用的时候将d1的地址传过去用隐含的this指针接受,Date * const this,this不能改变,但是*this可以改变,因此在传参的时候权限被放大了,导致出现错误。
解决方案:在成员函数的后边加上const用来修饰*this
13.取地址和const取地址重载
注:当我们不显示的去写调用的时候,编译器会调用自己的默认成员函数
14.再谈构造函数
14.1构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
14.2初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
1.初始化列表和函数体内初始化可以混着来:
2.每个初始化成员在初始化列表中只能出现一次(只能初始化一次)
3.初始化列表存在的意义:
类中的成员变量是声明,只有定义了对象之后,类中的变量才被定义,当类中的成员变量有const成员变量和引用成员变量的时候,如果没有初始化列表,const成员变量和引用成员变量不能再通过成员函数进行赋值,因为const成员变量和引用成员变量只能在第一次定义的时候初始化:
除了const成员和引用成员变量,对于自定义类型成员没有默认构造函数时也会报错,必须要走初始化列表
class A { public: A(int a) { _a = a; } private: int _a; }; class B { public: private: A _a; };
因此对于以上三种情况,必须要存在初始化列表:
class A { public: A(int a) { _aa = a; } private: int _aa; }; class B { public: B(int a, int b) :_a(a) ,_b(b) ,_aa(30) {} private: const int _a; int& _b; A _aa; }; int main() { B b(10,20); return 0; }
4.初始化列表的特性:
1.初始化列表是每个成员定义初始化的地方
2.每个成员都要走初始化列表,就算不显示在初始化列表写,编译器默认也会走初始化列表
3.如果在初始化列表显示写了就用显示写的初始化
4.如果没有在初始化列表显示初始化
(1)、内置类型,有缺省值用缺省值,没有就用随机值
(2)、自定义类型,调用默认它的默认构造函数,如果没有默认构造就报错
5. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A { public: A(int a) :_a1(a) , _a2(_a1) {} void Print() { cout << _a1 << " " << _a2 << endl; } private: int _a2; int _a1; }; int main() { A aa(1); aa.Print(); }
出现随机值的原因是因为,_a2先声明,因此在初始化列表中先初始化_a2出现了随机值
14.3explicit关键字
1.隐式类型转换:
2.单参数的隐式类型转换:
class Date { public: Date(int year) { _year = year; } private: int _year; }; int main() { //第一种普通构造 Date d1(2022); //第二种构造: Date d2 = 2022; return 0; }
支持第二种构造的原因是因为发生了隐式类型转换:
因此在引用的时候需要加const,因为临时变量具有常性:
3.多参数的隐式类型转换:
class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { // Date d1(2022, 10, 13); Date d2 = { 2022,10,13 };//C++11标准支持 const Date& d3 = { 2022,10,13 }; return 0; }
转换的时候同样也会产生临时变量,和上面一样
对于以上两种隐式类型转换如果不让支持就可以在构造函数的前面用explicit修饰:
15.static成员
15.1stati成员概念:
如何统计一个类创建了多少个对象呢?
方案1:可以定义一个全局的变量在构造和拷贝构造中统计次数,每进行一次说明创建了一个新的对象:
int N = 0; class A { public: A(int a) :_a(a) { N++; } A(const A& aa) :_a(aa._a) { N++; } private: int _a; }; int main() { A aa1(1); A aa2(aa1); cout << N << endl; return 0; }
存在的问题:如果使用全局变量可能会出现许多问题,比如可以在任何地方修改
解决方案二:可以通过在类域中定义一个静态成员变量进行统计
静态成员变量的特点是:声明周期是全局的,作用域是类域的
存在的问题N是私有成员,无法进行访问
解决方法:可以写一个函数来获取N
优化:可以用staict修饰GetN()函数 ,这样可以直接通过类名加访问作用域限定符进行访问
15.2static成员特性总结:
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
16.内部类
概念:一个类定义在一个类的内部,这个内部的类就叫做内部类
内部类的特性:
1.内部类和外部类相当于是两个独立的类
2.B天生就是A的友元,可以在B中直接访问A的私有成员:
3.B对象的创建受A类域的限制:
16.匿名对象
class A { public: A(int a = 0) :_a(a) { cout << "A(int a)" << endl; } ~A() { cout << "~A()" << endl; } private: int _a; }; int main() { //定义普通有名对象: A a1; A a2(10); A a3 = 20; //定义匿名对象: A(10); return 0; }
特点:匿名对象定义后使用一次就被销毁了
作用:可以不用创建对象直接进行调用类中成员函数
17.拷贝对象时编译器的优化
class A { public: A(int a=1) :_a(a) { cout << "A(int a=1)" << endl; } A(const A& aa) :_a(aa._a) { cout << "A(const A& aa)" << endl; } A& operator=(const A& aa) { cout << "A& operator=(const A& a)" << endl; if (this != &aa) { _a = aa._a; } return *this; } ~A() { cout << "~A()" << endl; } private: int _a; };
优化场景1:
优化场景2:
优化场景3:
优化场景4: