cpp►动态内存分配与析构之复制构造函数/赋值运算符

Posted itzyjr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了cpp►动态内存分配与析构之复制构造函数/赋值运算符相关的知识,希望对你有一定的参考价值。

动态内存分配

C++在分配内存时,让程序在运行时决定内存分配,而不是在编译时决定。这样,可根据程序的需要,而不是根据一系列严格的存储类型规则来使用内存。C++使用new和delete运算符来动态控制内存。遗憾的是,在类中使用这些运算符将导致许多新的编程问题。在这种情况下,析构函数将是必不可少的,而不再是可有可无的。有时候,还必须重载赋值运算符,以保证程序正常运行。

// stringbad.h
class StringBad {
private:
	char* str;// pointer to string
	int len;// length of string
	static int num_strings;// number of objects
public:
	StringBad(const char* s);
	StringBad();
	~StringBad();
	friend std::ostream& operator<<(std::ostream& os, const StringBad& st);
};
// stringbad.cpp
#include <cstring>
#include "stringbad.h"
int StringBad::num_strings = 0;// 初始化静态类成员
StringBad::StringBad(const char* s) {
	len = std::strlen(s);
	str = new char[len + 1];// 分配内存
	std::strcpy(str, s);// 不能str=s;
	num_strings++;
	cout << num_strings << ": '" << str << "' object created\\n";
}
StringBad::StringBad() {// 默认构造函数
	len = 4;
	str = new char[4];
	std::strcpy(str, "C++");
	num_strings++;
	cout << num_strings << ": '" << str << "' default object created\\n";
}
StringBad::~StringBad() {
	cout << "'" << str << "' object deleted, ";
	--num_strings;
	cout << num_strings << " left\\n";
	delete[] str;
}
std::ostream& operator<<(std::ostream& os, const StringBad& st) {
	os << st.str;
	return os;
}

不能这样做:str = s;因为这只保存了地址,而没有创建字符串副本。
当StringBad对象过期时,str指针也将过期。但str指向的内存仍被分配,除非使用delete将其释放。==删除对象可以释放对象本身占用的内存,但并不能自动释放属于对象成员的指针指向的内存。因此,必须使用析构函数。==在析构函数中使用delete语句可确保对象过期时,由构造函数使用new分配的内存被释放。

#include <iostream>
#include "stringbad.h"
void callme1(StringBad&);
void callme2(StringBad);
int main() {
	{
		cout << "Starting an inner block.\\n";
		StringBad headline1("Celery");
		StringBad headline2("Lettuce");
		StringBad sports("Spinach");
		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 to another:\\n";
		StringBad sailor = sports;
		cout << "sailor: " << sailor << endl;
		cout << "Assign one object to another:\\n";
		StringBad knot;
		knot = headline1;
		cout << "knot: " << knot << endl;
		cout << "Exiting the block.\\n";
	}
	cout << "End of main()\\n";
	return 0;
}
void callme1(StringBad& rsb) {
	cout << "String passed by reference: '" << rsb << "'" << endl;
}
void callme2(StringBad sb) {
	cout << "String passed by value: '" << sb << "'" << endl;
}

以下是程序运行结果及【】解释:

Starting an inner block.【▰提示句】
1: 'Celery' object created【▰构造函数调用。(num_strings++)=1】
2: 'Lettuce' object created【▰构造函数调用。(num_strings++)=2】
3: 'Spinach' object created【▰构造函数调用。(num_strings++)=3】
headline1: Celery【▰正常打印】
headline2: Lettuce【▰正常打印】
sports: Spinach【▰正常打印】
String passed by reference: 'Celery'【▰正常打印】
headline1: Celery【▰按引用传递,生命周期不在函数块,不会析构,正常打印】
String passed by value: 'Lettuce'【▰正常打印】
'Lettuce' object deleted, 2 left【▰按值传递,headline2对象拷贝一份给形参,在此调用了默认的复制构造函数,函数块结束,形参对象被析构。(--num_strings)=2】
【✦默认复制构造函数只拷贝成员变量,即拷贝char* str; 和 int len; 形参对象被析构时,str指针所指内存被释放】
headline2: 葺葺葺葺葺葺葺葺軸【▰拷贝的形参对象被析构delete[] str; 导致str指针所指内存被释放,由于str是个指针,拷贝的是个地址,而传递进函数的对象地址与拷贝的地址相同,所以原对象地址所指空间已经被释放掉了,所以打印出随机内存里面的乱码是应该的】
 
