C++ 复制构造函数

news2024/11/19 4:31:09

在讲解 C++ 的复制构造函数之前这里先明确一个概念,C++ 的复制构造函数的意思并不是字面上的意思复制一个构造函数,而是有一种专门用于复制内容的构造函数被叫做复制构造函数。

复制构造函数对于 C++ 来说是非常重要的概念,所以我们必须掌握并牢记它,下面我们就一起来逐步的了解复制构造函数。

1. 前言

已经学习过 C 语言或已经学习过 C++ 基础语法函数部分内容的小伙伴们因该都知道我们在给函数传递参数时需要先将实参复制到函数的形参,函数内部再使用形参当中的值,注意这种复制规则也适用于对象(类的实例)类型数据。下面来看一个函数参数传递的简单例子,计算圆的面积,如下代码。

double circleArea(double r)
{
    return 3.14 * r * r;
}

例如 circleArea(10) 先将实参 10 复制到函数的形参 r 中,函数内部再使用形参 r 中的值计算面积。

2. 复制类对象

对于普通类型的数据对象来说,它们之间的复制很简单,直接复制当然不会出现问题。但是对象类型与普通类型不同,类对象内部结构一般较为复杂,存在各种类型的成员变量,所以 C++ 对象类型的复制需要借助 复制构造函数,下面我们来看一个简单的对象类型复制的例子。

#include<iostream>

using namespace std;

class Animal {
private:
    int foot;
    int tail;

public:
    // 普通构造函数
    Animal(int i, int j) {
        foot = i;
        tail = j;
    }

    void show() {
        cout << "foot:" << foot << endl;
        cout << "tail:" << tail << endl;
    }
};

int main()
{
    Animal dog(4, 1);
    Animal cat = dog;
    cat.show();

    return 0;
}

实例化对象 dog 并使用数值 4 和 1 来初始化私有实例变量 foottail,再定义对象类型变量 cat,并将实例对象 dog 复制到对象类型变量 cat 中(注意在这里是复制而不是赋值到 cat,后面会说明这两者的区别)。

运行程序,最终函数输出 4 和 1。从运行结果可以看出,系统为对象 cat 分配了内存并完成了与对象 dog 的复制过程。

3. 认识复制构造函数

前面已经说了,类对象的复制需要借助复制构造函数,但是在前面的程序中并没有复制构造函数,但对象以及其内部成员变量确实复制成功了,这是怎么回事呢?

这是因为在我们没有显示的定义复制构造函数的情况下,编译器会给我们自动产生一个复制构造函数,这就是 默认拷贝构造函数。如果我们编写了复制构造函数,则编译器就不会再给我们生成默认的复制构造函数了。

3.1 复制构造函数定义

复制构造函数是一种特殊的构造函数,专门用于数据的复制,复制构造函数的名称必须和类名称一致(这一点和普通的构造函数一样),它必须的一个参数是本类型的一个 引用变量

class Animal {
public:
    Animal(const Animal & animal) {

    }
};

3.2 默认复制构造函数

默认复制构造函数很简单,仅仅是实现了从源对象到目标对象逐个字节的复制,即使得目标对象的每个成员变量都变得和源对象相等,如下所示。

#include<iostream>

using namespace std;

class Animal {
private:
    int foot;
    int tail;

public:
    // 普通构造函数
    Animal(int i, int j) {
        foot = i;
        tail = j;
    }

    // 复制构造函数
    Animal(Animal & animal) {
        foot = animal.foot;
        tail = animal.tail;
    }

    void show() {
        cout << "foot:" << foot << endl;
        cout << "tail:" << tail << endl;
    }
};

int main()
{
    Animal dog(4, 1);
    Animal cat = dog;
    cat.show();

    return 0;
}

程序中函数 Animal(Animal & animal) {} 就是我们自定义的构造函数,虽然这个复制构造函数是我们自定义的,实际上在我们没有定义复制构造函数时编译器给我们生成的默认复制构造函数就是这样的。

