关于c++:C五花八门的C初始化规则

9次阅读

共计 9401 个字符,预计需要花费 24 分钟才能阅读完成。

总结

  • 初始化的概念:创立变量时赋予它一个值(不同于赋值的概念)
  • 类的构造函数管制其对象的初始化过程,无论何时只有类的对象被创立就会执行构造函数
  • 如果对象未被用户指定初始值,那么这些变量会被执行默认初始化,默认值取决于变量类型和定义变量的地位
  • 无论何时只有类的对象被创立就会执行构造函数,通过显式调用构造函数进行初始化被称为显式初始化,否则叫做隐式初始化
  • 应用等号(=)初始化一个类变量执行的是拷贝初始化,编译器会把等号右侧的初始值拷贝到新创建的对象中去,不应用等号则执行的是间接初始化
  • 传统 C ++ 中列表初始化仅能用于一般数组和 POD 类型,C++11 新规范将列表初始化利用于所有对象的初始化(然而内置类型习惯于用等号初始化,类类型习惯用构造函数圆括号显式初始化,vector、map 和 set 等容器类习惯用列表初始化)

初始化不等于赋值

初始化的含意是创立变量时赋予其一个初始值,而赋值的含意是把对象的以后值擦去,并用一个新值代替它。

C++ 定义了初始化的好几种不同模式,例如咱们定义一个 int 变量并初始化为 0,有如下 4 种形式:

int i = 0;
int i = {0};
int i{0};
int i(0);

默认初始化与值初始化

Tips:C 不容许用户自定义默认值从而进步性能(减少函数调用的代价),C++ 默认也不做初始化从而进步性能,然而 C ++ 提供了构造函数让用户显式设置默认初始值。有个例外是把全局变量初始化为 0 仅仅在程序启动时会有老本,因而定义在任何函数之外的变量会被初始化为 0。

如果定义变量时没有指定初始值,则变量会被默认初始化或值初始化,此时变量被赋予了默认值,这个默认值取决于变量类型和定义地位。

#include <iostream>

class Cat {
 public:
    std::string name;
    Cat() = default;};

int main() {
    Cat cat1;          // 默认初始化
    Cat cat2 = Cat();  // 显式申请值初始化}

1. 内置类型的默认初始化

Tips:倡议初始化每一个内置类型的变量,起因在于定义在函数外部的内置类型变量的值是未定义的,如果试图拷贝或者以其余模式拜访此类值是一种谬误的编程行为且很难调试。

如果内置类型的变量未被显式初始化,它的值由定义的地位决定。定义于任何函数体之外的变量会被初始化为 0,定义在函数体外部的内置类型变量将不被初始化(uninitialized),一个未被初始化的内置类型变量的值时未定义的,如果试图拷贝或以其余模式拜访此类值将引发谬误。

#include <iostream>
int global_value;  // 默认初始化为 0

int main() {
    int local_value;  // 应用了未初始化的局部变量
    int* new_value = new int;
    std::cout << "new_value:" << *new_value << std::endl;       // 未定义
    std::cout << "global_value:" << global_value << std::endl;  // 0
    std::cout << "local_value:" << local_value << std::endl;    // 未定义, 且会报 warning

    return 0;
}

2. 类类型的默认初始化

定义一个类变量然而没有指定初始值时,会应用默认构造函数来初始化,所以没有默认构造函数的类不能执行默认初始化。定义于任何函数体之外的类变量会先进行零初始化再执行默认初始化,定义在函数体外部的类变量会间接执行默认初始化。

#include <iostream>

// Cat 类应用合成的默认构造函数
class Cat {
 public:
    int age;
};


// Dog 类应用自定义的默认构造函数
class Dog {
 public:
    int age;
    Dog() {}  // 默认构造函数, 然而不会初始化 age
};

// 在函数体内部定义的类会先执行零初始化, 再执行默认初始化, 因而尽管默认构造函数不会初始化 age 变量, 但 age 依然是 0
Cat global_cat;
Dog global_dog;

int main() {
    Cat local_cat;
    Dog local_dog;
    std::cout << "global_cat age:" << global_cat.age << std::endl;  // 0
    std::cout << "global_dog age:" << global_dog.age << std::endl;  // 0
    std::cout << "local_cat age:" << local_cat.age << std::endl;    // 随机值
    std::cout << "local_dog age:" << local_dog.age << std::endl;    // 随机值
    return 0;
}

没有默认构造函数的类是不能执行默认初始化的:

#include <iostream>

// Cat 类禁用默认构造函数, 无奈默认初始化
class Cat {
 public:
    int age;
    Cat() = delete;};

int main() {Cat local_cat;  // 编译报错: use of deleted function‘Cat::Cat()’return 0;
}

从实质上讲,类的初始化取决于构造函数中对数据成员的初始化,如果没有在构造函数的初始值列表中显式地初始化数据成员,那么成员将在构造函数体之前执行默认初始化,例如:

// 通过构造函数初始值列表初始化数据成员: 数据成员通过提供的初始值进行初始化
class Cat {
 public:
    int age;
    explicit Cat(int i) : age(i) {}};

// 数据成员先进行默认初始化, 再通过结构函数参数进行赋值操作
// 这种办法尽管非法然而比拟粗率, 造成的影响依赖于数据成员的类型
class Dog {
 public:
    int age;
    explicit Dog(int i) {age = i;}
};

3. 数组的默认初始化

  1. 如果定义数组时提供了初始值列表,那么未定义的元素若是内置类型或者有合成的默认结构则会先进行零初始化,如果元素是类类型,再执行默认构造函数
  2. 如果定义数组时未提供初始化列表,则每个元素执行默认初始化
class Cat {
 public:
    int age;
};


int main() {
    /* 内置类型在函数外部默认初始化, 随机值 */
    int int_array[5];
    for (int i = 0; i < 5; i++) {std::cout << int_array[i] << std::endl;  // 全都是随机值
    }

    /* 定义数组应用初始值列表, 除了前两个元素外都是 0 */
    int int_array2[5] = {22, 33};
    for (int i = 0; i < 5; i++) {std::cout << int_array2[i] << std::endl;  // 22,33,0,0,0
    }

    /* 定义数组应用初始值列表, 都是 0 */
    int int_array3[5] = {};
    for (int i = 0; i < 5; i++) {std::cout << int_array3[i] << std::endl;  // 0,0,0,0,0
    }

    /* 数组元素为类且应用初始值列表时 */
    Cat *my_cat = new Cat;
    Cat cat_array[5] = {*my_cat};
    for (int i = 0; i < 5; i++) {std::cout << cat_array[i].age << std::endl;  // 随机值,0,0,0,0
    }

    return 0;
}

4. 内置类型的值初始化(不举荐)

对于类类型而言,不指定初始值下会调用它的默认构造函数,因而不存在默认初始化和值初始化的区别。然而对于内置类型值初始化和默认初始化不同,只不过理论开发中咱们倡议显式初始化内置类型来防止产生未定义值的代码:

int *pi1 = new int;               // 默认初始化: *pi1 的值未定义
int *pi2 = new int();             // 值初始化: *pi2 的值为 0

int *pia1 = new int[10];          // 10 个默认初始化的 int: 值未定义
int *pia2 = new int[10]();        // 10 个值初始化的 int: 值都为 0

string *psa1 = new string[10];    // 10 个默认初始化的 string: 都为空
string *psa2 = new string[10]();  // 10 个值初始化的 string: 都为空

隐式初始化与显式初始化

1. 概念

无论何时只有类的对象被创立就会执行构造函数,通过显式调用构造函数进行初始化被称为显式初始化,否则叫做隐式初始化。

#include <iostream>

// Cat 提供两个构造函数
class Cat {
 public:
    int age;
    Cat() = default;
    explicit Cat(int i) : age(i) {}};

int main() {
    Cat cat1;           // 隐式初始化: 调用默认构造函数
    Cat cat2(10);       // 隐式初始化: 调用一个形参的构造函数

    Cat cat3 = Cat();   // 显式初始化: 调用默认构造函数
    Cat cat4 = Cat(5);  // 显式初始化: 调用一个形参的构造函数

    // 构造函数还能够搭配 new 一起应用, 用于在堆上分配内存
    Cat *cat5 = new Cat();
    Cat *cat6 = new Cat(3);
    delete cat5;
    delete cat6;

    return 0;
}

还有一些操作不会显式调用类的构造函数,比方:

  • 通过一个实参调用的构造函数定义了从结构函数参数类型向类类型隐式转换的规定
  • 拷贝构造函数定义了用一个对象初始化另一个对象的隐式转换
#include <iostream>

// Cat 提供两个构造函数
class Cat {
 public:
    int age;
    // 接管一个参数的构造函数定义了从 int 型向类类型隐式转换的规定, explicit 关键字能够组织这种转换
    Cat(int i) : age(i) {}
    // 拷贝构造函数定义了从一个对象初始化另一个对象的隐式转换
    Cat(const Cat &orig) : age(orig.age) {}};

int main() {
    Cat cat1 = 10;    // 调用接管 int 参数的拷贝构造函数
    Cat cat2 = cat1;  // 调用拷贝构造函数

    std::cout << cat1.age << std::endl;
    std::cout << cat2.age << std::endl;
    return 0;
}

// 输入:
10
10

2. explicit 禁用构造函数定义的类型转换

例如智能指针就把构造函数申明为explict,所以智能指针只能间接初始化。咱们也能够通过 explicit 禁用掉下面提到的两种隐式转换规则:

#include <memory>

class Cat {
 public:
    int age;
    Cat() = default;
    // 必须显式调用拷贝构造函数
    explicit Cat(const Cat &orig) : age(orig.age) {}};

int main() {
    Cat cat1;
    Cat cat2(cat1);      // 正确: 显式调用拷贝构造函数
    // Cat cat3 = cat1;  // 谬误: explicit 关键字限度了拷贝构造函数的隐式调用

    // std::shared_ptr<int> sp = new int(8);    // 谬误: 不反对隐式调用构造函数
    std::shared_ptr<int> sp(new int(8));        // OK
    return 0;
}

3. 只容许一步隐式类型转换

编译器只会主动执行一步隐式类型转换,如果隐式地应用两种转换规则,那么编译器便会报错:

class Cat {
 public:
    std::string name;
    Cat(std::string s) : name(s) {}  // 容许 string 到 Cat 的隐式类型转换};

int main() {// 谬误: 不存在从 const char[8]到 Cat 的类型转换, 编译器不会主动把 const char[8]转成 string, 再把 string 转成 Cat
    // Cat cat1 = "tomocat";

    // 正确: 显式转换成 string, 再隐式转换成 Cat
    Cat cat2(std::string("tomocat"));

    // 正确: 隐式转换成 string, 再显式转换成 Cat
    Cat cat3 = Cat("tomocat");
}

间接初始化与拷贝初始化

如果应用等号(=)初始化一个类变量,实际上执行的是拷贝初始化,编译器把等号右侧的值拷贝到新创建的对象中区;如果不应用等号,那么执行的是间接初始化。

以 string 为例:

string s1 = "tomocat";    // 拷贝初始化
string s2("tomocat");     // 间接初始化
string s3(10, 'c');       // 间接初始化, s3 内容为 cccccccccc

// s4 拷贝初始化
string s4 = string(10, 'c');
// 等价于
string temp = string(10, 'c');
string s4 = temp;

列表初始化

1. C++98/03 与 C ++11 的列表初始化

在 C ++98/03 中,一般数组和 POD(Plain Old Data,即没有结构、析构和虚函数的类或构造体)类型能够应用花括号 {} 进行初始化,即列表初始化。然而这种初始化形式仅限于上述提到的两种数据类型:

int main() {
    // 一般数组的列表初始化
    int arr1[3] = {1, 2, 3};
    int arr2[] = { 1, 3, 2, 4};  // arr2 被编译器主动推断为 int[4]类型
    
    // POD 类型的列表初始化
    struct data {
        int x;
        int y;
    } my_data = {1, 2};
}

C++11 新规范中列表初始化失去了全面利用,不仅兼容了传统 C ++ 中一般数组和 POD 类型的列表初始化,还能够用于任何其余类型对象的初始化:

#include <iostream>
#include <string>

class Cat {
 public:
    std::string name;
    // 默认构造函数
    Cat() {std::cout << "default constructor of Cat" << std::endl;}
    // 承受一个参数的构造函数
    Cat(const std::string &s) : name(s) {std::cout << "normal constructor of Cat" << std::endl;}
    // 拷贝构造函数
    Cat(const Cat &orig) : name(orig.name) {std::cout << "copy constructor of Cat" << std::endl;}
};

int main() {
    /*
     * 内置类型的列表初始化
     */
    int a{10};       // 内置类型通过初始化列表的间接初始化
    int b = {10};    // 内置类型通过初始化列表的拷贝初始化
    std::cout << "a:" << a << std::endl;
    std::cout << "b:" << b << std::endl;

    /*
     * 类类型的列表初始化
     */
    Cat cat1{};                 // 类类型调用默认构造函数的列表初始化
    std::cout << "cat1.name:" << cat1.name << std::endl;
    Cat cat2{"tomocat"};        // 类类型调用一般构造函数的列表初始化
    std::cout << "cat2.name:" << cat2.name << std::endl;

    // 留神列表初始化后面的等于号并不会影响初始化行为, 这里并不会调用拷贝构造函数
    Cat cat3 = {"tomocat"};     // 类类型调用一般构造函数的列表初始化
    std::cout << "cat3.name:" << cat3.name << std::endl;
    // 先通过列表初始化结构右侧 Cat 长期对象, 再调用拷贝构造函数(从输入上看如同编译器优化了, 间接调用一般构造函数而不会调用拷贝构造函数)
    Cat cat4 = Cat{"tomocat"};
    std::cout << "cat4.name:" << cat4.name << std::endl;

    /*
     * new 申请堆内存的列表初始化
     */
    int *pi = new int{100};
    std::cout << "*pi:" << *pi << std::endl;
    delete pi;
    int *arr = new int[4] {10, 20, 30, 40};
    std::cout << "arr[2]:" << arr[2] << std::endl;
    delete[] arr;}

// 输入:
a:10
b:10
default constructor of Cat
cat1.name:
normal constructor of Cat
cat2.name:tomocat
normal constructor of Cat
cat3.name:tomocat
normal constructor of Cat
cat4.name:tomocat
*pi:100
arr[2]:30

2. vector 中圆括号与花括号的初始化

总的来说,圆括号是通过调用 vector 的构造函数进行初始化的,如果应用了花括号那么初始化过程会尽可能会把花括号内的值当做元素初始值的列表来解决。如果初始化时应用了花括号然而提供的值又无奈用来列表初始化,那么就思考用这些值来调用 vector 的构造函数了。

#include <string>
#include <vector>

int main() {std::vector<std::string> v1{"tomo", "cat", "tomocat"};  // 列表初始化: 蕴含 3 个 string 元素的 vector
    // std::vector<std::string> v2("a", "b", "c");          // 谬误: 找不到适合的构造函数

    std::vector<std::string> v3(10, "tomocat");             // 10 个 string 元素的 vector, 每个 string 初始化为 "tomocat"
    std::vector<std::string> v4{10, "tomocat"};             // 10 个 string 元素的 vector, 每个 string 初始化为 "tomocat"

    std::vector<int> v5(10);     // 10 个 int 元素, 每个都初始化为 0
    std::vector<int> v6{10};     // 1 个 int 元素, 该元素的值时 10
    std::vector<int> v7(10, 1);  // 10 个 int 元素, 每个都初始化为 1
    std::vector<int> v8{10, 1};  // 2 个 int 元素, 值别离是 10 和 1
}

3. 初始化习惯

只管 C ++11 将列表初始化利用于所有对象的初始化,然而内置类型习惯于用等号初始化,类类型习惯用构造函数圆括号显式初始化,vector、map 和 set 等容器类习惯用列表初始化。

#include <string>
#include <vector>
#include <set>
#include <map>

class Cat {
 public:
    std::string name;
    Cat() = default;
    explicit Cat(const std::string &s) : name(s) {}};

int main() {// 内置类型初始化(包含 string 等规范库简略类类型)
    int i = 10;
    long double ld = 3.1415926;
    std::string str = "tomocat";

    // 类类型初始化
    Cat cat1();
    Cat cat2("tomocat");

    // 容器类型初始化(当然也能够用圆括号初始化, 列表初始化用于显式指明容器内元素)
    std::vector<std::string> v{"tomo", "cat", "tomocat"};
    int arr[] = {1, 2, 3, 4, 5};
    std::set<std::string> s = {"tomo", "cat"};
    std::map<std::string, std::string> m = {{"k1", "v1"}, {"k2", "v2"}, {"k3", "v3"}};
    std::pair<std::string, std::string> p = {"tomo", "cat"};

    // 动态分配对象的列表初始化
    int *pi = new int {10};
    std::vector<int> *pv = new std::vector<int>{0, 1, 2, 3, 4};

    // 动态分配数组的列表初始化
    int *parr = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
}

4. 列表初始化返回值

C++11 新标准规定,函数能够通过列表初始化来对函数返回的长期量进行初始化:

#include <string>
#include <vector>

std::vector<std::string> foo(int i) {if (i < 5) {return {};  // 返回一个空 vector 对象
    }
    return {"tomo", "cat", "tomocat"};  // 返回列表初始化的 vector 对象
}

int main() {foo(10);
}

5. initializer_list 形参

后面提到 C ++11 反对所有类型的初始化,对于类类型而言,尽管咱们应用列表初始化它会主动调用匹配的构造函数,然而咱们也能显式指定承受初始化列表的构造函数。C++11 引入了std::initializer_list,容许构造函数或其余函数像参数一样应用初始化列表,这才真正意义上为类对象的初始化与一般数组和 POD 的初 始化办法提供了对立的桥梁。

Tips:

  • 类对象在被列表初始化时会优先调用列表初始化构造函数,如果没有列表初始化构造函数则会依据提供的花括号值调用匹配的构造函数
  • C++11 新规范提供了两种办法用于解决可变数量形参,第一种是咱们这里提到的 initializer_list 形参(所有的形参类型必须雷同),另一种是可变参数模板(能够解决不同类型的形参)
#include <initializer_list>
#include <vector>

class Cat {
 public:
    std::vector<int> data;
    Cat() = default;
    // 承受初始化列表的构造函数
    Cat(std::initializer_list<int> list) {for (auto it = list.begin(); it != list.end(); ++it) {data.push_back(*it);
        }
    }
};

int main() {Cat cat1 = {1, 2, 3, 4, 5};
    Cat cat2{1, 2, 3};
}

初始化列表除了用于对象构造函数上,还能够作为一般参数形参:

#include <initializer_list>
#include <string>
#include <iostream>

void print(std::initializer_list<std::string> list) {for (auto it = list.begin(); it != list.end(); ++it) {std::cout << *it << std::endl;}
}

int main() {print({"tomo", "cat", "tomocat"});
}

Reference

[1] https://blog.csdn.net/xiongya…

[2] https://my.oschina.net/u/9202…

[3] C++ Primer

[4] https://blog.csdn.net/linda_d…

[5] https://en.cppreference.com/w…

正文完
 0