Initialize one object to another:【▰提示句】
【✦StringBad sailor = sports; 调用默认赋值运算符,同样拷贝的是地址】
sailor: Spinach【▰正常打印】
Assign one object to another:【▰提示句】
【✦StringBad knot; 调用默认构造函数初始化】
3: 'C++' default object created【▰默认构造函数调用。(num_strings++)=3】
knot: Celery【▰正常打印】
【✦knot = headline1; 调用默认赋值运算符,拷贝的是地址】
Exiting the block.【▰提示句】
【✦块结束,按创建顺序相反的顺序析构局部对象,析构顺序:knot、sailor、sports、headline2、headline1】
'Celery' object deleted, 2 left【▰块结束,knot对象被析构,导致headline1指向内存已释放。(--num_strings)=2】
'Spinach' object deleted, 1 left【▰块结束,sailor对象被析构,导致sports指向内存已释放。(--num_strings)=1】
'葺葺葺葺葺葺葺葺葺葺葺葺葺葺葺葺葺葺P' object deleted, 0 left【▰块结束,sports对象被析构。而sports指向内存已释放,乱码是应该的。(--num_strings)=0】
'@g' object deleted, -1 left【▰块结束,headline2对象被析构。而headline2指向内存已释放,乱码是应该的。(--num_strings)=-1】
'-|' object deleted, -2 left【▰块结束,headline1对象被析构。而headline1指向内存已释放,乱码是应该的。(--num_strings)=-2】
End of main()【▰提示句】

从上面输出结果与注释,我们可以看到,StringBad类出问题的原因在于默认的复制构造函数与默认的赋值运算符。
要想解决问题,那就得自定义复制构造函数与重载赋值运算符。

何时调用复制构造函数
// 假设motto是一个StringBad对象
StringBad ditto(motto);// 调用StringBad(const StringBad&)
StringBad metoo = motto;// 调用StringBad(const StringBad&)或operator=
StringBad also = StringBad(motto);// 调用StringBad(const StringBad&)
StringBad *pStringBad = new StringBad(motto);// 调用StringBad(const StringBad&)
如果重载了operator+(),则下面语句将调用复制构造函数:
StringBad x, y, z;
x + y = z; // x + y创建【临时对象】再被赋值z,创建临时对象时调用StringBad(const StringBad&)或operator=

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

自定义一个显式复制构造函数以解决问题:

StringBad::StringBad(const StringBad & st) {
    num_strings++; // 处理静态成员的更新
    len = st.len; // 相同长度
    str = new char[len + 1]; // 分配空间
    std::strcpy(str, st.str); // 复制字符串到新地址
    cout << num_strings << ": '" << str << "' object created\\n";
}

如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深度复制。复制的另一种形式(成员复制或浅复制)只是复制指针值。浅复制仅浅浅地复制指针信息,而不会深入“挖掘”以复制指针引用的结构。

重载赋值运算符:

StringBad& StringBad::operator=(const StringBad & st) {
    if (this == &st) // 它自己==对象st,地址相同
        return *this;// return invoking object
    delete [] str; // 释放调用对象原str所指内存空间!必要!重要!
    len = st.len;
    str = new char[len + 1]; // 为新字符串分配空间
    std::strcpy(str, st.str); // copy the string
    return *this; // return reference to invoking object
}

因为定义的两个构造都有new char[],故可delete[],this指的是调用对象(invoking object)的地址,假设调用对象为invokeObj,被赋值对象为abcObj,则使用赋值运算时,若重载了,则要注意:

  1. 代码首先检查自我复制,这是通过查看赋值运算符右边的地址(&st)是否与接收对象(this)的地址相同来完成的。如果相同,程序将返回*this,然后结束。

  2. invokeObj里面有str=new char[],所以给它赋值时,如果不清除原来的动态内存分配,就内存泄露了,所以要配套使用delele[];

  3. 若invokeObj与abcObj的引用相同,用了delete[]后就释放掉了invokeObj和abcObj的str成员了。既然相同,赋值就没必要了,直接返回调用对象的引用*this即可。之所以返回调用对象的引用是为了满足连续调用的需要,如invokeObj=lmnObj=xyzObj。

将前面介绍的复制构造函数和赋值运算符添加到StringBad类中后,所有的问题都解决了。

关于临时对象
1. net = force1 + force2;
2. force1 + force2 = net;

复制构造函数将创建一个临时对象来表示返回值。因此,在前面的代码中,表达式force1 + force2的结果为一个临时对象。在语句1中,该临时对象被赋给net;在语句2中,net被赋给该临时对象。

使用完临时对象后,将把它丢弃。例如,对于语句2,程序计算force1和force2之和,将结果复制到临时返回对象中,再用net的内容覆盖临时对象的内容,然后将该临时对象丢弃。原来的变量全都保持不变。

总之,如果方法或函数要返回局部对象,则应返回对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。如果方法或函数要返回一个没有公有复制构造函数的类(如ostream类)的对象,它必须返回一个指向这种对象的引用。最后,有些方法和函数(如重载的赋值运算符)可以返回对象,也可以返回指向对象的引用,在这种情况下,应首选引用,因为其效率更高。

以上是关于cpp►动态内存分配与析构之复制构造函数/赋值运算符的主要内容,如果未能解决你的问题,请参考以下文章

CPP游戏攻略03

构造函数与析构函数2

构造函数constructor 与析构函数destructor

c++_构造与析构

c++_构造与析构

c++_构造与析构