C++ 类和对象 拷贝构造函数

一 拷贝构造函数的概念:

拷贝构造函数是一种特殊的构造函数,用于创建一个对象是另一个对象的副本。当需要用一个已存在的对象来初始化一个新对象时,或者将对象传递给函数或从函数返回对象时,会调用拷贝构造函数。

二 拷贝构造函数的特点:

1:拷贝构造函数是构造函数的一个重载形式。

2:拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。

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

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

4:编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗? 当然像日期类这样的类是没必要的。

2.1 代码示例:

class Time 
{
public:
    // 普通构造函数
    Time(int hour = 0, int minute = 0, int second = 0) 
    {
        _hour = hour;
        _minute = minute;
        _second = second;
    }

    // 拷贝构造函数,使用引用传递
    Time(const Time& other) 
    {
        _hour = other._hour;
        _minute = other._minute;
        _second = other._second;
    }

    void Print() const 
    {
        std::cout << _hour << ":" << _minute << ":" << _second << std::endl;
    }

private:
    int _hour;
    int _minute;
    int _second;
};

int main()
{
    Time t1(10, 20, 30);   // 使用普通构造函数

    //构造函数的重载
    Time t2 = t1;          // 使用拷贝构造函数
   
    //Time t2(t1);        // 拷贝构造的另一种写法

    t1.Print();
    t2.Print();
    return 0;
}

输出:

2.2 为什么要使用引用呢?

我们在 increment 函数中改变x的值并没有间接性改变a,这是因为传过去的只是编译器创建实参的一个副本,而修改副本怎么可能可以改变a呢?

#include <iostream>

void increment(int x) 
{
    x = x + 1;  // 修改的是副本,不影响实参
}

int main() 
{
    int a = 5;
    increment(a);  // 传递a的副本
    std::cout << a << std::endl;  // 输出5,原始值a未被修改
    return 0;
}

知道传值传参的本质之后,再来想一想为什么要用引用?咱们先来说说如果没用用引用的后果会是怎么样,当把自定义类型传出去后且不用引用或者指针来接收,它会

调用 Time(const Time other),其中 othert1 的按值传递副本。

为了按值传递,编译器需要创建 other 的副本。

创建 other 的副本时,再次调用 Time(const Time other)

这个新调用的 Time(const Time other) 又需要创建自己的 other 副本,再次调用 Time(const Time other)

如此反复,导致无限递归调用,最终导致栈溢出。

图:

C++规定,自定义类型的拷贝,都会调用拷贝构造

那为什么要引用呢?

首先我们来回顾一下引用 :

1:引用是现有变量的另一个名字。

2:它们不创建新对象,只是指向已有对象。

3:引用只是指向现有对象,不创建新副本

因为引用就是它本身,所以何来创建新副本这一说法,创建新副本是怕改变副本从而导致改变实参值

2.3 总结:

1:按值传递会递归:每次传递对象会复制对象,导致无限递归。

2:引用传递避免递归:引用只是指向对象本身,不会复制对象

三 默认拷贝构造:

当你没有显式定义拷贝构造函数时,编译器会为你自动生成一个默认的拷贝构造函数。这个默认拷贝构造函数会逐个拷贝对象的所有成员变量。

3.1 内置类型与自定义类型的拷贝:

内置类型:如 int, char, float 等,拷贝时直接按照字节方式进行复制,也就是直接复制其值。

自定义类型:如类和结构体,拷贝时会调用该类型的拷贝构造函数。

3.2 代码示例:

内置类型:

#include <iostream>

class MyClass 
{
public:
    int x;  // 内置类型成员
};

int main() 
{
    MyClass obj1;
    obj1.x = 10;

    MyClass obj2 = obj1;  // 使用编译器生成的默认拷贝构造函数

    std::cout << "obj1.x: " << obj1.x << std::endl; 
    std::cout << "obj2.x: " << obj2.x << std::endl;

    return 0;
}

输出:

对于一个类里面只有内置类型成员那编译器生成的默认拷贝构造会自动复制其值。

自定义类型:

#include <iostream>

class Time 
{
public:
    // 默认构造函数
    Time() 
    {  
        _hour = 0;
        _minute = 0;
        _second = 0;
    }

    // 拷贝构造函数
    Time(const Time& other) 
    {
        _hour = other._hour;
        _minute = other._minute;
        _second = other._second;
        std::cout << "Time::Time(const Time& other)" << std::endl;
    }

private:
    int _hour;
    int _minute;
    int _second;
};

class MyClass 
{
public:
    int x;  // 内置类型成员
    Time t; // 自定义类型成员
};

