static 关键字:从 C 到 C++,一篇文章彻底搞懂它的“七十二变”

在C++中,static 是一个极具多义性的关键字,其具体含义取决于它出现的上下文。

而它的多义性也造就了其复杂和难以理解,所以今天我们来介绍和梳理一下static 关键字的用法。

C 语言中的用法

在 C 语言的世界里,static 表面上只是个关键字,实际上却掌管着生命周期和作用域两套大权。

它能让一个变量"活"得久,也能让一个变量或函数"藏"得深。今天我们就来揭开它的三种用法。

静态局部变量

定义:在函数内部用 static 修饰的变量。

特性:

  • 生命周期:从程序开始运行到结束,只初始化一次。(注意:在 C 语言中,静态局部变量在程序启动前就被初始化了,而不是在函数调用时)。
  • 作用域:仅限于定义它的函数内部,外部无法直接访问。
  • 存储位置:静态存储区(而不是栈上)。

来看个代码示例:

c 复制代码
void counter() 
{
    static int count = 0; // 只初始化一次
    count++;
    printf("我被调用了 %d 次\n", count);
}

int main() 
{
    counter(); // 输出:我被调用了 1 次
    counter(); // 输出:我被调用了 2 次
    counter(); // 输出:我被调用了 3 次

    return 0;
}

我们要注意的是:

  • 即使函数没有被调用,静态局部变量也已经分配了空间并初始化(如果有初始值的话)。
  • 如果静态局部变量没有显式初始化,则自动初始化为 0(数值类型)或空指针(指针类型)。
  • 不能使用函数参数或非常量表达式初始化静态局部变量。

静态全局变量

定义:在所有函数之外(全局范围)用 static 修饰的变量。

特性:

  • 作用域/链接属性:文件作用域,且具有内部链接。这意味着它只在当前源文件内可见,其他源文件即使使用 extern 声明也无法访问。
  • 生命周期:整个程序运行期间,与程序同生共死。
  • 存储位置:静态存储区。

代码示例:

c 复制代码
// file1.c
static int secret = 5; // 只能在本文件使用

void print_secret() 
{
    printf("秘密是:%d\n", secret);
}

// file2.c
extern int secret; // 尝试引用,但链接时会失败(找不到符号)

void try_steal() 
{
    secret = 100; // 链接错误
}

静态全局变量避免了不同文件之间的命名冲突,是模块化编程的好帮手。

不过要知道在头文件中定义静态全局变量,且该头文件被多个 .c 文件包含,则每个 .c 文件都会拥有自己独立的副本。

静态函数

定义:用 static 修饰的函数。

特性:

  • 链接属性:内部链接,函数只在当前源文件内可见,其他文件无法调用。
  • 作用:类似于把函数"私有化",限制其访问范围,有助于封装和减少全局命名空间的污染。
c 复制代码
// helper.c
static int add(int a, int b) // 静态函数,仅本文件可用
{ 
    return a + b;
}

int public_add(int a, int b) // 普通函数,可被其他文件调用
{  
    return add(a, b); // 内部调用静态函数
}

// main.c
extern int add(int, int); // 链接失败

int main() 
{
    add(3, 4); // 链接错误:undefined reference to `add`
    public_add(3, 4); // 正确,因为 public_add 是外部函数
    return 0;
}

静态函数可以避免与其它文件中的同名函数冲突。

如果你写了一个库,内部使用的工具函数都可以声明为 static,这样别人就算想调用也无门。

在 C 语言中,函数默认是全局的(外部链接),static 可以将其变为内部链接。

总结

用法 作用域(链接属性) 生命周期 关键点
静态局部变量 函数内 程序运行期 保持值不变,仅初始化一次
静态全局变量 文件内(内部链接) 程序运行期 隐藏全局变量,避免命名冲突
静态函数 文件内(内部链接) 程序运行期 隐藏函数实现,增强模块封装

记住这三条,你就能在 C 语言中轻松驾驭 static 了。

不过要注意,C++ 里的 static 又多了新花样(比如类静态成员),那是另一个故事了。

C++ 中的静态成员

到了 C++ 的类世界里,static 不再属于某个对象,而是属于整个类,是所有对象共享的"公共财产"。

今天我们就来认识一下 C++ 静态成员的三个面孔。

静态成员变量

定义:在类中用 static 修饰的成员变量。

