<<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. 静态成员变量不受影响,因为它们属于整个类,而不是各个对象。
相关推荐
测试界的酸菜鱼11 分钟前
Python 大数据展示屏实例
大数据·开发语言·python
我是谁??11 分钟前
C/C++使用AddressSanitizer检测内存错误
c语言·c++
晨曦_子画20 分钟前
编程语言之战:AI 之后的 Kotlin 与 Java
android·java·开发语言·人工智能·kotlin
Black_Friend28 分钟前
关于在VS中使用Qt不同版本报错的问题
开发语言·qt
发霉的闲鱼44 分钟前
MFC 重写了listControl类(类名为A),并把双击事件的处理函数定义在A中,主窗口如何接收表格是否被双击
c++·mfc
小c君tt1 小时前
MFC中Excel的导入以及使用步骤
c++·excel·mfc
希言JY1 小时前
C字符串 | 字符串处理函数 | 使用 | 原理 | 实现
c语言·开发语言
残月只会敲键盘1 小时前
php代码审计--常见函数整理
开发语言·php
xianwu5431 小时前
反向代理模块
linux·开发语言·网络·git
xiaoxiao涛1 小时前
协程6 --- HOOK
c++·协程