105.C++初始化

发布时间 2023-07-15 11:03:44作者: CodeMagicianT

105.C++初始化

C++中变量的初始化有很多种方式,如:默认初始化值初始化直接初始化拷贝初始化列表初始化

1.默认初始化

默认初始化是指定义变量时 没有指定初值时 进行的初始化操作。

默认初始化变量的值与变量的类型与变量定义的位置有关系:

1.1内置类型变量

对于内置类型变量(如int,double,bool等),如果定义在语句块外(即{}外),则变量被默认初始化为0;如果定义在语句块内(即{}内),变量将拥有未定义的值

1.2static修饰的内置类型

对于static修饰的内置类型如果没有显式初始化,初始值为0

1.3类类型的变量

对于类类型的变量(如string或其他自定义类型),不管定义于何处,都会执行默认构造函数。如果该类没有默认构造函数,则会引发错误。因此,建议为每个类都定义一个默认构造函数(=default)

默认构造函数 = 编译器合成的默认构造函数+用户自定义的默认构造函数

  • 如果类中的数据成员在默认构造函数中进行了赋值,则默认初始化值优先使用默认构造函数的值
  • 如果在默认构造函数中没有赋值,但是该数据成员提供了类内初始值,则创建对象时,其默认初始值就是类内初始值
  • 如果在默认构造函数中没有赋值且没有类内初始值,对于内置类型,则其值未定义,对于类类型,则对其进行默认初始化

1.3.1当我们定义一个对象时,如果没有为这些对象提供初值,那么就会通过默认构造函数执行初始化默认构造函数无需任何实参

class Student
{
private:
    int id;

public:
    Student()   //显示定义一个默认构造函数
    {
        id = 500;
    }
    Student(int ID) :id(ID) {}//有参数的构造函数

};
Student WuKong;//定义一个对象,没有提供初始值,执行默认构造函数,即id=500

1.3.2如果我们的类没有显示定义任何构造函数,那么编译器就有可能为我们隐式的定义一个构造函数,这个构造函数称为合成的默认构造函数。由合成的默认构造函数按照一定规则初始化对象。

class Student
{
private:
    int id;

public:
    //这一块没有显示定义构造函数
};
Student WuKong;//定义一个对象,没有提供初始值,执行合成的默认构造函数进行初始化

1.3.3合成的默认构造函数初始化规则(在《Effective C++》 P35中指出惟有默认构造函数”被需要“的时候编译器才会合成默认构造函数。)

对于大多数类来说,这个合成的默认构造函数将按照以下规则来初始化数据成员:

(1)如果存在类内的初始值,用它来初始化成员 -std=C++11
struct Sales_item
{
    std::string bookNo;
    unsigned char units_sold = 0;//类内存在初始值
    double revenue = 0;//类内存在初始值
};
Sales_item It;//执行合成的默认构造函数,类内初始值初始化(情形1)
//类内初始值初始化之后,It.units_sold=0, It.revenue=0
(2)如果不存在类内初始值,也无任何默认构造函数(包括合成的默认构造函数和自定义的默认构造函数)

A.在局部作用域中实例化的对象无法进行默认初始化

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class Test
{
public:
    int i;
    double d;
    char* ptr;
};

void fun()
{

    Test funcoutTest;
    cout << funcoutTest.d << endl;
}


int main()
{
    fun();

    return 0;
}
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class Test
{
public:
    int i;
    double d;
    char* ptr;
};

int main()
{
    Test funcinTest;//函数体类定义的对象,那么内置型成员变量将不被初始化
    cout << funcinTest.d << endl;//错误

    return 0;
}

B.在局部作用域外实例化的对象进行默认初始化(初始化规则按照①②来)

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class Test
{
public:
    int i;
    double d;
    char* ptr;
};

Test funcoutTest;//函数体外定义的对象,那么
//对象中的内置型成员变量初始化为0,即funcoutTest.i=0,funcoutTest.d=0,funcoutTest.ptr=NULL

int main()
{
    cout << funcoutTest.d << endl;//错误

    return 0;
}

输出:

0

C.含有类对象数据成员,该类对象类型有默认构造函数。

如果一个类没有任何构造函数,但是它含有一个类对象数据成员,且该类对象类型有默认构造函数,那么编译器就会为该类合成一个默认构造函数,不过这个合成操作只有在构造函数真正需要被调用的时候才会发生。举个例子,编译器将为类B合成一个默认构造函数:

class A
{
public:
    A(bool _isTrue = true, int _num = 0) { isTrue = _isTrue; num = _num; }; //默认构造函数
    bool isTrue;
    int num;
};

class B
{
public:
    A a;//类A含有默认构造函数
    int b;
    //...
};

int main()
{
    B b;    //编译至此时,编译器将为B合成默认构造函数
    return 0;
}

被合成的默认构造函数做了什么事情?大概如下面这样:

B::B()
{
    a.A::A();
}

被合成的默认构造函数内只含必要的代码,它完成了对数据成员a的初始化,但不产生任何代码来初始化B::b。正如上面所说,初始化类的内置类型或复合类型成员是程序的责任而不是编译器的责任。为了满足程序的需要,我们一般会自己写构造函数来对B::b进行初始化,像这样:

B::B()
{
    a.A::A(); //编译器插入的代码
    b = 0;      //显示定义的代码
}

如果类中有多种类对象成员,则编译器按照这些类对象成员声明的顺序,在构造函数按顺序插入调用各个类默认构造函数的代码。

D.基类带有默认构造函数的派生类。

当一个类派生自一个含有默认构造函数的基类时,该类也符合编译器需要合成默认构造函数的条件。编译器合成的默认构造函数将根据基类声明顺序调用上层的基类默认构造函数。同样的道理,如果设计者定义了多个构造函数,编译器将不会重新定义一个合成默认构造函数,而是把合成默认构造函数的内容插入到每一个构造函数中去。

E.带有虚函数的类

类带有虚函数可以分为两种情况:

a.类本身定义了自己的虚函数

b.类从继承体系中继承了虚函数(成员函数一旦被声明为虚函数,继承不会改变虚函数的”虚性质“)。

这两种情况都使一个类成为带有虚函数的类。这样的类也满足编译器需要合成默认构造函数的类,原因是含有虚函数的类对象都含有一个虚表指针vptr,编译器需要对vptr设置初值以满足虚函数机制的正确运行,编译器会把这个设置初值的操作放在默认构造函数中。如果设计者没有定义任何一个默认构造函数,则编译器会合成一个默认构造函数完成上述操作,否则,编译器将在每一个构造函数中插入代码来完成相同的事情。

F.带有虚基类的类

虚基类的概念是存在于类与类之间的,是一种相对的概念。例如类A虚继承于类X,则对于A来说,类X是类A的虚基类,而不能说类X就是一个虚基类。虚基类是为了解决多重继承下确保子类对象中每个父类只含有一个副本的问题,比如菱形继承。如下图:

于是,类A对象中含有一份类X对象,类C中也含有一份类X对象,当我们遇上如下代码时:

class X { public: int i; };
class A : public virtual X { public:int j; };
class B : public virtual X { public:double d; };
class C : public A, public B { public: int k; };

void function(A* pa)
{
    pa->i = 1000;
}

int main()
{
    A* a = new A();
    C* c = new C();
    function(a);  //关注重点在这里
    function(c);     //关注重点在这里
    return 0;
}

函数function参数pa的真正类型是可以改变的,既可以把A对象指针赋值给pa,也可以把对象指针赋值给pa,在编译阶段并无法确定pa存储的i是属于A还是C的虚基类对象。为了解决这问题,编译器将产生一个指向虚基类X的指针,使得程序得以在运行期确定经由pa而存取的X::i的实际存储位置。这个指针的安插,编译器将会在合成默认构造函数中完成,同样的,如果设计者已经写了多个构造函数,那么编译器不会重新写默认构造函数,而是把虚基类指针的安插代码插入已有的构造函数中。[(9条消息) C++ 编译器提供的合成默认构造函数详解_编译器默认生成的构造函数_每天学一点!的博客-CSDN博客]

1.3.4自定义的默认构造函数初始化规则

匹配地调用构造函数

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class Test
{
public:
    Test()
    {

    }
public:
    int i;
    double d;
    char* ptr;
};

int main()
{
    Test funcinTest;
    cout << funcinTest.d << endl;

    return 0;
}
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class Test
{
public:
    Test(int I, double D, char* Ptr)
    {
        i = I;
        d = D;
    }
public:
    int i;
    double d;
    char* ptr;
};

int main()
{
    char q = 'a';
    Test funcinTest(5, 6, &q);//函数体类定义的对象,那么内置型成员变量将不被初始化
    cout << funcinTest.d << endl;//错误

    return 0;
}

