前言大家好我是你们的 C 博主今天我们继续【从零开始学 C】专题的第四篇深入学习类和对象的进阶知识。上一篇我们已经了解了类和对象的基础概念这一篇我们要学习一些更重要的知识点初始化列表、类型转换、static 成员、友元、内部类、匿名对象、编译器优化。这些内容虽然看起来有点多但只要跟着我的节奏用大白话理解配合代码示例相信大家一定能轻松掌握目录前言一再探构造函数初始化列表1. 基础概念解释2. 代码示例【示例 1】必须使用初始化列表的场景【示例 2】初始化顺序的坑二类型转换1. 基础概念解释2. 代码示例【示例 1】隐式类型转换演示【示例 2】explicit 禁止隐式转换三static 成员1. 基础概念解释2. 代码示例【示例 1】统计创建了多少个对象【示例 2】静态成员函数没有 this 指针四友元1. 基础概念解释2. 代码示例【示例 1】友元函数【示例 2】友元类五内部类1. 基础概念解释2. 代码示例【示例 1】内部类基本用法六匿名对象1. 基础概念解释2. 代码示例【示例 1】匿名对象的生命周期七对象拷贝时的编译器优化1. 基础概念解释2. 代码示例【示例 1】编译器优化完整演示总结一再探构造函数初始化列表1. 基础概念解释大白话时间之前我们写构造函数时都是在函数体里面给成员变量赋值就像这样Date(int year, int month, int day) { _year year; // 这是赋值不是初始化 _month month; _day day; }但其实这叫赋值不叫初始化真正的初始化是在对象创建的时候就完成的就像你出生时就有了名字而不是出生后再取名字。初始化列表就是真正做初始化的地方格式是在构造函数后面加个冒号:然后用逗号分隔每个成员的初始化Date(int year, int month, int day) : _year(year) // 这才是真正的初始化 , _month(month) , _day(day) {}重要规则✅必须用初始化列表的三种情况引用成员、const 成员、没有默认构造的自定义类型成员✅ 成员变量的初始化顺序是按声明顺序不是按初始化列表的顺序✅ 即使你不写初始化列表编译器也会自动生成一个✅ C11 支持在声明时给缺省值这个缺省值就是给初始化列表用的2. 代码示例【示例 1】必须使用初始化列表的场景这个例子展示了哪些情况必须用初始化列表否则编译报错#include iostream using namespace std; // Time类没有默认构造函数 class Time { public: Time(int hour) : _hour(hour) { cout Time(int hour) 被调用 endl; } private: int _hour; }; class Date { public: // 引用、const成员、无默认构造的自定义类型必须在初始化列表初始化 Date(int x, int year 1, int month 1, int day 1) : _year(year) , _month(month) , _day(day) , _t(12) // Time没有默认构造必须在这里初始化 , _ref(x) // 引用必须初始化 , _n(100) // const成员必须初始化 { // 如果不在上面初始化这里就会报错 // error: 没有合适的默认构造函数 // error: 必须初始化引用 // error: 必须初始化常量限定类型的对象 } void Print() const { cout _year - _month - _day endl; cout 引用值: _ref , const值: _n endl; } private: int _year; int _month; int _day; Time _t; // 没有默认构造的类类型 int _ref; // 引用成员 const int _n; // const成员 }; int main() { int i 666;//i的值是传给了int x中的x Date d1(i); d1.Print(); return 0; }运行结果Time(int hour) 被调用 1-1-1 引用值: 666, const值: 100【示例 2】初始化顺序的坑这个例子告诉你初始化顺序是按声明顺序不是按初始化列表顺序#include iostream using namespace std; class A { public: A(int a) : _a1(a) // 第二步初始化_a1值为1 , _a2(_a1) // 第一步初始化_a2此时_a1还是随机值 {} void Print() { cout _a1 _a1 , _a2 _a2 endl; } private: // 注意声明顺序是 _a2 先_a1 后 int _a2 2; // 先初始化_a2 int _a1 2; // 后初始化_a1 }; int main() { A aa(1); aa.Print(); // 猜猜输出什么答案_a1 1, _a2 2因为_a2用了缺省值 return 0; }划重点一定要让初始化列表的顺序和成员声明顺序保持一致避免踩坑二类型转换1. 基础概念解释大白话时间C 很 聪明有时候会自动帮你做类型转换。比如你写A aa 1;编译器会自动把1转换成 A 类型的对象这就叫隐式类型转换。原理是编译器先用1构造一个临时的 A 对象然后用这个临时对象拷贝构造aa。现代编译器会优化成直接构造。但有时候这种自动转换会带来问题所以 C 提供了explicit关键字加上 explicit 就禁止隐式转换2. 代码示例【示例 1】隐式类型转换演示#include iostream using namespace std; class A { public: // 不加explicit支持隐式类型转换 A(int a1) : _a1(a1) { cout A(int a1) 构造 endl; } // C11支持多参数隐式转换 A(int a1, int a2) : _a1(a1) , _a2(a2) {} void Print() { cout _a1 _a1 , _a2 _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()) { cout B(const A a) 构造 endl; } private: int _b 0; }; int main() { cout 单参数隐式转换 endl; A aa1 1; // 1 隐式转换成A对象 aa1.Print(); cout \n 引用绑定临时对象 endl; const A aa2 1; // 临时对象具有常性要用const引用 cout \n C11多参数隐式转换 endl; A aa3 {2, 3}; aa3.Print(); cout \n 类类型之间的转换 endl; B b aa3; // A对象隐式转换成B对象 return 0; }【示例 2】explicit 禁止隐式转换#include iostream using namespace std; class A { public: // 加上explicit禁止隐式类型转换 explicit A(int a1) : _a1(a1) {} void Print() { cout _a1 _a1 endl; } private: int _a1; }; int main() { // A aa1 1; // 编译报错不允许隐式转换 A aa1(1); // ✅ 显式构造可以 aa1.Print(); A aa2 A(2); // ✅ 显式构造后拷贝也可以 aa2.Print(); return 0; }建议构造函数尽量加上explicit避免意外的隐式转换带来 bug三static 成员1. 基础概念解释大白话时间普通成员变量是每个对象自己一份就像每个人都有自己的钱包。但static 静态成员是整个类共享一份就像班级的公共基金所有同学共用这一份钱。关键特性静态成员变量不属于某个对象存在静态区所有对象共享静态成员变量必须在类外初始化类里只是声明静态成员函数没有 this 指针所以不能访问非静态成员静态成员可以通过类名::成员或对象.成员访问静态成员也受访问限定符public/private限制2. 代码示例【示例 1】统计创建了多少个对象这个经典例子完美展示了 static 的用法#include iostream using namespace std; class A { public: // 构造函数 A() { _scount; // 每创建一个对象计数1 cout 构造函数当前对象数 _scount endl; } // 拷贝构造函数 A(const A t) { _scount; // 拷贝构造也要计数 cout 拷贝构造当前对象数 _scount endl; } // 析构函数 ~A() { --_scount; // 对象销毁计数-1 cout 析构函数当前对象数 _scount endl; } // 静态成员函数获取当前对象数 static int GetACount() { return _scount; } private: // 类里面只是声明 static int _scount; }; // ✅ 静态成员变量必须在类外初始化 int A::_scount 0; int main() { cout 初始对象数 A::GetACount() endl; // 通过类名访问 A a1, a2; A a3(a1); // 拷贝构造 cout \n创建3个对象后 endl; cout 通过类名访问 A::GetACount() endl; cout 通过对象访问 a1.GetACount() endl; // cout A::_scount endl; // 编译报错_scount是private的 return 0; }运行结果初始对象数0 构造函数当前对象数1 构造函数当前对象数2 拷贝构造当前对象数3 创建3个对象后 通过类名访问3 通过对象访问3 析构函数当前对象数2 析构函数当前对象数1 析构函数当前对象数0【示例 2】静态成员函数没有 this 指针#include iostream using namespace std; class Test { public: static void Func() { // cout _a endl; // ❌ 报错静态函数没有this指针不能访问非静态成员 cout _s _s endl; // ✅ 可以访问静态成员 } void NormalFunc() { cout _a _a endl; // ✅ 非静态函数可以访问普通成员 cout _s _s endl; // ✅ 非静态函数也可以访问静态成员 } private: int _a 10; // 普通成员变量 static int _s; // 静态成员变量 }; int Test::_s 20; int main() { Test::Func(); // 不需要对象直接通过类名调用静态函数 Test t; t.NormalFunc(); return 0; }四友元1. 基础概念解释大白话时间类的封装就像你家的房子private 成员就是你卧室里的东西外人不能随便进。但你最好的朋友来了你肯定会给他开门让他随便参观。友元就是 C 给你的 开门权限友元可以突破访问限定符直接访问类的私有成员。友元分两种友元函数一个函数成为某个类的朋友友元类整个类都成为朋友注意事项友元是单向的A 是 B 的朋友 ≠ B 是 A 的朋友友元不能传递A 是 B 的朋友B 是 C 的朋友 ≠ A 是 C 的朋友友元会破坏封装不要滥用2. 代码示例【示例 1】友元函数#include iostream using namespace std; // 前置声明告诉编译器B类存在 class B; class A { // 声明func函数是A的友元 friend void func(const A aa, const B bb); private: int _a1 1; int _a2 2; }; class B { // 声明func函数也是B的友元 friend void func(const A aa, const B bb); private: int _b1 3; int _b2 4; }; // 这个函数同时是A和B的友元可以访问两个类的私有成员 void func(const A aa, const B bb) { cout 访问A的私有成员 aa._a1 , aa._a2 endl; cout 访问B的私有成员 bb._b1 , bb._b2 endl; } int main() { A aa; B bb; func(aa, bb); return 0; }运行结果访问A的私有成员1, 2 访问B的私有成员3, 4【示例 2】友元类#include iostream using namespace std; class A { // 声明B类是A的友元B的所有成员函数都能访问A的私有成员 friend class B; private: int _a1 1; int _a2 2; }; class B { public: void func1(const A aa) { cout func1访问A的私有 aa._a1 endl; cout B自己的成员 _b1 endl; } void func2(const A aa) { cout func2访问A的私有 aa._a2 endl; cout B自己的成员 _b2 endl; } private: int _b1 3; int _b2 4; }; int main() { A aa; B bb; bb.func1(aa); bb.func2(aa); // 注意友元是单向的A不能访问B的私有成员 return 0; }五内部类1. 基础概念解释大白话时间如果一个类定义在另一个类的里面里面那个就叫内部类。就像你家房子里还有个小房间。关键特性内部类是独立的类外部类的对象大小不包含内部类内部类默认就是外部类的友元可以直接访问外部类的私有成员内部类受外部类的类域和访问限定符限制如果把内部类放在 private 里那它就是外部类的 专属工具人外面用不了2. 代码示例【示例 1】内部类基本用法#include iostream using namespace std; class A { private: static int _k; // 静态成员 int _h 1; // 普通成员 public: // B是A的内部类 class B { public: void foo(const A a) { cout 访问A的静态成员 _k endl; // ✅ 直接访问 cout 访问A的普通成员 a._h endl; // ✅ 内部类默认是友元 } private: int _b1 10; }; }; // 静态成员初始化 int A::_k 666; int main() { cout A类的大小 sizeof(A) endl; // 输出4不包含B // 内部类的定义方式外部类::内部类 A::B b; A aa; b.foo(aa); return 0; }运行结果A类的大小4 访问A的静态成员666 访问A的普通成员1六匿名对象1. 基础概念解释大白话时间我们平时定义对象都是A aa(1);这叫有名对象生命周期是整个作用域。但有时候我们只需要用一下这个对象用完就扔那可以不用给它起名字这就是匿名对象A(1);特点匿名对象的生命周期只有当前这一行执行完就析构格式类名(参数)适合临时用一下的场景非常方便2. 代码示例【示例 1】匿名对象的生命周期#include iostream using namespace std; class A { public: A(int a 0) : _a(a) { cout A(int a) 构造a _a endl; } ~A() { cout ~A() 析构a _a endl; } private: int _a; }; class Solution { public: int Sum_Solution(int n) { return n * (n 1) / 2; } }; int main() { cout 有名对象 endl; A aa1(1); // 有名对象生命周期到main结束 cout \n 匿名对象 endl; A(); // 匿名对象这一行结束就析构 A(2); // 另一个匿名对象 cout \n 匿名对象的妙用 endl; // 只用一次的函数不用创建对象直接用匿名对象调用 cout 12...10 Solution().Sum_Solution(10) endl; cout \n main函数结束 endl; return 0; }运行结果注意析构顺序 有名对象 A(int a) 构造a 1 匿名对象 A(int a) 构造a 0 ~A() 析构a 0 A(int a) 构造a 2 ~A() 析构a 2 匿名对象的妙用 12...10 55 main函数结束 ~A() 析构a 1看到了吗匿名对象用完马上就析构了而有名对象要等到作用域结束七对象拷贝时的编译器优化1. 基础概念解释大白话时间现代编译器都很 聪明为了提高效率会在不影响结果的前提下帮我们省略一些不必要的拷贝构造。比如你写A aa 1;理论上应该是用 1 构造一个临时 A 对象用临时对象拷贝构造 aa但编译器会优化成直接用 1 构造 aa省掉了拷贝这一步常见优化场景传值传参时连续的构造 拷贝构造 → 优化为直接构造传值返回时连续的拷贝构造 拷贝构造 → 优化为一次拷贝不同编译器优化程度不同VS2022 比 VS2019 优化更激进2. 代码示例【示例 1】编译器优化完整演示#include iostream 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() { cout 场景1传值传参 endl; A aa1; f1(aa1); // 拷贝构造无法优化 cout endl; cout 场景2隐式类型转换优化 endl; f1(1); // 构造拷贝构造 → 优化为直接构造 cout endl; cout 场景3匿名对象传参优化 endl; f1(A(2)); // 构造拷贝构造 → 优化为直接构造 cout endl; cout 场景4传值返回 endl; f2(); // 不同编译器优化程度不同 cout endl; cout 场景5返回值接收优化 endl; A aa2 f2(); // 连续拷贝构造 → 优化 cout endl; cout 场景6赋值无法优化 endl; aa1 f2(); // 赋值重载无法优化 cout endl; return 0; }VS2022 下的运行结果优化后 场景1传值传参 A(int a) 构造 A(const A aa) 拷贝构造 ~A() 析构 场景2隐式类型转换优化 A(int a) 构造 // 直接构造没有拷贝 ~A() 析构 场景3匿名对象传参优化 A(int a) 构造 // 直接构造没有拷贝 ~A() 析构 场景4传值返回 A(int a) 构造 ~A() 析构 场景5返回值接收优化 A(int a) 构造 // VS2022直接优化成一次构造 ~A() 析构 场景6赋值无法优化 A(int a) 构造 A(const A aa) 拷贝构造 A operator 赋值重载 ~A() 析构 ~A() 析构 ~A() 析构 ~A() 析构划重点编译器优化是 锦上添花我们写代码时还是要按正常逻辑写不要依赖优化总结今天我们学习了类和对象的 7 个重要知识点知识点核心要点初始化列表真正的初始化引用 /const/ 无默认构造必须用按声明顺序初始化类型转换隐式转换很方便但有风险用 explicit 禁止不需要的转换static 成员全类共享类外初始化静态函数无 this 指针友元突破封装的后门单向、不传递慎用内部类定义在类里面默认是友元受类域限制匿名对象生命周期只有一行用完就扔临时用很方便编译器优化连续的构造 拷贝会优化我们按正常逻辑写就行这些都是 C 面向对象的核心知识一定要好好理解下一篇我们会学习敬请期待学习 C 一定要多动手写代码光看是学不会的把今天的例子都自己敲一遍运行看看你会理解得更深刻。有问题欢迎在评论区留言哦【从零开始学 C】系列文章第一篇C 入门基础第二篇类和对象上第三篇类和对象中第四篇类和对象下← 你在这里第五篇努力创作中尽请期待