二说 拷贝构造函数 拷贝赋值函数

article/2024/12/5 9:16:08

文章目录

    • 什么是拷贝构造函数
    • 拷贝构造函数的调用时机
      • 2.1 当函数的参数为类的对象时
      • 2.2 函数的返回值是类的对象
      • 2.3 对象需要通过另外一个对象进行初始化
    • 浅拷贝与深拷贝
      • 3.1 默认拷贝构造函数
      • 3.2 浅拷贝
      • 3.3 深拷贝
      • 3.4 防止默认拷贝发生
    • 拷贝构造函数的几个细节
      • 4.1 为什么拷贝构造函数必须是引用传递,不能是值传递?
      • 4.2 拷贝构造函数的作用。
      • 4.3 参数传递过程到底发生了什么?
      • 4.4 在类中有指针数据成员时,拷贝构造函数的使用?
      • 4.5 拷贝构造函数里能调用private成员变量吗?
      • 4.6 以下函数哪个是拷贝构造函数,为什么?
      • 4.7 一个类中可以存在多于一个的拷贝构造函数吗?
      • 4.8 什么情况下必须定义拷贝构造函数

什么是拷贝构造函数

首先对于普通类型的对象来说,它们之间的复制是很简单的,例如:

int a=100;
int b=a;

而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。
下面看一个类对象拷贝的简单例子。

#include<iostream>
using namespace std;
class CExample
{
private:int a;
public://构造函数CExample(int b){a=b;printf("constructor is called\n");}//拷贝构造函数CExample(const CExample & c){a=c.a;printf("copy constructor is called\n");}//析构函数~CExample(){cout<<"destructor is called\n";}void Show(){cout<<a<<endl;}
};
int main()
{CExample A(100);CExample B=A;B.Show(); return 0;
}

output

constructor is called     
copy constructor is called
100
destructor is called
destructor is called

运行程序,屏幕输出100。从以上代码的运行结果可以看出,系统为对象 B 分配了内存并完成了与对象 A 的复制过程。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。

CExample(const CExample& C) 就是我们自定义的拷贝构造函数。可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量

拷贝构造函数的调用时机

2.1 当函数的参数为类的对象时

#include<iostream>
using namespace std;
class CExample
{
private:int a;
public:CExample(int b){a=b;printf("constructor is called\n");}CExample(const CExample & c){a=c.a;printf("copy constructor is called\n");}~CExample(){cout<<"destructor is called\n";}void Show(){cout<<a<<endl;}
};
void g_fun(CExample c)
{cout<<"g_func"<<endl;
}
int main()
{CExample A(100);CExample B=A;B.Show(); g_fun(A);return 0;
}

output

constructor is called     
copy constructor is called
100
copy constructor is called
g_func
destructor is called
destructor is called
destructor is called

调用g_fun()时,会产生以下几个重要步骤:

  1. .A对象传入形参时,会先会产生一个临时变量,就叫 C 吧。
  2. .然后调用拷贝构造函数把A的值给C。 整个这两个步骤有点像:CExample C(A);
  3. 等g_fun()执行完后, 析构掉 C 对象。

2.2 函数的返回值是类的对象

#include<iostream>
using namespace std;
class CExample
{
private:int a;
public://构造函数CExample(int b){a=b;printf("constructor is called\n");}//拷贝构造函数CExample(const CExample & c){a=c.a;printf("copy constructor is called\n");}//析构函数~CExample(){cout<<"destructor is called\n";}void Show(){cout<<a<<endl;}
};
CExample g_fun()
{CExample temp(0);return temp;
}
int main()
{g_fun();return 0;
}

output

constructor is called
destructor is called

当g_Fun()函数执行到return时,会产生以下几个重要步骤:

  1. 先会产生一个临时变量,就叫XXXX吧。
  2. 然后调用拷贝构造函数把temp的值给XXXX。整个这两个步骤有点像:CExample XXXX(temp);
  3. 在函数执行到最后先析构temp局部变量。
  4. 等g_fun()执行完后再析构掉XXXX对象。

