现代C++语言-初始化

本文详细解释了C++中变量的初始化过程,包括构造时的初始值设定、不同初始化语法(如复制、聚合、列表和直接初始化),以及静态和动态初始化的时机、顺序和规则。特别关注了引用初始化、ODR使用和延迟初始化的情况。
摘要由CSDN通过智能技术生成

变量的初始化会在构造时提供变量的初始值。

初始值可以由声明符或 new 表达式的初始化器部分提供。在函数调用时也会发生:调用函数形参及函数返回值也会被初始化。

初始化器

对于每个声明符,初始化器(如果存在)必须是下列之一:

= 表达式 (1) 复制初始化

= {}
= { 初始化器列表 }
= { 指派初始化器列表 } (2) 聚合初始化

( 表达式列表 )
( 初始化器列表 ) (3) (C++11 前) 直接初始化

{}
{ 初始化器列表 }
{ 指派初始化器列表 } (4) (C++11 起) 列表初始化

  1. 复制初始化语法。
  2. 聚合初始化语法。 (C++11 前)列表初始化语法。 (C++11 起)
  3. 直接初始化语法。
  4. 列表初始化语法。

表达式 - (不带括号的逗号表达式以外的)任意表达式
表达式列表 - 包含(不带括号的逗号表达式以外的)任意表达式的逗号分隔列表
初始化器列表 - 包含初始化器子句(见下文)的逗号分隔列表
指派初始化器列表 - 包含指派初始化器子句的逗号分隔列表

初始化器语义

如果没有为对象指定初始化器,那么该对象会默认初始化。如果没有为引用指定初始化器,那么程序非良构。

如果为对象指定的初始化器是 ()(由于语法限制不能在声明符中出现),那么该对象会值初始化。如果为引用指定的初始化器是 (),那么程序非良构。

初始化器的语义定义如下:

如果要初始化的是引用,那么定义参考引用初始化
否则要初始化的是对象。给定该对象的类型为 T:
如果使用的是语法 (1) 的初始化器,那么对象会复制初始化
如果使用的是语法 (2) 的初始化器:
如果 T 是聚合体,那么就会应用聚合初始化。
如果 T 是标量类型,那么 T x = { a }; 等价于 T x = a;。
否则程序非良构。
(C++11 前)
如果使用的是语法 (2) 或 (4) 的初始化器,那么对象会列表初始化。
(C++11 起)
如果使用的是语法 (3) 的初始化器,那么对象会直接初始化。

#include <string>
 
std::string s1;           // 默认初始化
std::string s2();         // 不是初始化!
                          // 实际上声明了没有形参并且返回 std::string 的函数 “s2”
std::string s3 = "hello"; // 复制初始化
std::string s4("hello");  // 直接初始化
std::string s5{'a'};      // 列表初始化(C++11 起)
 
char a[3] = {'a', 'b'}; // 聚合初始化(C++11 起是列表初始化的一部分)
char& c = a[0];         // 引用初始化

请注意string不是聚合体或标量,a[3]是聚合体

非局部变量

所有具有静态存储期的非局部变量的初始化,会作为程序启动的一部分在 main 函数的执行之前进行(除非被延迟,见下文)。所有具有线程局部存储期的非局部变量的初始化,会作为线程启动的一部分进行,并按顺序早于线程函数的执行开始。对于这两种变量,初始化发生于两个截然不同的阶段:

静态初始化
静态初始化有两种形式:

  1. 如果可能,就应用常量初始化。
  2. 否则,非局部的静态及线程局域变量会被零初始化。
    实践中:

常量初始化通常在编译期进行。预先被计算的对象表示会作为程序映像的一部分存储下来。如果编译器没有这样做,那么它仍然必须保证该初始化发生早于任何动态初始化。
零初始化的变量将被置于程序映像的 .bss 段,它不占据磁盘空间,并在加载程序时由操作系统以零填充。

#include <iostream>
#include <thread>

// 具有静态存储期的非局部变量,在程序启动时初始化
int global_static_var = 42;

// 具有线程局部存储期的非局部变量,在线程启动时初始化
thread_local int global_thread_local_var = 100;

void foo() {
    std::cout << "global_static_var in foo(): " << global_static_var << std::endl;
    std::cout << "global_thread_local_var in foo(): " << global_thread_local_var << std::endl;
}

int main() {
    std::cout << "global_static_var in main(): " << global_static_var << std::endl;
    std::cout << "global_thread_local_var in main(): " << global_thread_local_var << std::endl;

    // 修改全局变量的值
    global_static_var = 50;
    global_thread_local_var = 200;

    std::thread t(foo);
    t.join();

    return 0;
}

