C++Primer Plus书之--C++动态内存和类, 复制构造函数和赋值运算符

从一个例子开始:

// 第一个文件
// stringbad.h
#include <iostream>
#ifndef STRINGBAD_H_
#define STRINGBAD_H_

class StringBad
{
private:
	// 指向字符串的指针
	char * str;
	// 字符串的长度
	int len;
	static int num_strings;

public:
	// 构造函数
	StringBad(const char * s);
	StringBad();
	// 析构函数
	~StringBad();
	// 友元函数
	// 重载了运算符<<
	friend std::ostream & operator<<(std::ostream & os, const StringBad & st);
};

#endif

第二个文件:

// stringbad.cpp
#include <cstring>
#include "stringbad.h"

using std::cout;
using std::endl;

// 初始化类的静态成员
int StringBad::num_strings = 0;

// 构造函数
StringBad::StringBad(const char* s)
{
	len = std::strlen(s);
	str = new char(len + 1);
	// 初始化指针str
	std::strcpy(str, s);
	num_strings++;
	cout << num_strings << ": " << str << " object created" << endl;
}

StringBad::StringBad()
{
	len = 4;
	str = new char[4];
	std::strcpy(str, "C++");
	num_strings++;
	cout << num_strings << ": " << str << " default object created" << endl;
}

// 析构函数
// 这个是必要的, 因为在析构函数中对new申请的内存进行了释放
StringBad::~StringBad()
{
	cout << str << " object deleted, ";
	--num_strings;
	cout << num_strings << " left" << endl;
	delete[] str;
}

// 重载了<<运算符, 友元函数
std::ostream & operator<<(std::ostream & os, const StringBad &st)
{
	os << st.str;
	return os;
}

要注意下面这句话

int StringBad::num_strings = 0;

要注意不能在类声明中初始化静态成员变量(类声明位于头文件中, 程序可能将头文件包含在多个文件中, 如果在头文件中进行初始化, 将出现多个初始化语句副本, 但是如果静态成员是整型或者枚举类型const 则可以在类声明中初始化), 这是因为声明描述了如何分配内存, 但并不分配内存. 对于静态类成员, 可以在类声明之外单独使用语句来进行初始化, 这是因为静态类成员是单独存储的, 而不是对象的组成部分. 注意初始化语句指明了类型, 并用了作用域运算符, 但没有使用关键字static

之所以使用下面的语句:

str = new char[4];
std::strcpy(str, "C++");

而不用:

str = s;

是因为字符串并不保存在对象中, 字符串单独保存在堆内存中, 对象仅保存了指出到哪里去查找字符串的信息.

在构造函数中使用new来分配内存时, 必须在相应的析构函数中使用delete来释放内存. 如果使用new[]来分配内存, 则应使用delete[]来释放内存.

第三个文件:

// 第三个文件
// vegnews.cpp
// compile with stringbad.cpp
#include <iostream>
#include "stringbad.h"

using std::cout;

void callme1(StringBad &);
void callme2(StringBad);

int main()
{
	using std::endl;
	{
		cout << "Starting an inner block." << endl;
		StringBad headline1("the first sentence");
		StringBad headline2("the second sentence");
		StringBad sports("sports sentence");
		cout << "headline1: " << headline1 << endl;
		cout << "headline2: " << headline2 << endl;
		cout << "sports: " << sports << endl;
		callme1(headline1);
		cout << "headline1: " << headline1 << endl;
		callme2(headline2);
		cout << "headline2: " << headline2 << endl;
		
		cout << "Initialize one object ot another:" << endl;
		StringBad sailor = sports;
		cout << "sailor: " << sailor << endl;
		cout << "Assign one object ot another: " << endl;
		StringBad knot;
		knot = headline1;
		cout << "knot: " << knot << endl;
		cout << "Exiting the block" << endl;
	}
	cout << "End of main()" << endl;
	return 0;
}

void callme1(StringBad & rsb)
{
	cout << "String passed by reference:" << std::endl;
	cout << "    \"" << rsb << "\"" << std::endl;
}

void callme2(StringBad rsb)
{
	cout << "String passed by value:" << std::endl;
	cout << "    \"" << rsb << "\"" << std::endl;
}

程序运行结果为:

从结果中可以看得出来, 程序没有按预期的运行, delete数据执行的过多, 最后程序也跑出了异常

下面分析出错的原因:

看下面这行代码:

StringBad sailor = sports;

这行代码并不会使用构造函数, 这种写法等效于:

StringBad sailor = StringBad(sports);

因为sports的类型为StringBad, 因此相应的构造函数原型如下:

StringBad(const StringBad &);

当我们使用一个对象来初始化另一个对象时, 编译器将自动生成上述构造函数(称为复制构造函数, 因为它创建对象的一个副本). 自动生成的构造函数不知道需要更新静态变量num_string, 因此会将计数搞乱.

特殊成员函数:

首先C++会自动为类提供下面这些成员函数:

1.默认构造函数, 如果没有定义构造函数

2.默认析构函数, 如果没有定义

