C++学习笔记(第12章->类和动态内存分配)

1动态内存和类

1.复习范例和静态类成员

这个例子,是一个不完美的程序,通过他的缺陷,我们来逐步分析和改进,进而了解这一章要学习的内容.

#ifndef _STRING_BAD_H_
#define _STRING_BAD_H_
#include<iostream>
class StringBad
{
private:
	char * str;//char pointer,need new to assign space
	int len;
	static int num_strings;//static
public:
	StringBad(const char * str);
	StringBad();
	~StringBad();
	friend std::ostream & operator<<(std::ostream & os, const StringBad & st);
};
#endif
创建一个char指针,需要使用new来为他分配空间,num_strings为类静态成员,无论创建了多少个类对象,程序只创建一个静态类变量副本.并不在头文件中定义他.

#include<cstring>
#include"StringBad.h"
using namespace std;

int StringBad::num_strings = 0; //static member

StringBad::StringBad(const char * s)
{
	len = strlen(s);
	str = new char[len + 1];
	strcpy(str, s);
	num_strings++;
	cout<<"num_strings = "<<num_strings<<"object created\n";
}
StringBad::StringBad()
{
	len = 4;
	str = new char[len];
	strcpy(str, "C++");
	num_strings++;
	cout<<"num_strings = "<<num_strings<<"object created\n";
}
StringBad::~StringBad()
{
	cout<<"\""<<str<<"\"object deleted";
	num_strings--;
	cout<<num_strings<<"left\n";
	delete [] str;
}
ostream & operator <<(ostream & os, const StringBad & st)
{
	os<<st.str;
	return os;
}
静态类成员num_strings在类声明中声明,在包含类方法的文件中初始化,不要在头文件中初始化,因为假如调用多次这个头文件,他将会被多次初始化.初始化时使用作用域解析操作符指出所属的类.但是,如果静态成员是整型枚举型const,则可以在类声明中初始化.

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

我们来看下测试类,看看测试的时候,会出现什么样的错误:

#include<iostream>
#include"StringBad.h"
using namespace std;
void callme1(StringBad &);
void callme2(StringBad);
int main()
{
	StringBad h1("hongzong.lin");
	StringBad h2("dizong.lin");
	StringBad h3("bingzai.lin");
	cout<<"h1:"<<h1<<endl;
	cout<<"h2:"<<h2<<endl;
	cout<<"h3:"<<h3<<endl;
	callme1(h1);
	cout<<"h1:"<<h1<<endl;
	callme2(h2);
	cout<<"h2"<<h2<<endl;
	cout<<"initial one object to another";
	StringBad h4 = h3;
	cout<<"h4:"<<h4<<endl;
	cout<<"assign one object to another";
	StringBad h5;
	h5 = h1;
	cout<<"h5:"<<h5<<endl;
	cout<<"end of main()\n";
	return 0;
	while(1);
}
void callme1(StringBad & st)
{
	cout<<"string pass by reference:\n";
	cout<<" \""<<st<<"\"\n";
}
void callme2(StringBad st)
{
	cout<<"string pass by value:\n";
	cout<<"\""<<st<<"\"\n";
}
输出结果是:


从中可以看到,程序逐渐开始崩溃,callme2()之后,输出就出现问题了,首先,h2作为函数参数传给他,导致析构函数被调用.其次,虽然按值传递防止修改原始数据,但实际上函数已使h2的原始数据无法使用了.

接下来,最先删除的是h5,h4,h3;h5,h4删除时是正常的,h3的时候,已经异常,同样h3的数据已经不可用,另外,计数也有问题.我们创建5个对象,那么计数应该是5,这里少了两个.类定义了两个构造函数,但是程序中使用了3个构造函数,

StringBad h4 = h3;
等效于
StringBad h4 = (StringBad)h3;
原型如下:

StringBad (const StringBad &);
当你使用一个对象来初始化另一个对象时,编译器将自动生成上述的构造函数,成为复制构造函数,因为他创建对象的一个副本,自动生成的复制构造函数他不知道需要更新静态变量num_strings,因此计数也将打乱了.

2.隐式成员函数

以上范例StringBad问题就是有自动定义的饮食成员函数引起的,这种函数行为和类设计不符.具体来说,C++自动提供以下成员函数:

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

(2)复制构造函数,如果没有定义

