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;
}