C++的变量及其初始化


变量的及其初始化可能是编程中最基础、最重要,但是也是最为大家忽略的一个主题,最近在看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++中的built-in type

在C++中,variableobject基本可以认为表示相同语义,具体指: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 scopeblock 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初始化。

根据该文

  1. Value initialization for fundamental types means zero-initialization, which in turn means that the variables are initialized to zero (which all fundamental types have).
  2. 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 and definitions.
    • 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 the extern.
  • 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
      

初始化那些事

根据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;

编译输出如下:
list initialization在隐式类型转换中的作用

在MacOS中,这里使用编译命令g++时要注意加上 -std=c++11

发现通过这种初始化方法,之前那种隐式的类型转换行不通的了,必须要显式地给出。

阅读资料

  1. C++ Primer - 2.2.1 Variable Definition
  2. C++ Primer - 6.1.1 Local Objects
  3. https://en.cppreference.com/w/cpp/language/declarations
  4. https://accu.org/journals/overload/25/139/brand_2379/
  5. https://en.cppreference.com/w/cpp/language/list_initialization

文章作者: alex Li
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 alex Li !
  目录