[c++] 深拷贝和浅拷贝,拷贝构造、赋值运算符

news2024/10/5 14:33:19

1 拷贝构造和赋值运算符

1.1 拷贝构造

拷贝构造在如下场景会被调用:

(1)函数调用时,函数参数是对象的值传递

(2)声明对象同时初始化的时候(而不是声明和初始化分开,因为声明的时候就创建了对象)

(3)函数返回的时候,返回对象的值。

第 3 种情况,默认情况下有返回值优化,不会调用拷贝构造函数。

通过编译参数 -fno-elide-constructors 可以禁用返回值优化。

注:

拷贝构造函数的形参必须是引用传递,如果是值传递的话,那么在传递过程中也会调用拷贝构造函数,这样就造成递归调用。

如果是值传递,会有编译错误。

#include <iostream>
#include <string>

class Test {
public:
  Test(int x) {
    a_ = x;
    std::cout << "Test(), this = " << this << std::endl;
  };

  Test(Test &test) {
    std::cout << "Test() copy constructor, this = " << this << ", &test = " << &test << std::endl;
  }

  ~Test() {
    std::cout << "~Test(), this = " << this << std::endl;
  }

  int a_;
};

void func1(Test test) {
  std::cout << "func1, &test = " << &test << std::endl;
}

Test func2() {
  Test t(2);
  std::cout << "func2, &t = " << &t << std::endl;
  return t;
}

int main() {
  std::cout << "before t1" << std::endl;
  Test t1(1);
  std::cout << "&t1 = " << &t1 << std::endl;
  std::cout << std::endl;

  std::cout << "before func1" << std::endl;
  // 函数调用,传参是对象的值传递,拷贝构造
  func1(t1);
  std::cout << std::endl;

  std::cout << "before t2" << std::endl;
  // 声明对象的时候使用另一个对象初始化,拷贝构造
  Test t2 = t1;
  std::cout << "&t1 = " << &t1 << ", &t2 = " << &t2 << std::endl;
  std::cout << std::endl;

  std::cout << "before func2" << std::endl;
  // 函数返回值是返回对象的值,拷贝构造
  Test t4 = func2();
  std::cout << "&t4 = " << &t4 << std::endl;
  std::cout << std::endl;

  std::cout << "before return" << std::endl;
  return 0;
}

编译命令:

g++ copy1.cpp -fno-elide-constructors

运行结果:

1.2 赋值运算符

赋值运算符,也是通过 = 赋值的时候调用。前提是 = 左边的对象已经构建了。

赋值运算符返回的是当前对象的引用,因为赋值运算符并没有创建新的对象,所以 = 左边的对象仍然指向原来的位置。

#include <iostream>
#include <string>

class Test {
public:
  Test(int x) {
    a_ = x;
    std::cout << "Test(), this = " << this << std::endl;
  };

  Test(Test &test) {
    std::cout << "Test() copy constructor, this = " << this << ", &test = " << &test << std::endl;
  }

  Test& operator=(const Test &test) {
    std::cout << "Test() operator=, this = " << this << ", &test = " << &test << std::endl;
    return *this;
  }

  int a_;
};

void func1(Test test) {
  std::cout << "func1" << std::endl;
}

Test func2() {
  Test t(2);
  std::cout << "func2" << std::endl;
  return t;
}

int main() {
  std::cout << "before t1" << std::endl;
  Test t1(1);
  std::cout << std::endl;

  std::cout << "before t2" << std::endl;
  Test t2(10);
  std::cout << "before t2 = t1" << std::endl;
  // 赋值运算符
  t2 = t1;
  std::cout << "&t2 = " << &t2 << std::endl;
  std::cout << std::endl;

  std::cout << "before func2" << std::endl;
  Test t3(3);
  // 赋值运算符
  // 默认情况下开启了返回值优化
  // 在 func2 中构造的对象会直接通过赋值运算符赋值给 t3
  // 如果禁用返回值优化,那么下边这句代码调用了拷贝构造函数,也调用了赋值运算符
  t3 = func2();
  std::cout << "&t3 = " << &t3 << std::endl;
  std::cout << std::endl;

  std::cout << "before return" << std::endl;
  return 0;
}

