0.1概要&序論
这里是白杨,本篇是类和对象的完结篇——从命名空间一路走到这里,C++前期最大的BOSS终于要被我们彻底打完了!本文会讲清楚友元(友元函数、友元类)与内部类(概念、特性),然后一起再次理解封装、再次理解面向对象。内容干货满满!。让我们现在开始吧!
一,友元
1.0为什么需要友元?
在正式讲友元之前,我们先解决一个历史遗留问题——流插入<<和流提取>>的重载。
之前讲运算符重载的时候我们留了个伏笔:<<和>>如果重载成成员函数,会出大问题:
class Date { public: ostream& operator<<(ostream& out) // 成员函数版本 { out << _year; return out; } private: int _year; }; int main() { Date d; d << cout; // 调用形式变成了这样,谁看谁迷糊 return 0; }因为成员函数的第一个参数永远是隐藏的this指针,d会被强行塞到左操作数的位置,cout只能去当右操作数——调用形式变成了d << cout,完全反人类。
所以<<和>>必须重载成全局函数。但是……全局函数访问不了类的私有成员啊!
这就是友元登场的时刻。
1.1友元函数
1.1.1友元函数的定义
友元函数是在类中用friend关键字声明的函数,它不是类的成员函数,但可以访问类的私有成员和保护成员。
class Date { friend ostream& operator<<(ostream& out, const Date& d); public: Date(int year = 2026, int month = 9, int day = 22) : _year(year) , _month(month) , _day(day) {} private: int _year; int _month; int _day; }; ostream& operator<<(ostream& out, const Date& d) { out << d._year << "年" << d._month << "月" << d._day << "日"; return out; }这里稍微解释一下:
返回类型为什么是
ostream&?是为了支持链式调用:cout << d1 << d2 << endl;每次返回的还是out本身,可以继续往下插入。如果返回void,就只能输出一次了。friend声明写在类的哪个位置?任意位置都行,不受public/private限制——因为它本来就不是成员,访问限定符管不着它。
小贴士:少数情况:
如图,我们有一个函数同时是A类和B类的友元函数,但是基于编译器的向上查找原则,编译器运行到A时,在查找不到B类型就会报错,所以什么必须在整个程序的开头添加一个B的类型声明:class B;
1.1.2友元函数的特点
- 友元函数可以访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰(它不是成员函数,没有this指针)
- 友元函数可以在类中任意位置声明,不受访问限定符限制
- 一个函数可以是多个类的友元(比如
operator<<可以同时声明为两个类的友元,同时访问两边的私有成员) - 友元函数的调用和普通函数完全一样
1.2友元类
1.2.1友元类的定义
如果一个类想访问另一个类的私有成员,也可以把整个类声明为友元:
class Time { friend class Date; // 声明Date是Time的友元类 public: Time(int hour = 0, int minute = 0) : _hour(hour) , _minute(minute) {} private: int _hour; int _minute; }; class Date { public: void SetTimeOfDay(int hour, int minute) { _t._hour = hour; // Date可以直接访问Time的私有成员 _t._minute = minute; } private: Time _t; };1.2.2友元类的三大铁律
先说结论:
友元关系是单向的、不能传递的、不能继承的。
- 单向:声明B是A的友元,B可以访问A的私有成员;但A不能访问B的私有成员。("我把你当朋友,你未必把我当朋友")
- 不能传递:A是B的友元,B是C的友元,但A不是C的友元。
- 不能继承:友元关系不会被派生类继承(继承是什么?先别急,下一篇就是它!)。
1.2.3友元的利弊
友元提供了一种便利,但破坏了封装,增加了类之间的耦合。
可以这么理解:封装是类给自己家上了锁,友元就是类亲手配出去的一把钥匙——钥匙当然可以配,但配得越多,家里越不安全。
原则:
能用公有接口解决,就不要用友元。友元应当是"最后的便利手段",而不是日常操作。
二,内部类
2.1概念
2.1.1内部类的定义
如果一个类定义在另一个类的内部,这个类就叫内部类(也叫嵌套类):
class A { public: class B // B就是内部类 { public: void Print() { cout << "我是内部类" << endl; } private: int _b; }; private: int _a; };2.1.2内部类与外部类的关系
先说结论:
内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。
两个要点:
- 内部类可以访问外部类的私有成员(从访问权限上说,内部类相当于外部类的"天然友元"),其中访问static成员不需要外部类的对象/类名,访问非静态成员需要借助外部类的对象/指针
- 外部类不能访问内部类的私有成员(“我可以进你家,你进不了我家”)
2.2特性
- 内部类可以定义在外部类的public、protected、private任意区域。定义在private/protected时,外部类之外无法使用
A::B——这正是"把实现细节藏起来"的手段。 - 内部类是独立的类,不占外部类对象的空间:
sizeof(外部类)和内部类没有任何关系。 - 内部类可以直接访问外部类的static成员,不需要外部类的对象/类名。
- 外部类不能访问内部类的私有成员。
- 内部类可以像普通类一样,有自己的成员、构造函数,甚至可以再嵌套一个类。
2.2.1sizeof验证
class A { public: class B { int _b[100]; // 内部类B有400字节 }; private: int _a; }; int main() { cout << sizeof(A) << endl; // 4:内部类B完全不影响外部类A的大小 return 0; }再次验证:内部类是独立的类,不占外部类对象的空间:sizeof(外部类)和内部类没有任何关系。
2.2.2内部类的一个实用场景
内部类最常见的用途,是把只有外部类才用得上的辅助类藏进private里:
class Stack { private: class Node // 只有Stack自己用的节点类,外界根本看不见 { public: Node(int val) : _val(val) , _next(nullptr) {} private: int _val; Node* _next; }; Node* _top = nullptr; size_t _size = 0; };外界连Stack::Node这个名字都访问不到,实现细节被彻底封死——这不就是封装思想的完美体现吗?
三,再次理解封装
友元和内部类都讲完了,现在我们回头再看"封装"这两个字,感受应该完全不一样了。
3.1封装是什么
封装 = 把数据 + 操作数据的方法打包在一起,再用访问限定符决定哪些能见人、哪些不能。
3.2封装到底封住了什么
以栈为例:
class Stack { public: void Push(int x); void Pop(); int Top(); private: int* _a; size_t _top; size_t _capacity; };使用者只能通过Push/Pop/Top这些接口操作栈,_top、_capacity这些内部状态被藏了起来。好处是什么?
- 使用者不用关心内部是怎么实现的(数组?链表?),只关心接口
- 内部实现随便改,只要接口不变,使用者的代码就不用动
- 内部状态不会被误操作(使用者没法直接把
_top改成-1)
3.3友元与封装的关系
友元是封装的例外,但这个例外的钥匙捏在类自己手里——只有类自己声明谁是友元,谁才能进来。
所以友元并没有打破"封装由类自己掌控"这个根本原则,它更像是类在权衡便利性之后,主动开的一扇小门。
四,再次理解面向对象
4.1从面向过程到面向对象
我们用"洗衣服"来对比两种思想:
- 面向过程:拿盆 → 放水 → 放衣服 → 倒洗衣粉 → 搓 → 拧干 → 晾。关注的是步骤(函数)
- 面向对象:人、衣服、洗衣机、洗衣粉。关注的是对象,以及对象之间的交互(人把衣服放进洗衣机,洗衣机负责洗)
C语言是典型的面向过程:数据(结构体)和操作数据的函数是分离的。C++是面向对象:数据 + 操作数据的方法,打包成一个类——这就是我们这整个专栏前半部分一直在干的事。
4.2类与对象的关系
- 类:一种自定义类型,是"图纸"、“模板”,描述了对象长什么样、能干什么
- 对象:类的实例化,是"按图纸造出来的实物"
4.3面向对象的三大特性
- 封装:把数据和方法打包,控制访问权限(这一路我们讲完了!)
- 继承:让代码可以复用、可以在原有基础上扩展(未来会讲)
- 多态:同一接口,不同表现(同样未来会讲!)
总结
类和对象到这里就正式结束了。从命名空间一路走到这里,你已经跨过了C++前期最陡的一段坡。接下来,我们就要进入继承与多态的世界了——新的篇章,敬请期待!
好了本期内容就到这里,如果对你有帮助,不要忘记点赞三联一波哦,我是白杨,我们下期再见!