static详解
static
在C/C++中当中非常重要,它能够控制变量和函数的生命周期、作用域、存储方式,还能在面向对象编程中实现类级别的共享特性。
C和C++中的static
有不同的用法,我们通过这两者之间的区别进行学习。
C语言中的static
文件作用域的static
当static用于修饰函数外部定义的全局变量或函数时,它的核心作用是改变链接属性,将其从默认的外部链接改为内部链接。
外部链接:默认情况下,全局变量和函数具有外部链接。这意味着它们不仅在当前源文件中可见,而且可以被其他源文件通过extern
声明来访问和使用。链接器在链接多个目标文件时,会解析这些具有外部链接的符号名,确保它们在整个程序中是唯一的(或者说,定义只有一个)。
内部链接:使用static修饰后,全局变量或函数的名字将只在当前的源文件中可见。其它源文件即是使用extern
声明也无法访问到它们。链接器在处理具有内部链接的符号时,不会将它们暴露给其它编译单元。
为什么需要内部链接?
- 避免命名冲突:在一个大型项目中,不同的模块(源文件)可能会无意中定义相同名称的全局变量或辅助函数。如果它们都具有外部链接,链接器会报告“符号重定义”错误。将只在模块内部使用的全局变量和函数声明为static,可以有效地将它们“隐藏起来”,避免与其他模块的同名符号冲突。这是一种基本的封装手段。
- 限制作用域,提高模块化:明确告知其它开发者(以及编译器和链接器),这个变量或函数是本模块内部使用的,不应该被外部直接依赖。这有助于降低模块间的耦合度,提高代码的可维护性。
我们先来复习一下extern
的作用。在C++中extern
关键字有两个作用:
- 声明变量的外部链接:当一个变量在一个文件中声明,但在另一个文件中定义时,我们可以使用
extern
关键字来告知编译器该变量在其他地方已经定义了,避免编译器的编译错误。 extern "C"
:在导出C++函数符号时,通过extern "C"
,可以保证导出的符号为C符号,而不是C++的符号,这样可以更好的做兼容。
假设我们有两个文件test1.cpp
和main.cpp
,并且我们想在main.cpp
中使用test1.cpp
中定义的变量。
// main.cpp#include <iostream>
using namespace std; extern int count; // 告诉编译器,count变量在其他地方定义了
int main()
{ cout << "Count:" << count << endl; return 0;
}// test1.cpp
int count = 10;
当我们编译main.cpp
时,编译器会知道count
变量在别的地方定义了。
示例代码:
// module_a.cpp
static int s_a = 10; // 内部链接的全局变量,仅module_a.cpp可见
int g_b = 10; // 外部链接的全部变量(默认) static void func() // 内部链接的函数,仅module_a.cpp可见
{ cout << "func: " << ++s_a << endl;
} void func2() // 外部链接的函数(默认)
{ cout << "func2: " << g_b << endl;
} // module_b.cpp // extern int s_a; // 尝试访问,链接时会失败
extern int g_b; // 可以访问module_a.cpp中的g_b // extern void func(); // 尝试访问,链接时会失败
extern void func2(); // 可以调用module_a.cpp中的func2 int main()
{ cout << "g_b: " << g_b << endl; func2(); // 调用module_a.cp 的公共函数 // func(); // 直接调用或通过extern声明调用都会失败 return 0;
}
在这段代码,s_a
和func
因为static的修饰,被限制在了module_a.cpp
内部,module_b.cpp
无法直接触及它们,从而保证了module_a.cpp
的内部实现细节不被外部干扰,也避免了潜在的命名冲突。
举个例子,我们可以将源文件想象成一个房间,static
修饰的全局变量/函数就像是房间里的私人物品,只有房间内的人(代码)可以使用。而没有static修饰的变量或者函数,就是公共物品,所有房间的人(其它源文件)都可能看到和使用。
函数作用域的static
当static用于修饰在函数内部定义的 局部变量时,它的含义完全不同。它不再影响链接属性(局部变量本来就没有链接属性),而是改变变量的存储期。
存储期分为自动存储期和静态存储期。
- 自动存储期
默认情况下,函数内部的局部变量具有自动存储期。它们在程序执行进入其作用域时被创建和初始化,在退出作用域时被销毁。每次函数调用都会创建新的实例,它们的值在函数调用之间不会保留。它们通常存储在栈上。 - 静态存储期
使用static修饰后,局部变量将具有静态存储期。这意味着:
- 生命周期延长:变量在程序第一次执行到其定义时被创建和初始化,并且一直存活到程序结束。它的内存通常分配在静态区而不是栈上 。
- 只初始化一次:带有static的局部变量的初始化只会在整个程序生命周期内发生一次(通常是在第一次执行到该定义时)。
- 值在调用间保持:由于变量的生命周期贯穿程序始终,它在函数调用结束后不会被销毁,其值会保留到下一次函数调用。
静态局部变量的好处是:
- 维护状态:需要在函数多次调用之间保持某个状态,例如计数器、缓存、标志位等。
- 避免重复初始化开销:如果一个局部变量的初始化比较昂贵(例如,需要计算或分配资源),但其值在后续调用中应保持不变,使用static可以确保初始化一次。
- 简单的单例模式(C风格):虽然不完成面向对象的单例,但可以用来确保某个资源或对象在函数内部只被创建一次。
示例代码:
void fun()
{ static int a = 0; // 只在第一次调用fun时初始化 cout << "static a: " << a << " "; int b = 0; cout << "b: " << b << " "; a++; b++; cout << endl;
} int main()
{ for(int i = 0; i < 5; i++) // 调用5次 fun(); // 每次a的值都会加1, b的值不会变 return 0;
}
结果:
static a: 0 b: 0
static a: 1 b: 0
static a: 2 b: 0
static a: 3 b: 0
static a: 4 b: 0
可以看到,静态局部变量,只初始化一次,而自动局部变量,每次调用都重新初始化。
需要注意的是,在C语言的多线程环境中,如果多个线程同时调用包含静态局部变量初始化的函数,其初始化过程可能不是线程安全的(取决于编译器实现和C标准版本,C11之前没有强制规定)。静态条件可能导致变量被初始化多次或初始化不完整。C++11及之后对此有明确的线程安全保证。
总结:
虽然static全局内容放到了头文件不会出错,但是会导致所有引用这个头文件的翻译单元都会有一份副本。如果想要所有翻译单元访问同一份资源,那应该用extern头文件声明+源文件定义的方式。
如果想要各自翻译单元有自己的资源,但是资源名字一样,那么应该在源文件中分别定义自己的static全局资源(不推荐这种写法)。
C++中的static
C++完全继承了C语言中static的上述两种用法,但也对其进行了一些演进,并赋予了它在类中的全新含义。
继承自C的用法及其演进
我们仍然可以在C++全局作用域或命名空间作业域使用static来定义具有内部链接的变量和函数。效果与C完全相同。
在C++中,我们更推荐使用匿名空间的方式来代替static实现内部链接。
namespace
{ // <--- 匿名命名空间开始 int internal_counter = 0; void internal_helper(){ // 默认具有内部链接 }
} // <--- 匿名命名空间结束 // C++ 不推荐方式(但仍然有效):static
// static int old_style_counter = 0;
// static void old_style_helper() {...} void public_api()
{ internal_helper(); // 可以直接调用同一编译单元内的匿名命名空间成员
}
使用匿名命名空间有几个好处:
- 一致性:它使用C++的核心语言特性(命名空间)来解决作用域的问题,而不是依赖一个具有多重含义的关键字。
- 适应性更广:匿名命名空间不仅可以用于变量和函数,还可以用于类型定义(如class,struct,enum等)。static不能用于限制类型的链接属性。
- 清晰性:代码意图更明确,namespace清晰地标示出一块内部使用的区域。
- 避免static歧义:减少static关键字段负担,让它主要承担在类中和函数内的角色。
静态局部变量
C++11及以后的线程安全初始化保证静态局部变量的初始化时线程安全的。这意味着即是多个线程可能同时首次执行到该static变量的定义处,C++运行时环境会确保初始化过程只发生一次,并且其它线程会等待初始化完成后才能继续执行。这极大地简化了在多线程环境下使用静态局部变量的代码。
#include <iostream>
#include <thread>
#include <vector>
using namespace std; class Singleton {
public: static Singleton& getInstance() { static Singleton instance; cout << "获取单例线程:thread "<< this_thread::get_id() << endl; return instance; } void showMessage() { cout << "单例调用:(" << this << ") says hello!" << endl; }
private: Singleton() { cout << "单例创建: thread " << this_thread::get_id() << endl; this_thread::sleep_for(chrono::milliseconds(100)); } ~Singleton() = default; Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete;
}; void worker()
{ Singleton::getInstance().showMessage();
} int main()
{ vector<thread> threads; cout << "Creating threads..." << endl; for(int i = 0; i < 5; i++) threads.emplace_back(worker); for(auto& t : threads) t.join(); cout << "All threads finished." << endl; return 0;
}
运行输出:(VS2019)
C++类中static
这是static在C++中独有的、至关重要的用法。当static用于修饰类的成员时,它表示该成员属于类本身,而不是类的任何特定对象(实例)
静态成员变量:
共享性:静态成员变量是该类所有对象共享的。无论创建了多少个类的对象,或者即使没有创建任何对象,静态成员变量都只有一份内存副本。
生命周期:它们具有静态存储期,在线程启动时被创建和初始化,直到程序结束才销毁。它们不存储在对象实例的内存布局当中。
作用域:它们属于类的作用域,访问时需要使用类名和作用域解析运算符::,或者通过类的对象或指针/引用访问,但推荐使用类名访问以强调其类成员的身份。
定义与初始化:
通常需要在类定义外部进行定义和初始化。在类定义内部只是声明。这一定义通常放在对应的
.cpp
源文件中,以确保它只被定义一次。
例外:const static整型/枚举成员(C++11前)或者constexpr static成员(C++11起):如果一个静态成员变量是const的整型(int,char,bool等)或枚举类型,可以在类定义内部直接初始化。
示例:
// counter.h
class ObjectCounter
{
public: ObjectCounter() { s_count++; // 对象创建时,共享计数器加1 } ~ObjectCounter() { s_count--; // 对象销毁是,共享计数器减1 } // 声明静态成员变量 static int s_count; // C++17: inline static member(可以在头文件定义和初始化) inline static double s_version = 1.0; // const static integral member(可在类内初始化) const static int s_maxObjects = 100; // constexpr static member(C++11,可在类内初始化,编译器常量) constexpr static const char* s_typeName = "ObjectCounter"; static int getCount(){ return s_count;} // 静态成员函数访问静态成员变量
}; // counter.cpp(如果不用inline static,需要在这里定义非const/constexpr static成员)
// include "counter.h"
// int ObjectCounter::s_count = 0; // 定义并初始化
用途:类范围的常量、所有对象共享的状态(如对象计数器、共享资源指针)、配置参数等。
静态成员函数:
属于类,不依赖对象:静态成员函数也是属于类本身的,调用它们不需要创建类的对象。
无 this 指针:最关键的区别在于,静态成员函数没有隐式的 this 指针。因为它们不与任何特定对象关联。
访问限制:由于没有 this 指针,静态成员函数不能直接访问类的非静态成员(变量或函数)。它们只能直接访问类的其他静态成员(静态变量和静态函数)。如果需要访问非静态成员,必须通过传递一个对象实例的引用或指针来实现。
调用方式:通常使用类名和作用域解析运算符 :: 调用(ClassName::staticFunc())。也可以通过对象或指针/引用调用(object.staticFunc() 或 ptr->staticFunc()),但这会掩盖其静态本质,不推荐。
想象一个班级(Class。静态成员变量就像是班级的公告栏(s_count),上面记录着全班的总人数,所有学生(对象)共享这个信息。静态成员函数就像是班主任(getCount),他可以查看和更新公告栏信息,但他的行动不依懒于某个特定的学生(没有this)。而非静态成员变量就像是每个学生的书包(instance_data),里面装着各自的东西。非静态成员函数就像是学生自己整理书(instance_method()),需要明确是哪个学生(this)在操作自己的书包。