C++类与对象总结(二)构造函数

构造器Constructor

构造器(函数)一种特殊的成员函数,用于对象的初始化。名称与类的名称相同,可以重载,无返回类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

class A
{
private:
int a,b;
public:
A(int aa,int bb)
{
a = aa;
b = bb;
}
};

int main()
{
A a(1,2);

return 0;
}

在这里我们定义的A类的一个参数为两个int的构造函数,在main函数中a被创建时需要用括号传参。

初始化列表Initializer List

对于上面的构造函数C++还有其他的做法,像下面代码中的构造函数。 初始化列表的动作是在构造函数体执行之前进行的,初始化顺序是按照成员变量的声明顺序进行的与初始化列表中的顺序无关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

class A
{
private:
int a,b;
public:
A(int aa,int bb):a(aa),b(bb){}
};

int main()
{
A a(1,2);

return 0;
}

在这里你可以简单的认为两者效果是一致的,但如果成员不是基础数据类型两者就会有很大不同。

举个例子:

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
#include <iostream>
using namespace std;

class B
{
private:
int a,b;
public:
B(int aa,int bb)
{
a = aa;
b = bb;
}
};

class A
{
private:
int a,b;
B c;
public:
A(int aa,int bb)
{
a = aa;
b = bb;
c(1,2);
}
};

int main()
{
A a(1,2);

return 0;
}

这里我们又声明了一个B类,在A类中添加了一个成员c为B类的对象。 我们用g++编译这个程序会得到这样的一条错误:

error: constructor for ‘A’ must explicitly initialize the member ‘c’ which does not have a default constructor

它说我们必须显式初始化成员c,因为c没有缺省构造函数(缺省构造函数在下面会讲解)。 这个错误很奇怪,我们明明那么明显的初始化了c,它竟然说我们没有显式初始化,还要什么缺省构造函数。 这里的原因就在于它在进构造函数之前就要进行初始化,如果没有初始化列表编译器会去找缺省构造函数去初始化它。如果B类有缺省构造函数,这个代码仍然是错误的,因为在26行这个不是正确的初始化,初始化一个对象要么在初始化列表中,要么在它定义是地方初始化,26行这种方式编译器会去找()的运算符重载函数。

1
2
3
4
5
6
7
8
class A
{
private:
int a,b;
B c;
public:
A(int aa,int bb):a(aa),b(bb),c(1,2){}
};

正确的A类的构造函数应该这样写,在初始化c时它会调用c的构造函数来初始化它。

特殊的构造函数

缺省构造函数Default Constructor

对于上面的A类来说如果需要创建一个A类的数组,我们需要这样一句话定义一个三单元的数组里面每个都要用括号给出参数。

1
A b[3] = {A(1,2),A(2,3),A(3,4)};

如果我们要创建一个100单元的数组,如果是int的,我们一般是先定义好再用循环赋值。 我们想这样写:

1
A c[100];

但对于A这样是不可以的,因为它的构造函数需要两个int作为参数而这里创建时并没有给出参数,在定义时它不能初始化这些对象。 我们需要另一种构造函数,缺省构造函数。只要是我们写的没有参数的构造函数就叫做缺省构造函数。

1
A(){}

重载构造函数就可以创建出c数组。 如果一个类没有声明任何构造函数,编译器会自动生成一个不接受任何参数不做任何操作的缺省构造函数。

拷贝构造函数Copy Constructor

1
2
3
A a(1,2);
A b = a;
// A b(a);

如果我们定义了a这个对象,如果在定义初始化b时想直接拿a的值来初始化b(需要明白这里的等号与括号初始化等价,与赋值无关),我们需要用到拷贝构造函数,拷贝构造函数需要拿另一个该类的对象(引用)作为参数,初始化时可以用等号或用括号。

1
2
3
4
5
A(const A &p)
{
a = p.a;
b = p.b;
}

对于A类简单的拷贝构造函数就是这样,拷贝构造函数只是说它的参数是该类的对象的引用,函数体里你可以做其他操作。 如果一个类没有声明拷贝构造函数,编译器会自动生成一个隐式的拷贝构造函数(Implicitly Copy Constructor),它将对做成员拷贝(Memberwise Copy),如果成员是基础数据类型就是基本的初始化,如果成员是其他对象就调用那个对象的拷贝构造函数做初始化,C++中的隐式拷贝构造并不是位拷贝(Bitwise Copy)。

拷贝构造函数的参数为什么必须是引用?

因为函数传值调用需要做一步拷贝构造,如果拷贝构造函数是传值调用,那么就会在传值时再次调用拷贝构造函数,就会引起无限的递归。

