当前位置: 首页 > news >正文

static详解

static在C/C++中当中非常重要,它能够控制变量和函数的生命周期、作用域、存储方式,还能在面向对象编程中实现类级别的共享特性。
C和C++中的static有不同的用法,我们通过这两者之间的区别进行学习。

C语言中的static

文件作用域的static

当static用于修饰函数外部定义的全局变量或函数时,它的核心作用是改变链接属性,将其从默认的外部链接改为内部链接
外部链接:默认情况下,全局变量和函数具有外部链接。这意味着它们不仅在当前源文件中可见,而且可以被其他源文件通过extern声明来访问和使用。链接器在链接多个目标文件时,会解析这些具有外部链接的符号名,确保它们在整个程序中是唯一的(或者说,定义只有一个)。
内部链接:使用static修饰后,全局变量或函数的名字将只在当前的源文件中可见。其它源文件即是使用extern声明也无法访问到它们。链接器在处理具有内部链接的符号时,不会将它们暴露给其它编译单元。
为什么需要内部链接?

  1. 避免命名冲突:在一个大型项目中,不同的模块(源文件)可能会无意中定义相同名称的全局变量或辅助函数。如果它们都具有外部链接,链接器会报告“符号重定义”错误。将只在模块内部使用的全局变量和函数声明为static,可以有效地将它们“隐藏起来”,避免与其他模块的同名符号冲突。这是一种基本的封装手段。
  2. 限制作用域,提高模块化:明确告知其它开发者(以及编译器和链接器),这个变量或函数是本模块内部使用的,不应该被外部直接依赖。这有助于降低模块间的耦合度,提高代码的可维护性。

我们先来复习一下extern的作用。在C++中extern关键字有两个作用:

  1. 声明变量的外部链接:当一个变量在一个文件中声明,但在另一个文件中定义时,我们可以使用extern关键字来告知编译器该变量在其他地方已经定义了,避免编译器的编译错误。
  2. extern "C":在导出C++函数符号时,通过extern "C",可以保证导出的符号为C符号,而不是C++的符号,这样可以更好的做兼容。
    假设我们有两个文件test1.cppmain.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_afunc因为static的修饰,被限制在了module_a.cpp内部,module_b.cpp无法直接触及它们,从而保证了module_a.cpp的内部实现细节不被外部干扰,也避免了潜在的命名冲突。
举个例子,我们可以将源文件想象成一个房间,static修饰的全局变量/函数就像是房间里的私人物品,只有房间内的人(代码)可以使用。而没有static修饰的变量或者函数,就是公共物品,所有房间的人(其它源文件)都可能看到和使用。

函数作用域的static

当static用于修饰在函数内部定义的 局部变量时,它的含义完全不同。它不再影响链接属性(局部变量本来就没有链接属性),而是改变变量的存储期。
存储期分为自动存储期和静态存储期。

  • 自动存储期
    默认情况下,函数内部的局部变量具有自动存储期。它们在程序执行进入其作用域时被创建和初始化,在退出作用域时被销毁。每次函数调用都会创建新的实例,它们的值在函数调用之间不会保留。它们通常存储在栈上
  • 静态存储期
    使用static修饰后,局部变量将具有静态存储期。这意味着:
  1. 生命周期延长:变量在程序第一次执行到其定义时被创建和初始化,并且一直存活到程序结束。它的内存通常分配在静态区而不是栈上 。
  2. 只初始化一次:带有static的局部变量的初始化只会在整个程序生命周期内发生一次(通常是在第一次执行到该定义时)。
  3. 值在调用间保持:由于变量的生命周期贯穿程序始终,它在函数调用结束后不会被销毁,其值会保留到下一次函数调用。

静态局部变量的好处是:

  1. 维护状态:需要在函数多次调用之间保持某个状态,例如计数器、缓存、标志位等。
  2. 避免重复初始化开销:如果一个局部变量的初始化比较昂贵(例如,需要计算或分配资源),但其值在后续调用中应保持不变,使用static可以确保初始化一次。
  3. 简单的单例模式(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();   // 可以直接调用同一编译单元内的匿名命名空间成员  
}

使用匿名命名空间有几个好处:

  1. 一致性:它使用C++的核心语言特性(命名空间)来解决作用域的问题,而不是依赖一个具有多重含义的关键字。
  2. 适应性更广:匿名命名空间不仅可以用于变量和函数,还可以用于类型定义(如class,struct,enum等)。static不能用于限制类型的链接属性。
  3. 清晰性:代码意图更明确,namespace清晰地标示出一块内部使用的区域。
  4. 避免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)在操作自己的书包。

相关文章:

  • 固态硬盘颗粒类型、选型与应用场景深度解析
  • Muduo网络库流程分析
  • 【Linux学习笔记】深入理解ELF和动静态库加载原理
  • python 程序实现了毫米波大规模MIMO系统中的信道估计对比实验
  • MySQL索引深度解析:从原理到实践
  • Maven Profile高级策略与冲突解决
  • 修复ubuntu server笔记本合盖导致的无线网卡故障
  • 电子学会的二级考试复习资料
  • 基于微信小程序的漫展系统的设计与实现
  • 【从0到1搞懂大模型】chatGPT 中的对齐优化(RLHF)讲解与实战(9)
  • 北京航空航天大学保研上机真题
  • 相机内参 opencv
  • Linux架构篇、第五章_03gitlab的搭建
  • Linux中的文件系统和软硬连接
  • 豆瓣电视剧数据工程实践:从爬虫到智能存储的技术演进(含完整代码)
  • 【linux】mount命令
  • 【TDengine源码阅读】taosMemoryDbgInit函数
  • Vue 3 (2) 模块化开发入门教程(ESM方式)
  • 深入解析MongoDB WiredTiger存储引擎:原理、优势与最佳实践
  • 【计算机网络】基于UDP进行socket编程——实现服务端与客户端业务
  • 手机网站排名优化软件/网站快速排名公司
  • 电影网站app怎么做的/刷赞网站推广永久
  • 弹窗网站制作器/洛阳网站建设
  • 自己做的网站怎么被百度收录/产品如何做市场推广
  • 门户网站模板源码/seo运营是什么意思
  • r2网站做生存分析/网上怎么推广产品