int main() 
{
    MyClass obj1;
    obj1.x = 10;

    MyClass obj2 = obj1;  // 使用编译器生成的默认拷贝构造函数

    std::cout << "obj1.x: " << obj1.x << std::endl;
    std::cout << "obj2.x: " << obj2.x << std::endl; 

    return 0;
}

当执行MyClass obj2 = obj1; 因obj1类里面有自定义类型 t 所以编译器生成的默认拷贝构造会自动调用Time(const Time& other) 来完成

3.3 总结:

内置类型:编译器默认拷贝构造函数会直接复制其值。

自定义类型:编译器默认拷贝构造函数会调用该类型的拷贝构造函数来复制其内容。

四 内存分区:

要理解好深拷贝与浅拷贝那就得先了解内存是怎么样分区的。

计算机程序运行时,内存通常被分为四个主要区域:栈区、堆区、全局静态区和只读区(常量区和代码区)。

4.1 栈区:

局部变量:函数内部定义的变量。

形参(函数参数):函数定义时的参数。

返回地址:函数调用后的返回地址。

特点:

栈区中访问速度快且栈的内存连续分配

因存储的都是 局部/形参/返回地址 所以栈区空间小,存储的生命周期短

在我们局部变量所在的函数执行完成时,它会自动释放内存

4.2 堆区:

动态分配的数据:通过 newmalloc 等动态分配函数分配的内存。

特点:

因存储的都是new 或者malloc开辟的空间所以堆区空间大,所以访问速度慢

堆中的内存分配和释放是通过指针进行的,可能不是连续的。

堆区的内存需要程序员手动管理,必须手动释放动态分配的内存,否则会导致内存泄漏。

4.3 全区/静态区:

全局变量:在所有函数外部定义的变量。

静态变量:使用 static 关键字定义的变量。

特点:

全局变量和静态变量在程序的整个运行期间一直存在,直到程序结束。

全局变量可以在程序的所有函数中访问,静态变量在声明的作用域内共享

4.4 只读常量区:

常量:程序中定义的常量。

代码:程序的指令代码。

特点:

常量区的数据在程序运行期间不能被修改,保证了数据的安全性和稳定性。

代码区存储程序的指令代码,在程序运行时被载入内存以执行。

五 浅拷贝:

首先我们来回顾C语言里面的基本类型指针类型

5.1 基本类型:

基本类型是C语言内置的数据类型,它们用于存储最基本的数值数据。常见的基本类型包括:int float char……

5.2 指针类型:

指针类型是存储内存地址的数据类型。指针用于指向其他变量或对象在内存中的位置。

5.3 基本类型代码示例:

#include <iostream>

class BasicType 
{
public:
    int value;

    // 构造函数
    BasicType(int v) 
    {
        value = v;
    }

    // 拷贝构造函数
    BasicType(const BasicType& other) 
    {
        value = other.value;
    }
};

int main() 
{
    BasicType obj1(10);
    BasicType obj2 = obj1;  // 浅拷贝,复制基本类型的值

    std::cout << "改变前: " << std::endl;
    std::cout << "obj1.value: " << obj1.value << std::endl;
    std::cout << "obj2.value: " << obj2.value << std::endl;

    obj2.value = 20;  // 修改obj2的值

    std::cout << "改变后: " << std::endl;
    std::cout << "obj1.value: " << obj1.value << std::endl;
    std::cout << "obj2.value: " << obj2.value << std::endl;

    return 0;
}

输出:

值会被复制但修改新对象的值不会影响原对象。

5.3 指针类型代码示例:

#include <iostream>

class SimplePointer 
{
public:
    int* ptr;  // 成员变量 ptr

    // 构造函数
   SimplePointer(int value)
{
    ptr = (int*)malloc(sizeof(int));  // 动态分配内存并初始化
    if (ptr != nullptr) 
    {
        *ptr = value;
    }
}
    SimplePointer(const SimplePointer& other) 
    {
       this->ptr = other.ptr;  // 浅拷贝,复制内存地址
    }

    void print() const 
    {
        std::cout << "Value: " << *ptr << std::endl;
    }
};

int main() 
{
    SimplePointer obj1(10);  // 创建第一个对象,并将值初始化为10
    SimplePointer obj2(obj1);  // 使用拷贝构造函数(浅拷贝)

    // 打印初始值
    std::cout << "Initial values:" << std::endl;
    obj1.print();
    obj2.print();

    // 修改obj2的值
    *obj2.ptr = 20;

    // 打印修改后的值
    std::cout << "After change:" << std::endl;
    obj1.print();
    obj2.print(); 

    return 0;
}

输出:

复制内存地址,共享同一块内存,修改会互相影响

六 深拷贝:

#include <iostream>
#include <cstdlib>
#include <cstring>

class SimpleClass 
{
public:
    int* ptr;