2.3 对象需要通过另外一个对象进行初始化

CExample A(100);
CExample B=A;

浅拷贝与深拷贝

3.1 默认拷贝构造函数

很多时候在我们都不知道拷贝构造函数的情况下,传递对象给函数参数或者函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值,它一般具有以下形式:

Rect::Rect(const Rect& r)
{width=r.width;height=r.height;
}

当然,以上代码不用我们编写,编译器会为我们自动生成。但是如果认为这样就可以解决对象的复制问题,那就错了,让我们来考虑以下一段代码:

#include<iostream>
using namespace std;
class Rect
{
public:Rect(){count++;}~Rect(){count--;}static int getCount(){return count;}
private:int width;int height;static int count;
};
int Rect::count=0;
int main()
{Rect rect1;cout<<"The count of Rect:"<<Rect::getCount()<<endl;Rect rect2(rect1);cout<<"The count of Rect:"<<Rect::getCount()<<endl;return 0;
}

这段代码对前面的类,加入了一个静态成员,目的是进行计数。在主函数中,首先创建对象rect1,输出此时的对象个数,然后使用rect1复制出对象rect2,再输出此时的对象个数,按照理解,此时应该有两个对象存在,但实际程序运行时,输出的都是1,反应出只有1个对象。此外,在销毁对象时,由于会调用销毁两个对象,类的析构函数会调用两次,此时的计数器将变为负数。

说白了,就是拷贝构造函数没有处理静态数据成员。

出现这些问题最根本就在于在复制对象时,计数器没有递增,我们重新编写拷贝构造函数,如下:

#include<iostream>
using namespace std;
class Rect
{
public:Rect(){count++;}Rect(const Rect& r){width=r.width;height=r.height;count++;}~Rect(){count--;}static int getCount(){return count;}
private:int width;int height;static int count;
};
int Rect::count=0;
int main()
{Rect rect1;cout<<"The count of Rect:"<<Rect::getCount()<<endl;Rect rect2(rect1);cout<<"The count of Rect:"<<Rect::getCount()<<endl;return 0;
}

3.2 浅拷贝

所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了,让我们考虑如下一段代码:

 #include<iostream>
#include<assert.h>
using namespace std;
class Rect
{
public:Rect(){p=new int(100);}~Rect(){assert(p!=NULL);delete p;}
private:int width;int height;int *p;
};
int main()
{Rect rect1;Rect rect2(rect1);return 0;
}

在这段代码运行结束之前,会出现一个运行错误。原因就在于在进行对象复制时,对于动态分配的内容没有进行正确的操作。我们来分析一下:

在运行定义rect1对象后,由于在构造函数中有一个动态分配的语句,因此执行后的内存情况大致如下:

在这里插入图片描述
在使用rect1复制rect2时,由于执行的是浅拷贝,只是将成员的值进行赋值,这时 rect1.p = rect2.p,也即这两个指针指向了堆里的同一个空间,如下图所示:

在这里插入图片描述
当然,这不是我们所期望的结果,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因。我们需要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。

3.3 深拷贝

在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间,如上面的例子就应该按照如下的方式进行处理:

#include<iostream>
#include<assert.h>
using namespace std;
class Rect
{
public:Rect(){p=new int(100);}Rect(const Rect& r){width=r.width;height=r.height;p=new int(100);*p=*(r.p);}~Rect(){assert(p!=NULL);delete p;}
private:int width;int height;int *p;
};
int main()
{Rect rect1;Rect rect2(rect1);return 0;
}

此时,在完成对象的复制后,内存的一个大致情况如下:

在这里插入图片描述
此时rect1的p和rect2的p各自指向一段内存空间,但它们指向的空间具有相同的内容,这就是所谓的“深拷贝”。

3.4 防止默认拷贝发生

通过对对象复制的分析,我们发现对象的复制大多在进行“值传递”时发生,这里有一个小技巧可以防止按值传递——声明一个私有拷贝构造函数。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。

