C++4(类与对象下篇)
作为 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){_year=year;// 这是赋初值,不是初始化_month=month;_day=day;}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<<" "<<_a2<<endl;}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_year=1900;};intmain(){Dated1(2024);d1=2025;// 新手疑惑:为啥int能直接赋值给Date对象?return0;}这是因为单参数构造函数默认支持隐式类型转换:编译器会先用2025 创建一个临时的 Date 对象,再把这个临时对象赋值给 d1。这种 “偷偷” 的转换,容易让代码出问题,也不利于新手理解
explicit 的作用:禁止隐式转换
只需要记住:在单参数构造函数前加explicit,就能阻止这种隐式转换。
classDate{public:explicitDate(intyear):_year(year){}// 加explicitprivate:int_year=1900;};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::_totalCount=0;// 初始化时不加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::_totalCount=0;intmain(){// 新手注意:静态成员函数可以直接用“类名::函数名”调用,不用创建对象cout<<"初始学生数:"<<Student::GetTotalCount()<<endl;// 输出0Students1("小明"),s2("小红");cout<<"当前学生数:"<<Student::GetTotalCount()<<endl;// 输出2return0;}2.3 必看:静态成员常见问题解答
1.静态成员函数能调用非静态成员函数吗?
不能!记:非静态成员函数需要 this 指针,静态成员函数没有 this,所以调不了。
2.非静态成员函数能调用静态成员函数吗?
能!非静态成员函数有 this 指针,调用静态成员函数时,不用传任何额外参数,直接调就行。
3.静态成员变量可以设置默认值吗?
C++11 及以后,静态成员变量如果是 const 类型,可以在类内直接初始化(比如static const int _maxNum = 100;),但非 const 的静态成员变量,必须类外初始化。
3. 🤝 友元:突破封装的 “特殊朋友”(慎用!)
C++ 的封装是 “把内部细节藏起来”,但有时候需要让外部的函数 / 类访问私有成员 —— 这时候就需要 “友元”。对新手来说,友元是 “特例”,能少用就少用,因为会破坏封装。
3.1 友元函数:为啥重载 <<和>> 必须用它?
学输入输出重载时,会发现一个问题:cout(输出流)要作为第一个参数,但类的成员函数第一个参数是 this 指针,根本没法重载。这时候就要用友元函数。
#include<iostream>usingnamespacestd;classDate{// 声明友元函数:加friend关键字,不受访问限定符限制friendostream&operator<<(ostream&_cout,constDate&d);friendistream&operator>>(istream&_cin,Date&d);public:Date(intyear=1900,intmonth=1,intday=1):_year(year),_month(month),_day(day){}private:int_year;int_month;int_day;};// 友元函数:全局函数,能直接访问Date的私有成员ostream&operator<<(ostream&_cout,constDate&d){_cout<<d._year<<"年"<<d._month<<"月"<<d._day<<"日";return_cout;// 支持链式输出(比如cout << d1 << d2)}istream&operator>>(istream&_cin,Date&d){cout<<"请输入年、月、日:";_cin>>d._year>>d._month>>d._day;return_cin;// 支持链式输入(比如cin >> d1 >> d2)}intmain(){Date d;cin>>d;// 调用重载的>>cout<<d<<endl;// 调用重载的<<return0;}总结友元函数的特点:
1.是全局函数,不属于任何类;
2.类内声明加friend,可以写在 public/private 里,都生效;
3.能直接访问私有成员,不用 get/set 函数;
4.调用方式和普通函数一样
3.2 友元类:单向的 “好友权限”
友元类就是 “整个类都是另一个类的朋友”,要记住:友元关系是 “单向的、不能传递的”。
classTime{// 声明Date是Time的友元类:Date能访问Time的私有成员friendclassDate;public:Time(inthour=0,intminute=0,intsecond=0):_hour(hour),_minute(minute),_second(second){}private:int_hour;int_minute;int_second;};classDate{public:voidSetTime(inthour,intminute,intsecond){// Date能直接访问Time的私有成员(因为是友元)_t._hour=hour;_t._minute=minute;_t._second=second;}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::B;
4.内部类可以直接访问外部类的 static 成员,不用外部类的对象 / 类名。
classA{private:staticint_k;// 静态成员int_h=10;// 非静态成员public:// 内部类B:住在A里面,是A的友元classB{public:voidShowA(constA&a){// 能访问A的static成员(直接访问)cout<<"A的静态成员_k:"<<_k<<endl;// 能访问A的非静态成员(需要A的对象)cout<<"A的非静态成员_h:"<<a._h<<endl;}voidSetB(intb){_b=b;}// 内部类自己的成员private:int_b;// 内部类的私有成员,A不能访问};};// 外部类的static成员初始化intA::_k=20;intmain(){// 新手注意:创建内部类对象的方式A::B b;A a;b.ShowA(a);// 输出:20 10// sizeof(A)只算A自己的成员,和B无关cout<<"sizeof(A):"<<sizeof(A)<<endl;// 输出4(只算_h)return0;}5. 💡 再聊封装:从现实世界看懂 C++ 的核心思想
对我们来说,封装不用记复杂的定义,用现实例子就能懂:
现实世界:你用手机时,只需要点屏幕、按按键(对外的接口),不用知道手机内部的电路板、芯片怎么工作(内部细节);
C++ 世界:类就是 “手机”,成员函数是 “按键 / 屏幕(接口)”,成员变量是 “电路板(内部细节)”,访问限定符(public/private)就是 “保护内部细节不被乱改”。
6. 📝 新手专属练习:边练边巩固,不怕忘
1.统计班级学生人数:用 static 成员变量统计创建了多少个 Student 对象,销毁时计数减 1;
2.日期类完善:给 Date 类加初始化列表,用 explicit 禁止隐式转换,重载 <<输出 “xxxx 年 xx 月 xx 日”;
3.友元练习:写一个 Time 类和 Date 类,让 Date 成为 Time 的友元,实现 Date 修改 Time 的小时 / 分钟;
4.内部类练习:在 Person 类里定义内部类 IDCard,实现 IDCard 访问 Person 的姓名和身份证号。