特性:

  • 属于类,不属于对象:无论创建多少个对象,静态数据成员只有一份拷贝,所有对象共享。
  • 生命周期:程序开始运行时分配(在 main 函数执行前),程序结束时销毁。
  • 存储位置:静态存储区,不在对象的内存布局中(因此 sizeof(类) 不包含静态成员)。
  • 访问方式:可以通过 '类名::静态成员' 或 '对象.静态成员' 的方式访问。

值得注意的是静态数据成员必须在类外定义并初始化,除非是 const 整型/枚举类型可以在类内直接初始化。

不过C++17 引入了 inline static 静态成员,可以直接在类内定义并初始化,无需类外定义。

代码示例:

c++ 复制代码
class Classroom 
{
public:
    static int blackboard; // 静态成员变量声明
    int seat; // 普通成员
};

int Classroom::blackboard = 0; // 类外定义并初始化

// C++17 可以这样:
class Classroom 
{
public:
    inline static int blackboard = 0; // 类内定义,无需类外
};

静态数据成员的类型可以是它所属的类类型(普通成员只能是指针或引用,因为类不完整),因为静态成员不包含在对象内,所以类类型是完整的。

静态数据成员也可以用作成员函数的默认实参(普通成员不行,因为依赖于对象)。

代码示例:

c++ 复制代码
class Classroom 
{
public:
    static Classroom sc; // 类型可以是它所属的类类型
    static int blackboard; // 静态数据成员声明
    int seat; // 普通成员

    Classroom(int val = 0) : seat(val) {}

    void func(int count = blackboard) //静态数据成员可以用作默认实参
    {
        std::cout << count << std::endl;
    }

    // void func(int count = seat); 编译错误
};

int Classroom::blackboard = 100; // 类外定义并初始化

int main() 
{
    Classroom c;
    c.func(); // 输出:100
    c.func(200); // 输出:200
    Classroom::blackboard = 300;
    c.func(); // 输出:300

    return 0;
}

静态数据成员属于类本身,不依赖于任何对象,因此可以在默认实参中安全使用。

静态成员函数

定义:用 static 修饰的成员函数。

特性:

  • 没有 this 指针:因此它无法访问普通成员(非静态成员),只能访问静态成员(静态数据成员和其他静态成员函数)。

  • 调用方式:可以通过 '类名::静态函数' 或 '对象.静态函数' 调用,但即使通过对象调用,函数内部也没有当前对象的 this。

  • 不能是 const 或 virtual:

    • 不能是 const 是因为 const 修饰的是 this 指向的对象,而静态函数没有 this。
    • 不能是 virtual 是因为静态函数属于类,不参与动态绑定(多态)。
  • 可以是私有成员:静态函数也可以是私有的,只能被类内部调用。

代码示例:

c++ 复制代码
class Classroom {
private:
    static int totalStudents;
    int mySeat;
    
public:
    Classroom(int val = 0) : mySeat(val) {}

    static void showTotal() 
    {
        std::cout << "总人数:" << totalStudents << std::endl;
        // std::cout << mySeat; // 不能访问非静态成员
    }
    
    void registerStudent() 
    {
        totalStudents++; // 普通成员函数可以访问静态成员
    }
};

int Classroom::totalStudents = 0;

int main() 
{
    Classroom::showTotal(); // 类名调用
    Classroom c;
    c.showTotal(); // 对象调用(但内部没有this)

    return 0;
}

静态成员函数不能访问非静态成员,但非静态成员函数可以访问静态成员。

静态成员函数可以被继承,但不会随着派生类而有多态行为(override 无效,隐藏规则依然适用)。

静态常量成员

定义:用 static const 或 static constexpr 修饰的成员。

特性:

  • 不可修改:一旦初始化,值不能改变。
  • 存储位置:取决于使用方式。如果只是作为编译时常量,可能不会分配存储空间。

初始化方式:

  • C++11之前:整型或枚举类型的 static const 可以在类内直接初始化,其他类型需要在类外定义。
  • C++11:可以使用 static constexpr 在类内初始化任何字面类型,且通常会在编译时求值。
  • C++17:inline static const / constexpr static 可以直接在类内定义,无需类外。

代码示例:

c++ 复制代码
class MyClass {
public:
    static const int MAX_COUNT = 1000; // C++11之前 类内初始化(整型常量)
    static constexpr double PI = 3.14159; // C++11 常量表达式
    static const std::string NAME; // 非整型,只能在类外定义
};