拷贝构造函数在哪里会被调用?

  1. 对象初始化
  2. 函数的传值调用
  3. 函数的返回值

第一种情况前面已经有所介绍,后两种情况需要注意不同的编译器会有不同的表现,一般来说编译器会把很多不必要的拷贝构造优化掉。 比如这段代码:
Ubuntu Pastebin : https://paste.ubuntu.com/=rCHw4pHhMn/

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
#include <iostream>
using namespace std;
int num = 0;
class A
{
private:
int a,b;
public:
A(){}
A(int aa,int bb):a(aa),b(bb){}
A(const A &p)
{
num++;
cout << num << "copy" << endl;
a = p.a;
b = p.b;
}
};

A f(A a) //1
{
A re(a); //2
return re; //3
}


int main()
{
A a(1,2);
A b = f(a); //4
cout << num << endl;
return 0;
}

如果是按刚刚讲的逻辑应该发生四次拷贝构造,我在代码中标注的四处。但如果你用不同编译器或者不同的编译选项编译都会有不同的结果,在这里我使用CentOS下的g++、MacOS下的g++和Windows 10下的DevC++(MinGW)测试的结果都为调用了两次。

1
2
3
4
5
6
7
[zyx@centos-linux c]$	g++ test.cpp
[zyx@centos-linux c]$ ./a.out
1copy
2copy
2

//CentOS g++ 64bit

而在Windows 10的Visual Studio 2013的结果为三次。

1
2
3
4
5
6
1copy
2copy
3copy
3

//Visual Studio 2013 64bit

对于Visual Studio 2017依然如此

1
2
3
4
5
6
1copy
2copy
3copy
3

//Visual Studio 2017 64bit

Windows下CB的与g++和MinGW相同

1
2
3
4
5
1copy
2copy
2

//CB 64bit

可以说明不同的编译器在拷贝构造上有不同的优化策略。

深入探究拷贝构造函数的编译器优化:

之前说的调用拷贝构造的规则在编译器的优化面前好像就没了规则,为什么会这样呢?对于发生了三次和两次拷贝构造的情况它们究竟优化掉了哪一次或两次呢? C++标准(N4527) § 12.8 Copying and moving class objects 第31条(P.293/294)中这样写到:

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object. If the first parameter of the selected constructor is an rvalue reference to the object’s type, the destruction of that object occurs when the target would have been destroyed; otherwise, the destruction occurs at the later of the times when the two objects would have been destroyed without the optimization. This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

这里只是说编译器可以在一些情况下不考虑副作用对拷贝或者移动构造函数进行优化,而且对于标准中的四种情况可以删除多份。 之后列举了四条基本情况,我们只关心第一和第三条:

(31.1) — in a return statement in a function with a class return type, when the expression is the name of a nonvolatile automatic object (other than a function parameter or a variable introduced by the exceptiondeclaration of a handler (15.3)) with the same type (ignoring cv-qualification) as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value. (31.3) — when the exception-declaration of an exception handler (Clause 15) declares an object of the same type (except for cv-qualification) as the exception object (15.1), the copy operation can be omitted by treating the exception-declaration as an alias for the exception object if the meaning of the program will be unchanged except for the execution of constructors and destructors for the object declared by the exception-declaration. [ Note: There cannot be a move from the exception object because it is always an lvalue. — end note ]

第一条:函数的 return 语句中的表达式是一个非 volatile 的对象,并且其非const&volatile类型和函数返回值的非const&volatile类型相同,此时可以省略一次拷贝或移动构造函数(移动构造函数为C++11的扩展)。 第三条:对于一个非const&volatile的临时对象且没有绑定引用,它的复制/移动操作,可以省略。 对于第一条明确的标准几乎所有的编译器对此都做了优化,也就是代码中第3号点被优化掉了 对于第三条在G++和CB中第4号点因此被优化,也就是对象b的构造没有调用拷贝构造函数。
g++提供了一个编译选项:-fno-elide-constructors 开启这个选项编译器会关闭上述关于拷贝和移动构造函数的优化。

1
2
3
4
5
6
7
8
9
[zyx@centos-linux c]$	g++ -fno-elide-constructors test.cpp
[zyx@centos-linux c]$ ./a.out
1copy
2copy
3copy
4copy
4

//CentOS g++ 64bit

运行结果确实是4次

这样我们可以通过对比汇编代码来看具体它在哪里优化了拷贝构造。

代码:

默认优化

Ubuntu Pastebin : https://paste.ubuntu.com/=Wm88H4Vhxf/

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
	.globl	__Z1f1A
