今天碰到一件令我百思不得其解的问题:为什么拷贝构造函数不按自己所预期的结果输出?按 C++ 的语法来说,本该如此,并非自己理解有误而导致的。

今天碰到一件令我百思不得其解的问题:为什么拷贝构造函数不按自己所预期的结果输出

按 C++ 的语法来说,本该如此,并非自己理解有误而导致的!

功夫不负有心人,经过几天的搜索🔍、学习👨‍💻,我总算明白并解决了这个问题,特此输出该文记录一下。

📈 背景知识

1. 构造函数

💛创建并初始化类的数据成员时调用

2. 析构函数

💚当对象生命周期终止时调用,用于释放对象占有的资源

3. 拷贝构造函数

❤调用时机:

  • 将某个对象用于初始化另一个新创建的对象
  • 当对象作为参数传递给函数,且函数形参为普通对象时(因为引用对象不会调用拷贝构造函数
  • 对象作为函数的返回值

💙注意:

  • 如果在类中没有定义拷贝构造函数,编译器会自行定义一个;
  • 如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。

🌄 进入正题

先来看一段包含构造函数析构函数拷贝构造函数的简单代码,代码中穿插着许多注释,这里就不再一一解释。

本文旨在探索拷贝构造函数,构造函数与析构函数仅为顺带学习而提及,可略过这二者。

顺带一提,注释中标注的各类函数调用顺序仅针对于预期结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#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;
}

运行结果

1
2
3
4
5
6
7
8
Point()
Copy-Constructor()
Copy-Constructor()
4~Point()
Point()
10~Point()
2~Point()
1~Point()

预期结果

🛕各类函数的调用顺序均已在注释中标明!

1
2
3
4
5
6
7
8
9
10
11
12
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 次拷贝构造函数吗?

1
2
3
4
5
6
7
8
9
10
11
// ...
Point returnPoint() {
int c = 6;
Point point(7, 5, &c);
return point;
}

int main() {
Point point3 = returnPoint();
// ...
}

就这部分代码而言,我的猜想是这样的:

  1. returnPoint() 函数返回对象时,将其拷贝到一个临时对象 temp 中(① 调用拷贝构造函数),然后释放函数中的局部对象;
  2. 当执行到 Point point3 = returnPoint(); 时,将对象赋值给 point3(② 再次调用拷贝构造函数),并释放临时对象 temp,最后释放 point3 对象。

差不多是这么回事

当然以上没有很严谨的科学依据,但是经过我几番调试,输出结果也吻合,估计是八九不离十!

🌍 解决办法

Q:如果一定想要让拷贝构造函数在这种情况下执行呢?

A:只需要让 GCC 不要优化:在编译命令中加入 -fno-elide-constructors 参数,例如 g++ -fno-elide-constructors CopyConstructor.cpp.

我个人是使用的 C++ IDE 是 CLion ,如下也给出相应的解决办法。

因为使用 IDE 就是为了快速编译运行,不可能每次都执行相应代码来运行程序,所以需要配置。

只需在 CMakeLists.txt 中添加如下代码:

1
2
# 添加编译选项! ==> 防止g++优化导致"返回对象不调用拷贝构造函数"
add_definitions(-fno-elide-constructors)

🗺提醒一下:如果你的代码依赖于拷贝构造函数的副作用,那么你的代码就写得很烂。你编写的拷贝构造函数就应该保证这样的优化是安全的。

⛵ 最后

gcc 和 g++ 是什么,有什么区别?

该段落出自:http://c.biancheng.net/view/7936.html

发展至今,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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 本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(库文件名称/可执行文件名称 链接的库文件名称)

✍️ Yikun Wu 已发表了 69 篇文章 · 总计 293k 字,采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处

🌀 本站总访问量