2025年4月10日 星期四 乙巳(蛇)年 正月十一 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > VC/VC++

C++拷贝构造函数的调用时机

时间:01-05来源:作者:点击数:460

当以拷贝的方式初始化对象时会调用拷贝构造函数。这里有两个关键点,分别是「以拷贝的方式」和「初始化对象」。

初始化对象

初始化对象是指,为对象分配内存后第一次向内存中填充数据,这个过程会调用构造函数。对象被创建后必须立即被初始化,换句话说,只要创建对象,就会调用构造函数。

初始化和赋值的区别

初始化和赋值都是将数据写入内存中,并且从表面上看起来,初始化在很多时候都是以赋值的方式来实现的,所以很容易混淆。请看下面的例子:

  • int a = 100; //以赋值的方式初始化
  • a = 200; //赋值
  • a = 300; //赋值
  • int b; //默认初始化
  • b = 29; //赋值
  • b = 39; //赋值

在定义的同时进行赋值叫做初始化(Initialization),定义完成以后再赋值(不管在定义的时候有没有赋值)就叫做赋值(Assignment)。初始化只能有一次,赋值可以有多次。

对于基本类型的数据,我们很少会区分「初始化」和「赋值」这两个概念,即使将它们混淆,也不会出现什么错误。但是对于类,它们的区别就非常重要了,因为初始化时会调用构造函数(以拷贝的方式初始化时会调用拷贝构造函数),而赋值时会调用重载过的赋值运算符。请看下面的例子:

  • #include <iostream>
  • #include <string>
  • using namespace std;
  • class Student{
  • public:
  • Student(string name = "", int age = 0, float score = 0.0f); //普通构造函数
  • Student(const Student &stu); //拷贝构造函数
  • public:
  • Student & operator=(const Student &stu); //重载=运算符
  • void display();
  • private:
  • string m_name;
  • int m_age;
  • float m_score;
  • };
  • Student::Student(string name, int age, float score): m_name(name), m_age(age), m_score(score){ }
  • //拷贝构造函数
  • Student::Student(const Student &stu){
  • this->m_name = stu.m_name;
  • this->m_age = stu.m_age;
  • this->m_score = stu.m_score;
  • cout<<"Copy constructor was called."<<endl;
  • }
  • //重载=运算符
  • Student & Student::operator=(const Student &stu){
  • this->m_name = stu.m_name;
  • this->m_age = stu.m_age;
  • this->m_score = stu.m_score;
  • cout<<"operator=() was called."<<endl;
  • return *this;
  • }
  • void Student::display(){
  • cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
  • }
  • int main(){
  • //stu1、stu2、stu3都会调用普通构造函数Student(string name, int age, float score)
  • Student stu1("小明", 16, 90.5);
  • Student stu2("王城", 17, 89.0);
  • Student stu3("陈晗", 18, 98.0);
  • Student stu4 = stu1; //调用拷贝构造函数Student(const Student &stu)
  • stu4 = stu2; //调用operator=()
  • stu4 = stu3; //调用operator=()
  • Student stu5; //调用普通构造函数Student()
  • stu5 = stu1; //调用operator=()
  • stu5 = stu2; //调用operator=()
  • return 0;
  • }

运行结果:

Copy constructor was called.
operator=() was called.
operator=() was called.
operator=() was called.
operator=() was called.

以拷贝的方式初始化对象

初始化对象时会调用构造函数,不同的初始化方式会调用不同的构造函数:

  • 如果用传递进来的实参初始化对象,那么会调用普通的构造函数,我们不妨将此称为普通初始化;
  • 如果用其它对象(现有对象)的数据来初始化对象,那么会调用拷贝构造函数,这就是以拷贝的方式初始化。

在实际编程中,具体有哪些情况是以拷贝的方式来初始化对象呢?

1) 将其它对象作为实参

以上面的 Student 类为例,我们可以这样来创建一个新的对象:

  • Student stu1("小明", 16, 90.5); //普通初始化
  • Student stu2(stu1); //以拷贝的方式初始化

即使我们不在类中显式地定义拷贝构造函数,这种初始化方式也是有效的,因为编译器会生成默认的拷贝构造函数。

2) 在创建对象的同时赋值

接着使用 Student 类,请看下面的例子:

  • Student stu1("小明", 16, 90.5); //普通初始化
  • Student stu2 = stu1; //以拷贝的方式初始化

这是最常见的一种以拷贝的方式初始化对象的情况,非常容易理解,我们也已经多次使用。

3) 函数的形参为类类型

如果函数的形参为类类型(也就是一个对象),那么调用函数时要将另外一个对象作为实参传递进来赋值给形参,这也是以拷贝的方式初始化形参对象。请看下面的代码:

  • void func(Student s){
  • //TODO:
  • }
  • Student stu("小明", 16, 90.5); //普通初始化
  • func(stu); //以拷贝的方式初始化

func() 函数有一个 Student 类型的形参 s,将实参 stu 传递给形参 s 就是以拷贝的方式初始化的过程。

函数是一段可以重复使用的代码,只有等到真正调用函数时才会为局部数据(形参和局部变量)在栈上分配内存。对于上面的 func(),虽然它的形参 s 是一个对象,但在定义函数时 s 对象并没有被创建,只有等到调用函数时才会真正地创建 s 对象,并在栈上为它分配内存。而创建 s 对象,就是以拷贝的方式进行的,它等价于下面的代码:

Student s = stu;

4) 函数返回值为类类型

当函数的返回值为类类型时,return 语句会返回一个对象,不过为了防止局部对象被销毁,也为了防止通过返回值修改原来的局部对象,编译器并不会直接返回这个对象,而是根据这个对象先创建出一个临时对象(匿名对象),再将这个临时对象返回。而创建临时对象的过程,就是以拷贝的方式进行的,会调用拷贝构造函数。

下面的代码演示了返回一个对象的情形:

  • Student func(){
  • Student s("小明", 16, 90.5);
  • return s;
  • }
  • Student stu = func();

理论上讲,运行代码后会调用两次拷贝构造函数,一次是返回 s 对象时,另外一次是创建 stu 对象时。

在较老的编译器上,你或许真的能够看到调用两次拷贝构造函数,例如 iPhone 上的 C/C++ 编译器。但是在现代编译器上,只会调用一次拷贝构造函数,或者一次也不调用,例如在 VS2010 下会调用一次拷贝构造函数,在 GCC、Xcode 下一次也不会调用。这是因为,现代编译器都支持返回值优化技术,会尽量避免拷贝对象,以提高程序运行效率。关于编译器如何实现返回值优化的我们不再展开讨论,有兴趣的读者请猛击:

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门