【c++笔记】类和对象流食般投喂(下)
声明以下知识相关资料来自比特官网和小编手搓~类和对象下1、再探构造函数2、类型转换3、static成员4、友元5、内部类6、匿名对象7、对象拷贝时的编译器优化1、再探构造函数在之前 ”类和对象流食般投喂中” 这篇文章里面我们已经介绍了关于构造函数的80%的知识点了在此篇文章里面我们投喂一下剩下的20%流食 --- 初始化列表。完成对类实例化出对象时的初始化这个活初始化列表也能干而且他更加的全面所以在 “用户不显示实现构造函数且编译器自动生成的默认构造函数不能满足对象的初始化” 这个场景时我们就会用到初始化列表不过还是建议能写初始化列表就写上能兜底。初始化列表用法特点1、初始化列表的写法是以冒号起手然后跟着一个以逗号间隔的列表插在函数名与函数体之间的位置列表中每个“成员变量”后面跟着一个括号括号里面可以是一个初始值或者一个表达式2、初始化列表本质是类实例化出的对象被初始化定义的地方所以只能初始化一次成员变量只能在列表出现一次3、C规定引用成员变量、const成员变量、没有默认构造的类类型变量都必须在初始化列表初始化否则会编译报错4、C11规定成员变量声明处可以使用缺省值这个设定是给未显示在初始化列表的成员变量使用的他会调用缺省值进行对象的初始化5、初始化列表的初始化顺序是按照成员变量的声明顺序进行初始化的跟初始化列表里面的排序无关但建议列表的成员变量排序和成员变量的声明顺序保持一致初始化列表小总结1、用户是否显示实现初始化列表每个构造函数都有初始化列表毕竟一个对象的创建肯定会有一个定义的地方之前只知道声明在哪现在知道掌管定义的地方就是初始化列表2、成员变量是否显示实现在初始化列表中成员变量的初始化都会调用初始化列表成员变量走初始化列表的逻辑1、显示实现初始化列表2、未显示实现初始化列表2.1、成员变量声明处有缺省值2.2、成员变量声明处无缺省值2.2.1、内置类型成员变量无明确规定2.2.2、自定义类型成员变量调用他自己的默认构造函数2.2.3、引用成员变量/const成员变量/没有默认构造的成员变量编译报错//引用成员变量/const成员变量/没有默认构造的成员变量必须写在初始化列表中/不写但有缺省值 //不满足会报错 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 };class Date { public: private: // 注意这⾥不是初始化这⾥给的是缺省值这个缺省值是给初始化列表的 // 如果初始化列表没有显⽰初始化默认就会⽤这个缺省值初始化 int _year 1; int _month 1; int _day; Time _t 1; const int _n 1; int* _ptr (int*)malloc(12); };来个小题目#includeiostream using namespace std; class A { public: A(int a) // a 1 :_a1(a) // 再用a初始化_a1_a1 1 , _a2(_a1) // 先用_a1初始化_a2但是此时_a1还没有初始化是个随机值 {} void Print() { cout _a1 _a2 endl; } private: int _a2 2; //先初始化_a2 int _a1 2; //再初始化_a1 }; int main() { A aa(1); aa.Print(); }2、类型转换在之前的投喂中我们可以察觉出编译器总是将数据分成内置类型和自定义类型两类来对成员变量进行分析所以两个大类其实是有一定割裂的但是C为了连贯便利支持了内置类型隐式转换成类类型对象不过需要有相关内置类型为参数的构造函数。在构造函数之前加上explicit就不再支持隐式类型转换了。类类型对象之间也支持隐式类型转换同样需要相应的构造函数支持。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) {} 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的临时对象再⽤这个临时对象拷⻉构造aa3 // 编译器遇到连续构造拷⻉构造-优化为直接构造 A aa1 1; aa1.Print(); const A aa2 1; // C11之后才⽀持多参数转化 A aa3 { 2,2 }; // aa3隐式类型转换为b对象 // 原理跟上⾯类似 B b aa3; const B rb aa3; // aa3是A类转换成B类会创建一个临时变量 // 编译器自动帮你做的 //B temp(aa3); // 调用B的转换构造函数生成临时对象temp //把 const 引用绑定到这个临时对象const B rb 就绑定到了temp上 //const 引用绑定临时对象时C 标准有一个特殊规则临时对象的生命周期会被延长到和引用的生命周期一致。 //原本A类的对象转成B类类型存在于一个临时变量里面生命周期只在当前行const引用将其生命延长 return 0; }3、static成员用static修饰的成员变量被称为静态成员变量他的生命周期和全局变量一样同是放在静态区在域的定位中是放在全局域的由此看出静态成员变量是被所有类对象所共享的不属于某一个对象不存在对象中因而静态成员变量的初始化是不走类的构造函数初始化列表的那一套的他的初始化必须是在类之外进行所以声明处就给不了缺省值了。用static修饰的成员函数被称为静态成员函数C规定静态成员函数是没有this指针的因而他只能访问同为静态的静态成员而不能访问非静态的成员因为没有this指针但是非静态的成员函数没有限制静态/非静态都可以访问。全局变量像是正常成年人静态成员变量像是背负了债务的正常成年人受债务限制你想用他你得帮他还债使他摆脱枷锁这里的静态成员也是类的成员所以是受public/protected/private访问限定符限制的用户想使用就要突破类域也就是解除成员的枷锁帮他还债此时还债突破枷锁的方式有类名::静态成员/对象.静态成员。class A { public: private: // 类⾥⾯声明 static int _scount; //不可以给缺省值因为不走类构造函数初始化列表的那一套 }; // 类外⾯初始化 int A::_scount 0;int main() { // 编译报错error C2248: “A::_scount”: ⽆法访问 private 成员(在“A”类中声明) //cout A::_scount endl; return 0; }4、友元友元是一种突破类访问限定符封装的方式细分为友元函数和友元类使用方式是在函数/类的声明前加上一个friend然后放在一个类中的任意位置不受访问限定符约束比较自由这样使用后这个类传递了这个外部函数是我这个类的朋友我邀请他到我内部玩他什么都可以把玩不限制外部友元函数可以访问类中私有和保护的成员。友元函数仅仅是声明写在类里面并不是类的成员函数。一个函数可以是多个类的友元函数友元函数是类在内部喊话“这个外部函数是我的朋友”的行为的代名词也就是说现有三个人我可以是每个人的朋友。友元类类在内部喊话“这个外部类是我的朋友”友元类里面的函数也可以看成是友元函数可以把友元类当成所有友元函数喊话的集合。友元关系都是单向的可以类比单相思11喜欢2222喜不喜欢11得听22承认友元关系也不具有传递性11喜欢2222喜欢33但是11喜不喜欢33是未知的。友元可以提供便利但增加耦合度两个模块的绑定深度破坏封装不宜多用。访问私有成员变量的问题有⼏种⽅法可以解决 1、成员放公有 2、Date提供getxxx函数 3、友元函数 4、重载为成员函数// 前置声明都则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; };class A { // 友元声明 friend class B; private: int _a1 1; int _a2 2; };5、内部类内部类天生自带双向访问权限不用写任何 friend房子管家人家人随便用房子双向互通无限制内部类和外部类是互相全开权限。内部类是把一个独立的类从原本在全局写移到了在一个类的类域里面写只是多了空间壁垒和访问限制所以内部类是不包含在外部类实例化出的对象中的。内部类默认是外部类的友元类。内部类本质也是一种封装当A类和B类的耦合度很高时A类实现出来就是给B类用的可以把A类设计成内部类此时已经有了空间壁垒了还想再突出专属特性可以把内部类写在private/protected的位置。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;6、匿名对象匿名对象的使用方法类型(实参)之前有名对象的使用方法类型 对象(实参)匿名对象和拷贝时创建的临时对象的定位是未命名对象他们的生命周期都只是在当前一行。适用场景一般临时定义一个对象在当前一行使用就可以用匿名对象。class Solution { public: int Sum_Solution(int n) { //... return n; } }; int main() { // 不能这么定义对象因为编译器⽆法识别下⾯是⼀个函数声明还是对象定义 //A aa1(); // 但是我们可以这么定义匿名对象匿名对象的特点不⽤取名字 // 但是他的⽣命周期只有这⼀⾏我们可以看到下⼀⾏他就会⾃动调⽤析构函数 A(); A(1); A aa2(2); // 匿名对象在这样场景下就很好⽤当然还有⼀些其他使⽤场景这个我们以后遇到了再说 Solution().Sum_Solution(10); return 0; }7、对象拷贝时的编译器优化现代编译器为了追求效率会在确保正确性的前提下对于传值/返回值的拷贝会进行省略。编译器的不同版本的不同优化的程度也是不同的VS2019的debug相对温和VS2019的realse有点激进而VS2022的debug直接用的就是VS2019realse那版了。主流编译器对于连续的拷贝会进行合并优化更“激进”的编译器还支持跨行跨表达式的合并优化