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

news2024/9/19 12:30:25

一 拷贝构造函数的概念:

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

二 拷贝构造函数的特点:

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.coloradmin.cn/o/1907625.html

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

相关文章

静态路由配置注意事项及黑洞路由的使用

静态路由 1 . 定义 从管理员处学习到的数据转发路径&#xff0c;就称为静态路由。 2 . 路由表 Proto &#xff1a;协议&#xff08; Protocol &#xff09; Direct — 直连链路Static — 静态路由RIP 、OSPF 等 — 动态路由 Pre : 优先级&#xff08; Preference &#x…

ozon跨境电商可以做吗,俄罗斯ozon跨境电商可不可以做

当今全球化的浪潮下&#xff0c;跨境电商已成为连接世界各地消费者与商家的桥梁&#xff0c;为无数企业开辟了全新的市场蓝海。俄罗斯&#xff0c;作为世界上最大的国家之一&#xff0c;其电商市场近年来蓬勃发展&#xff0c;尤其是ozon平台&#xff0c;作为俄罗斯本土的电商巨…

你真的会信息收集嘛,4k字渗透测试信息收集10大技巧

前言 在渗透测试中&#xff0c;信息收集是非常关键的一步&#xff0c;它为后续的漏洞发现和利用提供了重要的基础。以下是非常详细的信息收集方式&#xff1a; 一、被动信息收集 被动信息收集是指在不与目标系统直接交互的情况下&#xff0c;通过公开渠道获取目标系统的相关…

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 软件仓库…

高考志愿填报的六个不要

在高考志愿填报这个关键时刻&#xff0c;确实需要谨慎行事&#xff0c;避免一些常见的错误。以下是高考志愿填报的六个“不要”&#xff0c;希望能为你提供一些有用的建议&#xff1a; 1、不要盲目跟风 每个人的兴趣、能力和未来规划都不同&#xff0c;不要仅仅因为某个专业或…

202406 CCF-GESP Python 三级试题及详细答案注释

202406 CCF-GESP Python 三级试题及详细答案注释 1 单选题(每题 2 分,共 30 分)第 1 题 小杨父母带他到某培训机构给他报名参加CCF组织的GESP认证考试的第1级,那他可以选择的认证语言有几种?( ) A. 1 B. 2 C. 3 D. 4答案:C解析:目前CCF组织的GESP认证考试有C++、Pyth…

LVS+Nginx高可用集群---Nginx进阶与实战

1.Nginx中解决跨域问题 两个站点的域名不一样&#xff0c;就会有一个跨域问题。 跨域问题&#xff1a;了解同源策略&#xff1a;协议&#xff0c;域名&#xff0c;端口号都相同&#xff0c;只要有一个不相同那么就是非同源。 CORS全称Cross-Origin Resource Sharing&#xff…

Vue+ElementUi实现录音播放上传及处理getUserMedia报错问题

1.Vue安装插件 npm install --registryhttps://registry.npmmirror.com 2.Vue页面使用 <template><div class"app-container"><!-- header --><el-header class"procedureHeader" style"height: 20px;"><el-divid…

AJAX快速入门(一) express框架的安装和使用范例

主打一个有用 首先保证安装了nodejs环境 打开终端 初始化npm npm init安装express npm i express测试样例 目录结构 样例代码 express.js //引入express const express require(express);//创建应用对象 const app express();//创建路由规则 //req是请求对象&#x…

C#委托事件的实现

1、事件 在C#中事件是一种特殊的委托类型&#xff0c;用于在对象之间提供一种基于观察者模式的通知机制。 1.1、事件的发送方定义了一个委托&#xff0c;委托类型的声明包含了事件的签名&#xff0c;即事件处理器方法的签名。 1.2、事件的订阅者可以通过运算符来注册事件处理器…

HTTP 请求走私漏洞详解