const std::string MyClass::NAME = "xingxing";   // 类外定义

因为constexpr 静态成员隐式是 inline(C++17 起),所以一般不需要类外定义。

总结

类型 访问权限 是否需要对象 能否访问普通成员 用途
静态成员变量 类内任意访问 / 跨对象共享数据
静态成员函数 类内任意访问 不能 操作静态数据
静态常量成员 类内任意访问 / 提供类级别的常量

静态初始化的细节与陷阱

好了,现在我们认识了static 的各种形态。

但就像与别人相处久了,会发现他们的一些小毛病。

所以我们来看看 static 的那些坑。

静态初始化顺序问题

我们在不同源文件中定义的静态对象,它们的初始化顺序是未定义的。

如果你的一个静态对象依赖于另一个文件中的静态对象,程序可能崩溃。

代码示例:

c++ 复制代码
// defs.h
#pragma once
#include <iostream>

class A {
public:
    A(int val) : m_val(val) 
    {
        std::cout << "A constructed with " << m_val << std::endl;
    }
    int getVal() const { return m_val; }
private:
    int m_val;
};

// 声明全局静态对象(将在不同文件中定义)
extern A a;

class B {
public:
    B() 
    {
        // 此处使用了另一个文件中的静态对象 a
        // 如果 a 尚未初始化,getVal() 将访问未定义内存
        std::cout << "B constructed, a is value = " << a.getVal() << std::endl;
    }
};

extern B b;

我们先声明两个静态对象,然后分别在不同的文件中定义:

c++ 复制代码
// a.cpp
#include "defs.h"

A a(5); // 定义静态对象 a

// b.cpp
#include "defs.h"

B b; // 定义静态对象 b,其构造函数依赖于 a

// main.cpp
#include "defs.h"
int main() 
{
    std::cout << "Entering main()" << std::endl;
    // 什么也不做,静态对象的初始化在 main 之前发生
    return 0;
}

然后使用g++编译:g++ -o test a.cpp b.cpp main.cpp。正常输出:

css 复制代码
A constructed with 5
B constructed, a is value = 5
Entering main()

我们多次运行后可能会出现以下情况:

css 复制代码
B constructed, a is value = 0 (0 或 随机值)
A constructed with 5
Entering main()

因为 a 尚未构造,其 m_val 内存未初始化,getVal() 返回未定义值。

我们可以将全局静态对象改为函数内的局部静态对象,这样它们在第一次调用时才会初始化,并且初始化顺序由调用顺序决定,完全可控。

例如:

c++ 复制代码
A& getA() 
{
    static A a(5);
    return a;
}
B& getB() 
{
    static B b; // B 的构造函数可以安全调用 getA()
    return b;
}

静态局部变量的线程安全

老生常谈的问题(C++11 之前):函数内的静态局部变量初始化不是线程安全的。如果多个线程同时第一次调用这个函数,它们可能都会试图初始化这个静态变量,导致未定义行为。

C++11 标准规定:静态局部变量的初始化是线程安全的------编译器会自动加锁,保证只有一个线程执行初始化。

当然了,这仅保证初始化的线程安全,后续对对象的并发访问仍需我们自己加锁。

静态成员变量的模板特例化

类模板中的静态数据成员,可以为特定的模板参数提供专门的实现。

代码示例:

c++ 复制代码
template<typename T>
struct MyTemp 
{
    static int value; // 静态数据成员声明
};

// 主模板定义
template<typename T>
int MyTemp<T>::value = 0;

// 针对 int 的特化
template<>
int MyTemp<int>::value = 100; // 给 int 开小灶

// 针对 double 的特化(可以有不同的初始值)
template<>
int MyTemp<double>::value = 200;

不过要注意,静态数据成员的特化必须出现在使用之前,否则可能导致隐式实例化冲突。

静态成员与继承

这是一个容易混淆的话题,我们分两点说:

  1. 静态成员函数的继承

静态成员函数可以继承,而且没有二义性问题:

c++ 复制代码
class Base {
public:
    static void whoAmI() { std::cout << "Base" << std::endl; }
};

class Derived : public Base {};

int main() 
{
    Derived::whoAmI(); // 输出 "Base" ------ 从 Base 继承而来
    Base::whoAmI(); // 也是 "Base"
    return 0;
}

