调试C++代码卡了整整一个下午,一直在纠结什么时候调用拷贝构造函数,断点一步一步跑,看着对象凭空多出来一次构造,当时完全摸不着头绪,明明没有手动写对象复制的代码。

写了一个简单的自定义类,里面只放一个成员变量,重载打印函数,然后写了一个普通函数,参数直接写的类类型,没有加引用符号。创建好原对象,把它丢进这个函数,程序跑起来,断点停在拷贝构造里面,一瞬间愣在原地,没理解为什么会触发这一步。

之前总以为,只有用旧对象初始化新对象的时候,拷贝构造才会跑。就像ClassAb=a;这种写法,一眼就能看出来是复制。可这次根本没有定义新变量,只是传参进去,就莫名其妙进了拷贝构造。

值传递会生成临时副本。

函数调用的时候,实参的内容要拷贝一份给到形参,形参本质上是函数内部新生成的局部对象,生成这个局部对象,就会调用拷贝构造函数。只要参数是类对象,不带&引用,每次调用函数,都会执行一次拷贝,成员多一点的类,反复调用还会带来额外开销。当时随手改成引用传递,再重新跑一遍断点,拷贝构造直接不触发了,一下子就把两种情况分开。

后面又试了另一种场景,函数返回值是类对象,return一个局部对象回去,这时候也会调用拷贝构造。局部对象生命周期在函数结束就销毁,返回的时候,需要复制一份临时对象给到接收的变量,这一步同样会触发。那段时间反复写小段测试代码,来回打断点,区分这两种容易混淆的场景,有时候编译器会做返回值优化,把拷贝给省略掉,断点就不会命中,还以为自己代码写错了,折腾好久才搞明白,优化机制会掩盖拷贝构造的调用行为,不能只靠断点判断规则。

还有直接初始化新对象的写法,ClassAb(a);,这是最直观的场景,用已经存在的a去初始化b,毫无疑问调用拷贝构造。但是有个容易混淆的写法,ClassAb=a;,很多人会误以为是赋值运算符,其实不是,这是初始化阶段,依旧走拷贝构造,不是赋值重载。赋值重载只会在两个已经存在的对象之间赋值的时候执行,对象都还没创建,不存在赋值一说,这点最容易写混。

写代码的时候,没注意拷贝构造频繁调用带来的负担,写循环,循环里面反复调用值传参的函数,程序运行速度明显变慢,对象里面动态分配的内存,拷贝构造没有深拷贝,直接浅拷贝复制指针,程序退出的时候直接崩溃,两个对象的析构函数去释放同一块堆内存。排查崩溃的时候,才意识到浅拷贝的问题会跟着拷贝构造一起出现,只要触发拷贝,指针就会被原样复制。

区分清楚触发条件之后,写类的时候,传递对象优先使用const引用,既能拿到对象的数据,又不会产生副本,避免不必要的拷贝构造调用,也能规避浅拷贝带来的内存问题。

到晚上关掉编辑器的时候,屏幕上还留着那段测试代码,几个断点标记停留在拷贝构造函数第一行。