.p2align 4, 0x90
__Z1f1A: ## @_Z1f1A
.cfi_startproc
## BB#0:
pushq %rbp
Lcfi0:
.cfi_def_cfa_offset 16
Lcfi1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Lcfi2:
.cfi_def_cfa_register %rbp
subq $16, %rsp
movq %rdi, %rax
movq %rax, -8(%rbp) ## 8-byte Spill
callq __ZN1AC1ERKS_
movq -8(%rbp), %rax ## 8-byte Reload
addq $16, %rsp
popq %rbp
retq
.cfi_endproc

.globl __ZN1AC1ERKS_
.weak_def_can_be_hidden __ZN1AC1ERKS_
.p2align 4, 0x90





_main: ## @main
.cfi_startproc
## BB#0:
pushq %rbp
Lcfi6:
.cfi_def_cfa_offset 16
Lcfi7:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Lcfi8:
.cfi_def_cfa_register %rbp
subq $64, %rsp
leaq -32(%rbp), %rdi
movl $1, %esi
movl $2, %edx
movl $0, -20(%rbp)
callq __ZN1AC1Eii
leaq -48(%rbp), %rdi
leaq -32(%rbp), %rsi
callq __ZN1AC1ERKS_
leaq -40(%rbp), %rdi
leaq -48(%rbp), %rsi
callq __Z1f1A
movq __ZNSt3__14coutE@GOTPCREL(%rip), %rdi
movl _num(%rip), %esi
callq __ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEElsEi
leaq __ZNSt3__14endlIcNS_11char_traitsIcEEEERNS_13basic_ostreamIT_T0_EES7_(%rip), %rdi
movq %rax, -8(%rbp)
movq %rdi, -16(%rbp)
movq -8(%rbp), %rdi
callq *-16(%rbp)
xorl %edx, %edx
movq %rax, -56(%rbp) ## 8-byte Spill
movl %edx, %eax
addq $64, %rsp
popq %rbp
retq
.cfi_endproc

关闭优化

Ubuntu Pastebin : https://paste.ubuntu.com/=7CV893KdNY/

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
	.globl	__Z1f1A
.p2align 4, 0x90
__Z1f1A: ## @_Z1f1A
.cfi_startproc
## BB#0:
pushq %rbp
Lcfi0:
.cfi_def_cfa_offset 16
Lcfi1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Lcfi2:
.cfi_def_cfa_register %rbp
subq $32, %rsp
movq %rdi, %rax
leaq -8(%rbp), %rcx
movq %rdi, -16(%rbp) ## 8-byte Spill
movq %rcx, %rdi
movq %rax, -24(%rbp) ## 8-byte Spill
callq __ZN1AC1ERKS_
leaq -8(%rbp), %rsi
movq -16(%rbp), %rdi ## 8-byte Reload
callq __ZN1AC1ERKS_
movq -24(%rbp), %rax ## 8-byte Reload
addq $32, %rsp
popq %rbp
retq
.cfi_endproc





.globl _main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## BB#0:
pushq %rbp
Lcfi6:
.cfi_def_cfa_offset 16
Lcfi7:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Lcfi8:
.cfi_def_cfa_register %rbp
subq $64, %rsp
leaq -32(%rbp), %rdi
movl $1, %esi
movl $2, %edx
movl $0, -20(%rbp)
callq __ZN1AC1Eii
leaq -56(%rbp), %rdi
leaq -32(%rbp), %rsi
callq __ZN1AC1ERKS_
leaq -48(%rbp), %rdi
leaq -56(%rbp), %rsi
callq __Z1f1A
leaq -40(%rbp), %rdi
leaq -48(%rbp), %rsi
callq __ZN1AC1ERKS_
movq __ZNSt3__14coutE@GOTPCREL(%rip), %rdi
movl _num(%rip), %esi
callq __ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEElsEi
leaq __ZNSt3__14endlIcNS_11char_traitsIcEEEERNS_13basic_ostreamIT_T0_EES7_(%rip), %rdi
movq %rax, -8(%rbp)
movq %rdi, -16(%rbp)
movq -8(%rbp), %rdi
callq *-16(%rbp)
xorl %edx, %edx
movq %rax, -64(%rbp) ## 8-byte Spill
movl %edx, %eax
addq $64, %rsp
popq %rbp
retq
.cfi_endproc

