C++ 中 static 关键字的作用与使用规则

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 最可靠的方法。

相关推荐
蒸蒸yyyyzwd4 小时前
cpp 选手秋招学习笔记 day33
c++·笔记·求职招聘
一技安身9 小时前
【Docker】 镜像导出 & 导入,实验环境aarch64(arm64)Linux操作系统
linux·docker·容器
hansang_IR10 小时前
【题解】P4460 [CQOI2018] 解锁屏幕
c++·算法
代码村新手10 小时前
Linux调试器-gdb的使用
linux
ChampaignWolf11 小时前
在 SAP S/4HANA 内部基于 ICF Handler 从零实现 ABAP MCP Server:架构、代码与踩坑实录
c++·架构·sap·abap·mcp
新时代牛马11 小时前
PCI与PCIe 硬件原理、配置空间/BAR 与 Linux 驱动完整篇:从 LTSSM、TLP 到 ECAM 与probe
java·linux·服务器
尘客-追梦12 小时前
Day 01:插件化之前,先看清 ABI 这堵墙
开发语言·c++·qt
有点。13 小时前
*C++哈夫曼树与哈夫曼编码
java·c++·servlet
两点王爷13 小时前
解决 Linux 中 version `CXXABI_1.3.8‘ not found 报错
linux·运维·服务器