在C++中,static 是一个极具多义性的关键字,其具体含义取决于它出现的上下文。
而它的多义性也造就了其复杂和难以理解,所以今天我们来介绍和梳理一下static 关键字的用法。
C 语言中的用法
在 C 语言的世界里,static 表面上只是个关键字,实际上却掌管着生命周期和作用域两套大权。
它能让一个变量"活"得久,也能让一个变量或函数"藏"得深。今天我们就来揭开它的三种用法。
静态局部变量
定义:在函数内部用 static 修饰的变量。
特性:
- 生命周期:从程序开始运行到结束,只初始化一次。(注意:在 C 语言中,静态局部变量在程序启动前就被初始化了,而不是在函数调用时)。
- 作用域:仅限于定义它的函数内部,外部无法直接访问。
- 存储位置:静态存储区(而不是栈上)。
来看个代码示例:
c
void counter()
{
static int count = 0; // 只初始化一次
count++;
printf("我被调用了 %d 次\n", count);
}
int main()
{
counter(); // 输出:我被调用了 1 次
counter(); // 输出:我被调用了 2 次
counter(); // 输出:我被调用了 3 次
return 0;
}
我们要注意的是:
- 即使函数没有被调用,静态局部变量也已经分配了空间并初始化(如果有初始值的话)。
- 如果静态局部变量没有显式初始化,则自动初始化为 0(数值类型)或空指针(指针类型)。
- 不能使用函数参数或非常量表达式初始化静态局部变量。
静态全局变量
定义:在所有函数之外(全局范围)用 static 修饰的变量。
特性:
- 作用域/链接属性:文件作用域,且具有内部链接。这意味着它只在当前源文件内可见,其他源文件即使使用 extern 声明也无法访问。
- 生命周期:整个程序运行期间,与程序同生共死。
- 存储位置:静态存储区。
代码示例:
c
// file1.c
static int secret = 5; // 只能在本文件使用
void print_secret()
{
printf("秘密是:%d\n", secret);
}
// file2.c
extern int secret; // 尝试引用,但链接时会失败(找不到符号)
void try_steal()
{
secret = 100; // 链接错误
}
静态全局变量避免了不同文件之间的命名冲突,是模块化编程的好帮手。
不过要知道在头文件中定义静态全局变量,且该头文件被多个 .c 文件包含,则每个 .c 文件都会拥有自己独立的副本。
静态函数
定义:用 static 修饰的函数。
特性:
- 链接属性:内部链接,函数只在当前源文件内可见,其他文件无法调用。
- 作用:类似于把函数"私有化",限制其访问范围,有助于封装和减少全局命名空间的污染。
c
// 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++ 中的静态成员
到了 C++ 的类世界里,static 不再属于某个对象,而是属于整个类,是所有对象共享的"公共财产"。
今天我们就来认识一下 C++ 静态成员的三个面孔。
静态成员变量
定义:在类中用 static 修饰的成员变量。
特性:
- 属于类,不属于对象:无论创建多少个对象,静态数据成员只有一份拷贝,所有对象共享。
- 生命周期:程序开始运行时分配(在 main 函数执行前),程序结束时销毁。
- 存储位置:静态存储区,不在对象的内存布局中(因此 sizeof(类) 不包含静态成员)。
- 访问方式:可以通过 '类名::静态成员' 或 '对象.静态成员' 的方式访问。
值得注意的是静态数据成员必须在类外定义并初始化,除非是 const 整型/枚举类型可以在类内直接初始化。
不过C++17 引入了 inline static 静态成员,可以直接在类内定义并初始化,无需类外定义。
代码示例:
c++
class Classroom
{
public:
static int blackboard; // 静态成员变量声明
int seat; // 普通成员
};
int Classroom::blackboard = 0; // 类外定义并初始化
// C++17 可以这样:
class Classroom
{
public:
inline static int blackboard = 0; // 类内定义,无需类外
};
静态数据成员的类型可以是它所属的类类型(普通成员只能是指针或引用,因为类不完整),因为静态成员不包含在对象内,所以类类型是完整的。
静态数据成员也可以用作成员函数的默认实参(普通成员不行,因为依赖于对象)。
代码示例:
c++
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:
- 不能是 const 是因为 const 修饰的是 this 指向的对象,而静态函数没有 this。
- 不能是 virtual 是因为静态函数属于类,不参与动态绑定(多态)。
-
可以是私有成员:静态函数也可以是私有的,只能被类内部调用。
代码示例:
c++
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 修饰的成员。
特性:
- 不可修改:一旦初始化,值不能改变。
- 存储位置:取决于使用方式。如果只是作为编译时常量,可能不会分配存储空间。
初始化方式:
- C++11之前:整型或枚举类型的 static const 可以在类内直接初始化,其他类型需要在类外定义。
- C++11:可以使用 static constexpr 在类内初始化任何字面类型,且通常会在编译时求值。
- C++17:inline static const / constexpr static 可以直接在类内定义,无需类外。
代码示例:
c++
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 的那些坑。
静态初始化顺序问题
我们在不同源文件中定义的静态对象,它们的初始化顺序是未定义的。
如果你的一个静态对象依赖于另一个文件中的静态对象,程序可能崩溃。
代码示例:
c++
// 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;
我们先声明两个静态对象,然后分别在不同的文件中定义:
c++
// 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。正常输出:
css
A constructed with 5
B constructed, a is value = 5
Entering main()
我们多次运行后可能会出现以下情况:
css
B constructed, a is value = 0 (0 或 随机值)
A constructed with 5
Entering main()
因为 a 尚未构造,其 m_val 内存未初始化,getVal() 返回未定义值。
我们可以将全局静态对象改为函数内的局部静态对象,这样它们在第一次调用时才会初始化,并且初始化顺序由调用顺序决定,完全可控。
例如:
c++
A& getA()
{
static A a(5);
return a;
}
B& getB()
{
static B b; // B 的构造函数可以安全调用 getA()
return b;
}
静态局部变量的线程安全
老生常谈的问题(C++11 之前):函数内的静态局部变量初始化不是线程安全的。如果多个线程同时第一次调用这个函数,它们可能都会试图初始化这个静态变量,导致未定义行为。
C++11 标准规定:静态局部变量的初始化是线程安全的------编译器会自动加锁,保证只有一个线程执行初始化。
当然了,这仅保证初始化的线程安全,后续对对象的并发访问仍需我们自己加锁。
静态成员变量的模板特例化
类模板中的静态数据成员,可以为特定的模板参数提供专门的实现。
代码示例:
c++
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;
不过要注意,静态数据成员的特化必须出现在使用之前,否则可能导致隐式实例化冲突。
静态成员与继承
这是一个容易混淆的话题,我们分两点说:
- 静态成员函数的继承
静态成员函数可以继承,而且没有二义性问题:
c++
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;
}
如果派生类定义了同名的静态函数,则隐藏基类的版本(而不是重载或多态)。
- 静态成员变量的继承
严格来说,静态数据成员不被继承,但派生类可以访问基类的静态成员:
c++
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 在其中的作用:
- 静态成员指针/对象:持有唯一的实例。
- 静态成员函数:提供全局访问点(如 getInstance())。
代码示例:
c++
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 在其中的作用:
- 静态数据成员作为计数器,所有对象共享。
- 在构造函数中递增,析构函数中递减。
- 加一个静态函数返回当前计数。
代码示例:
c++
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)或加锁保护。
结尾
什么结尾?没有结尾,摸鱼去了。