    // 默认构造函数
    SimpleClass(int value) 
    {
        ptr = (int*)malloc(sizeof(int));  // 动态分配内存并初始化
        if (ptr != nullptr) 
        {
            *ptr = value;
        }
    }

    // 深拷贝构造函数
    SimpleClass(const SimpleClass& other) 
    {
        ptr = (int*)malloc(sizeof(int));  // 分配新内存
        if (ptr != nullptr) 
        {
            *ptr = *(other.ptr);  // 复制内容
        }
    }

    // 析构函数
    ~SimpleClass() 
    {
        if (ptr != nullptr) 
        {
            free(ptr);  // 释放内存
        }
    }

    void Print() const 
    {
        if (ptr != nullptr) 
        {
            std::cout << "Value: " << *ptr << std::endl;
        }
    }
};

int main() 
{
    SimpleClass obj1(10);  // 创建对象,ptr 指向的值为 10
    SimpleClass obj2 = obj1;  // 使用深拷贝构造函数

    obj1.Print();
    obj2.Print();

    // 修改 obj2 的值
    if (obj2.ptr != nullptr) 
    {
        *(obj2.ptr) = 20;
    }

    obj1.Print();
    obj2.Print();

    return 0;
}

输出:

深拷贝不仅复制对象的指针成员,还为指针指向的内容分配新的内存,并复制原对象的数据。这样,两个对象拥有独立的内存,修改一个不会影响另一个。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

LabVIEW高能质子束流密度分布测试系统

LabVIEW平台开发的高能质子束流密度分布测试系统。该系统主要应用于电子器件的抗辐射加固试验&#xff0c;旨在精确测量高能质子束的密度分布&#xff0c;以评估电子器件在辐射环境下的性能表现和耐受能力。 系统组成与设计 硬件组成&#xff1a; 法拉第杯探测器&#xff1a;…

自动化测试高级控件交互方法:TouchAction、触屏操作、点按,双击,滑动,手势解锁!

在自动化测试领域中&#xff0c;TouchAction 是一种非常强大的工具&#xff0c;它允许我们模拟用户在设备屏幕上的各种触摸事件。这种模拟不仅限于简单的点击操作&#xff0c;还包括滑动、长按、多点触控等复杂的手势。 点按与双击 点按和双击是触屏设备上最基本的操作之一。…

数据库图形化管理界面应用 Navicat Premium 使用教程

经同学介绍的一个把数据库可视化的软件Navicat Premium&#xff0c;很好用&#xff0c;在这里分享一下&#xff0c;需要的同学可以去了解看看 一&#xff1a;下载并解压 链接&#xff1a;https://pan.baidu.com/s/1ZcDH6m7EAurAp_QmXWx81A 提取码&#xff1a;e5f6 解压到合…

景芯SoC训练营DFT debug

景芯训练营VIP学员在实践课上遇到个DFT C1 violation&#xff0c;导致check_design_rule无法通过&#xff0c;具体报错如下&#xff1a; 遇到这个问题第一反映一定是确认时钟&#xff0c;于是小编让学员去排查add_clock是否指定了时钟&#xff0c;指定的时钟位置是否正确。 景芯…

Redis原理-数据结构

Redis原理篇 1、原理篇-Redis数据结构 1.1 Redis数据结构-动态字符串 我们都知道Redis中保存的Key是字符串&#xff0c;value往往是字符串或者字符串的集合。可见字符串是Redis中最常用的一种数据结构。 不过Redis没有直接使用C语言中的字符串&#xff0c;因为C语言字符串存…

【操作系统】进程管理——进程的同步与互斥(个人笔记)

学习日期&#xff1a;2024.7.8 内容摘要&#xff1a;进程同步/互斥的概念和意义&#xff0c;基于软/硬件的实现方法 进程同步与互斥的概念和意义 为什么要有进程同步机制&#xff1f; 回顾&#xff1a;在《进程管理》第一章中&#xff0c;我们学习了进程具有异步性的特征&am…

Apache AGE中的图

图由一组点和边组成&#xff0c;其中每个节点和边都具有属性映射。点是图的基本对象&#xff0c;可以独立于图中的其他任何对象存在。边创建了两个点之间的有向连接。 创建图 要创建图&#xff0c;可以使用 ag_catalog 命名空间中的 create_graph 函数。 create_graph() 语法…

C++进阶-二叉树进阶(二叉搜索树)

1. 二叉搜索树 1.1 二叉搜索树概念 二叉搜索树又称二叉排序树&#xff0c;它或者是一棵空树&#xff0c;或者是具有以下性质的二叉树: 1.若它的左子树不为空&#xff0c;则左子树上所有节点的值都小于根节点的值2.若它的右子树不为空&#xff0c;则右子树上所有节点的值都大于…

