2. C++基础概念-存储类关键字

105 阅读7分钟

存储类关键字

关键字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
  1. 编译阶段(每个文件单独编译为 .o)

    • main.cppmain.o

      • 记录了对符号 x 的“未定义引用”(undefined symbol)。

      • 不知道 x 在哪,只知道用到了 x

      • config.cppconfig.o

      • 记录了符号 x 的“定义”(defined symbol)。

      • 分配内存、初始化、记录符号地址。

  2. 链接阶段(将多个 .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,会自动把 isum 放入寄存器中。

现代编译器如何分配寄存器 ?

编译器后端在生成汇编代码前会经历以下几个步骤:

  1. 中间表示(IR)生成

将你的 C/C++ 源代码编译成中间语言(如 LLVM IR),再做一系列优化。

  1. 变量活跃性分析(Liveness Analysis)

分析每个变量在什么范围内是“活跃”的,即:

变量的值在那一段时间内会被使用,不能随便覆盖。

  1. 构建干涉图(Interference Graph)
  • 节点:变量

  • 边:表示两个变量活跃期重叠(不能放在同一个寄存器)

  1. 图着色算法(Graph Coloring)

把变量“着色”分配到寄存器(比如 RAX, RBX...),尽量避免冲突。

  • 如果寄存器不足,就发生 Spill(溢出到内存)

    • 编译器会自动插入 mov 指令,将变量写入栈
  1. 指令选择和机器码生成

根据寄存器分配结果生成最终汇编代码。


现代编译器会:

  • 内联函数:减少调用开销

  • 消除死代码

  • 常量折叠与传播

  • 尽量把频繁使用的变量放入寄存器

  • 使用 SIMD 寄存器(如 XMM、YMM)优化向量运算