【C++语法难点】 深拷贝和浅拷贝是什么,拷贝构造,拷贝赋值

news2025/7/18 6:08:17

文章目录

    • 1.开始:构造函数
    • 1.2 在栈区和堆区创建对象
    • 1.3 缺省构造函数
    • 1.4 类型转换构造函数
    • 1.5 拷贝构造函数
    • 1.6 缺省拷贝构造函数(浅拷贝)
    • 1.7 深拷贝构造函数 (深拷贝)
    • 1.8 拷贝赋值

1.开始:构造函数

语法形式

class 类名{
	类名(形参表){...}//构造函数
};

特点和作用

  • 与类名相同,没有返回值类型
  • 对象创建时自动被调用,不能像普通成员函数一样显示调用
  • 主要负责初始化成员变量
    • 即根据这个类创建一个对象,那怎样是创建一个对象嘞?
      • 给这个给这个对象分配个地址
      • 给这个对象的每个成员变量都赋初值

1.2 在栈区和堆区创建对象

在哪里创建对象

通常,根据使用场景的不同,我们可以再栈区堆区两个地方创建对象

在这两个地方创建对象有什么不同呢?

栈区(stack)

  • 堆区变量由编译器自动分配释放内存,一般存放函数的参数值、局部变量等
  • 变量名、函数名、指针名等都在栈区

堆区(heap)

  • 一般由程序员分配释放,若程序不是放,程序结束时可能由操作系统回收,也可能导致内存泄漏,一般必须手动释放
  • 注意:与数据结构中的堆是两回事,分配方式类似链表

代码举例

class A{
public:
	//这里定义了一个有参构造函数,参数为i并给其赋了缺省值为1
	//也就是说,如果给这个构造函数传入控制,则i的值为1
	//在构造函数中,我们把i的值传给了成员变量m_i
	A(int i = 0){
		m_i = i;
	}
private:
	int m_i;
}

int main(void){
	//在栈区创建对象
	A a1;//调用无参构造
	//下面两种写法效果都可以认为是一样的
	A a1(1);
	A a1 = A(1)

	//在栈区创建A列列表
	A arr1[3];
	A arr2[3] = {A(1), A(2), A(3)};

	//在堆区创建对象
	A* a2 = new A;//构造函数不传参
	A* a3 = new A(2);//构造函数传参
	//在栈区释放内存
	delete a2;
	delete a3;

	//在堆区创建A列列表
	A* arr3 = new A[3];
	A* arr4 = new A[3]{A(1), A(2), A(3)};
	//在堆区释放内存
	delete[] arr3;
	delete[] arr4;//注意:释放堆区的数组时,要加上[]
	
}

1.3 缺省构造函数

如果写一个类的时候,没有定义任何构造函数,为了保证类对象能正常创建出来,
编译器会为其提供一个缺省构造函数。

  • 对于基本类型成员变量,不做初始化
  • 对于类类型的成员变量(成员子对象),将自动调用相应的类缺省构造函数来初始化。

如果,一个类自己定义了构造函数,那么无论是否有参数,编译器都不会再提供缺省构造函数。所以,后面也是没办法调用缺省构造函数的。

class A{
public:
    A(){cout << "无参构造" << endl;m_i = 0;}
    int m_i;
};

class B{
public:
    int m_j;
    int* m_p;
    A m_a;
};

