C++的friend声明用于赋予指定的函数、类或者其他类成员函数访问当前类私有成员和保护成员的权限,在合理场景下使用可以平衡封装性和功能需求,其中单元测试和序列化是两个非常典型且合理的应用场景。

friend声明的基本语法
friend声明需要写在类的内部,可以是友元函数、友元类或者友元成员函数,语法形式如下:
// 友元函数
class MyClass {
private:
int value;
public:
MyClass(int v) : value(v) {}
// 声明外部函数为友元,允许其访问私有成员value
friend void printValue(const MyClass& obj);
};
void printValue(const MyClass& obj) {
// 可以直接访问obj的私有成员value
printf("value is %dn", obj.value);
}
// 友元类
class Helper {
public:
void modifyValue(MyClass& obj, int newVal) {
obj.value = newVal; // 可以访问MyClass的私有成员
}
};
class MyClass {
private:
int value;
// 声明Helper类为友元类,Helper的所有成员函数都可以访问MyClass的私有成员
friend class Helper;
public:
MyClass(int v) : value(v) {}
};
单元测试场景下的合理用例
单元测试的核心目标是验证类的内部逻辑是否正确,很多时候需要检查类的私有成员状态是否符合预期,如果为了测试把私有成员改成公有或者提供公有的getter方法,会破坏类的封装性,此时使用friend声明是更合理的选择。
通常我们会把测试类或者测试函数声明为被测试类的友元,让测试代码可以直接访问私有成员完成验证,同时不影响生产代码对外的封装性。
#include <cassert>
// 被测试的类,封装了内部计数逻辑
class Counter {
private:
int count; // 私有计数成员
public:
Counter() : count(0) {}
void increment() {
count++;
}
void decrement() {
count--;
}
// 声明测试函数为友元,仅测试代码可以访问count
friend void testCounter();
};
// 单元测试函数
void testCounter() {
Counter c;
c.increment();
c.increment();
// 直接访问私有成员count验证结果,不需要额外暴露公有接口
assert(c.count == 2);
c.decrement();
assert(c.count == 1);
printf("Counter test passedn");
}
这种方式的好处是,生产代码中Counter类的count成员依然是私有的,外部业务代码无法直接修改,只有测试函数可以访问,既满足了测试需求,又不会破坏封装性。
序列化场景下的合理用例
序列化是指将对象的状态转换为可存储或可传输的格式,反序列化则是从存储的格式还原对象状态。很多时候对象的内部状态是私有的,序列化逻辑需要读写这些私有成员,此时使用friend声明可以让序列化相关的函数访问私有成员,而不需要为所有私有成员提供公有的getter和setter。
常见的做法是将序列化、反序列化函数声明为类的友元,或者将负责序列化的工具类声明为友元。
#include <cstdio>
#include <cstring>
// 需要序列化的用户类
class User {
private:
char name[32];
int age;
public:
User(const char* n, int a) {
strncpy(name, n, sizeof(name)-1);
name[sizeof(name)-1] = ' ';
age = a;
}
// 声明序列化函数为友元
friend void serializeUser(const User& user, FILE* file);
// 声明反序列化函数为友元
friend User deserializeUser(FILE* file);
};
// 序列化函数,将User对象写入文件
void serializeUser(const User& user, FILE* file) {
// 直接访问私有成员写入文件
fwrite(user.name, sizeof(char), 32, file);
fwrite(&user.age, sizeof(int), 1, file);
}
// 反序列化函数,从文件还原User对象
User deserializeUser(FILE* file) {
char tmpName[32];
int tmpAge;
fread(tmpName, sizeof(char), 32, file);
fread(&tmpAge, sizeof(int), 1, file);
// 这里需要友元吗?如果是通过构造函数初始化则不需要,但如果要修改私有成员则需要
// 假设我们有一个内部初始化函数,或者反序列化函数作为友元直接赋值
User u("", 0);
// 直接修改私有成员完成反序列化
strncpy(u.name, tmpName, 32);
u.name[31] = ' ';
u.age = tmpAge;
return u;
}
int main() {
User u1("张三", 25);
FILE* f = fopen("user.dat", "wb");
serializeUser(u1, f);
fclose(f);
f = fopen("user.dat", "rb");
User u2 = deserializeUser(f);
fclose(f);
printf("Deserialized user: name=%s, age=%dn", u2.name, u2.age);
return 0;
}
如果不使用friend声明,要么需要把name和age改成公有,要么需要给它们添加公有的访问接口,这都会让类的内部状态暴露给所有外部代码,而使用friend声明后,只有序列化相关的函数可以访问这些私有成员,其他外部代码依然无法直接操作,保证了封装性。
使用friend声明的注意事项
虽然friend声明在单元测试和序列化场景下是合理的,但也不能滥用,需要遵循以下原则:
- 只给确实需要访问私有成员的特定函数或类授予友元权限,不要随意把无关的类声明为友元
- 友元关系是单向的,比如A是B的友元,不代表B是A的友元
- 友元关系不能被继承,父类的友元不会自动成为子类的友元
- 优先通过公有接口实现功能,只有当公有接口无法满足需求且会破坏封装性时,才考虑使用friend声明
总的来说,friend声明不是破坏封装性的工具,而是在特定场景下平衡封装性和功能需求的合理手段,在单元测试和序列化场景中合理使用,可以让代码既安全又满足功能要求。