C++ 的 static 和 extern 关键字以及相关的概念
介绍一下 C++ 的作用域和声明周期的概念,以及链接属性的概念,最后再讲解一下 static 和 extern 的用法
static
和 extern
都是C++中常见的关键字,主要用于控制变量和函数的存储期和链接性,从而决定了标识符在内存中的生命周期以及在不同文件之间的可见性。本文会先介绍作用域和生命周期的概念,然后说明 C++ 中链接属性,最后给出static
和 extern
这两个 C++ 关键字的常见用法。
1. 作用域与生命周期#
1.1 作用域#
作用域是一个编译时的概念,定义了一个标识符在代码中可以被有效访问的区域,通常会有以下几种作用域:
- 块作用域:也称局部作用域,任何在花括号
{}
内声明的标识符,其作用域都从声明点开始,到包含它的右花括号}
结束 - 函数作用域:标签在整个函数体内都是可见的,无论在何处定义,通常仅适用于
goto
语句中的label
标签 - 全局作用域:也称文件作用域,在所有函数、类和代码块之外声明的标识符用于全局作用域,它的作用域从声明点开始,直到当前源文件的末尾。
- 类作用域:在类(
class
或struct
)内部声明的成员拥有类作用域。在类的外部访问它们需要通过类的对象、引用、指针使用成员访问运算符(.
或->
),对于静态成员要通过类名使用作用域解析运算符::
- 命名空间作用域:在
namespace
中声明的标识符的作用域限定在该命名空间内。访问它们需要使用命名空间名称和作用域解析运算符::
,或者使用using
声明。
1.2 生命周期#
生命周期是一个运行时的概念,它指的是一个对象从被创建到被销毁的时间段,当一个对象的生命周期结束后,它所占用的内存可能会被回收,访问该对象将导致未定义行为。C++ 中对象的生命周期主要由它的存储期决定的:
- 自动存储期:局部变量具有自动存储期。当程序的执行流进入其声明所在的块时,对象被创建,当程序的执行流离开该块时,对象被自动销毁。
- 静态存储期:全局变量、静态局部变量和静态类成员具有静态存储期。静态存储期的变量在
main
函数执行前就被创建和初始化,在main
函数执行后或者调用exit
时被销毁。生命周期从程序开始,到程序结束。 - 动态存储期:使用
new
或者malloc
在堆或自由存储区上分配的内存中的对象,具有动态存储期。它在new
时被创建,在delete
时被销毁,生命周期与作用域无关。 - 线程存储期:使用
thread_local
修饰的变量,其生命周期和创建的线程一致,线程结束时自动销毁。
2. 链接属性#
在 C++ 中,一个程序可以由多个源文件组成的,每个源文件被称为一个翻译单元,编译器会独立地将每个翻译单元编译成一个目标文件,最后链接器会将这些目标文件链接在一起,生成最终的可执行文件。标识符的链接属性会决定该标识符在不同的翻译单元中是否指向同一个实体。C++ 主要由三种链接属性:
- 外部链接
- 内部链接
- 无链接
其中无链接指的是标识符仅存在于当前作用域,完全无法被其他地方访问,例如局部变量、函数参数、非静态类成员等。我们这里讨论的主要是外部链接和内部链接。
2.1 外部链接#
具有外部链接的标识符,无论在多少个翻译单元中被声明,都指向同一个唯一的实体,因此可以在一个文件中定义它,然后在其他文件中声明并使用它。
具有外部链接的情况如下:
- 在函数、类和命名空间之外定义的函数
- 默认情况下全局变量具有外部链接
- 类的定义本身具有外部链接,可以在一个文件中定义类,在另一个文件中创建它的实例
- 类的静态成员变量具有外部链接。
下面是一个很常见的例子:
helper.h
:
#ifndef HELPER_H
#define HELPER_H
// 声明一个具有外部链接的变量
// 'extern' 关键字表明 g_shared_data 的定义在别处
extern int g_shared_data;
// 声明一个具有外部链接的函数
void shared_function();
#endif
cpphelper.cpp
:
#include "helper.h"
#include <iostream>
// 定义(创建并初始化)这个全局变量
// 这是唯一的定义点
int g_shared_data = 100;
// 定义这个全局函数
void shared_function()
{
std::cout << "shared_function() called. g_shared_data is: " << g_shared_data << std::endl;
}
cppmain.cpp
:
#include "helper.h"
#include <iostream>
int main()
{
std::cout << "main() started. g_shared_data is: " << g_shared_data << std::endl;
g_shared_data = 200; // 修改共享数据
shared_function(); // 调用共享函数
return 0;
}
cpp最终会输出:
main() started. g_shared_data is: 100
shared_function() called. g_shared_data is: 200
plaintext这表明 main.cpp
和 helper.cpp
访问和修改的是同一个 g_shared_data
变量。
2.2 内部链接#
具有内部链接的标识符,在每个翻译单元中都指向一个独立的、唯一的实体。即使在不同的文件中声明了同名的标识符,它们也是完全不同、互不相干的实体。
具有内部链接的情况如下:
- 使用
static
关键字修饰的全局变量和函数,这是 C 语言中实现内部链接的传统方式。
cpp// 此变量和函数只在本文件内部可见 static int file_local_count = 10; static void print_message() { std::cout << "Message from file1. Count: " << file_local_count << std::endl; }
- 匿名命名空间,这是 C++ 中实现内部链接的现代且推荐的方式,使用匿名空间可以避免为每个实体都单独写
static
。例如一个名为greeter.cpp
的内容如下:
cpp#include <iostream> // greeter.cpp 的匿名命名空间 namespace { // 这个 greet() 函数只在 greeter.cpp 文件内有效 void greet() { std::cout << "Hello from greeter.cpp!" << std::endl; } } // 这是一个普通的、具有外部链接的函数,可以被其他文件调用 void say_hello_from_greeter() { // 它调用的是自己文件内部的 greet() greet(); }
main.cpp
的内容如下:最终会输出:
cpp#include <iostream> // main.cpp 的匿名命名空间 namespace { // 这个 greet() 函数与 greeter.cpp 中的同名函数完全无关 // 它只在 main.cpp 文件内有效 void greet() { std::cout << "Hello from main.cpp!" << std::endl; } } // 声明在 greeter.cpp 中定义的函数 void say_hello_from_greeter(); int main() { // 1. 调用当前文件 (main.cpp) 的 greet() greet(); // 2. 调用另一个文件的函数,那个函数会调用它自己的 greet() say_hello_from_greeter(); return 0; }
程序顺利运行,没有出现命名冲突的情况,因为不同文件里面的相同名称的函数被各自的匿名空间隐藏了,链接器认为它们是各自私有的,不会发生冲突。
plaintextHello from main.cpp! Hello from greeter.cpp!
- 默认情况下
const
全局变量具有内部链接,这个性质是非常重要的,因为这让在头文件中定义常量成为可能。否则多个源文件引用同一个头文件时会有命名冲突。
3. static
#
static
关键字在 C++ 中有多种用途,主要用途包括以下几个方面:
- 用于修饰静态局部变量:用
static
修饰函数内部的局部变量会改变该变量的存储期。普通局部变量在函数返回时销毁,而静态局部变量是在整个生命周期内都存在的,即使函数退出了也不会被销毁。静态局部变量只有在第一次执行到声明时初始化一次,后续的函数调用会跳过初始化语句。在 C++11 之后的多线程环境下,静态局部变量的初始化过程是线程安全的。 - 用于修饰静态全局变量或函数:普通的全局变量或函数是具有外部链接性的,可以在其他的项目中通过
extern
关键字访问。如果使用static
修饰全局变量或函数,则它们的作用域会被限制在单个源文件的内部,变成内部链接。这通常可以用来解决不同文件间因同名全局变量或函数导致的命名冲突问题。 - 用于修饰类成员:当用
static
修饰类的成员时,它表示该成员属于类本身,而不是特定的对象,所有类的对象共享同一个静态数据成员的实例。静态成员变量必须在类定义之外进行初始化和定义,静态成员函数内部是没有this
指针,它们只能直接访问类的静态成员变量,不能直接访问非静态成员变量。示例:
cpp#include <iostream> class Player { public: Player() { active_players_++; // 每当创建新玩家时,计数器加一 } ~Player() { active_players_--; // 玩家被销毁时,计数器减一 } // 静态成员函数 static int get_active_players_count() { return active_players_; } private: // 静态数据成员声明 static int active_players_; }; // 静态数据成员的定义和初始化 int Player::active_players_ = 0; int main() { std::cout << "Active players at start: " << Player::get_active_players_count() << std::endl; Player p1; Player p2; std::cout << "Active players now: " << Player::get_active_players_count() << std::endl; { Player p3; std::cout << "Active players inside scope: " << Player::get_active_players_count() << std::endl; } // p3 在这里被销毁 std::cout << "Active players after scope: " << Player::get_active_players_count() << std::endl; return 0; }
4. extern
#
extern
关键字的主要作用是声明一个变量或函数是在别处定义的,主要用于在多个源文件之间共享全局变量和函数。
下面是一个使用 extern
关键字共享全局变量的例子:
config.cpp
(定义):
// 定义一个全局配置变量
int global_log_level = 2; // Definition
cppconfig.h
(声明):
#ifndef CONFIG_H
#define CONFIG_H
// 使用 extern 声明全局变量,供其他文件使用
extern int global_log_level; // Declaration
#endif
cppmain.cpp
(使用):
#include <iostream>
#include "config.h" // 包含声明
void some_function() {
if (global_log_level > 1) {
std::cout << "High detail logging enabled." << std::endl;
}
}
int main() {
some_function();
global_log_level = 0; // 可以在这里修改它
std::cout << "Log level set to: " << global_log_level << std::endl;
return 0;
}
cpp