(本资料qq讨论群112133686)
因不良的编程习惯或对C++机制未吃透,编写的程序编译的时候虽然能通过,但在运行的时候,可能会出现这样那样的问题,如程序异常退出甚至崩溃。出现比较典型的错误如文件打开错误、除数为0、错误释放内存等。本章演示实例都是各类经典实例及其解决方法,如错误释放指针、栈溢出、使用判断语句不良习惯、cin输入错误、数组超界、作用域错误等经典实例。还举例说明出现异常时,直接调用abort()或者exit()函数退出程序,通过函数的返回值来判断异常,通过try{ } catch()的结构化异常处理来完成。
本章重点讲解了程序运行时候出现错误以及怎样处理,这些对于初学者在以后编程中经常遇到,反复多实践,养成好的编程习惯,多实践对C++机制熟悉防患于未然。
16.1 编码时的防错
if语句是C/C++语言中最简单、最常用的语句,但是很多程序员不良习惯,使用if语句会造成代码或运行错误。本实例讨论各种数据类型与零值比较,效果如图16-1所示。
图16-1 判断语句经典错误实例
定义布尔变量、整型变量、浮点变量和指针变量;然后布尔变量与零值比较,整型变量与零值比较,浮点变量与零值比较,指针变量与零值比较;分别给出正确写法代码,不良风格或错误代码写法。代码如下:
#include
using namespace std;
int main( void )
{
//1.不可将布尔变量直接与TRUE、FALSE或者1、0 进行比较
bool flag=true;
if (flag) //表示flag 为真
flag=false;
else
flag=true;
//其他的用法都属于不良风格,例如:if (flag == TRUE);if (flag == 1 );if (flag == FALSE);if (flag == 0)
//2.应当将整型变量用“==”或“!=”直接与0 比较
int value=5;
if (value == 0)
value=0;
if (value != 0)
value=5;
//不可模仿布尔变量的风格而写成:if (value),会让人误解 value 是布尔变量,if(!value)
//3.不可将浮点变量用“==”或“!=”与任何数字比较
float x=3.4;
float EPSINON=0.5;
if ((x>=-EPSINON) && (x<=EPSINON))
x=3.8;
//if (x == 0.0) // 隐含错误的比较
//4.应当将指针变量用“==”或“!=”与NULL 比较
char *p="3";
if (p == NULL) //p与NULL显式比较,强调p是指针变量
*p=NULL;
if (p != NULL)
*p=3;
//不要写成:if(p == 0),容易让人误解p 是整型变量,if(p != 0);或者if(p),容易让人误解p 是布尔变量if(!p)
cout<<"程序正常运行"<
system("pause");
return 0;
}
(1)不可将布尔变量直接与TRUE、FALSE或者1、0进行比较。根据布尔类型的语义,零值为“假”(记为FALSE),任何非零值都是“真”(记为TRUE)。TRUE的值究竟是什么并没有统一的标准。例如VC++将TRUE定义为1,而Visual Basic则将TRUE 定义为-1。
(2)应当将整型变量用“==”或“!=”直接与0比较。应当将指针变量用“==”或“!=”与NULL比较。指针变量的零值是“空”(记为NULL)。尽管NULL的值与0相同,但是两者意义不同。
(3)程序中有时会遇到if/else/return 的组合,应该将如下不良风格的程序if (condition)return x;return y;,改写为if (condition){return x;}else{return y;}或者改写成更加简练的return (condition ? x : y);。
提示:不良风格或错误代码应多测试看是否出现什么问题,实际编程中要注意这些细节。
编程中发生内存错误是件非常麻烦的事情。因为编译器编译的时候不能自动发现这些错误,只要在程序运行时才能捕捉到,往往让程序员头痛,所以如何预防是防止出错的主要的方法。本例效果如图16-2所示。
图16-2 函数实现把0内存删除
程序分两个演示,demo1()为单个的类重载new[ ]和delete[ ],内存的请求被定向到全局的new[ ]和delete[ ]操作符,而这些内存来自于系统堆;demo2()代码定义1个字符串arr,临时分配这些元素所需的内存空间大小arrSize,输入计算平均数个数,计算平均值,代码如下:
# include
# include
# include
class TestClass {
public:
void * operator new[ ](size_t size);
void operator delete[ ](void *p);
// .. other members here ..
};
void *TestClass::operator new[ ](size_t size)//重载new[ ]操作符
{
void *p = new int[size];//malloc(size);
return (p);
}
void TestClass::operator delete[ ](void *p) //重载delete[ ]操作符
{
delete[]p;
// free(p);
}
int demo1(void)
{
TestClass *p = new TestClass[10]; //为对象的数组分配内存
// ... etc ...
delete[ ] p; //释放数组分配的内存
cout<<"demo1 运行正常"<
return 0;
}
double fun(int a[],int n) //计算平均值函数
{
int i;
double s=0.0;
for(i=0;i
s+=a[i];
return s/n;
}
int demo2()
{
int arrSize; //元素的个数
int * arr,i;
cout<<"请输入计算平均数个数:";
cin>>arrSize; //临时分配元素的个数
arr=new int[arrSize]; //临时分配这些元素所需的内存空间(堆内存中)
if(arr!=NULL) //堆空间不够分配时,系统会返回一个空指针值NULL
{
cout<<"\n请输入计算平均数个数:\n";
for (i=0;i
cin>>arr[i];
cout<
cout<<"逐个输入数字:"<
for (i=0;i
cout<
cout<
double ave;
ave=fun(arr,arrSize);
cout<<"平均值="<
delete[]arr; //释放堆内存
}
else
cout<<"申请内存不足.\n";
return 0;
}
int main()
{
demo1();
cout <<"\n"; //换行
demo2();
cout << "\n";
system("pause");
return 0;
}
(1)演示1中,C++将对象数组的内存分配作为一个单独的操作,而不同于单个对象的内存分配。为了改变这种方式,同样需要重载new[ ]和delete[ ]操作符。但是提示:对于多数C++的实现,new[]操作符中的个数参数是数组的大小加上额外的存储对象数目的一些字节。在内存分配机制需重要考虑到这一点。应该尽量避免分配对象数组,从而使内存分配策略简单点。
(2)上述代码arr=new int[arrSize];,临时分配这些元素所需的内存空间,然后判断内存是否分配内存。
提示:常见的内存错误及其对策:内存分配未成功,却使用了它;内存分配虽然成功,但是尚未初始化就引用它;内存分配成功并且已经初始化,但操作越过了内存的边界;忘记了释放内存,造成内存泄露;释放了内存却继续使用它。
在编程中经常要给函数传输数值,要引起注意的是传送的是地址还是值,否则会引起错误,本例效果如图16-3所示。
图16-3 传送的是地址还是值
程序分两个演示:demo1()和demo2(),demo2()的输出就出现了错误,其代码如下:
#include
using namespace std;
void fucntion(char* b) //出现错误
{ b="abc"; //赋值
printf("In function1:%s\n",b);
}
int demo1(void)
{
char* a=NULL; //定义指针
printf("char* : %d\n",sizeof(a)); //打印大小
int* b=NULL;
printf("int* : %d\n",sizeof(b));
double* c=NULL; //定义指针
printf("double* : %d\n",sizeof(c)); //打印大小
return 0;
}
int demo2(void)
{
char* a; //定义指针
int b=(int)a; //转换成32位
printf("char* convert to int : %d\n",b);//打印
return 0;
}
void fucntion1(char** b)
{ *b="abc";
printf("In function:%s\n",*b);
}
void fucntion2(char*& b)
{ b="abc";
printf("In function:%s\n",b);
}
int main(int argc, char* argv[])
{
char* a=NULL; //指针初始化
demo1();
demo2();
fucntion(a); //调用函数
// fucntion1(&a);
// fucntion2(a); //调用函数
printf("In main:%s\n",a); //打印指针值
system("pause");
return 0;
}
demo2()程序输出“In function:abc,In main:(null)”,为什么main()函数中不显示abc呢?因为demo1()中,把char*的地址传给了函数,即把指针的地址传给了函数。所以demo2()中,只是把char*的值传给了函数,属于值传递。
程序的输入都建有一个缓冲区即输入缓冲区。一次输入过程是这样的,当一次键盘输入结束时会将输入的数据存入输入缓冲区,而cin函数直接从输入缓冲区中取数据。正因为cin函数是直接从缓冲区取数据,所以有时候当缓冲区中有残留数据时,cin函数会直接取得这些残留数据而不会请求键盘输入。本例效果如图16-4所示。
图16-4 cin输入列队错误实例
分3个函数进行演示:第一个演示demo(),getline()取得字符串并输出结果;第二个演示demo1(),cin取得字符串并输出结果;第三个演示demo2(),cin.get()取得字符串并输出结果。代码如下:
#include
using namespace std;
void demo(void)
{
char str[8]; //定义字符数组
cout<<"输入字符串"<
cin.getline(str, 5); //取得字符数组
cout<<"输出字符串为:"<
cin.getline(str, 5); //取得字符数组
cout<<"输出字符串为:"<
system("pause");
}
void demo1(void)
{
char str1[10], str2[10]; //定义字符数组
cout<<"输入2个字符串"<
cin>>str1; //取得字符数组
cin>>str2;
cout<<"输出字符串为:"<
cout<<"输出字符串为:"<
system("pause");
}
void demo2(void)
{
char c1, c2;
cout<<"输入2个字符"<
cin.get(c1); //取得字符
cin.get(c2);
cout<<"输出字符:"<
cout<
cout<<(int)c1<<" "<<(int)c2<
system("pause");
}
int main()
{
demo();
// demo1();
// demo2();
return 0;
}
(1)demo ()之所以第一次输入完后直接程序就结束了,而不是进行第二次输入,是因为第一次多输入的数据还残留在缓存区中,第二次输入就直接从缓存区中提取而不会请求键盘输入。
(2)demo1()第一次读取字符串时遇到空格则停止了,将abcd读入str1,并舍弃了空格,将后面的字符串给了第二个字符串。这证明了cin读入数据遇到空格结束,并且丢弃空格符;缓冲区有残留数据,读入操作直接从缓冲区中取数据。
(3)demo2()只执行了一次从键盘输入,显然第一个字符变量取的‘a’,第二个变量取的是Enter(ASCII值为10),这是因为该函数不丢弃上次输入结束时的Enter字符,所以第一次输入结束时缓冲区中残留的是上次输入结束时的Enter字符。
如果一个数组定义为n个元素,那么,对这n个元素(下标为0到n-1的元素)的访问都合法,如果对这n个元素之外的访问就是非法,称为“越界“。数组越界,这是一个很常见的错误,是初学者常犯的错误。编译时可能能通过,但运行的时候是隐患,运行时候会出现错误。本例效果如图16-5所示。
图16-5 数组超界访问实例
定义整型数组a[10],把1赋值a[10],输出这个值;定义字符型数组buf[10],赋值给这个字符型数组;定义整型数组a1[3],调用函数PutArray打印字符串。代码如下:
#include
#include
#include
using namespace std;
void PutArray(int *p, int length)
{
//在此判断入口参数p和length的有效性
//……
for(int i=0;i
printf("%d/t",p[i]); //打印字符串
}
void main(void)
{
int a[10] = {0}; //定义整型数组
int i = 0;
a[10] = 1;
printf("%d\n", i);
system("pause");
char buf[10]; //定义字符型数组
char i1 = 'a'; //定义字符
sprintf(buf, "123abcdefghijklmn");
printf(buf);
system("pause");
int a1[3]={2,4,6} ; //定义整型数组
printf("数组a1[3]调用函数PutArray的结果为:/n");
PutArray(a1, sizeof(a1)/sizeof(a1[0]));
}
(1)数组占用了一段连续的内存空间,可以通过指定数组下标来访问这块内存里的不同位置。因此当下标过大时,访问到的内存,就不再是这个数组“份内”的内存。访问的将是其他变量的内存了。常见的错误就是数组的size值和下标访问值弄错,数组的下标是从0开始的,最大的访问值是size-1。
(2)由于数组的元素个数默认情况下是不作为实参内容传入调用函数的,因此会带来数组访问越界的问题。函数PutArray()用传递数组元素个数的方法即:用两个实参,一个是数组名,一个是数组的长度。
提示:数组越界可能不会造成编译错误,但程序运行时数组访问越界的表现是不定的,有时似乎什么事也没有,程序一直运行(当然,某些错误结果已造成);有时则是程序一下子崩溃。因此在使用数组时,一定要在编程中判断是否越界以保证程序的正确性。
运算符sizeof()可以计算数组的大小(字节数)。但对指针来说,sizeof仅仅得到指针变量的字节数。当数组作为函数的参数进行传递时,数组就变成为同类型的指针,用sizeof是无法取得数组的大小的。本例效果如图16-6所示。
图16-6 数组错用sizeof实例
定义整型数组str1,输出这个数组大小;调用函数Sum(),输出数组大小;定义指针字符串pChar,把str1赋值该指针字符串,输出数组大小。代码实现如下:
#include
using namespace std;
void Sum(int *str)
{
cout<<"sizeof(str): "<
}
int main()
{
int str1[] = {21, 22, 22, 19, 34, 12};
cout<<"sizeof(str1): "<
Sum(str1); //整数数组作为参数传递给函数时,就变成为指针
int* pChar=str1;
cout<<"sizeof(pChar): "<
system("pause");
return 0;
}
函数Sum()中,大小为6的char型数组作为函数参数进行传递,代码cout<<"sizeof(str1)对数组sizeof()操作返回的只是指针的大小,无法返回数组的大小。
上面谈的数组越界,本实例演示的是sizeof长度不对,字符串会输出错误结果,效果如图16-7所示。
定义string类strArr1,string类pStrArr1分配内存并赋值,显示3个变量长度,演示输出strArr1和pStrArr1。代码如下:
#include
#include
using namespace std;
int main()
{
string strArr1[]={"Trend","Micro","sof"}; //定义string类型
string *pStrArr1 = new string[2]; //分配内存
pStrArr1 [0]="US";
pStrArr1 [1]="CN";
cout<<"strArr1长度:"<
cout<<"pStrArr1长度:"<
cout<<"string长度:"<
for(int i=0;i
cout<
cout<
for(i=0;i
//for(i=0;i
cout<
cout<
system("pause");
return 0;
}
(1)程序运行后输出TrendMicrosof,而不是USCN。这是因为sizeof(pStrArr1)运算得出的结果是指针pStrArr1的大小即4,这样就不能正确地输出USCN。
(2)字符串StrArr1是由3段构成的,所以sizeof(pStrArr1)是16*3=48。
定义类成员函数,对这个类的对象进行构造,如果对类操作中,对没有构造的类删除,那么就是出现致命错误,就如本例演示这样效果如图16-8所示。
图16-8 一个还没进入main()函数就崩溃的程序
定义类book,成员函数编号、价格和指向下一个指针,函数book *Bin,定义2个类*p1,*p2,输入编号和价格,输入‘0’退出。其代码如下:
#include
#include
using namespace std;
class book //定义类
{
public:
int bianhao; //编号
float jiage; //价格
book*uuu; //定义指向下一个指针
};
book *Bin()
{
book*p1,*p2; //定义2个类
cout<<"输入0退出\n";
while(1)
{
cout<<"请输入编号"<
p1=new book;
cin>>p1->bianhao; //取得编号
if(p1->bianhao==0) break;
cout<
cin>>p1->jiage; //取得价格
if(p1->jiage==0) break;
p2=p1; p1=p2->uuu; }
delete p1; //删除类
// if((p1->bianhao==0)||(p1->jiage==0)) //判断一下,再运行不会崩溃
p2->uuu=0; //删除类指针 return 0;
}
int main()
{
Bin(); //调用类book的函数Bin
return 0;
}
当第一次输入0的话,p2对象还没有构造出来,而跳出循环后却要delete这个没构造的对象,肯定出错。在delete的地方加个条件判断一下就不会崩溃了。
C++标准允许主函数main()函数既可以是无参函数,也可以是有参的函数;在main()中使用一个或更多的参数,如DOS命令行运行,需读取输入各种运行环境参数如用户名、密码等。这就是该实例所说的main()函数的两个参数argc和argv。本例效果如图16-9所示。
定义3个字符数组FullName、strGender、cGender和1个浮点数Salary,复制一个字符串,打印出结果。其代码如下:
图16-9 main()函数的参数传递
#include
using namespace std;
int main(int argc, char *argv[])
{
char FullName[40], strGender[20]; //定义字符数组
char cGender[10];
float Salary;
strcpy(FullName, argv[1]); //复制一个字符串
Salary = atof(argv[2]); //强制转换成浮点数
strcpy(cGender, argv[3]); //复制一个字符串
if( cGender == "m" || cGender == "M" ) //字符数组比较
strcpy(strGender, "Male"); //复制一个字符串
else if( cGender == "f" || cGender == "F" )
strcpy(strGender, "Female"); //复制一个字符串
else
strcpy(strGender, "Unknown Gender"); //复制一个字符串
cout << "Employee Information"; //打印字符串
cout << "\nFull Name: " << FullName; //打印字符数组
cout << "\nGender: " << strGender; //打印字符数组
cout << "\nSalary: " << Salary << endl; //打印字符数组
return 0;
}
DOS命令行中输入,179 "WWW.163.COM,LOOMMAN NETWORKS" 18.28 "M" ,得到上面图片所示的结果。main()函数形参的形式:main(int argc, char * argv[ ])。
代码main( int argc, char *argv[ ] )中:
— argc为整数,是传给main()的命令行参数个数,文件名也算一个参数。
— *argv为字符串数组,其中argv[0]为在命令行中执行程序名;argv[1]为在命令行中执行程序名的第一个字符串,argv[2]执行程序名的第2个字符串;argv[argc]为NULL。
提示:main()还有个形式:int main(int argc[,char *argv[ ] [, char *envp[ ] ] ]);,输入参数envp为环境变量数组指针。
数组存放同类数据的集合,其大小在定义时要事先定义好,不能在程序中进行调整,这就会造成一定存储空间的浪费。链表是动态数组,是在程序的执行过程中根据需要有数据存储时就向系统要求申请存储空间,决不构成对存储区的浪费。链表是一种复杂的数据结构,其数据之间的相互关系使链表分成三种:单链表、循环链表、双向链表,本实例举的单链表例子。本例效果如图16-10所示。
定义节点结构element和基类list,set继承基类,成员函数有插入、删除和输出元素。代码实现如下:
struct element
{ //定义节点结构
int val; //数据元素域
element *next; //链指针域
};
class list
{ //list类定义
element *elems; //elems为当前标识指针
public:
list(){elems=0;}
~list(); //析构函数
virtual bool insert(int val); //虚函数
virtual bool deletes(int val); //虚函数
bool contain(int val);
virtual void print(); //输出链表的各元素
};
class set:public list
{
int card;
public:
set(){card=0;}; //集合元素个数
bool insert(int val); //插入元素
bool deletes(int val); //删除元素
void print();
};
list::~list()
{
element *tmp=elems;
for(element *elem=elems;elem!=NULL;)
{ //循环释放各元素所占的内存
tmp=elem;
elem=elem->next; //指向下一个
delete tmp;
}
}
bool list::insert(int val)
{ //插入元素函数
element *elem=new element; //分配内存
if(elem!=NULL)
{ //将新元素插入到当前标识指针elems处
elem->val=val; //val赋值给新元素中的数据域内容
elem->next=elems;
elems=elem;
return true;
}
else return false;
}
bool list::deletes(int val){ //删除元素函数 }
bool list::contain(int val) //判断是否包含val
{
}
void list::print()
{ //输出链表的各元素
}
bool set::insert(int val) //插入元素
{
}
bool set::deletes(int val)
{
//删除链表中的元素
}
void set::print()//输出链表元素
{
}
void main()
{
list *p,list1; //定义list类链表
set set1; //定义链表
p=&list1;
//插入元素
p->insert(10); p->insert(28); p->insert(58); p->insert(78); p->insert(90);
p->print(); //输出链表元素
p->deletes(28); //删除元素
p->deletes(58);
p->print();
cout<
system("pause");
}
链表节点的数据结构定义:struct node{int num;struct node *p;} ;,除一个整型的成员外,成员p是指向与节点类型完全相同的指针。
单链表的创建过程如下:
(1)定义链表的数据结构。
(2)创建一个空表。
(3)利用malloc ( )函数向系统申请分配一个节点。
(4)将新节点的指针成员赋值为空。若是空表,将新节点连接到表头;若是非空表,将新节点接到表尾。
(5)判断是否有后续节点要接入链表,若有转到(3),否则结束。
单链表的输出过程如下:
(1)找到表头。
(2)若是非空表,输出节点的值成员,是空表则退出。
(3)跟踪链表的增长,即找到下一个节点的地址。
(4)转到(2)。
提示:在链表节点的数据结构中,非常特殊的一点就是结构体内的指针域的数据类型使用未定义成功的数据类型,这是在C语言中唯一规定可以先使用后定义的数据结构。
在C++程序中new[]操作符分配一块动态内存,delete[]对象撤销时释放内存,可以把内存的操作封装成一个类。本例效果如图16-16所示。
图16-16 编写一个堆内存管理类
程序声明类TestClass、指针成员string和浮点数number,对象创建时为string分配一块动态内存,对象撤销时释放内存,print是信息输出函数。代码实现如下:
#include
#include
using namespace std;
class TestClass
{
private:
char *string; //指针成员
float number; //数字
public:
TestClass(const char* sz,float p)
{
string=new char[strlen(sz)+1]; //对象创建时为string分配一块动态内存
strcpy(string,sz); //字符串复制
number=p; //价格
}
~TestClass()
{
delete[] string; //对象撤销时,释放内存,避免泄露
cout<<"清理现场"<
}
void print() //信息输出
{
cout<<"字符串:"<
cout<<"数字:"<
}
};
int main()
{
TestClass test1("test_string1",100); //调用构造函数声明TestClass变量
test1.print(); //信息输出
TestClass test2("test_string1",200); //调用构造函数声明TestClass变量
test2.print(); //信息输出
system("pause");
return 0;
}
(1)上述代码string=new char[strlen(sz)+1];,临时分配这些元素所需的内存空间,然后判断内存是否分配内存。
(2)C++将对象数组的内存分配作为一个单独的操作,而不同于单个对象的内存分配。为了改变这种方式,同样需要重载new[ ]和delete[ ]操作符。
提示:对于多数C++的实现,new[]操作符中的个数参数是数组的大小加上额外的存储对象数目的一些字节。在内存分配机制重要考虑的是这一点。应该尽量避免分配对象数组,从而使内存分配策略简单。
16.2 异常机制
C++允许在现有数据类型的基础上定义自己的数据类型。用关键字typedef实现这种定义,就像声明普通的int、double类型一样声明一个新的函数指针。本例效果如图16-12所示。
图16-12 类型改名——使用typedef定义类型
typedef int (*string_t)(const char*,const char*)返回值为int型,有两个char*参数函数指针声明了一个助记符(别名)string_t,在程序中可以使用“string_t cHar”。代码实现如下:
#include
#include
using namespace std;
typedef int (*string_t)(const char*,const char*);//使用typedef后,string_t可以当成一种类型来用
using namespace std;
int main()
{
char str1[]="hello,world";
char str2[]="world";
string_t cHar; //创建函数指针cHar
cHar=strcmp; //也可写作cHar=&strcmp;
int result=cHar(str1,str2); //也可写作int result=(*cHar)(str1,str2);
if (result==0)
cout<<"两个字符串相等"<
else
cout<<"两个字符串不等"<
system("pause");
return 0;
}
代码typedef char C;typedef unsigned int WORD;typedef char * string_t;typedef char field [50];定义了四种新的数据类型: C、WORD、string_t和field。它们分别代替char、unsigned int,、char*和char[50]。这样,就可以安全地使用以下代码:
C achar, anotherchar, *ptchar1;
WORD myword;
string_t ptchar2;
field name
typedef的格式是:typedef existing_type new_type_name;这里 existing_type是C++基本数据类型或其他已经被定义了的数据类型,new_type_name 是将要定义新的数据类型的名称。
提示:如果在一个程序中反复使用一种数据类型,而在以后的版本中有可能改变该数据类型的情况下,typedef 就很有用了。或者如果一种数据类型的名称太长,想用一个比较短的名字来代替,也可以是用typedef。
编写代码时需考虑周全,如欲打开一个文件,文件不存在。断言就是用于在代码中捕捉这些假设,可以将断言看作是异常处理的一种高级形式。断言表示为一些布尔表达式,程序员相信在程序中的某个特定点该表达式值为真。可以在任何时候启用和禁用断言验证,因此可以在测试代码时启用断言,而在部署代码时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新起用断言。本例效果如图16-13所示。
以可写的方式打开一个文件“test.txt”,如果不存在就创建一个同名文件,assert宏进行检测,关闭文件;以只读的方式打开一个文件"noexitfile.txt",assert宏进行检测,关闭文件。其代码如下:
图16-13 错误检查——使用assert宏进行检测
#include
#include
#include
int main( void )
{
FILE *fp;
fp = fopen( "test.txt", "w");//以可写的方式打开一个文件,如果不存在就创建一个同名文件
assert( fp ); //加断言,所以这里不会出错
fclose( fp );
fp = fopen( "noexitfile.txt", "r" ); //以只读的方式打开一个文件,如果不存在就打开文件失败
assert( fp ); //所以这里出错
fclose( fp ); //程序永远都执行不到这里来
printf("The end");
return 0;
}
assert宏的原型定义在
#include
每个assert只检验一个条件,因为同时检验多个条件,如果断言失败,就无法直观地判断是哪个条件失败。
提示:使用assert的缺点是,频繁的调用会极大地影响程序的性能,增加额外的开销。在调试结束后,可以通过包含#include
#include
本实例讨论程序中止和退出函数。exit()是C++语言函数库的一个函数,功能实现中止程序的执行的功能,并在退出前对程序占用的资源进行必要的清理。本例效果如图16-14所示。
定义4个双精型变量,程序读取输入变量,判断读取输入是否为0,如为0,exit结束程序。其代码如下:
#include
#include
using namespace std;
void main()
{
double num1,num2,a,b; //定义双精型
cout<<"Input the numbers.the first number:"; //输入要计算的第一个数
cin>>num1; //读取输入的第一个数
cout<<"Input the senond number:";
cin>>num2; //读取输入的第二个数
a=num1; //赋值给类变量a
b=num2; //赋值给类变量a
if(b==0)
{
cout<<"Error! The program will be terminated!"<
exit(0);
}
cout<<"a/b The result is:"<两数相除的值
system("pause");
}
(1)exit()函数结束程序,返回一个值给操作系统,告知程序的最后状态。在调用exit()函数之后,控制权会移交给操作系统。在结束程序之前,exit()函数会调用之前使用atexit()注册过的所有函数,按照LIFO(后进先出)次序调用,关闭所有打开的文件,删除tmpfile()函数建立的所有临时文件。
(2)abort()函数通过发出一个SIGABRT信号终止程序执行。它不会清空缓冲区,也没有调用之前用atexit()函数注册的清理函数。
abort()函数原型位于头文件
图16-15 使用abort()函数进行异常退出
定义2个定义浮点数,取得输入浮点数,如浮点数为0调用abort结束程序。其代码如下:
#include
#include
using namespace std;
int main()
{
float i,j; //定义浮点数
cout<<"请输入两个浮点数i和j:"<
cin>>i>>j;
if (j==0)
{
cout<<"输入错误,j为0";
abort(); //如果j等于0,调用abort函数
}
else
cout<<"i/j 是: "<<(i/j)<
system("pause");
return 0;
}
abort()表示程序异常结束。默认情况下,调用abort()导致运行期诊断和程序自毁,可能会也可能不会刷新缓冲区、关闭被打开的文件及删除临时文件,这依赖于编译器的具体实现。
提示:一般调用abort()处理灾难性的程序故障,因为abort()的默认行为是立即终止程序,就必须负责在调用abort()前存储重要数据。
C++异常处理机制是一个用来有效地处理运行错误非常强大且灵活的工具,它提供了更多的弹性、安全性和稳固性,克服了传统方法所带来的问题。实际上自己可以定义个异常,程序运行异常,如除数为0时候,捕获异常做相应的处理。本例效果如图16-16所示。
图16-16 异常对象——自定义异常对象
代码演示的是处理除数为0的异常。主函数定义异常,调用函数fuc(),fuc()将除数为0的异常用try/catch语句来捕获异常,并使用throw语句来抛出异常,从而实现异常处理。其代码如下:
#include
#include
using namespace std;
double fuc(double x, double y) //定义函数
{
if(y==0)
{
throw y; //除数为0,抛出异常
}
return x/y; //否则返回两个数的商
}
void main()
{
double res;
try //定义异常
{
res=fuc(2,3);
cout<<"The result of x/y is : "<
res=fuc(4,0); //出现异常,函数内部会抛出异常
}
catch(double) //捕获并处理异常
{
cerr<<"error of dividing zero./n";
system("pause");
exit(1); //异常,退出程序
}
system("pause");
}
如果在try语句块的程序段中(包括其中调用的函数)发现了异常,且抛弃了该异常,则这个异常就可以被try语句块后的某个catch语句所捕获并处理,捕获和处理的条件是被抛弃的异常的类型与catch语句的异常类型相匹配。由于C++使用数据类型来区分不同的异常,因此在判断异常时,throw语句中的表达式的值就没有实际意义,而表达式的类型就特别重要。
提示:函数原型中的异常说明要与实现中的异常说明一致,否则容易引起异常冲突。
实际编程中调用set_terminate中设定的终止函数,可以使用set_terminate指定的函数进行一些清除性的工作,其后再调用exit(int)函数终止程序。如果set_terminate函数被调用,这个时候意味着问题无法解决,情况不可收拾,必须结束程序。本例效果如图16-17所示。
自定义terminate()函数,设置新的teminate()函数,并返回之前teminate()函数指针。其代码如下:
图16-17 使用set_terminate()函数设置terminate()函数指针
#include
#include
#include
#include
#include
using namespace std;
namespace test
{
void doTerminate() //自定义terminate函数
{
cout << "1. go in doTerminate\n" << flush;
try
{
throw; //并没有重新抛出旧的int(其实已经被处理), 而是重新抛出异常
}
catch (int x)
{
cout << "2. get int exception\n"<< flush;
}
catch (...)
{
cout << "3. get unexcepted exception\n"<< flush;
}
cout << "4. leaving doTerminate\n" << flush;
abort(); //异常退出
}
void test()
{
int code = 0;
set_terminate(doTerminate);//设置新的teminate函数,并返回之前teminate函数指针
__try{
throw; //抛出特殊异常
}
__except((code = GetExceptionCode(), EXCEPTION_EXECUTE_HANDLER)) // catch (...)抓不住
{
cout << "got unknown exception, code=" << hex << code << endl << flush;
}
throw 1; //抛出一个int测试
}
}
void main(void)
{
test::test(); //调用命名空间,::作用域限定符
system("pause");
}
(1)throw如果在catch块中(或者catch块调用的函数中),throw会将异常重新抛出。
(2)throw如果不在catch中,会引发一个特殊异常,C++的catch(...)是无法抓到的,只能用windows的__except来抓(系统也是这么抓的)。关键terminate_handler是否是在系统的catch代码块中调用,如果是这样,就会出现Exception类型的异常并输出或产生一个新的特殊异常,导致系统直接退出。
提示:函数所抛出的异常没有列在异常规范说明中,系统自动调用库函数unexpected,还可以调用set_unterminate库函数中的自定义unexpected。
本实例定义嵌套处理时候析构的例子,上面实例谈了很多异常try{} throw例子,本实例谈异常处理时的析构,效果如图16-18所示。
图16-18 异常处理时的析构
定义两个类Expt、Demo,类中各自定义了构造函数和析构函数,类Expt成员函数ShowReason,捕获异常ry…catch处理。其代码如下:
#include
#include
void MyFunc( void );
class Expt //Expt类定义
{ public:
Expt(){}; //构造函数
~Expt(){}; //析构函数
const char *ShowReason() const
{
return "Expt类异常。";
}
};
class Demo
{ public:
Demo()
{ cout<<"构造 Demo."<
}
~Demo()
{ cout<<"析构 Demo."<
}
};
void MyFunc()
{ Demo D; //try块外定义的Expt类对象
cout<<"在MyFunc()中抛掷Expt类异常。"<
throw Expt(); //抛出异常
}
void main()
{
cout<<"在main函数中。"<
try
{ cout<<"在try块中,调用MyFunc()。" <
MyFunc(); //调用函数
}
catch( Expt E )
{ cout<<"在catch异常处理程序中。"<
cout<<"捕获到Expt类型异常:";
cout<
}
catch( char *str )
{ cout<<"捕获到其他的异常:"<
cout<<"回到main函数。从这里恢复执行。"<
system("pause"); }
找到一个匹配的catch异常处理后,初始化参数。将从对应的try块开始到异常被抛出处之间构造的(且尚未析构的)所有对象进行析构,处理之后开始恢复执行。
定义类和类的继承在编程中经常会涉及到。本实例定义内存池和异常类,继承这两类定义内存分配的新类,查看输出结果及程序运行时是怎样捕捉到异常的。效果如图16-19所示。
图16-19 内存整理算法
声明异常类Exception,线程池类PacketPool,内存分配类Packet继承PacketPool。代码如下:
typedef string QString;
//Class Exception 一种类似于Java的异常机制类
class Exception{
public:
Exception(const QString &pReason):reason(pReason){}
Exception(const char *pReason) {
reason=pReason;
}
QString what(){ return reason; }
QString reason;
};
//内存池的设计为 'multi-allocate, release once'.
//这是一个sniffer的内存特征
class PacketPool {
static vector
static size_t sizeLeftInUnit;
static unsigned char *currentPosition;
public:
inline void * operator new(size_t);
inline void operator delete(void*); //实际上是不能使用的
static void releasePool();
};
const size_t POOL_UNIT_SIZE =2097152; //2M大小
//const size_t POOL_UNIT_SIZE =100; //用小点内存测试下
vector
size_t PacketPool::sizeLeftInUnit =0;
unsigned char* PacketPool::currentPosition =NULL;
//在这种情况下,抛出异常:
//1. 值参数的'size'要大于POOL_UNIT_SIZE
//2.无足够内存分配给新的pool单元.
inline void * PacketPool::operator new (size_t size) {
if(size > POOL_UNIT_SIZE) {
Exception e("Size too big for a pool unit");
throw e; //抛出异常
}
if(sizeLeftInUnit
printf("Expanding pool...\n");
unsigned char *unit =(unsigned char*)malloc(POOL_UNIT_SIZE);
if(!unit) {
Exception e("Not enough memory for new packets");
throw e; //抛出异常
}
units.push_back(unit);
currentPosition =unit;
sizeLeftInUnit =POOL_UNIT_SIZE;
printf("Expanding pool !Pool Address :%x",(unsigned int)currentPosition);
}
//分配
sizeLeftInUnit -=size;
unsigned char* oldPosition =currentPosition;
currentPosition +=size;
return oldPosition;
}
inline void PacketPool::operator delete(void *) {
PacketPool::releasePool();
}
void PacketPool::releasePool() {
vector
while(iter !=units.end() ) {
free(*iter); //如果这里删除将嵌套调用
iter =units.erase(iter);
}
currentPosition =NULL; //为空
printf("Release Pool Complete\n");
}
class Packet :public PacketPool{ //定义类
char a[1001];
public:
Packet() {}
};
(1)这是一个特殊内存池的实现,特征为多次分配,一次释放(使用releasePool)。其使用的方法为公共继承,子类继承后使用new申请的实例都将在内存池上。注意文件中的一个测试,因内存池单元非常小,要自己根据需要手动修改const size_t POOL_UNIT_SIZE的值,同时一定要在调用中捕捉异常。
(2)定义异常类Exception,线程池类PacketPool,内存分配类Packet继承PacketPool。
提示:常见的内存错误及其对策:内存分配未成功,却使用了它。内存分配虽然成功,但是尚未初始化就引用它。内存分配成功并且已经初始化,但操作越过了内存的边界。忘记了释放内存,造成内存泄露。释放了内存却继续使用它。
16.3 异常发生时的内存管理
在编程中经常会出现内存泄漏的现象,也就是超出申请的内存空间。如申请了20个内存,指针指向位置超出了20,若这时执行delete []命令,程序就会出现错误,结果如图16-20所示。
图16-20 错误地释放指针导致程序出错
程序首先分配固定大小的指针变量,输入字符串,最后执行delete []命令。代码如下:
int main(void)
{
char *p=new char[20]; //分配固定大小的指针字符串
cin>>p;
//p=NULL; //① 如果不加上这条会导致程序崩溃
delete []p; //释放指针
p=NULL;
return 0;
}
(1)在①处,若p指针已经离开了动态分配的存储区。也就是说,分配了20字节的单元,p指针已经指向了第21字节处或更大,第21字节处已经离开了存储区,此时若再执行delete,释放的,将是没用过的第21字节的存储单元,这时系统将无法释放已经分配的内存,这就是所谓的“内存泄漏”问题,它是一个十分严重的错误。
(2)回收用 new[] 分配的一组对象的内存空间的时候用delete[],p=NUL表示指向空指针,指针清空。
提示:本程序简单介绍了内存泄漏,使用指针分配长度时要注意。
16.4 auto_ptr类
智能指针是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露。所有的智能指针都会重载->和*操作符,能自动销毁,复制时可以修改源对象等。
智能指针根据需求不同,设计也不同(写时复制,赋值即释放对象拥有权限、引用计数等,控制权转移等),auto_ptr即是一种常见的智能指针。本例效果如图16-21所示。
图16-21 使用类自动管理指针
HasPtr智能指针的声明如下,保存一个指向U_Ptr对象的指针,U_Ptr对象指向实际的int基础对象;U_Ptr为计数器;里面有个变量use和指针ip,use记录了*ip对象被多少个HasPtr对象所指。代码如下:
#include
using namespace std;
class U_Ptr //定义类
{
friend class HasPtr; //友元
int *ip;
size_t use;
U_Ptr(int *p) : ip(p) , use(1)
{
cout << "U_ptr constructor called !" << endl;
}
~U_Ptr()
{
delete ip;
cout << "U_ptr distructor called !" << endl;
}
};
class HasPtr
{
public:
//构造函数:p是指向已经动态创建的int对象指针
HasPtr(int *p, int i) : ptr(new U_Ptr(p)) , val(i)
{
cout << "HasPtr constructor called ! " << "use = " << ptr->use << endl;
}
//复制构造函数:复制成员并将使用计数加1
HasPtr(const HasPtr& orig) : ptr(orig.ptr) , val(orig.val)
{
++ptr->use;
cout << "HasPtr copy constructor called ! " << "use = " << ptr->use << endl;
}
//赋值操作符
HasPtr& operator=(const HasPtr&);
// 析构函数:如果计数为0,则删除U_Ptr对象
~HasPtr()
{
cout << "HasPtr distructor called ! " << "use = " << ptr->use << endl;
if (--ptr->use == 0)
delete ptr;
}
//获取数据成员
int *get_ptr() const { return ptr->ip; }
int get_int() const { return val; }
//修改数据成员
void set_ptr(int *p) const { ptr->ip = p; }
void set_int(int i) { val = i; }
//返回或修改基础int对象
int get_ptr_val() const { return *ptr->ip; }
void set_ptr_val(int i) { *ptr->ip = i; }
private:
U_Ptr *ptr; //指向使用计数类U_Ptr
int val;
};
//注意,这里赋值操作符在减少做操作数的使用计数之前使rhs的使用技术加1,从而防止自我赋值
HasPtr& HasPtr::operator = (const HasPtr &rhs)
{
//增加右操作数中的使用计数
++rhs.ptr->use;
// 将左操作数对象的使用计数减1,若该对象的使用计数减至0,则删除该对象
if (--ptr->use == 0)
delete ptr;
ptr = rhs.ptr; //复制U_Ptr指针
val = rhs.val; //复制int成员
return *this;
}
int main(void)
{
int *pi = new int(32);
HasPtr *hpa = new HasPtr(pi, 200); //构造函数
HasPtr *hpb = new HasPtr(*hpa); //拷贝构造函数
HasPtr *hpc = new HasPtr(*hpb); //拷贝构造函数
HasPtr hpd = *hpa; //拷贝构造函数
//返回或修改基础int对象
cout << hpa->get_ptr_val() << " " << hpb->get_ptr_val() << endl;
hpc->set_ptr_val(20000);
cout << hpa->get_ptr_val() << " " << hpb->get_ptr_val() << endl;
hpd.set_ptr_val(10);
cout << hpa->get_ptr_val() << " " << hpb->get_ptr_val() << endl;
delete hpa; //删除对象
delete hpb;
delete hpc;
cout << hpd.get_ptr_val() << endl; //返回或修改基础int对象
system("pause");
return 0;
}
上述代码定义仅由HasPtr类使用的U_Ptr类,用于封装使用计数和相关指针,这个类的所有成员都是private,不希望普通用户使用U_Ptr类,所以它没有任何public成员,将HasPtr类设置为友元,使其成员可以访问U_Ptr的成员。
提示:实际上智能指针的引用计数类似于java的垃圾回收机制。java的判定很简单,如果一个对象没有引用所指,那么该对象为垃圾,系统就可以回收了。
标准auto_ptr智能指针机制知道的程序员多,但使用它的人少。因为auto_ptr较好地解决了C++设计和编码中常见的问题,正确地使用它可以生成健壮的代码。本实例阐述了如何正确运用auto_ptr来使的代码更加安全、避免对auto_ptr危险但常见的误用。本例效果如图16-22所示。
图16-22 使用auto_ptr类智能指针管理堆内存
定义个类TC,定义构造函数和析构函数,定义成员函数foo,定义auto_ptr类智能指针,指针中捕获异常try…catch的处理。主函数调用成员函数foo,输入为true,捕获异常try…catch处理。其代码如下:
#include
#include
using namespace std;
class TC
{
public:
TC(){cout<<"TC()"<
~TC(){cout<<"~TC()"<
};
void foo(bool isThrow)
{
auto_ptr
//TC *pTC = new TC; //方法1
Try //抛出异常
{
if(isThrow)
throw "haha"; //抓取异常
}
catch(const char* e)
{
//delete pTC; //方法1
throw; //抓取异常
}
//delete pTC; //方法1
}
int main()
{
Try //抛出异常
{
foo(true);
}
catch(...) //抓取异常
{
cout<<"caught"<
}
system("pause");
}
(1)如果采用方法1,那么必须考虑到函数在throw异常的时候释放所分配的内存。这样造成的结果是在每个分支处都要很小心的手动delete pTC;。
(2)如果采用方法2,那就无需操心何时释放内存,不管foo()因何原因退出,栈上对象pTC的析构函数都将调用,因此托管之中的指针所指的内存必然安全释放。
(3)要注意使用中的一个陷阱,那就是指针的托管权是会转移的。实例中如果 auto_ptr
提示:auto_ptr不会降低程序的效率,但auto_ptr不适用于数组,auto_ptr更不可以大规模使用。
16.5 RTTI机制
标准C++中有四个类型转换符:static_cast、dynamic_cast、reinterpret_cast、和 const_cast。static_cast把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。dynamic_cast把expression转换成type-id类型的对象。 reinpreter_cast。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针。 const_cast常量指针被转化成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。
dynamic_cast主要用于在多态的时候,它允许在运行时刻进行类型转换,从而使程序能够在一个类层次结构中安全地转换类型,把基类指针(引用)转换为派生类指针(引用)。本例效果如图16-23所示。
图16-23 dynamic_cast使用
定义个类A,定义构造函数和析构函数,定义虚函数Print();定义个类A继承B。其代码如下:
#include
#include
using namespace std;
class A
{
public:
virtual void Print() { cout<<"This is class A."<
};
class B : public A
{
public:
void Print() { cout<<"This is class B."<
};
class C : public A
{
public:
void Print() { cout<<"This is class C."<
};
void Handle(A *a)
{
if (dynamic_cast(a))
{
cout<<"I am a B truly."<
}
else if (dynamic_cast
{
cout<<"I am a C truly."<
}
else
{
cout<<"I am alone."<
}
}
int main()
{
A *pA = new B();
Handle(pA);
delete pA;
pA = new C();
Handle(pA);
return 0;
}
(1)当类中存在虚函数时,编译器就会在类的成员变量中添加一个指向虚函数表的vptr指针,每一个class所关联的type_info object也经由virtual table被指出来,通常这个type_info object放在表格的第一个slot。当我们进行dynamic_cast时,编译器会帮我们进行语法检查。
(2)如果指针的静态类型和目标类型相同,那么就什么事情都不做;否则,首先对指针进行调整,使得它指向vftable,并将其和调整之后的指针、调整的偏移量、静态类型以及目标类型传递给内部函数。
(3)其中最后一个参数指明转换的是指针还是引用。两者唯一的区别是,如果转换失败,前者返回NULL,后者抛出bad_cast异常。
16.6 类型转换操作符
dynamic_cast主要用于在多态的时候,它允许在运行时刻进行类型转换,从而使程序能够在一个类层次结构中安全地转换类型,把基类指针(引用)转换为派生类指针(引用)。本例效果如图16-24所示。
图16-24 const_cast转换格式
定义个类CA,定义构造函数和析构函数,定义成员函数CA,定义auto_ptr类智能指针,指针中捕获异常try…catch的处理。主函数调用成员函数foo,输入为true,捕获异常try…catch处理。其代码如下:
#include
using namespace std;
class CA
{
public:
CA() :m_iA(10){}
int m_iA;
};
int main()
{
const CA *pA = new CA;
// pA->m_iA = 100; // Error
CA *pB = const_cast
pB->m_iA = 100;
// Now the pA and the pB points to the same object
cout << pA->m_iA << endl;
cout << pB->m_iA << endl;
const CA &a = *pA;
// a.m_iA = 200; // Error
CA &b = const_cast
b.m_iA = 200;
// Now the a and the b reference to the same object
cout << b.m_iA << endl;
cout << a.m_iA << endl;
}
(1)const_cast的转换格式:const_cast
(2)该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。
(3)常量指针被转化成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。
16.7 本章练习