今天碰到一件令我百思不得其解的问题:为什么拷贝构造函数
不按自己所预期的结果输出?
按 C++ 的语法来说,本该如此,并非自己理解有误而导致的!
功夫不负有心人,经过几天的搜索🔍、学习👨💻,我总算明白并解决了这个问题,特此输出该文记录一下。
📈背景知识
1. 构造函数
💛创建并初始化类的数据成员时调用
2. 析构函数
💚当对象生命周期终止时调用,用于释放对象占有的资源
3. 拷贝构造函数
❤调用时机:
- 将某个对象用于初始化另一个新创建的对象时
- 当对象作为参数传递给函数,且函数形参为普通对象时(因为引用对象不会调用拷贝构造函数)
- 对象作为函数的返回值时
💙注意:
- 如果在类中没有定义拷贝构造函数,编译器会自行定义一个;
- 如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。
🌄进入正题
先来看一段包含构造函数、析构函数、拷贝构造函数的简单代码,代码中穿插着许多注释,这里就不再一一解释。
本文旨在探索
拷贝构造函数
,构造函数与析构函数仅为顺带学习而提及,可略过这二者。顺带一提,注释中标注的各类函数调用顺序仅针对于预期结果。
#include <iostream>
using namespace std;
class Point {
public:
int x;
int y;
int *p;
Point(int xx, int yy, int *pp);
~Point();
Point(const Point &point);
};
// 构造函数
Point::Point(int xx, int yy, int *pp) : x(xx), y(yy) {
// 申请一块值为*pp的内存空间, 并让指针p指向它!
p = new int(*pp);
// p = new int;
// *p = *pp;
cout << "Point()" << endl;
}
// 析构函数
Point::~Point() {
delete p;
cout << this->x << "~Point()" << endl;
}
// 拷贝构造函数
Point::Point(const Point &point) {
this->x = point.x;
this->y = point.y;
p = new int;
*p = *point.p;
cout << "Copy-Constructor()" << endl;
}
// 参数为对象, 调用拷贝构造函数
// 若定义为 const Point &point 则不会调用拷贝构造函数: 因为 & 是引用, 会指向同一个对象, 而不是拷贝!
void display(Point point) {
point.x = 4;
}
// 返回值为对象, 调用拷贝构造函数
Point returnPoint() {
int c = 6;
Point point(7, 5, &c);
return point;
}
int main() {
int z = 3;
// 调用构造函数
Point point1(1, 2, &z); // 1.Point()、11.~Point()
// 情况1: 调用拷贝构造函数
Point point2 = point1; // 2.Copy-Constructor()、10.~Point()
point2.x = 2;
// 情况2: 调用拷贝构造函数
display(point2); // 3.Copy-Constructor()、4.~Point()
// 情况3: 调用拷贝构造函数???
Point point3 = returnPoint(); // 5.Point()、6.Copy-Constructor()、7.~Point()、8.Copy-Constructor()、9.~Point()、10.~Point()
point3.x = 10;
return 0;
}
运行结果
Point()
Copy-Constructor()
Copy-Constructor()
4~Point()
Point()
10~Point()
2~Point()
1~Point()
预期结果
🛕各类函数的调用顺序均已在注释中标明!
Point()
Copy-Constructor()
Copy-Constructor()
4~Point()
Point()
Copy-Constructor()
7~Point()
Copy-Constructor()
7~Point()
10~Point()
2~Point()
1~Point()
🤨分析原因
浅浅分析下运行结果与预期结果之间拷贝构造函数调用的差异。
如果你感兴趣,可以自己去调试下,最后发现问题出在 情况 3: Point point3 = returnPoint();
处,也就是函数的返回值为对象时。
为什么?!
🚀原来是 GCC 做了优化,当返回值为对象时,不再产生临时对象,因此不再调用拷贝构造函数。
再来对比下两个结果,可见直接把 2 个拷贝构造函数都优化掉了,
⭐这时候又会有人问了:诶,为什么是 2 个,情况 3 不就是对象作为函数返回值吗?不就只会调用 1 次拷贝构造函数吗?
// ...
Point returnPoint() {
int c = 6;
Point point(7, 5, &c);
return point;
}
int main() {
Point point3 = returnPoint();
// ...
}
就这部分代码而言,我的猜想是这样的:
returnPoint()
函数返回对象时,将其拷贝到一个临时对象temp
中(① 调用拷贝构造函数),然后释放函数中的局部对象;- 当执行到
Point point3 = returnPoint();
时,将对象赋值给point3
(② 再次调用拷贝构造函数),并释放临时对象temp
,最后释放point3
对象。
差不多是这么回事
当然以上没有很严谨的科学依据,但是经过我几番调试,输出结果也吻合,估计是八九不离十!
🌍解决办法
Q:如果一定想要让拷贝构造函数在这种情况下执行呢?
A:只需要让 GCC 不要优化:在编译命令中加入 -fno-elide-constructors
参数,例如 g++ -fno-elide-constructors CopyConstructor.cpp
.
我个人是使用的 C++ IDE 是 CLion,如下也给出相应的解决办法。
因为使用 IDE 就是为了快速编译运行,不可能每次都执行相应代码来运行程序,所以需要配置。
只需在 CMakeLists.txt
中添加如下代码:
# 添加编译选项! ==> 防止g++优化导致"返回对象不调用拷贝构造函数"!
add_definitions(-fno-elide-constructors)
🗺提醒一下:如果你的代码依赖于拷贝构造函数的副作用,那么你的代码就写得很烂。你编写的拷贝构造函数就应该保证这样的优化是安全的。
⛵最后
gcc 和 g++ 是什么,有什么区别?
发展至今,GCC 编译器的功能也由最初仅能编译 C 语言,扩增至可以编译多种编程语言,其中就包括 C++ 。
除此之外,当下的 GCC 编译器还支持编译 Go、Objective-C,Objective-C ++,Fortran,Ada,D 和 BRIG(HSAIL)等程序,甚至于 GCC 6 以及之前的版本还支持编译 Java 程序。
那么,在已编辑好 C 语言或者 C++ 代码的前提下,如何才能调用 GCC 编译器为我们编译程序呢?很简单,GCC 编译器已经为我们提供了调用它的接口,对于 C 语言或者 C++ 程序,可以通过执行 gcc
或者 g++
指令来调用 GCC 编译器。
值得一提的是:实际使用中我们更习惯使用 gcc
指令编译 C 语言程序,用 g++
指令编译 C++ 代码。需要强调的一点是,gcc
指令也可以用来编译 C++ 程序,同样 g++
指令也可以用于编译 C 语言程序。
⭐总结:
- gcc 是 GCC 中的 GUN C Compiler(C 编译器)
- g++ 是 GCC 中的 GUN C++ Compiler(C++编译器)
CMakeLists.txt 超傻瓜式教程
CMake 命令官网:cmake.org
# 本CMakeLists.txt的project名称
# 会自动创建两个变量,PROJECT_SOURCE_DIR和PROJECT_NAME
# ${PROJECT_SOURCE_DIR}:本CMakeLists.txt所在的文件夹路径
# ${PROJECT_NAME}:本CMakeLists.txt的project名称
project(xxx)
# 获取路径下所有的.cpp/.c/.cc文件,并赋值给变量中
aux_source_directory(路径 变量)
# 给文件名/路径名或其他字符串起别名,用${变量}获取变量内容
set(变量 文件名/路径/...)
# 添加编译选项
add_definitions(编译选项)
# 打印消息
message(消息)
# 编译子文件夹的CMakeLists.txt
add_subdirectory(子文件夹名称)
# 将.cpp/.c/.cc文件生成.a静态库
# 注意,库文件名称通常为libxxx.so,在这里只要写xxx即可
add_library(库文件名称 STATIC 文件)
# 将.cpp/.c/.cc文件生成可执行文件
add_executable(可执行文件名称 文件)
# 规定.h头文件路径
include_directories(路径)
# 规定.so/.a库文件路径
link_directories(路径)
# 对add_library或add_executable生成的文件进行链接操作
# 注意,库文件名称通常为libxxx.so,在这里只要写xxx即可
target_link_libraries(库文件名称/可执行文件名称 链接的库文件名称)