1.3 拷贝构造和赋值运算符的区别

是否有新对象产生,有新对象产生则是拷贝构造,否则是赋值运算符。

Test t1(10);

Test t2(20);

t1 = t2;

赋值运算符,t1 已经存在了,已经构造过了,只不过重新赋值。

没有产生新对象,所以调用的是赋值运算符。

Test t1(10);

Test t2 = t1;

拷贝构造,t2 之前还没有存在,在执行 Test t2 = t1 的时候才构造出来一个新的。

产生了新的对象,所以调用了拷贝构造。

2 深拷贝和浅拷贝

浅拷贝和深拷贝在有指针成员的时候需要注意。

如果拷贝的时候只把指针进行了拷贝,两个对象内的指针指向同一个内存块,这就是浅拷贝,浅拷贝,两个对象会相互影响。如果拷贝的时候,重新申请了内存,然后把指针指向的数据进行了拷贝,那就是深度拷贝。

直观理解的话,实际开发中,绝大多数情况下都应该使用深拷贝,避免使用浅拷贝,因为浅拷贝使用不当很容易导致问题。

2.1 浅拷贝

如下代码, Test 类中有一个指针 int *pi_,可以认为是一个 int 数组。在构造函数中如果没有指定数组长度那就申请 8 个长度,如果指定了,那申请指定的长度。

类中没有自己写拷贝构造函数,也没有自己实现赋值运算符,当自己没有指定的时候,编译器会自动生成拷贝构造函数和赋值构运算符。

#include <iostream>
#include <string>

class Test {
public:
  Test(unsigned int length = 0) {
    std::cout << "Test(), this = " << this << ", pi_ = " << pi_ << std::endl;
    if (length == 0) {
      pi_ = (int *)malloc(8 * sizeof(int));
      length_ = 8;
    } else {
      pi_ = (int *)malloc(length * sizeof(int));
      length_ = length;
    }
    std::cout << "pi_ = " << pi_ << std::endl;
  };
/*
  Test(Test &test) {
    std::cout << "Test() copy constructor, this = " << this << ", &test = " << &test << std::endl;
    std::cout << "pi_ = " << pi_ << ", test length = " << test.length_ << std::endl;
    pi_ = (int *)malloc(test.length_ * sizeof(int));
    length_ = test.length_;
    std::cout << "after copy, pi_ = " << pi_ << ", test length = " << test.length_ << std::endl;
  }

  ~Test() {
    std::cout << "~Test(), this = " << this << ", pi_ = " << pi_ << ", length_ = " << length_ << std::endl;
    if (pi_) {
      free(pi_);
    }
  }
*/
  int *pi_ = nullptr;
  unsigned int length_ = 0;
};

void func1(Test test) {
  std::cout << "func1, &test = " << &test << std::endl;
}

Test func2() {
  Test t(2);
  std::cout << "func2, &t = " << &t << std::endl;
  return t;
}

int main() {
  std::cout << "before t1" << std::endl;
  Test t1(1);
  *t1.pi_ = 100;
  std::cout << "&t1 = " << &t1 << ", t1 data p = " << t1.pi_ << std::endl;
  std::cout << std::endl;

  Test t2 = t1;
  std::cout << "t2 data p = " << t2.pi_ << std::endl;

  Test t3(1);
  *t3.pi_ = 200;
  std::cout << "before assign, t3 data p = " << t3.pi_ << ", data = " << *t3.pi_ << ", t1 data = " << *t1.pi_ << std::endl;
  t3 = t1;
  std::cout << "after assign, t3 data p = " << t3.pi_ << ", data = " << *t3.pi_ << std::endl;

  std::cout << "before return" << std::endl;
  return 0;
}

代码编译之后,运行结果如下。

(1)拷贝构造

t2 是使用拷贝构造构造出来的对象,打印了 t2 中 pi_,可以看到 t2 中的 pi_ 和 t1 中的 pi_ 是相同的。也就是说默认情况下的拷贝构造是浅拷贝。

(2)赋值运算符

t3 被 t1 赋值前后,打印 t3 的 pi_,赋值前后, t3 中的 pi_ 是不相同的,赋值之后 t3 中的 pi_ 与 t1 中的 pi_ 是相同的。所以赋值运算符也是只把指针进行了赋值,没有对内存中的值进行赋值。

