拷贝构造-传值传参的连锁反应与隐式函数的三个局限

拷贝构造:传值传参的连锁反应,与隐式函数的三个局限

我的github:(https://github.com/xcx55/ubuntu-linux-project)

感谢各位大佬参观我的github!!

六大默认成员函数的第三个:拷贝构造。它回答两个问题------用已有对象创建新对象时,成员怎么复制?以及为什么它有一个全 C++ 独一份的硬性规定:第一个参数必须是引用。

一、拷贝构造是什么

定义一句话:如果一个构造函数的第一个参数是自身类类型的引用,且其余参数都有默认值,它就是拷贝构造------拷贝构造是一个特殊的构造函数。

cpp 复制代码
Date(Date& d)
{
    _year = d._year;
    _month = d._month;
    _day = d._day;
}

用法就是拿一个已存在的对象去构造新对象:

cpp 复制代码
Date d1(2030, 1, 1);
Date d2(d1);   // 拷贝构造:以 d1 为模板造出 d2

普通构造函数是"按你给的方式初始化",拷贝构造是"按另一个对象的样子初始化"。它也是构造函数,所以同样受那套规矩管辖:隐式的拷贝构造永远有;显式一出,隐式就没。

二、为什么第一个参数必须是引用------无穷递归

这是拷贝构造最经典的面试题。假设允许传值:

cpp 复制代码
Date(Date d)   // 错误写法

推演一遍调用链就明白了:

  1. Date d2(d1) 要调拷贝构造;
  2. 调用函数得先把实参 d1 传值传参给形参 d;
  3. 传值传参本身就是一次拷贝------自定义类型传值传参要先用拷贝构造初始化形参;
  4. 于是又要调一次拷贝构造,又要传值传参,又要拷贝......

每次要调用拷贝构造函数之前要先传值传参,传值传参是一种拷贝,又形成一个新的拷贝构造------无穷递归,编译器根本不给你这个机会,直接报错。

解法:第一个参数用引用 。引用传参不产生拷贝(别名直接绑定本体),递归链条在第一步就断了。所以这条不是"建议",是语法强制:拷贝构造的第一个参数必须是引用 (习惯上加 const:Date(const Date& d),因为拷贝不该改动被拷贝者)。

顺带理清:传值传参的底层分工

这套递归推演里藏着一条通用规则:传值传参怎么拷,取决于参数类型。

  • 内置类型(int、float、double、指针......):传值传参就是直接字节对字节的拷贝,没有构造函数什么事;
  • 自定义类型 :传值传参要先对形参进行初始化,而这个初始化就是由拷贝构造函数完成的------"传值传参是以拷贝构造函数完成的"。

所以引用不只是救了拷贝构造,也是所有自定义类型传参的第一原则:能用引用传参就用引用,别让编译器白白多走一次拷贝构造。

三、浅拷贝:编译器白送的版本,只做了一半

拷贝构造有隐式版本。它做的事是浅拷贝:把实参对象的成员变量逐个字节赋给新对象。

对 Date 这种纯 int 成员的类,浅拷贝完全够用------每个成员独立,怎么抄都不会错。

但对 Stack 这种成员里有指针的类,浅拷贝就是事故:

cpp 复制代码
Stack st1;
Stack st2(st1);   // 隐式浅拷贝:st2._a = st1._a,两个指针指向同一块空间

浅拷贝只抄了指针的值 ,没抄指针指向的空间------st1 和 st2 的 _a 指向同一块内存。接着析构时按栈帧从高地址到低地址的顺序清理,先析构 st2 再析构 st1,同一块空间被 free 两次------这就是 double free,程序直接崩。

笔记里那句"这样会析构两次,这就是因为值(浅拷贝)导致的,空间指针不变",把因果说得很准:问题不在析构,在拷贝那一刻就埋下了。

四、隐式函数的三个局限

把构造、拷贝构造、析构三个的"编译器白送版本"放一起看,笔记里总结得很干净:

  • 构造函数------隐式版只能做无意义的初始化(内置类型成员根本不管),基本上都要自己写显式的重载;
  • 拷贝构造------隐式版只会浅拷贝,成员里有指针就必须自己写深拷贝;
  • 析构函数------隐式版管不了动态内存(它不知道哪些资源要你手动清理的策略)。

编译器确实想让这三个函数都"白送好用",但各有各的局限。于是学习它们就回到开头那两个角度:默认行为是什么?不满足就自己写。浅拷贝改深拷贝的具体写法,Stack 实现那篇会专门展开。


串一下这条线:拷贝构造 = 特殊的构造函数(第一个参数必须是自身类型的引用)→ 强制引用是因为传值传参要先拷贝、拷贝又要传值传参的无穷递归 → 传值传参对自定义类型就是调拷贝构造 → 隐式拷贝构造是浅拷贝 → 浅拷贝 + 指针成员 = 析构两次。到这里,六大函数的前三个都齐了,下一篇写第四个也是最难的一个:赋值运算符重载。

相关推荐
垆边人似月.1 小时前
华为机试题 :最大数
c++·算法·华为
方便面不加香菜2 小时前
C++ 关于多态的知识点讲解
开发语言·c++
jimy12 小时前
C++ 类成员函数的内联定义(inline)详解
开发语言·c++
sanqima2 小时前
CreateThread、_beginthreadex、AfxBeginThread三种线程创建的区别与联系
c++·tls·线程创建
sanqima2 小时前
依赖TLS和模块状态的MFC类
c++·mfc·tls·module state
All for pursuit.3 小时前
【动态规划-11】279.完全平方数
数据结构·c++·算法·leetcode·动态规划
瑶山3 小时前
Windows + WSL2 Ubuntu + VS Code配置C++开发环境
开发语言·c++·windows·vscode·wsl
小米里的大麦3 小时前
19 C、C++ 操作 MySQL
c语言·c++·mysql
有毒的教程4 小时前
Dev-C++ 使用扩展库两种方式:DevPak一键安装 / 手动配置(重点)
android·c++·adb