C++中inline, extern, static潜在的陷阱

简介: 公司有位C++标准委员会的大佬,一年偶尔会有几次技术分享。这是其中的一次,对照着材料学习了演讲视频,以下就是这次分享的内容。相信inline, extern, static这三个关键字对于C++程序员是非常熟悉的,但有些时候,其中隐藏的陷阱,可能会给你的程序带来一些很难诊断的问题。

公司有位C++标准委员会的大佬,一年偶尔会有几次技术分享。这是其中的一次,对照着材料学习了演讲视频,以下就是这次分享的内容。

相信inline, extern, static这三个关键字对于C++程序员是非常熟悉的,但有些时候,其中隐藏的陷阱,可能会给你的程序带来一些很难诊断的问题。

1. inline

我们先聚焦于inline函数(内联函数)。inline可以与名称空间一起使用,但这种用法并不常见。最初,inline关键字的使用有两个目的:

给优化器一个关于哪些函数要内联的提示;告诉编译器一个函数的定义可能出现多次;

关于第一个目的,使用inline给优化器暗示对该函数内联,对于优化器来说始终是一个暗示,因此可能被优化器忽略。如今,它总是这样;优化器依赖于自己的分析来决定何时内联或不内联。这就剩下了另一种情况,允许函数定义重复。例如,如果将以下两个文件编译到同一个程序中会发生什么?

//bar.cpp
int foo() { return 42; }
int bar() { return foo() / 2; }
//baz.cpp
int foo() { return 42; }
int baz() { return foo() * 2; }

C/C++程序员很容易知道,上面的代码在链接过程中会产生符号重复定义的link error;

这样的问题修复也很简单,我们只要想办法消除那些重复定义的符号。就这个例子来说,可以有两种修复方法:

1.声明foo()函数在单独的头文件,并实现它的定义在单独的cpp文件;2.或者声明并定义foo()函数在头文件,同时加上inline关键字; inline int foo() { return 42; }

这两种修复方法非常相似,除了函数的内联版本更有可能被优化器内联。这听起来好像inline关键字仍然被用作优化器的提示——但实际情况并非如此。真实的情况是,当编译器看到int bar() { return foo() / 2; }这样一行代码,编译器能够决定是否将foo()内联到bar()中,因为它已经可以看到foo()的完整定义。

所以,内联是编译器能做的最重要的优化之一。这很重要,因为如果foo()这样的一行程序包含实际函数调用的开销,那么它的速度会慢很多。

当然,inline也有自己的局限性,在大多数实现中,如果超出了某些限制,那么inline将停止:

如果优化器的内联决策函数递归过深,它就会停止。如果函数F是内联到其调用方C的候选函数,如果F太大,则不内联。如果函数F是内联到它的调用者C的候选函数,但是内联会使C太大,那么F就不会内联到C中。

优化器有这些限制,因为避免函数调用开销很重要,但是不要过于频繁地溢出指令缓存也很重要。

那究竟inline是在程序的什么时候发生呢?

编译时;链接时(通常只有当链接优化打开时)。加载时(目前只有微软的C++/CLI可以这样做)。

因此,如果你想内联一个函数,让它的函数定义对编译器可见。这通常意味着在头文件中定义它并将其标记为内联。

2. extern

我们知道,在C++中,函数的声明和定义是不同的概念。函数可以在定义之前声明。如果它第一次出现在定义中,那么这个定义也是一个声明。

//foo.h
int foo(); // A declaration, not a definition.
// foo.cpp
#include "foo.h"
int foo() { return 42; } // A declaration and a definition.

定义总是被视为声明(但不是反过来)。

// foo.h
inline int foo() { return 42; } // A declaration and a definition.

extern告诉编译器某个声明对于其他源文件中的代码是可见的。也就是说,声明具有链接性质。这意味着所有具有该名称的实体都指的是声明为extern的实体。因此,如果你想在多个文件中使用一个全局变量,应该使用extern去声明该变量:extern int global_i.该声明可以出现在头文件或源文件中。如果将其放入源文件,则必须在使用global_i的每个源文件中重复声明。

最后,全局是有问题的,跨越源文件的全局更是如此。请小心使用extern。

3. static

关于static,我们都知道:

在函数中,静态表示变量是函数的局部变量,只有在函数第一次调用时才初始化。在类作用域,它指示一个实体是一个全局的,它的作用域是这个类,并具有相应的访问级别(公共的、私有的等)。在名称空间(namespace)范围内,它表示实体是声明它的源文件的本地实体。也就是说,它不会在本文件以外使用(不具有链接性)。

3.1 函数中使用static

首先,在函数中使用static;因为它们是在第一次调用函数时被初始化,所以它们必须由一个不可见的bool变量保护。因此以下的函数

int fun()
{
    static int ret = 42;
    return ret;
}

相当于:

int fun_static_ret;
bool fun_static_init_done = false;
int fun()
{
    if(!fun_static_init_done)
    {
        fun_static_ret = 42;
        fun_static_init_done = true;
    }
    return fun_static_ret;
}

但是,以上的代码并不是线程安全的,因此在C++11中增加了对线程安全的支持,在C++11及以后的版本中,等价的代码如以下:

int fun_static_ret;
std::atomic<bool> fun_static_init_done = false;
int fun()
{
    bool expected = false;
    if(fun_static_init_done.compare_exchange_strong(expected, true))
        fun_static_ret = 42;
    return fun_static_ret;
}

如果真的关心这里的代码所隐含的开销,那么你可以求助于一个全局变量,使它位于一个非常模糊的位置,只有函数才会使用它。通常,这种性能开销并不明显。

