《C++回顾笔记》声明、定义与初始化

声明(Declaration)

所谓声明,是告诉编译器某个东西的名称和类型,但略去某些细节。在程序中,允许重复声明多次。下面的都是声明式:

1
2
3
4
5
6
7
8
9
10
extern int x;                         /// 声明一个变量

class Widget; /// 声明一个类

std::size_t numDigits(int number); /// 声明一个函数

class A{
public:
static int a; /// 声明一个静态变量
}
  • 任何包含了显式初始化的声明即成为定义,可以给由extern关键字标记的变量赋一个初始值,但是这么做也就抵消了extern的作用,也就不再是声明,而变成定义了:
    1
    extern int x = 0;                   /// 定义了一个变量,其类型为int
  • 此外,只有当extern声明位于函数外部时,才能执行赋值操作。

定义(Definition)

  定义的任务是,为编译器提供声明所遗漏的细节。对于对象而言,定义的任务是为此对象分配内存;对函数或者模板函数而言,定义提供了函数的实现;而对类或者模板类来说,定义列出其成员变量和成员函数。在程序中,只允许定义一次。下面的都是定义式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int x;                                /// 定义一个变量

class Widget /// 定义一个类
{
public:
Widget();
~Widget();
}

std::size_t numDigits(int number) /// 定义一个函数
{
std::size_t digit = 1;
return digit;
}

int A::a = 0; /// 定义一个静态变量并初始化

大多数情况下,定义也完成了声明的工作,不需要再额外声明。但以下两种情况,定义只是定义:

  • 在类定义之外,定义并初始化一个静态数据成员
  • 在类外定义非内联成员函数

声明和定义的关系

  为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译(separate compilation)机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。为了支持分离式编译,C++语言将声明和定义区分开来。声明告诉程序一个名字,一个文件如果想使用别处定义的变量则必须包含对那个名字的声明,而定义负责创建与名字关联的实体。

  • 程序使用到的每个名字都会指向一个特定的实体:变量函数类型
  • 变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。

  声明和定义的区别看起来也许微不足道,但实际上却非常重要。如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。


初始化(Initialization)

  初始化是指给予已定义的对象初始值的过程。如果定义变量时候没有初始化,则变量将会被默认初始化(隐式初始化,由编译器按照一定的规则自动完成)。一般来说,默认初始化的规则如下:

  • 栈中的变量(函数内定义的变量)和堆中的变量(动态内存)会有不确定的值
  • 全局变量和静态变量(包括局部静态变量)会被初始化为该变量的类型中0这个概念对应的值

    C++11: If no initializer is specified for an object, the object is default-initialized; if no initialization is performed, an object with automatic or dynamic storage duration has indeterminate value. Note: Objects with static or thread storage duration are zero-initialized.

  之所以对不同类型的变量,默认初始化的行为会不一致,是因为编译器存储它们的内存空间不同。关于编译器存储变量的内存空间分配,可以参阅进程的内存空间分配

  值得注意的是,如果变量的类型是自定义的类,那么该变量在被默认初始化的时候,将会调用其默认构造函数。倘若该类找不到默认构造函数,或者没有带缺省参数的构造函数,编译器将会报错。

对于自定义的类来说,其没有初始化的成员变量的值是否为一个不确定的值,取决于其所在对象的存储方式(对象也没被初始化)

  1. 如果对象为全局变量或者静态变量(存于BSS段),那么其成员变量的值将会被初始化为0
  2. 如果对象为动态对象或者局部变量(存于堆或者栈上),那么其成员变量的值将是一个不确定的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <stdlib.h>
#include <iostream>

using namespace std;

class Test
{
public:
Test()
{
cout << "Test(): a = " << a << endl;
}

int a;
};

class StaticTest
{
public:
StaticTest()
{
cout << "StaticTest(): a = " << a << endl;
}

int a;
};

class TestPtr
{
public:
TestPtr()
{
cout << "TestPtr(): a = " << a << endl;
}

int a;
};

Test GlobalTest; /// 对象存于BSS段,成员变量a的值会被初始化为0,并调用默认构造函数
static StaticTest StaticGlobalTest; /// 对象存于BSS段,成员变量a的值会被初始化为0,并调用默认构造函数

TestPtr* GlobalTestPtr = new TestPtr(); /// 对象存于堆上,成员变量a的值为不确定值,不会被初始化为0,并调用默认构造函数
TestPtr* GlobalTestPtr2; /// 指针GlobalTestPtr2存于BSS段,因此其值会被初始化为0,也就是nullptr

int main()
{

cout << "--------------------Initialization of local variable --------------------" << endl;

Test LocalTest; /// 对象存于栈上,成员变量a的值为不确定值,不会被初始化为0,并调用默认构造函数

static StaticTest StaticLocalTest; /// 对象存于BSS段,成员变量a的值会被初始化为0,并调用默认构造函数

TestPtr* LocalTestPtr = new TestPtr(); /// 对象存于堆上,成员变量a的值为不确定值,不会被初始化为0,并调用默认构造函数
TestPtr* LocalTestPtr2;

cout << "GlobalTestPtr2 = " << GlobalTestPtr2 << endl;
// cout << "LocalTestPtr2 = " << LocalTestPtr2 << endl; /// 将会报错,不允许使用未初始化的变量

system("pause");
return 0;
}

&emsp;&emsp;根据默认初始化的规则,不难得到上述事例程序的输出为:

1
2
3
4
5
6
7
8
9
10
Test(): a = 0
StaticTest(): a = 0
TestPtr(): a = -842150451
--------------------Initialization of local variable --------------------
Test(): a = -858993460
StaticTest(): a = 0
TestPtr(): a = -842150451
GlobalTestPtr2 = 00000000
LocalTestPtr2 = 00000000
请按任意键继续. . .

赋值与初始化

&emsp;&emsp;在C++中,可以使用=来完成初始化,因此这很容易让人认为初始化是赋值的一种。然而,事实上,在C++语言中,赋值和初始化是两个完全不同的操作,只是它们两者的区别在很多情况下可以忽略不计。需要强调的是,初始化不是赋值,初始化的含义是创建变量的时候赋予其一个初始值,而赋值的含义则是把变量的当前值擦除,并使用一个新值来替代。


《C++回顾笔记》声明、定义与初始化
https://asancai.github.io/posts/be6fc527/
作者
RainbowCyan
发布于
2021年4月25日
许可协议