Learn:C++ Primer Plus Chapter12

第12章 动态内存和类

Abstract

本章主要要介绍如何使用 动态内存 来创建和使用类,涉及到 constructordestructor 相应编写技巧。

  • 特殊成员函数
  • 类返回值
  • 类指针 this
  • 初始化列表

1. 特殊成员函数

1.1 An example

In the Book, using 一个例子来引出需要解决和注意的问题,我将这个例子进行了简化,同时也会在 Blog 中展示在汇编层面的代码是什么样的。

代码由 12.1.h12.1.cpp 两个文件 consist of,较为简单,唯一值得一说的便是在 Chapter 10 中介绍过的,静态成员,即通过 static 关键字进行修饰的变量,该变量为 class 中所有的对象所共用,存储在全局变量区。

  • 12.1.h
#ifndef CHAPTER_12_1_H
#define CHAPTER_12_1_H
#include <iostream>

class StringBad {
	// save the pointer of the string which will use new to allocate memory
	char* str;
	// length of the string
	int len;
	// a counter to save the number of string, use for debug
	static int nNumStr;
public:
	StringBad(const char* s);
	StringBad();
	~StringBad();
	friend std::ostream& operator<<(std::ostream&, const StringBad&);
};

#endif // !12_1_H
  • 12.1.cpp
#include "12.1.h"
using std::cout;

// initial the static member in the class StringBad
int StringBad::nNumStr = 0;

StringBad::StringBad(const char* s) {
	len = strlen(s);
	str = new char[len + 1];
	strcpy(str, s);
	nNumStr++;
	cout << "Create String:\n" << "\t" << str << " nNumber is " << nNumStr << ".\n";
	return;
}

StringBad::StringBad() {
	len = 3;
	str = new char[len + 1];
	strcpy(str, "C++");
	nNumStr++;
	cout << "Create Default string. " << "nNumber is " << nNumStr << ".\n";
	return;
}

StringBad::~StringBad() {
	cout << "Destruct String:\n" << "\t\"" << str << "\"";
	delete[] str;
	nNumStr--;
	cout << " nNumStr is " << nNumStr << ".\n";
	return;
}

std::ostream& operator<<(std::ostream& os, const StringBad& sbStr) {
	os << sbStr.str << "\n";
	return os;
}

void TransByRef(StringBad& sbStr) {
	cout << "String Transfer by Reference:\n";
	cout << "\t" << sbStr;
	return;
}

void TransByVal(StringBad sbStr) {
	cout << "String Transfer by Value:\n";
	cout << "\t" << sbStr;
	return;
}

int main() {
	// initial two StringBads
	StringBad sbA("Naruto");
	StringBad sbB("Sasuke");

	TransByRef(sbA);
	TransByVal(sbA);

	StringBad sbC;
	sbC = sbB;

	cout << "sbA is " << sbA << "\n";
	cout << "sbB is " << sbB << "\n";
	cout << "sbC is " << sbC << "\n";

	return 0;
}

Below is 上面代码的运行结果,可以看到在调用完 TransByVal 函数之后,运行了一次 ~StringBad 析构函数。将 sbA 中的指向的字符串释放了。

后面 sbC 通过默认构造函数进行创建时可能申请到了 sbA 一开始使用的内存导致后面在输出 sbA 时的内容变为了 unexpected 默认字符串。

最后是运行结束后进行析构函数的调用,当调用到释放 sbB 时就出现了异常导致程序终止(为什么是 sbB 因为编译器按照栈的方式添加的析构函数调用)。
在这里插入图片描述

  1. sbA unexpected 问题
    TransByVal 函数直接传递参数,会在栈上生成一个临时变量,相当于有一个构造函数完成了这个工作。
    在这里插入图片描述
    Before TransByVal 函数返回,就需要调用相应的析构函数来将这个变量进行释放。由于传递参数时采用的是 shallow copy 所以,会将 sbA.str 指向的内存释放,导致后续 sbA.str 变为一个 unexpected 的默认字符串,变为默认字符串只是一个巧合,与系统的内存分配策略相关。
    在这里插入图片描述
  2. 析构异常
    main 函数结束前会将栈中存储的 StringBad 进行析构,也就由 compiler 添加 calling 析构函数 implicitly。上面执行到 sbB 时发生异常,导致程序终止,这个异常产生的原因同样是 shallow copy。由于前面的复制操作 sbC = sbB 导致 when 析构 sbC 时就将 sbB.str 指向的内存释放了。
    在这里插入图片描述