2.2 深拷贝

如下代码,自己实现了拷贝构造函数和赋值运算符,申请了空间,并把内存区域中的数据进行了复制。

#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>

class Test {
public:
  Test(unsigned int length = 0) {
    std::cout << "Test(), this = " << this << ", pi_ = " << pi_ << std::endl;
    if (length == 0) {
      pi_ = (int *)malloc(8 * sizeof(int));
      length_ = 8;
    } else {
      pi_ = (int *)malloc(length * sizeof(int));
      length_ = length;
    }
    std::cout << "pi_ = " << pi_ << std::endl;
  };

  Test(Test &test) {
    std::cout << "Test() copy constructor, this = " << this << ", &test = " << &test << std::endl;
    std::cout << "pi_ = " << pi_ << ", test length = " << test.length_ << std::endl;
    pi_ = (int *)malloc(test.length_ * sizeof(int));
    length_ = test.length_;
    memcpy(pi_, test.pi_, test.length_ * sizeof(int));
    std::cout << "after copy, pi_ = " << pi_ << ", test length = " << test.length_ << std::endl;
  }


  Test& operator=(const Test &test) {
    std::cout << "Test() operator=, this = " << this << ", &test = " << &test << std::endl;
    if (pi_) {
      free(pi_);
    }
    pi_ = (int *)malloc(test.length_ * sizeof(int));
    if (pi_) {
      length_ = test.length_;
      memcpy(pi_, test.pi_, test.length_ * sizeof(int));
    }
    return *this;
  }

  ~Test() {
    std::cout << "~Test(), this = " << this << ", pi_ = " << pi_ << ", length_ = " << length_ << std::endl;
    if (pi_) {
      free(pi_);
    }
  }

  int *pi_ = nullptr;
  unsigned int length_ = 0;
};

void func1(Test test) {
  std::cout << "func1, &test = " << &test << std::endl;
}

Test func2() {
  Test t(2);
  std::cout << "func2, &t = " << &t << std::endl;
  return t;
}

int main() {
  std::cout << "before t1" << std::endl;
  Test t1(1);
  *t1.pi_ = 100;
  std::cout << "&t1 = " << &t1 << ", t1 data p = " << t1.pi_ << ", data = " << *t1.pi_ << std::endl;
  std::cout << std::endl;

  Test t2 = t1;
  std::cout << "t2 data p = " << t2.pi_ << ", data = " << *t2.pi_ << std::endl;
  std::cout << std::endl;

  Test t3(1);
  *t3.pi_ = 200;
  std::cout << "before assign, t3 data p = " << t3.pi_ << ", data = " << *t3.pi_ << ", t1 data = " << *t1.pi_ << std::endl;
  t3 = t1;
  std::cout << "after assign, t3 data p = " << t3.pi_ << ", data = " << *t3.pi_ << std::endl;

  std::cout << "before return" << std::endl;
  return 0;
}

t2 是调用拷贝构造函数,通过 t1 拷贝构造而来。可以看到拷贝构造之后,t2 中成员指向的内存和 t1 是不同的,内存中的值是相同的,所以是深度拷贝。

t3 被 t1 通过赋值运算符赋值,赋值之后,t3 指针指向的内存的值 与 t1 保持一致,所以是深度赋值。

2.3 派生类拷贝构造调用父类拷贝构造

#include <iostream>

class Base {
public:
  Base() : i(0) {
    std::cout << "Base()" << std::endl;
  }

  Base(int n) : i(n) {
    std::cout << "Base(int), i = " << i << std::endl;
  }

  Base(Base &b) : i(b.i) {
    std::cout << "Base(Base &), i = " << i << std::endl;
  }

private:
  int i;
};

class Derived : public Base {
public:
  Derived() : Base(0), j(0) {
    std::cout << "Derived()" << std::endl;
  }

  Derived(int m, int n) : Base(m), j(n) {
    std::cout << "Derived(int)" << std::endl;
  }

  Derived(Derived &d) : Base(d), j(d.j) {
    std::cout << "Derived(Derived &)" << std::endl;
  }

private:
  int j;
};