动态初始化
在所有静态初始化完成后,在下列情形中进行非局部变量的动态初始化:

  1. 无序的动态初始化,仅适用于未被显式特化的(静态/线程局域)类模板的静态数据成员及变量模板 (C++14 起)。这些静态变量的初始化相对于所有其他动态初始化之间是顺序不确定的,除非程序在初始化某个变量之前开始了一个线程,此时初始化则是无顺序的 (C++17 起)。这些线程局域变量的初始化相对于所有其他动态初始化之间是无顺序的。
  2. 部分有序的动态初始化,适用于并未被隐式或显式实例化的特化的所有内联变量。如果一个部分有序的 V 在每个翻译单元中比有序或部分有序的 W 更早定义,那么 V 的初始化按顺序早于(或若程序启动了线程,则为先发生于)W 的初始化。
    (C++17 起)
  3. 有序的动态初始化,适用于所有其他非局部变量:在单个翻译单元中,这些变量的初始化始终严格以其定义出现于源代码中的顺序定序。不同翻译单元中的静态变量的初始化之间是顺序不确定的。不同翻译单元中的线程局域变量的初始化之间是无顺序的。
    当拥有静态或线程存储期的非局部变量的初始化通过异常退出时,调用 std::terminate。
 #include <iostream>
#include <thread>

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructor called." << std::endl;
    }
};

template<typename T>
class MyTemplate {
public:
    static T value;
};

template<typename T>
T MyTemplate<T>::value;

int main() {
    std::thread t([](){
        MyTemplate<int>::value; // 可能在程序的任何点初始化
    });

    MyTemplate<double>::value; // 可能在程序的任何点初始化

    t.join();
    return 0;
}
#include <iostream>

inline int inlineVar = 1; // 内联变量

void function1() {
    std::cout << "inlineVar in function1: " << inlineVar << std::endl;
}

void function2() {
    std::cout << "inlineVar in function2: " << inlineVar << std::endl;
}

int main() {
    function2(); // inlineVar 在 function2 中被引用
    function1(); // inlineVar 在 function1 中被引用
    return 0;
}
#include <iostream>

int globalVar1 = 1;
int globalVar2 = 2;

void function1() {
    std::cout << "globalVar1 in function1: " << globalVar1 << std::endl;
}

void function2() {
    std::cout << "globalVar2 in function2: " << globalVar2 << std::endl;
}

int main() {
    function1(); // globalVar1 先初始化
    function2(); // globalVar2 后初始化
    return 0;
}

提早动态初始化
在下列条件都满足的情况下,允许编译器将动态初始化的变量的初始化作为静态初始化(实为编译期)的一部分进行:

  1. 初始化的动态版本不改变命名空间作用域中任何先于其初始化的对象的值
  2. 初始化的静态版本在被初始化变量中产生的值,与当所有不要求静态初始化的变量都被动态初始化时,由动态初始化所生成的值相同。
    因为上述规则,如果某对象 o1 的初始化涉及到命名空间作用域对象 o2,而它潜在地要求动态初始化,但在同一翻译单元中在其之后定义,那么所用的 o2 是完全初始化的 o2 的值(因为编译器把 o2 的初始化提升到编译时)还是 o2 仅被零初始化的值是未指明的。
inline double fd() { return 1.0; }
 
extern double d1;
 
double d2 = d1;   // 未指明:
                  // 如果 d1 被动态初始化则动态初始化为 0.0,或
                  // 如果 d1 被静态初始化则动态初始化为 1.0,或
                  // 静态初始化为 0.0(因为当两个变量都被动态初始化时将为这个值)
 
double d1 = fd(); // 可能静态或动态初始化为 1.0

延迟动态初始化
动态初始化是发生早于(对于静态变量)主函数或(对于线程局域变量)其线程的启动函数的首条语句,还是延迟到发生晚于它们,是由实现定义的。

如果非内联变量的 (C++17 起)初始化延迟到发生晚于主/线程函数的首条语句,那么它发生早于与所初始化的变量定义于同一翻译单元中的任何拥有静态/线程存储期的变量的首次 ODR 使用。如果给定翻译单元中没有 ODR 使用变量或函数,那么在该翻译单元定义的非局部变量可能始终不被初始化(这模仿按需的动态库的行为)。然而,只要翻译单元中 ODR 使用了任何事物,就会初始化所有在初始化或销毁中拥有副作用的非局部变量,即使程序中没有用到它们。

如果内联变量的初始化被延迟,那么它发生早于这个特定变量的首次 ODR 使用。

(C++17)
// ============
// == 文件 1 ==
 
#include "a.h"
#include "b.h"
 
B b;
A::A() { b.Use(); }
 
// ============
// == 文件 2 ==
 
#include "a.h"
 
A a;
 
// ============
// == 文件 3 ==
 
#include "a.h"
#include "b.h"
 
extern A a;
extern B b;
 
int main()
{
    a.Use();
    b.Use();
}

// 如果 a 在进入 main 之前被初始化,那么 b 可能在 A::A() 使用它的时间点仍未被初始化
// (因为动态初始化在翻译单元间是顺序不确定的)

// 如果 a 在某个 main 的首条语句之后的时间点初始化
// (它 ODR 使用了定义于文件 1 的函数,强制其初始化得以运行),
// 那么 b 将在 A::A 使用它前初始化

静态局部变量
有关局部(即块作用域)的静态和线程局部变量,见静态局部变量。

拥有外部或内部链接的变量的块作用域声明中不允许初始化器。这种声明必须带 extern 出现而且不能为定义。

类成员
非静态数据成员可以由成员初始化器列表或由默认成员初始化器初始化。

注解
非局部变量的销毁顺序在 std::exit 中描述。

  • 35
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值