1.2 问题归因

StringBad 类的问题是由特殊成员函数引起的,这些成员函数是自动定义的,as for StringBad,函数的 behavior 与类的 design 不符。C++ 自动提供了 below 成员函数 when using without definition。

  • default constructor
  • default destructor
  • copy constructor
  • assign operator(=)
  • address operator (&)

上述 example 中出现的问题是由 implicit 复制构造函数赋值运算符 引起的。

1.3 解决方案

定义 copy constructorassign operator explicitly,并在其中使用 deep copy 取代 shallow copy

  1. 复制构造函数
    复制构造函数 的编写没有什么太多需要强调的,就将 class 中需要使用 newnew[] 的成员也需要使用相应的操作申请内存。
    一般函数 PrototypeClassName(const ClassName&);。
StringBad::StringBad(const StringBad& sbStr) {
	len = sbStr.len;
	// allocate memory for new string
	str = new char[len + 1];
	// copy the string to new memory complete deep copy
	strcpy(str, sbStr.str);
	nNumStr++;
	cout << "Copy constructor create. " << "nNumber is " << nNumStr << ".\n";
	return;
}
  1. 赋值运算符
    编写 赋值运算符 重载时有一些注意步骤。
  • 首先需要排除掉给自身赋值的操作。
  • 接着原先通过 newnew[] 申请的内存进行释放。
  • 最后进行 deep copy
  • 函数的返回值也必须是 ClassName& 这样才能使得连续赋值可行,即 A = B = C。
StringBad& StringBad::operator=(const StringBad& sbStr) {
	// 1. check if assign to self
	if (this == &sbStr)
		return *this;
	// 2. release the previous memory
	delete[] str;
	// 3. deepcopy
	len = sbStr.len;
	//		allocate memory for new string
	str = new char[len + 1];
	//		copy the string to new memory complete deep copy
	strcpy(str, sbStr.str);
	nNumStr++;
	cout << "Assign operator create. " << "nNumber is " << nNumStr << ".\n";
	return *this;
}

添加上面两个函数的 explicit 定义后,重新运行程序,之前的问题都解决了。
在这里插入图片描述

1.4 编程技巧

Through 上面的例子抛砖引玉,可以得到这样的 Programing Principle 当定义 customed 类中用到了 newnew[] 进行动态内存分配时,It’s that 你 have to 自定义 copy constructorassign operator lest 出现运行错误。

  • copy constructor
//
ClassName::ClassName(const ClassName& clsArg) {
	//deep copy the content
	...
}
  • assign operator
ClassName& ClassName::operator=(const ClassName& clsArg) {
	//1. check if assign to self
	if(this == &clsArg) 
		return *this;
	//2. release the previous memory
	...
	//3. deep coyp the content
	...
}
  • 静态成员函数
    最后提一点关于 静态成员函数,which is 用来访问 class 中的静态成员,if it’s public。静态成员函数 无法访问每个对象的特定数据,only for static 成员。

例子中可以定义这样的静态成员函数来访问 privatenNumStr

class StringBad {
	...
	static int nNumStr
	...
public:
	...
	static int HowMany() {return nNumStr;}
	...
};

