C++修炼之筑基期第三层——拷贝构造函数

简介: C++修炼之筑基期第三层——拷贝构造函数

000000000000000000000000000000、.png

目录


拷贝构造函数的概念

拷贝构造函数的特性


文章导读


本章主要内容为6个默认成员函数之一的拷贝构造函数的认识与学习,充分理解浅拷贝与深拷贝。


正文


拷贝构造函数的概念


还记得上一章中提到的6个默认成员函数吗?当我们定义好一个类,不做任何处理时,编译器会自动生成以下6个默认成员函数

  • 默认成员函数:如果用户没有手动实现,则编译器会自动生成的成员函数。

1.png

同样,拷贝构造函数也属于6个默认成员函数,而且拷贝构造函数构造函数的一种重载形式

  • 拷贝构造函数的功能就如同它的名字——拷贝。我们可以用一个已存在的对象来创建一个与已存在对象一模一样的新的对象

🌼举例🌼

class Date
{
public:
  //构造函数
  Date()
  {
    cout << "Date()" << endl;
  }
  //拷贝构造函数
  Date(const Date& d)
  {
    cout << "Date()" << endl;
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }
  //析构函数
  ~Date()
  {
    cout << "~Date()" << endl;
  }
private:
  int _year = 0;
  int _month = 0;
  int _day = 0;
};
void TestDate()
{
  Date d1;
  //调用拷贝构造创建对象
  Date d2(d1);
}


0.png


拷贝构造函数的特性


拷贝构造函数作为特殊的成员函数同样也有异于常人的特性:

  1. 拷贝构造函数是构造函数的重载
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用。若使用传值的方式,则编译器会报错,因为理论上这会引发无穷递归

错误示例🌼

class Date
{
public:
  //错误示例
  //如果这样写,编译器就会直接报错,但我们现在假设如果编译器不会检查,
  //这样的程序执行起来会发生什么
  Date(const Date d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }
private:
  int _year = 0;
  int _month = 0;
  int _day = 0;
};
void TestDate()
{
  Date d1;
  //调用拷贝构造创建对象
  Date d2(d1);
}


  • 当拷贝构造函数的参数采用传值的方式时,创建对象d2,会调用它的拷贝构造函数d1会作为实参传递给形参d。不巧的是,实参传递给形参本身又是一个拷贝,会再次调用形参的拷贝构造函数…如此便会引发无穷的递归。2.png
  1. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝或者值拷贝

🌼举例🌼

class Date
{
public:
  //构造函数
  Date(int year = 0, int month = 0, int day = 0)
  {
    //cout << "Date()" << endl;
    _year = year;
    _month = month;
    _day = day;
  }
  //未显式定义拷贝构造函数
  /*Date(const Date& d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }*/
  void print()
  {
    cout << _year << " - " << _month << " - " << _day << endl;
  }
private:
  int _year = 0;
  int _month = 0;
  int _day = 0;
};
void TestDate()
{
  Date d1(2023, 3, 31);
  //调用拷贝构造创建对象
  Date d2(d1);
  d2.print();
}

4.png

有的小伙伴可能会有疑问:编译器默认生成的拷贝构造函数貌似可以很好的完成任务,那么还需要我们手动来实现吗?

答案是:当然需要。Date类只是一个较为简单的类且类成员都是内置类型,可以不需要。但是当类中含有自定义类型时,编译器可就办不了事儿了。

类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝;

🌼错误示例🌼

class stack
{
public:
  stack(int defaultCapacity=10)
  {
    _a = (int*)malloc(sizeof(int)*defaultCapacity);
    if (_a == nullptr)
    {
      perror("malloc fail");
      exit(-1);
    }
    _top =  0;
    _capacity = defaultCapacity;
  }
  ~stack()
  {
    cout << "~Stack()" << endl;
    free(_a);
    _a = nullptr;
    _top = _capacity = 0;
  }
  void push(int n)
  {
    _a[_top++] = n;
  }
  void print()
  {
    for (int i = 0; i < _top; i++)
    {
      cout << _a[i] << " ";
    }
    cout << endl;
  }
private:
  int* _a;
  int _top;
  int _capacity;
};
void TestStack()
{
  stack s1;
  s1.push(1);
  s1.push(2);
  s1.push(3);
  s1.push(4);
  s1.print();
  stack s2(s1);
  s2.print();
  s2.push(5);
  s2.push(6);
  s2.push(7);
  s2.push(8);
  s2.print();
}

3.png


如图所示,这段程序的运行结果是程序崩溃了,且通过观察发现,是在第二次析构时出现了错误。其实出现错误的原因是在第二次析构时对野指针进行free了。


🌼一个小tip🌼


多个对象进行析构的顺序如同栈一样,先创建的对象后析构,后创建的对象先析构。

