-
C++教程之类和堆
类和堆
一、构造函数和析构函数
前面的例子已经运用了new和delete来为类对象分配和释放内存。当使用new为类对象分配内存时,编译器首先用new运算符分配内存,然后调用类的构造函数;类似的,当使用delete来释放内存时,编译器会首先调用泪的析构函数,然后再调用delete运算符。
#include iostream.h
class Date
{
int mo,da,yr;
public:
Date() { cout<
~Date() { cout<
}
int main()
{
Date* dt = new Date;
cout<<<endl;
delete dt;
return 0;
}
程序定义了一个有构造函数和析构函数的Date类,这两个函数在执行时会显示一条信息。当new运算符初始化指针dt时,执行了构造函数,当delete运算符释放内存时,又执行了析构函数。
程序输出如下:
Date constructor
Process the date
Date destructor
二、堆和类数组
前面提到,类对象数组的每个元素都要调用构造函数和析构函数。下面的例子给出了一个错误的释放类数组所占用的内存的例子。
#include iostream.h
class Date
{
int mo, da, yr;
public:
Date() { cout<
~Date() { cout<
}
int main()
{
Date* dt = new Date[5];
cout<<<endl;
delete dt; //这儿
return 0;
}
指针dt指向一个有五个元素的数组。按照数组的定义,编译器会让new运算符调用Date类的构造函数五次。但是delete被调用时,并没有明确告诉编译器指针指向的Date对象有几个,所以编译时,只会调用析构函数一次。下面是程序输出;
Date constructor
Date constructor
Date constructor
Date constructor
Date constructor
Process the date
Date destructor
为了解决这个问题,C++允许告诉delete运算符,正在删除的那个指针时指向数组的,程序修改如下:
#include iostream.h
class Date
{
int mo, da, yr;
public:
Date() { cout<
~Date() { cout<
}
int main()
{
Date* dt = new Date[5];
cout<<<endl;
delete [] dt; //这儿
return 0;
}
最终输出为:
Date constructor
Date constructor
Date constructor
Date constructor
Date constructor
Process the date
Date destructor
Date destructor
Date destructor
Date destructor
Date destructor
三、重载new和delete运算符
前面已经介绍了如何用new和delete运算符函数来动态第管理内存,在那些例子中使用的都是全局的new和delete运算符。我们可以重载全局的new和delete运算符,但这不是好的想法,除非在进行低级的系统上或者嵌入式的编程。
但是,在某个类的内部重载new和delete运算符时可以的。这允许一个类有它自己的new和delete运算符。当一个类需要和内存打交道时,采用这种方法来处理其中的细节,可以获得很搞的效率,同时避免了使用全局new和delete运算符带来的额外开销。因为全局堆操作时调用操作系统函数来分配和释放内存,这样效率很低。
如果确定某个类在任何时候,其实例都不会超过一个确定的值,那么就可以一次性为类的所有实例分配足够的内存,然后用该类的new和delete运算符来管理这些内存。下面的程序说明了如何对new和delete进行重载。
#include iostream.h
#include string.h
#include stddef.h
#include new.h
const int maxnames = 5;
class Names
{
char name[25];
static char Names::pool[];
static bool Names::inuse[maxnames];
public:
Names(char* s) { strncpy(name,s,sizeof(name)); }
void* operator new(size_t) throw(bad_alloc);
void operator delete(void*) throw();
void display() const { cout<<NAME<
};
char Names::pool[maxnames * sizeof(Names)];
bool Names::inuse[maxnames];
void* Names::operator new(size_t) throw(bad_alloc)
{
for(int p=0; p
{
if(!inuse[p])
{
inuse[p] = true;
return pool+p*sizeof(Names);
}
}
throw bad_alloc();
}
void Names::operator delete(void* p) throw()
{
if(p!=0)
inuse[((char*)p - pool)/sizeof(Names)] = false;
}
int main()
{
Names* nm[maxnames];
int i;
for(i=0; i
{
cout<<ENDL<<<i+1<<:;
char name[25];
cin >> name;
nm[i] = new Names(name);
}
for(i=0; i
{
nm[i]->display();
delete nm[i];
}
return 0;
}
上面的程序提示输入5个姓名,然后显示它们。程序中定义了名为Names的类,它的构造函数初始化对象的name值。这个类定义了自己的new和delete运算符。这是因为程序能保证不会一次使用超过maxnames个姓名,所以可以通过重载默认的new和delete运算符来提高运行速度。
Names类中的内存池是一个字符数组,可以同时容纳程序需要的所有姓名。与之相关的布尔型数组inuse为每个姓名记录了一个true和false值,指出内存中的对应的项是否正在使用。
重载的new运算符在内存池中寻找一个没有被使用的项,然后返回它的地址。重载的delete运算符则标记那些没有被使用的项。
在类定义中重载的new和delete运算符函数始终是静态的,并且没有和对象相关的this指针。这是因为编译器会在调用构造函数之前调用new函数,在调用析构函数后调用delete函数。
new函数是在类的构造函数之前被调用的。因为这时内存中还不存在类的对象而且构造函数也没有提供任何初始化值,所以它不可以访问类的任何成员。同理,delete运算符是在析构函数之后被调用的,所以它也不可以访问类的成员。
四、异常监测和异常处理
1.检测异常
上面的例子还缺少必要的保护机制。比如,重载的delete运算符函数并没有检查它的参数,确认其是否落在内存池内部。如果你绝对相信自己编的程序中不会传递错误的指针值给delete运算符,那么可以省掉合法性检查以提高效率,特别是在优先考虑效率的程序中。否则应该使用预编译的条件语句。在软件的测试版本中加入这些检测,在正式的发行版本中去掉这些检查。
2.重载new和delete中的异常处理
上面的两个重载运算符函数都是用了异常处理。异常处理是C++的新内容之一,目前还没有讲到。在这里不必关心它是如何工作的。上面程序中,当试图分配超过内存池容量的Names缓冲区,重载的new运算符函数就会抛出异常,终止程序。
五、重载new[]和delete[]
对于上面的程序,假如有下面的语句:
Names *nms=new Names[10]
...
delete [] nms;
那么,这些语句会调用全局new和delete运算符,而不是重载过的new和delete。为了重载能为对象数组分配内存的new和delete运算符,必须像下面的程序一样,对new[]和delete[]也进行重载。
#include iostream.h
#include string.h
#include stddef.h
#include new.h
const int maxnames = 5;
class Names
{
char name[25];
static char Names::pool[];
static bool Names::inuse[maxnames];
public:
Names(char* s) { strncpy(name,s,sizeof(name)); }
void* operator new(size_t) throw(bad_alloc);
void operator delete(void*) throw();
void display() const { cout<<NAME<
};
char Names::pool[maxnames * sizeof(Names)];
bool Names::inuse[maxnames];
void* Names::operator new[](size_t size) throw(bad_alloc)
{
int elements=size/sizeof(Names);
int p=-1;
int i=0;
while((i {
if(!inuse[i]) p=i;
++i;
}
// Not enough room.
if ((p==-1) || ((maxnames-p)
for(int x=0; x return pool+p*sizeof(Names);
}
void Names::operator delete[](void* b) throw()
{
if(b!=0)
{
int p=((char*)b- pool)/sizeof(Names);
int elements=inuse[p];
for (int i=0; i }
}
int main()
{
Names* np = new Names[maxnames];
int i;
for(i=0; i
{
cout<<ENDL<
<<i+1<<> char name[25];
cin >> name;
*(np + i) = name;
}
for(i=0; idisplay();
delete [] np;
return 0;
}
重载new[]和delete[]要比重载new和delete考虑更多的问题。这是因为new[]运算符时为数组分配内存,所以它必须记住数组的大小,重载的delete[]运算符才能正确地把缓冲区释放回内存池。上面的程序采用的方法比较简单,吧原来存放缓冲区使用标志的布尔型数组换成一个整型数组,该数组的每个元素记录new[]运算符分配的缓冲区个数,而不再是一个简单的true。当delete[]运算符函数需要把缓冲区释放回内存池时,它就会用该数组来确认释放的缓冲区个数。
类的其他问题
一、拷贝构造函数
拷贝构造函数在下列情况下被调用:用已经存在的对象去初始化同一个类的另一个对象;在函数的参数中,以传值方式传递类对象的拷贝;类对象的值被用做函数的返回值。拷贝构造函数和前面说到的转换构造函数有些相似。转换构造函数是把一个类的对象转化为另一个类的对象;拷贝构造函数是用一个已经存在的对象的值实例化该类的一个新对象。
不同对象间的初始化和赋值的区别:赋值操作是在两个已经存在的对象间进行的;而初始化是要创建一个新的对象,并且其初值来源于另一个已存在的对象。编译器会区别这两种情况,赋值的时候调用重载的赋值运算符,初始化的时候调用拷贝构造函数。
如果类中没有拷贝构造函数,则编译器会提供一个默认的。这个默认的拷贝构造函数只是简单地复制类中的每个成员。
#include iostream.h
#include string.h
class Date
{
int mo, da, yr;
char* month;
public:
Date(int m = 0, int d = 0, int y = 0);
Date(const Date&);
~Date();
void display() const;
};
Date::Date(int m, int d, int y)
{
static char* mos[] =
{
January, February, March, April, May, June,
July, August, September, October, November, December
};
mo = m; da = d; yr = y;
if (m != 0)
{
month = new char[strlen(mos[m-1])+1];
strcpy(month, mos[m-1]);
}
else
month = 0;
}
Date::Date(const Date& dt)
{
mo = dt.mo;
da = dt.da;
yr = dt.yr;
if (dt.month != 0)
{
month = new char [strlen(dt.month)+1];
strcpy(month, dt.month);
}
else
month = 0;
}
Date::~Date()
{
delete [] month;
}
void Date::display() const
{
if (month != 0)
cout << month <<' '<< da << , << yr << std::endl;
}
int main()
{
Date birthday(6,24,1940);
birthday.display();
Date newday = birthday;
newday.display();
Date lastday(birthday);
lastday.display();
return 0;
}
本例中,用到了两次拷贝构造函数。一个是使用普通的C++初始化变量的语句:
Date newday = birthday;
另一个是使用构造函数的调用约定,即把初始化值作为函数的参数:
Date lastday(birthday);
一、构造函数和析构函数
前面的例子已经运用了new和delete来为类对象分配和释放内存。当使用new为类对象分配内存时,编译器首先用new运算符分配内存,然后调用类的构造函数;类似的,当使用delete来释放内存时,编译器会首先调用泪的析构函数,然后再调用delete运算符。
#include iostream.h
class Date
{
int mo,da,yr;
public:
Date() { cout<
~Date() { cout<
}
int main()
{
Date* dt = new Date;
cout<<<endl;
delete dt;
return 0;
}
程序定义了一个有构造函数和析构函数的Date类,这两个函数在执行时会显示一条信息。当new运算符初始化指针dt时,执行了构造函数,当delete运算符释放内存时,又执行了析构函数。
程序输出如下:
Date constructor
Process the date
Date destructor
二、堆和类数组
前面提到,类对象数组的每个元素都要调用构造函数和析构函数。下面的例子给出了一个错误的释放类数组所占用的内存的例子。
#include iostream.h
class Date
{
int mo, da, yr;
public:
Date() { cout<
~Date() { cout<
}
int main()
{
Date* dt = new Date[5];
cout<<<endl;
delete dt; //这儿
return 0;
}
指针dt指向一个有五个元素的数组。按照数组的定义,编译器会让new运算符调用Date类的构造函数五次。但是delete被调用时,并没有明确告诉编译器指针指向的Date对象有几个,所以编译时,只会调用析构函数一次。下面是程序输出;
Date constructor
Date constructor
Date constructor
Date constructor
Date constructor
Process the date
Date destructor
为了解决这个问题,C++允许告诉delete运算符,正在删除的那个指针时指向数组的,程序修改如下:
#include iostream.h
class Date
{
int mo, da, yr;
public:
Date() { cout<
~Date() { cout<
}
int main()
{
Date* dt = new Date[5];
cout<<<endl;
delete [] dt; //这儿
return 0;
}
最终输出为:
Date constructor
Date constructor
Date constructor
Date constructor
Date constructor
Process the date
Date destructor
Date destructor
Date destructor
Date destructor
Date destructor
三、重载new和delete运算符
前面已经介绍了如何用new和delete运算符函数来动态第管理内存,在那些例子中使用的都是全局的new和delete运算符。我们可以重载全局的new和delete运算符,但这不是好的想法,除非在进行低级的系统上或者嵌入式的编程。
但是,在某个类的内部重载new和delete运算符时可以的。这允许一个类有它自己的new和delete运算符。当一个类需要和内存打交道时,采用这种方法来处理其中的细节,可以获得很搞的效率,同时避免了使用全局new和delete运算符带来的额外开销。因为全局堆操作时调用操作系统函数来分配和释放内存,这样效率很低。
如果确定某个类在任何时候,其实例都不会超过一个确定的值,那么就可以一次性为类的所有实例分配足够的内存,然后用该类的new和delete运算符来管理这些内存。下面的程序说明了如何对new和delete进行重载。
#include iostream.h
#include string.h
#include stddef.h
#include new.h
const int maxnames = 5;
class Names
{
char name[25];
static char Names::pool[];
static bool Names::inuse[maxnames];
public:
Names(char* s) { strncpy(name,s,sizeof(name)); }
void* operator new(size_t) throw(bad_alloc);
void operator delete(void*) throw();
void display() const { cout<<NAME<
};
char Names::pool[maxnames * sizeof(Names)];
bool Names::inuse[maxnames];
void* Names::operator new(size_t) throw(bad_alloc)
{
for(int p=0; p
{
if(!inuse[p])
{
inuse[p] = true;
return pool+p*sizeof(Names);
}
}
throw bad_alloc();
}
void Names::operator delete(void* p) throw()
{
if(p!=0)
inuse[((char*)p - pool)/sizeof(Names)] = false;
}
int main()
{
Names* nm[maxnames];
int i;
for(i=0; i
{
cout<<ENDL<<<i+1<<:;
char name[25];
cin >> name;
nm[i] = new Names(name);
}
for(i=0; i
{
nm[i]->display();
delete nm[i];
}
return 0;
}
上面的程序提示输入5个姓名,然后显示它们。程序中定义了名为Names的类,它的构造函数初始化对象的name值。这个类定义了自己的new和delete运算符。这是因为程序能保证不会一次使用超过maxnames个姓名,所以可以通过重载默认的new和delete运算符来提高运行速度。
Names类中的内存池是一个字符数组,可以同时容纳程序需要的所有姓名。与之相关的布尔型数组inuse为每个姓名记录了一个true和false值,指出内存中的对应的项是否正在使用。
重载的new运算符在内存池中寻找一个没有被使用的项,然后返回它的地址。重载的delete运算符则标记那些没有被使用的项。
在类定义中重载的new和delete运算符函数始终是静态的,并且没有和对象相关的this指针。这是因为编译器会在调用构造函数之前调用new函数,在调用析构函数后调用delete函数。
new函数是在类的构造函数之前被调用的。因为这时内存中还不存在类的对象而且构造函数也没有提供任何初始化值,所以它不可以访问类的任何成员。同理,delete运算符是在析构函数之后被调用的,所以它也不可以访问类的成员。
四、异常监测和异常处理
1.检测异常
上面的例子还缺少必要的保护机制。比如,重载的delete运算符函数并没有检查它的参数,确认其是否落在内存池内部。如果你绝对相信自己编的程序中不会传递错误的指针值给delete运算符,那么可以省掉合法性检查以提高效率,特别是在优先考虑效率的程序中。否则应该使用预编译的条件语句。在软件的测试版本中加入这些检测,在正式的发行版本中去掉这些检查。
2.重载new和delete中的异常处理
上面的两个重载运算符函数都是用了异常处理。异常处理是C++的新内容之一,目前还没有讲到。在这里不必关心它是如何工作的。上面程序中,当试图分配超过内存池容量的Names缓冲区,重载的new运算符函数就会抛出异常,终止程序。
五、重载new[]和delete[]
对于上面的程序,假如有下面的语句:
Names *nms=new Names[10]
...
delete [] nms;
那么,这些语句会调用全局new和delete运算符,而不是重载过的new和delete。为了重载能为对象数组分配内存的new和delete运算符,必须像下面的程序一样,对new[]和delete[]也进行重载。
#include iostream.h
#include string.h
#include stddef.h
#include new.h
const int maxnames = 5;
class Names
{
char name[25];
static char Names::pool[];
static bool Names::inuse[maxnames];
public:
Names(char* s) { strncpy(name,s,sizeof(name)); }
void* operator new(size_t) throw(bad_alloc);
void operator delete(void*) throw();
void display() const { cout<<NAME<
};
char Names::pool[maxnames * sizeof(Names)];
bool Names::inuse[maxnames];
void* Names::operator new[](size_t size) throw(bad_alloc)
{
int elements=size/sizeof(Names);
int p=-1;
int i=0;
while((i {
if(!inuse[i]) p=i;
++i;
}
// Not enough room.
if ((p==-1) || ((maxnames-p)
for(int x=0; x return pool+p*sizeof(Names);
}
void Names::operator delete[](void* b) throw()
{
if(b!=0)
{
int p=((char*)b- pool)/sizeof(Names);
int elements=inuse[p];
for (int i=0; i }
}
int main()
{
Names* np = new Names[maxnames];
int i;
for(i=0; i
{
cout<<ENDL<
<<i+1<<> char name[25];
cin >> name;
*(np + i) = name;
}
for(i=0; idisplay();
delete [] np;
return 0;
}
重载new[]和delete[]要比重载new和delete考虑更多的问题。这是因为new[]运算符时为数组分配内存,所以它必须记住数组的大小,重载的delete[]运算符才能正确地把缓冲区释放回内存池。上面的程序采用的方法比较简单,吧原来存放缓冲区使用标志的布尔型数组换成一个整型数组,该数组的每个元素记录new[]运算符分配的缓冲区个数,而不再是一个简单的true。当delete[]运算符函数需要把缓冲区释放回内存池时,它就会用该数组来确认释放的缓冲区个数。
类的其他问题
一、拷贝构造函数
拷贝构造函数在下列情况下被调用:用已经存在的对象去初始化同一个类的另一个对象;在函数的参数中,以传值方式传递类对象的拷贝;类对象的值被用做函数的返回值。拷贝构造函数和前面说到的转换构造函数有些相似。转换构造函数是把一个类的对象转化为另一个类的对象;拷贝构造函数是用一个已经存在的对象的值实例化该类的一个新对象。
不同对象间的初始化和赋值的区别:赋值操作是在两个已经存在的对象间进行的;而初始化是要创建一个新的对象,并且其初值来源于另一个已存在的对象。编译器会区别这两种情况,赋值的时候调用重载的赋值运算符,初始化的时候调用拷贝构造函数。
如果类中没有拷贝构造函数,则编译器会提供一个默认的。这个默认的拷贝构造函数只是简单地复制类中的每个成员。
#include iostream.h
#include string.h
class Date
{
int mo, da, yr;
char* month;
public:
Date(int m = 0, int d = 0, int y = 0);
Date(const Date&);
~Date();
void display() const;
};
Date::Date(int m, int d, int y)
{
static char* mos[] =
{
January, February, March, April, May, June,
July, August, September, October, November, December
};
mo = m; da = d; yr = y;
if (m != 0)
{
month = new char[strlen(mos[m-1])+1];
strcpy(month, mos[m-1]);
}
else
month = 0;
}
Date::Date(const Date& dt)
{
mo = dt.mo;
da = dt.da;
yr = dt.yr;
if (dt.month != 0)
{
month = new char [strlen(dt.month)+1];
strcpy(month, dt.month);
}
else
month = 0;
}
Date::~Date()
{
delete [] month;
}
void Date::display() const
{
if (month != 0)
cout << month <<' '<< da << , << yr << std::endl;
}
int main()
{
Date birthday(6,24,1940);
birthday.display();
Date newday = birthday;
newday.display();
Date lastday(birthday);
lastday.display();
return 0;
}
本例中,用到了两次拷贝构造函数。一个是使用普通的C++初始化变量的语句:
Date newday = birthday;
另一个是使用构造函数的调用约定,即把初始化值作为函数的参数:
Date lastday(birthday);
最新更新
Objective-C语法之代码块(block)的使用
VB.NET eBook
Add-in and Automation Development In VB.NET 2003 (F
Add-in and Automation Development In VB.NET 2003 (8
Add-in and Automation Development in VB.NET 2003 (6
Add-in and Automation Development In VB.NET 2003 (5
AddIn Automation Development In VB.NET 2003 (4)
AddIn And Automation Development In VB.NET 2003 (2)
Addin and Automation Development In VB.NET 2003 (3)
AddIn And Automation Development In VB.NET 2003 (1)
2个场景实例讲解GaussDB(DWS)基表统计信息估
常用的 SQL Server 关键字及其含义
动手分析SQL Server中的事务中使用的锁
openGauss内核分析:SQL by pass & 经典执行
一招教你如何高效批量导入与更新数据
天天写SQL,这些神奇的特性你知道吗?
openGauss内核分析:执行计划生成
[IM002]Navicat ODBC驱动器管理器 未发现数据
初入Sql Server 之 存储过程的简单使用
SQL Server -- 解决存储过程传入参数作为s
武装你的WEBAPI-OData入门
武装你的WEBAPI-OData便捷查询
武装你的WEBAPI-OData分页查询
武装你的WEBAPI-OData资源更新Delta
5. 武装你的WEBAPI-OData使用Endpoint 05-09
武装你的WEBAPI-OData之API版本管理
武装你的WEBAPI-OData常见问题
武装你的WEBAPI-OData聚合查询
OData WebAPI实践-OData与EDM
OData WebAPI实践-Non-EDM模式