变量的及其初始化可能是编程中最基础、最重要,但是也是最为大家忽略的一个主题,最近在看C++,本来想越过这个主题,但是看到后边,越来越多的内容与变量的初始化有关,而且各种内容十分繁杂,想着这一篇总结一下。
关于variable的那些事
类型的作用
C++中,变量variable
提供了一个署名的内存空间,每个变量都有一个type与其绑定,type提供了非常重要的信息,决定了:
- variable的memory size和layout;
- 该memory space中能存储的最大的值;
- 对于该variable可以进行什么样的操作;
C++ is a statically typed language, which means that types are checked at compile time. The process by which types are checked is referred to as type checking.
C++中的built-in types包括以下几种:
在C++中,
variable
和object
基本可以认为表示相同语义,具体指:a region of memory that can contain data and has a type.
scope和lifetime的影响
C++中每个变量都有作用域scope
和生命周期lifetime
。这两个概念对于变量的初始化、内存的分配、资源的管理等有非常大的影响,但是经常被忽视。
the
scope
of a name is the part of the program’s text in which that name is visible.scope
可以分为global scope
和block scope
;- 任何函数内的
scope
都是block scope
,一般通过{}
标识,定义的变量为local variables
. - 不同的
block scope
之间可以发生嵌套,且inner scope
会隐藏outer scope
中定义的同名变量,但是强烈建议不要采用这种做法;
the
lifetime
of an object is the time during the program’s execution that the object exists.lifetime
受到scope
的影响;- 定义在
global scope
的object在整个program执行期间都会存在; - 定义在
block scope
的object在当前的作用域结束后,也会被destroyed,比如定义在函数内的变量(也有例外,如static
); - 已经被destroyed的变量不能再用了,这种问题在很多的指针操作、动态内存申请中都会出现;
automatic objects
正常情况下,定义在block scope
中的ordinary local variables
,在scope结束时,也会被destroyed,其value变为undefined,这种类型的objects称为automatic objects
.
在函数调用,参数的传递过程中,函数parameters只在该
block scope
内有效,当函数调用结束,即被destroyed。那么这些
automatic objects
是如何初始化的?- parameters被arguments初始化;
- 函数内部的其他local variables可以被显式初始化;
- 最后,如果没有显式初始化,则会默认初始化,其value成为
undefined
。
Local static objects
问题:有时需要在函数调用结束后,仍然让一些variable存在,而不是被destroyed,怎么办?
- 使用全局变量呗,但是这种操作将该变量的控制权暴露给所有的函数,这显然不是我们想要的。
- static变量可以有效解决这个问题:
- 其
lifetime
在函数对应的block scope
结束后,仍然存续; - 其次,其只能在该函数内进行访问,不会被别的函数修改;
- 其
这种定义在block scope
的variable称为local static objects
,关于static的进一步阅读可以参考此文。
- 它们在program结束时才会被destroyed;
- 这种特殊的local variables的初始化也有其特点:
- 它们的初始化只有一次,即在对该函数的第一次调用时完成;
- 如果没有对它们进行显式的初始化,它们通过value initilization完成,即全部用0初始化。
根据该文:
- Value initialization for fundamental types means zero-initialization, which in turn means that the variables are initialized to zero (which all fundamental types have).
- For objects of class type, both default- and value-initialization invoke the default constructor. What happens then depends on the constructor’s initializer list, and the game continues recursively for member variables.
代码示例如下:
void func(){
static int call_count;
cout << call_count << ": hello" << endl;
call_count += 1;
}
int main()
{
int times = 5;
for(int i = 0; i < times; i++) {
func();
}
return 0;
}
// output:
/**
0: hello
1: hello
2: hello
3: hello
4: hello
*
call_count
默认初始化为0,当显式给出1时,则输出从1:hello开始。
variable declaration & definition
解决什么问题
- To allow programs to be written in logical parts, C++ supports what is commonly known as separate compilation.
- Separate compilation lets us split our programs into several files, each of which can be compiled independently.
- To support separate compilation, C++ distinguishes between
declarations
anddefinitions
. - To use a variable in more than one file requires declarations that are separate from the variable’s definition.
两者对比
A declaration makes a name known to the program.
- A file that wants to use a name defined elsewhere includes a declaration for that name.
- A variable declaration specifies the type and name of a variable.
A definition creates the associated entity.
- A variable definition is a declaration.
- In addition to specifying the name and type, a definition also allocates storage and may provide the variable with an initial value. 在这个过程中发生的是初始化。
Variables must be defined exactly once but can be declared many times.
To use the same variable in multiple files, we must define that variable in one—and only one—file.
Other files that use that variable must declare—but not define—that variable.
两者在代码中的区别是啥?
// declares but does not define i
extern int i;
// declares and defines j
int j;
- Any declaration that includes an explicit initializer is a definition.
- We can provide an initializer on a variable defined as
extern
, but doing so overrides theextern
. - An extern that has an initializer is a definition:
extern double pi = 3.1416;
- It is an error to provide an initializer on an extern inside a function.
extern int a = 100; // compile output: // error: ‘a’ has both ‘extern’ and initializer
- It is an error to provide an initializer on an extern inside a function.
初始化那些事
根据C++官方文档,变量的初始化行为如下:
Initialization of a variable provides its initial value at the time of construction.
C++ Primer中这么描述initialization:
An object that is initialized gets the specified value at the moment it is created.
简单来说,initialization强调2点:
- 发生在变量构造期间:分配一个内存空间,即上述所说的
variable definition
中; - 赋值:给该变量一个特定的值;
变量的初始化,不止发生在普通的variable definition中,函数的参数传递和函数的返回值的获取,也是变量初始化的实际场景。
Initialization与Assignment的区别
简单的结论:Initialization不是Assignment,但是如果认为两者一样,在一些场合中也不会导致什么问题。
Initialization happens when a variable is given a value when it is created.
- 从这个角度理解,Initialization只有一次。
Assignment obliterates an object’s current value and replaces that value with a new one.
- Assignment在整个程序运行期间,可以发生多次。
默认初始化的行为
当在variable definition时,没有显式给出初始化的值时,变量会被default initialized,默认值的提供取决于两个因素:
变量的类型;
- 对于语言支持的基本数据类型,按照默认语言规定的默认初始化行为;
- 对于一些类类型或者自定义的类型,需要在构造函数中自行实现,如string默认初始化为空字符串;
Objects of class type that we do not explicitly initialize have a value that is defined by the class.
变量的位置;
- The value of an object of built-in type that is not explicitly initialized depends on where it is defined. Variables defined outside any function body are initialized to zero.
- variables of built-in type defined inside a function are uninitialized. The value of an uninitialized variable of built-in type is undefined.
- It is an error to copy or otherwise try to access the value of a variable whose value is undefined.
Uninitialized objects of built-in type defined inside a function body have undefined value.
示例代码如下:
void func(){
int l_int;
float l_float;
double l_double;
char l_char;
string l_str;
cout << l_int << endl
<< l_float << endl
<< l_double << endl
<< l_char << endl
<< l_str << endl;
}
int g_int;
float g_float;
double g_double;
char g_char;
string g_str;
int main()
{
cout << g_int << endl
<< g_float << endl
<< g_double << endl
<< g_char << endl
<< g_str << endl;
func();
return 0;
}
// output
/**
0
0
0
18192
1.4013e-45
3.17535e-310
*
不同的初始化方式及适用场景
在C++中定义了不同的初始化方式,如下:
// 最常见
int a = 0;
// c++中引入的list initialization
int a = {0};
int a{0};
// 类似类类型中对象的初始化
int a(0);
list initialization - {}
list initialization是方法是在C++11种引入的方法。
- 这种方法可以在任何需要初始化的地方使用;
- 甚至在一些进行赋值的场景中也能用;
虽然,有很多的初始化方法,但是为啥又引入新的一种,或者,换一个问题——这个方法有啥好处?int a = 100; // ok a = {200};
这种初始化方法用在built-in types时:
The compiler will not let us list initialize variables of built-in type if the initializer might lead to the loss of information.
long double pi = 3.1415926;
int a{pi};
int b = {pi};
int c(pi);
int d = pi;
cout << a << "," << b << "," << c << "," << d << endl;
编译输出如下:
在MacOS中,这里使用编译命令
g++
时要注意加上-std=c++11
。
发现通过这种初始化方法,之前那种隐式的类型转换行不通的了,必须要显式地给出。
阅读资料
- C++ Primer - 2.2.1 Variable Definition
- C++ Primer - 6.1.1 Local Objects
- https://en.cppreference.com/w/cpp/language/declarations
- https://accu.org/journals/overload/25/139/brand_2379/
- https://en.cppreference.com/w/cpp/language/list_initialization