目录
1、构造函数(补充)
构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
class Date { public: Date(int year = 2022, int month = 5, int day = 24) { _year = year; _year = 2023; //第二次赋值 _year = 2024; //第三次赋值 _month = month; _day = day; } private: int _year; int _month; int _day; };
既然构造函数体的语句只能称作为赋初值,现在,可否有一种方式进行初始化呢?即初始化列表初始化。
初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date { public: //构造函数: -->初始化列表初始化 Date(int year = 2022, int month = 5, int day = 24) :_year(year) , _month(month) , _day(day) {} private: int _year; int _month; int _day; };
当然,我可以在初始化列表初始化,也可以在大括号内进行赋值:
Date(int year = 2022, int month = 5, int day = 24) :_year(year) , _month(month) { _day = day; }
注意:
1、初始化列表可以认为就是对象成员变量定义的地方
2、每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
3、类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(该类没有默认构造函数)
4、尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
5、成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
- 解释注意点2:
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
Date(int year = 2022, int month = 5, int day = 24) :_year(year) , _year(month) // err {}
- 解释注意点3:
先前我们都知道引用的变量和const变量只能在定义时初始化,而普通的变量在定义时不强求初始化,所以我们就不能按照如下的方式操作:
由此可见,我引用成员变量和const成员变量必须在初始化列表进行初始化:
class Date { public: //初始化列表可以认为就是对象成员变量定义的地方 Date(int year, int n, int ref) :_n(n) , _ref(ref) { _year = year; } private: //定义时不强求初始化,后面可再赋值修改 int _year; //声明 //只能在定义时初始化 const int _n; int& _ref; };
自定义类型成员(该类没有默认构造函数)同样也得在初始化列表进行初始化:
class A { public: A(int x) //非默认构造函数,因为要主动传参 :_x(x) {} private: int _x; }; class Date { public: //在初始化列表对自定义类型 _aa 进行初始化 Date(int a) :_aa(a) {} private: A _aa; };
注意这里的条件,一定得是没有默认构造函数的自定义类型成员才得在初始化列表进行初始化,而默认构造函数简单来说就是不需要传参的函数,这里简单回顾下:
- 我们没写编译器默认生成的构造函数
- 无参构造函数
- 全缺省构造函数
补充:如果自定义类型成员有默认构造,还需要在初始化列表里写吗?
没必要,因为编译器会根据你定义成员变量的顺序,当走到自定义类型成员时自动调用其默认构造函数,如若你在初始化列表里对自定义类型初始化了,那么编译器会拿这个值作为形参传到其自定义类型的默认构造函数,继而输出的值为初始化列表里的值
对于自定义类型,如若我不再初始化列表里初始化,我还想改变其值,只能这样做:
综上。这三类成员必须在初始化列表里初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(该类没有默认构造函数)
而其它变量既可以在初始化列表初始化,也可以在函数体内初始化,不过建议尽量都在初始化列表里初始化。
Date(int year, int n, int ref, int a) :_year(year) , _n(n) , _ref(ref) , _aa(a) {}
补充:先前我们对内置类型成员给的缺省值其实是给初始化列表当备胎的。
- 解释注意点5:
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
看下面这道题:
class A { public: A(int a) :_a1(a) , _a2(_a1) {} void Print() { cout << _a1 << " " << _a2 << endl; } private: int _a2 = 1; int _a1 = 2; }; int main() { A aa(1); aa.Print(); }
A、输出1 1 B、程序崩溃 C、编译不通过 D、1 随机值
答案:D
解析:
- 注意我成员变量在类中声明次序就是其在初始化列表中的初始化顺序,既然_a2先声明,则必然进入初始化列表要先执行, _a2(_a1) 。意思是说拿_a1去初始化_a2,不过此时的_a1还是随机值,自然_a2即为随机值,随后执行:_a1(a)。拿a初始化_a1,所以输出的值为1和随机值。
总结1:
总结2:尽量使用初始化列表,但不排除有些情况仅仅使用初始化列表是不够的,还需要进一步检查或处理,则再继续使用函数体,比如malloc数组的操作:
class MyQueue { public: MyQueue() :_pushst(2000) ,_size(1) ,_a((int*)malloc(40)) { if (_a == nullptr) { //... } else { for (size_t i = 0; i < 10; i++) _a[i] = i; } } private: //C++11声明给缺省值 Stack _pushst = 1000; Stack _popst = 1000; int _size = 0; const int i = 1; int& ref = _size; int* _a = (int*)malloc(20); };
建议:
- 全部使用初始化列表显示初始化
- 全部使用缺省值
- 尽量不要上面二者混着用
2、explicit关键字
- C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
- 构造函数前面加explicit就不再支持隐式类型转换。
- 类类型的对象之间也可以隐式转换,需要相应的构造函数支持
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用。
class Date { public: Date(int year) :_year(year) {} private: int _year; }; int main() { Date d1(2022); //构造 //隐式类型转换 Date d2 = 2022; //构造 + 拷贝构造 --》 优化成构造 } //Date d3(d1); //拷贝构造 //Date d4 = d1;//拷贝构造
这里我 Date d2 = 2022; 这段代码就利用到了C++的隐式类型转换,中间会产生一个临时对象,其本质是先拿2022构造一个Date类型的对象,再拿这个对象去拷贝构造给d2。可是编译器会对其直接进行优化,将整个过程直接就是构造。
C++为了避免隐式类型的转换,特地新增一个关键字:explicit,加上了这个关键字它就避免隐式类型转换的发生。此时再去编译,就会报错了
虽然加上explicit后不支持隐式类型转换,但是依然支持显示类型转换,即强转:
class Date { public: //加上explicit后不支持隐式类型转换 explicit Date(int year) :_year(year) {} private: int _year; }; int main() { Date d1(2022); //构造 //显式类型转换 Date d2 = (Date)2022; }
- 补充:
在上述原有代码的基础上,我们可否这样加上引用?
class Date { public: Date(int year) :_year(year) {} private: int _year; }; int main() { Date d1(2022); Date& d4 = d1; const Date& d5 = 2022; }
- 我d4引用d1是没问题的,不过我d5要想引用2022必须加上const,首先我d5引用的是2022的临时变量,而临时变量具有常性(只读),加上const保持权限不变。
注:多参数的类型转换调用方式如下:
class A { public: //构造函数explicit不再支持隐式类型转换 //explicit A(int a1) A(int a1) :_a1(a1) {} //explicit A(int a1, int a2) A(int a1, int a2) :_a1(a1) ,_a2(a2) {} A(const A& aa) :_a1(aa._a1) ,_a2(aa._a2) {} void Print() { cout << _a1 << " " << _a2 << endl; } int Get() const { return _a1 + _a2; } private: int _a1 = 1; int _a2 = 2; }; int main() { A aa1(1); A aa2(1, 1); //单参数 A aa3 = 1; const A& aa4 = 1; //多参数 A aa5 = { 2, 2 }; const A& aa6 = { 2, 2 }; return 0; }
隐式类型转换的作用:
- 假设我有一个自定义类型的栈,同样我也有一个A类,现在想调用栈的push接口对A的对象进行push,以往我们只能这样进行:
class Stack { public: void push(const A& aa) {} }; int main() { Stack st; A a1(1); st.push(a1); A a2(2); st.push(a2); return 0; }
有了隐式类型转换后,我们就不需要定义A的对象了,直接利用类型转换进行push会方便很多,如下:
int main() { //以往的方式 Stack st; A a1(1); st.push(a1); A a2(2); st.push(a2); //采用类型转换的方式 st.push(7); st.push({ 8, 8 }); return 0; }
注:类类型的对象之间也可以隐式转换,需要相应的构造函数支持
class A { public: //构造函数explicit不再支持隐式类型转换 //explicit A(int a1) A(int a1) :_a1(a1) {} //explicit A(int a1, int a2) A(int a1, int a2) :_a1(a1) ,_a2(a2) {} A(const A& aa) :_a1(aa._a1) ,_a2(aa._a2) {} void Print() { cout << _a1 << " " << _a2 << endl; } int Get() const { return _a1 + _a2; } private: int _a1 = 1; int _a2 = 2; }; class B { public: B(const A& a) :_b(a.Get()) {} private: int _b = 0; }; int main() { A aa1(1); B bb1 = aa1; const B& bb2 = aa1; return 0; }
3、static成员
概念
声明为static的类成员称为类的静态成员,分为如下两类:
- 用static修饰的成员变量,称之为静态成员变量
- 用static修饰的成员函数,称之为静态成员函数
静态的成员变量一定要在类外进行初始化
特性
- 1、静态成员为所有类对象所共享,不属于某个具体的实例
静态成员变量不能在声明位置给缺省值初始化,因为缺省值是给构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表
class A { private: static int _n; int _k; char _a; }; int main() { cout << sizeof(A) << endl; //8 return 0; }
这里的运行结果为8,这里的计算规则是按照C语言那套计算结构体大小的规则。并没有把我静态成员变量_n考虑进去,因为静态成员变量属于整个类,是类的所有对象共享,存储在静态区,所以静态变量成员不计入总大小。
- 2、静态成员变量必须在类外定义,定义时不添加static关键字
class A { private: //声明 static int _n; static int _k; }; //定义 int A::_n = 0; int A::_k = 0;
- 3、静态成员函数没有隐藏的this指针,不能访问任何非静态成员
class A { public: static void Func() { cout << ret << endl; // err错误,访问了非静态成员,因为无this指针 cout << _k << endl; //正确 } private: //声明 int ret = 0; static int _k; }; //定义 int A::_k = 0;
- 4、访问静态成员变量的方式
当静态成员变量为公有时,可有如下三种进行访问:
- 通过对象.静态成员来访问
- 通过类名::静态成员来行访问
- 通过匿名对象突破类域进行访问
class A { public: static int _k; }; int A::_k = 0; int main() { A a; cout << a._k << endl; //通过对象.静态成员来访问 cout << A::_k << endl; //通过类名::静态成员来行访问 cout << A()._k << endl;//通过匿名对象突破类域进行访问 return 0; }
当静态成员变量变成私有时,可采用如下方式:
- 通过对象.静态成员函数来访问
- 通过类名::静态成员函数来访问
- 通过匿名对象调用成员函数进行访问
class A { public: static int GetK() { return _k; } private: static int _k; }; int A::_k = 0; int main() { A a; cout << a.GetK() << endl; //通过对象.静态成员函数来访问 cout << A::GetK() << endl;//通过类名::静态成员函数来行访问 cout << A().GetK() << endl; //通过匿名对象调用成员函数进行访问 return 0; }
- 5、静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值
来看两个问题:
- 1、静态成员函数可以调用非静态成员函数吗?
答案:不可以,因为静态成员函数是没有this指针的,无法调用非静态成员函数。
- 2、非静态成员函数可以调用类的静态成员函数吗?
答案:可以,因为静态成员为所有类对象所共享,不受访问限制
搞清楚了static的特性,来看一道面试题:
面试题:实现一个类,计算中程序中创建出了多少个类对象。
- 思路:
假设命名该类为A,那么A类型的对象一定是经过构造函数或拷贝构造的,那么我们就可以分别定义两个静态成员变量,在构造函数和拷贝构造里++变量,这样,每创建一次对象,变量就++一次,自然就好求了。如下:
class A { public: A() { ++_count1; } A(const A& aa) { ++_count2; } static int GetCount1() { return _count1; } static int GetCount2() { return _count2; } private: static int _count1; static int _count2; }; int A::_count1 = 0; int A::_count2 = 0; A Func(A a) { A copy(a); return copy; } int main() { A a1; A a2 = Func(a1); cout << a1.GetCount1() << endl; // 1 cout << a2.GetCount2() << endl; // 3 cout << A::GetCount1() + A::GetCount2() << endl; // 4 }
- 补充:这里用全局变量(count1和count2)也是可以的,但不推荐。在简单的程序里可以使用没问题,但是在项目中不推荐用全局的,因为可能会出现链接冲突的问题,还是用静态成员变量为优。
通过静态成员变量我们可以实现一个计算从1累加到指定整数n的功能:
通过类的构造函数逐步累加数值,最终获取累加结果,代码如下:
class Sum { public: Sum() { _ret += _i; _i++; } static int GetSum() { return _ret; } private: static int _i; static int _ret; }; int Sum::_i = 1; int Sum::_ret = 0; class Solution { public: int Sum_Solution(int n) { //Sum a[n];变长数组 Sum* a = new Sum[n];//动态开辟 delete[] a; return Sum::GetSum(); } };
总结:静态成员函数和非静态成员函数的区别:
1、调用方式:
- 静态成员函数:属于类本身,可直接通过类名加作用域运算符 :: 调用,如 ClassName::StaticFunction() ;也能通过对象名调用,但本质仍是调用类的函数 ,如 obj.StaticFunction() (obj 为 ClassName 类对象 ) 。
- 非静态成员函数:属于类的对象,必须通过类的对象或对象指针来调用 。如 obj.NonStaticFunction() (通过对象调用 ),或 ptr->NonStaticFunction() (ptr 为指向类对象的指针 )。
2、与对象的关系:
- 静态成员函数:不依赖于类的特定对象实例,没有 this 指针。它不区分具体对象,执行的操作通常与整个类相关,而非针对某个对象 。
- 非静态成员函数:依赖于类的特定对象实例,有 this 指针 。this 指针指向调用该函数的对象,函数可通过 this 访问和操作对象的成员变量与其他非静态成员函数 。
3、访问权限:
- 静态成员函数:只能访问类的静态成员变量和静态成员函数 ,不能直接访问非静态成员变量和调用非静态成员函数 。因为在类加载时静态成员函数就存在,此时非静态成员可能还未分配内存 。
- 非静态成员函数:既可以访问类的静态成员变量和静态成员函数,也能访问和操作非静态成员变量和其他非静态成员函数 。
4、内存分配:
- 静态成员函数:在内存中只有一份副本,为类的所有对象共享,不占用对象实例的内存空间 。
- 非静态成员函数:理论上每个对象实例都有一份副本(实际通过优化,函数代码存储在代码段,并非每个对象真的有一份函数代码拷贝 ),占用额外内存空间 。
5、其他特性:
- 静态成员函数:不能被声明为 virtual (虚函数依靠 this 指针访问虚函数表,静态成员函数无 this 指针 )、const 、volatile 。构造函数和析构函数不能是静态成员函数 。
- 非静态成员函数:可以声明为 virtual 实现多态,也能根据需要声明为 const (表示函数不修改对象状态 ) 、volatile 。
4、C++11的成员初始化新玩法
C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值。
class B { public: B(int b = 0) :_b(b) {} int _b; }; class A { public: void Print() { cout << a << endl; cout << b._b << endl; cout << p << endl; } private: // 非静态成员变量,可以在成员声明时给缺省值。 int a = 10; B b = 20; //隐式类型转换 int* p = (int*)malloc(4); static int n; //静态成员变量不能给缺省值 }; int A::n = 10;
5、友元
友元分为:友元函数和友元类
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元函数
说明如下:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰,因为友元没有this指针
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用和原理相同
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。(注意声明的时候编译器会向上寻找)
接下来,就写个自定义类型的<< 和 >>的重载来演示友元函数:
- >> 流提取
- << 流插入
C++里cout和cin是全局的对象包含在<iostream>的,cin是istream类型的对象,cout是ostream类型的对象
C++中,内置类型是直接支持cout流插入<<和cin流提取>>的,并且其可以自动识别类型。其原因是库里面已经把这些内置类型的给重载了:
而自定义类型就不能直接用>>或<<,因此,我们需要手写这两个的运算符重载。
问题:现在我们尝试去重载operator<<,然后发现我们没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以我们要将operator<<重载成全局函数。但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理。
class Date { //友元函数 friend ostream& operator<<(ostream& out, const Date& d);//流插入 << friend istream& operator>>(istream& in, Date& d);//流提取 >> public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; //流插入 << ostream& operator<<(ostream& out, const Date& d) { out << d._year << "-" << d._month << "-" << d._day << endl; return out; } //流提取 >> istream& operator>>(istream& in, Date& d) { in >> d._year >> d._month >> d._day; return in; } int main() { Date d1, d2; cin >> d1 >> d2; cout << d1 << d2; }
加上了友元,我们就可以在类外对类的私有成员进行访问。
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
- 友元关系不能传递。
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
Date类里想访问我Time类里的私有成员,就让我Date变成你Time的友元类,相反我Time类里想访问Date类里的成员,就让我Time变成Date类的友元类,谁想访问谁,就要把谁设为谁的友元。
class Date; // 前置声明 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; };
6、内部类
C++用到内部类的地方不是很多。
概念及特性
概念:
- 如果一个类定义在另一个类的内部,这个内部的类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
- 内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
注意:
- 内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
- sizeof(外部类) = 外部类,和内部类没有任何关系。
class A { private: static int k; int h = 0; public: class B //B天生就是A的友元 { public: void foo(const A& a) { cout << k << endl;//OK cout << a.h << endl;//OK } private: int _b; }; }; int A::k = 10; // A的静态成员k定义 int main() { A aa; cout << sizeof(A) << endl; // 4 sizeof(外部类) = 外部类,和内部类没有任何关系。 A::B bb; // bb.foo(aa); }
如上,我在A的类里定义了一个类B,则B天生就是A的友元,所以在B里可以直接访问A的私有成员变量,而A不是B的友元,这里只是单向,A类不能访问B的成员变量
- 注:当类B是类A的private私有时,A::B bb;就会报错,因为内部类B受外部类A类域限制和访问限定符限制
由此我们可以将上文实现的计算从1累加到指定整数n的功能设计成内部类的形式,如下:
class Solution { public: class Sum { public: Sum() { _ret += _n; _n++; } }; int Sum_Solution(int n) { Sum* ptr = new Sum[n]; return _ret; } private: static int _n; static int _ret; }; int Solution::_n = 1; int Solution::_ret = 0;
7、匿名对象
- 用 类型(实参) 定义出来的对象叫做匿名对象,相比之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象
- 匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象
class A { public: A(int a = 0) :_a(a) { cout << "A(int a)" << endl; } ~A() { cout << "~A()" << endl; } private: int _a; }; int main() { A aa1; A aa2(2); // 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义 // A aa1(); // 但是我们可以这么定义匿名对象,匿名对象的特点不⽤取名字, // 但是他的⽣命周期只有这⼀⾏,我们可以看到下⼀⾏他就会⾃动调⽤析构函数 A(); A(1); return 0; }
我们可以引用有名对象,不可以直接引用匿名对象,但可以用如下的方式引用匿名对象,有两点需要注意:
- 匿名对象和临时对象有一样的效果(不能修改),因此要加上const才能正确引用
- const引用会延长匿名对象的生命周期,匿名对象跟着引用走
class A { //... }; int main() { A aa1; // 匿名对象的⽣命周期只有这⼀⾏,我们可以看到下⼀⾏他就会⾃动调⽤析构函数 A(); A& ref1 = aa1; // const引用会延长匿名对象的生命周期,匿名对象跟着引用走 const A& ref2 = A(); return 0; }
- 如上当ref2销毁时,匿名对象A()才会销毁
匿名对象的使用场景:
- 使用匿名对象调用成员函数
- 自定义类型做参数时,匿名对象做缺省参数
解释使用场景1:我们可以使用有名对象调用成员函数,也可以使用匿名对象调用成员函数:
class Solution { public: int Sum_Solution(int n) { //... return n; } }; int main() { //有名对象调用 Solution s1; s1.Sum_Solution(100); //匿名对象调用 Solution().Sum_Solution(100); return 0; }
解释使用场景2:对于函数的缺省参数,如果形参是整形,我们给出的是一个常量,如果形参是自定义类型,我们给出的缺省值是一个匿名对象,自定义类型一般建议传引用传参,并且要用const修饰,因为匿名对象也是具有常性,否则权限放大
void func1(int i = 0) { } void func2(const A& aa = A()) { }
8、对象拷贝时的编译器优化
- 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝。
- 如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化。
- linux下可以将下面代码拷贝到test.cpp文件,编译时用 g++ test.cpp -fno-elide-constructors 的方式关闭构造相关的优化。
编译器优化将在如下几个场景出现:
- 类型转换
- 传值传参
- 传值返回
类型转换
我们之前已经了解过一点编译器优化,如下的类型转换即会发生编译器优化:
class A { public: A(int a = 0) :_a1(a) { cout << "A(int a)" << endl; } A(const A& aa) :_a1(aa._a1) { cout << "A(const A& aa" << endl; } A& operator=(const A& aa) { cout << "A& operator=(const A& aa)" << endl; if (this != &aa) _a1 = aa._a1; return *this; } ~A() { cout << "~A()" << endl; } private: int _a1 = 1; }; int main() { // 构造 + 拷贝 -》优化为直接构造 A aa1 = 1; // 直接构造 A aa2(1); return 0; }
中间会产生一个临时对象,其本质是先拿1构造一个A类型的对象,再拿这个对象去拷贝构造给aa1。可是编译器会对其直接进行优化,将整个过程直接就是构造。现在我们在linux环境下演示编译器优化前后的区别:
关闭优化后不仅体现了A aa1 = 1;是构造+拷贝构造的过程,随后调用的析构函数还帮助我们证明了中间产生的临时对象,拷贝构造完后析构
传值传参
对于如下的场景编译器不会进行优化,因为我对象aa1已经创建出来了,后续再调用f1函数,就是一个普通的构造+拷贝构造函数
void f1(A aa) { //... } int main() { A aa1(1); f1(aa1); return 0; }
当我按照如下的传参方式:
void f1(A aa) { //... } int main() { f1(1); f1(A(1)); return 0; }
对于f1(1),本质上是拿1先去构造一个临时对象,再拿这个临时对象拷贝构造给aa,此时编译器直接优化为构造,对于f1(A(1))也是如此,构造+拷贝构造优化为构造。我们同样是在linux下演示优化前和优化后的区别:
传值返回
- 不优化的情况下传值返回,编译器会生成一个拷贝返回对象的临时对象作为函数调用表达式的返回值
- 一些编译器会优化得更厉害,将构造的局部对象和拷贝构造的临时对象优化为直接构造
如下的f2函数即传值返回
A f2() { A aa; return aa; } int main() { f2(); return 0; }
此段代码本意是先调用构造函数定义了一个aa的对象,最后传值返回aa,此时aa会拷贝构造一个临时对象,再拿这个临时对象做返回。有些编译器就会把这种构造+拷贝构造行为直接优化成构造,我们在linux环境下做演示:
再看如下的代码:
A f2() { A aa; return aa; } int main() { A aa2 = f2(); return 0; }
针对此段代码,不同的编译器会有如下3种优化情况:
- 不优化:调用f2()函数时,本质先调用构造生成aa对象,返回时用aa拷贝构造一个临时对象,最后aa2接收时是拿这个临时对象再拷贝构造给aa2
- 优化一代:此时编译器会进行一定的优化成一次的构造+拷贝构造,相当于是把中间生成的临时对象给省掉了
- 优化二代:合三位一,我直接不产生aa,直接产生aa2,aa即aa2的别名,搞了个类似引用的东西,此时也不需要临时对象,也不需要拷贝构造,直接优化成构造,这里构造的是aa2
针对优化二代,我们分别打印下f2函数里aa和main函数里aa2的地址,会发现是一样的,间接证明了aa即aa2的引用
我们的有些行为会干扰编译器的优化:
A f2() { A aa; return aa; } int main() { A aa2; aa2 = f2(); return 0; }
首先,main函数种定义aa2对象调用构造,随后调用f2函数,定义aa对象调用构造,返回时调用拷贝构造给临时对象,出了f2作用域后调用析构,aa被析构,随后再拿这个临时对象赋值给aa2,赋值完成后,临时对象的生命周期结束,调用第二次析构,随后main函数执行结束,调用第三次析构,aa2被析构。当然这里有的编译器也会进行强制优化(把中间的临时对象给干掉了),我们在linux环境下进行演示:
9、再次理解封装
C++是基于面向对象的程序,面向对象有三大特性即:封装、继承、多态。
C++通过类,将一个对象的属性与行为结合在一起,使其更符合人们对于一件事物的认知,将属于该对象的所有东西打包在一起;通过访问限定符选择性的将其部分功能开放出来与其他对象进行交互,而对于对象内部的一些实现细节,外部用户不需要知道,知道了有些情况下也没用,反而增加了使用或者维护的难度,让整个事情复杂化。
下面举个例子来让大家更好的理解封装性带来的好处,比如:乘火车出行
我们来看下火车站:
- 售票系统:负责售票----用户凭票进入,对号入座
- 工作人员:售票、咨询、安检、保全、卫生等
- 火车:带用户到目的地
火车站中所有工作人员配合起来,才能让大家坐车有条不紊的进行,不需要知道火车的构造,票务系统是如何操作的,只要能正常方便的应用即可。
想想下,如果是没有任何管理的开放性站台呢?火车站没有围墙,站内火车管理调度也是随意,乘车也没有规矩,比如:
类比C++的封装,其本质就是一种更为严格的管理。
10、再次理解面向对象
可以看出面向对象其实是在模拟抽象映射现实世界。