烹饪披萨机
110.73M · 2026-03-10
在C++中,static 是一个极具多义性的关键字,其具体含义取决于它出现的上下文。
而它的多义性也造就了其复杂和难以理解,所以今天我们来介绍和梳理一下static 关键字的用法。
在 C 语言的世界里,static 表面上只是个关键字,实际上却掌管着生命周期和作用域两套大权。
它能让一个变量“活”得久,也能让一个变量或函数“藏”得深。今天我们就来揭开它的三种用法。
定义:在函数内部用 static 修饰的变量。
特性:
来看个代码示例:
void counter()
{
static int count = 0; // 只初始化一次
count++;
printf("我被调用了 %d 次n", count);
}
int main()
{
counter(); // 输出:我被调用了 1 次
counter(); // 输出:我被调用了 2 次
counter(); // 输出:我被调用了 3 次
return 0;
}
我们要注意的是:
定义:在所有函数之外(全局范围)用 static 修饰的变量。
特性:
代码示例:
// file1.c
static int secret = 5; // 只能在本文件使用
void print_secret()
{
printf("秘密是:%dn", secret);
}
// file2.c
extern int secret; // 尝试引用,但链接时会失败(找不到符号)
void try_steal()
{
secret = 100; // 链接错误
}
静态全局变量避免了不同文件之间的命名冲突,是模块化编程的好帮手。
不过要知道在头文件中定义静态全局变量,且该头文件被多个 .c 文件包含,则每个 .c 文件都会拥有自己独立的副本。
定义:用 static 修饰的函数。
特性:
// helper.c
static int add(int a, int b) // 静态函数,仅本文件可用
{
return a + b;
}
int public_add(int a, int b) // 普通函数,可被其他文件调用
{
return add(a, b); // 内部调用静态函数
}
// main.c
extern int add(int, int); // 链接失败
int main()
{
add(3, 4); // 链接错误:undefined reference to `add`
public_add(3, 4); // 正确,因为 public_add 是外部函数
return 0;
}
静态函数可以避免与其它文件中的同名函数冲突。
如果你写了一个库,内部使用的工具函数都可以声明为 static,这样别人就算想调用也无门。
在 C 语言中,函数默认是全局的(外部链接),static 可以将其变为内部链接。
| 用法 | 作用域(链接属性) | 生命周期 | 关键点 |
|---|---|---|---|
| 静态局部变量 | 函数内 | 程序运行期 | 保持值不变,仅初始化一次 |
| 静态全局变量 | 文件内(内部链接) | 程序运行期 | 隐藏全局变量,避免命名冲突 |
| 静态函数 | 文件内(内部链接) | 程序运行期 | 隐藏函数实现,增强模块封装 |
记住这三条,你就能在 C 语言中轻松驾驭 static 了。
不过要注意,C++ 里的 static 又多了新花样(比如类静态成员),那是另一个故事了。
到了 C++ 的类世界里,static 不再属于某个对象,而是属于整个类,是所有对象共享的“公共财产”。
今天我们就来认识一下 C++ 静态成员的三个面孔。
定义:在类中用 static 修饰的成员变量。
特性:
值得注意的是静态数据成员必须在类外定义并初始化,除非是 const 整型/枚举类型可以在类内直接初始化。
不过C++17 引入了 inline static 静态成员,可以直接在类内定义并初始化,无需类外定义。
代码示例:
class Classroom
{
public:
static int blackboard; // 静态成员变量声明
int seat; // 普通成员
};
int Classroom::blackboard = 0; // 类外定义并初始化
// C++17 可以这样:
class Classroom
{
public:
inline static int blackboard = 0; // 类内定义,无需类外
};
静态数据成员的类型可以是它所属的类类型(普通成员只能是指针或引用,因为类不完整),因为静态成员不包含在对象内,所以类类型是完整的。
静态数据成员也可以用作成员函数的默认实参(普通成员不行,因为依赖于对象)。
代码示例:
class Classroom
{
public:
static Classroom sc; // 类型可以是它所属的类类型
static int blackboard; // 静态数据成员声明
int seat; // 普通成员
Classroom(int val = 0) : seat(val) {}
void func(int count = blackboard) //静态数据成员可以用作默认实参
{
std::cout << count << std::endl;
}
// void func(int count = seat); 编译错误
};
int Classroom::blackboard = 100; // 类外定义并初始化
int main()
{
Classroom c;
c.func(); // 输出:100
c.func(200); // 输出:200
Classroom::blackboard = 300;
c.func(); // 输出:300
return 0;
}
静态数据成员属于类本身,不依赖于任何对象,因此可以在默认实参中安全使用。
定义:用 static 修饰的成员函数。
特性:
没有 this 指针:因此它无法访问普通成员(非静态成员),只能访问静态成员(静态数据成员和其他静态成员函数)。
调用方式:可以通过 '类名::静态函数' 或 '对象.静态函数' 调用,但即使通过对象调用,函数内部也没有当前对象的 this。
不能是 const 或 virtual:
可以是私有成员:静态函数也可以是私有的,只能被类内部调用。
代码示例:
class Classroom {
private:
static int totalStudents;
int mySeat;
public:
Classroom(int val = 0) : mySeat(val) {}
static void showTotal()
{
std::cout << "总人数:" << totalStudents << std::endl;
// std::cout << mySeat; // 不能访问非静态成员
}
void registerStudent()
{
totalStudents++; // 普通成员函数可以访问静态成员
}
};
int Classroom::totalStudents = 0;
int main()
{
Classroom::showTotal(); // 类名调用
Classroom c;
c.showTotal(); // 对象调用(但内部没有this)
return 0;
}
静态成员函数不能访问非静态成员,但非静态成员函数可以访问静态成员。
静态成员函数可以被继承,但不会随着派生类而有多态行为(override 无效,隐藏规则依然适用)。
定义:用 static const 或 static constexpr 修饰的成员。
特性:
初始化方式:
代码示例:
class MyClass {
public:
static const int MAX_COUNT = 1000; // C++11之前 类内初始化(整型常量)
static constexpr double PI = 3.14159; // C++11 常量表达式
static const std::string NAME; // 非整型,只能在类外定义
};
const std::string MyClass::NAME = "xingxing"; // 类外定义
因为constexpr 静态成员隐式是 inline(C++17 起),所以一般不需要类外定义。
| 类型 | 访问权限 | 是否需要对象 | 能否访问普通成员 | 用途 |
|---|---|---|---|---|
| 静态成员变量 | 类内任意访问 | 否 | / | 跨对象共享数据 |
| 静态成员函数 | 类内任意访问 | 否 | 不能 | 操作静态数据 |
| 静态常量成员 | 类内任意访问 | 否 | / | 提供类级别的常量 |
好了,现在我们认识了static 的各种形态。
但就像与别人相处久了,会发现他们的一些小毛病。
所以我们来看看 static 的那些坑。
我们在不同源文件中定义的静态对象,它们的初始化顺序是未定义的。
如果你的一个静态对象依赖于另一个文件中的静态对象,程序可能崩溃。
代码示例:
// defs.h
#pragma once
#include <iostream>
class A {
public:
A(int val) : m_val(val)
{
std::cout << "A constructed with " << m_val << std::endl;
}
int getVal() const { return m_val; }
private:
int m_val;
};
// 声明全局静态对象(将在不同文件中定义)
extern A a;
class B {
public:
B()
{
// 此处使用了另一个文件中的静态对象 a
// 如果 a 尚未初始化,getVal() 将访问未定义内存
std::cout << "B constructed, a is value = " << a.getVal() << std::endl;
}
};
extern B b;
我们先声明两个静态对象,然后分别在不同的文件中定义:
// a.cpp
#include "defs.h"
A a(5); // 定义静态对象 a
// b.cpp
#include "defs.h"
B b; // 定义静态对象 b,其构造函数依赖于 a
// main.cpp
#include "defs.h"
int main()
{
std::cout << "Entering main()" << std::endl;
// 什么也不做,静态对象的初始化在 main 之前发生
return 0;
}
然后使用g++编译:g++ -o test a.cpp b.cpp main.cpp。正常输出:
A constructed with 5
B constructed, a is value = 5
Entering main()
我们多次运行后可能会出现以下情况:
B constructed, a is value = 0 (0 或 随机值)
A constructed with 5
Entering main()
因为 a 尚未构造,其 m_val 内存未初始化,getVal() 返回未定义值。
我们可以将全局静态对象改为函数内的局部静态对象,这样它们在第一次调用时才会初始化,并且初始化顺序由调用顺序决定,完全可控。
例如:
A& getA()
{
static A a(5);
return a;
}
B& getB()
{
static B b; // B 的构造函数可以安全调用 getA()
return b;
}
老生常谈的问题(C++11 之前):函数内的静态局部变量初始化不是线程安全的。如果多个线程同时第一次调用这个函数,它们可能都会试图初始化这个静态变量,导致未定义行为。
C++11 标准规定:静态局部变量的初始化是线程安全的——编译器会自动加锁,保证只有一个线程执行初始化。
当然了,这仅保证初始化的线程安全,后续对对象的并发访问仍需我们自己加锁。
类模板中的静态数据成员,可以为特定的模板参数提供专门的实现。
代码示例:
template<typename T>
struct MyTemp
{
static int value; // 静态数据成员声明
};
// 主模板定义
template<typename T>
int MyTemp<T>::value = 0;
// 针对 int 的特化
template<>
int MyTemp<int>::value = 100; // 给 int 开小灶
// 针对 double 的特化(可以有不同的初始值)
template<>
int MyTemp<double>::value = 200;
不过要注意,静态数据成员的特化必须出现在使用之前,否则可能导致隐式实例化冲突。
这是一个容易混淆的话题,我们分两点说:
静态成员函数可以继承,而且没有二义性问题:
class Base {
public:
static void whoAmI() { std::cout << "Base" << std::endl; }
};
class Derived : public Base {};
int main()
{
Derived::whoAmI(); // 输出 "Base" —— 从 Base 继承而来
Base::whoAmI(); // 也是 "Base"
return 0;
}
如果派生类定义了同名的静态函数,则隐藏基类的版本(而不是重载或多态)。
严格来说,静态数据成员不被继承,但派生类可以访问基类的静态成员:
class Base {
public:
static int shared;
};
int Base::shared = 10;
class Derived1 : public Base {
// 没有定义自己的 shared
};
class Derived2 : public Base {
public:
static int shared; // 定义自己的 shared
};
int Derived2::shared = 20;
int main()
{
std::cout << Base::shared << std::endl; // 10
std::cout << Derived1::shared << std::endl; // 10 —— 访问的是 Base::shared
std::cout << Derived2::shared << std::endl; // 20 —— 自己的版本
Derived1::shared = 30; // 修改的是 Base::shared
std::cout << Base::shared << std::endl; // 30 —— 证实了这一点
return 0;
}
在整个继承体系中,静态数据成员只有一份实例,所有派生类共享基类的静态成员(除非派生类自己重新定义)。
派生类重新定义静态成员时,是隐藏基类的版本,而不是覆盖或继承。
写了这么多,有点累(想偷懒),就先简单介绍两个应用场景吧。
假如我们需要一个类只有一个实例,并提供全局访问点,可以使用单例模式。
static 在其中的作用:
代码示例:
class Singleton {
private:
Singleton() {} // 私有构造函数
~Singleton() {} // 私有析构
Singleton(const Singleton&) = delete; // 禁止拷贝
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton& getInstance()
{
static Singleton instance; // 静态局部变量,线程安全的初始化
return instance;
}
void doSomething() { /* ...摸鱼中... */ }
};
单例模式在整个程序里只有一个,谁想使用它就得调用getInstance()。
从初始化到程序结束只能有一个,所以要禁止拷贝。
当然析构函数也要处理好,避免资源泄漏。
当我们需要统计一个类当前有多少个存活的对象,或者总共创建了多少个对象,可以使用。
static 在其中的作用:
代码示例:
class Widget {
private:
static int aliveCount; // 存活对象计数
static int totalCreated; // 总共创建计数
public:
Widget()
{
++aliveCount;
++totalCreated;
}
~Widget()
{
--aliveCount;
}
Widget(const Widget&)
{
++aliveCount;
++totalCreated;
}
Widget& operator=(const Widget&) = default; // 赋值不影响计数
static int getAliveCount() { return aliveCount; }
static int getTotalCreated() { return totalCreated; }
};
int Widget::aliveCount = 0;
int Widget::totalCreated = 0;
静态计数器可以统计构造出多少个对象,每调用一次析构就会减1。
想知道还有多少个对象存活,直接调用getAliveCount()即可。
如果有拷贝构造和移动构造的话也要正确处理计数。
要是在多线程环境下使用,计数器操作需要是原子的(std::atomic)或加锁保护。
什么结尾?没有结尾,摸鱼去了。