2.值初始化

值初始化是指使用了初始化器(即使用了圆括号花括号)但却没有提供初始值的情况。

2.1什么时候进行值初始化

●当以空的括号或花括号 (C++11 起)对组成的初始化器创建无名临时对象时。
●当 new 表达式以空的括号或花括号 (C++11 起)对组成的初始化器创建具有动态存储期的对象时
●当以由空花括号对(不包括括号)组成的初始化器初始化已命名变量(自动、静态或线程局部)时。

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<vector>
using namespace std;

int main()
{
    int* p = new int();//值初始化
    vector<int> vec(10);//值初始化

    int c = int();//值初始化
    int d = int{};//正确
    //int e = ();//错误
    int f = {};//正确
    //int a();错误
    int b{};//正确

    return 0;
}

注意:当不采用动态分配内存的方式(即不采用new运算符)时,写成int a();是错误的值初始化方式,因为这种方式声明了一个函数而不是进行值初始化。如果一定要进行值初始化,必须结合拷贝初始化使用,即写成int a=int();

2.2值初始化的效果

  • 对于内置类型初始值为0
  • 对于类类型则调用其默认构造函数,如果没有默认构造函数,则不能进行值初始化。

(1)vector 对象的值初始化

vector<int> v1(10);//值初始化。10个元素,每个元素的初始化为0
vector<string> v2(10);//值初始化。10个元素,每个元素都为空
vector<int> v3{10};//列表初始化。v2有1个元素,该元素的值是10
vector<int> v4(10, 1);//直接初始化。v3有10个元素,每个的值都是1
vector<int> v5{10, 1};//列表初始化。v4有2个元素,值分别是10和1

(2)数组对象的值初始化

int a1[5]{1,2,3}; // a1被初始化为{1,2,3,0,0}
int a2[5] = {}; // a2被初始化为{0,0,0,0,0}

(3)聚合类的值初始化

struct Data
{
    string s;
    int ival;
};
Data data1 = {"hello"}; // data1.ival被初始化为0

(4)使用new动态分配和初始化对象

我们可以使用直接初始化方式来初始化一个动态分配的对象:

int *pi = new int(1024);//pi指向的对象的值为1024
string *ps = new string(10,'9');//*ps为"9999999999"

也可以对动态分配的对象进行值初始化,只需要在类型名后跟一对空括号即可:

string *ps1 = new string;//默认初始化为空string
string *ps2 = new string();//值初始化为空string
int *pi1 = new int;//默认初始化
int *pi2 = new int();//值初始化为0 

参考:[(9条消息) C++中的初始化_c语言initialized怎么解决_公子¥小白的博客-CSDN博客]

3.直接初始化

直接初始化是指采用小括号或者大括号的方式进行变量初始化(小括号里一定要有初始值,如果没提供初始值,那就是值初始化了!)。

int a(6);
int b{5};
int c(a);
string s("hello");//直接初始化
//s为字面值hello的副本。这里没有使用等号,所以为直接初始化
string str1(10,'9');//直接初始化
string str2(str1);//直接初始化

在下列场合进行直接初始化:

语法:

T 对象(实参);
T 对象(实参1, 实参2, ...);    (1)
-------------------------------------------------------------------------
T 对象{ 实参 };   (2)
-------------------------------------------------------------------------
T(其他对象);
T(实参1, 实参2, ...)   (3)
-------------------------------------------------------------------------
static_cast<T>(其他对象)  (4)
-------------------------------------------------------------------------
new T(实参列表,...)   (5)
-------------------------------------------------------------------------
类::类():成员(实参列表,...)   (6)
-------------------------------------------------------------------------
[实参](){...}   (7)

解释:

1)以表达式或花括号初始化器列表 (C++11 起)的非空带括号列表初始化

//C++11 container initializer
vector<string> vs={"first", "second", "third"};    
map<string,string> singers ={{"Lady Gaga", "+1 (212) 555-7890"},{"Beyonce Knowles", "+1 (212) 555-0987"}};

2)以花括号环绕的单个初始化器初始化一个非类类型对象(注意:对于类类型和其他使用花括号初始化器列表的初始化,见列表初始化

int b{5};
string s{ "sss" };

3)用函数式转型或以带括号的表达式列表初始化纯右值临时量 (C++17 前)纯右值的结果对象 (C++17 起)

