在C++面向对象编程中,封装性是一个核心特性。通常情况下,类的私有成员只能被该类的其他成员函数访问。但友元函数(friend function)打破了这一规则,它允许外部函数访问类的所有私有和保护成员。
友元函数的声明方式是在类定义中使用friend关键字。例如:
cpp复制class MyClass {
private:
int secretData;
public:
friend void friendFunction(MyClass& obj);
};
void friendFunction(MyClass& obj) {
obj.secretData = 42; // 可以直接访问私有成员
}
友元函数不是类的成员函数,但它可以访问类的所有成员。这种特性在以下场景特别有用:
<<用于输出注意:过度使用友元函数会破坏封装性,应该谨慎使用。通常优先考虑通过公有成员函数来访问私有数据。
| 特性 | 友元函数 | 普通函数 |
|---|---|---|
| 访问权限 | 可以访问类的私有和保护成员 | 只能访问公有成员 |
| 所属关系 | 不属于类的成员 | 不属于类的成员 |
| this指针 | 没有this指针 | 没有this指针 |
| 继承性 | 不被派生类继承 | 不涉及继承 |
常量数据成员(const data member)是指那些在对象生命周期内值不能被修改的成员变量。它们在构造函数初始化后就不能再改变。
常量成员必须在构造函数的初始化列表中初始化,不能在构造函数体内赋值:
cpp复制class Circle {
private:
const double PI;
double radius;
public:
Circle(double r) : PI(3.14159), radius(r) {} // 正确初始化
// Circle(double r) { PI = 3.14159; } // 错误!不能在函数体内赋值
};
提示:如果需要在多个对象间共享常量,应该使用static const成员而不是实例const成员。
常量成员函数(const member function)是指那些不会修改对象状态的成员函数。它们在声明和定义时都要在参数列表后加上const关键字。
cpp复制class MyClass {
public:
void nonConstFunc(); // 普通成员函数
void constFunc() const; // 常量成员函数
};
void MyClass::constFunc() const {
// 不能修改任何成员变量
// 只能调用其他const成员函数
}
常量对象(被const修饰的对象)只能调用常量成员函数:
cpp复制const MyClass obj;
obj.constFunc(); // 正确
obj.nonConstFunc(); // 错误!
这种限制保证了常量对象的状态不会被意外修改。
常量正确性(const-correctness)是指合理使用const来防止意外修改的编程实践。遵循这一原则可以:
有时我们需要在const成员函数中修改某些与对象逻辑状态无关的成员(如缓存、计数器等)。这时可以使用mutable关键字:
cpp复制class CachedValue {
private:
mutable bool cacheValid;
mutable int cachedResult;
int computeValue() const;
public:
int getValue() const {
if (!cacheValid) {
cachedResult = computeValue();
cacheValid = true;
}
return cachedResult;
}
};
mutable成员可以在const成员函数中被修改,但应该谨慎使用,确保这种修改不会影响对象的逻辑状态。
让我们通过一个完整的例子来展示这些概念如何协同工作:
cpp复制#include <iostream>
#include <cmath>
class Vector {
private:
double x, y;
mutable int accessCount = 0; // 记录访问次数的mutable成员
public:
Vector(double x, double y) : x(x), y(y) {}
// 常量成员函数
double length() const {
accessCount++;
return std::sqrt(x*x + y*y);
}
// 非常量成员函数
void normalize() {
double len = length();
x /= len;
y /= len;
}
// 友元函数 - 向量点积
friend double dotProduct(const Vector& v1, const Vector& v2);
// 友元函数 - 输出运算符
friend std::ostream& operator<<(std::ostream& os, const Vector& v);
};
double dotProduct(const Vector& v1, const Vector& v2) {
return v1.x * v2.x + v1.y * v2.y;
}
std::ostream& operator<<(std::ostream& os, const Vector& v) {
os << "(" << v.x << ", " << v.y << ")";
return os;
}
int main() {
const Vector v1(3, 4);
Vector v2(1, 1);
std::cout << "v1: " << v1 << ", length: " << v1.length() << std::endl;
std::cout << "v2: " << v2 << ", length: " << v2.length() << std::endl;
std::cout << "Dot product: " << dotProduct(v1, v2) << std::endl;
v2.normalize();
std::cout << "Normalized v2: " << v2 << std::endl;
// v1.normalize(); // 错误!v1是const对象
return 0;
}
在这个例子中,我们展示了:
这是C++的类型安全机制。常量成员函数承诺不修改对象状态,如果它调用了非常量成员函数,后者可能会修改对象状态,这就违背了const的承诺。
解决方案:
友元确实会降低封装性,但有时是必要的。合理使用友元的准则:
在实际项目中,const正确性可以显著提高代码质量:
mutable虽然有用,但容易被滥用。应该只在以下情况使用mutable:
const关键字为编译器提供了更多优化机会:
友元函数通常是内联的好候选,因为它们:
考虑将简单的友元函数直接在类定义中实现(隐式内联)。
现代编译器可以利用const信息进行更好的代码生成:
C++11及后续标准引入了一些与const相关的新特性:
constexpr比const更严格,表示值或函数在编译时就可确定:
cpp复制class Circle {
private:
static constexpr double PI = 3.141592653589793;
// ...
};
constexpr函数可以在编译时求值,适合用于常量计算。
虽然右值引用(&&)本身与const无关,但移动语义可以与const交互:
cpp复制class Buffer {
public:
Buffer(const Buffer&); // 拷贝构造
Buffer(Buffer&&); // 移动构造
Buffer(const Buffer&&) = delete; // const右值引用通常无意义
};
noexcept说明符经常与const成员函数一起使用,表示该函数不仅不修改对象状态,而且不会抛出异常:
cpp复制class MyClass {
public:
int getValue() const noexcept { /* ... */ }
};
这种组合特别适用于需要高性能和异常安全的场景。