C++中头文件中class的两个花括号后面要加上分号,否则会出现很多的莫名奇妙的错误。
一、
每一个C++程序(或者由多个源文件组成的C++项目)都必须包含且只有一个main()函数。对于预处理指令,如#include <iostream.h>为C语言风格的头文件,标准C++仍支持这种格式。但也可采用C++风格:#include <iostream>,但为了使iostream中的定义对程序有效,还需加上名称空间编译指令:using namespace std;//不要忘记分号。using是代码编译之前处理的指令。namespace为名称空间,它是标准C++的 一个新特性,用于解决程序中同名标识符(如同名函数)存在的潜在危机。
二、
/*…*/用于多行注释,//用于单行注释。每个预处理命令(宏定义,文件包含,条件编译)必须单独占用一行,且在尾部不能加分号,必须以#开始(#define,#include, #ifde f..#else..#endif,#if..#elif..#endif)。
三、
在C++中常用exit()和abort()函数来终止程序执行,exit(0)表示正常退出程序;exit(非0值)表示异常退出。
四、 try与catch、throw用法
(1)基础介绍
try语句块的作用是启动异常处理机制,侦测try语句块的程序语句执行时可能产生异常。如有异常则抛出异常(也可以直接用throw主动抛出异常)。
catch语句块用来捕捉try语句块产生的异常(包括用throw抛出的异常),然后进行处理。try总是与catch一同出现的,在一个try语句块之后,至少应该有一个catch语句块。catch语句块之前必须是try语句或另一个catch块。当catch中的整个形参为"..."时,表示catch能捕捉任何类型的异常。
try{...
throw value; //程序中主动抛出异常
}catch(形参类型[形参名]) //将异常值传入形参,并在catch块中使用该形参
{ ...
//异常处理语句
}
throw抛出异常值,catch接受(传入形参),此时throw必须在try语句块中才行。
(2)深入throw:
(i) 程序接受到throw语句后就会自动调用析构器,把该域(try后的括号内)对象clean up,然后再进入catch语句(如果在循环体中就退出循环)。这种机制会引起一些致命的错误,比如,当“类”有指针成员变量时(又是指针!),在“类的构建器”中的throw语句引起的退出,会导致这个指针所指向的对象没有被析构。这里很基础,就不深入了,提示一下,把指针改为类就行了,比如模板类来代替指针,在模板类的内部设置一个析构函数。
(ii)语句“throw;”抛出一个无法被捕获的异常,即使是catch(...)也不能捕捉到,这时进入终止函数
(3)深入catch:一般的catch出现的形式为
try
{...
}catch(except1 &)
{...
}catch(except2 &)
{...
}catch(...) //接受所有异常
{...
}
一般都写成引用(except1&),原因很简单,效率。
抛出异常,但是catch不到异常怎么办?(注意没有java类似的finally语句)在 catch没有捕获到匹配的异常的时候,会调用默认的终止函数。可以调用set_terminate()来设置终止函数,参数是一个函数指针,类型是:void (*terminate)()。
若没有try-catch,直接在程序中throw,会怎么样?导致程序终止
(4) try一个函数体,形式如下
void fun(type1,type2) try //try放在函数体之后
{
//函数定义
}catch(type X)
{...}
这个用法的效果就相当于:
void fun()
{
try{函数定义}
}
(5)throw一个函数体,形式如下:
void fun (); //能抛出任何类型的异常
void fun () throw(except1,except2,except3)
{...} //后面括号里面是一个异常参数表,本例中只能抛出这3中异常
void fun () throw(){} //参数表为空,不能抛出异常
假设fun()中抛出了一个不在“异常参数表”中的异常,会怎么样?
答:调用set_terminate()中设定的终止函数。然而,这只是表面现象,实际上是调用默认的unexpected()函数,然而这个默认的unexpected()调用了set_terminate()中设定的终止函数。可以用set_unexpected()来设置unexpected,就像set_terminate()一样的用法,但是在设定了新的“unexpected ()”之后,就不会再调用set_terminater中设定的终止函数了。这个语法是很有用的,因为在用别人的代码时,不知道哪个地方会调用什么函数又会抛出什么异常,用一个异常参数表在申明时限制一下,很实用。
(6)throw不一定出现在try语句块中,它可以出现在任何需要的地方,即使在catch块中,仍然可以继续使用throw,只要最终有catch可以捕捉它抛出的异常即可。当throw出现在catch块中时,通过throw既可以重新抛出一个新类型的异常,也可以重新抛出当前这个异常(此时,throw不应带任何实参)。
try
{ ...
}catch(int)
{
throw "hello exception";//抛出一个新的异常,异常类型为const char*
}catch(float)
{
throw; //重新抛出当前的float类型异常
}
五、
对于类中的public成员,它们是公有的,可以在类外访问;
对于protected成员,它们是半保护的,具有半公开性质,可在类中或子类中访问;
对于privated成员来说,它们是私有的,不能在类外访问,数据成员和函数成员只有由本类的其它函数成员定义时调用(对象都不能访问私有成员)。
需要说明的是,若一个成员函数的 声明和定义同时在类体中完成,则该成员函数不需要再单独定义,类体中定义的函数相当在类外定义的内联(incline)函数。如果所有的成员函数都在类体中定义,则在类外的成员函数定义可以省略。当类的成员函数的定义是在类体外部完成时,必须用作用域运算符::来告知编译器该函数所属的类。该类对象的成员访问方式与结构体访问成员相同,对象只能访问类中的公有成员,对于保护成员和私有成员均不能访问。
六、
类的构造函数,析构函数,赋值函数
(1)构造函数:该类对象被创建时,编译系统为对象分配内存空间,并自动调用该构造函数->由构造函数完成成员的初始化工作。 构造函数的作用:初始化对象的数据成员,构造函数的函数名与类名相同。构造函数一般有以下几种:
无参构造函数:example()。如果创建一个类你没有写任何构造函数,则系统会自动生成默认的无参构造函数,函数为空,什么都不做。只要你写了某一种构造函数,系统就不会再自动生成这样一个默认的构造函数,如果希望有一个这样的无参构造函数,则需要自己显示地写出来。
一般构造函数(也称重载构造函数):example(arg1,arg2...)。一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同(基于c++的重载函数原理)。创建对象时根据传入的参数不同调用不同的构造函数。
复制构造函数(也称为拷贝构造函数):example(const example &c)。复制构造函数的参数为本类其它对象的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中。若没有显示的写复制构造函数,则系统会默认创建一个复制构造函数,但当类中有指针成员时,由系统默认创建该复制构造函数会存在风险,具体原因请查询 有关 “浅拷贝” 、“深拷贝”的文章论述。系统创建的默认复制构造函数只会执行“浅拷贝”,即将被拷贝对象的数据成员的值一一赋值给新创建的对象,若该类的数据成员中有指针成员,则会使得新的对象的指针成员所指向的地址与被拷贝对象的指针成员所指向的地址相同,delete该指针时则会导致两次重复delete而出错。此时应该自己定义复制构造函数,在复制构造函数中用new为新对象的指针成员指重新开辟一块内存,然后用strcpy函数将被拷贝对象的指针成员所指向的内容拷贝到新内存中去,这样则新创建的对象的指向新内存地址的指针成员与原对象的指针成员就不再指向同一地址了。
example(const example & c)
{
// 将对象c中的数据成员值复制过来,函数中可以直接访问对象c的私有成员
m_real =
c.m_real;
m_img =
c.m_img;
}
类型转换构造函数:example(double c)。根据一个指定的类型的对象创建一个本类的对象。前面将根据一个double类型的对象创建了一个example对象。
void main()
{
// 调用了无参构造函数,数据成员初值被赋为0.0
example c1,c2;
// 调用一般构造函数,数据成员初值被赋为指定值
example c3(1.0,2.5);
example c3 = example(1.0,2.5); // 另一种形式
// 把c3的数据成员的值赋值给c1
// 由于c1已经事先被创建,故此处不会调用任何构造函数,只会调用 = 号运算符重载函数(赋值函数)
c1 = c3;
// 调用类型转换构造函数
// 系统首先调用类型转换构造函数,将5.2创建为一个本类的临时对象,然后调用等号运算符重载,将该临时对象赋值给c1
c2 = 5.2;
// 调用拷贝构造函数( 有下面两种调用方式)
example c5(c2);
example c4 = c2; // 注意和 = 运算符重载区分,这里等号左边的对象事先没有创建,故需要调用拷贝构造函数,参数为c2
}
(2)赋值函数:example &operator=( const example &rhs)。注意,这个类似复制构造函数,将=右边的本类对象的值复制给等号左边的对象,它不属于构造函数,等号左右两边的对象必须已经被创建。若没有显示的写=运算符重载,则系统也会创建一个默认的=运算符重载,只做一些基本的拷贝工作。
example &operator=( const examplw &rhs )
{
// 首先检测等号右边的是否就是左边的对象本,若是本对象本身,则直接返回
if ( this == &rhs )
{
return *this;
}
// 复制等号右边的成员到左边的对象中
this->m_real =
rhs.m_real;
this->m_imag =
rhs.m_imag;
// 把等号左边的对象再次传出
// 目的是为了支持连等eg: a=b=c,系统首先运行b=c
// 然后运行a= ( b=c的返回值,这里应该是复制c值后的b对象)
return *this;
}
(3)析构函数:~example()。如果没有定义析构函数,编译器会自动生成。其作用是释放一个对象所占的内存(当要删除对象时调用)。它的一个典型应用是在构造函数中用new为一个类中指针型成员开辟独立的动态内存空间,而在析构函数中用delete释放它们。析构函数也只是一个函数,和普通函数没有区别,你想它做什么他就做什么,只是你要告诉他而已。因此析构函数中也可以是一个空函数,只是在合理的时候释放自己分配的内存,如new了一定要delete掉,malloc了一定要free掉,至于你放在哪里随你愿意,不必非要在析构里面,而且放在析构函数里可能会造成不想再使用的内存在程序运行时一直占用(因为只有删除对象时才会调用析构函数)。
七、
关于function & test(){return value;}中函数名前面的&,表示函数的引用返回。引用返回用于当想用函数找到引用应该被绑定在哪一个变量上面时。定义一个变量a,即可调用a = &test();表示变量a的地址与返回值value的地址相同。test()返回值变化引起变量a的值变化。若value为数组名,则写成function * test(){return value;}
所谓引用&就是一个变量的别名,它后面紧跟着就是它要引用的变量名。引用只是一个别名,并不是一种类型,只能看到它所引用数据的地址,所以它与指针符*不同。引用在内存中没有地址,也不占用空间,一旦初始化就不能修改,所以没有指向引用的指针。也就是说引用相当于一个常量指针,指向的地址已固定(但是指向地址中的内容可以改变),不能再用此引用指针指向其它地址。
引用其实是指向某个东西,所以从这点意义上讲是"指针",但是他绝对不是单独存在的,他一定是指示某个已经存在的东西,他一定是某个已经存在的物体的"别名"。而且它如果被初始化赋值给某个变量后,他就始终是这个变量的替身。对引用的改变就是对这个变量的改变。
比如int &a=b;即a的地址就是b的地址,那么b就可以看成是变量a的别名,因为是将a的引用赋给变量b。按我的理解,变量b只关心变量a地址的内容,并不关心变量a本身。只是由于在内存中变量a地址的内容就是变量a本身,所以才将b看成变量a的别名。变量a本身一旦改变,则变量a地址的内容也改变,即b也随之改变。