//防止按值传递
class CExample 
{ 
private: int a; public: //构造函数CExample(int b) { a = b; cout<<"creat: "<<a<<endl; } private: //拷贝构造函数,只是声明CExample(const CExample& C); public: ~CExample() { cout<< "delete: "<<a<<endl; } void Show () { cout<<a<<endl; } 
}; void g_Fun(CExample C) 
{ cout<<"test"<<endl; 
} int main() 
{ CExample test(1); //g_Fun(test);   //按值传递将出错return 0; 
}

小结:
拷贝有两种:深拷贝,浅拷贝。

当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。

深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。

拷贝构造函数的几个细节

4.1 为什么拷贝构造函数必须是引用传递,不能是值传递?

简单的回答是为了防止递归引用。

具体一些可以这么讲:
当 一个对象需要以值方式传递时,编译器会生成代码调用它的拷贝构造函数以生成一个复本。如果类A的拷贝构造函数是以值方式传递一个类A对象作为参数的话,当 需要调用类A的拷贝构造函数时,需要以值方式传进一个A的对象作为实参; 而以值方式传递需要调用类A的拷贝构造函数;结果就是调用类A的拷贝构造函数导 致又一次调用类A的拷贝构造函数,这就是一个无限递归。

4.2 拷贝构造函数的作用。

作用就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实例。

4.3 参数传递过程到底发生了什么?

将地址传递和值传递统一起来,归根结底还是传递的是"值"(地址也是值,只不过通过它可以找到另一个值)!

  • 值传递:
    对于内置数据类型的传递时,直接赋值拷贝给形参(注意形参是函数内局部变量);
    对于类类型的传递时,需要首先调用该类的拷贝构造函数来初始化形参(局部对象);如void foo(class_type obj_local){}, 如果调用foo(obj); 首先class_type obj_local(obj) ,这样就定义了局部变量obj_local供函数内部使用
  • 引用传递:
    无论对内置类型还是类类型,传递引用或指针最终都是传递的地址值!而地址总是指针类型(属于简单类型), 显然参数传递时,按简单类型的赋值拷贝,而不会有拷贝构造函数的调用(对于类类型).

4.4 在类中有指针数据成员时,拷贝构造函数的使用?

如果不显式声明拷贝构造函数的时候,编译器也会生成一个默认的拷贝构造函数,而且在一般的情况下运行的也很好。但是在遇到类有指针数据成员时就出现问题 了:因为默认的拷贝构造函数是按成员拷贝构造,这导致了两个不同的指针(如ptr1=ptr2)指向了相同的内存。当一个实例销毁时,调用析构函数 free(ptr1)释放了这段内存,那么剩下的一个实例的指针ptr2就无效了,在被销毁的时候free(ptr2)就会出现错误了, 这相当于重复释放一块内存两次。这种情况必须显式声明并实现自己的拷贝构造函数,来为新的实例的指针分配新的内存。

问题 1 和 2 回答了为什么拷贝构造函数使用值传递会产生无限递归调用的问题;
问题 3 回答了回答了在类中有指针数据成员时,拷贝构造函数使用值传递等于白显式定义了拷贝构造函数,因为默认的拷贝构造函数就是这么干的。

4.5 拷贝构造函数里能调用private成员变量吗?

拷贝构造函数其时就是一个特殊的构造函数,操作的还是自己类的成员变量,所以不受private的限制。

4.6 以下函数哪个是拷贝构造函数,为什么?

X::X(const X&);   //拷贝构造函数
X::X(X); 
X::X(X&, int a=1);   //拷贝构造函数
X::X(X&, int a=1, int b=2);  //拷贝构造函数

解答:对于一个类X, 如果一个构造函数的第一个参数是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数.

4.7 一个类中可以存在多于一个的拷贝构造函数吗?

类中可以存在超过一个拷贝构造函数。

class X { 
public: X(const X&); // const 的拷贝构造 X(X&); // 非const的拷贝构造 
};

注意,如果一个类中只存在一个参数为 X& 的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化.
如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数。
这个默认的参数可能为 X::X(const X&)或 X::X(X&),由编译器根据上下文决定选择哪一个。