Jenkins教程-15-常用插件-Blue Ocean

上一小节我们学习了Jenkins定时任务构建的方法&#xff0c;本小节我们讲解一下Jenkins常用插件Blue Ocean的使用方法。 Blue Ocean 提供了一套可视化操作界面来帮助创建、编辑 Pipeline 任务。 Blue Ocean 特性&#xff1a; 流水线编辑器&#xff1a;用于创建贯穿始终的持续交…

一、redis-万字长文读懂redis

高性能分布式缓存Redis `第一篇章`1.1缓存发展史&缓存分类1.1.1 大型网站中缓存的使用带来的问题1.1.2 常见缓存的分类及对比与memcache对比1.2 数据类型选择&应用场景1.2.1 string1.2.2 hash1.2.3 链表1.2.4 set1.2.5 sortedset有序集合类型1.2.6 总结1.3 Redis高级应…

mysql在linux系统下重置root密码

mysql在linux系统下重置root密码 登录服务器时候mysql密码忘记了&#xff0c;没办法只能重置&#xff0c;找了一圈&#xff0c;把行之有效的方法介绍在这里。 错误展示&#xff1a; 我还以为yes就可以了呢&#xff0c;这是不行的意思。 关掉mysql服务 sudo systemctl stop …

百度、谷歌、必应收录个人博客网站

主要是给各个搜索引擎提交你的sitemap文件&#xff0c;让别人能搜到你博客的内容。 主题使用的Butterfly。 生成sitemap 安装自动生成sitemap插件。 npm install hexo-generator-sitemap --save npm install hexo-generator-baidu-sitemap --save在站点配置文件_config.yml…

Redhat 安装 docker 网络连接超时问题

目录 添加阿里云的Docker CE仓库 更新YUM缓存 安装 Docker Engine 启动并设置Docker自启动 验证 Docker 安装 [userlocalhost ~]$ sudo yum-config-manager --add-repohttps://download.docker.com/linux/centos/docker-ce.repo 正在更新 Subscription Management 软件仓库…

PHP中的运算符与表达式:深入解析与实战应用

目录 一、基础概念 1.1 运算符的定义 1.2 表达式的定义 二、PHP运算符的分类 2.1 算术运算符 2.2 赋值运算符 2.3 比较运算符 2.4 逻辑运算符 2.5 位运算符 2.6 字符串运算符 2.7 错误控制运算符 三、表达式的优先级与结合性 3.1 优先级 3.2 结合性 四、实战应…

挑战全网最清晰解决文本文件乱码方案

标题 文本文件出现乱码之全网最清晰解决方案乱码出现的原因解决方案第一步&#xff1a;获取文件的原始编码格式。第二步&#xff0c;获取当前系统的格式第三步&#xff0c;将文件的内容以当前系统编码格式进行译码并且输出到新的文件中第四步&#xff0c;删除原文件&#xff0c…

【SOLID原则前端中的应用】接口隔离原则(Interface Segregation Principle,ISP)- vue3示例

接口隔离原则&#xff08;Interface Segregation Principle&#xff0c;ISP&#xff09;在Vue 3中的应用 接口隔离原则&#xff08;Interface Segregation Principle&#xff0c;ISP&#xff09;规定&#xff0c;客户端不应该被迫依赖于它不使用的方法。 换句话说&#xff0c;…

【Python_GUI】tkinter常用组件——文本类组件

文本时窗口中必不可少的一部分&#xff0c;tkinter模块中&#xff0c;有3种常用的文本类组件&#xff0c;通过这3种组件&#xff0c;可以在窗口中显示以及输入单行文本、多行文本、图片等。 Label标签组件 Label组件的基本使用 Label组件是窗口中比较常用的组件&#xff0c;…

JavaEE初阶-网络原理1

文章目录 前言一、UDP报头二、UDP校验和2.1 CRC2.2 md5 前言 学习一个网络协议&#xff0c;最主要就是学习的报文格式&#xff0c;对于UDP来说&#xff0c;应用层数据到达UDP之后&#xff0c;会给应用层数据报前面加上UDP报头。 UDP数据报UDP包头载荷 一、UDP报头 如上图UDP的…

使用ifconfig命令获取当前服务器的内网IP地址

如何使用ifconfig命令获取当前服务器的内网IP地址呢&#xff1f; ifconfig eth0 | grep inet | awk {print $2}

redis运维:sentinel模式如何查看所有从节点

1. 连接到sentinel redis-cli -h sentinel_host -p sentinel_port如&#xff1a; redis-cli -h {域名} -p 200182. 发现Redis主服务器 连接到哨兵后&#xff0c;我们可以使用SENTINEL get-master-addr-by-name命令来获取当前的Redis主服务器的地址。 SENTINEL get-master-a…