类的高级话题——static、const、友元与嵌套类
本篇文章是《C++ 类与对象完全指南:从入门到工程实践》系列的第八篇。
前面七篇我们走过了类的诞生、核心语法、生命周期、拷贝控制、移动语义和内存模型。今天我们要补齐类语法中最后几块拼图:static、const、friend 和嵌套类。这些关键字看似零散,但它们都有一个共同的主题——控制访问权限与作用域。理解了它们,你才算真正掌握了类的完整语法。
先来看一段代码,测试一下你对 static 和 const 的理解:
class Counter {
public:
Counter() { ++count; }
~Counter() { —count; }
static int getCount() { return count; }
int getValue() const { return value; } // const 成员函数
private:
int value = 0;
static int count; // 静态成员变量
};
int Counter::count = 0;
int main() {
Counter c1, c2;
cout << Counter::getCount() << endl; // 2(不依赖对象)
cout << c1.getCount() << endl; // 2(也可以用对象调用)
const Counter c3;
// c3.getValue() 能调用吗?c3.getCount() 能调用吗?
}
const 对象能调用哪些成员函数?static 成员函数能不能访问普通成员变量?这些问题看似细节,却在日常开发中频繁出现。
一、static 成员——属于类,而非对象
普通的成员变量和成员函数是“属于对象”的——每个对象都有自己的一份数据。而 static 成员是“属于类”的——所有对象共享同一份数据。
1. static 成员变量
class Student {
public:
Student(const string& name) : _name(name) {
++_totalCount; // 每创建一个学生,计数加一
}
~Student() {
—_totalCount; // 销毁时计数减一
}
static int GetTotal() { return _totalCount; }
private:
string _name;
static int _totalCount; // 声明:所有 Student 对象共享
};
int Student::_totalCount = 0; // 定义:必须在类外分配存储空间
三大关键特性:
| 存储位置 | 静态存储区(数据段),不占用对象内存 |
| 共享性 | 所有对象共享同一份,任何一个对象修改,其他对象都能看到 |
| 生命周期 | 从程序启动到程序结束,与对象无关 |
必须在类外定义: 类内只是声明,不会分配内存。类外定义时不需要再加 static 关键字(加上也不报错,但语义上多余)。
int Student::_totalCount = 0; // 正确
// static int Student::_totalCount = 0; // 也合法,但没必要
访问方式: 可以通过类名访问(推荐),也可以通过对象访问。
cout << Student::GetTotal() << endl; // ✅ 推荐:不依赖对象
Student s1("张三");
cout << s1.GetTotal() << endl; // ✅ 可以,但不推荐——看起来像依赖对象
2. static 成员函数
static 成员函数没有 this 指针。 这是理解它所有限制的根源。
| 没有 this 指针 | 因此不能访问非静态成员变量和非静态成员函数 |
| 只能访问 static 成员 | 因为它属于类,不属于任何对象 |
| 不能用 const 修饰 | const 修饰的是 this 指向的对象,而它没有 this |
| 不能用 virtual 修饰 | 虚函数依赖 this 指针找虚表 |
class Demo {
public:
static void StaticFunc() {
// _value = 10; // ❌ 错误:没有 this,无法访问非静态成员
// NonStaticFunc(); // ❌ 错误:同上
_staticValue = 10; // ✅ 正确:可以访问静态成员
}
void NonStaticFunc() {
_staticValue = 10; // ✅ 正确:非静态函数可以访问静态成员
_value = 10; // ✅ 正确
}
private:
int _value;
static int _staticValue;
};
3. 应用场景:单例模式
static 成员最经典的应用就是单例模式(Singleton)——保证一个类只有一个实例。
class Singleton {
public:
// 全局访问点:静态成员函数
static Singleton& GetInstance() {
static Singleton instance; // 局部静态对象:C++11 保证线程安全
return instance;
}
void DoSomething() { cout << "干活" << endl; }
// 禁用拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
// 私有构造:禁止外部创建对象
Singleton() { cout << "Singleton 构造" << endl; }
~Singleton() { cout << "Singleton 析构" << endl; }
};
int main() {
Singleton& s1 = Singleton::GetInstance();
Singleton& s2 = Singleton::GetInstance();
s1.DoSomething();
// Singleton s3; // ❌ 编译错误:构造函数是私有的
// 且 s1 和 s2 是同一个对象
}
为什么单例的构造函数要私有? 因为要阻止外部用 Singleton s; 创建对象,强制用户通过 GetInstance() 获取唯一实例。而 GetInstance() 必须是 static,否则用户得先有对象才能调用它——鸡生蛋问题。
单例的进阶:饿汉 vs 懒汉
| 饿汉模式 | 程序启动时就创建实例 | 简单、线程安全,但如果没人用会浪费 |
| 懒汉模式 | 第一次调用时才创建(如上面代码) | 延迟加载,C++11 后局部静态变量天然线程安全 |
二、const 成员——承诺不修改
const 在类中有两种用法:const 成员变量和 const 成员函数。后者更重要,也更常用。
1. const 成员变量
const 成员变量必须在初始化列表中初始化(因为构造函数体内的赋值语句无法修改 const 变量)。
class Circle {
public:
Circle(double r) : _radius(r), _PI(3.14159) {} // ✅ 初始化列表
private:
double _radius;
const double _PI; // const 成员:初始化后不能修改
};
2. const 成员函数——只读接口
在成员函数后面加 const,表示这个函数不会修改对象的任何非静态成员变量。
class Date {
public:
int GetYear() const { // const 成员函数:不修改对象
return _year;
}
void SetYear(int year) { // 非 const 成员函数:可以修改对象
_year = year;
}
private:
int _year;
};
核心规则:const 对象只能调用 const 成员函数。
const Date d1; // const 对象
d1.GetYear(); // ✅ 可以调用 const 成员函数
// d1.SetYear(2026); // ❌ 编译错误:const 对象不能调用非 const 成员函数
Date d2; // 普通对象
d2.GetYear(); // ✅ 可以调用 const 成员函数
d2.SetYear(2026); // ✅ 可以调用非 const 成员函数
为什么? 因为 SetYear 的隐藏参数是 Date* const this,而 d1 是 const Date,const Date* 不能传给 Date*。而 GetYear 的隐藏参数是 const Date* const this,可以接收 const Date 的地址。
3. 权限放大与缩小——指针和引用的 const 传递
void Func1(const Date& d) {
d.GetYear(); // ✅ const 引用调用 const 成员
// d.SetYear(); // ❌ 不能调用非 const 成员
}
void Func2(Date& d) {
d.GetYear(); // ✅ 非 const 引用可以调用 const 成员
d.SetYear(1); // ✅ 也可以调用非 const 成员
}
int main() {
const Date d1;
Func1(d1); // ✅ const 对象只能传给 const 引用/指针
// Func2(d1); // ❌ 错误:const 对象不能传给非 const 引用
Date d2;
Func1(d2); // ✅ 非 const 对象可以传给 const 引用(权限缩小,安全)
Func2(d2); // ✅
}
口诀: const 权限只能缩小或平移,不能放大。非 const 对象转 const 是安全的,反过来是危险的。
4. mutable —— const 中的“例外”
mutable 修饰的成员变量可以在 const 成员函数中被修改。它用于那些“逻辑上不影响对象状态”的成员。
class Cache {
public:
int GetData() const {
++_accessCount; // ✅ mutable 成员可以在 const 函数中修改
return _data;
}
private:
int _data = 42;
mutable int _accessCount = 0; // 访问计数:逻辑上不影响对象状态
};
典型应用场景: 缓存、访问计数、互斥锁(mutable std::mutex)。
5. const 成员函数的重载
const 和非 const 成员函数可以构成重载,编译器根据对象的 const 性选择。
class String {
public:
char& operator[](size_t pos) {
cout << "非 const 版本" << endl;
return _str[pos];
}
const char& operator[](size_t pos) const {
cout << "const 版本" << endl;
return _str[pos];
}
private:
char* _str;
};
int main() {
String s1;
s1[0] = 'a'; // 调用非 const 版本
const String s2;
cout << s2[0]; // 调用 const 版本
}
三、友元——突破封装的“特权通道”
封装是好的,但有时候外部的某个函数或类确实需要访问私有成员。为此,C++ 提供了 friend 关键字。
友元的本质: 声明某个函数或类是“自己人”,允许它访问私有成员。友元是单向的、不能继承、不能传递的。
1. 友元函数
class Date {
public:
Date(int year, int month, int day)
: _year(year), _month(month), _day(day) {}
// 声明友元函数:它不是成员函数,但能访问私有成员
friend ostream& operator<<(ostream& out, const Date& d);
private:
int _year, _month, _day;
};
// 友元函数的定义(注意:没有 Date:: 前缀)
ostream& operator<<(ostream& out, const Date& d) {
out << d._year << "-" << d._month << "-" << d._day; // ✅ 可以访问私有成员
return out;
}
int main() {
Date d(2026, 8, 30);
cout << d << endl; // 2026-8-30
}
为什么 operator<< 必须作为友元? 因为它的第一个参数是 ostream&,不是 Date 对象。如果作为成员函数,调用会变成 d << cout,非常别扭。所以只能作为全局函数,但需要访问私有成员 → 只能声明为友元。
2. 友元类
整个类都可以成为另一个类的友元。
class A {
friend class B; // B 是 A 的友元,B 的所有成员函数都能访问 A 的私有成员
private:
int _secret = 42;
};
class B {
public:
void Peek(A& a) {
cout << a._secret << endl; // ✅ B 是友元,可以访问
}
};
注意: 友元关系是单向的。B 是 A 的友元,不代表 A 是 B 的友元。同时,友元关系不能被继承。
3. 友元的优缺点与使用建议
| 提供受控的访问通道 | 破坏封装性,增加耦合 |
| 简化某些运算符重载 | 难以维护,修改时需同步更新 |
| 提高效率(免去 Getter 调用) | 滥用会削弱类的设计价值 |
使用建议:
- 优先考虑用 public 成员函数替代友元。
- 只在实现运算符重载(如 <<)或紧耦合的类对(如容器与迭代器)中使用友元。
- 不要为了图方便而滥用友元。
四、嵌套类——类中的类
嵌套类(Nested Class) 是定义在另一个类内部的类。
class Outer {
public:
class Inner { // 嵌套类
public:
void DoSomething() { cout << "Inner::DoSomething" << endl; }
};
Inner CreateInner() { return Inner(); }
};
int main() {
Outer::Inner inner; // 访问方式:外部类名::内部类名
inner.DoSomething();
Outer outer;
auto inner2 = outer.CreateInner();
}
嵌套类的核心特性:
| 独立作用域 | 嵌套类有自己的作用域,与外部类互相独立 |
| 访问权限 | 嵌套类不能直接访问外部类的非静态成员(没有隐式外部对象) |
| 外部访问内部 | 外部类不能直接访问嵌套类的私有成员(除非声明友元) |
| 名字查找 | 嵌套类可以访问外部类的 static 成员和类型定义 |
class Outer {
private:
int _value = 10;
static int _staticValue;
public:
class Inner {
public:
void Test(Outer& o) {
// _value = 1; // ❌ 错误:不能直接访问外部类的非静态成员
o._value = 1; // ✅ 正确:需要显式传对象
_staticValue = 1; // ✅ 正确:静态成员可以直接访问
}
};
};
典型应用:迭代器
STL 中的 list 和 vector 的迭代器,就是用嵌套类实现的。
template <typename T>
class MyVector {
public:
class Iterator { // 嵌套类:迭代器
public:
T& operator*() { return *_ptr; }
Iterator& operator++() { ++_ptr; return *this; }
private:
T* _ptr;
};
Iterator Begin() { return Iterator{_data}; }
Iterator End() { return Iterator{_data + _size}; }
private:
T* _data;
size_t _size;
};
五、综合案例:一个线程安全的计数器
把本篇文章的知识点综合一下,设计一个线程安全的引用计数器:
#include <mutex>
class RefCounter {
public:
RefCounter() : _count(0) {}
void AddRef() const {
std::lock_guard<std::mutex> lock(_mutex); // ✅ mutable 成员
++_count;
}
void Release() const {
std::lock_guard<std::mutex> lock(_mutex);
—_count;
}
int GetCount() const { // ✅ const 成员函数
std::lock_guard<std::mutex> lock(_mutex);
return _count;
}
// 禁用拷贝(避免 mutex 被拷贝)
RefCounter(const RefCounter&) = delete;
RefCounter& operator=(const RefCounter&) = delete;
private:
mutable std::mutex _mutex; // ✅ mutable:锁本身不是"逻辑状态"
mutable int _count; // ✅ mutable:引用计数可在 const 函数中修改
};
这个类综合运用了:
- const 成员函数:GetCount 不修改对象的逻辑状态。
- mutable:_mutex 和 _count 需要在 const 函数中被修改。
- 删除拷贝控制:std::mutex 不可拷贝,因此禁用拷贝构造和赋值。
六、总结
| static 成员变量 | 属于类,所有对象共享;类外定义;不占对象内存 |
| static 成员函数 | 没有 this 指针,不能访问非静态成员;不能用 const/virtual |
| const 成员变量 | 必须在初始化列表中初始化 |
| const 成员函数 | 承诺不修改对象;const 对象只能调用 const 成员函数 |
| mutable | 突破 const 限制,允许在 const 函数中修改成员 |
| friend 友元 | 单向、不可继承、不可传递;建议少用 |
| 嵌套类 | 独立作用域;不自动访问外部类非静态成员;常用于迭代器 |
核心思想回顾: static、const、friend、嵌套类——这四个特性看似零散,实则都在回答同一个问题:谁有权访问什么? static 回答“谁能共享数据”,const 回答“谁能修改数据”,friend 回答“谁能突破边界”,嵌套类回答“谁的作用域在哪里”。理解了访问控制的边界,你就理解了 C++ 封装设计的全部精髓。
下篇预告
至此,我们已经完整走过了 C++ 类与对象的全部核心语法——从类的诞生到 this 指针,从构造析构到拷贝控制,从移动语义到内存模型,从 static/const 到友元嵌套类。
但语法只是工具,如何设计出优秀的类才是工程实践的核心。最后一篇,我们将进入《设计类的最佳实践(工程篇)》,结合《Effective C++》的经典条款和真实工程场景,讨论如何设计一个易用、不易误用、可维护的类。
思考题:
为什么 static 成员函数不能是 virtual 的?(提示:想想虚函数依赖什么来找到正确的函数版本)
如果一个 const 成员函数内部需要调用一个非 const 成员函数,该怎么办?除了 mutable,还有没有别的方法?(提示:const_cast)
单例模式中,GetInstance() 返回的是引用而不是指针,这样设计有什么好处?如果返回指针,会带来哪些问题?
欢迎在评论区留下你的思考。下一篇,也是本系列的收官之作,我们将从"会写类"走向"设计好类"——不见不散!