所以程序中我们定义的复制构造函数就是默认复制构造函数,只是我们将这个默认复制构造函数显式定义了。

4. 浅拷贝与深拷贝概念

4.1 概念理解

在讲解浅拷贝与深拷贝之前我们需要先来了解浅拷贝和深拷贝的概念,要理解概念我们可以通过一个 C 语言例程来讲解,如下。

#include <stdio.h>

unsigned char * str = NULL;

void speak_init(unsigned char * s)
{
    str = s;
}

void speak()
{
    printf("%s\n", str);
}

int main()
{
    speak_init("Hello, World!");
    speak();
}

函数 speak_init() 用于初始化函数 speak() 要输出的内容,将形参指针 s 指向的内容 Hello, World! 的首地址复制到指针 str 中。最后函数 speak() 输出内容。

但是这里有一个问题,就是 speak_init() 执行完后,形参指针 s 就被销毁,指针 s 指向的地址变为无效。这就导致函数 speak() 使用指针 str 访问无效地址上的内容而发生错误。

所以正确的做法是开辟一段足够并且不会被别人销毁的存储空间,将可能会被销毁地址上的内容复制到这个空间中。后续需要访问这些内容时通过访问这个空间内的地址来访问内容,如下。

#include <stdio.h>
#include <string.h>

unsigned char str[16];

void speak_init(unsigned char * s)
{
    memset(str, '\0', 16);    
    strcpy(str, s);
}

void speak()
{
    printf("%s\n", str);
}

int main()
{
    speak_init("Hello, World!");
    speak();
}

所以浅拷贝问题并不是 C++ 的特色,仔细追究起来这还是从 C 语言身上继承而来的问题呢,谁叫 C++ 是 C 语言的扩展呢

4.2 概念总结

浅拷贝:浅拷贝只复制指向某个对象的指针(即只复制内容的首地址),而不复制具体的内容,新旧对象还是共享同一块内存,如果内容地址随着旧对象被销毁而无效,新对象也将无法继续访问地址上的内容。

深拷贝:深拷贝会另外开辟存储空间,并将新的对象内容拷贝到该存储空间下,新对象跟原对象不共享内存,此时旧对象的销毁也不会影响到新对象的内容。

所以浅拷贝问题主要体现在指针类型拷贝的场景下,普通数据类型进行一对一的复制是没有问题的。

5. 浅拷贝问题分析

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

// 复制构造函数
Animal::Animal(Animal & animal) {
    foot = animal.foot;
    tail = animal.tail;
}

5.1 浅拷贝

class Rect {
private:  
    int width;
    int height;
    int * p; // 指针成员

public:
    Rect() { // 构造函数,p 指向堆中分配的一空间,并赋值 100。
        p = new int(100);
    }

    ~Rect() { // 析构函数,释放动态分配的空间
        if (p != NULL)
            delete p;
    }
};

int main()  
{  
    Rect rect1;  
    Rect rect2 = rect1; // 复制对象
    return 0;  
}

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

在运行定义 rect1 对象后,由于在构造函数中有一个动态分配的语句,因此执行后将开辟一段内存。
在使用 rect1 复制 rect2 时,由于执行的是浅拷贝,只是将成员的值进行赋值,这时 rect1.p = rect2.p,也就是这两个指针指向了同一个空间。

请添加图片描述

在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是运行错误的原因。我们想要的结果当然不是两个指针有相同的地址值(没有人会关心数据所在的地址),而是希望两个指针指向的空间有相同的内容,解决办法就是使用 深拷贝

5.2 深拷贝

对象复制操作会执行类的复制构造函数,我们在复制构造函数中会为新对象的指针成员变量分配内存空间,并将旧对象的指针成员变量所指向的内容复制到我们给当前对象的指针成员变量新分配的缓冲区中,程序如下。

class Rect {
private:  
    int width;
    int height;
    int * p; // 指针成员

public:
    Rect() { // 构造函数,指针 p 指向堆中分配的一空间,并赋值 100。
        p = new int(100);
    }