4.8 什么情况下必须定义拷贝构造函数

当类的对象用于函数值传递时(值参数,返回类对象),拷贝构造函数会被调用。如果对象复制并非简单的值拷贝,那就必须定义拷贝构造函数。例如大的堆栈数据拷贝。如果定义了拷贝构造函数,那也必须重载赋值操作符。


http://chatgpt.dhexx.cn/article/QNRaghxJ.shtml

相关文章

C++拷贝构造函数、构造函数和析构函数

一、拷贝构造函数 转载自&#xff1a;http://www.cnblogs.com/BlueTzar/articles/1223313.html 1、类对象的拷贝 对于普通类型的对象来说&#xff0c;它们之间的复制是很简单的&#xff0c;例如&#xff1a; int a88; int ba; 而类对象与普通对象不同&#xff0c;类对象内部…

拷贝构造函数起作用的三种情况

拷贝构造函数起作用的三种情况&#xff1a; 1.当用类的对象去初始化同类的另一个对象时。 Date d2(d1); Date d2 d1; //初始化语句&#xff0c;并非赋值语句。 2.当函数的形参是类的对象&#xff0c;调用函数进行形参和实参结合时。 void Func(A a1) //形参是类Date的对象…

【拷贝构造函数】c++类拷贝构造函数详解

【拷贝构造函数】c类拷贝构造函数详解 目录 【拷贝构造函数】c类拷贝构造函数详解一、什么是拷贝构造函数二、拷贝构造函数的几种调用时机1. 当函数的参数为类的对象时2. 函数的返回值是类的对象3. 当成员变量为类类型时4. 普通派生类构造函数的写法 三、浅拷贝与深拷贝1. 默认…

拷贝(复制)构造函数定义及3种调用情况举例

一、拷贝构造函数是一种特殊的构造函数&#xff0c;其形参为本类的对 象引用。 class 类名 { public : 类名&#xff08;形参&#xff09;&#xff1b;//构造函数 类名&#xff08;类名 &对象名&#xff09;&#xff1b;//拷贝构造函数 ... }&#xff1b; //拷贝构造函…

C++——拷贝构造函数详解

C——拷贝构造函数详解 1.拷贝构造函数的特点&#xff1a;2.通过例子引入拷贝构造&#xff1a;3构造对象的时候使用引用返回与不使用引用返回的问题&#xff1a;3.1不使用引用返回&#xff1a;3.2引用返回——从已经死亡的地址接收值不牢靠&#xff1a; 4.缺省的拷贝构造和等号…

C++ 拷贝构造函数详解

C 拷贝构造函数详解 下面的讲解将以C标准库的string类作为讲解对象&#xff0c;string类&#xff1a;class with pointer member(s) 1、拷贝构造函数和拷贝赋值函数 1.1引入 下面是给出的测试函数&#xff0c;也是我们要能在自己设计的myString类中实现的功能&#xff1a; …

详解析构函数、拷贝构造函数

目录 一.析构函数&#xff08;析构器&#xff09; &#xff08;一&#xff09;.使用方式及注意事项 1.使用方式 2.注意事项 &#xff08;二&#xff09;.默认析构函数 二.拷贝构造函数 &#xff08;一&#xff09;.使用方式及注意事项 1.使用方式 2.注意事项 &#xff0…

【深入理解C++】拷贝构造函数

文章目录 1.拷贝构造函数2.默认的拷贝操作3.默认拷贝构造函数4.何时调用拷贝构造函数 1.拷贝构造函数 拷贝构造函数是构造函数的一种。当利用已存在的对象创建一个新对象时&#xff0c;就会调用新对象的拷贝构造函数进行初始化。 拷贝构造函数的格式是固定的&#xff0c;即接…

C++拷贝构造函数详解

一. 什么是拷贝构造函数 首先对于普通类型的对象来说&#xff0c;它们之间的复制是很简单的&#xff0c;例如&#xff1a; int a 100; int b a; 而类对象与普通对象不同&#xff0c;类对象内部结构一般较为复杂&#xff0c;存在各种成员变量。 下面看一个类对象拷贝的简…

