☰
C++入门篇(七):类和对象(下)——类和对象·完结篇:友元、内部类,再次理解封装与面向对象
2026/9/25 21:38:35 网站建设 项目流程

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友元函数的特点
  1. 友元函数可以访问类的私有和保护成员,但不是类的成员函数
  2. 友元函数不能用const修饰(它不是成员函数,没有this指针)
  3. 友元函数可以在类中任意位置声明,不受访问限定符限制
  4. 一个函数可以是多个类的友元(比如operator<<可以同时声明为两个类的友元,同时访问两边的私有成员)
  5. 友元函数的调用和普通函数完全一样

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友元类的三大铁律

先说结论:

友元关系是单向的、不能传递的、不能继承的。

  1. 单向:声明B是A的友元,B可以访问A的私有成员;但A不能访问B的私有成员。("我把你当朋友,你未必把我当朋友")
  2. 不能传递:A是B的友元,B是C的友元,但A不是C的友元。
  3. 不能继承:友元关系不会被派生类继承(继承是什么?先别急,下一篇就是它!)。
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特性

  1. 内部类可以定义在外部类的public、protected、private任意区域。定义在private/protected时,外部类之外无法使用A::B——这正是"把实现细节藏起来"的手段。
  2. 内部类是独立的类,不占外部类对象的空间:sizeof(外部类)和内部类没有任何关系。
  3. 内部类可以直接访问外部类的static成员,不需要外部类的对象/类名。
  4. 外部类不能访问内部类的私有成员。
  5. 内部类可以像普通类一样,有自己的成员、构造函数,甚至可以再嵌套一个类。
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++前期最陡的一段坡。接下来,我们就要进入继承与多态的世界了——新的篇章,敬请期待!

好了本期内容就到这里,如果对你有帮助,不要忘记点赞三联一波哦,我是白杨,我们下期再见!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询