什么是构造函数?默认构造函数
构造函数是类对象在使用之前进行的初始化工作。对于类来说,可以通过构造函数来进行初始化相关操作。
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
Student() {
// 进行初始化操作
age = 18;
name = "hello world";
}
int age; // 年龄
string name; // 名字
};
int main(void) {
Student st; // 构建一个 Student 的对象,这个过程会初始化 st 这个变量,初始化的方式就是构造函数
cout << "student>>st: name=" << st.name << " age=" << st.age << endl;
return 0;
}
在这个过程中,首先创建了一个 Student 的变量,名为 st,这个过程会调用 st 的 Student 类的默认构造函数:Student(), 来进行初始化操作。
在使用下面这种方法初始化的时候:
Student st;
默认调用的是 Student 的无参的构造函数,我们显示提供了 Student 的无参构造函数,编译器不会再提供给我们新的无参构造函数。
提示:如果我们没有显式提供无参构造函数,编译器就会给我们的代码提供默认的无参构造函数。
总结一下就是,只要是用一个类的对象,都必须要先调用它的构造函数,无论是调用有参的构造函数,还是无参的构造函数,反正必须得调用一个。只有这样,编译器才能认为这个对象是一个完整可用的对象。
默认构造函数
用下面这种方式创建的类对象,使用的是默认构造函数:
Person person;
默认构造函数有如下特点:
- 当创建对象时没有提供任何初始化参数,编译器会自动调用默认构造函数
- 两种存在形式
- 编译器隐式生成的
- 如果类中没有定义任何构造函数,编译器会自动生成一个 public 的、inline 的默认构造函数
- 这个隐式构造函数不会初始化基本类型成员(int、double、指针等),它们的值是未定义的
- 用户自定义的
- 只要程序员定义了任意形式的构造函数,编译器就不会自动生成默认构造函数
- 如果想保留默认版本,可以用
= default强制生成
- 编译器隐式生成的
总结下来就是:
- 无参或全缺省
class Demo {
public:
Demo() { } // 无参
Demo(int a = 0, int b = 0) { } // 全缺省(也属于默认构造函数,但有二义性风险)
};
- 对成员执行默认初始化
- 类类型成员(自定义的类) → 调用其默认构造函数
- 基本类型成员 → 不初始化(除非有类内初始值)
- 数组成员 → 如果是基本类型,不初始化
显式提供无参构造函数
#include <iostream>
using namespace std;
class MyClass {
public:
int value;
// 无参构造函数(默认构造函数)
MyClass() {
value = 0;
cout << "无参构造函数被调用!" << endl;
}
};
int main() {
MyClass obj; // 调用默认构造函数
cout << "value = " << obj.value << endl;
return 0;
}
显式提供无参构造函数作为默认构造函数之后,编译器不再提供无参构造函数作为默认构造函数。
所有参数都有默认值的构造函数
这种也称为全缺省的默认构造函数。
#include <iostream>
using namespace std;
class MyClass {
public:
int a, b;
// 所有参数都有默认值
MyClass(int x = 0, int y = 0) {
a = x;
b = y;
cout << "带默认参数的构造函数被调用!" << endl;
}
};
int main() {
MyClass obj1; // 全部使用默认值
MyClass obj2(5); // a=5, b=0(默认值)
MyClass obj3(10, 20); // a=10, b=20
cout << "obj1: a=" << obj1.a << ", b=" << obj1.b << endl;
cout << "obj2: a=" << obj2.a << ", b=" << obj2.b << endl;
cout << "obj3: a=" << obj3.a << ", b=" << obj3.b << endl;
return 0;
}
编译器提供的构造函数
当没有显式提供上面两种构造函数的时候,编译器就会提供一个无参的构造函数作为默认构造函数,但是编辑器提供的,具有很多限制:
#include <iostream>
using namespace std;
class MyClass {
public:
int x;
double y;
// 没有定义任何构造函数
};
int main() {
MyClass obj; // 调用编译器生成的无参构造函数
cout << "x = " << obj.x << ", y = " << obj.y << endl;
return 0;
}
上述代码是有问题的,因为编译器提供的无参构造函数,是不会管内置类型的,因此它里面值是随机值,根本就无法使用,甚至有点编辑器编译直接报错,提示你:obj 未初始化。
而且如果在有自定义类型的类中调用编译器提供的无参构造函数,就会出现问题。
调用的逻辑是,创建 c 变量,调用 Container 的默认构造函数,这个默认构造函数是编译器默认提供的,他不管内置类型,并且会调用自定义类型(BadInner)的默认构造函数,但是 Badinner 的默认构造函数也是编译器提供的,因此也不会管内置类型,也就是 BadInner 的 value 属性。
这个时候如果你访问 c.member.value,那么这个 value 就是未定义的,有的编译器甚至会直接编译报错。
除非你给他加上默认值,或者显式提供 BadInner 的默认构造函数。
上述三种方法都可以输出 a = 10。
但是需要注意的是,默认构造函数中,程序员手动提供的和编译器提供的只能存在一个,如果手动提供(无参或者全缺省),那么编译器就不会再提供。
显式提供的也只能在无参和全缺省中选一个,如果全部提供,在调用的时候,就会产生歧义:
#include <iostream>
class BadInner {
public:
int value;
BadInner(int value_input = 10) { // value = value_input; }
BadInner() { value = 200; }
};
class Container {
public:
BadInner member;
};
int main() {
Container c;
int a = c.member.value;
using namespace std;
cout << "a = " << a << endl;
return 0;
}
编译直接报错。
显式提供的无参构造函数,其自定义逻辑全在程序员身上,内置类型如果不手动初始化,就默认不做处理,其值为变量所占内存地址空间的随机垃圾值。如果自定义类型不初始化,同样会调用其默认构造函数。
但是,此时如果自定义类型没有默认构造函数,那么他作为成员去被默认构造函数初始化的时候,就会编译报错。
C++ 就关于默认构造函数这块,逻辑比较复杂,我们再复习一次:
如果你不显示提供构造函数,那么编译器在编译的时候就会生成一个默认的构造函数,这个编译器生成的构造函数不会管内置类型,有自定义类型,就会去调用自定义类型的默认构造函数,如果自定义类型没有默认构造函数,也就是自定义类型显式提供了有参数的构造函数,那么就会编译报错,如果有默认构造函数,就会调用默认构造函数,去初始化这个定义类型,但是如果这个内部的自定义类型的默认构造函数也是编译器提供的,那么就会产生套娃逻辑,如果是用户提供的默认构造函数,那么就会执行这个用户自定义构造函数,至于说里面的变量能初始化到什么程度,全看程序员,小心你会使用到编译没有报错,但是没有被初始化的内置类型变量值,因为他可能忘了初始化内置类型。即使是用户提供的默认构造函数没有初始化自定义,同样也会去调用其自定义类型的默认构造函数。
以下三种都属于默认构造函数:
- 没有显式提供构造函数,编译器默认提供的构造函数
- 显式提供的无参数的构造函数
- 显式提供参数为全缺省的构造函数
具有参数的构造函数
只有没有内置类型,而且其自定类型能根据默认构造函数完成自身初始化的类,才适合编译器提供无参构造函数。
如果你提供了带有参数的构造函数,那么编译器不再提供无参的构造函数。
#include <iostream>
using namespace std;
class Person {
public:
int age_;
string name_;
Person(int age, string name) {
age_ = age;
name_ = name; // std::string 重载了=
}
};
int main(){
Person p(19, "xiaobo");
cout << "age = " << p.age_ << ", name = " << p.name_ << endl;
return 0;
}
此时会输出:
age = 19, name = xiaobo
但是此时你无法通过默认构造函数来初始化这个类的对象。
IDE 提示错误。
那有人就要问,那我既要又要你怎么办?没事,C++ 为我们提供了一种显式要求提供默认构造函数的方法:
#include <iostream>
using namespace std;
class Person {
public:
int age_;
string name_;
Person(int age, string name) {
age_ = age;
name_ = name; // std::string 重载了=
}
Person() = default; // 显式要求添加默认构造函数
};
int main(){
Person p1(19, "xiaobo");
cout << "age1 = " << p1.age_ << ", name1 = " << p1.name_ << endl;
Person p2; // 下面的语句显然是未定义行为
cout << "age2 = " << p2.age_ << ", name2 = " << p2.name_ << endl;
return 0;
}
再次强调:不要出现未定义行为的代码!!这样会增加维护和 bug 排查的难度!!!
或者自己再写一个也行:
class Person {
public:
int age_;
string name_;
Person(int age, string name) {
age_ = age;
name_ = name; // std::string 重载了=
}
//Person() = default;
Person() { // 自己定义一个无参的构造函数
// 你的逻辑。。。
}
};
定义了有参构造函数之后,我依然给出无参构造函数,但是这个时候你就得思考一下了,这个无参构造函数是否安全,或者是否有意义。
构造函数参数隐式类型转换
是的你没听错,构造函数的参数会发生隐式类型转换。
#include <iostream>
using namespace std;
class MyString {
public:
MyString(const char* s) {
printf("%s\n", s);
}
};
void printString(const MyString& s) {
// 在此处发生隐式类型转换
cout << "调用 printString() 函数" << endl;
}
int main() {
printString("hello");
}
输出:
hello 调用 printString() 函数
先输出了 hello,明显是先执行了 MyString 的有参构造函数,也就是:
MyString s("hello");
结合函数重载的知识,我们又可以构建出这样一个场景:
#include <stdio.h>
class MyString {
public:
MyString(int value) {
printf("执行了 MyString 的有参构造函数\n");
printf("s == %d\n", value);
}
};
void print(const MyString& s) {
printf("执行了 print(const MyString& s) 函数");
}
void print(int x) {
printf("执行了 print(int x) 函数");
}
int main(void) {
print(1); // 这个代码最终会输出什么?
return 0;
}
这个代码到底输出什么?答案是未定义的:
执行了 print(int x) 函数
虽然输出是这么多,但是不带别别的编译器也会如此,别的编译器甚至会直接因为二义性问题报错。
可以看出,这个代码重载了 print,但是一个参数是 int 一个参数是自定义类型,但是自定义类型的这个 print 编译是没问题的,因为 1 传给参数是自定义类型的 print 之后,会发生隐式类型转换,也就是:
print(MyString s(1));
然后就会调用他的构造函数。
也就是说,如果我注释掉以 int 为参数的 print,那么是可以正常输出的:
#include <stdio.h>
class MyString {
public:
MyString(int value) {
printf("执行了 MyString 的有参构造函数\n");
printf("s == %d\n", value);
}
};
void print(const MyString& s) {
printf("执行了 print(const MyString& s) 函数");
}
//void print(int x) { // printf("执行了 print(int x) 函数"); //}
int main(void) {
print(1); // 这个代码最终会输出什么?
return 0;
}
输出如下:
执行了 MyString 的有参构造函数 s == 1 执行了 print(const MyString& s) 函数
可以看出,先执行了构造函数,然后构造完成就去执行 print 的函数体。
如果我要明确杜绝这种隐式类型转换,但是总防不住傻子调用怎么办?没关系 C++ 为你提供了 explicit 关键字,其使用方法如下:
#include <stdio.h>
class MyString {
public:
// 明确这个构造函数是禁止隐式类型转换的
explicit MyString(int value) {
printf("执行了 MyString 的有参构造函数\n");
printf("s == %d\n", value);
}
};
构造函数重载
如果不知道什么是函数重载的可以自己搜一下。
C++ 相比于 C 语言的函数,C++ 多了函数重载的功能,当然构造函数也是函数,它也可以实现重载功能。
在这个类中,Person() 和 Person(int age, string name) 构成了重载。
但是还是需要提一下前面的默认构造函数的问题,代码如下:
#include <iostream>
using namespace std;
class Person {
public:
int age_;
string name_;
Person() {
age_ = 19;
name_ = "xiaobo";
}
Person(int age = 18, string name = "haohao") {
age_ = age;
name_ = name;
}
};
int main(void) {
Person a;
cout << "age == " << a.age_ << ", name = " << a.name_ << endl;
return 0;
}
Person() 和 Person(int age = 18, string name = "haohao") 构成了重载,那么到底会调用谁来初始化对象?
这种情况其实也算函数重载,在是这里明显出现了歧义,也就是二义性,不能明确到底调用哪个构造函数,因此编译器会直接编译失败。
初始化列表
我们上述的初始化对象的过程都是在构造函数内部完成,但构造函数还有一种方法,也就是初始化列表。如下是初始化列表的样例代码:
#include <iostream>
using namespace std;
class Person {
public:
int age_;
string name_;
// 默认构造函数
Person() {
age_ = 19;
name_ = "xiaobo";
}
// 使用初始化列表
Person(int age, string name) : age_(age), name_(name) {
cout << "调用 Person(int age, string name)!" << endl;
cout << "age == " << age_ << ", name = " << name_ << endl;
}
};
int main(void) {
Person a(18, "bobo");
return 0;
}
可以看到有参的构造函数之后,多了一些东西,如下:
Person(int age, string name) :age_(age), name_(name) { ... ... }
这就是初始化列表的基本语法结构,这里记一记就好了,无非就是在构造函数的")"后面加一个冒号,然后用成员变量 (参数) 的方式来初始化成员。
为此我们抽象出来初始化列表语法的一般形式:
ClassName(type param1, type param2, ... ) :member1(param1), member2(param2), ... {
// 构造函数体
}
初始化列表和构造函数体内赋值的关键区别在于,初始化列表式对象构造完成时直接完成初始化,非常高效,函数体内赋值是先调用默认构造函数,然后再初始化成员变量。
编写代码:使用初始化列表的案例就拿上面那个代码案例,然后重新写一个不使用初始化列表的 cpp 文件如下。
#include <iostream>
using namespace std;
class Person {
public:
int age_;
string name_;
// 默认构造函数
//Person() { // age_ = 19; // name_ = "xiaobo"; //}
// 使用初始化列表
Person(int age, string name)
//: age_(age),
//name_(name)
{
age_ = age;
name_ = name;
cout << "调用 Person(int age, string name)!" << endl;
cout << "age == " << age_ << ", name = " << name_ << endl;
}
};
int main(void) {
Person a(18, "bobo");
return 0;
}
通过生成汇编代码发现,在 main 附近的调用 Person 有参构造函数这一块的汇编代码其实都是一样的,也就是他们在执行到 Person a(...) 这句代码的时候,其实执行的逻辑都是一样的,不一样的地方是从进入到 Person 的有参构造函数开始的。
下面是它两生成的在 main 函数附近的汇编代码:
int main(void) {
00007FF7041C4920 push rbp
00007FF7041C4922 push rdi
00007FF7041C4923 sub rsp,1C8h
00007FF7041C492A lea rbp,[rsp+20h]
00007FF7041C492F lea rdi,[rsp+20h]
00007FF7041C4934 mov ecx,3Ah
00007FF7041C4939 mov eax,0CCCCCCCCh
00007FF7041C493E rep stos dword ptr [rdi]
00007FF7041C4940 mov rax,qword ptr [__security_cookie (07FF7041D4040h)]
00007FF7041C4947 xor rax,rbp
00007FF7041C494A mov qword ptr [rbp+190h],rax
00007FF7041C4951 lea rcx,[__152856F7_类\main@cpp (07FF7041DA07Bh)]
00007FF7041C4958 call __CheckForDebuggerJustMyCode (07FF7041C1325h)
00007FF7041C495D nop
Person a(18, "bobo");
00007FF7041C495E lea rax,[rbp+118h]
00007FF7041C4965 mov qword ptr [rbp+158h],rax
00007FF7041C496C lea rdx,[string "bobo" (07FF7041D048Ch)]
00007FF7041C4973 mov rcx,qword ptr [rbp+158h]
00007FF7041C497A call std::basic_string<char,std::char_traits<char>,std::allocator<char> >::basic_string<char,std::char_traits<char>,std::allocator<char> > (07FF7041C1082h)
00007FF7041C497F mov qword ptr [rbp+188h],rax
00007FF7041C4986 mov r8,qword ptr [rbp+188h]
00007FF7041C498D mov edx,12h
00007FF7041C4992 lea rcx,[a]
00007FF7041C4996 call Person::Person (07FF7041C160Eh)
00007FF7041C499B nop
return 0;
00007FF7041C499C mov dword ptr [rbp+174h],0
00007FF7041C49A6 lea rcx,[a]
00007FF7041C49AA call Person::~Person (07FF7041C13D9h)
00007FF7041C49AF mov eax,dword ptr [rbp+174h]
}
00007FF7041C49B5 mov edi,eax
00007FF7041C49B7 lea rcx,[rbp-20h]
00007FF7041C49BB lea rdx,[__xt_z+3E0h (07FF7041CFE80h)]
00007FF7041C49C2 call _RTC_CheckStackVars (07FF7041C136Bh)
00007FF7041C49C7 mov eax,edi
00007FF7041C49C9 mov rcx,qword ptr [rbp+190h]
00007FF7041C49D0 xor rcx,rbp
00007FF7041C49D3 call __security_check_cookie (07FF7041C137Fh)
00007FF7041C49D8 lea rsp,[rbp+1A8h]
00007FF7041C49DF pop rdi
00007FF7041C49E0 pop rbp
00007FF7041C49E1 ret
然后我们来看看进入到构造函数之后的汇编的区别。
下面是没有使用初始化列表的汇编:
Person(int age, string name)
00007FF7041C53B0 mov qword ptr [rsp+18h],r8
00007FF7041C53B5 mov dword ptr [rsp+10h],edx
00007FF7041C53B9 mov qword ptr [rsp+8],rcx
00007FF7041C53BE push rbp
00007FF7041C53BF push rdi
00007FF7041C53C0 sub rsp,108h
00007FF7041C53C7 lea rbp,[rsp+20h]
00007FF7041C53CC lea rcx,[__152856F7_类\main@cpp (07FF7041DA07Bh)]
00007FF7041C53D3 call __CheckForDebuggerJustMyCode (07FF7041C1325h)
00007FF7041C53D8 nop
//: age_(age),
//name_(name)
{
00007FF7041C53D9 mov rax,qword ptr [this]
00007FF7041C53E0 add rax,8
00007FF7041C53E4 mov rcx,rax
00007FF7041C53E7 call std::basic_string<char,std::char_traits<char>,std::allocator<char> >::basic_string<char,std::char_traits<char>,std::allocator<char> > (07FF7041C1708h)
00007FF7041C53EC nop
age_ = age;
00007FF7041C53ED mov rax,qword ptr [this]
00007FF7041C53F4 mov ecx,dword ptr [age]
00007FF7041C53FA mov dword ptr [rax],ecx
name_ = name;
00007FF7041C53FC mov rax,qword ptr [this]
00007FF7041C5403 add rax,8
00007FF7041C5407 mov rdx,qword ptr [name]
00007FF7041C540E mov rcx,rax
00007FF7041C5411 call std::basic_string<char,std::char_traits<char>,std::allocator<char> >::operator= (07FF7041C16E0h)
00007FF7041C5416 nop
cout << "调用 Person(int age, string name)!" << endl;
00007FF7041C5417 lea rdx,[string "\xb5\xf7\xd3\xc3Person(int age, string name@"... (07FF7041D0080h)]
00007FF7041C541E mov rcx,qword ptr [__imp_std::cout (07FF7041D81A0h)]
00007FF7041C5425 call std::operator<<<std::char_traits<char> > (07FF7041C127Bh)
00007FF7041C542A mov qword ptr [rbp+0C0h],rax
00007FF7041C5431 lea rdx,[std::endl<char,std::char_traits<char> > (07FF7041C104Bh)]
00007FF7041C5438 mov rcx,qword ptr [rbp+0C0h]
00007FF7041C543F call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF7041D8120h)]
00007FF7041C5445 nop
cout << "age == " << age_ << ", name = " << name_ << endl;
00007FF7041C5446 lea rdx,[string "age == " (07FF7041D04B8h)]
00007FF7041C544D mov rcx,qword ptr [__imp_std::cout (07FF7041D81A0h)]
00007FF7041C5454 call std::operator<<<std::char_traits<char> > (07FF7041C127Bh)
00007FF7041C5459 mov qword ptr [rbp+0C0h],rax
00007FF7041C5460 mov rax,qword ptr [this]
00007FF7041C5467 mov eax,dword ptr [rax]
00007FF7041C5469 mov dword ptr [rbp+0C8h],eax
00007FF7041C546F mov edx,dword ptr [rbp+0C8h]
00007FF7041C5475 mov rcx,qword ptr [rbp+0C0h]
00007FF7041C547C call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF7041D8118h)]
00007FF7041C5482 lea rdx,[string ", name = " (07FF7041D04A8h)]
00007FF7041C5489 mov rcx,rax
00007FF7041C548C call std::operator<<<std::char_traits<char> > (07FF7041C127Bh)
00007FF7041C5491 mov rcx,qword ptr [this]
00007FF7041C5498 add rcx,8
00007FF7041C549C mov rdx,rcx
00007FF7041C549F mov rcx,rax
00007FF7041C54A2 call std::operator<<<char,std::char_traits<char>,std::allocator<char> > (07FF7041C1578h)
00007FF7041C54A7 mov qword ptr [rbp+0D0h],rax
00007FF7041C54AE lea rdx,[std::endl<char,std::char_traits<char> > (07FF7041C104Bh)]
00007FF7041C54B5 mov rcx,qword ptr [rbp+0D0h]
00007FF7041C54BC call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF7041D8120h)]
00007FF7041C54C2 nop
}
00007FF7041C54C3 mov rcx,qword ptr [name]
00007FF7041C54CA call std::basic_string<char,std::char_traits<char>,std::allocator<char> >::~basic_string<char,std::char_traits<char>,std::allocator<char> > (07FF7041C1591h)
00007FF7041C54CF nop
00007FF7041C54D0 mov rax,qword ptr [this]
00007FF7041C54D7 lea rsp,[rbp+0E8h]
00007FF7041C54DE pop rdi
00007FF7041C54DF pop rbp
00007FF7041C54E0 ret
下面是使用初始化列表的汇编:
00007FF759C13290 mov qword ptr [rsp+18h],r8
00007FF759C13295 mov dword ptr [rsp+10h],edx
00007FF759C13299 mov qword ptr [rsp+8],rcx
00007FF759C1329E push rbp
00007FF759C1329F push rdi
00007FF759C132A0 sub rsp,108h
00007FF759C132A7 lea rbp,[rsp+20h]
00007FF759C132AC lea rcx,[__152856F7_类\main@cpp (07FF759C2A07Bh)]
00007FF759C132B3 call __CheckForDebuggerJustMyCode (07FF759C11325h)
00007FF759C132B8 nop
#include <iostream>
using namespace std;
class Person {
public:
int age_;
string name_;
// 默认构造函数
//Person() { // age_ = 19; // name_ = "xiaobo"; //}
// 使用初始化列表
Person(int age, string name) : age_(age),
00007FF759C132B9 mov rax,qword ptr [this]
00007FF759C132C0 mov ecx,dword ptr [age]
00007FF759C132C6 mov dword ptr [rax],ecx
name_(name)
00007FF759C132C8 mov rax,qword ptr [this]
00007FF759C132CF add rax,8
00007FF759C132D3 mov qword ptr [rbp+0C0h],rax
00007FF759C132DA mov rdx,qword ptr [name]
00007FF759C132E1 mov rcx,qword ptr [rbp+0C0h]
00007FF759C132E8 call std::basic_string<char,std::char_traits<char>,std::allocator<char> >::basic_string<char,std::char_traits<char>,std::allocator<char> > (07FF759C114C4h)
00007FF759C132ED nop
cout << "调用 Person(int age, string name)!" << endl;
00007FF759C132EE lea rdx,[string "\xb5\xf7\xd3\xc3Person(int age, string name@"... (07FF759C20080h)]
00007FF759C132F5 mov rcx,qword ptr [__imp_std::cout (07FF759C281A0h)]
00007FF759C132FC call std::operator<<<std::char_traits<char> > (07FF759C1127Bh)
00007FF759C13301 mov qword ptr [rbp+0C0h],rax
00007FF759C13308 lea rdx,[std::endl<char,std::char_traits<char> > (07FF759C1104Bh)]
00007FF759C1330F mov rcx,qword ptr [rbp+0C0h]
00007FF759C13316 call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF759C28120h)]
00007FF759C1331C nop
cout << "age == " << age_ << ", name = " << name_ << endl;
00007FF759C1331D lea rdx,[string "age == " (07FF759C204B8h)]
00007FF759C13324 mov rcx,qword ptr [__imp_std::cout (07FF759C281A0h)]
00007FF759C1332B call std::operator<<<std::char_traits<char> > (07FF759C1127Bh)
00007FF759C13330 mov qword ptr [rbp+0C0h],rax
00007FF759C13337 mov rax,qword ptr [this]
00007FF759C1333E mov eax,dword ptr [rax]
00007FF759C13340 mov dword ptr [rbp+0C8h],eax
00007FF759C13346 mov edx,dword ptr [rbp+0C8h]
00007FF759C1334C mov rcx,qword ptr [rbp+0C0h]
00007FF759C13353 call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF759C28118h)]
00007FF759C13359 lea rdx,[string ", name = " (07FF759C204A8h)]
00007FF759C13360 mov rcx,rax
00007FF759C13363 call std::operator<<<std::char_traits<char> > (07FF759C1127Bh)
00007FF759C13368 mov rcx,qword ptr [this]
00007FF759C1336F add rcx,8
00007FF759C13373 mov rdx,rcx
00007FF759C13376 mov rcx,rax
00007FF759C13379 call std::operator<<<char,std::char_traits<char>,std::allocator<char> > (07FF759C11578h)
00007FF759C1337E mov qword ptr [rbp+0D0h],rax
00007FF759C13385 lea rdx,[std::endl<char,std::char_traits<char> > (07FF759C1104Bh)]
00007FF759C1338C mov rcx,qword ptr [rbp+0D0h]
00007FF759C13393 call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF759C28120h)]
00007FF759C13399 nop
}
00007FF759C1339A mov rcx,qword ptr [name]
00007FF759C133A1 call std::basic_string<char,std::char_traits<char>,std::allocator<char> >::~basic_string<char,std::char_traits<char>,std::allocator<char> > (07FF759C11591h)
00007FF759C133A6 nop
00007FF759C133A7 mov rax,qword ptr [this]
00007FF759C133AE lea rsp,[rbp+0E8h]
00007FF759C133B5 pop rdi
00007FF759C133B6 pop rbp
00007FF759C133B7 ret
可以看到内容非常多啊,我们挑着看。
使用非初始化列表:
00007FF7041C53D9 mov rax,qword ptr [this] ; 获取 this 指针
00007FF7041C53E0 add rax,8 ; 计算 name_地址
00007FF7041C53E4 mov rcx,rax ; 准备调用构造函数
00007FF7041C53E7 call std::basic_string<...>::basic_string<...> (07FF7041C1708h) ; **默认构造**(创建空 string)
00007FF7041C53ED mov rax,qword ptr [this] ; 获取 this 指针
00007FF7041C53F4 mov ecx,dword ptr [age] ; 获取 age 参数
00007FF7041C53FA mov dword ptr [rax],ecx
00007FF7041C53FC mov rax,qword ptr [this] ; 获取 this 指针
00007FF7041C5403 add rax,8 ; 计算 name_地址
00007FF7041C5407 mov rdx,qword ptr [name] ; 获取 name 参数
00007FF7041C540E mov rcx,rax ; 传入 name_地址
00007FF7041C5411 call std::basic_string<...>::operator= (07FF7041C16E0h) ; **调用赋值运算符**
进入到构造函数之后,先去拿调用者传来的参数,分别是:
- this 指针
- age
- name
在 64 位系统中,this 指针常常通过 rcx 寄存器传递,这是 C++ 编译器的标准。
mov rax, qword ptr
对于这第一个代码
这句话的大概的意思就是将栈传来的内存地址(this 指针存储在 rcx,从 rcx 中拿,是总所周知的)存入 rax 寄存器。
add rax, 8
这句话就简单了,add 是加和的意思,也就是将 rax 中的值 +8,rax 里面的值是前面传来的 this 引用表示的内存地址。为什么是 +8 不是 +4,是因为内存对齐的原因,我当前为 64 位系统,age_占 4 字节,后四个字节不能用,如果 string 用了这四个字节,那么要完整读出 string 就可能要多 io 一次,导致性能下降:
class MyClass {
int a; // 4 字节(偏移 0)
// 这里可能有 4 字节 填充(为了对齐到 8 字节)
std::string b; // 假设 24 字节(从偏移 8 开始)
};
mov rcx, rax
然后将 rax 存入 rcx。为什么用 rcx?因为 C++ 标准规定用 rcx 来传递 this 指针,也就意味着,rcx 中现在存储的是 main 函数中传来的栈空间上的 Person 的对象的内存地址 +8,也就是这个对象的 string name 这个成员变量的地址。
我们说过 rcx 是传递 this 指针的,因此可以猜测后面肯定会接着一个 call 指令,果不其然:
00007FF7041C53E7 call std::basic_string<...>::basic_string<...> (07FF7041C1708h) ;
这句指令调用了 string 的默认构造函数来构造一个空字符串。
关键:这行指令是多余操作!如果用初始化列表,这行指令根本不会出现。记住这句话
随后就开始初始化 age,call 返回之后,又将 main 传过来的栈对象的地址传入 rax:
00007FF7041C53ED mov rax,qword ptr
这里为什么要又给 rax 赋值一次 main 传过来的栈对象的地址?因为前面给 name_构建空字符串的时候,污染了 rax(目前是记录的 this+8),一次需要重置 rax 为 this 指针。
00007FF7041C53F4 mov ecx,dword ptr
随后将 age 的地址值传入 ecx。这里为什么不是计算机组成原理里面所说的 ebp+8 或者 ebp+12 来传递参数?这是因为这里做了简化逻辑,直接使用
也就是你可以默认编译器知道
00007FF7041C53FA mov dword ptr
也就是将 ecx(age 参数)中的内容给移动到 rax 的值指向的 2 个字(四字节)的内存地址空间(this 指针指向的地方,this 指针往后四个字节就是 age),从而 完成 age_的初始化。
经过上述几步之后,age_的值在内存里面已经被赋值,然后 name_被赋值为空字符串表示。
随后就是赋值 name_的操作,重复的代码我就不解读了,请读者自行思考:
00007FF7041C53FC mov rax,qword ptr
接下来就是赋值 name_:
00007FF7041C5407 mov rdx,qword ptr
这一步仅仅只是记录参数 name 的地址。随后将 rax 中存储的 this+8 传给 rcx:
00007FF7041C540E mov rcx,rax ; 将 name_地址放入 rcx(作为 this 参数)
随后调用 call,去执行赋值运算符来操作 name_:
00007FF7041C5411 call std::basic_string<...>::operator= (07FF7041C16E0h) ; 调用赋值运算符
string 重载了 operation=的运算符(operator=(const string&)),会将 name 这个字符串参数的值赋值给 name_。
我们总结上述过程其实就可以发现,name_这个成员其实是被构建了 2 次的。
- 第一次是初始化了一个 string 的空串,说是空串,但是其实 string 内部为了维护这个字符串,还设置了其他的许多变量,可能是容量和 size 这些,所以空串并不代表空对象,它依旧占用了一些内存空间。
- 第二次是调用 string 的赋值重载函数,将空串替换为参数指定的字符串,这个过程如果原来的空间不够,就会产生扩容逻辑,就会造成额外的性能消耗,但是如果你直接构建,或许就不会出现这个扩容的逻辑,string 会直接为你生成一个足够大的合适的内存空间。
上述就是使用非初始化列表的过程和问题。我们再来看看初始化列表的好处,依旧是从指令出发,但是我们不一一讲解,而是从指令的不同之处出发,其他的汇编大家可以自行阅读,来提高自己能力。
下面是使用初始化列表的构造函数的汇编:
00007FF759C132B9 mov rax,qword ptr [this] ; 获取 this 指针
00007FF759C132C0 mov ecx,dword ptr [age] ; 获取 age 参数
00007FF759C132C6 mov dword ptr [rax],ecx ; 直接初始化 age_ = age
00007FF759C132C8 mov rax,qword ptr [this] ; 获取 this 指针
00007FF759C132CF add rax,8 ; 计算 name_的地址
00007FF759C132D3 mov qword ptr [rbp+0C0h],rax ; 保存 name_地址
00007FF759C132DA mov rdx,qword ptr [name] ; 获取 name 参数
00007FF759C132E1 mov rcx,qword ptr [rbp+0C0h] ; 传入 name_地址
00007FF759C132E8 call std::basic_string<...>::basic_string<...> (07FF759C114C4h) ; 直接构造 name_
大多数的指令我们都知道是什么意思,类似的我都将结果,我们直接口述这个过程:
- 开始依旧是从 rcx 获取 this 指针的地址,然后存储到 rax
- 然后获取 age 参数的值给 ecx
- 然后将 ecx 中的值,也就是 age 参数的值传递给 rax 中,rax 此时还是 this 指针,this 指针后四个字节刚好归 age_所有,这一步设置好了 age_的值
- 随后继续重置 this 指针到 rax(这一步或许有点多余)
- 然后计算出 name_的地址,也就是 this+ 8,存入 rax
- 随后将 rax 中的 name_地址保存在栈地址
- 然后将参数 name 的地址存入 rdx
- 然后将 name_的地址传入 rcx
- 调用构造函数,读取 rcx 来识别 string 对象,然后读取 rdx 来识别要构建的 string 的内容。后续就会走 string 的构造函数的逻辑,此时创建的大小就可以根据字符串的长度来快速设置和申请。
这里有个疑难点,也就是我已经把 name_的地址存入了 rax,为什么还要保存在 rbp + 0c0h 中?
问题在于:
在 x64 调用约定中:
- rcx:第 1 个参数
- rdx:第 2 个参数
- r8:第 3 个参数
- r9:第 4 个参数
- rax:通常用于返回值,且被调用者可以随意修改!
所以综合对比下来,初始化列表可能有着不小的优势:
| 项目 | 初始化列表 | 构造函数体内赋值 |
|---|---|---|
| C++ 语义 | name_(name) | name_ = name; |
| 编译器行为 | 直接调用构造函数 | 先默认构造 + 赋值运算符 |
| 汇编指令 | call basic_string(char*) | call basic_string() + call operator= |
| 内存操作 | 1 次分配(直接构造) | 2 次分配(默认构造 + 赋值) |
| 性能 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
但是我们的代码最多表示 C++ 的 std::string 会根据是否在初始化列表中选择是否是先默认构造再赋值啊。别急别的类也大差不差,我们通过一个表来查看:
| 成员类型 | 初始化列表 | 构造函数体内赋值 | 优势 | 汇编关键指令 |
|---|---|---|---|---|
| string | name_(name) | name_ = name; | 避免默认构造 + 赋值 | call basic_string(char*) |
| 自定义类 | obj_(x) | obj_ = MyClass(x); | 避免默认构造 + 赋值 | call MyClass(int) |
别的类也是如此,会直接构造并完成初始化。而不是先初始化再赋值。
普通类(非 string)在体内赋值与初始化列表的性能差异与 string 类似,甚至可能更差,例如这个普通类中存在着大量需要动态管理的成员变量,其初始化逻辑与 string 类似。
但也不是所有的类都这样,据情况而定,但是养成初始化列表的方式进行初始化并不是一件坏事~
拷贝构造函数
拷贝构造函数也是构造函数的一种,也可以称为是一种特殊的构造函数或者构造函数的重载。它的核心特点就是参数列表的第一个参数必须为它自身类型的引用。如下是拷贝构造函数的代码案例:
#include <iostream>
using namespace std;
class Person {
public:
int age_;
string name_;
Person(int age, string name) : age_(age), name_(name) {
cout << "调用 Person(int age, string name)!" << endl;
cout << "age == " << age_ << ", name = " << name_ << endl;
}
// 拷贝构造函数
Person(const Person& other): age_(other.age_), name_(other.name_) {
cout << "调用 Person(Person& other)!" << endl;
cout << "age == " << age_ << ", name = " << name_ << endl;
}
};
int main(void) {
Person a(18, "bobo");
Person b = a; // 执行拷贝构造
Person c(a); // 执行拷贝构造
return 0;
}
可以看出拷贝构造函数的基本用法就是:
className a;
className b = a;
或者 className c(a);
第一个函数为自身的引用类型,并且要加上 const,因为毕竟是拷贝,你仅仅只需要拷贝数据,而不能通过引用修改形参对应对象的值。
这里为何一定要加引用?难道不能传递非引用?比如它自身?答案是不行的,因为调用拷贝构造的时候,给上述代码中的 other 传参也是使用的拷贝构造函数,于是就会产生无穷递归:第一次调用拷贝构造函数构造 b,进入到 b 的拷贝构造函数之后给 other 进行拷贝构造,然后 other 调用拷贝构造函数又需要进行拷贝构造 other 的入参的拷贝构造,就如此形成死循环。
因此这里必须是引用,b 调用拷贝构造去拷贝 a,将 a 传递给引用类型,其本质就是传递指针,而非再次拷贝构造,因此不会无穷递归。
此外拷贝构造函数还可以拥有多个参数,但是第一个参数还只能是自身类型的常量引用。后面的参数也要有缺省值。
例如你在拷贝构造函数的时候,可以指定一些配置:
#include <iostream>
using namespace std;
class Person {
public:
int age_;
string name_;
Person(int age, string name) : age_(age), name_(name) {
cout << "========================= 有参构造 start" << endl;
cout << "调用 Person(int age, string name)!" << endl;
cout << "age == " << age_ << ", name = " << name_ << endl;
cout << "========================= 有参构造 end\n" << endl;
}
// 带有额外参数的拷贝构造函数
// deepcopy 字段指定是否进行深拷贝
Person(const Person& other, bool deepcopy = false): age_(other.age_), name_(other.name_) {
cout << "========================= 拷贝构造 start" << endl;
if (deepcopy) {
cout << "执行深拷贝逻辑!!" << endl;
} else {
cout << "执行浅拷贝逻辑!!" << endl;
}
cout << "调用 Person(Person& other)!" << endl;
cout << "age == " << age_ << ", name = " << name_ << endl;
cout << "========================= 拷贝构造 end\n" << endl;
}
};
int main(void) {
Person a(18, "bobo");
Person b = a; // 执行浅拷贝构造
Person c(a, true); // 执行深拷贝
return 0;
}
输出:
========================= 有参构造 start
调用 Person(int age, string name)!
age == 18, name = bobo
========================= 有参构造 end
========================= 拷贝构造 start
执行浅拷贝逻辑!!
调用 Person(Person& other)!
age == 18, name = bobo
========================= 拷贝构造 end
========================= 拷贝构造 start
执行深拷贝逻辑!!
调用 Person(Person& other)!
age == 18, name = bobo
========================= 拷贝构造 end
也就是说只要满足:
- 第一个参数是自身类型的引用
- 其他参数都有默认值
那么这个构造函数就是拷贝构造函数,编译器会在需要拷贝时调用它。
那你有没有想过,C++ 为什么要求额外的参数必须缺省?其实他的理由和默认构造函数类似,因为编译器不是一个顶级的天才,它也只是人写的代码,不可能非常智能。也就是说,其根本原因在于:C++ 的许多拷贝操作是'隐式'发生的,编译器只知道源对象,不知道你想要什么额外配置,自然就不知道这些额外的参数该填什么,但是又必须保证程序在编译器不知道额外参数的情况下正常运行,所以就要求缺省。
此外,C++ 规定对自定义类型的拷贝都必须拷贝构造,所以自定义类型的参数和返回值在使用时都会调用其自身定义的拷贝构造函数。
拷贝拷贝,如果遇到指针类型,那么拷贝的结果就成了把一个对象的某个成员的指针拷贝给了另外一个对象的对应的指针,那么这个时候,这两个对象在结束生命周期并调用析构函数的时候就会手动 free 两次,造成 double free 的情况,这个时候就需要深拷贝。
深拷贝也就是在拷贝构造函数里面处理那些动态申请的内存空间的时候的指针的时候,不要直接传递指针地址,而是重新申请一次内存地址空间,然后将要拷贝的对象的这个指针的地址对应的内容再次拷贝给自己的对应指针的地址空间。
如下是一个深拷贝的例子:
#include <iostream>
using namespace std;
class Person {
public:
int age_;
string name_;
int* weight_; // 构造函数
Person(int age, string name, int* weight) : age_(age), name_(name), weight_(weight) {
cout << "调用 Person(Person& other)!" << endl;
cout << "age == " << age_ << ", name = " << name_ << ", weight = " << *weight_ << endl;
}
// 拷贝构造函数
Person(const Person& other) : age_(other.age_), name_(other.name_), weight_((int*)malloc(sizeof(int))) // 动态申请的内存地址赋值给 weight_
{
*weight_ = *other.weight_;
cout << "调用 Person(Person& other) 进行深拷贝!!" << endl;
cout << "age == " << age_ << ", name = " << name_ << ", weight = " << *weight_ << endl;
}
};
int main(void) {
int* weight = (int*)malloc(sizeof(int));
*weight = 100;
Person a(18, "bobo", weight);
Person b = a; // 执行深拷贝构造
return 0;
}
如果不显示提供,那么编译器会提供一个默认的拷贝构造函数,这个拷贝构造函数会执行浅拷贝,也就是说对于内置类型或者内置类型的指针类型会完成值拷贝和浅拷贝,对于自定义类型,会调用其自身的拷贝构造函数。
但如果你手写了拷贝构造并且没有任何类容,那么:编译器不再帮你初始化任何成员!
class A {
int x;
std::string s;
double* p;
public:
A(const A& other) {
// 如果你什么都不写...
// 编译器不会帮你去拷贝任何数据
}
};
编译器不会帮你去拷贝任何数据