(3)赋值操作符,如果没有定义

(4)默认析构函数,如果没有定义

(6)地址操作符,如果没有定义

结果表明,StringBad问题就是由隐士赋值构造函数和隐式赋值操作符引起的.

2.1默认构造函数

如果没有提供任何构造函数,C++将创建默认构造函数.编译器会提供一个不接受任何参数,也不执行任何操作的构造函数,因为创建对象时总是会调用构造函数.

StringBad(){}

如果定义了构造函数,C++将不会定义默认构造函数.带参数的构造函数也可以是默认构造函数,只要多有参数都有默认值.

2.2复制构造函数

(1)类的复制构造函数原型如下:

class_name (const class_name &)//StringBad(const StringBad&);
复制构造函数,何时被调用呢?

新建一个对象并将其初始化为同类现有对象时,他就会被调用.如下:

StringBad h5(h4);
StringBad h5 = h4;
StringBad h5 = StringBad(h4);
StringBad *ph = new SringBad(h4);
每当函数生成对象副本时,编译器都将使用复制构造函数.当函数按值传递(例如callme2())或函数返回对象时,都将使用复制构造函数.
(2)复制构造函数的功能

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

如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象.

(3)范例中,哪里出了问题

当callme2()被调用时,复制构造函数用来初始化callme2()的形参,还被用来将对象h4初始化为h3对象.默认的复制构造函数不说明其行为,因此它不创建过程,也不增加计数器num_strings的值,但析构函数在对象过期时每次都会更新num_strings,这将导致计数异常.

办法之一是,提供一个显示的复制构造函数用于更新计数:

StringBad::StringBad(const StringBad & st)
{
   num_strings++;
   ...
}
如果类中包含这样的静态成员,他的值将在新对象被初始化时发生变化,则应该提供一个显示复制构造函数来处理这个计数问题.

第二个问题是出现乱码,这是因为隐式复制构造函数是按值进行复制:

h4.str = h3.str;
这里复制的仅仅是h3的指针,而没有复制h.str指向的内容.所以当调用析构函数时,释放两次这个指针指向的内容,这将会导致不确定的结果.

(4)范例问题的解决办法

采用深度复制(deep copy),复制构造函数应该复制字符串并将副本的地址赋给str成员,而不仅仅是复制字符串地址.每个对象都应该有自己的字符串,而不是引用其他对象的字符串.调用析构函数时,释放对象本身的字符串而不是去释放其他对象已经释放的字符串.下面我们来编写一个复制构造函数:

StringBad::StringBad(const StringBad & st)
{
    num_strings++;
    len = st.len;
    str = new char[len + 1];
    strcpy(str, st.str);
    cout<<num_strings<<"\""<<str<<"\"object created\n";
}
必须定义复制构造函数的原因在于,一些类成员是使用new初始化的,指向数据的指针,而不是数据本身.

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

2.2赋值操作符

C++允许类对象赋值,这是通过自动为类重载赋值操作符实现的。原型如下:

class_name & class_name::operator = (const class_name &);//StringBad & StringBad::operator = (const StringBad &);
(1)何时调用赋值操作符,将已有的对象赋给另一个对象时,将使用重载的赋值操作符,

StringBad h4;
h4 = h3;//assignment operator invoked
初始化对象并不一定会使用赋值操作符.

StringBad h4 = h3;//use copy contructor,possibly assignment too
 这里使用复制构造函数,使用了=,也可能调用赋值操作符.

(2)与复制构造函数相似,赋值操作符的隐式实现也对成员逐个复制,如果成员本身就是类对象,则程序将使用为这个类定义的赋值操作符来复制成员,静态数据成员不受影响.

	StringBad h5;
	h5 = h1;
范例中,h5析构时正常,h1析构出错,原因和复制构造函数类似,自动定义的赋值操作符仅仅是浅复制.

(3)解决范例赋值问题

当然是提供赋值操作符的定义.

A.由于目标对象可能引用了以前分配的数据,所以函数应使用delete[ ]来释放这些数据

B.函数应当避免将对象赋给自身,否则,给对象赋值之前,释放内存操作可能删除对象的内容.

C.函数返回一个指向调用对象的引用.

下面我们来为StringBad类编写赋值操作符:

StringBad & StringBad::operator = (const StringBad & sd)
{
   if(this == &sd)
       return *this;
    delete [] sd;
    len = sd.len;
    str = new char[len + 1];
    strcpy(str, sd.str);
    return *this;
}
赋值操作符只能由类成员函数重载的操作符之一,赋值操作符并没有创建新的对象,因此不需要调整静态数据成员num_strings的值.

3.改进后新的范例String

(1)修订后的默认构造函数:

String::String()
{
    len = 0;
    str = new char[1];
    str[0] = '\0';
}
这么做是为了与类析构函数搭配,deleted[] str;

(2)静态成员函数

A.不能通过对象调用静态成员函数

B.由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员.

(3)进一步重载赋值操作符

String name;
char temp[40];
cin.getline(temp, 40);
name = temp;
我们来看看这是怎么工作的:

首先,使用构造函数String(const char * s)来创建一个临时对象,其中包含temp中的字符串拷贝

其次,使用String & String::operator = (const String &)函数将临时对对象中的信息赋值到name对象中.

最后,程序调用析构函数~String()来删除临时对象.

为了提高效率,最简单的方法是重载赋值操作符,使之能够直接使用常规字符串,这样就不用创建临时对象和删除临时对象.

String & String::operator = (const char * s)
{
    delete [] str;
    len = strlen(s);
    str = new char[len + 1];
    strcpy(str, s);
    return *this;
}

程序清单:

#ifndef _STRING_H_
#define _STRING_H_
#include<iostream>
using namespace std;
class String
{
private:
	char * str;
	int len;
	static int num_strings;
	static const int CINLIM = 80;
public:
	String(const char * s);
	String();
	String(const String &);
	~String();
	int length()const{return len;};
	String & operator = (const char *);
	String & operator = (const String &);
	char & operator[](int i);
	const char & operator[](int i)const;
	friend bool operator < (const String &str,const String &str2);
	friend bool operator > (const String &str,const String &str2);
	friend bool operator == (const String &str,const String &str2);
	friend ostream & operator<<(ostream & os, const String & st);
	friend istream & operator>>(istream & is, String & st);
	static int howmany();
};
#endif
#include<cstring>
#include"string.h"
using namespace std;
int String::num_strings = 0;
int String::howmany()
{
	return num_strings;
}
String::String(const char * s)
{
	len = strlen(s);
	str = new char[len + 1];
	strcpy(str, s);
	num_strings++;
}
String::String()
{
	len = 1;
	str = new char[1];
	str[0] = '\0';
	num_strings++;
}
String::~String()
{
	--num_strings;
	delete [] str;
}
String & String::operator=(const String & st)
{
	if(this == &st)
		return *this;
	delete [] str;
	len = st.len;
	str = new char[len+1];
	strcpy(str, st.str);
	return *this;
}
String & String::operator=(const char *s)
{
	delete [] str;
	len = strlen(s);
	str = new char[len + 1];
	strcpy(str, s);
	return *this;
}
char & String::operator[](int i)
{
	return str[i];
}
const char & String::operator[](int i)const
{
	return str[i];
}
bool operator < (const String & st1, const String & st2)
{
	return strcmp(st1.str, st2.str)<0;
}
bool operator > (const String & st1, const String & st2)
{
	return st2.str <st1.str;
}
bool operator == (const String & st1, const String & st2)
{
	return (strcmp(st1.str, st2.str) == 0);
}
ostream & operator<<(ostream & os, const String & st)
{
	os<<st.str;
	return os;
}
istream & operator>>(istream & is, String & st)
{
	char temp[String::CINLIM];
	is.get(temp, String::CINLIM);
	if(is)
		st = temp;
	while(is && is.get()!='\n')
		continue;
	return is;
}
#include"string.h"
const int ArSize = 10;
const int MaxLen = 81;
using namespace std;
int main()
{
	String name;
	cout<<"what is your name?";
	cin>>name;
	cout<<name<<",please enter up to"<<ArSize<<"short sayings<empty line to quit!>"<<endl;
	String sayings[ArSize];
	char temp[MaxLen];
	int i;
	for(i = 0; i < ArSize; i++)
	{
		cout<<i+1<<": ";
		cin.get(temp, MaxLen);
		while(cin && cin.get() != '\n')
			continue;
		if(!cin || temp[0] == '\0')
			break;
		else
			sayings[i] = temp;
	}
	int total = i;
	cout<<"here are your sayings:\n";
	for(i = 0; i < total; i++)
	{
		cout<<sayings[i][0]<<": "<<sayings[i]<<endl;
	}
	int shorest = 0;
	int first = 0;
	for(i = 1; i < total; i++)
	{
		if(sayings[i].length() < sayings[shorest].length())
			shorest = i;
		if(sayings[i] < sayings[first])
			first = i;
	}
	cout<<"shorest saying:\n"<<sayings[shorest]<<endl;
	cout<<"first alphabetiaclly:\n"<<sayings[first]<<endl;
	cout<<"this program used"<<String::howmany()<<"string objects.bye\n";
	while(1);
	return 0;
}

