static 是 C/C++ 中非常重要的关键字。它并不是只有一种含义,而是会随着所在位置不同,影响变量或函数的存储期、作用域、链接属性以及对象归属关系。
在 C++ 中,常见的 static 可以分为四类:类的静态数据成员、类的静态成员函数、函数内部的静态局部变量,以及命名空间或文件作用域中的静态变量和静态函数。除此之外,在模板、多线程和 C++17 之后的 inline static 中还有一些需要特别注意的规则。
一、类中的静态数据成员
1. 普通成员与静态成员的区别
普通数据成员属于具体对象。例如:
class Student
{
public:
int age;
};
创建两个对象:
Student s1;
Student s2;
此时:
s1.age
s2.age
是两个独立的变量,每个对象都有自己的一份 age。
如果改成:
class Student
{
public:
static int count;
};
那么 count 不再属于某一个 Student 对象,而属于整个 Student 类:
Student
│
└── count // 整个类只有一份
s1 ─┐
├── 共享 Student::count
s2 ─┘
因此通常使用:
Student::count
访问静态数据成员。
即使写成:
s1.count;
s2.count;
访问的仍然是同一个 Student::count。
2. 静态数据成员不属于对象本身
例如:
class A
{
public:
int x;
static int y;
};
x 是对象的一部分,而 y 属于类。
因此从对象内存布局的角度:
A对象
┌─────────────┐
│ x │
└─────────────┘
类级别:
A::y
static 数据成员通常不会作为每个对象的一部分重复存储,因此不会像普通成员那样增加每个对象的实例数据大小。
3. 静态数据成员的定义
传统 C++ 中:
class A
{
public:
static int count;
};
这一句主要完成成员声明。
通常还需要在类外定义:
int A::count = 0;
如果类是模板:
template <typename T>
class ThreadPool
{
private:
static int _threadnum;
};
类外定义需要写成:
template <typename T>
int ThreadPool<T>::_threadnum = 5;
这里的 template<typename T> 并不是说明 _threadnum 是一个"模板变量",而是说明:
_threadnum属于类模板ThreadPool<T>。
二、类中的静态成员函数
1. static 成员函数没有 this 指针
普通成员函数:
class A
{
public:
void Func()
{
x = 10;
}
private:
int x;
};
调用:
A a;
a.Func();
普通成员函数内部存在一个隐藏的 this 指针,可以理解为:
this->x = 10;
而静态成员函数:
class A
{
public:
static void Func()
{
}
};
可以直接:
A::Func();
调用时不需要创建对象,因此静态成员函数不存在某个具体对象对应的 this 指针。
2. static 成员函数不能直接访问普通成员
例如:
class A
{
private:
int x;
static int y;
public:
static void Func()
{
y = 10; // 正确
// x = 10; // 错误
}
};
原因并不是访问权限,而是 x 必须属于某一个具体对象。
编译器面对:
x = 10;
无法确定应该修改:
a1.x
a2.x
a3.x
中的哪一个。
而 y 整个类只有一份,所以可以直接访问。
如果静态成员函数已经获得了对象指针,则仍然可以访问普通成员:
static void Func(A* self)
{
self->x = 10;
}
这也是 pthread 封装中常见的设计。
例如:
static void* Routine(void* args)
{
Thread* self = static_cast<Thread*>(args);
self->EnableRunning();
self->_func();
return nullptr;
}
由于 Routine() 是静态成员函数,没有 this,因此通过 args 恢复原来的 Thread*,再访问具体对象。
三、函数内部的 static 局部变量
函数内部的 static 与类中的 static 含义不同。
例如:
void Func()
{
static int count = 0;
++count;
cout << count << endl;
}
连续调用:
Func();
Func();
Func();
输出:
1
2
3
如果没有 static:
void Func()
{
int count = 0;
++count;
}
每次进入函数都会重新创建 count。
静态局部变量的特点是:
作用域:仍然只在当前函数内部
生命周期:持续到程序结束
初始化次数:一次
因此需要区分两个概念:
作用域
决定"在哪里能够访问"
生命周期
决定"变量存在多久"
静态局部变量虽然活到程序结束,但在函数外仍然不能直接访问。
1. 静态局部变量只初始化一次
例如:
void Func()
{
static int x = GetValue();
}
第一次执行到该语句时调用:
GetValue();
完成初始化。
之后再次调用 Func(),不会重新初始化 x。
这一特性经常用于单例模式:
class ThreadPool
{
public:
static ThreadPool& GetInstance()
{
static ThreadPool instance;
return instance;
}
};
从 C++11 开始,函数局部静态变量的初始化具有语言层面的线程安全保证。
也就是说,多个线程同时第一次执行:
static ThreadPool instance;
不会因此正常构造多个 instance。
需要注意的是:
线程安全保证针对的是"初始化过程",并不意味着后续对该对象的所有读写自动线程安全。
四、文件作用域与命名空间作用域中的 static
例如在 A.cpp 中:
static int g_count = 0;
这里的 static 主要影响的是链接属性。
该变量具有内部链接:
只能在当前翻译单元中使用
例如:
A.cpp
static int value = 10;
B.cpp
static int value = 20;
这两个 value 是不同变量。
静态函数同理:
static void Func()
{
}
表示该函数只供当前翻译单元使用。
这种 static 和类成员中的:
static int count;
并不是同一个概念。
前者强调:
当前翻译单元内部可见
后者强调:
整个类共享一份成员
五、static 与类模板
模板中的静态成员尤其容易混淆。
例如:
template <typename T>
class Test
{
public:
static int count;
};
然后:
Test<int>
Test<double>
属于两个不同的模板实例。
因此:
Test<int>::count
和:
Test<double>::count
不是同一个变量。
可以理解成:
Test<int>
└── count A
Test<double>
└── count B
线程池中:
template <typename T>
class ThreadPool
{
private:
static ThreadPool<T>* inc;
static Mutex _lock;
};
意味着:
ThreadPool<int>
拥有自己的 inc 和 _lock
ThreadPool<Task>
拥有自己的 inc 和 _lock
所以这种实现不是"整个程序只有一个线程池",而是:
每一种不同的
ThreadPool<T>各自拥有一个单例。
六、C++17 中的 inline static
传统写法:
class A
{
public:
static int count;
};
int A::count = 0;
从 C++17 开始,可以使用:
class A
{
public:
inline static int count = 0;
};
这样可以直接在类定义内部完成定义和初始化。
对于模板类尤其方便:
template <typename T>
class ThreadPool
{
private:
inline static Mutex _lock;
inline static ThreadPool<T>* inc = nullptr;
};
不再需要在类外额外写:
template <typename T>
Mutex ThreadPool<T>::_lock;
以及:
template <typename T>
ThreadPool<T>* ThreadPool<T>::inc = nullptr;
如果是固定常量,还可以使用:
inline static constexpr int gnum = 5;
其中:
static
表示属于类
constexpr
表示编译期常量
inline
允许直接在头文件中的类定义内完成定义
七、static 与多线程安全
static 本身绝不代表线程安全。
例如:
static int count = 0;
如果多个线程同时执行:
++count;
仍然可能发生数据竞争。
static 只解决:
变量有几份
变量存在多久
变量在哪里可见
并不会自动提供:
互斥
原子性
同步
因此多线程共享修改静态变量时,仍然需要根据情况使用:
std::mutex
或者:
std::atomic<int>
例如:
inline static std::atomic<int> count{0};
比普通的:
inline static int count = 0;
更适合多个线程并发递增。
八、static 的核心规则总结
学习 static 时,不应把它机械理解成"全局共享"。更准确的判断方法是先看它出现在哪里。
类中的:
static int x;
表示数据属于类,不属于单个对象。
类中的:
static void Func();
表示函数不依赖具体对象,因此没有 this 指针。
函数内部的:
static int x;
表示变量只初始化一次,作用域仍在函数内部,但生命周期持续到程序结束。
文件或命名空间作用域中的:
static int x;
表示该名字具有内部链接,主要限制在当前翻译单元中使用。
模板类中的静态成员:
template<typename T>
class A
{
static int x;
};
则是每一种模板实例各有一份。
最后还需要牢牢记住:
static ≠ 线程安全
static ≠ 全局变量
static ≠ 所有场景含义相同
判断 static 的含义,必须结合它所在的作用域、对象关系和链接环境 。这也是理解 C++ 中 static 最可靠的方法。