C++避坑指南(八)

关注公众号【高性能架构探索】,第一时间获取最新文章;公众号内回复【pdf】,免费获取经典书籍

static

笔者在前面章节吐槽了const这个命名,也吐槽了“右值引用”这个命名。那么static就是笔者下一个要重点吐槽的命名了。static这个词本身没有什么问题,其主要的槽点就在于“一词多用”,也就是说,这个词在不同场景下表示的是完全不同的含义。(作者可能是出于节省关键词的目的吧,明明是不同的含义,却没有用不同的关键词)。

  1. 在局部变量前的static,限定的是变量的生命周期

  2. 在全局变量/函数前的static,限定的变量/函数的作用域

  3. 在成员变量前的static,限定的是成员变量的生命周期

  4. 在成员函数前的static,限定的是成员函数的调用方(或隐藏参数)

上面是static关键字的 4 种不同含义,接下来逐一我会解释。

静态局部变量

当用static修饰局部变量时,static表示其生命周期:

void f() {   static int count = 0;   count++; }

上例中,count是一个局部变量,既然已经是“局部变量”了,那么它的作用域很明显,就是f函数内部。而这里的static表示的是其生命周期。普通的全局变量在其所在函数(或代码块)结束时会被释放。而用static修饰的则不会,我们将其称为“静态局部变量”。 静态局部变量会在首次执行到定义语句时初始化,在主函数执行结束后释放,在程序执行过程中遇到定义(和初始化)语句时会忽略。

