类和对象 [ 完结 ]

发布时间:2026/8/15 14:42:22
类和对象 [ 完结 ] 深入理解构造函数初始化列表与隐式类型转换在C中构造函数负责对象的初始化。除了在函数体内赋值还有一种更核心、更高效的初始化方式——初始化列表。同时C支持的单参和多参构造函数的隐式类型转换也为代码编写带来了便利。本文将系统梳理这些知识点帮助您彻底掌握。一、初始化列表构造函数的“真正初始化战场”1. 什么是初始化列表初始化列表是构造函数定义中位于函数体之前的一段以冒号开头后跟逗号分隔的成员变量列表每个成员变量后面用括号给出初始值或表达式。class Date { public: // 初始化列表以冒号开始逗号分隔 Date(int year, int month, int day) : _year(year), _month(month), _day(day) { // 函数体这里已经是赋值而非初始化 } private: int _year, _month, _day; };关键理解初始化列表是成员变量定义并初始化的地方。每个成员变量在初始化列表中只能出现一次。可以认为无论你是否在初始化列表中显式写出某个成员它都会“走一遍”初始化列表——如果没写编译器会尝试用默认方式初始化它。2. 必须在初始化列表中初始化的三种成员有些成员变量必须在初始化列表中显式初始化否则编译报错引用成员变量int ref;引用必须在定义时绑定一个对象不能先定义再赋值。const成员变量const int cst;const变量也必须在定义时初始化不能后续赋值。没有默认构造函数的类类型成员MyClass obj;如果该类型没有提供无参或全缺省的默认构造函数编译器无法在初始化列表中自动构造它必须由你显式传参。class MyClass { public: MyClass(int x) {} // 只有带参构造无默认构造 }; class Test { private: int ref; // 引用 const int cst; // const变量 MyClass obj; // 无默认构造的类类型 public: // 必须使用初始化列表初始化上述三种成员 Test(int r, int c, int m) : ref(r), cst(c), obj(m) { // 函数体内不能初始化这三种成员 } };3. 初始化列表与函数体的配合初始化列表和函数体并非互斥它们可以打配合初始化列表负责定义和初始化函数体负责进一步的逻辑处理如错误检查、复杂赋值。class Test { private: int* _ptr; public: Date() : _ptr((int*)malloc(12)) { // 初始化列表分配内存 // 函数体检查并初始化内存 if (_ptr nullptr) { perror(malloc fail); } else { memset(_ptr, 0, 12); } } };4. C11的成员声明缺省值C11允许在成员变量声明时直接给缺省值private: int _year 1; // 声明时的缺省值 int _month 1; int _day 1;这个缺省值的作用是当该成员在初始化列表中没有被显式初始化时编译器会使用这个缺省值来初始化它。可以理解为缺省值是提供给初始化列表的“备选方案”。注意对于引用、const、无默认构造的类类型成员即使在声明时给了缺省值也必须在初始化列表中显式初始化否则依然编译报错。因为编译器认为缺省值是“赋值逻辑”而非“初始化逻辑”而这三者必须通过初始化来绑定或构造。5. 初始化列表的初始化顺序重要初始化列表的执行顺序不是按照列表中的书写顺序而是严格按照成员变量在类中声明的顺序。这是因为成员变量在内存中的布局由声明顺序决定初始化必须遵循该顺序。class A { public: A(int a) : _a1(a), _a2(_a1) {} // 看起来先_a1后_a2 void Print() { cout _a1 _a2 endl; } private: int _a2 2; // 声明_a2在前 int _a1 2; // 声明_a1在后 }; int main() { A aa(1); aa.Print(); // 输出结果1 随机值_a2先初始化此时_a1未初始化 }如上例虽然初始化列表先写_a1(a)但由于_a2在类中先声明所以先初始化_a2值为未初始化的_a1再初始化_a1值为1。建议声明顺序与初始化列表顺序保持一致以避免此类混淆。6. 总结初始化的完整规则每个成员变量都会走初始化列表显式在初始化列表中直接用给定的值/表达式初始化。未在初始化列表中如果声明处有缺省值使用缺省值。如果声明处无缺省值内置类型是否初始化取决于编译器通常不处理为随机值。自定义类型调用其默认构造函数无参/全缺省若没有则编译报错。引用、const、无默认构造的自定义类型必须在初始化列表中显式初始化。最佳实践尽量使用初始化列表来初始化所有成员这更高效也更能避免遗漏。二、构造函数的隐式类型转换C允许通过隐式类型转换用其他类型的值直接构造对象这极大地简化了代码。1. 单参数构造函数的隐式转换当构造函数只有一个参数时或有缺省值使得可单参调用可以用赋值语法进行隐式转换class A { public: A(int a 0) { _a1 a; } // 单参数/全缺省构造 private: int _a1; }; int main() { A aa1(1); // 直接构造 A aa2 2; // 隐式类型转换先用2构造临时A对象再拷贝构造aa2编译器通常会优化为直接构造 return 0; }实际上A aa2 2;等价于A aa2 A(2);会生成一个临时对象但编译器会优化掉中间的临时对象直接构造。常性临时对象生成的临时对象具有常性const因此如果用引用绑定它必须加constcppconst A raa 2; // 正确const引用绑定临时对象 // A raa 2; // 错误非常量引用不能绑定临时对象2. 多参数构造函数的隐式转换C11起C11开始支持多参数的隐式类型转换使用花括号列表cppclass A { public: A(int a1, int a2) { _a1 a1; _a2 a2; } private: int _a1, _a2; }; int main() { A aa3 {1, 1}; // 多参数隐式转换 const A raa4 {2, 2}; // const引用绑定临时对象 return 0; }3. 实际应用场景函数传参隐式类型转换在函数传参时非常方便cppclass Stack { public: void Push(const A aa) { // 参数为const引用可以接收临时对象 // ... } }; int main() { Stack st; A aa(3); st.Push(aa); // 传已有对象 st.Push(3); // 隐式转换3 - 临时A对象再传给Push st.Push({1,2}); // 多参数隐式转换 return 0; }4. 禁止隐式类型转换explicit关键字如果不想让编译器进行这种隐式转换可以在构造函数前加explicit关键字cppclass A { public: explicit A(int a 0) { _a1 a; } }; int main() { A aa1(1); // 正确直接构造 // A aa2 2; // 错误禁止隐式转换 return 0; }三、补充默认构造函数辨析默认构造函数是指可以无参调用的构造函数包括编译器自动生成的默认构造函数如果用户未定义任何构造函数。用户定义的无参构造函数。用户定义的全缺省构造函数所有参数都有默认值。以下代码中的Date(int year, month, day)不是默认构造函数因为它必须传三个参数cppclass Date { public: Date(int year, int month, int day) { /*...*/ } // 非默认构造 }; int main() { Date d1(2024, 7, 14); // 正确 Date d2; // 错误没有匹配的默认构造函数 return 0; }如果希望Date有默认构造可以定义全缺省构造函数或让编译器自动生成需注意内置类型默认不初始化。四、析构顺序小贴士成员析构顺序与初始化顺序声明顺序相反即先声明的后析构。多个对象析构后创建的对象先析构栈的后进先出特性。以下是针对 “必须在初始化列表中初始化的成员变量” 的示例#include iostream using namespace std; // 示例类没有默认构造函数只定义了带参构造 class MyClass { public: // 只有带参数的构造函数无默认构造函数 MyClass(int x) { cout MyClass 带参构造函数被调用x x endl; } }; class Test { private: int ref; // 引用成员变量 const int cst; // const成员变量 MyClass obj; // 没有默认构造函数的类类型变量 public: // 必须使用初始化列表初始化上述三种成员变量 Test(int r, int c, int m) : ref(r), cst(c), obj(m) { // 注意以下操作都是错误的不能在函数体内初始化这三种变量 // ref r; // 错误引用必须在定义时初始化 // cst c; // 错误const变量必须在定义时初始化 // obj MyClass(m); // 错误MyClass没有默认构造函数此处会先调用默认构造再赋值导致编译失败 } }; int main() { int a 10; Test t(a, 20, 30); return 0; }说明引用成员变量ref引用必须在定义时立即绑定对象无法先定义再赋值因此必须在初始化列表中初始化。const 成员变量cstconst 变量一旦定义就不能修改必须在定义时初始化因此必须在初始化列表中指定初始值。没有默认构造函数的类类型变量obj当类中没有默认构造函数时创建对象必须传入参数。如果不在初始化列表中初始化编译器会尝试调用其默认构造函数导致编译报错。#includeiostream using namespace std; class A { public: //如果不想让类型转换发生可以在构造函数前加explicit函数 /*explicit A(int a 0) { _a1 a; }*///多参数也是如此 A(int a 0)//单参数可以 { _a1 a; } A(int a1, int a2) { _a1 a1; _a2 a2; } A(const A aa) { _a1 aa._a1; } void Print() { cout _a1 _a2 endl; } private: int _a1 1; int _a2 2; }; class Stack { public: void Push(const A aa)//临时对象具有常性要加const { //... } private: A _arr[10]; int _top; }; int main() { A aa1(1);//调用构造 aa1.Print(); // 隐式类型转换类型转换会生成中间变量 // 2先构造⼀个A的临时对象再⽤这个临时对象拷⻉构造aa2 // 编译器遇到连续构造拷⻉构造-优化为直接构造 //调试观察 A aa2 2; aa2.Print(); A raa1 aa2; //A aa2 2;可以验证 //并不是直接构造会生成临时变量具有常性 const A aa2 2; //真正用意 Stack st; A aa3(3); st.Push(aa3); //写起来更简单消耗是一样的 st.Push(3); //多参数 //A aa4 1, 1;//不支持 //但是C11后才支持多参数转换 A aa4 { 1, 1 }; const A raa5 { 2,2 }; st.Push(aa4); st.Push({ 2,2 }); return 0; }注意初始化列表是他定义的地方那么每个成员都会去走初始化列表哪怕是不写因为是定义初始化的地方还有一类自定义类型EGclass Time { public: Time(int hour 1) :_hour(hour) { cout Time() endl; } private: int _hour; };不在初始化列表中定义 不写也走只调用Time的默认构造函数无参/全缺省如果Time没有默认构造调不到只能自己传爱传什么传什么无默认构造class Time { public: Time(int hour) :_hour(hour) { cout Time() endl; } private: int _hour; }; class Date { public: Date(int xx, int year, int month, int day) : _year(year) , _month(month) , _day(day) , _n(1) , _ref(xx) , _t(1) {} void Print()const { cout _year 年 _month 月 _day 日 endl; } private: //声明 int _year; int _month; int _day; const int _n; int _ref; Time _t; };初始化列表和函数体内可以混着赋值EG初始化列表和函数体可以打配合Date() :_ptr((int*)malloc(12)) { //初始化列表和函数体内可以混着赋值 if (_ptr nullptr) { perror(malloc fail); } else { memset(_ptr, 0, 12); } }C11 ⽀持在成员变量声明的位置给缺省值这个缺省值主要是给没有显⽰在初始化列表初始化的成员使⽤的注意const 成员变量、引用成员变量、没有默认构造的类类型变量即使在声明时给了缺省值也必须在初始化列表中显式初始化否则依然会编译报错。编译器会认为缺省值是 “赋值逻辑”而非 “初始化逻辑”注定义是有开空间下列是声明C11及后private: //声明,后面为缺省值-初始化列表用的 int _year 1; int _month 1; int _day 1;;可以通过缺省函数理解只不过是给初始化类表用的结合下图理解尽量使⽤初始化列表初始化因为那些你不在初始化列表初始化的成员也会⾛初始化列表如果这个成员在声明位置给了缺省值初始化列表会⽤这个缺省值初始化。如果你没有给缺省值对于没有显⽰在初始化列表初始化的内置类型成员是否初始化取决于编译器C 并没有规定。对于没有显⽰在初始化列表初始化的⾃定义类型成员会调⽤这个成员类型的默认构造函数如果没有默认构造会编译错误注意与默认构造函数区分可以无参调用全缺省无参的才是默认构造以下并不是#includeiostream using namespace std; class Date { public: Date(int year, int month, int day) : _year(year) , _month(month) //, _day(day)对于内置类型是不做处理的可能是个随机值 { } void Print()const { cout _year 年 _month 月 _day 日 endl; } private: //声明,后面为缺省值-初始化列表用的 int _year 1; int _month 1; int _day 1;; }; int main() { Date d1(2024, 7, 14); d1.Print(); Date d2;//会报错所以并不是默认构造 return 0; }因此让编译器自动生成默认构造便可行还有不仅仅可以给以上这种缺省值private: int _year 1; int _month 1; int _day 1;; int* _ptr (int*)malloc(12); Time _t 1;初始化列表中按照成员变量在类中声明顺序进⾏初始化跟成员在初始化列表出现的先后顺序⽆关。建议声明顺序和初始化列表顺序保持⼀致成员析构顺序与声明顺序相反先构造的 m1 后析构后构造的 m3 先析构多个对象析构时后创建的先析构t2 比 t1 后创建t2 先析构。例题下面程序的运行结果是什么DA. 输出 1 1B. 输出 2 2C. 编译报错D. 输出 1 随机值E. 输出 1 2F. 输出 2 1#includeiostream using namespace std; class A { public: A(int a) :_a1(a) , _a2(_a1) {} void Print() { cout _a1 _a2 endl; } private: int _a2 2; int _a1 2; }; int main() { A aa(1); aa.Print(); }因为定义是先_a2后_a1,先对_a2进行初始化这时候_a1的缺省值还没发现给_a2的初始化就是编译器自己决定的为_a1的随机值将_a1的值给了_a2,再到_a1,将1的值给了_a1所以answer是D(调试很快发现)总结尽可能写初始化列表每个成员都要走初始化列表每个构造函数都有初始化列表在初始化列表初始化的成员直接用显示写没有在初始化列表的成员不显示写a. 声明的地方有缺省值用缺省值b. 没有缺省值内置类型不确定看编译器大概率是随机值自定义类型调用默认构造没有默认构造就编译报错引用const没有默认构造的自定义必须在初始化列表初始化不要把初始化列表和默认构造扯上关系初始化列表中按照成员变量在类中声明顺序在内存中存放的顺序进行初始化声明中的缺省值是给初始化列表用的缺省值是走初始化列表的2.类型转换C ⽀持内置类型隐式类型转换为类类型对象需要有相关内置类型为参数的构造函数构造函数前⾯加explicit就不再⽀持隐式类型转换以下是展示 C 中内置类型隐式转换为类类型以及使用explicit关键字禁止隐式转换的代码示例#include iostream using namespace std; class MyInt { private: int value; public: // 普通构造函数允许隐式转换 MyInt(int v) : value(v) { cout MyInt 构造函数被调用value v endl; } int getValue() const { return value; } }; class ExplicitInt { private: int value; public: // 用 explicit 修饰的构造函数禁止隐式转换 explicit ExplicitInt(int v) : value(v) { cout ExplicitInt 构造函数被调用value v endl; } int getValue() const { return value; } }; // 测试函数参数为 MyInt 类型 void printMyInt(MyInt m) { cout printMyInt: m.getValue() endl; } // 测试函数参数为 ExplicitInt 类型 void printExplicitInt(ExplicitInt e) { cout printExplicitInt: e.getValue() endl; } int main() { // 1. 测试普通构造函数允许隐式转换 MyInt m1 10; // 隐式转换int → MyInt调用 MyInt(10) printMyInt(20); // 隐式转换int → MyInt调用 MyInt(20) // 2. 测试 explicit 构造函数禁止隐式转换 ExplicitInt e1(30); // 直接初始化允许 // ExplicitInt e2 40; // 错误不允许隐式转换编译报错 // printExplicitInt(50); // 错误不允许隐式转换编译报错 // 显式转换是允许的 ExplicitInt e3 ExplicitInt(40); // 显式构造 printExplicitInt(ExplicitInt(50)); // 显式构造后传递参数 return 0; }代码说明MyInt类其构造函数未加explicit允许int类型隐式转换为MyInt对象如MyInt m1 10或printMyInt(20)。ExplicitInt类其构造函数加了explicit禁止隐式转换ExplicitInt e2 40会编译报错不允许隐式转换printExplicitInt(50)会编译报错不允许隐式转换但显式转换如ExplicitInt(40)仍然允许。运行结果MyInt 构造函数被调用value 10 MyInt 构造函数被调用value 20 printMyInt: 20 ExplicitInt 构造函数被调用value 30 ExplicitInt 构造函数被调用value 40 ExplicitInt 构造函数被调用value 50 printExplicitInt: 50通过explicit关键字可以避免意外的隐式类型转换增强代码的可读性和安全性。C 中 “内置类型隐式转换为类类型” 的原理本质是编译器自动调用单参数构造函数或可接收单个参数的构造函数将内置类型值转换为目标类的临时对象。具体过程可拆解为 3 步当需要一个类类型对象如函数参数、赋值操作的右侧但实际提供的是一个内置类型值时编译器会检查该类是否有以该内置类型为参数的构造函数可以是单参数构造或其他参数有默认值的多参数构造如MyClass(int a, int b0)可接收单个int。若存在这样的构造函数编译器会自动调用该构造函数创建一个临时的类对象用内置类型值初始化这个临时对象。这个临时对象会被用来参与后续操作如传递给函数参数、赋值给类对象等。3.static成员⽤static修饰的成员变量称之为静态成员变量静态成员变量⼀定要在类外进⾏初始化内只能对静态成员变量进行声明需用static关键字不能直接初始化。必须在类外单独初始化格式为类型 类名::静态成员变量名 初始值;不加static关键字。例外情况对于静态常量成员变量static constC11 允许在类内直接初始化但仍可在类外初始化以类外为准。对于模板类的静态成员变量初始化规则略有不同可在类内或类外初始化视具体场景而定。示例代码class Test { public: // 类内声明静态成员变量不可直接初始化除 static const 外 static int a; // 正确声明 static const int b 10; // 正确静态常量可在类内初始化C11 起 // static int c 20; // 错误非 const 静态成员不能在类内初始化 }; // 类外初始化静态成员变量必须做 int Test::a 0; // 正确类外初始化 // const int Test::b 30; // 可选若类内已初始化类外可再次初始化覆盖类内值 int main() { return 0; }个人问题提出static 加上了 const那干嘛不直接使用const也就是这两个有什么区别static const和普通const成员变量的核心区别在于作用域和存储方式二者适用场景不同不能简单替代static const成员变量属于类级别的常量所有对象共享同一份数据存储在全局数据区不占用对象内存。可通过类名::成员名直接访问无需创建对象。常用于表示类的公共常量如类的版本号、默认配置等。class Config { public: static const int MAX_SIZE 1024; // 所有对象共享此常量 }; // 使用时无需创建对象 int main() { cout Config::MAX_SIZE endl; // 直接访问 return 0; }普通const成员变量属于对象级别的常量每个对象都有独立的副本存储在对象内存中。必须通过对象访问且初始化后当前对象内不可修改但不同对象可有不同初始值。常用于表示对象独有的常量属性如学生的学号、身份证号等。class Student { private: const int id; // 每个学生有唯一且不可修改的学号 public: Student(int i) : id(i) {} // 必须在初始化列表中赋值 }; int main() { Student s1(1001); // id1001对象1的常量 Student s2(1002); // id1002对象2的常量 return 0; }若常量属于类本身所有对象共享且逻辑一致用static const。若常量属于每个对象不同对象可能有不同值用普通const。二者的设计初衷不同不能相互替代。静态成员变量为当前类的所有对象所共享不属于某个具体的对象不存在对象中存放在静态区⽤static修饰的成员函数称之为静态成员函数静态成员函数没有this 指针如果_scount是私有的那么指定类域的方式就行不通了这时候就可以提供一个静态的成员函数static int GetACount() { return _scount; } // 静态成员函数中 return _scount 的本质是 return 类名::_scount编译器会自动补充类域而非依赖 this 指针的对象成员访问。 // 如果 _scount 不是 static 修饰的普通成员变量那么在静态成员函数中直接访问 _scount 会编译报错。静态成员函数中可以访问其他的静态成员但是不能访问非静态的因为没有 this 指针static int GetACount() { _a;//报错//不能访问⾮静态的因为没有this指针 return _scount; }非静态的成员函数可以访问任意的静态成员变量和静态成员函数void func() { cout _scount endl; cout GetACount() endl; }突破类域就可以访问静态成员可以通过类名静态成员或者对象.静态成员来访问静态成员变量和静态成员函数cout A::GetACount() endl; cout a1.GetACount() endl;静态成员也是类的成员受public、protected、private访问限定符的限制(类里面定义的静态成员变量某种程度上可以理解为静态的全局只是受类域的限制也就是他的生命周期是全局的且不属于某个对象严格来说只能在所在的类里面使用类外面也行只是受访问限定符的限制)静态成员变量不能在声明位置给缺省值初始化因为缺省值是给构造函数初始化列表的静态成员变量不属于某个对象不⾛构造函数初始化列表4.友元友元提供了⼀种突破类访问限定符封装的⽅式友元分为友元函数和友元类在函数声明或者类声明的前⾯加friend并且把友元声明放到⼀个类的⾥⾯外部友元函数可访问类的私有和保护成员友元函数仅仅是⼀种声明它不是类的成员函数友元函数可以在类定义的任何地⽅声明不受类访问限定符限制⼀个函数可以是多个类的友元函数我不仅仅和你还要和别人做朋友#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;//把B定义成A的友元类-----解决点---B的成员函数都成为了A的友元函数 private: int _a1 1; int _a2 2; }; class B { public://B要一直访问A类的私有保护成员-----问题点 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类的友元但是B类不是A类的友元友元类关系不能传递如果A是B的友元 B是C的友元但是A不一定是C的友元有时提供了便利。但是友元会增加耦合度破坏了封装所以友元不宜多⽤个人问题提出基类中声明友元是不是直接可以访问其子类相反呢基类中声明的友元与子类的访问关系以及子类中声明的友元与基类的访问关系遵循以下规则1. 基类的友元不能直接访问子类的私有 / 保护成员基类的友元仅能访问基类自身的私有 / 保护成员无法自动访问子类的私有 / 保护成员即使子类继承了基类。子类的成员包括从基类继承的成员中只有子类自己声明为public的成员才能被外部访问私有 / 保护成员受子类自身的访问限定符保护与基类的友元无关。示例class Base { friend void FriendFunc(Base b); // 友元函数声明 protected: int _base_val 10; }; class Derived : public Base { private: int _derived_val 20; // 子类私有成员 }; void FriendFunc(Base b) { // 可以访问基类的保护成员 cout b._base_val endl; // 若传入子类对象仍无法访问子类的私有成员 Derived d (Derived)b; // 强制转换为子类引用 // cout d._derived_val endl; // 错误无法访问子类私有成员 }2. 子类的友元不能直接访问基类的私有 / 保护成员子类的友元仅能访问子类自身的私有 / 保护成员无法访问基类的私有 / 保护成员除非基类主动将该友元声明为自己的友元。基类的私有 / 保护成员仅对基类自身和基类声明的友元开放与子类的友元无关。示例class Base { private: int _base_val 10; // 基类私有成员 }; class Derived : public Base { friend void FriendFunc(Derived d); // 子类友元声明 private: int _derived_val 20; }; void FriendFunc(Derived d) { // 可以访问子类的私有成员 cout d._derived_val endl; // 无法访问基类的私有成员即使子类继承了基类 // cout d._base_val endl; // 错误无法访问基类私有成员 }总结友元关系是单向且局限于声明它的类与继承关系无关。基类的友元 → 只能访问基类的私有 / 保护成员无权访问子类的私有 / 保护成员。子类的友元 → 只能访问子类的私有 / 保护成员无权访问基类的私有 / 保护成员。若需要跨类访问必须在对应的类中单独声明友元如基类想让子类的友元访问自己需在基类中额外声明该友元。5.内部类如果一个类定义在另一个类的内部这个内部类就叫做内部类。内部类是一个独立的类跟定义在全局相比它仅仅只是受外部类类域限制和访问限定符限制所以外部类定义的对象中不包含内部类内类不是不属外类的成员内部类默认是外部类的友元类我是你的朋友所以我可以访问你的私有内部类本质也是一种封装当 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 } private: int _b 1; }; }; int A::_k 1; int main() { cout sizeof(A) endl;//4:实际上只有一个_h,没带_b//B不是A的成员 A::B b;//指定B是属于A的类域受访问限定符限制 A aa; b.foo(aa); return 0; }6.匿名对象用“类型 (实参)”定义出来的对象叫做匿名对象相比之前用“类型 对象名 (实参)”定义的对象后者被称为有名对象匿名对象的生命周期只在当前一行一般用于临时定义一个对象并立即使用比如调用成员函数、传递参数用完后下一行就会自动调用析构函数匿名对象的核心作用就是简化代码、提升便捷性避免为临时使用的对象额外定义一个对象名。 代码示例匿名对象 vs 有名对象#include iostream using namespace std; class MyClass { public: MyClass(int val) : _val(val) { cout 构造函数val _val endl; } ~MyClass() { cout 析构函数val _val endl; } void ShowVal() { cout 当前值 _val endl; } private: int _val; }; int main() { // 1. 有名对象需要定义对象名生命周期到作用域结束main函数结束 cout 有名对象 endl; MyClass obj(10); // 有名对象对象名obj obj.ShowVal(); // 通过对象名调用成员函数 // 2. 匿名对象无对象名生命周期仅当前行 cout \n 匿名对象 endl; MyClass(20).ShowVal(); // 匿名对象直接调用成员函数一行内用完即析构 cout \n main函数即将结束 endl; return 0; } 运行结果与分析 有名对象 构造函数val 10 当前值10 匿名对象 构造函数val 20 当前值20 析构函数val 20 // 匿名对象在当前行结束后立即析构 main函数即将结束 析构函数val 10 // 有名对象在作用域结束main结束后析构从结果能明显看到匿名对象在调用完ShowVal()后立刻析构而有名对象要等到main函数结束才析构这正是二者生命周期的核心区别。7.对象拷贝时的编译器优化现代编译器会为了尽可能提高程序效率在不影响代码正确性的前提下尽量减少传参和传返回值过程中可省略的拷贝操作通常称为 “拷贝省略” 优化关于如何优化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(const A aa) endl; if (this ! aa) { _a1 aa._a1; } return *this; } ~A() { cout ~A() endl; } void Print() { cout A::Print _a1 endl; } private: int _a1 1; };单参数类型的构造函数的隐式类型转化的优化示例int main() { //单参数类型的构造函数的隐式类型转化 A aa1 1;//构造加拷贝构造构造临时对象对临时对象进行拷贝构造因为类型转化会产生临时对象 //开始执行发现编译器优化成直接拷贝 const A aa2 1;//生成临时对象是引用没有拷贝构造 return 0; }传参优化示例void f1(A aa)//传值传参---会产生一个拷贝 {} void f2(A aa) {} int main() { A aa1(1);//构造 //f1(aa1);//实参传给形参进行拷贝调用构造拷贝构造//不会优化一般是在连续一个步骤才有优化//但是不排除其他更激进的 cout endl; //用引用减少拷贝构造 f2(aa1); cout endl; //用匿名对象在连续过程 f1(A(1));//优化成直接构造 cout endl; f1(1);//本意是1走隐式类型转化生成临时对象再拷贝构造优化成直接构造 cout endl; return 0; }传返回值优化示例A f3()//传值传参返回会产生一个临时对象因为aa出了作用域就销毁了 { A aa(1); return aa;//产生临时对象 } //应该是aa作为局部对象出了作用域销毁调用~A()由aa产生临时对象调用Print //VS2022优化严格来说没有生成aa合二为一只生成临时对象看执行结果可推 int main() { f3().Print();//因为临死对象构造拷贝构造//用产生的临时对象调用Print//临时对象的周期就在这一行 cout endl; return 0; }A f4() { A aa(1);//构造 return aa;//拷贝构造 } //优化省略了临时对象VS2022更激进连aa都省掉了建议用2019-debug观察 int main() { A ret f4();//拷贝构造 ret.Print(); cout endl; return 0; }A f4() { A aa(1);//构造 cout ~~~~~~~~~ endl;//构造了aaaa充当临时对象 return aa;//拷贝构造 } int main() { A ret; ret f4();//赋值//赋值完后。临时对象析构 ret.Print(); cout endl; return 0; }