// use like this
int nNum = StringBad::HowMany();
  • preemptive definition
    在某些情况下,你在类中使用了 new 运算符,但是又不 want 麻烦的去定义 copy constructorassign operator,because 暂时用不到。But when error occurs,你又 forget 了没有进行相应的定义,此时编译器也不会给出任何提示,你就在一片骂声中开始 debug 了。

    为了防止这种情况的出现,让编译器在能够给出相应的报错信息,我们可以采用一个伪 private 的方式解决。

class ClassA {
private:
	//preemptive definition
	ClassA (const ClassA& cls) {}
	ClassA& operator=(const ClassA& cls) { return *this;}
};

使用上面的方式进行定义后下面的语句就是非法的,编译器就会给出报错。

ClassA cls1, cl2;
ClassA cls3(cls1);	//can't use the private method outside object
cls1 = cls2;		//can't assign

当然这种方案并不是完美的,只是权宜之计。当使用 Value 进行传参时将使用 copy constructor 创建临时变量,此时如果是成员函数,则编译会顺利通过,但如果遵守 Reference 的原则这个也是可以尽量避免的。

2. 类返回值

函数返回一个类的 Object,存在以下几种情况:

  • Reference
  • const Reference
  • Value
  • const Value

考虑到按值返回的方式只能返回一个临时对象,当其完成后续操作后就会被调用 destructor 被释放掉,所以对其进行修改是没有意义,甚至会因为其能够被修改而导致一些逻辑错误没有被即使发现,所以在返回对象时最好不要返回 non-const 的 Value 值。

2.1 返回 non-const/const Reference

对于返回值为对象引用的函数,返回的引用必须在函数调用前就已经存在了,不能返回在 callee 中的局部变量。返回引用时会不触发 copy constructor

2.2 返回 const Value

Assume that 这里有一个类,它重载了 operator+,并且其返回值为 non-const 类型的 Value。那么你在编写代码时可能会将比较运算符 == 错误的写成了赋值运算符 = ,此时你会编译器能够正常的编译而不会给出任何 error 信息。

这是因为返回的是一个临时的 non-const 变量,可以进行赋值操作。

if(clsA + clsB = clsC)	// you write the operator carelessly
if(clsA + clsB == clsC)

当如果将返回值限定为 const,则clsA + clsB = clsC 操作就是错误的,编译器将无法正常编译。

3. 类指针 this

这里 fishwheel 只想提一下有关定位 new 运算符相关的内容,只需要记住一句话,定位 new 需要程序员自行管理内存,释放内存时需要调用析构函数 explicitly,而不是通过 delete implicitly 调用。

4. 初始化列表

初始化列表(member initializer list),是 C++ 构造函数中特有的初始化方式,only 能在 constructor 中使用。当遇到 非静态 const 变量和 Reference 变量时 have to 使用该方式进行初始化。

class ClassA  {
	const int nNum;	//non-static const var
	ClassB& rclsB;	//reference var
public:
	ClassA(ClassB& rcls) : nNum(0), rclsB(rcls) {
	
	}
};

member initializer list 是在变量创建是完成的,也就是在函数体 { } 中的代码执行之前就已经完成了。

C++ 11 标准中提供了另一种初始化变量的方式,类内初始化。即在类的声明中添加了初始化值。

class ClassA  {
	int a = 1;
	int b = 2;
public:
	ClassA() {}
}

Unarchived Verify

  1. 验证临时变量的析构时机。

使用 1.1 的例子中的 StringBad 类,将字符串 “What hell” 赋值给 sbA,由于存在 StringBad(const char* s) 构造函数,此时会作为 转换函数 创建一个临时的对象,然后再将其赋值给 sbA

int main() {
	StringBad sbA;
	sbA = "What hell";
	StringBad sbB("good"), sbC("very");
	cout << sbA << "\n";
	cout << sbB << "\n";
	cout << sbC << "\n";
	return 0;
}

运行结果如下所示:
在这里插入图片描述
IDA 中可以看到,确实是创建了临时变量,并通过临时变量将值赋值给了 sbA,当赋值完毕之后 calling 析构函数 immediately。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值