int main() {
  Base b(1);
  Derived d(2, 3);

  std::cout << "----------------" << std::endl;
  Derived d1(d);
  std::cout << "----------------" << std::endl;
  return 0;
}

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

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

相关文章

游戏配置内存“瘦身”策略

背景 游戏配置数据绝对是游戏服务器进程的内存大头,有些游戏服务器单纯数据配置的容量就超过一个G。因此,这部分内存优化也就放在首要位置了。 优化策略 在《服务器进程如何降低内存》一文中,我们讲述了可以通过“优化游戏配置缓存”来降低游戏服务器进程的内存使用量。本…

【电子通识】认识FMEA(失效模式和影响分析)

FMEA是Failure Mode and Effect Analysis的英文缩写&#xff0c;中文名称为失效模式和影响分析。主要应用于航空航天、食品、汽车和核电等行业。 FMEA讨论的是事先策划以及执行措施&#xff0c;预防问题的发生或控制问题的发展&#xff0c;降低设计和过程的风险。由于问题还没…

C语言------操作符的巧妙使用

1.计算一个数字二进制补码里面1的个数 &#xff08;1&#xff09;方法一 根据这个10进制的整数&#xff0c;对这个数进行%10&#xff0c;/10不断地进行下去&#xff0c; %10得到最后一位&#xff0c;/10得到舍去最后一位之后剩余的数&#xff1b; 同理得到&#xff1a;二进…

深入理解 CSS 定位与布局高级技巧

更多web开发知识欢迎访问我的专栏>>> CSS高级 目标&#xff1a;掌握定位的作用及特点&#xff1b;掌握 CSS 高级技巧 01-定位 作用&#xff1a;灵活的改变盒子在网页中的位置 实现&#xff1a; 1.定位模式&#xff1a;position 2.边偏移&#xff1a;设置盒子的位…

构造百万测试数据五大方法!

在测试的工作过程中&#xff0c;很多场景是需要构造一些数据在项目里的&#xff0c;方便测试工作的进行。比如下面的场景&#xff1a; 项目需要做性能测试&#xff0c;需要大量的数据就算是功能测试&#xff0c;比如测试搜索功能&#xff0c;需要有数据做搜索测试需要检查数据…

Windows Server 2012 IIS中发布ASP.NET CORE项目

服务器安装IIS&#xff1a; 微软官网下载SDK&#xff1a; 下载Runtime官网&#xff1a;https://dotnet.microsoft.com/download/dotnet-core 安装成功重启IIS&#xff1a; VS发布项目&#xff1a;

js滑动窗口算法

滑动窗口算法&#xff08;Sliding Window Algorithm&#xff09;是一种用于解决数组或字符串的子串问题的有效算法。其核心思想是通过维护一个窗口&#xff0c;根据问题的要求移动窗口的左右边界&#xff0c;从而在窗口内部找到符合条件的子串。 一般步骤如下&#xff1a; 初…

acwing算法学习笔记 ------ 双链表

1、定义 这里可以做一个投机取巧&#xff0c;我们不再像单链表去用head去存头和尾&#xff0c;直接让r[0] 1,l[1] 0; idx 2.进行初始化&#xff0c; 解释一下l[N] 和 r[N] l[N]:是表示指向左面下一个节点下标&#xff0c; r[N]:表示指向下一个节点的下标。大家不用担心i…

学习 LangChain 的 Passing data through

学习 LangChain 的 Passing data through 1. Passing data through2. 示例 1. Passing data through RunnablePassthrough 允许不改变或添加额外的键来传递输入。这通常与 RunnableParallel 结合使用&#xff0c;将数据分配给映射中的新键。 RunnablePassthrough() 单独调用&…

【Java程序员面试专栏 算法思维】一 高频面试算法题:排序算法

一轮的算法训练完成后,对相关的题目有了一个初步理解了,接下来进行专题训练,以下这些题目就是汇总的高频题目,本篇主要聊聊排序算法,包括手撕排序算法,经典的TOPK问题以及区间合并,所以放到一篇Blog中集中练习 题目关键字解题思路时间空间快速排序双指针+递归+基准值分…

