<<C++ Primer Plus(第6版)>>12.1 动态内存和类:知识记录

我的测试环境

所有代码win11的vs2017 版本15.9.24 中测试。

重点知识/结论总结

  1. 虽然原书中提到用等号来初始化值可能会导致调用赋值运算符(多执行赋值的步骤,效率低),但是我实测并没有此问题。当然,这个结果是符合预期的,毕竟明明是没必要去多执行一次赋值运算符来赋值的。但是不排除别的环境,别的VS版本,甚至在别的优化等级下表现不同。所以建议自行测试。
  2. 即便如此,还是建议初始化一个对象使用变量后面用括号赋值初始化的值的方式: StringBad ditto(motto);来规避可能的歧义和不确定的执行流程。
  3. 默认的复制构造函数和默认的赋值运算符都是浅拷贝/浅复制。所以在类中如果有成员变量是指针,指向在构造函数中动态申请的内存的时候,务必自定义复制构造函数和赋值运算符(而不是使用默认的),否则必出现同一块内存重复释放的问题!

正文开始

特殊成员函数

书中原文如下:

12.1.2 特殊成员函数

C++自动提供了下面这些成员函数:

• 默认构造函数,如果没有定义构造函数:

• 默认析构函数,如果没有定义;

• 复制构造函数,如果没有定义;

• 赋值运算符,如果没有定义:

• 地址运算符,如果没有定义。

下面为了方便举例,我们假设有这样一个类:

C++ 复制代码
class A {
	int a;
public:
	int geta(void) { return a; }
	...
};

1. 默认构造函数

如果没有提供任何构造函数,C++将创建默认构造函数,即编译器将提供一个不接受任何参数 ,也不执行任何操作的构造函数。函数形如:

cpp 复制代码
A::A() { }  // 通过默认构造函数创建的对象的值是未知的

验证代码:

cpp 复制代码
#include<iostream>
using namespace std;

class A {
	int a;
public:
	int geta(void) { return a; }
};

#if 1
int main(void) {
	A a;
	cout << a.geta() << endl;   // 输出结果不一定
}
#endif

对于没有提供任何构造函数的理解是,你不能定义一个A::A(int n){a = n;}的构造函数,然后还期望存在一个默认构造函数。

更具体的说就是,如果你定义了A::A(int n){a = n;}的构造函数,那么你只能这样创建类A的对象,而不赋初值是非法的。

验证代码:

cpp 复制代码
#include<iostream>
using namespace std;

class A {
	int a;
public:
	int geta(void) { return a; }
	A(int t) { a = t; }
};

#if 1
int main(void) {
	A a(3);
	cout << a.geta() << endl;

	A b;  // 此处编译报错,提示如下:
//错误	C2512	"A": 没有合适的默认构造函数可用
//错误(活动)	E0291	类 "A" 不存在默认构造函数

	cout << b.geta() << endl;
}
#endif

2. 复制构造函数

它用于初始化过程中(包括按值传递参数)而不是常规赋值过程中!复制构造函数原型通常如下:

cpp 复制代码
Class_name(const Class_name &);

何时调用复制构造函数

新建上个对象并将其初始化为同类现有对象时。下面4种声明都将调用复制构造函数

cpp 复制代码
StringBad ditto(motto);  // calls StringBad(const StringBad &)
StringBad metoo = motto; // calls StringBad (const StringBad &)
StringBad also = StringBad(motto);  // calls StringBad(const StringBad &)
StringBad * pStringBad = new StringBad(motto);  // calls StringBad (const StringBad &)

前3种形式多少为了初始化一个对象(而不是一个对象指针),但是由于第2,3中方式中有等号,所以可能 会生成一个临时变量,然后调用赋值运算符来赋值。
所以初始化一个对象强烈建议使用方式一,即变量后面用括号赋值初始化的值的方式 ,这样能避免调用赋值运算符来赋值,能保证执行更快是一方面,另一方面是确保不会执行赋值运算符来赋值。

至于如何验证是仅使用复制构造函数还是也会使用赋值运算符来赋值,那就需要自定义复制构造函数和赋值运算符,在里面加打印来确认了。

验证过程以及结果

先说结论,上述的方式全部都只会使用复制构造函数,不会使用赋值运算符来赋值。当然,这个结果是符合预期的,毕竟明明是没必要去多执行一次赋值运算符来赋值的。但是不排除别的环境,别的VS版本,甚至的别的优化等级下表现不同。所以建议自行测试。

验证代码:

cpp 复制代码
#include<iostream>
using namespace std;

class A {
	int a;
public:
	int geta(void) { return a; }
	A(int t) { a = t; }
	A(const A& t) { a = t.a; cout << "use copy \n"; }
	A & operator=(const A& t) { a = t.a; cout << "use operator\n"; return *this; }
};

#if 1
int main(void) {
	A a(1004);

	cout << "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n    init way: A aa(a)\n";
	A aa(a);
	cout << aa.geta() << "\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n";

	cout << "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n    init way: A b=a\n";
	A b=a;
	cout << b.geta() << "\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n";

	cout << "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n    init way: A c = A(a);\n";
	A c = A(a);
	cout << c.geta() << "\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n";

	cout << "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n    init way: A* p = new A(a);\n";
	A* p = new A(a);
	cout << p->geta() << "\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n";


	cout << "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n    operator= test: b=a\n";
	b = a;
	cout << b.geta() << "\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n";
}
#endif

输出结果:

vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    init way: A aa(a)
use copy
1004
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    init way: A b=a
use copy
1004
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    init way: A c = A(a);
use copy
1004
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    init way: A* p = new A(a);
use copy
1004
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    operator= test: b=a
use operator
1004
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

默认复制构造函数的功能

  1. 默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。
  2. 如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。
  3. 静态成员变量不受影响,因为它们属于整个类,而不是各个对象。

3. 赋值运算符

ANSI C允许结构赋值,而C++允许类对象赋值,这是通过自动为类重载赋值运算符实现的。

原型

这种运算符的原型如下:

Class_name & Class_name: :operator=(const Class_name &) ;

它接受并返回一个指向类对象的引用。例如,StringBad类的赋值运算符的原型如下:

StringBad & StringBad :: operator=(const StringBad &);

默认赋值运算符的功能

与默认复制构造函数的功能类似,标黄的的差异点。默认赋值运算符的功能是:

  1. 逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。
  2. 如果成员本身就是类对象,则将使用这个类的赋值运算符来复制对象。
  3. 静态成员变量不受影响,因为它们属于整个类,而不是各个对象。
相关推荐
练小杰7 分钟前
Linux系统 C/C++编程基础——基于Qt的图形用户界面编程
linux·c语言·c++·经验分享·qt·学习·编辑器
勤又氪猿8 分钟前
【问题】Qt c++ 界面 lineEdit、comboBox、tableWidget.... SIGSEGV错误
开发语言·c++·qt
Ciderw20 分钟前
Go中的三种锁
开发语言·c++·后端·golang·互斥锁·
查理零世21 分钟前
【算法】经典博弈论问题——巴什博弈 python
开发语言·python·算法
jk_1011 小时前
MATLAB中insertAfter函数用法
开发语言·matlab
啥也学不会a1 小时前
PLC通信
开发语言·网络·网络协议·c#
C++小厨神1 小时前
C#语言的学习路线
开发语言·后端·golang
心之语歌2 小时前
LiteFlow Spring boot使用方式
java·开发语言
人才程序员2 小时前
【C++拓展】vs2022使用SQlite3
c语言·开发语言·数据库·c++·qt·ui·sqlite
OKkankan3 小时前
实现二叉树_堆
c语言·数据结构·c++·算法