友元
类的主要特点之一是数据隐藏,即类的私有成员无法在类的外部(作用域之外)访问。但是,有时候需要在类的外部访问类的私有成员怎么办?
解决方法是使用友元函数,友元函数是一种特权函数,C++允许这个特权函数访问私有成员。这一点从现实生活中也可以很好的理解:
比如你的家有客厅、有你的卧室,那么你的客厅是Public
的,所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去。但是呢,你也可以允许你的闺蜜或者好基友进去。又比如:部门内部的人员只对部门领导负责,任何问题都要请示领导后才执行。若两个部门需要协同工作,领导可能会安排相应的对接人员,对接人员直接商讨事物会提高办事效率,对接人员就相当于友元,但是假如对接人员把握不好方向,领导又没有及时干预,则可能造成一定的后果。
程序员可以把一个全局函数、某个类中的成员函数、甚至整个类声明为友元。
语法
- friend关键字只出现在声明处
- 其他类、类成员函数、全局函数都可声明为友元
- 友元函数不是类的成员,不带
this指针
- 友元函数可访问对象任意成员属性,包括私有属性
示例代码01
#include <iostream>
using namespace std;
// 1、全局函数作为友元
class Room {
friend void GoodGay(Room& room);
public:
Room() {
this->m_BedRoom = "卧室";
this->m_SittingRoom = "客厅";
}
public:
string m_SittingRoom;
private:
string m_BedRoom;
};
void GoodGay(Room &room) {
cout << "好基友正在访问" << room.m_SittingRoom << endl;
cout << "好基友正在访问" << room.m_BedRoom << endl;
}
int main(int argc, char *argv[]) {
Room r1;
GoodGay(r1);
return 0;
}
程序输出:
好基友正在访问客厅
好基友正在访问卧室
示例代码02
#include <iostream>
using namespace std;
// 2、类作为友元
class Room {
friend class GoodGay;
public:
Room() {
this->m_BedRoom = "卧室";
this->m_SittingRoom = "客厅";
}
public:
string m_SittingRoom;
private:
string m_BedRoom;
};
class GoodGay {
public:
GoodGay() {
this->m_room = new Room;
}
void visit() {
cout << "好基友正在访问" << this->m_room->m_SittingRoom << endl;
cout << "好基友正在访问" << this->m_room->m_BedRoom << endl;
}
private:
Room* m_room;
};
int main(int argc, char *argv[]) {
GoodGay gg;
gg.visit();
return 0;
}
程序输出:
好基友正在访问客厅
好基友正在访问卧室
示例代码03
#include <iostream>
using namespace std;
// 3、类内成员函数作为友元
class Room; // 先声明类,在后部代码实现类
class GoodGay {
public:
GoodGay(); // 先声明成员函数,在类外进行函数实现
void visit();
void visit2();
Room* m_room;
};
class Room {
friend void GoodGay::visit(); // 声明另一个类中的成员函数为友元
public:
Room();
public:
string m_SittingRoom;
private:
string m_BedRoom;
};
Room::Room() {
this->m_BedRoom = "卧室";
this->m_SittingRoom = "客厅";
}
GoodGay::GoodGay() {
this->m_room = new Room;
}
void GoodGay::visit() {
cout << "好基友正在访问" << this->m_room->m_SittingRoom << endl;
cout << "好基友正在访问" << this->m_room->m_BedRoom << endl;
}
void GoodGay::visit2() {
cout << "好基友正在访问" << this->m_room->m_SittingRoom << endl;
//cout << "好基友正在访问" << this->m_room->m_BedRoom << endl;
// 没有被声明为友元的进行private访问会导致程序错误
}
int main(int argc, char *argv[]) {
GoodGay gg;
gg.visit();
gg.visit2();
return 0;
}
程序输出:
好基友正在访问客厅
好基友正在访问卧室
好基友正在访问客厅
友元类注意
- 友元关系不能被继承。
- 友元关系是单向的,类A是类B的朋友,但类B不一定是类A的朋友。
- 友元关系不具有传递性。类B是类A的朋友,类C是类B的朋友,但类C不一定是类A的朋友。
思考: C++是纯面向对象的吗?
如果一个类被声明为friend,意味着它不是这个类的成员函数,却可以修改这个类的私有成员,而且必须列在类的定义中,因此他是一个特权函数。C++不是完全的面向对象语言,而只是一个混合产品。增加friend关键字只是用来解决一些实际问题,这也说明这种语言是不纯的。毕竟C++设计的目的是为了实用性,而不是追求理想的抽象。
--- Thinking in C++
课堂练习
请编写电视机类,电视机有开机和关机状态、有音量、有频道,提供音量操作的方法、频道操作的方法。由于电视机只能逐一调整频道、不能指定频道,增加遥控类,遥控类除了拥有电视机已有的功能,再增加根据输入调台功能。
提示:遥控器可作为电视机类的友元类。
点击查看答案
#include <iostream>
using namespace std;
class Remote;
class Television {
friend class Remote;
public:
enum { On, Off }; //电视状态
enum { minVol, maxVol = 100 }; //音量从0到100
enum { minChannel = 1, maxChannel = 255 }; //频道从1到255
Television() {
mState = Off;
mVolume = minVol;
mChannel = minChannel;
}
//打开电视机
void OnOrOff() {
this->mState = (this->mState == On ? Off : On);
}
//调高音量
void VolumeUp() {
if (this->mVolume >= maxVol) {
return;
}
this->mVolume++;
}
//调低音量
void VolumeDown() {
if (this->mVolume <= minVol) {
return;
}
this->mVolume--;
}
//更换电视频道
void ChannelUp() {
if (this->mChannel >= maxChannel) {
return;
}
this->mChannel++;
}
void ChannelDown() {
if (this->mChannel <= minChannel) {
return;
}
this->mChannel--;
}
//展示当前电视状态信息
void ShowTeleState() {
cout << "开机状态:" << (mState == On ? "已开机" : "已关机") << endl;
if (mState == On) {
cout << "当前音量:" << mVolume << endl;
cout << "当前频道:" << mChannel << endl;
}
cout << "-------------" << endl;
}
private:
int mState; //电视状态,开机,还是关机
int mVolume; //电视机音量
int mChannel; //电视频道
};
//电视机调台只能一个一个的调,遥控可以指定频道
//电视遥控器
class Remote {
public:
Remote(Television* television) {
pTelevision = television;
}
public:
void OnOrOff() {
pTelevision->OnOrOff();
}
//调高音量
void VolumeUp() {
pTelevision->VolumeUp();
}
//调低音量
void VolumeDown() {
pTelevision->VolumeDown();
}
//更换电视频道
void ChannelUp() {
pTelevision->ChannelUp();
}
void ChannelDown() {
pTelevision->ChannelDown();
}
//设置频道 遥控新增功能
void SetChannel(int channel) {
if (channel < Television::minChannel || channel > Television::maxChannel) {
return;
}
pTelevision->mChannel = channel;
}
//显示电视当前信息
void ShowTeleState() {
pTelevision->ShowTeleState();
}
private:
Television* pTelevision;
};
//直接操作电视
void test01() {
Television television;
television.ShowTeleState();
television.OnOrOff(); //开机
television.VolumeUp(); //增加音量+1
television.VolumeUp(); //增加音量+1
television.VolumeUp(); //增加音量+1
television.VolumeUp(); //增加音量+1
television.ChannelUp(); //频道+1
television.ChannelUp(); //频道+1
television.ShowTeleState();
}
//通过遥控操作电视
void test02() {
//创建电视
Television television;
//创建遥控
Remote remote(&television);
remote.OnOrOff();
remote.ChannelUp();//频道+1
remote.ChannelUp();//频道+1
remote.ChannelUp();//频道+1
remote.VolumeUp();//音量+1
remote.VolumeUp();//音量+1
remote.VolumeUp();//音量+1
remote.VolumeUp();//音量+1
remote.ShowTeleState();
}
int main(int argc, char *argv[]) {
test01();
test02();
return 0;
}
程序输出:
开机状态:已关机
-------------
开机状态:已开机
当前音量:4
当前频道:3
-------------
开机状态:已开机
当前音量:4
当前频道:4
-------------
强化训练
数组类封装
#ifndef MYARRAY_H
#define MYARRAY_H
class MyArray {
public:
//无参构造函数,用户没有指定容量,则初始化为100
MyArray();
//有参构造函数,用户指定容量初始化
explicit MyArray(int capacity);
//用户操作接口
//根据位置添加元素
void SetData(int pos, int val);
//获得指定位置数据
int GetData(int pos);
//尾插法
void PushBack(int val);
//获得长度
int GetLength();
//析构函数,释放数组空间
~MyArray();
private:
int mCapacity; //数组一共可容纳多少个元素
int mSize; //当前有多少个元素
int* pAdress; //指向存储数据的空间
};
#endif
#include"MyArray.h"
MyArray::MyArray() {
this->mCapacity = 100;
this->mSize = 0;
//在堆开辟空间
this->pAdress = new int[this->mCapacity];
}
//有参构造函数,用户指定容量初始化
MyArray::MyArray(int capacity) {
this->mCapacity = capacity;
this->mSize = 0;
//在堆开辟空间
this->pAdress = new int[capacity];
}
//根据位置添加元素
void MyArray::SetData(int pos, int val) {
if (pos < 0 || pos > mCapacity - 1) {
return;
}
pAdress[pos] = val;
}
//获得指定位置数据
int MyArray::GetData(int pos) {
return pAdress[pos];
}
//尾插法
void MyArray::PushBack(int val) {
if (mSize >= mCapacity) {
return;
}
this->pAdress[mSize] = val;
this->mSize++;
}
//获得长度
int MyArray::GetLength() {
return this->mSize;
}
//析构函数,释放数组空间
MyArray::~MyArray() {
if (this->pAdress != nullptr) {
delete[] this->pAdress;
}
}
#include <iostream>
using namespace std;
#include"MyArray.h"
void test() {
//创建数组
MyArray myarray(50);
//数组中插入元素
for (int i = 0; i < 50; i++) {
//尾插法
myarray.PushBack(i);
//myarray.SetData(i, i);
}
//打印数组中元素
for (int i = 0; i < myarray.GetLength(); i++) {
cout << myarray.GetData(i) << " ";
}
cout << endl;
}
int main(int argc, char *argv[]) {
test();
return 0;
}
程序输出:
面试题
内部类访问权限问题
以下几个问题的前提是:B是A的内部类
Q1:B能否访问A的private成员?
默认情况下,B不能直接访问A的private成员。C++中内部类(嵌套类)与外部类的关系是逻辑包含,但不自动拥有友元权限。若需访问,需显式声明友元关系。
Q2:A能否访问B的private成员?
A默认无法访问B的private成员。B的私有成员仅对自身可见,除非B将A声明为友元(friend class A;
)或提供公有访问接口。
Q3:有什么办法能让B访问A的私有成员?
方法1:友元声明
在A类中声明B为友元类:
class A {
private:
int privateData;
friend class B; // B成为A的友元
public:
class B {
void accessA(A& a) {
a.privateData = 42; // 可直接访问A的私有成员
}
};
};
代价:友元会破坏封装性,允许B直接操作A的私有成员。
方法2:间接访问(推荐,不破坏封装)
A通过公有或受保护的接口暴露私有成员,B通过接口访问:
class A {
private:
int privateData;
public:
int getData() const { return privateData; } // 公有接口
class B {
void accessA(A& a) {
int data = a.getData(); // 通过接口访问
}
};
};
此方式通过设计模式(如代理模式)维护封装性,符合面向对象原则。
Q4:友元破坏了封装性,那么不破坏封装性的情况下怎么做?
接口隔离原则:A提供有限的公有/受保护接口,仅暴露必要的功能,避免直接暴露私有数据。
中间层设计:通过中间类或接口类封装A的私有操作,B通过中间层间接调用。
依赖注入:B持有A的引用,通过A的公有方法操作数据,而非直接访问私有成员。
Q5:如何在B对象内调用A的成员函数?
方式1:传递A的实例
在B中保存A的引用或指针,通过实例调用:
class A {
public:
class B {
A& parent; // 保存A的引用
public:
B(A& a) : parent(a) {}
void callA() {
parent.aPublicFunc(); // 调用A的公有函数
}
};
};
方式2:友元关系
若B需要调用A的私有函数,需将B声明为A的友元:
class A {
private:
void privateFunc() {}
friend class B; // B可调用A的私有函数
public:
class B {
void callA(A& a) {
a.privateFunc(); // 直接调用A的私有函数
}
};
};
方式3:静态成员函数
若A的函数为静态,B可通过类名直接调用:
class A {
public:
static void staticFunc() {}
class B {
void callA() {
A::staticFunc(); // 调用静态函数
}
};
};
总结
- 权限控制:内部类与外部类默认无特殊访问权限,需通过友元或接口设计实现交互。
- 封装性维护:优先通过公有接口和设计模式实现交互,避免滥用友元。
- 代码示例:结合实例引用、友元声明和接口设计,灵活应对不同场景。
此回答覆盖了权限机制、设计原则和代码实践,兼顾理论深度与工程实用性,符合C++后端开发的高分标准。