3.复制构造函数(见上述的例子), 如果没有定义

4.赋值运算符, 如果没有定义

5.地址运算符, 如果没有定义

结果表明, StringBad类中的问题是由隐式复制构造函数和隐式复制运算符引起的.

复制构造函数

复制构造函数用于将一个对象复制到新创建的对象中, 也就是说, 它用于初始化过程中(包括按值传递参数), 而不是常规的赋值过程中, 类的复制构造函数原型通常如下:

Class_name(const Class_name &);

例如: StringBad类的复制构造函数原型如下:

StringBad(const StringBad &);

何时调用复制构造函数:

新建一个对象并将其初始化为同类现有对象时, 复制构造函数都将被调用. 这在很多情况下都可能发生, 最常见的情况是将新对象显示地初始化为现有的对象. 例如: 假设motto是一个StringBad对象, 则西面4中声明都将调用复制构造函数:

StringBad ditto(motto);
StringBad metoo = motto;
StringBad also = StringBad(motto);
StringBad * pStringBad = new StringBad(motto);

其中中间的2中声明可能会使用复制构造函数直接创建metoo和alse, 也可能使用复制构造函数生成一个临时对象, 然后将临时对象的内容赋给metoo和also, 这取决于具体实现. 最后一种声明使用motto初始化一个匿名对象, 并将新对象的地址赋给pStringBad指针.

每当程序生成了对象副本时, 编译器都将使用复制构造函数. 具体说, 当函数按值传递对象或函数返回对象时, 都将使用复制构造函数. 按值传递意味着创建原始变量的一个副本, 编译器生成临时对象时, 也将使用复制构造函数.

例如上面的例子中的

callme2(headline2);

默认的复制构造函数的功能:

默认的复制构造函数逐个复制非静态成员(成员复制也成浅复制), 复制的是成员的值

在上面的StringBad类的例子中:

StringBad sailor = sports;

与下面的代码等效:

StringBad sailor;
sailor.str = sports.str;
sailor.len = sports.len;

如果成员本身就是类对象, 则将使用这个类的复制构造函数来复制成员对象. 静态成员(如num_strings)不受影响, 因为他们属于整个类, 而不是对象.

上面那个StringBad类出问题的地方就是:

1.callme2()被调用的时候, 复制构造函数被用来初始化callme2()的形参

2.复制构造函数还被用来将对象sailor初始化为对象sports.

这两个地方导致没有正常调用num_strings的更新, 但是临时变量的析构函数仍然更新了计数.

解决办法是提供一个对计数进行更新的显示复制构造函数:

StringBad::StringBad(const StringBad & s)
{
    ...
    num_string++;
    ...
}

提示:如果类中包含这样的静态数据成员, 即其值将在新对象被创建时发生变化, 则应该提供一个显示复制构造函数来处理计数问题.

另一处异常则更微妙, 也更危险

原因在于隐式复制构造函数是按值进行复制的, 例如StringBad例子中, 隐式复制构造函数的功能相当于:

sailor.str = sport.str;

这里复制的并不是字符串, 由于str是个char*, 所以是个指向字符串的指针, 因此当调用析构函数的时候delete[] sailor.str; 相当于将原本的字符数组的空间内容给释放了

完整的复制构造函数如下, 实现的是深度复制(deep copy), 也就是说, 复制构造函数应当复制字符串并将副本的地址赋给str成员, 而不仅仅是复制字符串地址.

StringBad::StringBad(const StringBad & st)
{
	num_string++;
	len = st.len;
	str = new char[len + 1];
	std::strcpy(str, st.str);
	cout << num_string << ": " << "Object created" << std::endl;
}

赋值运算符

StringBad的其他问题: 赋值运算符

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

赋值运算符的原型如下:

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

StringBad的赋值运算符的原型如下:

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

赋值运算符的功能以及何时使用它:

将已有的对象赋给另一个对象时, 将使用重载的赋值运算符:

StringBad headlin1("asdfljk");
...
StringBad knot;

// 使用赋值运算符
knot = headlin1;

// 这里metoo是一个新创建的对象, 被初始化Wieknot的值, 因此使用的是复制构造函数
// 也有可能会使用赋值运算符
StringBad metoo = knot;

与复制构造函数相似, 赋值运算符的隐式实现也是对成员进行逐个复制.

StringBad的例子中:

knot = headline1;

这个使用赋值运算符, 也是调用析构函数的时候会造成问题.

解决办法就是重载赋值运算符:

StringBad & StringBad::operator=(const StringBad & st)
{
	// 函数应避免将对象赋值给自身: 否则, 给对象重新赋值前, 释放内存操作可能删除对象的内容.
	if(this == &st)
		return *this;
	// 由于str中可能存在以前分配的数据, 因此函数应使用delete[] 来释放这些数据.
	delete[] str;
	len = st.len;
	str = new char[len + 1];
	std::strcpy(str, st.str);
	// 函数返回一个指向调用对象的引用.
	return *this;
}

猜你喜欢

转载自blog.csdn.net/c1392851600/article/details/84996618