    Rect(const Rect & r) { // 复制构造函数
        width = r.width;
        height = r.height;

        p = new int; // 为新对象重新动态分配空间  
        *p = *(r.p); // 将旧对象的值拷贝到新地址上
    }

    ~Rect() { // 析构函数,释放动态分配的空间
        if (p != NULL)
            delete p;
    }
};

int main()  
{  
    Rect rect1;  
    Rect rect2 = rect1; // 复制对象
    return 0;  
}

执行之后对象 rect1 的指针和对象 rect2 的指针各自指向一段内存空间,虽然指向的空间不同但它们指向的空间具有相同的内容,最终执行的拷贝就是 深拷贝

请添加图片描述

6. 调用复制构造函数

6.1 区分对象的复制和赋值

在 C++ 中赋值和复制这两者在语法上是完全相同的,此时我们该如何来区分当前场景是使用复制还是赋值操作。实际上区分很简单,只需要知道 = 号左边的对象是否已经被定义,还是正在被定义。

  • 如果 = 左边的对象正在定义,那么给正在定义的对象赋值就属于初始化对象,初始化对象执行的是复制操作。
Complex c1;
Complex c2 = c1;
  • 如果 = 左边的对象已经定义,对象定义的过程中即使我们没有提供值初始化对象,编译器也将使用一个默认的值初始化对象。所以给定义后的对象赋值就不再属于初始化对象,而是属于修改对象,此时执行的是赋值操作。
Complex c1, c2;
c1 = c2;

6.2 复制构造函数调用场景

(1) 当用一个对象去初始化同类的另一个对象时,会引发复制构造函数被调用。例如,下面的两条语句都会引发复制构造函数的调用,用以初始化 c2。

Complex c2(c1);
Complex c2 = c1;

注意:这两条语句是等价的,都是初始化对象。

(2) 如果函数 Func 的参数是类 Hello 的对象,那么当 Func 被调用时,类 Hello 的复制构造函数将被调用。换句话说,作为形参的对象,是用复制构造函数初始化的,而且调用复制构造函数时的参数,就是调用函数时所给的实参。

#include<iostream>

using namespace std;

class Hello {
public:
    Hello() {

    }

    Hello(Hello & hello) {
        cout << "Copy constructor called" << endl;
    }
};

void Func(Hello hello)
{

}

int main()
{
    Hello hello;
    Func(hello);

    return 0;
}

前面说过,函数的形参的值等于函数调用时对应的实参,现在可以知道这不一定是正确的。如果形参是一个对象,那么形参的值是否等于实参,取决于该对象所属的类的复制构造函数是如何实现的。

以对象作为函数的形参,在函数被调用时,生成的形参要用复制构造函数初始化,这会带来时间上的开销。如果用对象的引用而不是对象作为形参,就没有这个问题了。但是以引用作为形参有一定的风险,因为这种情况下如果形参的值发生改变,实参的值也会跟着改变。如果要确保实参的值不会改变,又希望避免复制构造函数带来的开销,解决办法就是将形参声明为对象的 const 引用。

void Func(const Hello & hello)
{

}

这样使用 const 引用之后,Func 函数中出现任何有可能导致 hello 的值被修改的语句,都会引发编译错误。

(3) 如果函数的返冋值是类 A 的对象,则函数返冋时,类 A 的复制构造函数被调用。换言之,作为函数返回值的对象是用复制构造函数初始化 的,而调用复制构造函数时的实参,就是 return 语句所返回的对象。

#include<iostream>

using namespace std;

class Hello {
public:
    int a;

    Hello(int n) {
        a = n; 
    };

    Hello(const Hello & hello) {
        a = hello.a;
        cout << "Copy constructor called" << endl;
    }
};

Hello Func()
{
    Hello hello(4);
    return hello;
}

int main()
{
    cout << Func().a << endl;
    return 0;
}