如果派生类定义了同名的静态函数,则隐藏基类的版本(而不是重载或多态)。

  1. 静态成员变量的继承

严格来说,静态数据成员不被继承,但派生类可以访问基类的静态成员:

c++ 复制代码
class Base {
public:
    static int shared;
};
int Base::shared = 10;

class Derived1 : public Base {
    // 没有定义自己的 shared
};

class Derived2 : public Base {
public:
    static int shared;  // 定义自己的 shared
};
int Derived2::shared = 20;

int main() 
{
    std::cout << Base::shared << std::endl; // 10
    std::cout << Derived1::shared << std::endl; // 10 ------ 访问的是 Base::shared
    std::cout << Derived2::shared << std::endl; // 20 ------ 自己的版本

    Derived1::shared = 30; // 修改的是 Base::shared
    std::cout << Base::shared << std::endl; // 30 ------ 证实了这一点

    return 0;
}

在整个继承体系中,静态数据成员只有一份实例,所有派生类共享基类的静态成员(除非派生类自己重新定义)。

派生类重新定义静态成员时,是隐藏基类的版本,而不是覆盖或继承。

应用场景

写了这么多,有点累(想偷懒),就先简单介绍两个应用场景吧。

单例模式

假如我们需要一个类只有一个实例,并提供全局访问点,可以使用单例模式。

static 在其中的作用:

  • 静态成员指针/对象:持有唯一的实例。
  • 静态成员函数:提供全局访问点(如 getInstance())。

代码示例:

c++ 复制代码
class Singleton {
private:
    Singleton() {} // 私有构造函数
    ~Singleton() {} // 私有析构
    Singleton(const Singleton&) = delete; // 禁止拷贝
    Singleton& operator=(const Singleton&) = delete;

public:
    static Singleton& getInstance() 
    {
        static Singleton instance; // 静态局部变量,线程安全的初始化
        return instance;
    }

    void doSomething() { /* ...摸鱼中... */ }
};

单例模式在整个程序里只有一个,谁想使用它就得调用getInstance()。

从初始化到程序结束只能有一个,所以要禁止拷贝。

当然析构函数也要处理好,避免资源泄漏。

类级别的计数器

当我们需要统计一个类当前有多少个存活的对象,或者总共创建了多少个对象,可以使用。

static 在其中的作用:

  • 静态数据成员作为计数器,所有对象共享。
  • 在构造函数中递增,析构函数中递减。
  • 加一个静态函数返回当前计数。

代码示例:

c++ 复制代码
class Widget {
private:
    static int aliveCount;      // 存活对象计数
    static int totalCreated;    // 总共创建计数
public:
    Widget() 
    {
        ++aliveCount;
        ++totalCreated;
    }
    ~Widget() 
    {
        --aliveCount;
    }
    Widget(const Widget&) 
    {
        ++aliveCount;
        ++totalCreated;
    }
    Widget& operator=(const Widget&) = default; // 赋值不影响计数

    static int getAliveCount() { return aliveCount; }
    static int getTotalCreated() { return totalCreated; }
};

int Widget::aliveCount = 0;
int Widget::totalCreated = 0;

静态计数器可以统计构造出多少个对象,每调用一次析构就会减1。

想知道还有多少个对象存活,直接调用getAliveCount()即可。

如果有拷贝构造和移动构造的话也要正确处理计数。

要是在多线程环境下使用,计数器操作需要是原子的(std::atomic)或加锁保护。

结尾

什么结尾?没有结尾,摸鱼去了。

相关推荐
xlp666hub21 小时前
Leetcode第一题:用C++解决两数之和问题
c++·leetcode
不想写代码的星星1 天前
C++继承、组合、聚合:选错了是屎山,选对了是神器
c++
不想写代码的星星2 天前
std::function 详解:用法、原理与现代 C++ 最佳实践
c++
樱木Plus4 天前
深拷贝(Deep Copy)和浅拷贝(Shallow Copy)
c++
blasit6 天前
笔记:Qt C++建立子线程做一个socket TCP常连接通信
c++·qt·tcp/ip
肆忆_7 天前
# 用 5 个问题学懂 C++ 虚函数(入门级)
c++
不想写代码的星星7 天前
虚函数表:C++ 多态背后的那个男人
c++
端平入洛9 天前
delete又未完全delete
c++
端平入洛10 天前
auto有时不auto
c++