前几节内容我们提到了友元static等新的知识本节内容就是对他们进行开展学习同时也是类和对象的收尾工作那开始本节内容的学习一.再探构造函数之前我们实现构造函数时初始化成员变量主要使⽤函数体内赋值构造函数初始化还有⼀种⽅式就是初始化列表初始化列表简单来说构造函数后面就是初始化列表在进入函数大括号之前执行用来初始化成员Date(int y, int m, int d) :_year(y), _month(m), _day(d) { }始化列表的使⽤⽅式是以⼀个冒号开始接着是⼀个以逗号分隔的数据成员列表每个成员变量后⾯跟⼀个放在括号中的初始值或表达式。初始化列表的特点特点一一个成员在初始化列表只能写 1 次Test():a(1),a(5) //会报错a初始化两次 {}初始化列表可以理解为变量诞生时赋值但变量不能诞生两次所以只能出现一次特点二三种成员被强制只能在初始化列表初始化在函数体内赋值会报错const int aconst 变量创建必须赋值之后不能修改int r引用定义时必须绑定实体无法后期赋值自定义类成员没有无参默认构造函数编译器无法自动初始化必须手动进行传参初始化以上三种要是在大括号内进行函数赋值就会立马报错。特点三C11⽀持在成员变量声明的位置给缺省值这个缺省值主要是给没有显⽰在初始化列表初始化的成员使⽤的class Test { int x 10; //声明默认值 public: Test(){} //列表没写xx10 Test():x(20){} //列表写了x优先20默认10不起作用 };初始化列表没手动初始化成员 使用类内默认值列表显式手动初始化 类内缺省值失效。推荐全都用初始化列表原因首先所有成员一定会经过初始化列表阶段对于C当中的两种类型自定义类成员列表没写 就 自动调用默认构造无默认构造编译器直接报错内置类型 (int/double/char)类内给默认值 就 自动用默认值初始化没给默认、列表也不写 就是值随机C 标准不强制初始化另外一个原因不用列表 先默认初始化、再函数体内覆盖赋值效率低还容易出现随机垃圾值因此优先初始化列表。初始化的顺序class Test { int a; //1先声明 int b; //2后声明 public: Test() :b(100), a(10) //列表先写b再a {} }; //实际初始化顺序先 a后 b初始化列表中按照成员变量在类中声明顺序进⾏初始化跟成员在初始化列表出现的的先后顺序⽆关。建议声明顺序和初始化列表顺序保持⼀致。这里最爱挖坑了我第一此也以为按照我声明的顺序。记住各位老铁不要用b(a)这种写法这种写法顺序错乱会拿到随机值规范初始化列表书写顺序 成员声明顺序初始化列表总结⽆论是否显⽰写初始化列表每个构造函数都有初始化列表⽆论是否在初始化列表显⽰初始化成员变量每个成员变量都要⾛初始化列表初始化图解为了加深大家的印象练习一下下⾯程序的运⾏结果是什么DA. 输出 1 1B. 输出 2 2C. 编译报错D. 输出 1 随机值E. 输出 1 2F. 输出 2 1我理解就是三步走1. 类里面声明成员决定初始化先后顺序private: int _a22; //先声明 int _a12; //后声明因为内存空间在这里开辟初始化顺序永远按这个书写先后和初始化列表无关。 没在初始化列表写的成员先用类内 的默认值。2. 冒号初始化列表初始化赋值:_a1(a),_a2(_a1)按照上面声明顺序依次初始化变量列表赋值 类内默认值默认值作废。 你原题 先初始化_a2 (_a1) → _a1 是空随机 → _a2 乱码 再初始化_a1 (1) → _a113. 构造函数大括号 {} 内部覆盖赋值最后执行把前面初始化好的值直接覆盖改掉。方便各位老铁记忆先声明定顺序 --再列表初始化 --最后大括号覆盖赋值二.类型转换C⽀持内置类型隐式类型转换为类类型对象需要有相关内置类型为参数的构造函数。构造函数前⾯加explicit就不再⽀持隐式类型转换。类类型的对象之间也可以隐式转换需要相应的构造函数⽀持。C 隐式类型转换代码演示#includeiostream using namespace std; class A { public: // 无explicit单参数构造支持int隐式转A类类型 A(int a1) :_a1(a1) {} // 无explicitC11支持多参数列表隐式构造 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: // 参数为const A支持A类对象隐式转换为B B(const A a) :_b(a.Get()) {} private: int _b 0; }; int main() { A aa1 1; aa1.Print(); const A aa2 1; A aa3 { 2,2 }; B b aa3; const B rb aa3; return 0; }进入代码分析阶段类中A(int a1): _a1(a1) { }单参构造无 explicit初始化列表把传入值给_a1没初始化_a2使用默认值 2不加 explicit允许 int 隐式转 AA aa1 1合法A(int a1,int a2) :_a1(a1), ._a2(a2) { }双参构造无 explicit两个成员都在初始化列表赋值类内默认值失效C11 支持A aa3{2,2}隐式列表转换void Print()、int Get() const普通成员函数打印数据、计算两数之和const函数不修改成员。int _b 0;私有成员_b默认初始 0。B(const A a) :_b(a.Get()) { }构造参数是const A接收 A 对象调用a.Get()求和赋值给_b。无 explicitA 可以隐式转 BB baa3合法main函数1.A aa1 1;无 explicit 单参构造触发隐式转换int 1 生成A(1)临时对象 ,编译器优化直接构造aa1_a11_a2使用类内缺省值2会输出结果1 2。​2. const A aa2 1;编译器构造临时A(1)临时对象生命周期直到至引用销毁去掉const直接编译报错。3.A aa3 { 2,2 };C11 新标准特性多参数构造无 explicit列表初始化隐式调用A(2,2)_a12_a22。4. B b aa3;B的构造参数是const A触发自定义类隐式转换aa3隐式构造 B 对象_b 224。5. const B rb aa3;aa3 隐式生成 B 临时对象临时对象绑定 const 引用。explicit 禁用隐式转换给构造加 explicitexplicit A(int a1):_a1(a1){} explicit B(const A a):_b(a.Get()){}只能显式构造A aa1(1); A aa3(2,2); B b(aa3);explicit关键字修饰构造函数禁止隐式类型转换只允许显式调用构造不加 explicit编译器自动做隐式转换加 explicit和隐式写法全部报错。总结加了 explicit等号隐式全不行只能括号老老实实构造。注意explicit 只修饰构造函数别的函数不能用只拦截隐式转换不影响括号显式初始化单参数构造默认全部加 explicit避免无意间发生隐形类型转换的bug。三.static成员⽤static修饰的成员变量称之为静态成员变量静态成员变量⼀定要在类外进⾏初始化。静态成员变量为所有类对象所共享不属于某个具体的对象不存在对象中存放在静态区。⽤static修饰的成员函数称之为静态成员函数静态成员函数没有this指针。静态成员函数中可以访问其他的静态成员但是不能访问⾮静态的因为没有this指针。⾮静态的成员函数可以访问任意的静态成员变量和静态成员函数。突破类域就可以访问静态成员可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。静态成员也是类的成员受public、protected、private 访问限定符的限制。静态成员变量不能在声明位置给缺省值初始化因为缺省值是个构造函数初始化列表的静态成员变量不属于某个对象不⾛构造函数初始化列表代码演示#includeiostream using namespace std; class Person { public: static int count; int age 18; static void StaPrint() { cout count endl; //cout age; } void Print() { cout age count endl; StaPrint(); } }; int Person::count 0; int main() { Person::count 10; Person::StaPrint(); Person p1,p2; p1.count; cout p2.count endl; p1.Print(); return 0; }开始一一讲解要点提前要点普通成员每个对象自己独有static 静态成员整个类所有对象共用一份独立存放1、静态成员变量static 修饰成员变量必须类外初始化、全对象共享、放静态区class Person { public: static int count; // 只是声明不开内存告诉编译器有这个变量 int age; }; int Person::count 0; // 类外定义初始化唯一开辟内存的地方int age每个对象单独一份p1.age、p2.age互不干扰在对象内存里static int count全类共用同一个变量内存不在对象里存在程序静态区程序在运行一开始就开好内存不管创建几个对象永远只有一块 count。Person p1,p2; p1.count; coutp2.count;//结果变成1因为共用同一块数据类里写static int count;只是声明没开空间必须在类外面int Person::count0;才算创建变量、分配内存。2、静态成员函数static 修饰函数没有 this 指针普通函数普通成员函数void Show(){}编译器偷偷加参数void Show(Person* this)p1.Show()等价Show(p1)this 存当前对象地址用来找对象里的普通成员。静态函数static void StaShow(){}没有隐藏的 this 指针没有对象地址不依附任何对象存在。没有 this xxx咱们不知道现在是 p1 还是 p2 在调用函数。3.静态成员函数只能访问静态成员不能访问普通成员static void StaShow() { coutcount;//可以静态变量全局共用不用对象 //coutage;//报错age属于某个对象需要this-age否则静态函数没this找不到 }静态函数不知道是哪个对象的 age没有 this 拿不到每个对象独有的普通变量 静态变量全类唯一不用找对象随便用。4.非静态普通成员函数静态、非静态全都能访问void Show() { coutage; //有this访问自己对象的普通成员 coutcount;//静态成员全类共享直接访问 StaShow(); //调用静态函数 }普通函数自带 this 指针既能找到自己对象独有的数据也能找到全类共用的静态数据。5.静态成员两种访问方式类名:: /或者对象.静态不归对象管没有创建对象也能访问写法一Person::count; Person::StaShow();类名::直接穿过类域找静态写法二Person p; p.count; p.StaShow();编译器自动转成类名访问不通过对象内存取值// 没创建任何对象照样能用 coutPerson::count; Person::StaShow();6.静态成员遵守访问限定符 public/protected/private静态还是类的内部成员对它的访问权限规则不变class Person { private: static int count; //私有静态 }; int Person::count0; //main里 Person::count10; 报错私有外部不能访问public外面随便类名::访问private/protected只能类里面的函数访问外部禁止直接访问7.静态成员不能在类内写缺省值初始化C17 前class Person { public: int age 18; //普通成员可以类内缺省 //static int count 0; C17之前报错 };底层原理int age18缺省值是构造函数初始化列表用的创建对象进行构造的时候赋值每个对象新建时初始化自己的 agestatic count不属于对象、不进行构造函数、不跟着对象创建初始化不走初始化列表所以不能用类内缺省赋值只能类外初始化。补充C17 新标准inline static int count0;才允许类内初始化。四.友元定义友元提供了⼀种突破类访问限定符封装的⽅式友元分为友元函数和友元类在函数声明或者类声明的前⾯加friend并且把友元声明放到⼀个类的⾥⾯。通常情况下类外面不能碰private/protected修饰的私有成员。friend友元就是让类主动开门让类外部函数 / 另一个整类有权限访问自己的私有、保护成员。语法规则friend写在类内部用来声明谁是我的友元。外部友元函数可访问类的私有和保护成员友元函数仅仅是⼀种声明他不是类的成员函数。友元函数可以在类定义的任何地⽅声明不受类访问限定符限制。⼀个函数可以是多个类的友元函数。友元类中的成员函数都可以是另⼀个类的友元函数友元函数都可以访问另⼀个类中的私有和保护成员。友元类的关系是单向的不具有交换性⽐如A类是B类的友元但是B类不是A类的友元。友元类关系不能传递如果A是B的友元 B是C的友元但是A不是C的友元。有时提供了便利。但是友元会增加耦合度破坏了封装所以友元不宜多⽤。各位老铁开始讲解友元类一个类 A 可以将另一个类 B声明为自己的友元类方式就是在 A 中写friend class B;结果就是B 的所有成员函数都可以访问 A 的private和protected成员。友元类的三个特点特点一单向性A 是 B 的友元但 B 不一定 是 A 的友元#include iostream using namespace std; class B; // 前面要声明 class A { private: int secretA 10; // A 没有声明 B 为友元 public: void showB(B b); // 声明 }; class B { private: int secretB 20; friend class A; // 记住这是B 主动给 A 友元权限 }; // 结果就是A可以访问B的私有成员 void A::showB(B b) { cout A 访问 B 的私有成员: b.secretB endl; } int main() { A a; B b; a.showB(b); // A 是 B 的友元 // b 无法访问 a 的私有成员因为 B 不是 A 的友元 // cout b.secretA; // 报错 return 0; }大致过程先进行前置声明告诉编译器有 B 这个类A 里面用到 B 传值会开多余的空间浪费内存哈 不报错我们看A 里面没写任何friend可以理解A 没有给 B 开门B 没有办法访问 A 的私有secretA接下来看B的类B 把门打开允许 A 全类所有成员函数访问自己的私有成员 secretBA 的成员函数靠着 B 给的友元权限直接拿到 B 的私有变量secretB20代码正常运行。a.showB(b)A 访问 B 私有没问题B 提前授权 A 了。b.secretAB 想访问 A 的私有不行A 没写 friend class B没给 B 开门。friend class A写在 B 里面 A 是 B 友元A 里无 friend B 不是 A 友元特点二不传递性A 是 B 的友元B 是 C 的友元但 A 不是 C 的友元class C { private: int secretC 100; friend class B; //C只开门给B只有B能访问C私有 }; class B { private: int secretB 200; friend class A; //B只开门给A只有A能访问B私有 public: void showC(C c) { cout B 访问 C: c.secretC endl; //B是C友元 } }; class A { public: void showC(C c) { //cout c.secretC; //报错 cout A 无法直接访问 C 的私有成员 endl; } };大致过程我们看到C 类里写friend class B就是说 C 主动把自身私有成员的访问权限开放给 BB 所有成员函数都能访问 C 的私有变量secretCB 类里写friend class A代表 B 给权限让 A 访问自己的私有成员secretB。从关系上看 A 是 B 的朋友、B 是 C 的朋友但友元权限不会因为中间对象就自动自动传递不一定A和C是朋友哈。简单理解就是C 只给 B 开了家门B 只给 A 开了家门即便 A 能去 B 家里做客、B 能去 C 家里做客C 没给 A 开门A 就不能跟着 B 顺路蹭进 C 家里。小细节函数参数采用引用C c不会拷贝新对象不浪费空间直接操作原本的 C 对象。所以 B 的showC依靠友元权限可以正常访问 C 私有A 没有得到 C 的授权在showC里直接访问secretC编译报错。特点三友元类可以访问私有和保护成员#include iostream #include string using namespace std; class A { private: int a 5; protected: string b 保护数据; friend class B; // A把B设为友元 }; class B { public: void print(A tmp) { cout 私有 tmp.a endl; cout 保护 tmp.b endl; } }; int main() { A a1; B b1; b1.print(a1); return 0; }大致过程我们看到A 里面私有成员a、保护成员b正常外部不能访问friend class BA把B设成友元A 主动给 B 开门B 全部成员函数都是 A 的友元print(A tmp)是引用tmp 就是 main 里 a1 的别名没有拷贝新对象,不浪费空间 B 凭借友元权限既能读取 A 的 private也能读取 protected。友元函数友元函数 老铁们理解就是是一个普通函数但不是类的成员函数哈一般在类内部用friend关键字声明它可以访问该类的 私有 和 保护 成员而且声明位置不受访问限定符限制友元函数特点特点一不是类的成员函数#include iostream using namespace std; class A { private: int num 100; public: friend void print(A o); //声明全局友元 void show() { cout 普通成员函数 endl; } }; //全局友元函数不加A:: void print(A a) { cout 友元取私有 o.num endl; } int main() { A a; print(a); // a.print(a); 这里就会报错友元不是成员 return 0; }大致内容print是全局函数不属于 A 类不能用对象.调用直接print(a)A 里面写了 friend 权限所以 print 能拿到 A 的私有 numshow是成员函数只能a.show()调用。特点二可以在类的任何地方声明#include iostream using namespace std; class A { private: int x 1; friend void f1(); protected: int y 2; friend void f2(); public: int z 3; friend void f3(); friend void f4(); }; // 四个友元全都能访问私有、保护、公有 void f1() { A tmp; cout tmp.x tmp.y tmp.z; } void f2() { A tmp; cout tmp.x tmp.y tmp.z; } void f3() { A tmp; cout tmp.x tmp.y tmp.z; } void f4() { A tmp; cout tmp.x tmp.y tmp.z; } int main() { f1(); return 0; }大致内容friend函数不管写在private、protected、public哪个位置全部生效我们看到f1 写在私有区、f2 写在保护区、f3/f4 写在公有区四个函数全是 A 的友元。只要是友元就能随便拿私有的x、保护的y、公有的z。但明白普通外部函数不能碰 x、y只有被 friend 给权限才行。五.内部类如果⼀个类定义在另⼀个类的内部这个内部类就叫做内部类。内部类是⼀个独⽴的类跟定义在全局相⽐他只是受外部类类域限制和访问限定符限制所以外部类定义的对象中不包含内部类。内部类默认是外部类的友元类。内部类本质也是⼀种封装当A类跟B类紧密关联A类实现出来主要就是给B类使⽤那么可以考虑把A类设计为B的内部类如果放到private/protected位置那么A类就是B类的专属内部类其他地⽅都⽤不了。#include iostream using namespace std; class Out { // 外部类 private: int num 10; // 内部类定义在外部类里面 class In { public: void show(Out a) { // 内部类默认是外部类友元直接访问外部类私有 cout a.num endl; } }; public: void test() { In in; in.show(*this); } }; int main() { Out a; a.test(); // In in; return 0; }通过上面的代码了解内部类的特点1.如果一个类定义在另一个类的内部这个内部类就叫做内部类。内部类是一个独立的类跟定义在全局相比只是受外部类类域和访问限定限制外部类对象里不包含内部类。对应到代码里就是我们看到In写在Out里面就是内部类。In 是独立的类Out 实例对象内存里没有存 In不会跟着 Out 创建对象。全局类随便在哪创建内部类受 Out 的public/private管控。2、内部类默认是外部类的友元类。对应到代码里In的成员函数可以随便读取Out的private私有成员num不用手动写friend。 代码里show(Out a)直接访问a.num就是靠这个特性。3、内部类本质封装A 只给 B 用就做成内部类放 private/protected 就是专属内部类类外无法使用。类class In写在 Out 的private里面只有 Out 自己的成员函数test()能创建In对象就连main 主函数不能定义In in;外面彻底用不了实现专属一个类的封装。但不是没有办法哈就是把内部类放 public外部就能使用class Out { public: class In{}; }; int main() { Out::In in; //public内部类可以通过类域访问 }六.匿名对象⽤ 类型(实参) 定义出来的对象叫做匿名对象相⽐之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象匿名对象⽣命周期只在当前⼀⾏⼀般临时定义⼀个对象当前⽤⼀下即可就可以定义匿名对象。#include iostream 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() { // 有名对象起名字aa1作用域到main结束才销毁 A aa1; //A aa1(); 易错编译器识别成函数声明不是创建对象 //匿名对象没有变量名 //格式类名(参数)生命周期仅限当前一行 A(); A(1); //有名对象aa2全程可用 A aa2(2); //匿名对象临时调用成员用完立刻销毁 Solution().Sum_Solution(10); return 0; }用 类型 (实参) 定义出来的对象叫做匿名对象类型 对象名 (实参) 是有名对象匿名对象生命周期只有当前一行通过这个代码给大家解释一下我们看到有名对象A aa1;、A aa2 (2); 对象有名字 aa1、aa2从创建开始整个 main 函数执行完才析构释放后面代码随时可以使用。匿名对象A ();、A (1);、Solution ().Sum_Solution (10); 没有变量名字只在当前这一行生效这行代码跑完马上调用析构销毁下一行直接消失A aa1();可能编译器当成函数声明名叫 aa1、返回 A、无参不会实例化对象。生命周期这一块大家记住就一句话:起名 就能活到代码块末尾不起名类名 ()就是当场用完当场销毁。七.对象拷⻉时的编译器优化现代编译器会为了尽可能提⾼程序的效率在不影响正确性的情况下会尽可能减少⼀些传参和传返回值的过程中可以省略的拷⻉(这里就提到我之前讲析构函数的时候为啥少几行了。如何优化C标准并没有严格规定各个编译器会根据情况⾃⾏处理。当前主流的相对新⼀点的编译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化有些更新更激进的编译器还会进⾏跨⾏跨表达式的合并优化。#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 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 ******************8 endl; f2(); A aa2 f2(); aa1 f2(); return 0; }我们看看这个代码哪里优化了A aa1; f1(aa1);aa1 是现成有名对象没法优化。值传参要复制一份给形参必然调用拷贝构造函数用完销毁副本。f1(1);没优化前先用 1 造出临时对象再把临时拷贝给形参。 编译器偷懒优化直接拿数字 1 在形参位置原地构造中间临时扔掉少一次拷贝。f1(A(2));没优化前先造匿名临时再拷贝临时给形参。编译器优化后匿名对象直接顶替形参不再额外复制。这里简单小结一哈传参只要是临时 / 字面量编译器能就地构造就省拷贝传入已有变量不能省。那咱打个比方理解一下没优化先单独造一个临时物件再复制一份送去当形参。优化之后临时物件直接占位当形参省去复制。f2();无优化函数内部造局部 aa再拷贝一份变成返回临时局部销毁。 编译器优化后不开辟局部变量直接在外面临时空间构造对象只构造 1 次无拷贝。A aa2 f2();看一下不优化啥情况f2函数里面造局部对象aa构造 1 次return aa把局部 aa拷贝成外面临时对象拷贝构造 1 次函数里局部 aa 销毁临时对象再拷贝给 aa2又一次拷贝构造。一共1 构造 2 次拷贝。编译器优化编译器清楚最后这个数据就是要放到aa2的内存里。 直接不去函数内部新开空间造局部 aa直接跑到外面 aa2 的地盘就地构造对象。 只执行1 次普通构造没有任何拷贝省去两次拷贝。如果老铁还不明白打个比方未优化厨房做一份饭局部 aa打包一份送到楼道临时桌临时对象→再从临时桌搬到你餐桌 aa2。搬两次 两次拷贝。优化厨师直接在你餐桌上做饭做完就是你的不用来回搬运。aa1 f2();f2 依旧编译器优化后内部直接生成返回临时。但是等号赋值不能优化省略已经存在 aa1 对象只能调用赋值运算符把临时数据赋值过去。各位老铁就可以理解为编译器规则就是咱能合并空间就合并能少拷贝就少拷贝但是赋值运算没法合并必须正常调用重载。我用的就是VS2022。DebugVS2019优化保守部分拷贝保留DebugVS2022/Release优化绝大多数拷贝全被编译器干掉。到这里我们再次理解一下类和对象计算机只认识二进制数据0和1不认识现实中的实体。要让计算机认识这些实体程序员需要用面向对象语言比如C定义类——类就是对某一类实体的描述包括它的属性叫什么、多大和行为能做什么。定义完类就创建了一个新的数据类型。然后通过这个类型去实例化对象计算机才能真的处理这些实体。我们可以这样理解计算机很笨它只认识 0 和 1。你想要计算机处理一个“人”或者“汽车”它根本不认识这些东西。所以你需要用代码来告诉计算机“计算机你看好了我定义一个‘人’这个东西他有名字、年龄他能吃饭、能走路。以后我说的‘人’就是这个样子。”这个“告诉计算机的过程”就是定义类。然后你让计算机根据这个“图纸”造一个具体的人出来比如“张飞18岁”这就是创建对象。一句话概括就是类就是“说明书”对象就是“产品”。计算机看不懂实物但看得懂你写的说明书。各位老铁本节内容就讲完了咱们类和对象也都是阐述完毕希望给大家带来新的收获如果觉得我讲的还行点赞收藏一下我们共同学习相互进步完结撒花下节内容见