c++拷贝构造函数(深拷贝,浅拷贝)详解

一、什么是拷贝构造函数 首先对于普通类型的对象来说,它们之间的复制是很简单的,例如: int a=100; int b=a; 而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。 下面看一个类对象拷贝的简单例子。 #include<iostream> using n…

YOLO 裂缝检测

环境 python3.5 yolov3 opencv keras

基于OpenCV的混凝土裂纹检测

基于OpenCV的混凝土裂纹检测 前言 这是我发的第一次博客&#xff0c;有什么建议大家可以给我留言&#xff0c;感激不尽! 接下来&#xff0c;我们进入正题。 一、使用函数库 numpy, opencv, heapq, skimage.morphology 二、使用步骤 1.初步预处理 初步预处理包括&#xf…

【图像识别】基于计算机视觉实现路面裂缝检测识别系统matlab代码

1 简介 随着公路与铁路事业的飞速发展,各类车辆和里程的增加,铁路的一次次提速,都对路面产生了巨大的压力。不论是公路路面还是铁路路面,路面裂纹都能随处可见,由路面裂纹造成的交通事故时有发生。研究路面裂纹检测方法对于路面维护、交通安全具有极其重大意义。近年来,路面裂…

基于计算机视觉的裂纹检测方案

点击上方“小白学视觉”&#xff0c;选择加"星标"或“置顶” 重磅干货&#xff0c;第一时间送达01. 数据集 我们首先需要从互联网上获取包含墙壁裂缝的图像&#xff08;URL格式&#xff09;数据。总共包含1428张图像&#xff1a;其中一半是新的且未损坏的墙壁&#x…

halcon裂纹缺陷检测

针对这一类表面的检测就不能单纯依靠帧差或者背景差来完成&#xff0c;因为背景的纹理不可能和当前图像的纹理完全相同。 方法一—局部阈值分割 一、局部阈值分割 1、gen_sin_bandpass–局部阈值分割 dyn_threshold(OrigImage, ThresholdImage : RegionDynThresh : Offset,…

使用 Python 进行深度学习以进行裂纹检测

使用 Python 进行深度学习以进行裂纹检测 问题陈述数据集准备训练模型结论参考 问题陈述 虽然新技术已经改变了我们生活的方方面面&#xff0c;在建筑领域似乎牛逼正在努力追赶。目前&#xff0c;建筑物的结构状况仍然主要是人工检查。简单来说&#xff0c;即使现在需要检查结…

Halcon-表面检测-----裂纹检测

对应示例程序&#xff1a; detect_mura_defects_blur.hdev 目标&#xff1a;实例实现LCD上有很多污点干扰下&#xff0c;检测LCD的印痕检测。 思路为&#xff1a;对LCD图像进行拆分&#xff0c;提取RGB三个分量。 对B分量进行处理&#xff0c;将其转换为频域内图像&#xff0…

图像中的裂纹检测

01. 数据集 我们首先需要从互联网上获取包含墙壁裂缝的图像(URL格式)数据。总共包含1428张图像:其中一半是新的且未损坏的墙壁;其余部分显示了各种尺寸和类型的裂缝。 第一步:读取图像,并调整大小。 images = []for url in tqdm.tqdm(df[content]): response = req…

html图片与文字轮播,我是这样写文字轮播的

原标题&#xff1a;我是这样写文字轮播的 作者&#xff1a;一半水一半冰 原文&#xff1a;http://www.cnblogs.com/jingh/p/6377736.html 1写在前面 最近总结下之前的工作&#xff0c;才恍然发现时间的流逝永远是悄无声息的&#xff0c;离开学校那座象牙塔已经也有大半年的时间…

IOS 文本文字下面添加下划线

2019独角兽企业重金招聘Python工程师标准>>> UILabel *infolabel [[UILabel alloc] initWithFrame:CGRectMake(30*RATIO,260*RATIO, self.view.bounds.size.width-60*RATIO, 20*RATIO)];infolabel.text "infoimcba.com";infolabel.textColor [UIColor …