C++类和对象(下) 目录再探构造函数类型转换static成员友元内部类匿名对象对象拷贝时的编译器优化再探构造函数•之前我们实现构造函数时初始化成员变量主要使用函数体内赋值构造函数初始化还有⼀种方式就是初始化列表初始化列表的使用方式是以⼀个冒号开始接着是⼀个以逗号分隔的数据成员列表每个成员变量后面跟⼀个放在括号中的初始值或表达式。•每个成员变量在初始化列表中只能出现⼀次语法理解上初始化列表可以认为是每个成员变量定义初始化的地⽅。•引⽤成员变量const成员变量没有默认构造的类类型变量必须放在初始化列表位置进行初始化否则会编译报错。•C11⽀持在成员变量声明的位置给缺省值这个缺省值主要是给没有显示在初始化列表初始化的 成员使用的。•尽量使用初始化列表初始化因为那些你不在初始化列表初始化的成员也会⾛初始化列表如果这 个成员在声明位置给了缺省值初始化列表会用这个缺省值初始化。如果你没有给缺省值对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器C并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会调⽤这个成员类型的默认构造函数如果没有默认构造会编译错误。•初始化列表中按照成员变量在类中声明顺序进行初始化跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持⼀致。初始化列表总结无论是否显示写初始化列表每个构造函数都有初始化列表无论是否在初始化列表显示初始化成员变量每个成员变量都要走初始化列表初始化#includeiostream using namespace std; class Time { public: Time(int hour) :_hour(hour) { cout Time() endl; } private: int _hour; }; class Date { public: Date(int x, int year 1, int month 1, int day 1) :_year(year) ,_month(month) ,_day(day) ,_t(12) ,_ref(x) ,_n(1) { // error C2512: Time: 没有合适的默认构造函数可用 // error C2530: Date::_ref: 必须初始化引用 // error C2789: Date::_n: 必须初始化常量限定类型的对象 } void Print() const { cout _year - _month - _day endl; } private: int _year; int _month; int _day; Time _t; // 没有默认构造 int _ref; // 引用 const int _n; // const }; int main() { int i 0; Date d1(i); d1.Print(); return 0; }#includeiostream using namespace std; class Time { public: Time(int hour) :_hour(hour) { cout Time() endl; } private: int _hour; }; class Date { public: Date() :_month(2) { cout Date() endl; } void Print() const { cout _year - _month - _day endl; } private: // 注意这里不是初始化这里给的是缺省值这个缺省值是给初始化列表的 // 如果初始化列表没有显示初始化默认就会用这个缺省值初始化 int _year 1; int _month 1; int _day; Time _t 1; const int _n 1; int* _ptr (int*)malloc(12); }; int main() { Date d1; d1.Print(); return 0; }类型转换•C 支持内置类型隐式类型转换为类类型对象需要有相关内置类型为参数的构造函数。•构造函数前面加explicit就不再支持隐式类型转换。•类类型的对象之间也可以隐式转换需要相应的构造函数支持。#includeiostream using namespace std; 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) {} 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() { // 1构造一个A的临时对象再用这个临时对象拷贝构造aa1 // 编译器遇到连续构造拷贝构造-优化为直接构造 A aa1 1; aa1.Print(); const A aa2 1; // C11之后才支持多参数转化 A aa3 { 2,2 }; // aa3隐式类型转换为b对象 // 原理跟上面类似 B b aa3; const B rb aa3; return 0; }static成员•用static修饰的成员变量称之为静态成员变量静态成员变量⼀定要在类外进行初始化。•静态成员变量为所有类对象所共享不属于某个具体的对象不存在对象中存放在静态区。•用static修饰的成员函数称之为静态成员函数静态成员函数没有this指针。•静态成员函数中可以访问其他的静态成员但是不能访问⾮静态的因为没有this指针。•非静态的成员函数可以访问任意的静态成员变量和静态成员函数。•突破类域就可以访问静态成员可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。•静态成员也是类的成员受public、protected、private 访问限定符的限制。•静态成员变量不能在声明位置给缺省值初始化因为缺省值是个构造函数初始化列表的静态成员变量不属于某个对象不走构造函数初始化列表。#includeiostream 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 main() { cout A::GetACount() endl; A a1, a2; A a3(a1); cout A::GetACount() endl; cout a1.GetACount() endl; // 编译报错: error C2248: A::_scount: 无法访问 private 成员(在“A”类中声明) //cout A::_scount endl; return 0; }友元•友元提供了⼀种突破类访问限定符封装的方式友元分为友元函数和友元类在函数声明或者类声明的前⾯加friend并且把友元声明放到⼀个类的里面。•外部友元函数可访问类的私有和保护成员友元函数仅仅是⼀种声明他不是类的成员函数。•友元函数可以在类定义的任何地方声明不受类访问限定符限制。•⼀个函数可以是多个类的友元函数。•友元类中的成员函数都可以是另⼀个类的友元函数都可以访问另⼀个类中的私有和保护成员。•友元类的关系是单向的不具有交换性比如A类是B类的友元但是B类不是A类的友元。•友元类关系不能传递如果A是B的友元 B是C的友元但是A不是C的友元。•有时提供了便利。但是友元会增加耦合度破坏了封装所以友元不宜多⽤。#includeiostream using namespace std; // 前置声明否则A的友元函数声明编译器不认识B class B; class A { // 友元声明 friend void func(const A aa, const B bb); private: int _a1 1; int _a2 2; }; class B { // 友元声明 friend void func(const A aa, const B bb); private: int _b1 3; int _b2 4; }; void func(const A aa, const B bb) { cout aa._a1 endl; cout bb._b1 endl; } int main() { A aa; B bb; func(aa, bb); return 0; } #includeiostream using namespace std; class A { // 友元声明 friend class B; private: int _a1 1; int _a2 2; }; class B { public: void func1(const A aa) { cout aa._a1 endl; cout _b1 endl; } void func2(const A aa) { cout aa._a2 endl; cout _b2 endl; } private: int _b1 3; int _b2 4; }; int main() { A aa; B bb; bb.func1(aa); bb.func1(aa); return 0; }内部类•如果⼀个类定义在另⼀个类的内部这个内部类就叫做内部类。内部类是⼀个独立的类跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。•内部类默认是外部类的友元类。•内部类本质也是⼀种封装当A类跟B类紧密关联A类实现出来主要就是给B类使用那么可以考虑把A类设计为B的内部类如果放到private/protected位置那么A类就是B类的专属内部类其 他地方都用不了。#includeiostream using namespace std; class A { private: static int _k; int _h 1; public: class B // B默认就是A的友元 { public: void foo(const A a) { cout _k endl; //OK cout a._h endl; //OK } int _b1; }; }; int A::_k 1; int main() { cout sizeof(A) endl; A::B b; A aa; b.foo(aa); return 0; }匿名对象•用类型(实参) 定义出来的对象叫做匿名对象相比之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象•匿名对象生命周期只在当前⼀行⼀般临时定义⼀个对象当前用⼀下即可就可以定义匿名对象。#includeiostream using namespace std; class A { public: A(int a 0) :_a(a) { cout A(int a) endl; } ~A() { cout ~A() endl; } private: int _a; }; class Solution { public: int Sum_Solution(int n) { //... return n; } }; int main() { A aa1; // 不能这么定义对象因为编译器无法识别下面是一个函数声明还是对象定义 //A aa1(); // 但是我们可以这么定义匿名对象匿名对象的特点不取名字 // 但是他的生命周期只有这一行我们可以看到下一行他就会自动调用析构函数 A(); A(1); A aa2(2); // 匿名对象在这样场景下就很好用当然还有一些其他使用场景这个我们以后遇到了再说 Solution().Sum_Solution(10); return 0; }对象拷贝时的编译器优化•现代编译器会为了尽可能提高程序的效率在不影响正确性的情况下会尽可能减少⼀些传参和传返回值的过程中可以省略的拷贝。•如何优化C标准并没有严格规定各个编译器会根据情况自行处理。当前主流的相对新⼀点的编译器对于连续⼀个表达式步骤中的连续拷贝会进⾏合并优化有些更新更激进的编译器还会进⾏跨行跨表达式的合并优化。•linux下可以将下面代码拷贝到test.cpp⽂件编译时⽤ g test.cpp -fno-elide-constructors 的方式关闭构造相关的优化。#includeiostream using namespace std; 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; }; void f1(A aa) {} A f2() { A aa; return aa; } int main() { // 传值传参 // 构造拷贝构造 A aa1; f1(aa1); cout endl; // 隐式类型连续构造拷贝构造-优化为直接构造 f1(1); // 一个表达式中连续构造拷贝构造-优化为一个构造 f1(A(2)); cout endl; cout **************************************** endl; // 传值返回 // 不优化的情况下传值返回编译器会生成一个拷贝返回对象的临时对象作为函数调用表达式的返回值 // 无优化vs2019 debug // 一些编译器会优化得更厉害将构造的局部对象和拷贝构造的临时对象优化为直接构造。(vs2022 debug) f2(); cout endl; // 返回时一个表达式中连续拷贝构造拷贝构造-优化一个拷贝构造。(vs2019 debug) // 一些编译器会优化得更厉害进行跨行合并优化将构造的局部对象aa和拷贝的临时对象和接收返回值对象aa2优化为一个直接构造。(vs2022 debug) A aa2 f2(); cout endl; // 一个表达式中开始构造中间拷贝构造赋值重载-无法优化(vs2019 debug) // 一些编译器会优化得更厉害进行跨行合并优化将构造的局部对象aa和拷贝临时对象合并为一个直接构造(vs2022 debug) aa1 f2(); cout endl; return 0; }