默认优化下程序在17和51行调用了A的拷贝构造函数,而在关闭拷贝构造后程序分别在20、23、55、61行调用了拷贝构造函数。
这里分析可以发现编译器优化掉了 函数返回值的构造 和 对象b的构造。
Visual Studio 2013 64bit 反汇编代码:
Ubuntu Pastebin : https://paste.ubuntu.com/=h3mV88hjrn/

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
A f(A a)    //1
{
00395850 55 push ebp
00395851 8B EC mov ebp,esp
00395853 81 EC D4 00 00 00 sub esp,0D4h
00395859 53 push ebx
0039585A 56 push esi
0039585B 57 push edi
0039585C 8D BD 2C FF FF FF lea edi,[ebp-0D4h]
00395862 B9 35 00 00 00 mov ecx,35h
00395867 B8 CC CC CC CC mov eax,0CCCCCCCCh
0039586C F3 AB rep stos dword ptr es:[edi]
0039586E A1 00 00 3A 00 mov eax,dword ptr ds:[003A0000h]
00395873 33 C5 xor eax,ebp
00395875 89 45 FC mov dword ptr [ebp-4],eax
A re(a); //2
00395878 8D 45 0C lea eax,[a]
0039587B 50 push eax
0039587C 8D 4D F0 lea ecx,[re]
0039587F E8 37 BB FF FF call A::A (03913BBh)
return re; //3
00395884 8D 45 F0 lea eax,[re]
00395887 50 push eax
00395888 8B 4D 08 mov ecx,dword ptr [ebp+8]
0039588B E8 2B BB FF FF call A::A (03913BBh)
00395890 8B 45 08 mov eax,dword ptr [ebp+8]
}




int main()
{
00F16060 55 push ebp
00F16061 8B EC mov ebp,esp
00F16063 81 EC F0 00 00 00 sub esp,0F0h
00F16069 53 push ebx
00F1606A 56 push esi
00F1606B 57 push edi
00F1606C 8D BD 10 FF FF FF lea edi,[ebp-0F0h]
00F16072 B9 3C 00 00 00 mov ecx,3Ch
00F16077 B8 CC CC CC CC mov eax,0CCCCCCCCh
00F1607C F3 AB rep stos dword ptr es:[edi]
00F1607E A1 00 00 F2 00 mov eax,dword ptr ds:[00F20000h]
00F16083 33 C5 xor eax,ebp
00F16085 89 45 FC mov dword ptr [ebp-4],eax
A a(1, 2);
00F16088 6A 02 push 2
00F1608A 6A 01 push 1
00F1608C 8D 4D F0 lea ecx,[a]
00F1608F E8 81 B3 FF FF call A::A (0F11415h)
A b = f(a); //4
00F16094 83 EC 08 sub esp,8
00F16097 8B CC mov ecx,esp
00F16099 8D 45 F0 lea eax,[a]
00F1609C 50 push eax
00F1609D E8 19 B3 FF FF call A::A (0F113BBh)
00F160A2 8D 4D E0 lea ecx,[b]
00F160A5 51 push ecx
00F160A6 E8 6D B0 FF FF call f (0F11118h)
00F160AB 83 C4 0C add esp,0Ch
cout << num << endl;
00F160AE 8B F4 mov esi,esp
00F160B0 68 E8 13 F1 00 push 0F113E8h
00F160B5 8B FC mov edi,esp
00F160B7 A1 20 03 F2 00 mov eax,dword ptr ds:[00F20320h]
00F160BC 50 push eax
00F160BD 8B 0D A0 10 F2 00 mov ecx,dword ptr ds:[0F210A0h]
00F160C3 FF 15 94 10 F2 00 call dword ptr ds:[0F21094h]
00F160C9 3B FC cmp edi,esp
00F160CB E8 64 B2 FF FF call __RTC_CheckEsp (0F11334h)
00F160D0 8B C8 mov ecx,eax
00F160D2 FF 15 90 10 F2 00 call dword ptr ds:[0F21090h]
00F160D8 3B F4 cmp esi,esp
00F160DA E8 55 B2 FF FF call __RTC_CheckEsp (0F11334h)
return 0;
00F160DF 33 C0 xor eax,eax
}

VS的反汇编代码中有C++代码注释,它分别在20、25、57行调用了A的拷贝构造函数,可以看出VS只是优化掉了对象b的构造

本文标题:C++类与对象总结(二)构造函数

文章作者:赵砚潇

发布时间:2018年02月11日 - 22:02

最后更新:2018年07月05日 - 01:07

原始链接:https://blog.zyx.sh/2018/02/11/cpp-class-2/

许可协议: 署名-非商业性使用-相同方式分享 4.0 国际 转载请保留原文链接及作者。

Donate comment here