ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C++ - > 类和对象

C++ - > 类和对象 1. 类的定义1.1 类定义格式^ class 为定义类的关键字Stack 为类的名字{}中为类的主体注意类定义结束时后面分号不能省略。类中内容称为类的成员类中的变量称为类的属性或成员变量类中的函数称为类的方法或者成员函数。^ 为了区分成员变量一般习惯上成员变量会加一个特殊标识如成员变量前面或者后面加 _ 或者 m 开头注意C中这个并不是强制的只是一些惯例具体看公司的要求。^ C中 struct 也可以定义类C兼容C中 struct 的用法同时 struct 升级成了类明显的变化是 struct 中可以定义函数一般情况下我们推荐用class定义类。^ 定义在类里面的成员函数默认为 inline。class Stack { public: // 成员函数 void Init(int n 4) { array (int*)malloc(sizeof(int) * n); if (nullptr array) { perror(malloc申请空间失败); return; } capacity n; top 0; } void Push(int x) { // ...扩容 array[top] x; } int Top() { assert(top 0); return array[top - 1]; } void Destroy() { free(array); array nullptr; top capacity 0; } private: // 成员变量 int* array; size_t capacity; size_t top; }; // 分号不能省略class Date { public: void Intit(int year, int month, int day) { _year year; _month month; _day day; } private: // 为了区分成员变量一般习惯上成员变量会加一个特殊标识 // 如 _ 或者 m 开头 int _year; int _month; int _day; };1.2 访问限定符^ C一种实现封装的方式用类将对象的属性与方法结合在一起让对象更加完善通过访问权限选择性的将其接口提供给外部的用户使用。^ public 修饰的成员在类外可以直接被访问protected 和 private 修饰的成员在类外不能直接被访问。^ 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止如果后面没有访问限定符作用域就到 } 即类结束。1.3 类域^ 类定义了一个新的作用域类的所以成员都在类的作用域中在类体外定义成员时需要使用作用域操作符指明成员属于哪个类域。^ 类域影响的是编译的查找规则下面程序中 Init 如果不指定类域 Stack那么编译器就把 Init 当成全局函数那么编译时找不到 array 等成员的声明/定义在哪里就会报错。指定类域Stack就是知道 Init 是成员函数当前域中找不到的 array 等成员就会到类域中去查找。class Stack { public: // 成员函数 void Init(int n 4); private: // 成员变量 int* array; size_t capacity; size_t top; }; // 声明和定义分离需要指定类域 void Stack::Init(int n) { array (int*)malloc(sizeof(int) * n); if (nullptr array) { perror(malloc申请空间失败); return; } capacity n; top 0; }2. 实例化2.1 实例化概念^ 用类类型在物理内存中创建对象的过程称为类实例化出对象。^ 类是对象进行一种抽象描述是一个模型一样的东西限定了类有哪些成员变量这些成员变量只是声明没有分配空间用类实例化出对象时才会分配空间。^ 一个类可以实例化出多个对象实例化出的对象占用实际的物理空间用来存储类成员变量。打个比方类实例化出对象就像现实中使用建筑设计图建造房子类就像是设计图用设计图修建房子房子才能住人。同样类就像设计图一样不能存储数据实例化出的对象分配物理内存存储数据。#includeiostream using namespace std; class Date { public : void Init(int year, int month, int day) { _year year; _month month; _day day; } void Print() { cout _year / _month / _day endl; } private: // 这里只是声明没有开空间 int _year; int _month; int _day; }; int main() { // Date类实例化出对象 d1 和 d2 Date d1; Date d2; return 0; }2.2 对象大小分析一下类对象中哪些成员呢类实例化出的每个对象都有独立的数据空间所以对象中肯定包含成员变量那么成员函数是否包含呢首先函数被编译后是一段指令对象中没办法存储这些指令存储在一个单独的区域代码段那么对象总非要存储的话只能是成员函数的指针。在分析一下对象中是否有存储指针的必要呢Date实例化d1和d2两个对象d1和d2都要各自独立的成员变量存储各自的数据但是d1和d2的成员函数 Init 指针却是一样的存储在对象中就浪费了。如果Date实例化100个对象那么成员函数指针就重复存储100次太浪费了。其实函数指针是不需要存储的函数指针是一个地址调用函数被编译成汇编指令[call地址]其实编译器在编译链接时就要找到函数的地址不是在运行时找只有动态多态是在运行时找就需要存储函数地址。内存对齐规则1. 第一个成员在与结构体偏移量为0的地址2. 其他成员变量要对齐到某个数字对齐数的整数倍的地址处3. 注意对齐数 编译器默认的一个对齐数与该成员大小的较小值4. VS中默认的对齐数为 85. 结构体总大小为最大对齐数所有变量类型最大者与默认对齐参数取最小的整数倍6. 如果嵌套了结构体的情况嵌套的结构体对齐到自己的最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数含嵌套结构体的对齐数的整数倍#includeiostream using namespace std; // 计算⼀下A/B/C实例化的对象是多⼤ class A { public: void Print() { cout _ch endl; } private: char _ch; int _i; }; class B { public: void Print() { //... } }; class C { }; int main() { A a; B b; C c; cout sizeof(a) endl; cout sizeof(b) endl; cout sizeof(c) endl; return 0; }上面的程序运行后我们看到没有成员变量的B和C类对象的大小是1为什么没有成员变量话要给1个字节呢因为如果一个字节都不给怎么表示对象存在过呢所以这里给1个字节纯粹是为了占位标识对象存在。3. this 指针^ Date类中有 Init 与 Print 两个成员函数函数体中没有关于不同对象的区分那当d1调用Init和Print函数时该函数是如何知道应该访问的是d1对象还是d2对象呢那么这里就要看C给了一个隐含的this 指针解决这里的问题。^ 编译器编译后类的成员函数默认都会在形参第一个位置增加一个当前类类型的指针叫做this 指针。如Date类的Init的真实原型为void Init (Date* const this , int year , int month , int day )^ 类的成员函数中访问成员变量本质都是通过this指针访问的如Init函数中给 _year 赋值this_year year。^ C规定不能再实参和形参的位置显示的写 this 指针编译时编译器会处理但是可以在函数体内显示使用 this 指针。4. 类的默认成员函数默认成员函数就是用户没有显示实现编译器会自动生成的成员函数称为默认成员函数。一个类我们不写的情况下编译器会默认生成一下6个默认成员函数需要注意的是这6个中最重要的是前4个最后两个取地址重载不重要我们稍微了解一个即可。默认成员函数很重要也比较复杂我们要从两个方面去学习1. 我们不写时编译器默认生成的函数行为是什么是否满足我们的需求。2. 编译器默认生成的函数不满足我们的需求我们需要自己实现那么如何自己实现5. 构造函数构造函数是特殊的成员函数需要注意的是构造函数虽然名称叫构造但是构造函数的主要任务并不是开空间创建对象我们常使用的局部对象是栈帧创建时空间就开好了而是对象实例化时初始化对象。构造函数的本质是替代我们以前Stack和Date类中写的Init函数的功能构造函数自动调用的特点就完美的替代了Init。构造函数的特点1. 函数名与类名相同。2. 无返回值。返回值啥都不需要给也不需要写 void 不要纠结C规定如此3. 对象实例化时系统会自动调用对应的构造函数。4. 构造函数可以重载。5. 如果类中没有显示定义构造函数则C编译器会自动生成一个无参的默认构造函数一旦用户显示定义编译器将不再生成。6. 无参构造函数全缺省构造函数我们不写构造时编译器默认生成的构造函数都叫做默认构造函数。但是这三个函数有且只有一个存在不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载但是调用时存在歧义。要注意很多同学会认为默认构造函数就是编译器默认生成那个叫默认构造实际上无参构造函数全缺省构造函数也是默认构造总结一下就是不传实参就可以调用的构造就叫默认构造。7. 我们不写编译器默认生成的构造对内置类型成员变量的初始化没有要求也就是说是是否初始化是不确定的看编译器。对于自定义类型成员变量要求调用这个成员变量的默认构造函数初始化。如果这个成员变量没有默认构造函数那么就会报错我们要初始化这个成员变量需要用初始化列表才能解决。8. 每个成员变量在初始化列表只能出现一次语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。9. 引用成员变量const 成员变量没有默认构造的类类型变量必须放在初始化列表位置进行初始化否则会编译报错。10. C11 直接在成员变量声明的位置给缺省值这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。11. 尽量使用初始化列表初始化因为那些你不在初始化列表初始化的成员也会走初始化列表如果这个成员在声明位置给了缺省值初始化列表会使用这个缺省值初始化。如果没有给缺省值对于没有显示在初始化的内置类型成员是否取决于编译器C并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数如果没有默认构造会编译报错。12. 初始化列表中按照成员变量在类中声明顺序进行初始化跟成员在初始化列表出现的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。说明C把类型分成内置类型基本类型和自定义类型。内置类型就是语言提供的原生数据类型如 int / char / double / 指针等自定义类型就是我们使用 class / struct等关键字自己定义的类型。初始化列表总结无论是否显示写初始化列表每个构造函数都有初始化列表无论是否在初始化列表显示初始化成员变量每个成员变量都要走初始化列表初始化。#includeiostream using namespace std; class Date { public: // 1. 无参构造函数 Date() { _year 1; _month 1; _day 1; } // 2. 带参构造函数 Date(int year,int month,int day) { _year year; _month month; _day day; } // 3. 全缺省构造函数 /*Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; }*/ void Print() { cout _year / _month / _day endl; } private: int _year; int _month; int _day; }; int main() { Date d1; // 调用默认构造函数 Date d2(2026,9,16); // 调用带参的构造函数 // 注意如果通过无参构造函数的创建对象时对象后面不用跟括号 // 区分这里是函数声明还是实例化对象 // warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?) Date d3(); d1.Print(); d2.Print(); return 0; }6. 析构函数析构函数与构造函数功能相反析构函数不是完成对对象本身的销毁比如局部对象是存在栈帧的函数结束栈帧销毁他就释放了不需要我们管C规定对象在销毁时会自动调用析构函数完成对象中资源的清理释放工作。析构函数的功能类比我们之前 Stack 实现的 Destroy 功能而像 Date 没有 Destroy ,其实就是没有资源需要释放所以严格说 Date 是不需要析构函数的。析构函数的特点1. 析构函数名是在类名前加上字符 ~。2. 无参数无返回值。这里跟构造类似也不需要加 void )3. 一个类只能有一个析构函数若未显示定义系统会自动生成默认的析构函数。4. 对象生命周期结束时系统会自动调用析构函数。5. 跟构造函数类似我们不写编译器自动生成的析构函数对内置类型成员不做处理自定义类型成员会调用他的析构函数。6. 还需要注意的是我们显示写析构函数对于自定义类型成员也会调用他的析构也就是说自定义类型成员无论什么情况都会自动调用析构函数。7. 如果类中没有申请资源时析构函数可以不写直接使用编译器生成的默认析构函数如Date;如果默认生成的析构就可以用也就不需要显示写析构如MyQueue; 但是有资源申请时一定要自己写析构否则会造成资源泄露如 Stack。8. 一个局部域的多个对象C规定后定义的先析构。7. 拷贝构造函数如果一个构造函数的第一个参数是自身类类型的引用且任何额外的参数都有默认值则次构造函数也叫做拷贝构造函数也就是拷贝构造是一个特殊的构造函数。拷贝构造的特点1. 拷贝构造函数是构造函数的一个重载。2. 拷贝构造函数的第一个参数必须是类类型对象的引用使用传值方式编译器直接报错因为语法逻辑上会引发无穷递归调用。拷贝构造函数也可以多个参数但是第一个参数必须是类类型对象的引用后面的参数必须有缺省值。3. C规定自定义类型对象进行拷贝行为必须调用拷贝构造所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。4. 若未显示定义拷贝构造编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝 / 浅拷贝一个字节一个字节的拷贝。对自定义类型成员变量会调用他的拷贝构造。5. 像 Date 这样的类成员变量全是内置类型且没有指向什么资源编译器自动生成的拷贝构造就可以完成需要的拷贝所以不需要我们显示实现拷贝构造。像 Stack 这样的类虽然也都是内置类型但是指向了资源编译器自动生成的拷贝构造完成的值拷贝 / 浅拷贝不符合我们的需求所以需要我们自己实现深拷贝对指向的资源也进行拷贝。像 MyQueue 这样的类型内部主要是自定义类型 Stack 成员编译器自动生成的拷贝构造会调用 Stack 的拷贝构造也不需要我们显示实现。这里还有一个技巧如果一个类显示实现了析构并释放资源那么他就需要显示写拷贝构造否则就不需要。6. 传值返回会产生一个临时对象调用拷贝构造传值引用返回返回的是返回对象的别名引用没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象函数结束就销毁了那么使用引用返回是有问题的这时的引用相当于一个野引用类似一个野指针一样。传引用返回可以减少拷贝但是一定要确保返回对象在当前函数结束还在才能用引用返回。8. 赋值运算符重载8.1 运算符重载^ 当运算符被用于类类型的对象时C语言允许我们通过运算符冲重载的形式指定新的含义。C规定类类型对象使用运算符时必须转换成调用对应运算符重载若没有对应的运算符重载则会编译报错。^ 运算符重载是具有特殊名字的函数他的名字是由 operator 和后面要定义的运算符共同构成。和其他函数一样他也具有其返回类型和参数列表以及函数体。^ 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数二元运算符有两个参数二元运算符的左侧运算对象传给第一个参数右侧运算对象传给第二个参数。^ 如果一个重载运算符函数是成员函数则它的第一个运算对象默认传给隐式的 this 指针因此运算符重载作为成员函数时参数比运算对象小一个。^ 运算符重载以后其优先级和结核性与对应的内置类型运算符保持一致。^ 不能通过连接语法中没有的符号来创建新的操作符比如 operator。^注意以上5个运算符不能重载。^ 重载操作符至少有一个类类型参数不能通过运算符重载改变内置类型对象的含义。^ 一个类需要重载哪些运算符是看哪些运算符重载后有意义。^ 重载运算符时有前置和后置运算符重载函数名都是 operator无法很好的区分。C规定后置重载时增加一个int形参跟前置构成函数重载方便区分。^ 重载和时需要重载为全局函数因为重载为成员函数this 指针默认抢占了第一个形参位置第一个形参位置是左侧运算对象调用时就变成了 对象cout不符合使用习惯和可读性。重载为全局函数把 ostream / istream 放到第一个形参位置就可以了第二个形参位置当类类型对象。#includeiostream using namespace std; // error:运算符 必须至少包含一个类类型的参数 //int operator(int x, int y) //{ // return x - y; //} class Date { public: Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; } void Print() { cout _year - _month - _day endl; } bool operator(const Date d) { return _year d._year _month d._month _day d._day; } Date operator() { cout 前置 endl; return *this; } Date operator(int) { Date tmp; cout 后置 endl; return tmp; } private: int _year; int _month; int _day; }; int main() { Date d1(2026, 9, 16); Date d2(2026, 10, 1); // 运算符重载函数可以显示调用 d1.operator(d2); // 编译器会转换成 d1.operator(d2) d1 d2; // 编译器会转换成 d1.operator() d1; // 编译器会转换成 d2.operator(0) d1; return 0; }8.2 赋值运算符重载赋值运算符重载是一个默认成员函数用于完成两个已经存在的对象直接的拷贝赋值这里要注意跟拷贝构造区分拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。赋值运算符重载的特点1. 赋值运算符重载是一个运算符重载规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引用否则会传值传参会有拷贝。2. 有返回值建议写成当前类类型引用引用返回可以提高效率有返回值目的是为了支持连续赋值场景。3. 没有显示实现时编译器会自动生成一个默认赋值运算符重载默认赋值运算符重载行为跟默认拷贝构造函数类似对内置类型成员会完成值拷贝 / 浅拷贝一个字节一个字节的拷贝对自定义类型成员变量会调用它的赋值重载函数。4. 像 Date 这样的类成员变量全是内置类型且没有指向什么资源编译器自动生成的赋值运算符重载就可以完成需要的拷贝所以不需要我们显示实现赋值运算符重载。像 Stack 这样的类虽然也都是内置类型但是指向了资源编译器自动生成的赋值运算符重载完成的值拷贝 / 浅拷贝不符合我们的需求所以需要我们自己实现深拷贝对指向的资源也进行拷贝。像 MyQueue 这样的类型内部主要是自定义类型 Stack 成员编译器自动生成的赋值运算符重载会调用 Stack 的赋值运算符重载也不需要我们显示实现 MyQueue 的赋值运算符。这里还有一个技巧如果一个类显示实现了析构并释放资源那么就需要显示写赋值运算符重载否则就不需要。8.3 日期类实现#pragma once // Date class implementation #includeiostream #includeassert.h using namespace std; class Date { // friend functions friend ostream operator(ostream out, const Date d); friend istream operator(istream in, Date d); public: Date(int year 1900, int month 1, int day 1); void Print()const; int GetMonthDay(int year, int month) { assert(month 0 month 13); static int monthDayArray[13] { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; if (month 2 ((year % 4 0 year % 100 ! 0) || year % 400 0)) { return 29; } else { return monthDayArray[month]; } } bool CheckDate(); bool operator(const Date d)const; bool operator(const Date d)const; bool operator(const Date d)const; bool operator(const Date d)const; bool operator(const Date d)const; bool operator!(const Date d)const; // d1 value Date operator(int day); Date operator(int day)const; // d1 - value Date operator-(int day); Date operator-(int day)const; // d1-d2 int operator-(const Date d)const; // d1 - d1.operator() Date operator(); // d1 - d1.operator(0) Date operator(int); // --d1 Date operator--(); // d1-- Date operator--(int); private: int _year; int _month; int _day; }; // function declaration ostream operator(ostream out, const Date d); istream operator(istream in, Date d);#includetest.h bool Date::CheckDate() { if (_month 1 || _month12 || _day1 || _dayGetMonthDay(_year, _month)) { return false; } else { return true; } } Date::Date(int year, int month, int day) { _year year; _month month; _day day; if (!CheckDate()) cout 日期非法 endl; } void Date::Print()const { cout _year - _month - _day endl; } // d1 d2 bool Date::operator(const Date d)const { if (_year d._year) { return true; } else if (_year d._year) { if (_month d._month) { return true; } else if (_month d._month) { return _day d._day; } } return false; } // d1 d2 bool Date::operator(const Date d)const { return *this d || *this d; } bool Date::operator(const Date d)const { return !(*this d); } bool Date::operator(const Date d)const { return !(*this d); } bool Date::operator(const Date d)const { return _year d._year _month d._month _day d._day; } bool Date::operator!(const Date d)const { return !(*this d); } Date Date::operator(int day) { if (day 0) { return *this - -day; } _day day; while (_day GetMonthDay(_year, _month)) { _day - GetMonthDay(_year, _month); _month; if (_month 13) { _year; _month 1; } } return *this; } Date Date::operator(int day)const { Date tmp *this; tmp day; return tmp; } Date Date::operator-(int day) { if (day0) { return *this -day; } _day - day; while (_day 0) { --_month; if (_month 0) { _month 12; _year--; } _day GetMonthDay(_year, _month); } return *this; } Date Date::operator-(int day)const { Date tmp *this; tmp - day; return tmp; } // d1 - d2, return days between two dates int Date::operator-(const Date d)const { int flag 1; Date max *this; Date min d; if (*this d) { max d; min *this; flag -1; } int days 0; while (min max) { days; min 1; } return days * flag; } // d1 - d1.operator(), pre-increment Date Date::operator() { *this 1; return *this; } // d1 - d1.operator(0), post-increment Date Date::operator(int) { Date tmp *this; *this 1; return tmp; } // --d1 Date Date::operator--() { *this - 1; return *this; } // d1-- Date Date::operator--(int) { Date tmp *this; *this - 1; return tmp; } // output stream ostream operator(ostream out, const Date d) { out d._year - d._month - d._day endl; return out; } // input stream istream operator(istream in, Date d) { in d._year d._month d._day; if (!d.CheckDate()) { cout 日期非法 endl; } return in; }9. 取地址运算符重载9.1 const 成员函数^ 把 const 修饰的成员函数称之为 const 成员函数const 修饰成员函数放到成员函数参数列表的后面。^ cosnt 实际修饰该成员函数隐含的 this 指针表明在该成员函数中不能对类的任何成员进行修改。^ 很多人误认为 const 修饰函数本身其实不然。C类的成员函数底层都会隐藏一个默认参数 this 指针用来指向当前调用函数的对象。普通成员函数的 this 指针Student* const this 指针地址不可改指向的内容可改const 成员函数的 this 指针const Student* const this 指针地址指向内容都不可修改。这就是 const 成员函数无法修改成员变量的本质原因this 指针指向的对象被 const 修饰变成只读状态。class 类名 { public: // const成员函数声明 返回值类型 函数名(参数列表) const; }; // const成员函数类外实现 返回值类型 类名::函数名(参数列表) const { // 函数体 }9.2 取地址运算符重载^ 取地址运算符只能以类成员函数形式重载不支持全局函数重载无参数返回值为当前类指针类型。class 类名 { public: // 重载普通取地址运算符 类名* operator() { // 自定义返回地址逻辑 return this; } };^ 和 const 成员函数特性一致取地址运算符也可以重载 const 版本适配 const 对象取地址场景构成函数重载class 类名 { public: // 非const对象取地址 类名* operator() { return this; } // const对象取地址 const 类名* operator() const { return this; } };10. 类型转换^ 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的临时对象再⽤这个临时对象拷⻉构造aa3 // 编译器遇到连续构造拷⻉构造-优化为直接构造 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; }11. static 成员^ 用 static 修饰的成员变量称之为静态成员变量静态成员变量一定要在类外进行初始化。^ 静态成员变量为所有类对象所共享不属于某个具体的对象不存在对象中存放在静态区。^ 用 static 修饰的成员函数称之为静态成员函数静态成员函数没有 this 指针。^ 静态成员函数可以访问其他的静态成员但是不能访问非静态的因为没有 this 指针。^ 非静态的成员函数可以访问任意的静态成员变量和静态成员函数。^ 突破类域就可以访问静态成员可以通过 类名静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。^ 静态成员也是类的成员受 publicprotectedprivate 访问限定符的限制。^ 静态成员变量不能在声明位置给缺省值初始化因为静态成员变量不属于对象不走构造函数初始化列表而类内给成员写的缺省值本质是给构造初始化列表用的。12. 友元^ 友元提供了一种突破类访问限定符封装的方式友元分为友元函数和友元类在函数声明或者类声明的前面加 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; }#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; };13. 内部类^ 如果一个类定义在另一个类的内部这就叫内部类。内部类是一个独立的类跟定义在全局相比它只是受外部类类域限制和访问限定符限制所以外部类定义的对象中不包含内部类。‘^ 内部类默认是外部类的友元类。^ 内部类本质也是一种封装当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; }; };14. 匿名对象^ 用 类型(实参定义出来的对象叫做匿名对象相比之前我们定义的 类型 对象名(实参 定义出来的叫有名对象。^ 匿名对象生命周期只在当前一行一般临时定义一个对象当前用一下即可就可以定义匿名对象。#include iostream using namespace std; class Person{ public: Person(){cout构造endl;} ~Person(){cout析构endl;} void say(){couthiendl;} }; int main(){ Person(); // 匿名对象本行结束立刻析构 Person().say(); return 0; }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; } 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; }
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进