为什么会出现对野指针进行free呢?


原因是,对象s1与对象s2中的成员_a,指向的是同一块空间。在s2析构完成后,这块空间已经被释放,此时的s1._a就是野指针。这就是浅拷贝导致的后果。

🌼理解浅拷贝🌼


编译器默认生成的拷贝构造函数是按字节序拷贝的,在创建s2对象时,仅仅是把s1._a的值赋值给s2._a,并没有重新开辟一块与s1._a所指向的空间大小相同内容相同的空间。我们把前者的拷贝方式称为浅拷贝,后者称为深拷贝。

5.png

当开启监视窗口来观察这一过程,我们可以看到s2在进行push时,s1的内容也在跟着改变,且s1._a=s2._a


6.png


🌼正确的做法🌼

class stack
{
public:
  stack(int defaultCapacity=10)
  {
    _a = (int*)malloc(sizeof(int)*defaultCapacity);
    if (_a == nullptr)
    {
      perror("malloc fail");
      exit(-1);
    }
    _top =  0;
    _capacity = defaultCapacity;
  }
  //用户自己定义拷贝构造函数
  stack(const stack& s)
  {
    _a= (int*)malloc(sizeof(int) * s._capacity);
    if (_a == nullptr)
    {
      perror("malloc fail");
      exit(-1);
    }
    memcpy(_a, s._a, sizeof(int) * s._capacity);
    _top = s._top;
    _capacity = s._capacity;
  }
  ~stack()
  {
    cout << "~Stack()" << endl;
    free(_a);
    _a = nullptr;
    _top = _capacity = 0;
  }
  void push(int n)
  {
    _a[_top++] = n;
  }
  void print()
  {
    for (int i = 0; i < _top; i++)
    {
      cout << _a[i] << " ";
    }
    cout << endl;
  }
private:
  int* _a;
  int _top;
  int _capacity;
};


拷贝构造函数典型调用场景:

使用已存在对象创建新对象;

函数参数类型为类类型对象;

函数返回值类型为类类型对象。

为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。


本章的内容到这里就结束了,下一章我们将学习运算符重载与取地址操作符的重载~ 觉得内容有用的话就支持一下吧~


目录
相关文章
|
5月前
|
存储 编译器 C++
C++进阶之路:何为拷贝构造函数,深入理解浅拷贝与深拷贝(类与对象_中篇)
C++进阶之路:何为拷贝构造函数,深入理解浅拷贝与深拷贝(类与对象_中篇)
49 0
|
5月前
|
C++ 容器
【C++】拷贝构造函数、拷贝赋值函数与析构函数
【C++】拷贝构造函数、拷贝赋值函数与析构函数
108 6
|
5月前
|
存储 编译器 C++
【C++】:拷贝构造函数和赋值运算符重载
【C++】:拷贝构造函数和赋值运算符重载
29 1
|
5月前
|
存储 编译器 C++
【C++】类和对象③(类的默认成员函数:拷贝构造函数)
本文探讨了C++中拷贝构造函数和赋值运算符重载的重要性。拷贝构造函数用于创建与已有对象相同的新对象,尤其在类涉及资源管理时需谨慎处理,以防止浅拷贝导致的问题。默认拷贝构造函数进行字节级复制,可能导致资源重复释放。例子展示了未正确实现拷贝构造函数时可能导致的无限递归。此外,文章提到了拷贝构造函数的常见应用场景,如函数参数、返回值和对象初始化,并指出类对象在赋值或作为函数参数时会隐式调用拷贝构造。
|
6月前
|
存储 编译器 C++
【C++从练气到飞升】04---拷贝构造函数
【C++从练气到飞升】04---拷贝构造函数
|
5月前
|
程序员 编译器 C++
C++中的构造函数以及默认拷贝构造函数
C++中的构造函数以及默认拷贝构造函数
31 0
|
6月前
|
存储 编译器 C++
【C++成长记】C++入门 | 类和对象(中) |拷贝构造函数、赋值运算符重载、const成员函数、 取地址及const取地址操作符重载
【C++成长记】C++入门 | 类和对象(中) |拷贝构造函数、赋值运算符重载、const成员函数、 取地址及const取地址操作符重载
|
6月前
|
存储 编译器 对象存储
【C++】类与对象(构造函数、析构函数、拷贝构造函数、常引用)
【C++】类与对象(构造函数、析构函数、拷贝构造函数、常引用)
33 0
|
6月前
|
存储 编译器 C++
【c++】拷贝构造函数
【c++】拷贝构造函数
【c++】拷贝构造函数
|
6月前
|
存储 安全 编译器
【c++】类和对象(四)深入了解拷贝构造函数
朋友们大家好啊,本篇内容带大家深入了解拷贝构造函数
【c++】类和对象(四)深入了解拷贝构造函数