【c++】类和对象(四)深入了解拷贝构造函数

简介: 朋友们大家好啊,本篇内容带大家深入了解拷贝构造函数

1.拷贝构造函数

拷贝构造函数是一种特殊的构造函数,在对象需要以同一类的另一个对象为模板进行初始化时被调用。它的主要用途是初始化一个对象,使其成为另一个对象的副本


我们先引用前面所用到的日期类的例子:


1. class Date
2. {
3. public:
4. 
5.  Date(int year = 1, int month = 1, int day = 1)
6.  {
7.   _year = year;
8.   _month = month;
9.   _day = day;
10.   }
11. private:
12.   int _year;
13.   int _month;
14.   int _day;
15. };

简单来说,假如我现在定义了一个日期对象:


int main()
{
  Date d1(2005, 6, 23);
  return 0;
}

我需要定义一个d2,与我的d1的数据相同,如何定义呢?


方法如下:


int main()
{
  Date d1(2005, 6, 23);
  Date d2(d1);
  return 0;
}


这里用到了拷贝构造,那么拷贝函数是如何实现的呢?我们接下来来探讨一下


拷贝构造函数通常声明为接受一个对同一类对象的常量引用参数:


class ClassName {
public:
    ClassName(const ClassName& other);
};


参数:const ClassName& other是对另一个同类型对象的引用,使用const确保不会无意中修改other。

函数体:在函数体内部,你可以决定如何复制other对象的成员到新对象中。对于简单的情况,这可能仅仅是复制每个成员变量的值。对于涉及动态分配内存或其他资源的类,可能需要进行深拷贝

下面来探讨上述的Date类的实现


class Date
{
public:
  Date(int year = 1, int month = 1, int day = 1)
  {
  _year = year;
  _month = month;
  _day = day;
  }
  Date(const Date& d)
  {
  _year = d._year;
  _month = d._month;
  _day = d._day;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2005, 6, 23);
  Date d2(d1);
  return 0;
}

拷贝构造函数是构造函数的一个重载形式,拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用,这个我们后面进行讲解


Date(const Date& d)
  {
  _year = d._year;
  _month = d._month;
  _day = d._day;
  }


这里的d2就相当于this,d1就是另一个参数

1.1传值调用的无限调用

我们上面提到,拷贝构造函数参数只有一个且必须是类类型对象的引用,那么如果我使用传值调用会有什么结果呢??


我们下面先来进行简单的铺垫


void fun1(Date d)
{
}
void fun2(Date& rd)
{
}
int main()
{
  Date d1(2005, 6, 23);
  fun1(d1);
  fun2(d1);
  return 0;
}


构造两个函数,他们的参数不同,第一个函数为传值传参,在c语言中我们知道,传值传参是一个拷贝的过程,即把d1的值拷贝给d,c++规定,自定义类型的拷贝,都会调用拷贝构造


我们进行调试


在这里按F11,我们目的是进入fun1,函数,这里却跳入拷贝构造函数

再按f11,才会进入fun1函数中

大概过程如下

传值传参需要调用拷贝构造



fun2函数可以直接进入


在上述讲解后,我们来探讨,如果拷贝函数是传值引用,会发生什么?


调用拷贝构造,需要传参,这里传值传参,就会调用一个新的拷贝构造


所以,这里也是我们为什么只能用引用传参


1.2浅拷贝

class Date
{
public:
  Date(int year = 1, int month = 1, int day = 1)
  {
  _year = year;
  _month = month;
  _day = day;
  }
  void Print()
  {
  cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2005, 6, 23);
  Date d2(d1);
  d1.Print();
  d2.Print();
  return 0;
}

我们现在屏蔽掉拷贝构造,看会发生什么


若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象(内置类型成员)按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝


那如果有自定义类型呢?

我们看下面的代码:


class Time
{
public:
  Time()
  {
  _hour = 1;
  _minute = 1;
  _second = 1;
  }
  Time(const Time& t)
  {
  _hour = t._hour;
  _minute = t._minute;
  _second = t._second;
  cout << "Time::Time(const Time&)" << endl;
  }
private:
  int _hour;
  int _minute;
  int _second;
};
class Date
{
private:
  // 基本类型(内置类型)
  int _year = 2024;
  int _month = 1;
  int _day = 1;
  // 自定义类型
  Time _t;
};
int main()
{
  Date d1;
  Date d2(d1);
  return 0;
}class Time
{
public:
  Time()
  {
  _hour = 1;
  _minute = 1;
  _second = 1;
  }
  Time(const Time& t)
  {
  _hour = t._hour;
  _minute = t._minute;
  _second = t._second;
  cout << "Time::Time(const Time&)" << endl;
  }
private:
  int _hour;
  int _minute;
  int _second;
};
class Date
{
private:
  // 基本类型(内置类型)
  int _year = 2024;
  int _month = 1;
  int _day = 1;
  // 自定义类型
  Time _t;
};
int main()
{
  Date d1;
  Date d2(d1);
  return 0;
}

在这个代码示例中,我们有两个类:Time 和 Date。Date 类中包含了一些基本类型的成员变量(_year, _month, _day)和一个自定义类型的成员变量(_t,一个 Time 类型的对象)。当创建 Date 类的对象时,不仅会初始化其基本类型的成员变量,也会调用其自定义类型成员的构造函数来初始化

函数的调用过程


Date 对象的默认构造函数调用:当 Date 类的对象被创建时,它的默认构造函数(编译器自动生成的,因为没有显式定义)会被调用。由于成员变量 _year, _month, _day 在类定义中已经被直接初始化,编译器将这些初始化纳入默认构造函数的操作中。


Time 成员的构造函数调用:在 Date 的构造函数执行过程中,会自动调用 _t(Time 类型的成员变量)的默认构造函数来初始化 _t。Time 的默认构造函数设置 _hour, _minute, _second 为 1,并不打印任何信息。


拷贝 Date 对象:当 Date d2(d1); 执行时,d2 是通过拷贝构造函数初始化的。因为 Date 类没有显式定义拷贝构造函数,编译器会为它生成一个默认的拷贝构造函数。这个默认的拷贝构造函数会逐个拷贝 Date 类中的所有成员变量,包括基本类型和自定义类型的成员。


对于基本类型成员(如 _year, _month, _day),直接进行值的复制

对于自定义类型的成员(_t),会调用该成员的拷贝构造函数(Time 类中定义的 Time(const Time&))来进行拷贝。在这个过程中,Time 的拷贝构造函数会输出信息:Time::Time(const Time&)

因此,在执行 Date d2(d1); 时,调用过程如下:


首先,调用 Date 的默认拷贝构造函数(自动生成)来初始化 d2。

在初始化 d2 的过程中,对于其自定义类型成员 _t,调用 Time 的拷贝构造函数来初始化,此时会输出 Time::Time(const Time&)。

这就是自定义类型成员在 Date 类拷贝过程中构造函数的调用情况,其他的基本类型成员变量则是通过简单的值复制来初始化的


在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的


如果我们删掉Time的默认的拷贝构造函数呢?


1. class Time
2. {
3. public:
4.  Time(const Time& t)
5.  {
6.   _hour = t._hour;
7.   _minute = t._minute;
8.   _second = t._second;
9.   cout << "Time::Time(const Time&)" << endl;
10.   }
11. private:
12.   int _hour;
13.   int _minute;
14.   int _second;
15. };
16. class Date
17. {
18. private:
19.   // 基本类型(内置类型)
20.   int _year = 2024;
21.   int _month = 1;
22.   int _day = 1;
23.   // 自定义类型
24.   Time _t;
25. };
26. int main()
27. {
28.   Date d1;
29.   Date d2(d1);
30.   return 0;
31. }


拷贝构造本身就是一种构造函数,所以编译器不会生成默认构造函数


在这个代码中,由于 Time 类中没有显式定义一个无参数的默认构造函数(只定义了一个拷贝构造函数),而 Date 类的实现依赖于 Time 类的这个默认构造函数来初始化其 _t 成员,所以编译器将尝试调用 Time 类的默认构造函数时会失败,因为找不到合适的构造函数来初始化 _t


当尝试创建 Date 类的实例 d1 时,Date 类的默认构造函数(由编译器隐式生成)会被调用。默认构造函数会尝试初始化所有成员变量,对于基本类型的成员变量 _year,_month, _day,由于它们已经在类定义中直接初始化,不会有问题。但对于 _t(Time 类型的成员变量),编译器需要调用 Time 类的默认构造函数来初始化它。由于 Time类中没有定义无参数的默认构造函数,编译过程中会出现错误