U盘乱码与文件丢失:恢复指南与预防策略

U盘乱码文件丢失是一种常见的技术问题&#xff0c;通常表现为存储在U盘中的文件名显示为不可识别的字符或文件无法正常打开&#xff0c;有时甚至文件会完全消失。这种情况可能由多种原因引起&#xff0c;包括但不限于文件系统损坏、不正确的拔插操作、病毒感染、兼容性问题等。…

Linux学习之vi/vim详细介绍

目录 ​编辑 1. 什么是 vim&#xff1f; 2. vi/vim 的使用 2.1 命令模式 2.2 输入模式 2.3 底线命令模式 3. vi/vim 使用实例 3.1 使用 vi/vim 进入一般模式 3.2 按下 i 进入输入模式(也称为编辑模式)&#xff0c;开始编辑文字 3.3 按下 ESC 按钮回到一般模式…

相信未来:技术的进步意味着重构

十年以来&#xff0c;呼声最高&#xff1a;AI、BigData、Cloud Service。 以本人看来&#xff0c;仅AI技术的进步和应用&#xff0c;整个软件行业&#xff0c;所有软件将被重构。 提醒&#xff1a;非大学毕业、非计算机及相关专业&#xff0c;在IT这个行业&#xff0c;特别是…

大数据开发项目--音乐排行榜

环境&#xff1a;windows10&#xff0c;centos7.9&#xff0c;hadoop3.2、hbase2.5.3和zookeeper3.8完全分布式&#xff1b; 环境搭建具体操作请参考以下文章&#xff1a; CentOS7 Hadoop3.X完全分布式环境搭建 Hadoop3.x完全分布式环境搭建Zookeeper和Hbase 1. 集成MapReduce…

安装 WSL 报错 Error code: Wsl/WININET_E_NAME_NOT_RESOLVED 问题解决

问题描述 在执行 wsl --install 安装Windows子系统Linux WSL (Windows Subsystem for Linux) 时报错&#xff1a; 无法从“https://raw.githubusercontent.com/microsoft/WSL/master/distributions/DistributionInfo.json”中提取列表分发。无法解析服务器的名称或地址 Error…

代码随想录算法训练营第60天 | 647.回文子串 516.最长回文子序列

回文子串 这道题主要难在dp数组的定义以及递推关系的构建。如果直接用 dp[i] 表示[0,i]子串中包含的回文串的数目&#xff0c;是无法找到递推关系的。通过回文串的性质可以构造这样的递推关系&#xff1a;对于判断[i,j]是否是回文串&#xff0c;如果s[i] s[j]&#xff0c;只需…

网络攻防之ARP欺骗和DNS劫持实验

目录 ARP单向欺骗 ARP双向欺骗 DNS劫持 实验环境&#xff1a; 攻击主机&#xff1a;kali2023虚拟机&#xff0c;IP地址为192.168.133.141 靶机&#xff1a;Windows10虚拟机&#xff0c;IP地址为192.168.133.129 网关地址&#xff1a;192.168.133.2 (1)ARP协议介绍 在以…

2/23 work

1> 使用消息队列完成两个进程间相互 a: #include<myhead.h> #define MSGSIZE sizeof(struct msgbuf)-sizeof(long) struct msgbuf {long mtype;char mtext[1024];}; int main(int argc, const char *argv[]) {pid_t pidfork();if(pid>0){key_t key 0;if((keyfto…

Git笔记——3

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 一、合并模式和分支策略 二、bug分支 三、强制删除分支 四、创建远程仓库 五、克隆远程仓库_HTTPS和_SSH 克隆远程仓库_HTTPS 克隆远程仓库_SSH 六、向远程仓库…

【数据结构】时间复杂度(加法乘法规则、渐近时间复杂度、循环时间复杂度总结

2.2 时间复杂度 什么是时间复杂度&#xff1f; 评估算法时间开销 T ( n ) O ( f ( n ) ) T(n)O(f(n)) T(n)O(f(n)) 在实际求解中&#xff0c;只留表达式中最高阶的部分&#xff0c;丢弃其他部分。 如何求解&#xff1f; 求解步骤 1.找到一个最深层的基本操作&#xff1b; 2.分…