前些天我发布了一篇文章《如何理解一门编程语言》,文中我以C++中的const为例,给大家分享了如何通过修饰词的修饰对象来理解编程语言中晦涩难记的语法。
如果把 const 作为一个形容词来理解,那么今天的文章,我们便尝试去理解一个名词,即一个概念本身。当然,这可能有些抽象和复杂,不过我会尽量用一些详实的例子来解释这个概念。
为了更好理解,我建议您先阅读这篇文章:二胖:如何理解一门编程语言?
今天我们以C++中的复制构造函数为例,为什么选择复制构造函数呢?因为复制构造函数难以理解,并且容易与赋值运算符“=”混淆。不知道什么是复制构造函数不要紧,下面我会简单地介绍。
为了照顾部分没学过面向对象编程的同学,我们先解释一下什么是构造函数。所谓构造函数,正如它的名字,首先是一个函数,作用是构造(一个对象),在新生成对象的时候做一些初始化的工作。
举个例子,当我们编写一个学生类,这个类有两个成员变量,一个是学生姓名name,一个是学生编号number。
#include<iostream>
#include <string>
using namespace std;
class Student {
private:
string name;
string number;
public:
Student(string name,string number);//构造函数声明
string getName() { return this->name; }
string getNum() { return this->number; }
};
//构造函数实现
Student::Student(string name,string number) {
this->name = name;
this->number = number;
cout << "构造函数被调用了" << endl;
}
int main() {
Student student1("二胖","001");
cout << "我的名字是:" << student1.getName() <<", 我的编号是:"<< student1.getNum()<<endl;//读取成员对象的
}
其中:
Student(string name,string number);
就是构造函数。在生成一个对象的时候,它对成员变量进行一系列的初始化工作。
如在main函数中,我们生成了一个对象:
Student student1("二胖","001");
在对象生成的时候就会自动调用其构造函数,为了证明其在对象生成的时候被调用了,我在构造函数中添加了一条打印语句:“构造函数被调用了”。
构造函数的作用是给成员变量name,number赋值。运行main函数的结果是:
与设想一致。
归纳一下就是:
构造函数只有在对象生成时才会被自动调用,作用是做一些初始化的工作。
这里有一个重点——构造函数何时被调用。注意,构造函数一定是在有对象生成时才会被调用,也就是说一个对象在其生命周期内只会被调用一次。
和之前的理解方式一样,我们先把这个名词分开。所谓复制构造函数:
- 首先它是一个函数
- 作用是构造(一个对象)
- 这个构造函数是复制另外一个已有的对象来初始化新生成的对象
我们可以用克隆来理解复制构造函数,就是在原有对象的基础上复制出一个一模一样(暂且这样理解),同时可以定制化的复制对象。
第一个对象student1是我们手动指定name和number来生成的,而第二个对象student2不必这么做,可以把student1作为形参来初始化student2即可。
可以这样调用:
Student student2(student1);
有新对象生成,构造函数就一定会被调用。一个类可以有多个构造函数,如普通构造函数、复制构造函数、类型转换构造函数等,但是一个新对象的生成只会调用其中一个构造函数。用一个对象去初始化一个新的对象,调用的就是复制构造函数。
我们把之前的代码改写一下,如下所示:
#include<iostream>
#include <string>
using namespace std;
class Student {
private:
string name;
string number;
public:
Student(string name,string number);//构造函数
Student(Student & student);//复制构造函数
string getName() { return this->name; }
string getNum() { return this->number; }
};
//构造函数实现
Student::Student(string name,string number) {
this->name = name;
this->number = number;
cout << "构造函数被调用了" << endl;
}
//复制构造函数实现
Student::Student(Student & student) {
this->name = student.name;
this->number = student.number;
cout << "复制构造函数被调用了" << endl;
}
int main() {
Student student1("二胖","001");
Student student2(student1);
cout << "student1,我的名字是:" << student1.getName() <<", 我的编号是:"<< student1.getNum()<<endl;//读取成员对象的
cout << "student2,我的名字是:" << student2.getName() <<", 我的编号是:"<< student2.getNum()<<endl;//读取成员对象的
}
其中复制构造函数是:
Student(Student & student);
为了证明复制构造函数被调用了,我同样在构造函数内手动添加了一条打印语句:“复制构造函数被调用了”。
运行main函数的结果是:
和设想一样,第一个对象调用的是构造函数,第二个对象调用的是复制构造函数,它们两个的成员变量都是一样的。
复制构造函数除了能被显式调用,还能用赋值号调用,如下所示:
Student student2(student1);//显示调用
Student student3 = student1;//使用赋值号即"="调用。
为了大家更清楚地理解,我们把main函数改写一下:
int main() {
Student student1("二胖","001");
Student student2(student1);
Student student3 = student1;
}
结果如下:
和设想一样,student1的生成调用了构造函数,student2和student3都调用了复制构造函数。
很多人觉得复制构造函数难,因为他们很容易把赋值和复制构造函数弄混。不信?我们再来改改main函数。
int main() {
Student student1("二胖","001");
Student student2(student1);
Student student3 = student1;//这里是复制构造函数
Student student4("三胖","002");
student2 = student4;//这里是赋值,不调用构造函数
}
易混点就在于符号“=”,赋值和复制构造函数的调用都会因为“=”的出现而发生,可是两者的本质是不同的。仅仅赋值并不会调用构造函数,因为其没有新对象的生成,student2是调用构造生成的,一个对象在其生命周期内只会调用一次构造函数,那么当执行到student2 = student4的时候仅仅是赋值,并没有新对象生成,所以不会调用构造函数。
运行的结果如下:
总结一句话就是:构造函数一定是在新对象生成时才被调用。这样就一目了然啦!
这才是本文的重点,一般书本讲到复制构造函数的时候,都会归纳复制构造函数何时被调用,多为以下三点:
(1)用一个对象去初始化同类的另一个对象时
Student student2(student1);
Student student2 = student1;
(2)如果某函数有一个参数是类A的对象,那么该函数被调用时,类A的复制构造函数将被调用
voidFunc(Student student1){ }
int main(){
Student student2;
Func(student2);
return 0;
}
(3)如果函数的返回值是类A的对象,则函数返回时类A的复制构造函数被调用
Student Func() {
Student student("func_test',"num");
return student;
}
int main() {
cout<<Func().getName()<<endl;
return 0;
}
看完以上三点,很多同学会死记硬背,却根本不知道为啥在这三种情况下复制构造函数会被调用。第一种情况我在前文已经举例说明了,这里就不多说了。
第二种情况是因为形参的传值使得新变量生成,即当调用函数
Func(student2);
时其实相当于执行了如下语句:
Student student1 = student2
这样就转化为了第一种情况,有新对象student1生成。
第三种情况是因为函数返回值,使得临时对象的生成调用了复制构造函数,因为我们return返回的是一个内部的对象,而内部对象的生命周期是Func()调用的过程,当Func运行完毕后其内部的对象就消亡了,所以外部需要有新对象生成,内部对象才能被延续。return student这个语句被执行的时候,C++内部是这样处理的。
Student 临时对象 = Func()内部对象
//Func().getName()相当于调用了临时对象.getName()
这样就又转化成第一种情况了。
(ps:这里是传值,如果是传引用和指针,情况就大不相同了,这个今天就不多讲了)
其实说来说去,我们根本没必要去记忆到底在什么情况下复制构造函数会被调用,我们只需要记住复制构造函数被调用一定要满足的两个条件:
(1)它是一个构造函数,一定要有对象生成的时候才会被调用;
(2)一定要有对象间的复制才会被调用。
回过头来看,上面所举例的三种情况都逃不开这两点,这就是理解了原理的好处。思来想去,透过现象看本质才是最好的学习方法,希望大家细心观察每一个知识点,这样你就一辈子都忘不了了。
大家也可以看看我下面的这篇文章,别忘了点个赞哟!
二胖:分享一段代码-微信好友分析zhuanlan.zhihu.com