存储类关键字
| 关键字 | C++用途 | C是否支持 |
|---|---|---|
auto | 自动类型推导(C++11起语义改变) | ✔(语义不同) |
static | - 文件作用域的静态变量/函数 - 类的静态成员 - 局部静态变量 | ✔ |
extern | 声明外部链接的变量/函数 | ✔ |
register | 建议编译器将变量放入寄存器(C++17弃用) | ✔ |
1.1 auto
(1) C++ 中的auto(类型自动推导)
自C++11 起, auto 表示让编译器自动根据初始化表达式的类型 推导出变量类型,如:
auto x = 10; // x 是 int
auto y = 3.14; // y 是 double
auto z = "hello"; // z 是 const char*
auto v = std::vector<int>{1, 2, 3}; // v 是 std::vector<int>
这样做的好处也是十分明显的,可以显著减少冗长代码、防止类型错误以及更易维护。但要注意的是auto 必须初始化,否则编译器无法推导类型,同时也应该明确auto 不是动态类型,是编译期推导!
(2) C 中的 auto(自动变量,即默认局部变量)
在 C 语言中,auto 是一种存储类型说明符,用于指定变量的存储期是自动的(即作用域内有效,离开自动销毁),如:
auto int x = 5; // 等同于 int x = 5;
在实际开发中,几乎可以完全忽略,因为局部变量默认auto,只有用于强调时才偶尔使用。
1.2 static
(1) 函数内的 static 变量(静态局部变量)
在函数内声明的 static 变量只初始化一次,其生命周期是整个程序运行期间,但作用域仅限于函数内部, 如:
void counter() {
static int count = 0; // 只初始化一次
count++;
std::cout << count << "\n";
}
“程序运行期间”指的是:从程序启动开始(main 函数被调用),到程序结束(返回或退出)之间的这段时间。
调用 counter() 三次,输出将是:
1
2
3
普通局部变量每次调用都会重新初始化,而
static保留上次的值。
(2) 全局范围或命名空间中的 static(内部链接)
该种场景下的static 使该变量或函数在当前翻译单元中可见,即不对其他文件暴露, 可用于实现模块封装,避免全局命名冲突,如:
// file1.cpp
static int g_val = 42; // 只在本文件中可见
static void helper() { // 外部文件无法调用
std::cout << "Helper\n";
}
(3) 类中的 static 成员(类共享变量或函数)
该种情形下,static 修饰的成员变量和函数属于类本身,而不是某个对象,所有对象共享一份,如:
class MyClass {
public:
static int count;
static void showCount() {
std::cout << count << "\n";
}
};
// 定义并初始化
int MyClass::count = 0;
可通过
MyClass::count或任意对象访问。
(4) static 与 lambda 表达式(C++17)
在 C++17 中,lambda 表达式内部也可以使用 static 局部变量,实现惰性初始化或记忆状态,如:
auto f = []() {
static int id = 0;
return id++;
};
1.3 extern
extern 是 C 和 C++ 中用于**声明变量或函数存在于其他翻译单元(源文件)**的关键字,表示“外部链接”。
从编译器和链接器的角度看,extern 的作用是:
“告诉编译器:这个符号(变量/函数)不在当前文件定义,你先别报错,去别的目标文件或库里找。 ”
这意味着编译器 只做语义检查,不分配空间,真正解决这个符号的位置是在链接阶段。
(1) 跨文件共享变量(全局变量声明)
当下存在两个.cpp 文件,一个定义变量,一个使用变量:
🔸 config.cpp
int x = 42; // 定义
🔸 main.cpp
#include <iostream>
extern int x; // 声明:变量定义在别处
int main() {
std::cout << x << "\n";
return 0;
}
当使用g++编译时,需执行:
g++ main.cpp config.cpp -o main
-
编译阶段(每个文件单独编译为 .o)
-
main.cpp➜main.o:-
记录了对符号
x的“未定义引用”(undefined symbol)。 -
不知道
x在哪,只知道用到了x。 -
config.cpp➜config.o: -
记录了符号
x的“定义”(defined symbol)。 -
分配内存、初始化、记录符号地址。
-
-
-
链接阶段(将多个
.o合并成可执行程序)-
链接器(如 GNU
ld)将:-
把
main.o中的x的未定义引用, -
和
config.o中的x的定义对应起来, -
然后把地址修正为
config.o中真正的地址。
-
-
(2) 声明外部函数
该场景通常是在创建头文件时,为了保证代码提供的接口清晰,我们常选择仅在头文件中声明,在其他文件中给予定义。如下所示:
🔸 utils.hpp
extern int fib(int n); // 声明函数
🔸 utils.cpp
#include <iostream>
int fib(int n) {
if (n <= 1) return n;
int prev = 0, curr = 1;
for (int i = 2; i <= n;++i) {
int k = curr + prev;
prev = curr;
curr = k;
}
return curr;
}
🔸 main.cpp
#include "utils.hpp"
int main() {
std::cout << fib(2) << endl;
return 0;
}
| 阶段 | main.cpp 里的 fib(); |
|---|---|
| 编译阶段 | 编译器只检查 fib() 是否有合法声明 |
| 链接阶段 | 链接器寻找 fib 的符号定义(如在 utils.o) |
以上过程可以使用nm 进行查看,具体操作如下:
g++ -c main.cpp
g++ -c utils.cpp
nm main.o
nm utils.o
(3) extern "C"(C++ 特有)
用于防止 C++ 的名称修饰(name mangling) ,从而让 C++ 调用 C 函数。在 C++ 中,函数名会被“名称修饰”(Name Mangling),例如:
void foo(int);
在目标文件中可能会变成:
_Z3fooi
这种修饰操作可能会导致C++无法直接连接C 写的函数,因此我们需要对C函数进行extern C 进行修饰,如下示例:
extern "C" {
void c_function(); // 调用 C 写的函数
}
如果你用的是 .h 文件来自 C 项目,常见形式如下:
#ifdef __cplusplus
extern "C" {
#endif
void c_api();
#ifdef __cplusplus
}
#endif
注意事项
-
extern不定义变量,只是声明,不分配内存; -
如果只写
extern int x;而没有int x = 0;在别处定义,会导致链接错误(undefined reference); -
函数的
extern是默认属性,不写也一样。
1.4 register
在 C/C++ 中,register 是一个存储类说明符,用于提示编译器:
把变量尽可能放在 CPU 寄存器 中,而不是 RAM,以提高访问速度。
基本语法
register int counter = 0;
考虑一下,编译器会真的放进寄存器吗?
不一定。register 是一种 建议,现代编译器通常会忽略它,因为:
-
编译器比程序员更清楚何时使用寄存器(有优化器在)。
-
编译器已经在做寄存器分配优化。
所以从 C++17 起,register 已被弃用,因为它不再有实际作用。
由 register 修饰的变量存在一定限制
不能对 register 变量使用地址运算符 &,原因也是很清楚,如果变量在寄存器中,没有内存地址可取。
同时我们也应当注意,extern 只能用于局部变量(函数体内),不能是全局变量或类成员。
早期典型的用途就是在循环优化频繁访问的变量,不过现代编译器通常不需要你手动指定 register,会自动把 i 和 sum 放入寄存器中。
现代编译器如何分配寄存器 ?
编译器后端在生成汇编代码前会经历以下几个步骤:
- 中间表示(IR)生成
将你的 C/C++ 源代码编译成中间语言(如 LLVM IR),再做一系列优化。
- 变量活跃性分析(Liveness Analysis)
分析每个变量在什么范围内是“活跃”的,即:
变量的值在那一段时间内会被使用,不能随便覆盖。
- 构建干涉图(Interference Graph)
-
节点:变量
-
边:表示两个变量活跃期重叠(不能放在同一个寄存器)
- 图着色算法(Graph Coloring)
把变量“着色”分配到寄存器(比如 RAX, RBX...),尽量避免冲突。
-
如果寄存器不足,就发生 Spill(溢出到内存)
- 编译器会自动插入
mov指令,将变量写入栈
- 编译器会自动插入
- 指令选择和机器码生成
根据寄存器分配结果生成最终汇编代码。
现代编译器会:
-
内联函数:减少调用开销
-
消除死代码
-
常量折叠与传播
-
尽量把频繁使用的变量放入寄存器
-
使用 SIMD 寄存器(如 XMM、YMM)优化向量运算