void f() {    static int count = 0;    count++;    std::cout << count << std::endl; } int main(int argc, const char *argv[]) {   f(); // 第一次执行时count被定义,并且初始化为0,执行后count值为1,并且不会释放   f(); // 第二次执行时由于count已经存在,因此初始化语句会无视,执行后count值为2,并且不会释放   f(); // 同上,执行后count值为3,不会释放 } // 主函数执行结束后会释放f中的count

例如上面例程的输出结果会是:

1 2 3

详细的说明已经在注释中,这里不再赘述。

内部全局变量/函数

static修饰全局变量或函数时,用于限定其作用域为“当前文件内”。同理,由于已经是“全局”变量了,生命周期一定是符合全局的,也就是“主函数执行前构造,主函数执行结束后释放”。至于全局函数就不用说了,函数都是全局生命周期的。

因此,这时候的static不会再对生命周期有影响,而是限定了其作用域。与之对应的是extern。用extern修饰的全局变量/函数作用于整个程序内,换句话说,就是可以跨文件:

// a1.cc int g_val = 4; // 定义全局变量 // a2.cc extern int g_val; // 声明全局变量 void Demo() {   std::cout << g_val << std::endl; // 使用了在另一个文件中定义的全局变量 }

而用static修饰的全局变量/函数则只能在当前文件中使用,不同文件间的static全局变量/函数可以同名,并且互相独立。

// a1.cc static int s_val1 = 1; // 定义内部全局变量 static int s_val2 = 2; // 定义内部全局变量 static void f1() {} // 定义内部函数 // a2.cc static int s_val1 = 6; // 定义内部全局变量,与a1.cc中的互不影响 static int s_val2; // 这里会视为定义了新的内部全局变量,而不会视为“声明” static void f1(); // 声明了一个内部函数 void Demo() {   std::cout << s_val1 << std::endl; // 输出6,与a1.cc中的s_val1没有关系   std::cout << s_val2 << std::endl; // 输出0,同样不会访问到a1.cc中的s_val2   f1(); // ERR,这里链接会报错,因为在a2.cc中没有找到f1的定义,并不会链接到a1.cc中的f1 }

所以我们发现,在这种场景下,static并不表示“静态”的含义,而是表示“内部”的含义,所以,为什么不再引入个类似于inner的关键字呢?这里很容易让程序员造成迷惑。

静态成员变量

静态成员变量指的是用static修饰的成员变量。普通的成员变量其生命周期是跟其所属对象绑定的。构造对象时构造成员变量,析构对象时释放成员变量。

struct Test {   int a; // 普通成员变量 }; int main(int argc, const char *argv[]) {   Test t; // 同时构造t.a   auto t2 = new Test; // 同时构造t2->a   delete t2; // t2所指对象析构,同时释放t2->a } // t析构,同时释放t.a

而用static修饰后,其声明周期变为全局,也就是“主函数执行前构造,主函数执行结束后释放”,并且不再跟随对象,而是全局一份。

struct Test {   static int a; // 静态成员变量(基本等同于声明全局变量) }; int Test::a = 5; // 初始化静态成员变量(主函数前执行,基本等同于初始化全局变量) int main(int argc, const char *argv[]) {   std::cout << Test::a << std::endl; // 直接访问静态成员变量   Test t;   std::cout << t.a << std::endl; // 通过任意对象实例访问静态成员变量 } // 主函数结束时释放Test::a

所以静态成员变量基本就相当于一个全局变量,而这时的类更像一个命名空间了。唯一的区别在于,通过类的实例(对象)也可以访问到这个静态成员变量,就像上面的t.aTest::a完全等价。

静态成员函数

static关键字修饰在成员函数前面,称为“静态成员函数”。我们知道普通的成员函数要以对象为主调方,对象本身其实是函数的一个隐藏参数(this 指针):

struct Test {   int a;   void f(); // 非静态成员函数 }; void Test::f() {   std::cout << this->a << std::endl; } void Demo() {   Test t;   t.f(); // 用对象主调成员函数 }

上面其实等价于:

struct Test {   int a; }; void f(Test *this) {   std::cout << this->a << std::endl; } void Demo() {   Test t;   f(&t); // 其实对象就是函数的隐藏参数 }

也就是说,obj.f(arg)本质上就是f(&obj, arg),并且这个参数强制叫做this。这个特性在 Go 语言中尤为明显,Go 不支持封装到类内的成员函数,也不会自动添加隐藏参数,这些行为都是显式的:

type Test struct {   a int } func(t *Test) f() {   fmt.Println(t.a) } func Demo() {   t := new(Test)   t.f() }

回到 C++的静态成员函数这里来。用static修饰的成员函数表示“不需要对象作为主调方”,也就是说没有那个隐藏的this参数。

struct Test {   int a;   static void f(); // 静态成员函数 }; void Test::f() {   // 没有this,没有对象,只能做对象无关操作   // 也可以操作静态成员变量和其他静态成员函数 }

可以看出,这时的静态成员函数,其实就相当于一个普通函数而已。这时的类同样相当于一个命名空间,而区别在于,如果这个函数传入了同类型的参数时,可以访问私有成员,例如:

class Test {  public:    static void f(const Test &t1, const Test &t2); // 静态成员函数  private:    int a; // 私有成员 }; void Test::f(const Test &t1, const Test &t2) {   // t1和t2是通过参数传进来的,但因为是Test类型,因此可以访问其私有成员   std::cout << t1.a + t2.a << std::endl; }

或者我们可以把静态成员函数理解为一个友元函数,只不过从设计角度上来说,与这个类型的关联度应该是更高的。但是从语法层面来解释,基本相当于“写在类里的普通函数”。

小结

其实 C++中static造成的迷惑,同样也是因为 C 中的缺陷被放大导致的。毕竟在 C 中不存在构造、析构和引用链的问题。说到这个引用链,其实 C++中的静态成员变量、静态局部变量和全局变量还存在一个链路顺序问题,可能会导致内存重复释放、访问野指针等情况的发生。这部分的内容详见后面“平凡、标准布局”的章节。

总之,我们需要了解static关键字有多义性,了解其在不同场景下的不同含义,更有助于我们理解 C++语言,防止踩坑。

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/1262

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2022年11月2日 下午12:19
下一篇 2022年11月2日 下午12:22

相关推荐