void defCons_test(){
    B b;
    cout << b.m_j << endl;//未知,自己的基本成员变量会被初始化成什么是不确定的。
    cout << b.m_a.m_i << endl;//类类型会调用自己的默认构造函数进行初始化

1.4 类型转换构造函数

一般情况下,当构造函数只有一个参数的时候,都可以看成类型转换构造函数。
这种函数,可以将传入参数的类型转换成目标类类型。

隐式转换

若采用隐式转换形式,编译器会自己去类中寻找能匹配上的构造函数进行调用。以完成类型转换。但是隐式转换代码可读性很不好,一般不建议使用。

显示转换

更推荐使用显示转换,c++风格显示转换的格式和调用类型转换构造函数的格式相同。
同时,在定义类型转换构造函数的时候,前面可以用explicit关键字修饰。表示,这个类型转换只能使用显示类型转换,不能使用隐式类型转换。那么,使用隐式类型转换就会报错。


class Integer{
public:
    Integer(){cout << "Integer()" << endl; m_i = 0;}

    //类型转换构造函数
    //加上explicit表示,类型转换只能显示转换,不能隐式转换
    explicit Integer(int i){
        cout << "Integer(int)" << endl;
        m_i = i+15;
    }
    void print(void){
        cout << m_i << endl;
    }
private:
    int m_i;
};

void castCons_test(){
    Integer i;
    i.print();
    
    //explicit Integer(int)
    //前面有explicit,所有,不能进行隐式的类型转换,顾报错
    //i = 100;//编译器进行了隐式转换
    //编译器会在构造函数中找,最匹配的那个构造函数,调用

    //显示转换(更推荐)
    i = (Integer)200;//c风格显示转换
    i = Integer(300);//c++风格显示转换,和构造函数调用写法一样
    i.print();
}

1.5 拷贝构造函数

用一个已经定义的对象构造同类型的副本对象,将调用该类的拷贝构造函数。

//const修饰,避免传入的对象被修改
类名(const 类型& that){
	//克隆源对象that的副本对象
}


类型 对象1;
类名 对象2(对象1);//拷贝构造
类名 对象2 = 对象1;//和上面一样

class A{
public:
    A(int data = 0):m_data(data){cout << "A(int)" << endl;}
    A(const A& that){
    //注意,拷贝构造函数形参,必须加引用,必须加const
    /*1.为什么必须加引用?
     *如果没有引用,相当于不是穿这个对象的地址,而是传这个对象的副本。
     *既然出现了副本,那是不是要对这个对象拷贝一个副本出来。
     *既然要拷贝一个副本出来,那又要使用拷贝构造了。
     *既然使用拷贝构造,又要传一个对象副本了
     *...
     *死循环了,所以,编译不会通过。
     *2.为什么必须加const?
     *如果不加const,那就形参只是一个普通引用。
     *普通引用只能引用左值。
     *如果传入右值,则会直接报错。
     *所以,必须加const,将形参变为右值引用(万能引用)
     */
        cout << "A(const A&)" << endl;
        m_data = that.m_data;
    }
    int m_data;
};

void coyCons_test(){
    A a1(100);
    //A a2(a1);
    A a2 = a1;//和上一行意思一样
    cout << a1.m_data << endl;
    cout << a2.m_data << endl;
}

1.6 缺省拷贝构造函数(浅拷贝)

如果我们在类中没有定义拷贝构造函数,但是后面使用了拷贝构造。
那么编译器会给我们提供一个缺省的拷贝构造函数。
缺省的拷贝构造函数是浅拷贝的。

  • 对基本类型成员变量,按字节复制
  • 对类类型成员变量,调用类成员自己的拷贝构造函数

class A{
public:
    A(int data = 0):m_data(data){cout << "A(int)" << endl;}
    A(const A& that){
        cout << "A(const &A)" << endl;
        m_data = that.m_data;
    }
    int m_data;
};

class B{
public:
    A m_a;//成员子对象

};
void cpCons_test(){
    /*编译器处理逻辑:
     *如果,是用无参构造创建对象,
     *那么,对象中的类成员变量也是用自己的无参构造进行创建。
     *如果,使用拷贝构造创建对象,
     *呢么,对象中的类成员变量也是用自己的拷贝构造进行创建。
     */
    B b1;//调用B缺省无参构造函数
    cout << b1.m_a.m_data << endl;//0
    B b2(b1);//拷贝构造
    cout << b2.m_a.m_data << endl;//0

}

拷贝构造函数调用时机(笔试会考)

  • 用已定义的对象作为同类型对象的构造实参
  • 以对象的形式向函数传递参数
  • 丛函数中返回对象(有时候会因为编译优化而被省略)

拷贝构造过程风险高效率低,设计时应该尽可能避免

  • 避免或减少对象的拷贝
  • 传递对象形式参数时候,使用引用型参数
  • 丛函数中返回对象时候,使用引用函数返回值

一个笔试题例子:

class A{
public:
    A(){
        cout << "A的无参构造" << endl;
    }
    A(const A& that){
        cout << "A的拷贝构造函数" << endl;
    }
};

void func(A a){}

A bar(){
    A a;
    cout << "&a=" << &a << endl;
    return a;
}

void bishi_cpConst_test(){
    //问:代码执行期间会调用多少次构造函数?6次
    A a1;//1,无参构造
    A a2 = a1;//2,拷贝构造
    func(a1);//3,func的形参不是引用,所以,传入的是对象的副本,所以,调用一次拷贝构造
    A s3 = bar();//编译器优化,会优化两次
    //6,返回值是A,不是引用,所以,返回副本,调用一次拷贝构造
    //bar函数内部还创建了一个A的对象,调用一次无参构造
    //最后,这个语句本身是拷贝构造,将bar()返回的对象拷贝构造给了s3
    cout << "&s3=" << &s3 <<endl;
    //全局变量s3就是局部变量a的引用,两个指向同一块地址,这是编译器优化的结果。
}

1.7 深拷贝构造函数 (深拷贝)

先说什么是浅拷贝

类中缺省的拷贝构造函数,对指针形式的成员变量按照字节复制,不会复制指针所指向的内容,这种拷贝方式被称为浅拷贝。

看下面的例子,第一个对象实例,i1。
他的成员变量m_pi是个指向int的指针。
我们让这个指针指向地址为1001的int,这个int里面存的是100。

第二个对象i2,它由i1浅拷贝构造而来,
i2也有个成员变量m_pi也是个指针。

由于是浅拷贝构造,i2的成员变量m_pi会复制值i1中m_pi的内容(图中step1)。
那i1中存的是什么呢?是100的地址1001。(这个地址是我编的,为了好理解)
也就是说,i1中的m_pi和i2中的m_pi都指向了同一个地址的100。

请添加图片描述

会出现这么两种情况

  • 我通过i1的m_pi和i2的m_pi都可以改变存在1001地址上的100
  • 1001这个内存会被释放两次,释放i1时一次,释放i2时一次
    • 也就是我们常说的double free

深拷贝

为了避免浅拷贝的问题,必须自己定义拷贝构造函数,针对指针形式的成员变量,实现对指针指向内容的赋值,即深拷贝。
也就是说,如果,你定义的类中成员变量有指针,那就必须使用深拷贝,浅拷贝是哒咩的。

那深拷贝要完成什么功能呢?

就行下面图中所展示的。
i1对象有个成员变量m_pi是个指针。
指向的地址为1001,这个地址上存的数字是100。
然后,我要通过i1拷贝构造一个i2,但是是深拷贝。

请添加图片描述

深拷贝这样操作:

  • 分配一块新内存,地址为1002,让i2的m_pi指向这块内存
  • 把i1的m_pi指向的1001上存的100复制到i2中m_pi所指的内存上

好处:

  • i1中m_pi和i2中m_pi指向了不同的地址,这样不会相互干扰,也不会double free
  • i1中m_pi所指向的地址存的值复制到了i2的m_pi所指的地址上,完美。

![[深拷贝.png]]

class Integer{
public:
    Integer(int i = 0){
        m_pi = new int(i);//类里面在堆区分配了内存,但是用户如果不知道,就忘记销毁了
    }
    ~Integer(void){
        cout << "析构函数" << m_pi << endl;
        delete m_pi;
    }
    /*
    Integer(const Integer& that){
        cout<< "缺省浅拷贝构造" << endl;
        m_pi = that.m_pi;
    }*/

    //自定义深拷贝
    Integer(const Integer& that){
        cout <<"自定义深拷贝构造"<< endl;
        m_pi = new int;//先给新的对象的成员变量分配一块内存(分配新的地址)
        *m_pi = *that.m_pi;//再进行复制运算
    }
    void print(void)const{
        cout << *m_pi << endl;
    }
    void Set(){
        *(this->m_pi) =199;
    }
private:
    int* m_pi;
};

void constructor_test(){
    Integer i1(100);
    Integer i2(i1);

    i1.print();
    i2.print();
    i2.Set();
    i1.print();
    i2.print();
}

1.8 拷贝赋值

为什么要说拷贝赋值呢?
因为拷贝赋值和拷贝构造一样,也有深赋值和浅赋值,原理也是一样的,所以一起分析。

浅赋值问题

  • 不同之间数据共享
  • double free
  • 内存泄漏
    • 比如,先初始化一个对象,给这个对象成员分配了内存地址,即有个指针指向他
    • 然后,让这个对象的的成员指针指向另一个成员变量
    • 那原来那个变量就没有指针指向了,我们也没有进行free操作

所以这时候,我们要自己定义深拷贝赋值函数

#include <iostream>
using namespace std;

int main(void){
	int i1 = 100;
	int i2 = 0;
	i2 = i1;//赋值运算符,如果看成是一个函数,那这个函数的返回值就是等号左边的
	//返回结果是个左值(左值是非const的,和右值const区分)
	(i2 = i1) = 300;//这样写是没有问题的
	//相当于先给i2赋值成100,再对i2赋值300
	i2 = 100;
	cout << i1 << "," << i2 << endl;
}

自定义深拷贝赋值操作符函数

A& operator=(const A& that){
	if(this != &that)//防止自赋值
	{
		//释放旧资源,因为我们要赋值的可能和旧内存大小不一样
		//分配新资源
		//拷贝新数据
	}
	return *this;
}

class Integer{
public:
    Integer(int i = 0){
        m_pi = new int(i);//类里面在堆区分配了内存,但是用户如果不知道,就忘记销毁了
    }
    ~Integer(void){
        cout << "析构函数" << m_pi << endl;
        delete m_pi;
    }
    /*
    Integer(const Integer& that){
        cout<< "缺省浅拷贝构造" << endl;
        m_pi = that.m_pi;
    }*/

    //自定义深拷贝构造
    Integer(const Integer& that){
        cout <<"自定义深拷贝构造"<< endl;
        m_pi = new int;//先给新的对象的成员变量分配一块内存(分配新的地址)
        *m_pi = *that.m_pi;//再进行复制运算
    }

    //i3 = i2 -> i3.operator=(i2)
    //形参用const修饰,表示可以传入左值,也可以传入右值,也就是可以直接传个int
    //形参是引用,为了提升效率
    //返回值是个引用表示返回这个对象本身,而不是副本
    /*Integer& operator=(const Integer& that){
        cout << "缺省拷贝赋值函数" << endl;//也是浅拷贝
        m_pi = that.m_pi;
        return *this;//返回调用这个函数的对象自身
    }*/

    //深拷贝赋值函数
    Integer& operator=(const Integer& that){
        cout << "自定义拷贝赋值函数" << endl;
        if(this != &that ){
            delete m_pi;//释放旧内存
            m_pi = new int;//分配新内存
            *m_pi = *that.m_pi;//赋值
        }
        return *this;//返回自引用
    }
    void print(void)const{
        cout << *m_pi << endl;
    }
    void Set(){
        *(this->m_pi) =199;
    }
private:
    int* m_pi;
};

void constructor_test(){
    Integer i1(100);
    Integer i2(i1);
    Integer i3;
    i3.print();

    //对于两个自定义的类,编译器不知道怎们对其进行赋值=操作
    //编译器会为其提供一个函数:operator=
    //这个函数如果咱没自己定义,编译器会为咱们提供的缺省函数
    //下面代码相当于i3.operator=(i2)
    //其中i3是函数的调用者,i2是传入函数的参数(左调右参)
    i3 = i2;//i3=100,拷贝赋值(浅)
    //i3.operator=(i2);//这一行和上面意思相同

    i1.print();
    i2.print();
    i2.Set();//将i2=199
    i1.print();
    i2.print();
    
    i3.print();//按理说应该是100,但是是199,改变i2就会改变i3
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/17514.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

UE5笔记【三】UE5材质Materials

材质&#xff1a;可以将材质看作是StaticMesh上面的绘画。这副绘画Paint是由图层组成的&#xff0c;这些图层形成了所谓的物理基础渲染&#xff08;Physically Based Rendering OR PBR&#xff09;。这些PBR的特殊之处在于&#xff1a;几乎可以让我们模拟显示世界中的任何材质。…

「Redis数据结构」QuickList

「Redis数据结构」QuickList 文章目录「Redis数据结构」QuickList一、前言二、概述三、结构四、小结一、前言 在前面一章&#xff0c;我们已经学习了ZipList压缩列表&#xff0c;ZipList虽然节省内存&#xff0c;但也引发了不少问题。 问题1&#xff1a;ZipList虽然节省内存&am…

【Bio】基础生物学 - 细胞 cell

文章目录1. 细胞2. 原核细胞 真核细胞3. 细胞器4. 细胞核5. 动物细胞5.1 细胞质5.2 核糖体5.3 内质网6. 植物细胞6.1 液泡6.2 线粒体6.3 叶绿体Ref1. 细胞 生命系统的结构层次依次为&#xff1a; 细胞 (cell)\blue{\text{细胞 (cell)}}细胞 (cell) →\rightarrow→ 组织 (tiss…

玩转MySQL:程序中的“田氏代齐”,InnoDB为何能替换MyISAM?

引言 MySQL是一款支持拔插式引擎的数据库&#xff0c;在开发过程中你可以根据业务特性&#xff0c;从支持的诸多引擎中选择一款适合的&#xff0c;例如MyISAM、InnoDB、Merge、Memory(HEAP)、BDB(BerkeleyDB)、Example、Federated、Archive、CSV、Blackhole..... 不过虽然各款…

[附源码]java毕业设计全国人口普查管理系统论文

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

LeetCode刷题复盘笔记—一文搞懂746. 使用最小花费爬楼梯(动态规划系列第二篇)

今日主要总结一下动态规划的一道题目&#xff0c;746. 使用最小花费爬楼梯 题目&#xff1a;746. 使用最小花费爬楼梯 题目描述&#xff1a; 给你一个整数数组 cost &#xff0c;其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用&#xff0c;即可选择…

Linux之关于Nginx

目录 1、什么是Nginx&#xff1f; 1.1、负载均衡&#xff1a;流量分摊​编辑 1. 2、反向代理 &#xff1a;处理外网访问内网问题 1.3、动静分离:判断动态请求还是静态请求&#xff0c;选择性的访问指定服务器 2、Nginx的使用 2.1.Nginx安装 2.1.1 添…

11月27日PMI认证才聚各考点防疫要求,PMP考生必看

11月27日深圳才聚、珠海才聚、东莞才聚、南宁才聚防疫要求及如下&#xff1a; 注意&#xff1a;由于疫情防控影响&#xff0c;以下城市的考试将延期举办&#xff0c;该考点的考生无需做任何操作。 北京、天津、石家庄、廊坊、保定、哈尔滨、大庆、呼和浩特、太原、郑州、兰州…

【王道计算机网络笔记】计算机网络体系结构-计算机网络概述

文章目录计算机网络的概念计算机网络的功能计算机网络的组成计算机网络的分类标准化工作及相关组织相关组织计算机网络的性能指标速率带宽吞吐量时延时延带宽积往返时延RTT利用率计算机网络的概念 计算机网络&#xff1a;是一个分散的、具有独立功能的计算机系统&#xff0c;通…

[附源码]java毕业设计汽车租赁系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

绿竹生物获上市“大路条”:融资不搞研发去理财,孔健下什么棋?

11月19日&#xff0c;绿竹生物发布消息称&#xff0c;该公司于2022年11月11日获得中国证监会关于首次公开发行境外上市外资股&#xff08;H股&#xff09;及境内未上市股份全流通&#xff08;即“大路条”&#xff09;的批复&#xff0c;下一步将根据香港联交所的聆讯进度安排及…

【JavaSE】接口

前言&#xff1a; 作者简介&#xff1a;爱吃大白菜1132 人生格言:纸上得来终觉浅&#xff0c;绝知此事要躬行 如果文章知识点有错误的地方不吝赐教&#xff0c;和大家一起学习&#xff0c;一起进步&#xff01; 如果觉得博主文章还不错的话&#xff0c;希望三连支持&#xff01…

web课程设计网页规划与设计----公司官网带轮播图 (页面精美 1页)

⛵ 源码获取 文末联系 ✈ Web前端开发技术 描述 网页设计题材&#xff0c;DIVCSS 布局制作,HTMLCSS网页设计期末课程大作业 | 公司官网网站 | 企业官网 | 酒店官网 | 等网站的设计与制 | HTML期末大学生网页设计作业&#xff0c;Web大学生网页 HTML&#xff1a;结构 CSS&#…

垃圾回收相关概念概述(宋红康JVM学习笔记)

System.gc() 在默认情况下&#xff0c;通过System.gc()或者Runtime.getRuntime().gc()的调用&#xff0c;会显式触发Full GC&#xff0c;同时对老年代和新生代进行回收&#xff0c;尝试释放被丢弃对象占用的内存。 然而System.gc()调用附带一个免责声明&#xff0c;无法保证对…

java 基于springBoot上传文件/文件夹使用实例

最近项目中遇到一个文件批量上传的需求&#xff0c;对单个的文件、多文件或者文件夹的方式上传文件都可以满足要求&#xff0c;总结一下使用经验。 案例基于springBoot. 1、文件上传请求 这里postman测试了单文件和多文件的上传&#xff0c;同时测试了文件件方式上传。 postman…

物联网开发笔记(48)- 使用Micropython开发ESP32开发板之控制OLED ssd1306屏幕

一、目的 这一节我们学习如何使用我们的ESP32开发板来控制OLED ssd1306屏幕&#xff0c;此处使用的是I2C协议&#xff0c;大家可自行百度学习一下I2C。 二、环境 ESP32 OLED ssd1306屏幕 Thonny IDE&#xff08;或者WOKWI在线仿真&#xff09; 几根杜邦线 本次使用在线仿真…

vue 项目在加载完成之前,显示预置加载动画

vue 项目在加载完成之前&#xff0c;显示预置加载动画 自己有一个日记项目&#xff0c;由于服务器带宽很小1MB&#xff0c;在加载之前页面中显示是空白的&#xff0c;就想给它加个前置的动画&#xff0c;这个用户体验更好。 一、实现 1. 定义项目入口 如果你是 pwa 应用&am…

[附源码]计算机毕业设计JAVA化妆品销售管理系统

[附源码]计算机毕业设计JAVA化妆品销售管理系统 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM myba…

SQL语句的约束 总结

目录 基本概念 主键约束 概念 操作 自增长约束 概念 操作 非空约束 概念 操作 唯一约束 概念 操作 默认约束 概念 操作 零填充约束 概念 约束总结 基本概念 主键约束 概念 主键约束相当于 唯一约束 非空约束 的组合&#xff0c;主键约束列不允许重复&am…

http网站升级为https网站,证书、http-flv视频显示处理

一、使用OpenSSL生成自签名证书 升级https网站需要自签名证书&#xff0c;证书在视频服务器Nginx中也需要&#xff0c;使用OpenSSL生成。 1.下载安装OpenSSL 2.以管理员身份运行cmd进入OpenSSL的安装目录查看安装版本 3.生成自签名证书。 生成私钥&#xff1a;openssl genr…