我强烈建议你不要在你的代码中使用单例——因为它们只是全局数据的一个花哨的名字——但如果你真的,真的需要使用单例,这样做:(可以通过函数调用去控制初始化顺序)

my_singleton_type & get_my_singleton()
{
    static my_singleton_type retval;
    return retval;
}

3.2 类中使用static

在class中使用static,必须声明并在其他地方去定义。

class foo {
    static int i;
};
int foo::i = 42;

对于指针和整型数据(int, char等),初始化也可以内联完成:

class foo {
    static int i = 42;
};

如果静态变量也是const类型,并且是内联定义的,那么它可以作为编译时常量使用,就像一个文字数:


class foo {
    static int const len = 1024;
    int array[len];
};

但是下面的代码时不起作用的,因为在解析变量数组时,编译器还不知道len的值。

class foo {
    static int const len;
    int array[len];         // Error!
};
int const foo::len = 1024;

3.3 命名空间(namespace)中使用static

名称空间范围静态用于定义只在一个源文件中可见的全局变量(函数)。也就是说,静态变量没有链接。

// a.cpp
static int magic_number = 42;
// b.cpp
extern int magic_number; // 链接错误!!!

匿名名称空间允许指示其中的实体是源文件的本地实体,但是这些实体仍然具有链接性。只是它们不可能在另一个源文件命名:

// a.cpp
namespace {
    int magic_number = 42;
}
// b.cpp
extern int magic_number; // 链接错误!!!

在b.cpp不能看到magic_number的原因是,对链接器来说,它的名称实际上是生成的命名空间名称。这是因为: namespace { int magic_number = 42; }相当于

namespace some_impossible_to_guess_name { int magic_number = 42; }
using namespace some_impossible_to_guess_name;

正因为在c++中的一些实体需要链接才能正常工作,因此使用匿名名称空间,而不是在命名空间作用域中使用静态变量是一种良好的编程习惯。

但是,在文件的本地函数中使用匿名名称空间还有一个更重要的原因,它使程序员避免违反一个定义的规则(the One Definition Rule)。

一个定义规则("ODR")规定,在多个源文件中定义的任何具有链接的实体都必须使用完全相同的令牌序列来定义。如果你违反了这个规则,编译器不需要告诉你,而且可能不会告诉你,你会有奇怪的行为。这就是一开始谈到的那些难以诊断的错误之一。

以下的代码,其中一项assert可能会被触发:

// a.cpp
int foo() { return 42 * 2; }
assert(foo() == 42 * 2);
// b.cpp
int foo() { return 42 / 2; }
assert(foo() == 42 / 2);

但是如果用匿名名称空间,下面的assert都不会被触发:

// a.cpp
namespace {
    int foo() { return 42 * 2; }
}
assert(foo() == 42 * 2);
// b.cpp
namespace {
    int foo() { return 42 / 2; }
}
assert(foo() == 42 / 2);

4. 总结

以下的总结是直接来自于演讲人的材料,因为担心自己的翻译会对大家造成歧义,所以直接将原文放在这里,相信这点英文对于一个程序员应该是没有什么难度的。

inline has nothing to do with the inlining optimization.inline tells the compiler that multiple definitions of this function may appear, and the linker should treat them as the same function, with the same address.Forgetting inline on a function that is defined in-line may cause duplicate-symbol linker errors, or may fail silently.extern tells the compiler that a certain declaration is defined elsewhere, and that the linker will be able to resolve the implied dependency.Class-scope static declares global entities within the scope of a class name, possibly with access restrictions.Namespace-scope static declares a file-local entity.Anonymous namespaces do essentially the same thing as a namespace-scope static, but is more user-friendly.

本文作者:focuscode,16年控制工程毕业,不慎误入C/C++怀抱,希望有一天可以给简历的精通C++加上双引号~。

声明:本文为 脚本之家专栏作者 投稿,未经允许请勿转载。

相关文章
|
2月前
|
存储 编译器 C语言
详解C/C++中的static和extern
本文详解了C/C++中`static`和`extern`关键字的用法和区别,通过具体代码示例说明了在不同情境下如何正确使用这两个关键字,以及`extern "C"`在C++中用于兼容C语言库的特殊作用。
详解C/C++中的static和extern
|
6月前
|
C++
C++当类模板遇到static
C++当类模板遇到static
|
1月前
|
存储 编译器 C++
【C++】深入探索类和对象:初始化列表及其static成员与友元(一)
【C++】深入探索类和对象:初始化列表及其static成员与友元
|
8天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
33 5
|
1月前
|
C语言 C++
C 语言的关键字 static 和 C++ 的关键字 static 有什么区别
在C语言中,`static`关键字主要用于变量声明,使得该变量的作用域被限制在其被声明的函数内部,且在整个程序运行期间保留其值。而在C++中,除了继承了C的特性外,`static`还可以用于类成员,使该成员被所有类实例共享,同时在类外进行初始化。这使得C++中的`static`具有更广泛的应用场景,不仅限于控制变量的作用域和生存期。
57 10
|
1月前
|
存储 编译器 数据安全/隐私保护
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解2
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解
31 3
|
1月前
|
编译器 C++
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解1
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解
47 3
|
1月前
|
C++
【C++】深入探索类和对象:初始化列表及其static成员与友元(二)
【C++】深入探索类和对象:初始化列表及其static成员与友元
|
1月前
|
编译器 C++
【C++】深入探索类和对象:初始化列表及其static成员与友元(三)
【C++】深入探索类和对象:初始化列表及其static成员与友元
|
2月前
|
C++
C/C++静态链接pthread库的坑【-static -pthread】
C/C++静态链接pthread库的坑【-static -pthread】