四、类和对象(三) 欢迎阅读这篇文章 目录1、构造函数延伸2、类型转换3、static成员4、友元5、内部类6、匿名对象7、对象拷贝时的编译器优化1、构造函数延伸我们上一篇文章中介绍了构造函数我们在实现构造函数的时候初始化成员变量主要使用函数体赋值构造函数的初始化还有另一种方式——初始化列表初始化列表的使用方式是以一个冒号开始接着是一个以逗号分割的数据成员列表每个成员变量后⾯跟⼀个放在括号中的初始值或表达式。classDate{public:Date(intyear1,intmonth1,intday1):_year(year),_month(month),_day(day){}private:int_year;int_month;int_day;};每个成员变量在初始化列表中只能出现一次在语法上可以将初始化列表理解为每个成员变量定义初始化的地方。引用成员变量、const成员变量、没有默认构造的类类型变量必须放在初始化位置进行初始化。可以理解为这三个定义和初始化要同时进行所以只能在初始化列表定义的同时就初始化了classTime{public://没有默认构造函数Time(inthour):_hour(hour){coutTime()endl;}private:int_hour;};classDate{public:Date(intx,intyear1,intmonth1,intday1):_year(year),_month(month),_day(day),_ref(x),_t(12),_n(1){// error C2512: “Time”: 没有合适的默认构造函数可⽤// error C2530 : “Date::_ref” : 必须初始化引⽤// error C2789 : “Date::_n” : 必须初始化常量限定类型的对象}private://声明int_year;int_month;int_day;//下面这三个可以理解为定义和初始化要同时进行所以只能在初始化列表定义的同时就初始化了int_ref;//引用Time _t;//没有默认构造的类类型变量constint_n;//const成员变量};C11支持在成员变量声明的时候给缺省值这个缺省值主要是给没有显式在初始化列表初始化的成员使用的。尽量使用初始化列表初始化因为那些不在初始化列表初始化的成员如果这个成员声明的时候给了缺省值那么初始化列表就会用这个缺省值初始化。如果声明的时候没有给缺省值而且没有显式在初始化列表初始化的成员对于内置类型成员是否初始化取决于编译器C没有规定。对于自定义类类型成员会调用这个成员类型的默认构造函数如果没有默认构造函数就会报错。classTime{public://没有默认构造函数Time(inthour):_hour(hour){coutTime()endl;}private:int_hour;};classDate{public:Date(intx)//成员声明的时候给了缺省值那么初始化列表就会用这个缺省值初始化。:_ref(x),_t(12){coutDate(int x)endl;}private://声明int_year2026;//给缺省值int_month10;//给缺省值int_day;//没有给缺省值而且没有显式在初始化列表初始化的成员,内置类型成员是否初始化取决于编译器C没有规定。//下面这三个可以理解为定义和初始化要同时进行所以只能在初始化列表定义的同时就初始化了int_ref;//引用——必须在初始化列表中初始化//Time _t;//没有默认构造的类类型变量——没有给缺省值而且没有显式在初始化列表初始化的成员,自定义类型成员会调用它的默认构造。——没有默认构造报错Time _t;//没有默认构造的类类型变量——没有给缺省值但是显式在初始化列表初始化的成员会在初始化列表中初始化。constint_n10;//const成员变量声明给缺省值};intmain(){intx0;Dated1(x);return0;}初始化列表总结⽆论是否显式写初始化列表每个构造函数都有初始化列表⽆论是否在初始化列表显式初始化成员变量每个成员变量都要⾛初始化列表初始化成员变量走初始化列表的步骤如下图初始化列表中按照成员变量在类中声明顺序进⾏初始化跟成员在初始化列表出现的的先后顺序⽆关。建议声明顺序和初始化列表顺序保持⼀致。练习下⾯程序的运⾏结果是什么A. 输出 1 1B. 输出 2 2C. 编译报错D. 输出 1 随机值E. 输出 1 2F. 输出 2 1classA{public:A(inta):_a1(a),_a2(_a1){}voidPrint(){cout_a1 _a2endl;}private:int_a22;int_a12;};intmain(){Aaa(1);aa.Print();}已知初始化列表中按照成员变量在类中声明顺序进⾏初始化那么就会先初始化_a2在初始化列表中显式写了那么就按初始化列表中给的值初始化给了_a1这时_a1还未进行初始化是随机值所以_a2就是随机值。再初始化_a1在初始化列表中显式写了那么就按初始化列表中给的值初始化给了1所以_a1就是1。输出1、随机值。通过上方的练习题我们就可以感受到如果声明顺序和初始化列表顺序不保持⼀致那么就有可能出现随机值。2、类型转换C支持内置类型隐式类型转换为类类型对象需要有相关的以内置类型为参数的构造函数。构造函数前⾯加explicit就不再⽀持隐式类型转换。classA{public://以内置类型为参数的拷贝构造//构造函数前加explicit就不再支持隐式转换//explicit A(int a)A(inta):_a1(a){}A(inta1,inta2):_a1(a1),_a2(a2){}voidPrint(){cout_a1endl;}private:int_a1;int_a2;};classArray{public:voidPushBack(constAa){}};intmain(){//构造Aaa1(2);aa1.Print();//类型转换//语法逻辑上构造用内置类型2进行构造出临时对象拷贝构造优化-构造A aa22;aa2.Print();//引用的是A类型的临时变量要const引用constAaa31;//使用场景Array arr;constAaa42;arr.PushBack(aa4);//传参更方便(内置类型“2”隐式转换成自定义类型A类型的变量传参给PushBack)arr.PushBack(2);//构造Aaa5(2,3);//多个参数的类型转换将内置类型用{}括起来A aa6{1,2};constAaa7{1,2};Array arr2;arr2.PushBack(aa7);arr2.PushBack({2,3});}类类型的对象之间也可以隐式转换需要相应的构造函数⽀持。classA{public://以内置类型为参数的拷贝构造//构造函数前加explicit就不再支持隐式转换//explicit A(int a)A(inta):_a1(a){}A(inta1,inta2):_a1(a1),_a2(a2){}voidPrint(){cout_a1endl;}intGet()const{return_a1_a2;}private:int_a1;int_a2;};classB{public://构造函数——将A类型对象a的a1和a2之和拷贝给B类型——用于支持类与类之间的构造B(constAa):_b(a.Get())//a是const A*类型的所以Get需要是const成员函数{}private:int_b;};intmain(){//构造Aaa1(2);aa1.Print();//用aa1构造出B类型临时对象拷贝给bb1B bb1aa1;constBbb2aa1;}3、static成员用static修饰的成员变量称为静态成员变量静态成员变量一定要在类外进行初始化。静态成员变量为所有类对象所共享不属于某个具体的对象不存在对象中存放在静态区。用static修饰的成员函数称之为静态成员函数静态成员函数没有this指针。静态成员函数中可以访问其他的静态成员但是不能访问⾮静态的因为没有this指针。非静态的成员函数可以访问任意的静态成员变量和静态成员函数。突破类域可以访问静态成员可以通过类名::静态成员或者对象.静态成员来访问静态成员变量和静态成员函数。静态成员也是类的成员受public、protected、private 访问限定符的限制。静态成员变量不能在声明位置给缺省值因为缺省值是给构造函数初始化列表的静态成员变量不属于某个对象不走构造函数初始化列表。classA{public://以内置类型为参数的构造函数//构造函数前加explicit就不再支持隐式转换//explicit A(int a)A(inta):_a1(a){}A(inta1,inta2):_a1(a1),_a2(a2){}voidPrint(){cout_a1endl;}staticintGetcount(){//return _a1 _a2; static修饰的成员函数没有this指针不可以访问非静态成员return_scount;//static静态成员函数可以访问其他静态成员不可以访问非静态成员}intGet()const{//return _scount;returnGetcount();}private:int_a1;int_a2;public:staticint_scount;//声明静态成员变量};intA::_scount2;//静态成员变量要在类外初始化intmain(){Aaa1(1);A aa22;coutA::_scountendl;//突破类域访问静态成员变量coutaa1._scountendl;coutA::Getcount()endl;//突破类域访问静态成员函数coutaa1.Getcount()endl;coutaa1.Get()endl;//非静态成员函数可以访问静态成员和静态成员函数return0;}// 实现一个类计算程序中创建出了多少个类对象classA{public://构造函数A(){_scount;}//拷贝构造A(constAa1){_scount;}//析构~A(){--_scount;}staticintGetACount(){return_scount;}private:staticint_scount;};intA::_scount0;intmain(){coutA::GetACount()endl;A a1,a2;Aa3(a1);coutA::GetACount()endl;couta1.GetACount()endl;return0;}求123…nclasssum{public:sum(){_reti;i;}staticintGetret(){return_ret;}private:staticint_ret;staticinti;};intsum::_ret0;intsum::i1;classSolution{public:intSum_Solution(intn){sum arr[n];//变长数组实例化n次执行n次构造函数构造函数哪进行相加returnsum::Getret();}};4、友元友元提供了一种突破类访问限定符封装的方式友元分为友元函数和友元类。在函数声明或者类声明前加friend并且把友元声明放到⼀个类的⾥⾯。外部友元函数可访问类的私有和保护成员友元函数仅仅是类的一种声明不是类的成员函数。友元函数可以在类定义的任何地⽅声明不受类访问限定符限制。一个函数可以是多个类的友元函数。友元类中的成员函数可以是另一个类的友元函数可以访问另一个类中的保护和私有成员。友元类的关系是单向的不具有交换性比如A是B的友元但是B不是A的友元。友元类的关系不能传递如果A是B的友元 B是C的友元但是A不是C的友元。有时提供了便利。但是友元会增加耦合度破坏了封装所以友元不宜多⽤。5、内部类如果一个类定义在另一个类的内部这个类就是内部类。内部类是一个独立的类跟定义在全局相比他只是受外部类类域限制访问限定符限制所以外部类定义的对象中不包含内部类。内部类默认是外部类的友元类。内部类本质也是⼀种封装当A类跟B类紧密关联A类实现出来主要就是给B类使⽤那么可以考虑把A类设计为B的内部类如果放到private/protected位置那么A类就是B类的专属内部类其他地⽅都⽤不了。//内部类classA{private:staticint_k;int_h1;public:classB{public:voidfoo(constAa){cout_kendl;//OKcouta._hendl;//OK}int_b1;};};intA::_k0;intmain(){coutsizeof(A)endl;A::B b;A aa;b.foo(aa);return0;}求123…n//改造classSolution{public:classsum{public:sum(){_reti;i;}staticint_ret;staticinti;};intSum_Solution(intn){sum arr[n];//变长数组实例化n次执行n次构造函数构造函数哪进行相加returnsum::_ret;}};intsum::_ret0;intsum::i1;6、匿名对象用类型(实参)定义出来的对象叫匿名对象。匿名对象⽣命周期只在当前⼀⾏⼀般临时定义⼀个对象当前⽤⼀下即可就可以定义匿名对象。//匿名对象classA{public:A(inta0):_a(a){coutA(int a)endl;}~A(){cout~A()endl;}private:int_a;};classSolution{public:intSum_Solution(intn){//...returnn;}};voidPushBack(A aa){}intmain(){A aa1;//匿名对象生命周期只有这一行可以观察会进行析构A();A(1);//可以传参数构造Solution so;so.Sum_Solution(1);Solution().Sum_Solution(2);//利用匿名对象可以直接这样使用Aaa3(3);PushBack(aa3);PushBack(A(3));//与上方等价return0;}7、对象拷贝时的编译器优化现代编译器会为了尽可能提⾼程序的效率在不影响正确性的情况下会尽可能减少⼀些传参和传返回值的过程中可以省略的拷⻉。如何优化C标准并没有严格规定各个编译器会根据情况⾃⾏处理。当前主流的相对新⼀点的编译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化有些更新更激进的编译器还会进⾏跨⾏跨表达式的合并优化。在接收返回值的时候尽量用拷贝构造接收返回值而不是赋值重载这样编译器有更大的空间去优化。