调用了 Func 函数,其返回值是一个对象,该对象就是用复制构造函数初始化的, 而且调用复制构造函数时,实参就是第 return 语句所返回的 hello。复制构造函数确实完成了复制的工作,所以 Func 函数的返回值和 Func 内部的 hello 对象相等。

7. 防止默认复制发生

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

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

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

相关文章

jsp设备信息查询系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 jsp设备信息查询系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为Mysql&#xff0c;使用…

【算法笔记(六)】检索算法

算法笔记(六) 检索算法算法笔记(六)前言一、线性查找1.什么是线性查找2.需求规则3.人工图示演示4.代码实现二、二分查找1.什么是二分查找2.需求规则3.人工图示演示4.代码实现三.插值查找1.什么是插值查找2.需求规则3.人工图示演示4.代码实现四.斐波那契查找1.什么是斐波那契查找…

[附源码]计算机毕业设计基于springboot框架的资产管理系统设计与实现

项目运行 环境配置&#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…

TCP三次握手四次挥手及常见问题解决方案

一、重要概念 位码即tcp标志位,有6种表示: SYN(synchronous建立连接) ACK(acknowledgement 表示响应、确认) PSH(push表示有DATA数据传输) FIN(finish关闭连接) RST(reset表示连接重置) URG(urgent紧急指针字段值有效) 二、三次握手四次挥手流程图 三、三次握手 第一次握手&am…

一段有关MPU配置代码的几个小疑问

当我们阅读一些STM32F7或STM32H7系列芯片例程&#xff0c;或者基于这两类芯片通过cubeMx进行配置并用到MPU功能时&#xff0c;往往会在代码里看到下面这段MPU配置。对这段代码可能有人有些疑问&#xff0c;这里重点一起聊聊其中的3个&#xff0c;供参考。第一个疑问&#xff0c…

【AI工程论文解读】04-通过Ease.ML/CI实现机器学习模型的持续集成(上)

作者&#xff1a;王磊 更多精彩分享&#xff0c;欢迎访问和关注&#xff1a;https://www.zhihu.com/people/wldandan 持续集成是一种软件开发实践&#xff0c;即团队开发成员经常集成他们的工作&#xff0c;通常每个成员每天至少集成一次&#xff0c;也就意味着每天可能会发生多…

机器人xacro设计+gazebo/rviz启动

机器人xacro设计gazebo/rviz启动项目需求方案机器人的本体设计机器人本体集成car_gazebo.xacroinertial惯性矩阵 head.xacro小车底盘base.xacro摄像头camera.xacro雷达laser.xacro机器joint关节控制器传感器sensor设计机器人joint关节控制器move.xacro摄像头传感器camera_senso…

Metabase学习教程:系统管理-7

使用MetabaseAPI MetabaseAPI简介。 本文介绍如何使用Metabase的API。我们自己使用该API连接前端和后端&#xff0c;因此您可以编写Metabase几乎可以执行的所有操作。 警告&#xff1a;MetabaseAPI可能会更改 开始之前有两个注意事项&#xff1a; API可能会更改。API与前端…

软件测试要学会哪些东西才能拿2w+的工资?

软件开发人员的月薪达到2万还是比较轻松的&#xff0c;但是软件测试人员想要月薪过万的话&#xff0c;我认为可以从两个方面去考虑&#xff1a; 1. 一种就是项目的测试负责人&#xff1a;测试人员需要对软件的整体性能改进提出建设性方案&#xff0c;所以很多软件测试人员最终…

数据结构和算法之《栈》详解

标题&#xff1a;栈的思路及代码实现 作者&#xff1a;Ggggggtm 寄语&#xff1a;与其忙着诉苦&#xff0c;不如低头赶路&#xff0c;奋路前行&#xff0c;终将遇到一番好风景 文章目录&#xff1a; 一、栈的概念及结构 1、1 栈的概念 1、2 栈的结构 二、栈的思路及代码实现详解…

电脑技巧:推荐5个非常实用的软件