超详细的HTTP请求走私漏洞教程&#xff0c;看完还不会你来找我。 1. 简介 HTTP请求走私漏洞&#xff08;HTTP Request Smuggling&#xff09;发生在前端服务器&#xff08;也称代理服务器&#xff0c;一般会进行身份验证或访问控制&#xff09;和后端服务器在解析HTTP请求时&…

【GIt】变基(rebase)

目录 变基(rebase)是什么为什么有变基变基后的时间线变基前的时间线 变基原理怎么变基同一个分支变基不同分支变基 参考文章 变基(rebase)是什么 Git 变基&#xff08;rebase&#xff09;是一种用于整合分支的方法&#xff0c;它的工作原理是将一系列提交&#xff08;或分支合…

太实用了吧?手把手教你华为eNSP模拟器桥接真实网络!

号主&#xff1a;老杨丨11年资深网络工程师&#xff0c;更多网工提升干货&#xff0c;请关注公众号&#xff1a;网络工程师俱乐部 晚上好&#xff0c;我的网工朋友。 今天聊聊eNSP桥接正式网络&#xff0c;就是把eNSP桥接进真实的网络&#xff0c;利用我们的物理网卡通过实体路…

MoonBit 周报 Vol.48:默认开启诊断信息渲染、test block 不再返回 Result 类型的结果

weekly 2024-07-08 MoonBit 更新 【重大更新】修改 array slice 的语法&#xff0c;从 arr[start..end] 修改为类似 Python 的 arr[start:end]。这是为了避免和接下来要支持的 cascade method call x..f() 产生语法冲突。旧的语法会在近期删除。 【Wasm后端重大更新】将 fn i…

什么是C#

C#是一种面向对象的语言与c语言不同 C语言是面向过程的编程 C#运行于.NETFramework和.NETCore之上的高级语言 C#是由C和C衍生而来的一种语言 在C#中不建议使用指针 什么叫面向对象 是一种编程范式&#xff0c;它将现实世界中的事物抽象为对象&#xff0c;并通过对象之间的…

田地行走-美团2023笔试(codefun2000)

题目链接 田地行走-美团2023笔试(codefun2000) 题目内容 塔子哥是一个农民&#xff0c;他有一片 nm 大小的田地&#xff0c;共 n 行 m 列&#xff0c;其中行和列都用从 1 开始的整数编号&#xff0c;田地中有 k 个格子中埋有土豆。我们记第 a 行第 b 列的格子为 (a,b) 。塔子哥…

JAVA:常用的队列指南

1、简述 在计算机科学中&#xff0c;队列是一种常见的线性数据结构&#xff0c;它遵循先进先出&#xff08;FIFO&#xff0c;First In First Out&#xff09;的原则。队列在各种应用中广泛使用&#xff0c;例如任务调度、消息队列和宽度优先搜索等。在 Java 中&#xff0c;队列…

android perfetto使用技巧梳理

1 抓取方法 根据不同的配置参数&#xff0c;会显示不同的功能。 比如有的trace文件就无法显示线程状态信息&#xff0c;有的无法显示锁依赖信息等等&#xff0c;要看你的参数&#xff0c;我这个是很全的&#xff0c;基本够了&#xff0c;如果还想添加&#xff0c;可以命令行看…

我与OceanBase|一位DBA老兵的国产数据库探索之旅

本文作者&#xff1a;尚雷&#xff0c;有超过十年的工作经验&#xff0c;目前就职于南京一家上市互联网企业&#xff0c;担任DBA。Oracle 11g OCM&#xff0c;Oracle及PG的 ACE认证&#xff0c;并有AWS及国产知名数据库等多项认证。他热衷于技术交流与分享&#xff0c;爱交友&a…

Apache AGE 安装部署

AGE概述 概述 我们可以通过源码安装、拉取docker镜像运行、直接使用公有云三种方式中的任意一种来使用Apache AGE 获取 AGE 发布版本 可以在 https://github.com/apache/age/releases 找到发布版本和发布说明。 源代码 源代码可以在 https://github.com/apache/age 找到…