【C++ 入门】类与对象:拷贝构造函数与赋值运算符重载详解

【C++ 入门】类与对象:拷贝构造函数与赋值运算符重载详解 一、拷贝构造函数拷贝构造属于一种特殊的构造函数第一个参数是本类的引用其余参数全都带默认值这个构造函数就是拷贝构造。拷贝构造函数特点拷贝构造本质就是构造函数的重载。第一个参数必须是本类的引用绝对不能传值。如果用传值调用拷贝构造的时候又会触发拷贝无限递归编译器直接报错。可以有多个形参但第一位一定是本类引用后面所有参数必须给缺省值。C 规则自定义类对象发生拷贝动作就会调用拷贝构造。所以自定义类型传值做函数实参、函数传值返回对象都会跑拷贝构造。自己不写拷贝构造编译器会自动生成一份默认拷贝构造内置类型成员按字节直接复制也就是浅拷贝类类型自定义成员去调用这个成员自己的拷贝构造。什么时候不用手写拷贝构造什么时候必须手写Date类全是 int 这种内置类型没有堆上申请的资源。编译器默认生成的拷贝构造够用不用自己写。Stack类成员虽然也是内置类型但是指针指向堆内存。编译器默认是浅拷贝两个对象共用同一块堆空间析构的时候会重复释放直接崩溃。这种情况我们要手写深拷贝把指针指向的堆资源也复制一份。MyQueue类内部成员是Stack自定义对象。编译器默认拷贝会自动调用 Stack 的拷贝构造MyQueue 就不用再手写拷贝构造。小技巧总结只要这个类需要手动写析构释放资源基本就要手动实现拷贝构造否则一般不用写。传值返回 vs 引用返回传值返回生成临时对象触发拷贝构造发生拷贝开销。引用返回返回对象的别名不会产生拷贝效率更高。⚠️大坑不能引用返回函数里面的局部对象。 局部对象函数结束生命周期直接销毁引用就变成野引用效果等同于野指针。使用引用返回的前提保证函数执行结束后这个返回的对象还活着。#include iostream using namespace std; class Date { public: Date(int year1991,int month1,int day1) { _yearyear; _monthmonth; _dayday; } void Print() { cout_year/_month/_dayendl; } //拷贝构造函数 // Data Data d)这种写法不行 //但是拷贝函数本身再一直调用拷贝函数本身的话会出现无限递归的问题,所以要使用传引用传参 //加个const可以让下面传实参的选择更多避免出现权限扩大等问题 Date(const Date d) { _yeard._year; _monthd._month; _dayd._day; } // 这个 Date(Date* d) 能实现“拷贝效果”但它严格来说 不是拷贝构造函数只是一个“参数是指针的普通构造函数”。 /*Date(Date* d) { _year d-_year; _month d-_month; _day d-_day; }*/ private: int _year; int _month; int _day; }; //自定义类型传值传参和传值返回都会调用拷贝构造完成 //这里的参数 d 不是原来的对象本身而是 新创建出来的一个 Date 对象。 //传值传参会调用拷贝函数 void func(Date d) { } //传值返回也会调用拷贝函数但是传引用就不会 //Date func2()因为返回的是 d 的引用也就是 d 的别名。可是 func2 函数一结束d 就没了。外面拿到的引用就指向一个已经销毁的对象叫 野引用。 Date func2()//这里最好用传值返回 { Date d(2026,9,4); return d; } int main() { Date d1(2026,9,5); //下面两种写法都可以 Date d2d1; Date d3(d1); Date retfunc2(); d2.Print(); d3.Print(); ret.Print(); return 0; }关于对内置类型处理和深浅拷贝#include iostream using namespace std; class Stack { public: Stack(int capacity4) { _a(int*)malloc(capacity*sizeof(int)); if (_anullptr) { perror(malloc); exit(-1); } _capacitycapacity; _top0; } ~Stack() { if (_a!nullptr) free(_a); _capacity0; _top0; } void push(int x) { if (_top_capacity) { int newcapcity2*_capacity; int *temp(int*)realloc(_a,sizeof(int)*newcapcity); if (tempnullptr) { perror(realloc); exit(0); } _atemp; _capacitynewcapcity; } _a[_top]x; } Stack(const Stack s) { //这里绝对不能这样写这样写你拷贝的是地址仅仅是浅拷贝和原来公用一块空间了 //_as._a; _a(int*)malloc(sizeof(int)*s._capacity);//申请一块同样大小的空间 if (_anullptr) { perror(malloc); exit(-1); } //再把原来空间指向的值再拷贝过去 memcpy(_a, s._a, s._top * sizeof(int)); _capacitys._capacity; _tops._top; } private: int* _a; size_t _capacity; size_t _top; }; int main() { Stack s; s.push(1); s.push(2); s.push(3); s.push(4); s.push(5); Stack s1s; return 0; }二、赋值运算符重载运算符重载运算符重载 - 这些运算符本来只对 int、double 这种内置类型生效。C 允许我们给自定义类对象重新定义这些运算符该怎么干活。类对象用运算符本质就是去调用对应的重载函数没写重载函数就直接编译报错。运算符重载本质就是特殊函数函数名固定写法operator要重载的运算符有返回值、参数、函数体和普通函数一样。参数数量要匹配运算符本身一元运算符 1 个参数二元运算符 2 个参数。二元运算符左边的东西传给第一个参数右边传给第二个参数。如果把运算符重载写成类的成员函数左边操作数会交给隐藏的this指针所以写出来的形参数量会比运算符需要的运算对象少一个。重载完不会改运算符原本的优先级、结合顺序。❌ 不能凭空造新运算符比如自己写operator是不行的。五个运算符绝对不能重载选择题考点.*、::、sizeof、?:三目运算符、.点访问运算符重载运算符至少要有一个参数是自定义类类型。不能改写原本内置类型的行为比如你不能自己写一个int operator(int x,int y)去改变普通 int 相加逻辑。不是所有运算符都适合重载只有逻辑说得通才写。举例子Date 日期类日期-日期合理但是日期 日期逻辑不通就不要重载但日期加数字天数又是合理的。区分前置、后置两个函数名都叫operator没法靠名字区分。C 规定后置 多增加一个 int 类型的哑参数靠这个参数构成函数重载用来区分前后置。输出、输入运算符必须写成全局普通函数不要写成成员函数原因如果做成成员函数第一个参数被 this 指针占了this 代表对象本身也就是运算符左边调用语法就变成对象 cout和我们习惯写的cout 对象完全反过来读起来很别扭。 写成全局函数把ostream/cin放在第一个参数运算符左边自定义类对象放第二个参数就能正常写cout obj。#include iostream using namespace std; class Date { public: Date(int year 1991, int month 1, int day 1) { _year year; _month month; _day day; } Date(const Date d) { _year d._year; _month d._month; _day d._day; } //其实这里用默认生成的也会处理内置类型进行浅拷贝在这里是没问题的。 //这也是和构造和析构的一个区别构造和析构不会处理内置类型 int GetYear() { return _year; } //d1d2,传d2就行d1有this指针但是在实参和形参不能直接写出来函数体内可以 bool operator(const Date d2) { return this-_yeard2._year this-_month d2._month this-_day d2._day; } void Print() { cout _year / _month / _day \n; } private: int _year; int _month; int _day; }; int main() { Date d1(2026,9,5); Date d2(2026,9,6); Date d3(2026,9,5); //我们在这里就需要实现运算符重载函数 d1d2; //如果成成员函数了显示调用是这样的 //d1.operator(d2); //只要传一个参d2就行d1通过this指针但是不能在实参和形参显示写出来的 //再加上运算符重载要求参数个数和运算符作用对象一样多所以只能传一个 //可以具体去看看上面类里面怎么实现的 cout (d1 d2) \n;//这里需要打括号优先级的问题,0 cout (d1 d3) \n;//1 return 0; }接着咱们看下.*这个符号//.*符号一般也只会在c里出现但也用的超级少了解即可 #include iostream using namespace std; void fun1() { cout void fun1() endl; } class A { public: void func2() { cout A::func2() endl; } }; int main() { //普通函数指针 void (*pf1)() fun1; (*pf1)(); //A类型成员函数指针 void (A::*pf2)() A::func2; A aa; (aa.*pf2)(); //这里使用的就是。* return 0; }赋值运算符重载赋值运算符重载属于默认成员函数作用给两个已经创建好的对象做赋值拷贝。分清和拷贝构造的区别拷贝构造新对象正要创建出来用已有对象去初始化这个新对象。赋值重载operator两个对象都已经存在了把一个对象的数据拷贝赋值给另一个老对象。赋值运算符重载特点它属于运算符重载语法强制要求必须写成类的成员函数不能写成全局函数。参数推荐写const 本类类型如果不用引用传值传参就会触发拷贝效率低。加上const既避免权限放大同时能接收普通对象、const 对象传参适配场景更多。要有返回值推荐返回本类的引用用引用返回省去拷贝提升性能。必须有返回值是为了支持连续赋值比如a b c;这种写法。自己不写编译器会自动生成默认赋值重载。 默认版本行为和默认拷贝构造几乎一模一样内置类型成员直接浅拷贝按字节复制自定义类型的成员会去调用这个成员自己的赋值运算符重载。什么时候不用自己写什么时候必须手写例如Date类全部成员都是普通内置类型没有堆上申请的资源。编译器生成的默认赋值重载够用不用我们手动实现。例如Stack类成员虽然是内置指针但是指针指向堆内存资源。编译器默认做浅拷贝两个对象指针指向同一块堆空间会出问题必须自己实现深拷贝把指向的资源也复制一份。例如MyQueue内部的成员是自定义的Stack类型。因为 Stack 已经写好了赋值重载编译器自动生成的 MyQueue 赋值重载会直接调用 Stack 的赋值重载MyQueue 就不需要手写。#include iostream using namespace std; class Date { public: Date(int year 1991, int month 1, int day 1) { _year year; _month month; _day day; } //拷贝构造函数 Date(const Date d) { _year d._year; _month d._month; _day d._day; } Date operator (const Date d){ //自己等于自己就可以不用赋值了 if (this ! d) { _year d._year; _month d._month; _day d._day; } //比如d1d2表达式的返回对象应该为d1,也就是*this return *this; } //赋值运算符重载但其实在Date类型里面不写也没影响跟拷贝构造函数处理内置类型原理一样 void Print() { cout _year / _month / _day \n; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(2026, 9, 7); d1 d2;//赋值重载 Date d3; d3 d1 d2;//从右往左 d1.Print(); d2.Print(); d3.Print(); // 需要注意下面这里是拷⻉构造不是赋值重载 // 要牢牢记住赋值重载完成两个已经存在的对象直接的拷⻉赋值 // 而拷⻉构造用于一个对象拷⻉初始化给另⼀个要创建的对象 Date d4 d1;//因为拷贝构造如果写出这样就有点容易混 //Date d4(d1);//写成这样的时候不太容易混淆 return 0; }