❤️作者主页&#xff1a;IT技术分享社区 ❤️作者简介&#xff1a;大家好,我是IT技术分享社区的博主&#xff0c;从事C#、Java开发九年&#xff0c;对数据库、C#、Java、前端、运维、电脑技巧等经验丰富。 ❤️个人荣誉&#xff1a; 数据库领域优质创作者&#x1f3c6;&#x…

uniapp 之使用 u-upload 组件来实现图片上传

uniapp 之使用 u-upload 组件来实现图片上传前言一、官方示例用法分析二、关闭自动上传,使用手动上传的方式,代码html 代码js 代码css 代码总结分析前言 在使用 uniapp 开发的微信小程序中使用了图片上传功能,使用了 uniapp 的图片上传组件 注意&#xff1a;我这里后端接口接收…

小程序开发平台

小程序开发平台顾名思义就是一个可以开发小程序的地方。 小程序开发平台&#xff1a;【电脑浏览器输入3M.FKW.COM了解详情】 适合群体&#xff1a;企业、机构、个体户 小程序开发方式&#xff1a; 自建——可以通过套用小程序模板&#xff0c;利用拖拽式小程序开发工具&…

某组态软件工程文件加密机制探究

某组态软件工程文件加密机制探究 前言 在工业自动化控制领域&#xff0c;组态软件是数据采集与过程控制的专用软件&#xff0c;是实现人机交互必不可少的工具。工程设计人员使用组态软件在PC机上进行工程画面组态的编辑&#xff0c;然后把编译后的组态逻辑通过以太网或串口下载…

HSRP协议(思科私有)/VRRP协议(公有)

数据来源 1、HSRP热备份路由协议&#xff08;备份网关&#xff09; 出现背景&#xff1a; 如下图一个公司拉两条网线一条用来备份网关是192.168.0.253&#xff0c;平时用的网关是254&#xff0c;如果网关是254的这条网线出问题了就可以使用备份不影响公司让人员上网&#xff…

2022最新版 Java 学习线路图

第 1 阶段 - 企业级开发 - java 基础 学习掌握本阶段的内容&#xff0c;可以实现诸如迅雷下载软件、QQ 聊天客户端、中小网站&#xff0c;例如&#xff1a;小型旅游网站、小型电商网站的开发 第 2 阶段 - 企业及开发 - 基础框架 学习掌握本阶段内容&#xff0c;可以快速、规范的…

CRC校验——以SHT4xA温湿度传感器为例

CRC校验——以SHT4xA温湿度传感器为例一、简介二、计算方法&#xff08;一&#xff09;步骤&#xff08;二&#xff09;参考代码&#xff08;C语言&#xff09;&#xff08;三&#xff09;检验&#xff1a;CRC(0xBEEF) 0x92三、参考一、简介 循环冗余校验码&#xff08;CRC&am…

[附源码]计算机毕业设计JAVA疫情防控下高校教职工健康信息管理系统

[附源码]计算机毕业设计JAVA疫情防控下高校教职工健康信息管理系统 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&…

试剂盒和示踪剂—艾美捷FLIVO探针活体凋亡检测分析

针对FLIVO(FLuorescence in vIVO)探针的研究&#xff0c;本篇文章推荐艾美捷ImmunoChemistry&#xff08;ICT&#xff09;FLIVO探针系列的&#xff1a;天冬氨酸蛋白酶(Caspases)活性检测试剂盒&#xff0c;及 FLIVO示踪剂&#xff0c;主要用于细胞凋亡活体检测&#xff0c;助力…

大型分布式系统下缓存应该怎么玩,才能用得高效

大家好&#xff0c;今天我们来聊一聊在大型分布式系统中&#xff0c;缓存应该怎么玩&#xff0c;从毕业到现在也有三年多了&#xff0c;大大小小的系统也经历了几十个&#xff0c;今天就从各个角度来讨论一下&#xff0c;我们的不同的缓存应该怎么玩&#xff0c;才能用的高效。…