(4)在构造函数中使用new注意事项

A.构造函数使用new,析构函数使用delete,而且必须匹配.

B.应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象.

C.复制构造函数应分配足够的空间来存储复制的数据,并复制数据,不仅仅是数据的地址.另外,还应该更新所有受影响的静态类成员.

D.应当定义一个赋值操作符,通过深度复制将一个对象复制给另一个对象.

E.分配一个字符,str = new char[1],str = '\0';为了和delete [] 兼容.

(5)再谈new布局操作符

布局new操作符让你能够在分配内存时,指定内存位置.我们来看一个简单的例子,使用布局new操作符和常规new操作符给对象分配内存.看个例子:

#include<iostream>
#include<string>
#include<new>
using namespace std;
const int BUF = 512;
class JustTesting
{
private:
	string words;
	int number;
public:
	JustTesting(const string & s = "just testing", int n = 0){words = s; number = n;}
	~JustTesting(){cout<<words<<"destroyed";}
	void show()const{cout<<words<<","<<number<<endl;}
};
int main()
{
	char * buffer = new char[BUF];
	JustTesting *pc1, *pc2;
	pc1 = new(buffer)JustTesting;
	pc2 = new JustTesting("heap2", 20);
	cout<<"memory block address:\n"<<"buffer: "<<(void *)buffer<<" heap: "<<pc2<<endl;
	cout<<"memory contents:\n";
	cout<<pc1<<":";
	pc1->show();
	cout<<pc2<<":";
	pc2->show();
	JustTesting *pc3, *pc4;
	pc3 = new(buffer + sizeof(JustTesting))JustTesting("better idea", 6);//防止覆盖pc1的数据
	pc4 = new JustTesting("heap4", 10);
	cout<<"mem"	cout<<"memory block address:\n"<<"buffer: "<<(void *)buffer<<" heap: "<<pc3<<endl;
	cout<<"memory contents:\n";
	cout<<pc3<<":";
	pc3->show();
	cout<<pc4<<":";
	pc4->show();
	delete pc2;
	delete pc4;
	pc3->~JustTesting();//!!notice,destroy object pointed to by pc3
	pc3->~JustTesting();//显示的调用析构函数,delete pc3不可行,他不与布局new操作符对应使用.
	delete [] buffer;//不会为使用布局new操作符创建的对象调用析构函数.
	return 0;
}

4.复习各种技术

4.1重载<<操作符
要重新定义<<操作符,以便他将和cout一起用来显示对象的内容,请定义下面友元操作符函数:

ostream & operator<<(ostream & os, const c_name & obj)
{
   os<<...;//c_name是类名
   return os;
}

4.2转换函数

要将单个值转换为类型,需要创建原型如下的类型的名称:

c_name (type_name value);//c_name为类名,type_name是要转换的类型的名称
使用转换函数要小心,可以在声明构造函数的时候使用关键字explict,以防止它被用于隐式转换.

4.3其构造函数使用new的类

(1)对于指向的内存是由new分配的所有类成员,必须在析构函数中使用delete

(2)如果析构函数通过对指针类成员使用delete来释放内存,则每个构造函数都应当使用new来初始化指针,或将他置为空指针.

(3)new,new [],和delete,delete[]要一一对应

(4)应定义一个分配内存的复制构造函数.通常为className(const className &)

(5)应定义一个重载赋值操作符的类成员函数.范例:

c_name & c_name::operator = (const c_name & cn)
{
    if(this == &cn)
        return *this;
    delete [] c_pointer;
    c_pointer = new type_name[size];
    ....
    return *this;
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值