当尝试通过拷贝构造函数创建 d2 时(Date d2(d1);),同样会遇到问题。虽然 Date 类的拷贝构造函数(编译器自动生成的)会尝试逐个拷贝所有成员变量,对于 _t,它会尝试调用 Time类的拷贝构造函数,这部分没有问题。但在创建 d1 时已经失败,因此这一步也无法成功执行


c++也可以加入这串代码进行强制生成:


Time() = default;

1.3深拷贝

如果你没有为类显式定义拷贝构造函数,C++编译器会自动生成一个默认的拷贝构造函数。默认拷贝构造函数会逐个复制对象的所有成员(浅拷贝)。对于基本数据类型和指向动态分配内存的指针成员,这意味着只复制指针值而不复制指针指向的数据


我们来看下面的代码:


1. typedef int DataType;
2. class Stack
3. {
4. public:
5.  Stack(size_t capacity = 10)
6.  {
7.   _array = (DataType*)malloc(capacity * sizeof(DataType));
8.   if (nullptr == _array)
9.   {
10.     perror("malloc申请空间失败");
11.     return;
12.   }
13.   _size = 0;
14.   _capacity = capacity;
15.   }
16.   void Push(const DataType& data)
17.   {
18.   // CheckCapacity();
19.   _array[_size] = data;
20.   _size++;
21.   }
22.   ~Stack()
23.   {
24.   if (_array)
25.   {
26.     free(_array);
27.     _array = nullptr;
28.     _capacity = 0;
29.     _size = 0;
30.   }
31.   }
32. private:
33.   DataType* _array;
34.   size_t _size;
35.   size_t _capacity;
36. };
37. int main()
38. {
39.   Stack s1;
40.   s1.Push(1);
41.   s1.Push(2);
42.   s1.Push(3);
43.   s1.Push(4);
44.   Stack s2(s1);
45.   return 0;
46. }

我们没有提供拷贝构造函数,编译器默认提供,我们来看运行结果:

程序崩溃,我们进行调试观察

当通过 Stack s2(s1); 这样的语句创建一个 Stack 类的对象时,如果没有显式定义拷贝构造函数,C++ 编译器会提供一个默认的拷贝构造函数,它进行浅拷贝。这意味着 _array 指针的值被复制过来,但指向的内存空间没有被复制。这会导致多个对象共享同一块内存空间,进而导致双重释放等问题



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


**浅拷贝(Shallow Copy)**只复制对象的顶层结构,如果对象中包含指针指向动态分配的内存,则副本的这些指针将指向与原始对象相同的内存地址。这意味着两个对象共享部分资源。浅拷贝通常是通过默认的拷贝构造函数和赋值操作符实现的

深拷贝则复制对象所有的层级结构。对于对象内部的每一个指针指向的内存,深拷贝都会在堆上分配新的内存,然后将原始数据复制到这块新分配的内存中。这样,原始对象和副本对象将拥有完全独立的数据副本

1.4深拷贝的实现

深拷贝需要我们手动实现,对于上述的代码,我们需要手动补充,于对象内部的每个指向动态分配内存的指针,都需要:


为副本分配新的内存空间。

将原始对象指针指向的数据复制到新分配的内存中:

1. typedef int DataType;
2. class Stack
3. {
4. public:
5.  Stack(size_t capacity = 10)
6.  {
7.   _array = (DataType*)malloc(capacity * sizeof(DataType));
8.   if (nullptr == _array)
9.   {
10.     perror("malloc申请空间失败");
11.     return;
12.   }
13.   _size = 0;
14.   _capacity = capacity;
15.   }
16.   //注意,这里s2是this,s是s1
17.   Stack(const Stack& s)
18.   {
19.   DataType* tmp == (DataType*)malloc(sizeof(DataType) * s._capacity);
20.   if (tmp = nullptr)
21.   {
22.     perror("malloc fail");
23.     exit(-1);
24.   }
25.   memcpy(tmp, s._array, sizeof(DataType) * s._size);
26.   _array = tmp;
27.   _size = s._size;
28.   _capacity = s._capacity;
29.   }
30.   void Push(const DataType& data)
31.   {
32.   // CheckCapacity();
33.   _array[_size] = data;
34.   _size++;
35.   }
36.   ~Stack()
37.   {
38.   if (_array)
39.   {
40.     free(_array);
41.     _array = nullptr;
42.     _capacity = 0;
43.     _size = 0;
44.   }
45.   }
46. private:
47.   DataType* _array;
48.   size_t _size;
49.   size_t _capacity;
50. };
51. int main()
52. {
53.   Stack s1;
54.   s1.Push(1);
55.   s1.Push(2);
56.   s1.Push(3);
57.   s1.Push(4);
58.   Stack s2(s1);
59.   return 0;
60. }
61. //注意,这里s2是this,s是s1
62.   Stack(const Stack& s)
63.   {
64.   DataType* tmp = (DataType*)malloc(sizeof(DataType) * s._capacity);
65.   if (tmp == nullptr)
66.   {
67.     perror("malloc fail");
68.     exit(-1);
69.   }
70.   memcpy(tmp, s._array, sizeof(DataType) * s._size);
71.   _array = tmp;
72.   _size = s._size;
73.   _capacity = s._capacity;
74.   }

这个拷贝构造函数的主要功能是创建一个新的 Stack 对象,该对象是对现有 Stack 对象(称为 s)的深拷贝。深拷贝意味着新对象将拥有与原对象相同的数据副本,但这些数据存储在新分配的内存中。这样,两个对象的状态互不影响,修改一个对象的内容不会影响另一个


内存分配:

使用 malloc 根据原栈 (s) 的容量 (_capacity) 分配足够的内存空间来存储数据副本。这里的内存大小是 s._capacity * sizeof(DataType)


数据复制:

使用 memcpy 将原栈 (s) 的数据 _array 复制到新分配的内存 tmp 中。复制的长度是 s._size * sizeof(DataType),即仅复制原栈中实际存在的元素


更新成员变量:

将新栈的 _array 指针更新为指向新分配的内存 tmp。

将新栈的 _size 和 _capacity 设置为与原栈 (s) 相同的值。这样保证了新栈在逻辑上与原栈完全相同,拥有相同数量的元素和相同的容量


这下我们的问题也就解决了


class myqueue {
private:
  Stack st1;
  Stack st2;
};
int main()
{
  myqueue q1;
  myqueue q2(q1);
  return 0;
}


有一个 Stack 类,它实现了一个简单的栈,并提供了深拷贝功能。然后,创建一个 myqueue 类,它内部使用了两个 Stack 实例。在 main 函数中,创建了一个 myqueue 对象 q1 并尝试使用 q1 来初始化另一个 myqueue 对象 q2。这里的关键点在于理解 Stack 的深拷贝实现如何影响 myqueue 对象的复制行为


myqueue 类及其复制行为

myqueue 类内部包含两个 Stack 对象:st1 和 st2。当使用一个 myqueue 对象来初始化另一个(如 myqueue q2(q1);)时,myqueue 的隐式(或默认)拷贝构造函数被调用。C++ 默认的拷贝构造函数会逐个复制类的成员,使用各成员自己的拷贝构造函数。因此,q1 中的 st1 和 st2 会使用它们各自的深拷贝构造函数来初始化 q2 中的 st1 和 st2

由于 Stack 类已经提供了深拷贝的实现,myqueue 类中的 st1 和 st2 成员在 myqueue

对象被复制时也会被深拷贝。这意味着 q1 和 q2 中的 st1 和 st2 在内存上是独立的:q1.st1 和

q2.st1 指向不同的内存区域,q1.st2 和 q2.st2 同理。因此,q1 和 q2

在逻辑上是完全独立的队列,它们内部的栈互不影响


隐式拷贝构造函数:myqueue 类在这段代码中并没有显式定义自己的拷贝构造函数。它依赖于 C++ 自动生成的默认拷贝构造函数来正确地复制其成员。这在 Stack 提供深拷贝的情况下是安全的

本篇内容到此结束,感谢大家观看!!!!


相关文章
|
28天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
50 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
101 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
87 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
102 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
32 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
32 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
29 1
|
2月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
2月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
2月前
|
存储 编译器 C语言
【C++类和对象(上)】—— 我与C++的不解之缘(三)
【C++类和对象(上)】—— 我与C++的不解之缘(三)