📌 阅读时长:22分钟 | 关键词:C++、友元函数、友元类、friend、单例模式、设计模式
引言
前面文章中我们反复强调"封装"——用private把数据藏起来。但有时,我们确实需要给某些外部的函数或类开一扇"后门",让它们能访问私有成员。这扇门就叫友元(friend)。文章最后,我们还会用静态成员 + 友元的知识,实现第一个设计模式——单例模式。
一、友元函数:类的"特许通行证"
1.1 什么是友元函数?
在类中用friend声明的普通函数,可以访问该类的私有成员:
classBox{private:doublewidth;public:Box(doublew):width(w){}friendvoidprintWidth(Box&b);// 声明友元函数};// 定义友元函数(在类外部)voidprintWidth(Box&b){// 可以直接访问私有成员!std::cout<<"宽度:"<<b.width<<std::endl;}intmain(){Boxbox(10.0);printWidth(box);// 输出:宽度:10}1.2 友元函数可以修改私有成员
classCuboid{private:doublelength,width,height;public:Cuboid(doublel,doublew,doubleh):length(l),width(w),height(h){}friendvoidupdateDimensions(Cuboid&c,doublel,doublew,doubleh);frienddoublecalculateVolume(constCuboid&c);};voidupdateDimensions(Cuboid&c,doublel,doublew,doubleh){c.length=l;c.width=w;c.height=h;// 直接修改私有成员}doublecalculateVolume(constCuboid&c){returnc.length*c.width*c.height;// 直接读取私有成员}1.3 友元函数的要点
| 特性 | 说明 |
|---|---|
| 不是成员函数 | 没有this指针,通过参数传递对象 |
| 声明位置 | 类内任意位置(public/protected/private 都行) |
| 访问权限 | 可访问该类的所有成员(public + protected + private) |
| 集中声明 | 建议将友元声明集中在类的开头或结尾,便于代码维护 |
1.4 友元的优缺点
| 优点 | 缺点 |
|---|---|
实现运算符重载 (<<,>>) 的自然语法 | 破坏封装性 |
| 多类协作时访问对方私有成员 | 增加类之间的耦合度 |
| 简化某些特殊操作的代码 | 滥用后代码难以维护 |
二、友元类:整班都是 VIP
声明一个类为另一个类的友元,则该类的所有成员函数都能访问对方的私有成员:
classCircle{private:doubleradius;public:Circle(doubler):radius(r){}friendclassGeometry;// Geometry 是 Circle 的友元类};classGeometry{public:doublecalcArea(constCircle&c){return3.14159*c.radius*c.radius;// 访问私有成员}voidsetRadius(Circle&c,doubler){c.radius=r;// 修改私有成员}};intmain(){Circlec(5.0);Geometry g;std::cout<<g.calcArea(c)<<std::endl;// 78.5397g.setRadius(c,8.0);std::cout<<g.calcArea(c)<<std::endl;// 201.062}友元关系的三大特性
// 1. 单向性:A 是 B 的友元 ≠ B 是 A 的友元classA{friendclassB;};// B 能访问 A 的私有// A 不能访问 B 的私有 ← 除非 B 也 friend class A// 2. 非传递性:A→B 是友元,B→C 是友元 ≠ A→C 是友元// 3. 不能被继承:父类的友元不能自动访问子类的新增私有成员三、友元 + 运算符重载
友元最经典的用法是重载流运算符<<和>>:
classCuboid{private:doublelength,width,height;public:Cuboid(doublel,doublew,doubleh):length(l),width(w),height(h){}// 友元重载 + 运算符friendCuboidoperator+(constCuboid&a,constCuboid&b);// 友元重载 << 运算符friendstd::ostream&operator<<(std::ostream&os,constCuboid&c);};Cuboidoperator+(constCuboid&a,constCuboid&b){returnCuboid(a.length+b.length,std::max(a.width,b.width),std::max(a.height,b.height));}std::ostream&operator<<(std::ostream&os,constCuboid&c){os<<"Cuboid("<<c.length<<", "<<c.width<<", "<<c.height<<")";returnos;}intmain(){Cuboidc1(3,2,1),c2(4,1,5);std::cout<<c1+c2<<std::endl;// Cuboid(7, 2, 5)}四、设计模式初探:单例模式(Singleton)
学完了静态成员 + 私有构造函数 + 友元,我们已经具备了实现单例模式的能力——一个全局只能存在一个实例的类。
4.1 为什么需要单例?
- 日志记录器:全局共用一个
- 数据库连接池:避免重复创建连接
- 配置管理器:全局一份配置
4.2 基本实现
classSingleton{private:staticSingleton*instance;// 静态指针,保存唯一实例Singleton(){}// ① 构造函数是私有的!外部不能 newSingleton(constSingleton&)=delete;// ② 禁止拷贝Singleton&operator=(constSingleton&)=delete;// ③ 禁止赋值public:staticSingleton*getInstance(){// ④ 静态方法获取唯一实例if(instance==nullptr)instance=newSingleton();returninstance;}voiddoSomething(){std::cout<<"单例模式工作中..."<<std::endl;}};Singleton*Singleton::instance=nullptr;// 静态成员定义intmain(){// Singleton s; ❌ 构造函数是私有的Singleton*s1=Singleton::getInstance();Singleton*s2=Singleton::getInstance();std::cout<<(s1==s2)<<std::endl;// 1 — 同一个对象!s1->doSomething();}4.3 C++11 线程安全版(Meyer’s Singleton)
classSingleton{public:// C++11 保证局部静态变量的初始化是线程安全的!staticSingleton&getInstance(){staticSingleton instance;returninstance;}voiddoSomething(){std::cout<<"线程安全单例"<<std::endl;}private:Singleton()=default;Singleton(constSingleton&)=delete;Singleton&operator=(constSingleton&)=delete;};// 使用:Singleton::getInstance().doSomething();| 实现方式 | 线程安全 | 内存释放 | 代码量 |
|---|---|---|---|
| 原始指针 + new | ❌ | 不自动 | 多 |
| Meyer’s Singleton | ✅ (C++11) | 自动 | 极少 |
💡 日常开发中直接用Meyer’s Singleton,简单安全,无需手动 delete。
4.4 设计模式思维
| 模式 | 核心思想 | C++ 实现关键 |
|---|---|---|
| 单例 (Singleton) | 全局唯一实例 | 私有构造函数 + 静态变量 |
| 工厂 (Factory) | 集中创建对象 | 静态方法 + 返回指针/智能指针 |
| 观察者 (Observer) | 一对多通知 | 虚函数 + 指针列表 |
设计模式不是银弹,但了解它们能让你在面对常见问题时不再"重新发明轮子"。
小结
| 序号 | 知识点 | 一句话总结 |
|---|---|---|
| 1 | 友元函数 | friend 声明的外部函数可访问类私有成员,常用于运算符重载 |
| 2 | 友元类 | 整个类都能访问对方的私有成员 |
| 3 | 友元三大特性 | 单向、非传递、不能被继承 |
| 4 | 友元利弊 | 方便但破封装,只在确实需要时用 |
| 5 | 单例模式 | 私有构造 + 静态变量 → 全局唯一实例 |
| 6 | Meyer’s Singleton | C++11 线程安全,局部静态变量自动清理,推荐首选 |
至此,面向对象核心模块全部完成!下一篇文章,我们将进入模板编程——用泛型编程写出类型无关的高复用代码。
本文是「C++ 从基础到项目实战」系列的第 9 篇。关注我,不错过后续更新。