C++ 教程 | cpp 初始化过程那些事
默认初始化
什么是默认初始化?
默认初始化是指定义变量时没有指定初值时进行的初始化操作。例如 int a
,Sales_data myData
等等。
这些变量被定义了而不是仅仅被声明(因为没有extern关键字修饰),而且没有显式的赋予初值。
特别的,如果采用动态分配内存的方式(即采用new关键字)创建的变量,不加括号时(如int *p=new int
;)也是默认初始化,加了括号(如int *p=new int()
)为值初始化。
规则
变量的值与变量的类型与定义的位置有关系:
对于内置类型变量(如
int
,double
,bool
等),定义于任何函数之外的变量被初始化为0, 定义于函数体内部的内置类型将不被初始化,一个未被初始化的内置类型变量的值是未定义的.对于类类型的变量(如
string
或 其他自定义类型),不管定义于何处,都会执行默认构造函数。如果该类没有默认构造函数,则会引发错误。因此,建议为每个类都定义一个默认构造函数(=default
)。
例如, 对于在类中:
1
2
3
4
5
6
class Persion {
private:
std::string name; // 执行默认初始化过程
int age; // = 0
char sex; // = 0
};
在执行构造函数之前, 变量将首先进入默认初始化。 name
将首先使用默认构造函数进行初始化, 而 age
, sex
将 被初始化为 0
.
对于函数中:
1
2
3
4
int *pi=new int; // 默认初始化, *pi的值为未定义。
int *pi=new int(); // 值初始化为0,*pi的值为0
string *ps=new string; // 默认初始化 *ps的值为空string
string *ps=new string(); // 值初始化为空string
值初始化
值初始化是值使用了初始化器(即使用了圆括号或花括号)但却没有提供初始值的情况。例如,int *p=new int()
, vector<string> vec(10)
; 等等都是典型的值初始化方式。
注意,当不采用动态分配内存的方式(即不采用new运算符)时,写成int a()
; 是错误的值初始化方式,因为这种方式声明了一个函数而不是进行值初始化。
如果一定要进行值初始化,必须结合拷贝初始化使用,即写成int a=int()
; 值初始化和默认初始化一样,对于内置类型初始化为 0,对于类类型则调用其默认构造函数,如果没有默认构造函数,则不能进行初始化。
直接初始化与拷贝初始化
直接初始化与拷贝初始化对应,其内部实现机理不同。
直接初始化是指采用小括号的方式进行变量初始化(小括号里一定要有初始值,如果没提供初始值,那就是值初始化了!)。
例如 :
1
2
3
4
5
int a(12);
Sales_data myData(para);
vector<int> ivec(ivec2);
string s("123456");
...
拷贝初始化是指采用等号=
进行初始化的方式。
例如 :
1
2
3
int a=12;
string s=string("123456");
...
拷贝初始化看起来像是给变量赋值,实际上是执行了初始化操作,与先定义再赋值本质不同。
对于内置类型变量(如
int
,double
,bool
等),直接初始化与拷贝初始化差别可以忽略不计。对于类类型的变量(如string或其他自定义类型),直接初始化调用类的构造函数(调用参数类型最佳匹配的那个),拷贝初始化调用类的拷贝构造函数。
特别的,当对类类型变量进行初始化时,如果类的构造函数采用了 explicit
修饰而且需要隐式类型转换时,则只能通过直接初始化而不能通过拷贝初始化进行操作。
拷贝构造函数和赋值运算符
在默认情况下(用户没有定义,但是也没有显式的删除),编译器会自动的隐式生成一个拷贝构造函数和赋值运算符。但用户可以使用 delete
来指定不生成拷贝构造函数和赋值运算符,这样的对象就不能通过值传递,也不能进行赋值运算。
1
2
3
4
5
6
7
8
9
10
11
12
class Person
{
public:
Person(const Person& p) = delete;
Person& operator=(const Person& p) = delete;
private:
int age;
string name;
};
上面的定义的类Person显式的删除了拷贝构造函数和赋值运算符,在需要调用拷贝构造函数或者赋值运算符的地方,会提示_无法调用该函数,它是已删除的函数_。 还有一点需要注意的是,拷贝构造函数必须以引用的方式传递参数。这是因为,在值传递的方式传递给一个函数的时候,会调用拷贝构造函数生成函数的实参。如果拷贝构造函数的参数仍然是以值的方式,就会无限循环的调用下去,直到函数的栈溢出。
何时调用
拷贝构造函数和赋值运算符的行为比较相似,都是将一个对象的值复制给另一个对象;但是其结果却有些不同: 拷贝构造函数使用传入对象的值生成一个新的对象的实例,而赋值运算符是将对象的值复制给一个已经存在的实例。
这种区别从两者的名字也可以很轻易的分辨出来,拷贝构造函数也是一种构造函数,那么它的功能就是创建一个新的对象实例;赋值运算符是执行某种运算,将一个对象的值复制给另一个对象(已经存在的)。
调用的是拷贝构造函数还是赋值运算符,主要是看是否有新的对象实例产生。如果产生了新的对象实例,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符。
调用拷贝构造函数主要有以下场景:
- 对象作为函数的参数,以值传递的方式传给函数。
- 对象作为函数的返回值,以值的方式从函数返回
- 使用一个对象给另一个对象初始化
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
class Person
{
public:
Person(){}
Person(const Person& p)
{
cout << "Copy Constructor" << endl;
}
Person& operator=(const Person& p)
{
cout << "Assign" << endl;
return *this;
}
private:
int age;
string name;
};
void f(Person p)
{
return;
}
Person f1()
{
Person p;
return p;
}
int main()
{
Person p;
Person p1 = p; // 1
Person p2;
p2 = p; // 2
f(p2); // 3
p2 = f1(); // 4
Person p3 = f1(); // 5
getchar();
return 0;
}
分析如下:
- 这是虽然使用了”=”,但是实际上使用对象p来创建一个新的对象p1。也就是产生了新的对象,所以调用的是拷贝构造函数。
- 首先声明一个对象p2,然后使用赋值运算符”=”,将p的值复制给p2,显然是调用赋值运算符,为一个已经存在的对象赋值 。
- 以值传递的方式将对象p2传入函数f内,调用拷贝构造函数构建一个函数f可用的实参。
- 这条语句拷贝构造函数和赋值运算符都调用了。函数f1以值的方式返回一个Person对象,在返回时会调用拷贝构造函数创建一个临时对象tmp作为返回值;返回后调用赋值运算符将临时对象tmp赋值给p2.
- 按照4的解释,应该是首先调用拷贝构造函数创建临时对象;然后再调用拷贝构造函数使用刚才创建的临时对象创建新的对象p3,也就是会调用两次拷贝构造函数。不过,编译器也没有那么傻,应该是直接调用拷贝构造函数使用返回值创建了对象p3。
列表初始化
列表初始化是 C++ 11 新引进的初始化方式,它采用一对花括号(即{}
)进行初始化操作。能用直接初始化和拷贝初始化的地方都能用列表初始化,而且列表初始化能对容器进行方便的初始化,因此在新的C++标准中,推荐使用列表初始化的方式进行初始化。
列表初始化的应用场景有:
1
2
3
int a{12};
string s{"123"};
vector<int> vec{1,2,3};
这里一定要注意,列表初始化使用的是花括号而不是圆括号!
转载自