4)用 static_cast 表达式初始化纯右值临时量 (C++17 前)纯右值的结果对象 (C++17 起)

5)用带有非空初始化器的 new 表达式初始化具有动态存储期的对象

6)用构造函数初始化器列表初始化基类或非静态成员

7)在 lambda 表达式中从按复制捕获的变量初始化闭包对象的成员

4.拷贝初始化

语法:

T 对象 = 其他对象;   (1)
T 对象 = { 其他对象 };   (2)
函数(其他对象);   (3)
return 其他对象;   (4)
throw 其他对象;
catch (T对象)   (5)
T数组[N] = { 其他对象序列 };   (6)

解释:

复制初始化在下列情况进行:

1)当声明非引用类型 目标类型 的具名变量(自动、静态或线程局部),带有以等号后随一个表达式所构成的初始化器时。

2)(C++11 前)当声明标量类型 目标类型 的具名变量,带有以等号后随一个花括号环绕的表达式所构成的初始化器时(注意:从 C++11 开始,这被归类为列表初始化,且不允许窄化转换)。

3)当按值传递参数到函数时

4)当从具有返回值的函数返回

5)当按值抛出捕获异常时

6)作为聚合初始化的一部分,以初始化提供了初始化器的每个元素

5.列表初始化

语法:

直接列表初始化

T 对象{ 实参1, 实参2, ... };
T{ 实参1, 实参2, ... }	(2)
new T{ 实参1, 实参2, ... }	(3)
类 
{
	T 成员{ 实参1, 实参2, ... };
};	(4)
类::类() : 成员{ 实参1, 实参2, ... } {...	(5)

拷贝列表初始化:

T 对象 = { 实参1, 实参2, ... };	(6)
函数({ 实参1, 实参2, ... })	(7)
return { 实参1, 实参2, ... };	(8)
对象[{ 实参1, 实参2, ... }](9)
对象 = { 实参1, 实参2, ... }	(10)
U({ 实参1, 实参2, ... })	(11)
类 
{
	T 成员 = { 实参1, 实参2, ... };
};	(12)

在下列情形进行列表初始化:

  • 直接列表初始化(考虑 explicit 和非 explicit 构造函数)

1)以 花括号初始化器列表(即花括号环绕的可以为空的表达式或 花括号初始化器列表 的列表)初始化具名变量

2)以 花括号初始化器列表 初始化无名临时量

3)以 new 表达式初始化具有动态存储期的对象,它的初始化器是 花括号初始化器列表 列表

4)在不使用等号的非静态数据成员初始化器

5)在构造函数的成员初始化列表中,如果使用 花括号初始化器列表

  • 复制列表初始化(考虑 explicit 和非 explicit 构造函数,但只能调用非 explicit 构造函数)

6)以等号后的 花括号初始化器列表 初始化具名变量

7) 函数调用表达式中,以 花括号初始化器列表 为实参,以列表初始化对函数形参初始化

8)在以 花括号初始化器列表 为返回表达式的 return 语句中,以列表初始化对返回的对象初始化

9)在具有用户定义的 operator[]下标表达式中,以列表初始化对重载运算符的形参初始化

10)在赋值表达式中,以列表初始化对重载的运算符的形参初始化

11) 函数式转型表达式或其他构造函数调用,其中 花括号初始化器列表 用作构造函数实参。以复制初始化对构造函数的形参初始化(注意:此例中的类型 U 不是被列表初始化的类型;但 U 的构造函数的形参是)

12)在使用等号的非静态数据成员初始化器

解释:

T 类型的对象的列表初始化的效果是:

  • 如果 T 是聚合类且 花括号初始化器列表 拥有单个(可有 cv 限定的)相同类型或派生类型的元素,那么从该元素初始化对象(对于复制列表初始化为复制初始化,对于直接列表初始化为直接初始化)。

  • 否则,如果 T 是字符数组且初始化器列表拥有单个类型适当的字符串字面量元素,那么照常从字符串字面量初始化数组。

  • 否则,如果 T聚合类型,那么进行聚合初始化

  • 否则,如果 花括号初始化器列表 列表为空,且 T 是拥有默认构造函数的类类型,那么进行值初始化

  • 否则,如果 Tstd::initializer_list 的特化,那么依据它所在的语境以 花括号初始化器列表 所初始化的同类型的纯右值 (C++17 前)直接初始化或复制初始化该 T 对象。

  • 否则,以两个阶段考虑 T 的构造函数: