)
作为 C 初学者在掌握了类和对象的基础用法后下篇的知识点会显得 “有点绕”—— 初始化列表、static、友元这些概念总让人分不清今天就用能看懂的语言把这些知识点掰开揉碎学完既能理解又能上手写代码复习也一目了然 超贴心目录1. 构造函数进阶别再把 “赋初值” 当 “初始化” 啦1.1 构造函数体赋值为啥说它不是真・初始化1.2 初始化列表新手必避的坑都在这1.3 explicit 关键字禁止 “偷偷” 的类型转换2.⚡ static 成员所有对象 “共享” 的专属道具2.1 静态成员变量类内声明、类外初始化是铁律2.2 静态成员函数没有 this 指针的 “特殊选手”2.3 新手必看静态成员常见问题解答3. 友元突破封装的 “特殊朋友”慎用3.1 友元函数为啥重载 和 必须用它3.2 友元类单向的 “好友权限”4. 内部类藏在别人家里的 “独立打工人”4.1 内部类的核心特性新手别搞混这几点4.2 实战示例一看就懂的内部类用法 再聊封装从现实世界看懂 C 的核心思想 专属练习边练边巩固不怕忘1. 构造函数进阶别再把 “赋初值” 当 “初始化” 啦对于初学者来说构造函数最开始学的是 “给成员变量赋值”但其实这里藏着一个容易混淆的点构造函数体里的赋值不是真正的初始化。1.1 构造函数体赋值为啥说它不是真・初始化写构造函数时大概率会这么写classDate{public:// 新手常见写法构造函数体赋值Date(intyear,intmonth,intday){_yearyear;// 这是赋初值不是初始化_monthmonth;_dayday;}private:int_year;int_month;int_day;};为啥说这不是 “初始化”初始化变量创建时只赋一次值是“从无到有”的过程赋初值变量已经创建默认初始化了只是修改它的值是 “从有到改” 的过程。简单记构造函数体里的代码是对象创建好之后才执行的所以只能改值不能算 “初始化”1.2 初始化列表必避的坑都在这既然构造函数体赋值不是真初始化那 C 里 “真正的初始化” 靠啥——初始化列表。对初学者来说记住语法和规则就能少踩 90% 的坑。classDate{public:// 初始化列表冒号开头成员变量用括号赋值Date(intyear,intmonth,intday):_year(year)// 这才是真正的初始化,_month(month),_day(day){}// 函数体可以空着private:int_year;int_month;int_day;};必记的 4 条规则记牢不踩坑1.必须用初始化列表的 3 种情况最容易忘引用成员变量引用必须在创建时初始化没法先创建再赋值const 成员变量const 变量一旦创建就不能改必须初始化自定义类型成员且该类无默认构造函数比如 A 类只有带参数的构造函数那么包含 A 类的 B 类必须用初始化列表给 A 类成员赋值。友好示例// 自定义类A只有带参数的构造函数classA{public:A(inta):_a(a){}// 没有默认构造函数private:int_a;};// 类B包含A类成员、引用、constclassB{public:// 新手注意这3个成员必须用初始化列表初始化B(inta,intref):_aobj(a)// A类无默认构造必须初始化,_ref(ref)// 引用必须初始化,_n(10)// const必须初始化{}private:A _aobj;// 自定义类型成员int_ref;// 引用成员constint_n;// const成员};2.初始化顺序≠书写顺序高频坑成员变量的初始化顺序只看它在类里 “声明的顺序”和初始化列表里写的先后无关classA{public:// 新手以为先初始化_a1再用_a1初始化_a2A(inta):_a1(a),_a2(_a1){}voidPrint(){cout_a1 _a2endl;}private:int_a2;// 先声明先初始化_a2int_a1;// 后声明后初始化_a1};intmain(){Aaa(1);aa.Print();// 输出1 随机值// 原因初始化_a2时_a1还没初始化所以_a2是随机值}避坑写初始化列表时按成员变量在类里的声明顺序写就不会错3.所有成员都会走初始化列表哪怕你没写初始化列表编译器也会自动帮你走一遍 ——比如自定义类型成员会先调用它的默认构造函数。所以新手尽量都用初始化列表代码更高效4.每个成员只能初始化一次初始化列表里一个成员只能出现一次符合 “初始化只做一次” 的规则1.3 explicit 关键字禁止 “偷偷” 的类型转换classDate{public:Date(intyear):_year(year){}private:int_year1900;};intmain(){Dated1(2024);d12025;// 新手疑惑为啥int能直接赋值给Date对象return0;}这是因为单参数构造函数默认支持隐式类型转换编译器会先用2025 创建一个临时的 Date 对象再把这个临时对象赋值给 d1。这种 “偷偷” 的转换容易让代码出问题也不利于新手理解explicit 的作用禁止隐式转换只需要记住在单参数构造函数前加explicit就能阻止这种隐式转换。classDate{public:explicitDate(intyear):_year(year){}// 加explicitprivate:int_year1900;};intmain(){Dated1(2024);// d1 2025; // 编译报错新手再也不用担心误写return0;}补充多参数构造函数如果除了第一个参数其他都有默认值也会触发隐式转换同样可以用explicit禁止。2. ⚡ static 成员所有对象 “共享” 的专属道具新手可以把 static 成员理解为属于 “整个类” 的道具不是某个对象的—— 不管创建多少个对象这个道具只有一份所有对象都能用。2.1 静态成员变量类内声明、类外初始化是铁律最容易犯的错只在类里声明静态成员变量忘了类外初始化。classStudent{public:Student(string name):_name(name){_totalCount;// 每创建一个学生总数1}private:string _name;// 每个学生的专属名字非静态staticint_totalCount;// 静态成员所有学生的总数类内声明};// 静态成员变量类外初始化必写intStudent::_totalCount0;// 初始化时不加static理解_totalCount是所有 Student 对象共享的哪怕创建 100 个学生_totalCount只有一份值会跟着对象创建累加。2.2 静态成员函数没有 this 指针的 “特殊选手”静态成员函数是为了操作静态成员变量而生的要记住它没有隐藏的this指针。.classStudent{public:Student(string name):_name(name){_totalCount;}// 静态成员函数获取学生总数staticintGetTotalCount(){// 能访问静态成员变量return_totalCount;// 不能访问非静态成员变量比如_name因为没有this指针// return _name; // 编译报错}private:string _name;staticint_totalCount;};intStudent::_totalCount0;intmain(){// 新手注意静态成员函数可以直接用“类名::函数名”调用不用创建对象cout初始学生数Student::GetTotalCount()endl;// 输出0Students1(小明),s2(小红);cout当前学生数Student::GetTotalCount()endl;// 输出2return0;}2.3 必看静态成员常见问题解答1.静态成员函数能调用非静态成员函数吗不能记非静态成员函数需要 this 指针静态成员函数没有 this所以调不了。2.非静态成员函数能调用静态成员函数吗能非静态成员函数有 this 指针调用静态成员函数时不用传任何额外参数直接调就行。3.静态成员变量可以设置默认值吗C11 及以后静态成员变量如果是 const 类型可以在类内直接初始化比如static const int _maxNum 100;但非 const 的静态成员变量必须类外初始化。3. 友元突破封装的 “特殊朋友”慎用C 的封装是 “把内部细节藏起来”但有时候需要让外部的函数 / 类访问私有成员 —— 这时候就需要 “友元”。对新手来说友元是 “特例”能少用就少用因为会破坏封装。3.1 友元函数为啥重载 和 必须用它学输入输出重载时会发现一个问题cout输出流要作为第一个参数但类的成员函数第一个参数是 this 指针根本没法重载。这时候就要用友元函数。#includeiostreamusingnamespacestd;classDate{// 声明友元函数加friend关键字不受访问限定符限制friendostreamoperator(ostream_cout,constDated);friendistreamoperator(istream_cin,Dated);public:Date(intyear1900,intmonth1,intday1):_year(year),_month(month),_day(day){}private:int_year;int_month;int_day;};// 友元函数全局函数能直接访问Date的私有成员ostreamoperator(ostream_cout,constDated){_coutd._year年d._month月d._day日;return_cout;// 支持链式输出比如cout d1 d2}istreamoperator(istream_cin,Dated){cout请输入年、月、日;_cind._yeard._monthd._day;return_cin;// 支持链式输入比如cin d1 d2}intmain(){Date d;cind;// 调用重载的coutdendl;// 调用重载的return0;}总结友元函数的特点1.是全局函数不属于任何类2.类内声明加friend可以写在 public/private 里都生效3.能直接访问私有成员不用 get/set 函数4.调用方式和普通函数一样3.2 友元类单向的 “好友权限”友元类就是 “整个类都是另一个类的朋友”要记住友元关系是 “单向的、不能传递的”。classTime{// 声明Date是Time的友元类Date能访问Time的私有成员friendclassDate;public:Time(inthour0,intminute0,intsecond0):_hour(hour),_minute(minute),_second(second){}private:int_hour;int_minute;int_second;};classDate{public:voidSetTime(inthour,intminute,intsecond){// Date能直接访问Time的私有成员因为是友元_t._hourhour;_t._minuteminute;_t._secondsecond;}private:int_year;int_month;int_day;Time _t;};intmain(){Date d;d.SetTime(10,30,0);// 成功设置Time的私有成员// Time不能访问Date的私有成员比如_year会编译报错return0;}避坑单向性A 是 B 的友元 ≠ B 是 A 的友元不可传递A 是 B 的友元B 是 C 的友元 ≠ A 是 C 的友元友元类的所有成员函数都能访问被友元类的私有成员4. 内部类藏在别人家里的 “独立打工人”内部类就是 “一个类定义在另一个类里面”容易误以为它是外部类的成员但其实它是独立的类只是 “住在外部类家里”4.1 内部类的核心特性1**.内部类是外部类的 “天生友元”**能访问外部类的所有成员私有 / 公有2.内部类独立外部类不能访问内部类的成员sizeof (外部类) 不包含内部类3.访问方式外部类名::内部类名比如A::B4.内部类可以直接访问外部类的 static 成员不用外部类的对象 / 类名。classA{private:staticint_k;// 静态成员int_h10;// 非静态成员public:// 内部类B住在A里面是A的友元classB{public:voidShowA(constAa){// 能访问A的static成员直接访问coutA的静态成员_k_kendl;// 能访问A的非静态成员需要A的对象coutA的非静态成员_ha._hendl;}voidSetB(intb){_bb;}// 内部类自己的成员private:int_b;// 内部类的私有成员A不能访问};};// 外部类的static成员初始化intA::_k20;intmain(){// 新手注意创建内部类对象的方式A::B b;A a;b.ShowA(a);// 输出20 10// sizeof(A)只算A自己的成员和B无关coutsizeof(A)sizeof(A)endl;// 输出4只算_hreturn0;}5. 再聊封装从现实世界看懂 C 的核心思想对我们来说封装不用记复杂的定义用现实例子就能懂现实世界你用手机时只需要点屏幕、按按键对外的接口不用知道手机内部的电路板、芯片怎么工作内部细节C 世界类就是 “手机”成员函数是 “按键 / 屏幕接口”成员变量是 “电路板内部细节”访问限定符public/private就是 “保护内部细节不被乱改”。6. 新手专属练习边练边巩固不怕忘1.统计班级学生人数用 static 成员变量统计创建了多少个 Student 对象销毁时计数减 12.日期类完善给 Date 类加初始化列表用 explicit 禁止隐式转换重载 输出 “xxxx 年 xx 月 xx 日”3.友元练习写一个 Time 类和 Date 类让 Date 成为 Time 的友元实现 Date 修改 Time 的小时 / 分钟4.内部类练习在 Person 类里定义内部类 IDCard实现 IDCard 访问 Person 的姓名和身份证号。