17.2.1不能通过基类指针访问派生类特有的方法》》》》》 24
第一章节 《入门》
G++ --version >>>检查是否安装gcc
http://developer.apple.com》》》注册成为APPle开发人员
PATH:》》》计算机》》高级系统设置》》》环境变量》》》在变量值文本框中添加:PATH》》》OK
G++源代码文件:扩展名>>>>> .cpp || .cxx || .cp || .c 一般选择 .cpp
(1)文本编辑器创建源代码;
(2)将源代码转换为目标文件;
(3)使用链接器链接按目标文件和必要的库,生成可执行的程序,
(4)运行可执行文件;
Gcc编译器将编译和链接合二为一: g++ out.cpp -O out.exe
操作系统自带的简单的编译器:Notepad 、vim 、gedit 或 Emacs
G++面向对象编程》》》》》(1)封装性(2)继承(3)多态
1》类:c++支持通过创建用户定义的类型来封装属性,这种类型成为类;
2》继承和重用:可以将新类型声明为现有类型的扩展、新类型成为继承了现有的类型;
3》多态:是指同一样东西的多种形态;
#include<iostream> int main( ){std::cout<<”hello world!\n”; return 0;}
#符号支出当前代码行是一个编译指令,需要在程序编译器处理的命令;
函数签名由函数名、参数及其排列顺序组成;
#define和const定义的常量有何不同:
(1)只有一个预处理器处理;
(2)只有一个指定了类型
#define没有指定数据类型,编译器也看不到他们
Const创建的常量有数据类型,将编译器处理,
声明::
4-5章属于c基础,不做介绍;
第四章 《表达式、语句、运算符》
第五章 《函数的调用》
Long set4DPoint (int X,int y,int z=1;int t;
以上错误,以为如果int t 这个参量没有默认值,那么它前面的z也不能有默认的参数
Long set4DPoint(int X,int y,int z=1,int t=20);
调用的时候:set4DPoint(10,15);>>>>>后面则是默认的参数
C++中可以有多个同名的函数,只要他们的参数不同即可;;
要满足参的可以有多个同名的函数,只要它们的参数不同即可
要满足参数的数据类型和参加的数码量不同或者两者兼而有之;
Int storc(int ,int)
Int store
Int store(long)
函数的重载也可以称作函数的多态:
int average(int ,int); long average(long,long); float average(float,float);
关键字inline:提示编译器,你希望将函数嵌入,
如果将函数声明为内联函数》》加关键字inline :inline int double(int);
注意事项: 1):递归函数不能定义为内联函数
2):内联函数一般适合于不存在while和switch等复杂函数结构中,并且只有1~5条语句的小函数上,否则编译器将该函数视为普通函数;
3):内联函数只能先定义后使用,否则编译器也会把他认为是普通函数;
4):不能进行异常的接口声明;
5)内联函数的功能和预处理宏的功能很相似》》》》》
函数调用要有一定的时间和空间方面的开销,于是影响其效率,然而宏只是在预处理的地方把宏展开,不需要额外的时间和空间方面的开销,效率也很高;
缺点是:不能访问私有的成员;而宏的定义很容易产生二义性;
内联函数和宏的区别在于:宏是预处理器对宏替换;然而内联函数是通过编译器控制来实现 的,它是真正的函数;只是在用到的时候,内联函数像宏一样展开,所以消除了函 数的参数压栈,减少了调用的开销;
inline tablefunction(int 1){return 1*1} 》》》内联函数
在类的内部定义的函数默认是内联函数;比普通函数的效率要高;
内联函数不能是virtual虚函数;
内联函数是一个静态行为,而虚函数是个动态的行为;
第六章 《控制程序流》
For(int x=0,y=0;x<10;x++,y++){std::cout<<x*y<<“\n”;}
如果要初始化计数变量,且每次循环迭代都检查并递增该变量,应该考虑使用for循环;如果变量已经初始化,或者无需每次循环迭代都递增它,while循环可能是更好的选择;
第七章 《使用数组和字符串存储信息》
Std::cin>>boy;
存在两个大问题》》》首先,如果用户输入的字符超过了缓冲区的长度,cin写入时跨国缓冲区边界,导致程序不能正确运行,还可能导致安全问题;其次是如果用户输入了空格,cin》》》将认为字符串就此结束,不在将接下来的内容写入缓冲区;
(1) 要填充的缓冲区 (2)最多读取多少个字符;
Std::cin.getline(boy,18); //最多提取18个字符(包括空字符)
Std::cin.getline(boy,18, ‘ ‘ ); //这条语句在遇到空格后停止读取输入,
对于只有24个元素的数组,如果写入第25个元素,结果如何:》》》》》》
这将写入其他变量占据的内容,给程序带来灾难,这可能覆盖程序使用的其他内容,导致程序不能正常运行,据安全专家说:恶意程序员利用最多的软件漏洞是,写入数据时超越缓冲区边界,并利用这种错误来执行新代码,新代码通常可执行任何操作,如修改或者删除文件、将系统特权授予不信任的用户以及复制病毒;
《第二部分 类》
第八章 《创建基本类》
C++类是一个模板,用于创建对象。定义类后,便可像使用其他类型那样使用根据它创建的对象。类是一系列捆绑在一起的变量和函数,其中的变量可以是任何其他类型包括其他类;变量构成了类的数据,而函数使用这些数据来执行任务,将变量和函数捆绑在一起成为封装;
类中的变量称为成员变量:成员变量也被称为数据成员或者实例变量,他们是类的组成部分;类中的函数使用和修改成员变量,他们被称为类的成员函数或者方法,与成员变量一样,成员函数也是类的组成部分,它决定了类的对象能做什么;
声明一个类: class Tricycle{ public:
unsigned int speed;unsigned int wheelsize;
pedal();break(); };
创建类对象》》》 Tricycle Wichita;
创建对象后。可使用句点运算符(.)来访问其成员函数和成员变量。如前面的》》要使用Tricycle类中名为speed的成员变量,要设置这个变量,可使用(.);
例如》》》》》 Tricycle Wichita;Wichita.speed=6; Wichita.pedal();
注意》》》》》在默认情况下所有的成员变量和成员函数都是私有的,私有成员只能在其所属类的函数内访问,而公有成员可在任何地方访问,
关键字》》》》》private(私有的)public(公有的)protecte(受保护的)
要使用除开公有的成员,就必须使用存取器(设置或获取私有成员变量值得函数)
class back{ public: int getSpeed(); void setSpeed(int speed); void pedal();
private: int speed;};
int back::getSpeed() { return speed;} //对私有成员变量的复值;
void back::setSpeed(int newspeed) { if (newspeed >=0) speed=newspeed;}
成员函数的定义 void back::pedal() {std::cout<<”你好!”<<end;}
int main()
{ back boy; boy.setSpeed(0); //c++支持重载
boy.pedal(); //成员函数的调用 }
成员函数的定义以类名打头,然后是作用域解析运算符(::)和函数名:
void back::pedal(){std::cout<<“pedaling trike\n”;}
构造函数:是一个特殊的成员函数,每次实例化对象时都将调用它。它的职责是创建一个有效的对象,这通常包括初始化成员数据。构造函数与类同名,且无返回值,构造函数可以接受参数,也可以不接受,就想其他函数一样;如back类的构造函数》》》》》
back ::back(int initialSpeed){ setSpeed(initialSpeed); }
上面的构造函数作用为设置speed的初始值;注意如果声明了一个构造函数,那么就得声明一个析构函数。它们的关系为》》》》》构造函数创建并且初始化对象,而析构函数则是执行清理工作并且释放分配给对象的内存,析构函数的名称总是由腭化符号(~)和类名组成。析构函数不接受任何参数,也没有返回值;back的析构函数
~ back::back(){ //do nothing } //析构函数无需执行特殊的操作来释放内
创建对象的时候有多种调用构造函数的方式;一种方式是在括号内指定一个或者多个参数》》》》》 back Wichita(5);
这些参数将传递给构造函数,上述示例设置了一个初始值,
也可以在创建对象的时候不指定参数:back Wichita();默认调用没有参数的构造函数
没有声明构造函数时,将由编译器自动创建一个默认构造函数(不执行任何操作);
注意!1>默认的构造函数是没有参数的构造函数,你可以自己定义,也可以让编译器提供;
2>只要你定义了构造函数(无论是否接受参数),编译器就不会为你提供默认构造函数,在这种情况下,如果需要默认构造函数,你必须自己定义;
如果你没有定义析构函数,编译器也会自己提供一个。其函数体也是空的,无操作;
只要定义了构造函数就必须定义一个析构函数,哪怕它什么也不执行;
例子》》》》》》 class back{ public: int getSpeed( ); void setSpeed(int speed); void pedal();
private: int speed;};
back ::back(int initialSpeed){ setSpeed (initialSpeed);}//构造函数
back ::~back(){//do nothing } //析构函数
int back::getSpeed() { return speed;} //对私有成员变量的复值;
void back::setSpeed(int newspeed) { if (newspeed >=0) speed=newspeed;}
void back::pedal() {std::cout<<”你好!”<<end;} //成员函数的定义
int main(){
back boy ; boy.setSpeed(0); //c++支持重载
boy.pedal( ); //成员函数的调用 }
总结:面向对象编程(oop)》》程序由一个或者多个对象组成,其中每个对象都有自己的数据(成员数据)和函数(成员函数),对象彼此分开,专门用于特定用途;
问题对象有多大?对象占据的内存量取决于其成员函数变量的长度;类函数不占据为对象分配的内容;有些编译器在内存中对齐变量,这将导致2字节变量实际占用的内存多余2字节。
第九章 《高级类》
如果使用了关键字const将成员函数声明为常量函数,则表明他不会修改任何类成员的值,要将函数声明为常量函数,可在括号后面添加关键字const:注意应该尽可能的将函数声明为常量函数,这是一种好的编程习惯。通常这样做,可以让编译器发现您对成员函数变量的无意修改, 避免这些错误出现在运行阶段;
const 修饰函数时,表示不能使用该函数来修改对象;
修饰对象时,表示对象中的成员不可以被修改;
修饰变量时,表示对象的值不可以被修改,只能通过构造函数的参数初始化表进行初始化操作;
可将常规函数声明为内联函数,也可以将成员函数声明为内联,为此,只需要在返回类型的前面加上关键字inline;或者直接将函数定义放在类声明中,这样函数将自动变成内联函数;
将其他类用作成员数据的类:
《第三部分 内存管理》
第十章 《创建指针》
变量是可存储一个值得对象:整型变量存储一个数字,字符变量存储一个字母,而指针是存储地址变量;
计算机内存是存储变量值得地方,计算机内存被划分成顺序编号的内存单元,每个内存单元都有地址,在内存中,每个变量都位于特定的地址处,而不管其类型如何。
声明指针: 》》》新增变量nullptr 表示空指针;
它用于存储地址。指针是一种特殊的变量,用于存储对象在内存中的地址;声明指针的时候,务必指定它将指向的变量类型,这告诉编译器如何处理指针指向的内存单元;
一定要明确指向》》》 int howold=550; int *pAge=nullptr; pAge=&howold;
unsigned short int howold=50; unsigned short int * pAge=&howold;
简介运算符(*) 》》》也被称为解除引用运算符
对指针引用的时候,将获取指针存储的地址处的值,如两个变量之间的赋值:
unsigned short int howold=55;unsigned short int* pAge=&howold;
unsigned short int yourAge; yourAge=*pAge;//存储在.......处的值;
注意:声明的时候:* >>表示这是指针,而不是普通的变量;
解引用的时候:* >>表示这是指针指向的内存单元中的值,而不是指针的地址本身;
指针、地址、变量:
区分哪个是指针,该指针存储的是那个变量的地址,该地址处的值是多少;
使用指针操作数据:
查看存储在指针中的地址:
为何使用指针:
1>管理堆中的数据;2>访问类的成员数据和成员函数;3>按引用将变量传递给函数;
通常处理需要5个内存区域;
>全局名称空间;------全局变量;
>寄存器; ------用于内部管理,如跟踪栈顶和指令指针;
>代码空间; ------代码
>栈; ------局部变量和函数参数
>堆;(自由存储空间) ------余下的内存都在堆中
局部变量:不会持久化函数返回,将丢失 ; 全局变量解决了这个问题(易Bug)
放在堆中能解决这种bug :必须询问预留的地址,将其存储到指针中,然后丢掉;
注意* 每当函数返回的时候,都会清理栈;然而只有当程序结束的时候才会清理堆,所以在使用完预留的内存后,要将其释放;让不需要的信息留在堆中成为内存泄漏
堆的优点:在显式释放前,你预留的内存始终可用。如果在函数中预留堆中的内存,在函数返回后,该内存任然可用;只有有权访问指针的函数才能访问它所指向的数据;这提供了控制严密的数据接口,消除了函数意外修改数据的问题;故就需要使用能够创建指向堆中内存区域的指针,------>>>>>> new 关键字
返回一个内存地址,必须将其赋给指针;如>>>>>>
unsigned short int * pPointer;
pPointer = new unsigned short int ;
*pPointer=72;//将72 赋值给pPointer指向的堆内存区域;
同:unsigned short int* pPointer=new unsigned short int;
如果不能从堆中分配内存(因为内存资源有限),将引发异常;
使用分配完的内存区域后,必须对指针调用delete,将内存归还给堆;指针本身为局部变量,这不同于它指向的内存;当声明指针的函数返回时,指针将不再在作用域中,因此被丢弃,然而,使用new运算符分配的内存不会自动释放,这些内存将不可用,这被称为内存泄漏,因此在程序结束前,内存不会归还给堆,就像内存从计算机中泄漏了;删除指针时,应将其设置为NULL,对空指针调用delete是安全的;若不NULL则两次调用将造成崩溃;
如何给一个变量分配堆内存》》》》
int * pHeap = new int ;
if(pHeap==nullptr){std::cout<<”erro !No!”;return 1;}
*pHeap=3; delete pHeap;
没有释放指针指向的内存就给它重新赋值;
指针为何如此重要?
因为它们用于存储堆中的对象地址以及按引用传递参数;
所有的对象都应存储到堆中吗?
堆中的对象在函数返回后仍然存在,另外,将对象存储在堆中的功能让您能够在运行阶段决定需要多少个对象,而不必预先声明!
第十一章 《开发高级指针》
定义了类型Cat之后:便可声明一个指向这种对象的指针,并在堆中实例化一个 Cat对象:
Cat *PCat=new Cat;
这将调用默认构造函数——》》不接受任何参数的构造函数,每将在堆或者栈中创建对象时,都将调用构造函数;
对指向堆中对象的指针调用delete时,将调用对象的构造函数,然后释放内存。这让类有机会执行清理工作;就像销毁栈中的对象一样;
对于在栈中创建的Cat对象,使用句点运算符(.)来访问其成员数据和成员函数;要访问堆中的Cat对象,必须对指针解除引用,并对指针指向的对象使用句点运算符。因此,要访问成员函数GetAge,>>>>>>>> (*pRags).GetAge( ); 等 价 *pRags ->GetAge( );
#include<iostream> class SimpleCat{ public:
SimpleCat() {itsAge=2;} ~SimpleCat(){} //构造函数 和析构函数
int GetAge() const { return itsAge;} //隐藏了一个this指针
void SetAge(int age){ itsAge = age;} //类中的带参函数》》内联函数
private: int itsAge; };
int main(){ SimpleCat *Frisky=new SimpleCat;
std::cout<<”Frisky is”<<Frisky->GetAge(); //2
Frisky->SetAge(5);
std::cout<<”Frisky is”<<Frisky->GetAge(); //5
delete Frisk;return0;}
类可能有一个或者多个数据成员指针,指向堆中的对象(内存)。可在构造函数或成员函数中分配内存,并在析构函数中释放内存;自己编写析构函数将不会造成内存泄漏;因为编译器只删除指向堆内存的指针,而不会删除堆中的内容;
#include<iostream> class SimpleCat{ public:
SimpleCat() ; ~SimpleCat(); //构造函数 和析构函数
int GetAge() const { return *itsAge;} //打印调用Get获取的意思
void SetAge(int age){ *itsAge = age;} //内联函数初始化*itsAge;
int GetWeight() const{return *itsWeight;}
void setWeight(int weight){ *itsWeight=weight;}
private: int *itsAge; int *itsWeight; };
SimpleCat::SimpleCat(){ itsAge=new int(2); itsWeight=new int(5);}
SimpleCat::~SimpleCat(){ delete itsAge; delete itsWeight; }//析构
int main(){ SimpleCat *Frisky=new SimpleCat;
std::cout<<”Frisky is”<<Frisky->GetAge(); //2
Frisky->SetAge(5);
std::cout<<”Frisky is”<<Frisky->GetAge(); //5
delete Frisky;return0;}
每个类成员函数都有一个隐藏的参数:this指针,它指向相应的对象。因此,每次调用GetAge()或者SetAge()时,都通过隐藏参数包含指向相应对象的this指针;this指针指向其函数被调用对象,通常不需要它,而只是调用函数并设置成员变量,但偶尔需要访问对象本身(可能旨在返回一个指向当前对象的指针),在这种情况下,this指针将很有用;
#include<iostream> class SimpleCat{ public:
SimpleCat() {itsAge=2;} ~SimpleCat(){} //构造函数 和析构函数
int GetAge() const { return this->itsAge;}
void SetAge(int age){ this->itsAge = age;} //内联函数初始化itsAge
private: int itsAge; };
int main(){ SimpleCat Frisky;
std::cout<<”Frisky is”<<Frisky.GetAge(); //2
Frisky.SetAge(5);
std::cout<<”Frisky is”<<Frisky.GetAge(); //5
delete Frisk;return0;}
导致Bug的罪魁祸首之一;对指针调用delete(释放它所指向的内存)后,如果没有重新赋值就是用它,将导致悬摆指针;注意对指针调用delete后千万不要使用它。该指针仍指向原来的内存区域,但是编译器可能在这里存储了其他的数据;使用该指针可能导致程序崩溃,故delete后一定要nullptr,解除武装!
const int *pOne; >>>>>>>>>不能修改它指向的值 *pOne=5; (X)
int const * pOne;
int * const pTwo; >>>>>>可修改指向的变量的值,指向不能更改;
const int * const pThree; >>>>>>>都不能修改
注意:将对象声明为常量时,实际上是将this声明为指向常量对象的指针,常量this指针只能用于调用常量成员函数;
类中的数据成员可以是指向堆中对象的指针,可在类的构造函数或其他函数中分配内存,并在析构函数中将其释放;
Java创始人认为对大多数任务,对不需要指针,可用其他方法得到相同的结果,他认为指针太麻烦了,就排除了!在C++中》》仅当需要直接使用硬件(如需要编写设备驱动程序)时,指针才有绝对的作用;
第十二章 《创建引用》
指针是存储另一个对象的地址的变量;而引用是对象的别名;
int someInt; int &rSomeRef=someInt; 操作rSomeRef就等于操作someInt;
注意:引用运算符和地址运算符是同一个符号 &,用于声明中;别忘了,声明指针时,星号(*)声明的变量是指针;在语句中与指针一起使用时,星号表示间接运算符;
也可以声明一个类:如现有类President Back_Obama; President &Obama= Back_Obama;
这里只有President一个类,对Obama执行的任何操作都将影响Back_Obama;
可引用任何对象,包括用户定义的对象。请注意,您创建的是指向对象的引用,而不是指向类或者数据类型的引用(如int)的对象。不能写成>>>> int &rone=int ;
可以像使用对象那样使用指向对象的引用:访问成员函数和成员数据时,使用类成员访问符(.),与内置类型的引用一样,指向对象的引用也是对象的别名;
指针未初始化或被删除时都应该赋值NULL;对于引用却不是,引用不能为空,让引用指向空对象的程序是非法的,可能正常运行或者删除硬盘重要的数据;
通过按引用传递对象,可让函数修改指向的对象;指针和引用语法不同效果一样;
swap(int * a,int * b);指针>>>地址 swap(int &a,int &b);引用>>>取别名
#include<iostream> enum ERR_CODE { SUCCESS,ERROR; };
ERR_CODE factor(int ,int &,int &);
int main(){
int number,squared,cubed; ERR_CODE result;
std::cout<<”enter a number (0-20)”<<endle;
std::cin>>number;
result = factor(number,squared,cubed);
if(result==SUCCESS) std::cout<<number<<”\n”<<squared<<”\n”<<cubed<<”\n”;
else std::cout<<”ERROR encountered”<<”\n”; return 0;}
ERR_CODE factor(int n,int &rSquared,int &rCubed){
if(n>20) return ERROR;
else { rSquared=n*n; rCubed=n*n*n;return SUCCESS;}}
总结:记住引用是给对象取别名;指针是存储对象的地址变量;
优点:引用更容易使用和理解,引用的间接关系被隐藏,无需不断解除引用;
缺点:引用不能为NULL;也不能重新赋值,指针提供了更大的灵活性,使用起来稍难;
在C++中默认以哪种方式将变量传递给函数;按值传递》》》》将变量的备份而不是变量本身传递给函数;这让函数无法修改原始值,为了避免按值传递,一种方式是使用指针,这将传递原始变量的地址;另一种方式是使用引用,这将传递原始变量的别名;
第十三章 《高级引用和指针》
按值传递的时候,传入函数时,返回时都要创建该对象的备份;(非常不可取)
在栈中用户创建的对象的大小为该成员变量的大小之和,而每个成员变量本身也可能是用户创建的对象;每次备份创建的类,编译器都将调用一个特殊的构造函数》复制构造函数;SimpleCat::SimpleCat(SimpleCat &);//复制构造函数结束后自动调用;
按引用传递避免了创建备份以及调用复制构造函数,构造函数和析构函数效果更好;
传递const指针》》》》》》const SimpleCat * const Function(const SimpleCat *const theCat);
使用const关键字避免了对象在按引用传递时候暴露在被修改的风险中。
经典的一句话按值传递类似与将作品的照片提供给博物馆,即使在照片上做记号也不会损坏原作;而按引用传递就好比让参观者到博物馆直接与作品接触;故要用const;
作为指针替代品的引用》》》》》 const SimpleCat &Function(const SimpleCat &theCat);
那么问题来了》》》什么情况下使用引用以及什么情况下使用指针呢?
如果需要依次指向不同的对象,就必须使用指针;
引用不能为nullptr,因此如果要指向的对象可能为NULL,就必须使用指针,而不能使用引用比如从堆中分配内存就必须用到指针;
#include<iostream>
class SimpleCat{ public: SimpleCat(int age,int weight); ~SimpleCat( ){ }
int GetAge( ) {return itsAge;} int GetWeight(){return itsWeight;}
private: int itsAge; int itsWeight; };
SimpleCat::SimpleCat(int age,int weight):itsAge(age),itsWeight(weight){}
SimpleCat &TheFunction();
SimpleCat &rCat=TheFunction();
int age=rCat.GetAge(); std::cout<<”rCat is”<<age<<”years old!\n”; return 0;
}
SimpleCat &TheFunction(){ SimpleCat Frisky(5,9); return Frisky; }
SimpleCat Frisky(5,9); >>>>>>局部变量对象Frisky已经销返回的引用指向了不存在的对象;
int main(){
SimpleCat &rCat=TheFunction();
int age=rCat.GetAge(); std::cout<<”rCat is”<<age<<”years old!\n”;
std::cout<<”&rCat”<<&rCat<<”\n”;
SimpleCat *PCat=&rCat; delete PCat;
return 0;
}
SimpleCat &TheFunction(){ SimpleCat *pFrisky=new SimpleCat(5,9); //堆中分配内存
std::cout<<”pFrisky:”<<pFrisky<<”\n”;
return Frisky; }
面对上表面的定时炸弹解决的办法》》》
原因是:不能对引用调用delete;故创建另一个指针并将其初始化为从rCat获得的地址,这确实能释放内存,避免了内存泄漏:但是rCat指向什么????引用必须始终是一个实际存在的对象的别名,如果它指向的是空对象,如上面则程序非法!!!!!
警告!!!!!如果程序包含指向空对象的引用,那么它可能能够通过编译,但是非法;
方案一:返回一个指针,指向new所分配的内存,这样在发出调用的函数内,可在使用完该指针后将其删除。为此,需要将TheFunction的返回值类型声明为指针而不是引用!并且返回指针,而不是对指针解除引用的结果;
SimpleCat *TheFunction(){
SimpleCat *pFrisky=new SimpleCat(5,9);
std::cout<<”pFrisky:”<<pFrisky<<”\n”; return pFrisky;}
方案二:在发出调用的函数中声明对象,然后将其按引用传递给TheFunction();优点在于》》》分配内存的函数(发出调用函数),也负责释放内存,这更可取如下所示》》》》》》》》》
程序在堆中分配内存的时候将返回一个指针,必须一直在某个指针指向这块内存,因为指针丢失后,将无法得到释放该内存,从而导致内存泄漏;
记住》》在函数之间传递内存块时:使用引用传递内存块中的值,而分配内存块的函数负责释放它;如果一个函数分配内存另一个函数来释放该内存是一件非常严重的事情》》》》》》!!!!
如果你需要分配内存块并且需要将其传递给调用它的函数,应该考虑修改接口:让发出调用的函数分配内存,然后按引用将其传递给被调用的函数,这样便将管理内存的职责赋予了释放它的函数!
总结!!!引用不能为空,也不能重新赋值,指针提供了更大的灵活性,但是使用起来要复杂一些;
为什么要求函数按值返回???
如果返回的是局部对象!必须按值返回,否则返回的引用将指向不存在的对象!
鉴于按引用返回的危险,为什么不总是按值返回?
按引用返回的效率要高得多,可以节省内存且程序的运行速度更快!!!
《第四部分 高级C++》
第十四章 《高级函数》
回忆:函数的重载>就是编写多个名称相同采纳数不同的函数;成员也是可以重载的;
class Rectangle{ public:Rectangle(int width,int height); ~Rectangle(){}
void drawShape()const;
void drawShape(int width,int height)const;
private: int width;int height;};
Rectangle::Rectangle(int newWidth,int newHeight){width=newWidth;height=newHeigt;}
void Rectangle::drawShape( ) const { drawShape( width, height ); }
void Rectangle::drawShape(int width,int height ) const{
for(int i=0;i<heigth;i++) { for(int j=0;j<width;j++){
std::cout<<”*”;}std::cout<<”\n”;} }
Rectangle box(30,5); std::cout<<”drawShape():\n”;
box.drawShape(); std::cou<<”\ndrawShape(40,2):\n”;
box.drawShape(40,2); return 0;}
使用默认参数值》》》》》
void drawShape(int aWidth,int aHeight,bool useCurrentValue=false)const;
调用的时候可以
int main( ) {
Rectangle box(30,5); std::cout<<”drawShape(0,0,true):\n”;
box.drawShape(0,0,true); std::cou<<”\ndrawShape(40,2):\n”;
box.drawShape(40,2); return 0;}//默认为false
初始化对象》》》》》
与成员函数一样,构造函数也可以重载。重载构造函数功能强大》》》
一个接受参数的构造函数;一个不接受参数;单数要注意的是不能重载析构函数,析构函数:名称为类名之前加(~),且不接受任何参数;
析构函数由初始化部分和函数体组成;可在初始化部分设置成员变量,也可以在构造函数的函数体内设置:如下所示初始化成员变量》》》》》
Tricycle::Tricycle:speed(5),wheelSize(12){//body of constructor}
警告!!由于不能给阴影和常量赋值,因此必须以这种方式给他们初始化;
复制构造函数》》》》》
除了默认构造函数和析构函数外,还有一种就是默认的复制构造函数,每当创建对象的备份时,都将调用复制构造函数;
按值将对象传入或者传出函数时,都将创建对象的一个临时备份。如果对象是用户自定义的,就将调用相应类的复制构造函数。所有的复制构造函数都接受一个参数:一个引用,它指向所属类的对象,最好将该引用声明为常量,因为复制构造函数不用修改传入的对象::Tricycle(const Tricycle &trike );
浅复制(成员复制):默认复制构造函数只将作为参数传入的对象的每个成员变量复制到新对象中;将一个对象的成员变量的值复制到另一个对象中,这导致两个对象中的指针指向相同的内存地址,另一方面:深复制就是讲堆内存中的值复制到新分配的堆内存中;
使用默认的复制构造函数将有可能导致程序崩溃:::
如果其中一个Tricycle对象没有在作用域内之后,调用该对象的析构函数将释
放分配的内存;然而另一个对象仍然指向该内存,如果尝试访问该内存,将崩溃!!
所以最好是自己定义复制构造函数,并在复制是正确地分配内存,通过创建深复制 的复制构造函数,将把现有的值复制到新内存中》》》》》》》
例子:
class Tricycle{ public: Tricycle( ); Tricycle( const Tricycle &); ~Tricycle( );
int getSpeed( ) const { return * speed;}
void setSpeed(int newSpeed) {*speed=newSpeed;}
void pedal( ); void brake( );
private: int *speed; };
Tricycle::Tricycle( ) { //默认构造函数speed赋值为5
speed =new int ; *speed=5; }
Tricycle::Tricycle(const Tricycle &rhs){
speed=new int ; *speed=rhs.getSpeed();} // 新开辟的speed
Tricycle::~Tricycle( ){
delete speed; speed =nullptr; }
void Tricycle::pedal(){
setSpeed(*speed +1); std::cout<<getSpeed( );
void Tricycle::brake(){
setSpeed(*speed -1 ); std::cout<<getSpeed( );
int main( ){
std::cout<<”Creating trike named wichita .......”;
Tricycle wichita; wichita.pedal(); //6
std::cout<<”Creating trike named dallas..........”;
Tricycle dallas(wichita);
std::cout<<wichita.getSpeed()<<”\n”; //6
std::cout<<dallas.getSpeed()<<”\n”; //6
std::cout<<”Setting wichita to 10.....”;
wichita.setSpeed(10);
std::cout<<wichita.getSpeed( )<<”\n”; //10
std::cout<<dallas.getSpeed( )<<”\n”; //6
return 0; }
Tricycle::Tricycle(const Tricycle &rhs){
speed=new int ; *speed=rhs.getSpeed();} // 新开辟的speed
这就说明了将现有Tricycle对象指向的值,复制到新Tricycle对象分配的内存中;
这就是深复制
问题:::既然可以重载函数,为什么还使用默认参数值?
因为维护一个函数比维护两个函数更容易,并且理解带默认参数的函数比研究两个函数的函数体更容易,另外更新一个重载版本而忽略另一个是导致bug的主要原因;
::鉴于重载存在的问题,为何不总是使用默认参数呢?
重载提供了使用默认参数无法提供的功能,如改变参数类型,而不仅仅是改变参数数量;
::编写构造函数的时候,如何确定在初始化部分设置那些成员变量以及在函数体内设置那些变量?
尽可能的在初始化部分初始化所有成员变量,有些代码(std::cout语句和计算)必须放在构造函数的函数体内;
可以;但是没有理由不能结合使用这两种方法。可以有一个或者多个重载函数有默认参数,规则与在常规函数中使用默认参数相同;
第十五章 《运算符重载》
几乎所有的c++运算符都可以重载;
要在类中重载运算符,最常见的方式是使用成员函数,这种成员函数的声明类似于:
返回类型 + 运算符(参数 名, ....) { 实现功能 }
returnType operatorsymbol ( paeameter list) {//body .......function }
class Counter{ public: Counter( );~Counter( ){ }
int getValue( ) const { return value;}
void setValue(int x) { value =x ;}
void increment( ){ ++value;}
const Counter & operator ++( );
private:
int value; };
Counter : : Counter( ) : value(0) { }
const Counter & Counter : : operator++( ){
++value; return *this; }
int main( ){
Counter c;
std::cout<<”the value c is:”<<c.getValue()<<”n”;
c.increment( ); //自加一次
std::cout<<”the value c is:”<<c.getValue()<<”n”;
++c;
std::cout<<”the value c is:”<<c.getValue()<<”n”;
Counter a=++c;
std::cout<<”the value a is:”<<a.getValue()<<”n”;
std::cout<<”and c is : ”<<c.getValue()<<”n”; return 0;}
给成员函数operator++( )添加一个int的参数;虽然在函数体内,不会使用这个参数,他只是用于表明该函数定义的是后缀运算符;前缀运算符导致先修改变量的值之后在表达式中使用这个值;而后缀运算符则是先返回变量的值,然后才对变量递增或者递减;后缀运算符要求使用原始值,而不是递增后的值;
必须按值(而不是引用)返回该临时对象;否则函数返回时,它将不再作用域内;
const Counter Counter : : operator++( int ){
Counter temp(*this);
++value; return temp; }
递增递减运算符是单目运算,所以只需要一个操作符,加法运算符(+)双目运算,将两个操作数相加; Counter var1,var2,var3;var3=var2+var1;
class Counter{ public: Counter( ); ~Counter( ){ }
Counter ( int initialValue);
int getValue( ) const { return value;}
void setValue(int x) { value =x ;}
Counter operator +( const Counter&);
private: int value; };
Counter : : Counter(int initialValue ) : value(initalValue) { } //带参默认构造函数
Counter : : Counter( ) : value(0) { }//默认构造函数
Counter Counter : : operator+( const Counter &rhs){
return Counter ( value + rhs.getValue( ) ); }
int main( ){
Counter alpha(4),beta(13),gamma;
std::cout<<”alpha:”<<alpha.getValue()<<”n”;
std::cout<<” beta:”<< beta.getValue()<<”n”;
std::cout<<” gamma:”<< gamma.getValue()<<”n”;
return 0;}
不能重载用于内置类型的运算符;不能改变运算符的优先级和数目(单目运算双目运算或则三目);另外不能创建新运算符,因此不能将**声明为指数(乘方)运算符;
c++编译器给每个类都提供了默认的构造函数、析构函数和复制构造函数;编译器提供的第4个成员函数是赋值运算符————
重载赋值运算符时,首先需要做的是:使用类似以下语句释放内存》》delete speed;
class Tricycle{ public: Tricycle();
int getSpeed( ) const {return * speed;}
void setSpeed(int newSpeed) {*speed =newSpeed;}
Tricycle operator=(const Tricycle&);
private: int * speed; };
Tricycle::Tricycle(){ speed =new int; *speed =5;}
Tricycle Tricycle::operator=(const Tricycle & rhs ){
if (this==&rhs) return *this;
delete speed;
speed=new int; *speed=rhs.getSpeed(); return *this;}
int main(){
Tricycle wichita;
std::cout<<”wichita:”<< wichita.getSpeed()<<”n”;
wichita.setSpeed(6);
Tricycle dallas;
std::cout<<”dallas:”<< dallas.getSpeed()<<”n”;
wichita=dallas;
std::cout<<”dallas:”<< dallas.getSpeed()<<”n”;
return 0;}
注意!!释放了成员变量speed指向的内存,然后重新在堆中给他分配内存。虽然这不是绝对必要的,但是使用未重载赋值运算符的变长对象时,应避免内存泄漏;
如果试图将一个内置类型(如int或者unsigned short)变量复制给一个用户定义的类对象::将会出错》》》》编译不会通过:
#include<iostream>
class Counter{public: Counter(); ~Counter(){}
Counter(int newValue);
int getValue() const{ return value;}
void setValue(int newValue){ value=newVlaue;}
operator unsigned int( );
private: int value; };
Counter:: Counter( ):value(0){ }
Counter::Counter(int newValue):value(newValue){ }
Counter::operator unsigned int(){
return (value);}
int main( ){ int beta=5;
Counter alpha=beta;
Counter epsilon(20);
int zeta=epsilon;
std::cout<<”alpha is :”<<alpha.getValue();
std::cout<<”zeta is :”<<zeta.getValue(); return 0;}
总结:
既然创建成员函数来实现同样的功能,为何需要重载运算符?
如果重载运算符的行为是大家熟悉的,那么使用起来将容易。另外完成同样的任务所需要的代码更少,而类可模拟内置类型的功能;
复制构造函数使用现有的对象创建新的对象;而赋值运算符修改现有的对象,使其值与另一个对象相同;
转换运算符的用途:将对象转换为内置类型》
《第五部分 继承和多态》
第十六章 《使用继承扩展类》
C++将类定义为从另一个派生类而来。派生是一种表示的是“是一种”关系的方式;可从类Mammal派生出新类Dog,在这种情况下,无需显式地指出狗能走动,因为他从Mammal哪里继承了这种功能。Dog是从Mammal派生而来的,它自动就获取了走动的功能;
如果一个类在现有的类的基础上添加了新的功能,那么这个类就被称为从原来的派生类而来,而原来的类被称为基类;
如果Dog类是从Mammal类派生而来的,那么Mammal就是Dog的基类;派生类是基类的超集。就像狗在哺乳动物的基础上添加了新特征一样;Dog类也在Mammal类的基础上添加了新的方法或数据;
基类可以有多个派生类。就像狗猫马都是哺乳动物一样,它们对应的类也是从Mammal来派生而来的;
要从一个类派生出另一个类,可在声明中的类名后面加上冒号,在指定类的访问控制(public、protected、或private)以及基类:class Dog :public Mammal
例子:#include<iostream> enum BREED {YORKIE,CAIRN,DANDIE,SHETLAND,DOBERMAN,LAB};
class Mammal { public: Mammal( ); ~Mammal( );
int getAge( ) const; void setAge(int);
int getWeight ( ) const; void setWeight( );
void speak( ); void sleep( );
protected: int age; int weight;};
class Dog : public Mammal { public : Dog ( ); ~Dog( );
BREED getBreed( ) const;
void setBreed( BREED );
protected: BREED breed;};
int main( ){ return 0; }
派生类不能访问私有成员!如果您希望数据对当前类及其派生类来说可见,为此便可以使用protected。受保护的数据成员和函数对派生类来说是可见的,但其他方面与私有成员完全相同。
对于三个限定符:public、private和protected。只要有类对象,函数就能访问该类的所有成员数据和成员函数;然而,成员函数可以访问基类的所有私有数据和函数。
enum BREED {YORKIE,CAIRN,DANDIE,SHETLAND,DOBERMAN,LAB};
class Mammal{ public:Mammal( ):age(2),weight(5){} ~Mammal( ){ };
//accessors 数据的获取与设置
int getAge() const {return age;}
void setAge(int newAge){age=newAge;}
int getWeight()const{return weight;}
void setWeight(int newWeight){weight=newWeight;}
//other methods
void speak()const{std::cout<<”Mammal sound!\n”;}
void sleep()const{std::cout<<”Shh.I’m sleeping.\n”;}
protected: int age;int weight;
};
class Dog : public Mammal
{ public: Dog():breed(YORKIE){ } ~Dog() { }
BREED getBreed( ) const {return breed;}
void setBreed(BREED newBreed){breed=newBreed;}
void wagTail(){std::cout<<”hello world!..\n”;}
void begForFood(){std::cout<<”begFor... ...\n”;}
private: BREED breed; };
int main( ){ Dog fido; //具有Mammal和Dog的所有特征
fido.speak( );
fido.wagTail( );
std::cout<<”Fidois“<<fido.getAge()<<”years old\n”;
return 0;}
对于C++的继承,要明白创建一个派生类的对象时,将调用多个构造函数。
如上面的程序当派生类的对象是基类时,在派生类中创建fido时,将首先调用基类构造函数创建一个Mammal对象,然后调用Dog类的构造函数完成对象的创建,由于创建的fido没有指定任何参数,因此在这两个阶段都将调用默认构造函数。
在销毁fido对象的时候,将首先调用Dog类的析构函数,然后调用Mammal类的析构函数,这让每个函数都有机会清理对象的相应部分。构造函数按继承顺序调用,而析构函数按相反的顺序调用;
您可能想重载Mammal和Dog的构造函数,使其分别设置age和breed。如果将参数age和weight传递给Mammal的构造函数;要在派生类的初始化阶段进行基类初始化,可指定基类名称,并在后面跟基类构造函数需要的参数,如上面的
Mammal基类中的age和weight要在派生类中初始化:
Dog( );
Dog(int age);
Dog(int age,int weight);
Dog(int age,int weight,BREED breed);
Dog::Dog():Mammal(),breed(YORKIE){std::cout<<”hello,,,\n”;}
Dog::Dog(int age):Mammal(age),breed(YORKIE){std::cout<<”hello,,,\n”;}
。。。。。。。。。。。
Dog这个派生类不但可以访问基类的所有成员,还可以访问自身的任何函数;另外还可以重写基类函数,重写基类函数意味着在派生类中修改基类函数的实现。创建派生类的对象时,将调用正确的函数;
如果派生类创建了一个返回类型和签名都与基类成员函数相同的函数,但是提供了新的实现,就叫做重写该函数。
重写函数时返回类型和签名必须与基类函数相同。签名是指除了返回类型外的函数原型,这包括函数名,参数列表和关键字const(若使用)。函数的签名由其名称以及参数的数量和类型组成,但是不包括返回类型。
如》》》》》》
enum BREED {YORKIE,CAIRN,DANDIE,SHETLAND,DOBERMAN,LAB};
class Mammal{ public: Mammal( ){std::cout<<”Mammal constructor...\n”;}
~Mammal( ){std::cout<<”Mammal destructor...\n”;};
//other methods
void speak()const{std::cout<<”Mammal sound!\n”;}
void sleep()const{std::cout<<”Shh.I’m sleeping.\n”;}
protected: int age;int weight;
};
class Dog : public Mammal
{ public: Dog( ){std::cout<<”Dog constructor...\n”;}
~Dog( ){std::cout<<”Dog destructor...\n”;};
void wagTail(){std::cout<<”hello world!..\n”;}
void begForFood(){std::cout<<”begFor... ...\n”;}
void speak() const {std::cout<<”Woof... ...\n”;}
private: BREED breed; };
int main( ){
Mammal bigAnimal;// 重写基类
Dog fido;
bigAnimal.speak();
fido.speak();
return 0;}
Mammal constructor...
Dog constructor...
Mammal sound!
Woof... ...
Dog destructor...
Mammal destructor...
Mammal destructor...
本语重载和重写相似,C++中的功能也相似。重载成员函数时,创建了多个名称相同但是签名不同的函数;而重写成员函数时,在派生类中创建了一个名称和签名都与基类函数相同的函数。
在上一个函数的例子中,Dog类的成员函数speak( )隐藏了基类的同名函数,这正是您希望的;这样可能导致意外的结果。比如Mammal有一个重载了的move( )方法,而Dog重写了该函数,那么Dog的move( )方法将隐藏Mammal的所有同名函数;
同样如果Mammal中有三个move( )重载版本,一个不接受任何参数,一个接受一个整型参数,还有一个接受整型的方向参数,而Dog只重写了不接受任何参数版本,那么使用Dog对象将难以访问其他的两个版本。
重写基类时如果忘记了原来的关键字const,就将隐藏它,这是一种错误!
注意* 只要你提供了构造函数,编译器将不再提供默认构造函数;虽然可以在派生类中隐藏基类的成员函数,但是这种隐藏通常是错误导致的,这就是发出警告的原因!
class Mammal{public:
void move()const{std::cout<<”Mammal move...”;}
void move(int distance) const{
std::cout<<”distance”<<distance;}
protected:
int age; int weight; };
class Dog::public Mammal{public:
void move( ) const {std::cout<<”Dog moves 5 steps”;}
int main( ) {
Mammal bigAnimal;
Dog fido;
bigAnimal.move( );
bigAnimal.move(2);
fido.move( );
//fido.move(10);
return 0; }
即使重写了基类的方法,仍然可以使用全限定名来调用它。可指定基类名、冒号和方法名: Mammal::move( );
总结! 继承的数据和函数会传递给后代吗?如果Dog是从Mammal派生而来,而Mammal是从Animal派生而来!那么Dog能否继承Animal的函数和数据?
答:会!连续派生时,派生类将继承其所有基类的函数和数据;
在派生类中,可将公有的基类函数改为私有的吗?
答:可以!在接下来的派生中,它始终保持为私有的!
为何在派生类中隐藏基类的成员函数?
答:已禁止使用它。有时候派生类与基类的行为差别很大,导致有些基类成员函数不合适。鉴于并非总是可以修改基类。
创建派生类对象时,将以继承顺序调用构造函数!
第十七章 《使用多态和派生类》
多态让你能够将派生对象视为基类对象,例如:假设你创建了从Mammal派生出的Dog、Cat、Horse等类,而Mammal类包含很多适用于这些派生类的成员函数,其中之一就是speak( ),它能实现所有哺乳动物都能发出声音的功能;
你如果想让每个派生类都发出特殊的声音,如猫叫、犬吠..... 每个派生类都必须能够重写speak( )方法的实现;
与此同时,如果有一系列Mammal对象,如有Dog、Cat、Horse和Cow对象的农场,你希望让这些对象发出声音,而无需知道或者关心它们的speak( )实现。将这些对象都视为哺乳动物而调用方法Mammal.speak( )时,便使用了多态。
多态是个不寻常的单词,意味着具有多种形态。它起源于poly和morph,前者表示多,而后者表示形态;
你将使用Mammal的多种形态:使用多态需要声明一个Mammal指针,并将在堆中创建的Dog对象的地址赋给它(基类)。由于Dog是一个Mammal,因此下面的代码完全合法; Mammal * pMammal=new Dog;
然后可以使用该指针对Mammal调用任何成员。你希望调用重写了的函数时,将根据指针指向的对象类型,调用正确的函数;虚成员函数让你能够实现这个目标。为实现多态,你使用Mammal指针来调用方法,不用知道也不关心该指针指向的是那种对象以及该对象的方法是如何实现的!下例就是用虚函数来实现多态》》》》》
Mammal( ):age(1) {std::cout<<”Mammal constructor....\n”;}
~Mammal( ) { std::cout <<”Mammal destructor ... \n”;}
void move( ) const {std::cout<<”Mammal move one step\n”;}
virtual void speak( ) const{std::cout<<”Mammal speak!\n”;}
protected: int age;
};
class Dog::public Mammal {public:
Dog( ){std::cout<<”Dog constructor...\n”;}
~Dog( ){std::cout<<”Dog destructor....\n”;}
void wagTail( ){std::cout<<”Wagging tail ...\n”;}
void speak( ) const{std::cout<<”woof ...!\n”;}
void move( ) const{std::cout<<”Dog move Two step !\n”;} };
int main( ){
Mammal *pDog =new Dog;
pDog->move( ); //pDog是一个基类的指针,在基类中查找任何函数合法
pDog->speak( ); //由于虚函数的存在,因此调用Dog重写后的函数;
return 0;}
更好地诠释virtual》》》》》
Mammal( ):age(1) { }
~Mammal( ) { }
virtual void speak( ) const{std::cout<<”Mammal speak!\n”;}
protected: int age;
};
class Dog::public Mammal {public:
void speak( ) const{std::cout<<”Dog ...!\n”;} };
class Cat::public Mammal {public:
void speak( ) const{std::cout<<”Cat ...!\n”;} };
class Horse::public Mammal {public:
void speak( ) const{std::cout<<”Horse ...!\n”;} };
class Pig::public Mammal {public:
void speak( ) const{std::cout<<”Pig ...!\n”;} };
int main( ) {
Mammal * array[5];
Mammal * ptr;
int choice,i;
for(i=0;i<5;i++) {
std::cout<<”(1)Dog (2)Cat (3)horse (4)pig:”;
std::cin>>choice;
switch(choice) {
case 1: ptr=new Dog; break;
case 2: ptr=new Cat; break;
case 3: ptr=new Horse; break;
case 4: ptr=new Pig; break;
default: ptr=new Mammal;break; }
array[i]=ptr; }
for(i=0;i<5;i++) array[i]->speak( );
return 0;}
注意!!!在编译阶段,无法知道将创建什么类型的对象!因此无法知道将调用那个speak()方法;ptr指向的对象是在运行阶段确定的,这被称为后期绑定或运行阶段绑定!与此相对的是静态绑定或者编译阶段绑定!
创建派生对象(如Dog对象)时,首先调用基类的构造函数,然后调用派生类的构造函数。注意基类部分和派生类部分在内存中是相邻的!
在类中创建虚函数后!这个对象必须跟踪它。很多编译器
创建虚函数表(V-table)。每个类都有一个虚函数表,而每个对象都有一个
指向虚函数表的指针(V-ptr或V-pointer)。
虽然实现方式不同,但是所有编译器都必须完成这项工作!如果在基类表明某个函数被定义为virtual(虚函数)后!使用基类指针时,将根据基类指针指向的对象的实际类型指向正确的函数!这样调用speak( )时,将调用正确的函数!
如果Dog有成员函数WagTail( ),但是Mammal没有!就不能通过Mammal指针访问它(除非将其转换为派生类指针 )。由于WagTail( )不是虚函数,且基类没有它,因此如果没有Dog对象或者Dog指针,就不能访问它;
虽然可以将基类指针转换为派生类指针,但是通常有更好、更安全的调用WagTail( )的方法。C++不赞成强制类型转换!因为这样容易出错。18章24章有介绍!
仅当通过指针和引用进行调用时,才能发挥虚函数的魔力;按值传递对象时不能发挥虚函数的魔力!如下所示》》
class Mammal{public: Mammal:age(1) { }
~Mammal( ) { }
virtual void speak const{std::cout<<”Mammal speak!\n”;}
protected: int age; };
class Dog:public Mammal{
public: void speak const{std::cout<<”Dog speak!\n”;} };
class Cat:public Mammal{
public: void speak const{std::cout<<”Cat speak!\n”;} };
void valueFunction(Mammal);
void ptrFunction(Mammal *);
void refFunction(Mammal &);
int main( ){
Mammal * ptr=0;
int choice;
while( 1) {
bool fQuit=false;
std::cout<<”(1)dog(2)cat(0)quit:”;
std::cin>>choice;
switch(choice) {
case 0: fQuit=true; break;
case 1:ptr=new Dog; break;
case 2:ptr=new Cat;break;
default:ptr=new Mammal; break;}
if(fQuit) break;
ptrFunction(ptr);
refFunction(*ptr);
valueFunction(*ptr);
}
return 0; }
void valueFunction(Mammal mammalvalue){
mammalvalue.speak( ); }
void ptrFunction(Mammal *pMammal){
pMammal->speak( ); }
void refFunction(Mammal &Mammal ){
rMammal.speak( ); }
对于valueFunction( );按值传递解除指针引用。函数希望接受一
Mammal对象, 因此编译器将Dog对象切除到只余下Mammal部分。这将调用Mammal ->speak( )方法得到:Mammal speak!
从存储空间角度,虚函数对应一个指向v-table虚函数表的指针,这大家都知道,可是这个指向vtable的指针其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
从使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
17.2.3.2虚析构函数》》》》》
在需要积累指针的地方使用指向派生类对象的指针是一种合法和常见的做法。当指向派生类对象的指针被删除时将发生什么情况?(回收资源的)如果析构函数是virtual,将执行:调用派生类的析构函数。由于派生类的析构函数会自动调用基类的析构函数,因此整个对象将被正确的销毁;
类中任何一个函数时虚函数,那么析构函数也应该是虚函数!!
如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
构造函数不能使虚函数,然而有时候程序非常需要通过传递一个指向基类对象的指针,创建派生类对象的备份。解决方案一:在基类中创建一个clone( )成员函数,并将其设置为虚函数。clone( )函数创建当前对象的备份,并返回该对象!由于每个派生类都重写clone( ) 方法,因此使其创建派生类对象的备份。
使用虚成员函数的类就必须维护一个v-table,因此使用虚函数会带来一些开销。如果类很小,不打算从它派生出其他类,就根本不需要用到虚函数;
将任何函数都声明为虚函数之后,便付出了V-table的大部分代价(每增加一个表项都会增加一些内存开销);故应将析构函数声明为虚函数!并假设其他所有函数也可能是虚函数。
总结!!!为什么不将类函数都声明为虚函数?
创建V-table表的开销伴随第一个虚函数的创建而发生。在此之后,创建其他虚函数带来的开销将很小!如果一个函数为虚函数!那么其他所有函数也应该为虚函数!又得程序员又不认为是这样!
如果一个基类的一个函数是虚函数,且被重载以便能接受一个或者两个int参数!而派生类重写了接受一个int参数的版本!那么通过指向派生类对象的指针调用接受两个int参数的函数时,将调用那个函数?
重写接受一个int参数的版本将隐藏基类中所有同名函数,因此将出现变异错误!指出该函数只接受一个int参数。
在运行阶段将指针绑定到对象成为什么?
晚期绑定发生在运行阶段;而静态绑定发生在编译阶段!
第十八章 《使用高级多态》
如果基类有成员函数speak( ),并在派生类中重写了它,将派生类对象赋给基类指针时,可使用该指针来完成正确的工作;
class Mammal{public:Mammal( ):age(1){ std::cout<<”Mammal constructor...”;}
virtual ~Mammal( ) { std::cout<<”Mammal speak!.”;}
virtual void speal( ) const{std::cout<<”Mammal speak”;}
protected: int age; };
class Dog : public Mammal {public: Dog( ){ std::cout<<”Dog constructor....\n”;}
~Dog( ){std::cout<<”Dog destructor....\n”;}
void speak( ) const{std::cout<<”Dog speak..”;} };
int main( ) { Mammal *pDog=new Dog;
pDog->speak( ); return 0;}
声明一个指针变量为基类指针,将派生类对象的地址赋给了这个指针变量后;
注意在派生类中添加了一个不适合基类的成员函数,如果使用基类指针调用这个成员函数将导致错误!虽然可以解决这个问题:把派生类中特有的函数放在基类中是最糟糕的做法!导致代码难以维护!
一般而言将派生类对象赋给基类指针是为了一多态的方式使用它,所以你不能试图访问派生类特有的函数!
解决方案就是将基类指针转换为派生类指针,告诉编译器,我知道这是一个派生类对象,请按我的要求去做!运算符dynamic_cast<派生类名 *>,以确保转换安全!
int main( ) { const int numberMammals=3;
Mammal *zoo[numberMammals];
Mammal *pMammal; int choice ,i;
for(i=0;i<numberMammas;i++) {
std::cout<<”(1)Dog(2)Cat:”;
std::cin>>choice; if(choice==1) pMammal=new Dog;
else pMammal=new Cat;
zoo[i]=pMammal; }
std::cout<<”\n”;
for(i=0;i<numberMammals;i++) {
zoo[i]->speak( );
Cat *pRealCat=dynamic_cast<Cat *>(zoo[i]);
if (pRealCat) pRealCat->purr( );
else std::cout<<”Uh oh,not a cat!\n”;
delete zoo[i];
std::cout<<”\n”; }
return 0; }
当指向派生类对象的指针被删除时将发生什么情况?(回收资源的)如果析构函数是virtual,将执行:调用派生类的析构函数。由于派生类的析构函数会自动调用基类的析构函数,因此整个对象将被正确的销毁;
经常需要创建层次结构!如可能创建Shape类,然后将其作为基类,派生出Rectangle和Circle。从Rectangle类又可能派生出Square类,将其作为Rectangle的特例;
每个派生类都将重写draw( )、getArea( )方法等。Circle从Shape(基类)派生而来,并重写了基类的虚成员函数,就没有理由在使用virtual关键字。因为这是其继承性的一部分;如果要写也没有坏处!如逐层继承!
C++通过提供纯虚函数来支持创建抽象数据类型!纯虚函数是必须在派生类中重写的虚函数。通过将虚函数初始化为0来将其声明为纯虚的!virtual void draw()=0;
任何包含一个或多个纯虚函数的类都是DAT,不能对其实例化!试图这样做将导致编译器错误。将纯虚函数放在类中向其客户指出了以下两点:
(1)不要创建这个类的对象,而应该从其派生。
(2)务必重写这个类继承的纯虚函数。
在从ADT派生而来的类中,继承的纯虚函数仍是纯虚的,要实例化这种类的对象,必须重写每个纯虚函数。因此,如果Rectangle从Shape派生而来,而Shape有三个纯虚函数,那么Rectangle必须重写这三个纯虚函数,否则它也将是ADT。
要将虚函数声明为抽象的,可在函数声明后面添加=0;virtual long getArea=0;
下面重写了Shape类,使其成为抽象数据类型:
class Shape{ public: Shape(){ }
virtual ~Shape(){ }
virtual long getArea()=0;
virtual long getPerim()=0;
virtual void draw()=0;
private: };
通常不实现抽象基类的纯虚函数。由于不能创建抽象的对象,因此没有理由提供实现,另外,ADT只用作其派生而来的类的接口定义。
然而可以给纯虚函数提供实现。这样,就可以通过从ADT派生而来的对象调用该函数,该函数可能旨在给所有重写函数提供通用功能。
下面的程序Shape2将Shape声明为ADT,并提供了纯虚函数draw( )的实现。Circle类重写了draw( )方法(必须这样做),并调用了基类的draw( )函数以提供额外的功能。
#include<iostream>
class Shape{ public: Shape(){ }
virtual ~Shape(){ }
virtual long getArea( )=0;
virtual long getPerim( )=0;
virtual void draw( )=0;
private: };
void Shape:: draw( ){
std::cout<<”Abstract drawing mechanism!”;}
class Circle : public Shape {
public: Circle(int newRadius):radius(newRadius) { }
~Circle ( ) { }
long getArea{ return 3 * radius * radius;}
long getPerim( ){return 6 * radius ;}
void draw( );
private:
int radius; int circumference; };
void Circle::draw( ) {
std::cout<<”Circle drawing routine here!\n”;
Shape::draw( );}
class Rectangle :public Shape{ public:
Rectangle(int newLen,int newWidth):
length(newLen),width(newWidth){ }
virtual ~Retangle( ){ }
long getArea( ) {return length *width;}
long getPerim( ){return 2*(length +width);}
virtual int getLength( ){return length;}
virtual int getWidth( ){return Width;}
void draw( );
private: int width;int length; };
void Rectangle::draw( ) {
for(int i=0;i<length;i++){
for(int j=0;j<width;j++)
std::cout<<”*”;
std::cout<<”\n”;}
Shape::draw( ); }
class Square : public Rectangle { public:
Square(int len);
Square(int len,int width);
~Square( ){ }
long getPerim( ){ return 4* getLength( );} };
Square : : Square(int newlen) : Rectangle( newlen,newlen){ }
Square : : Square(int newlen,int newWidth) :
Rectangle( newlen,newWidth){
if(getLength( )!=getWidth( ))
std::cout<<”Error ,not square...\n”; }
int main( ){
int choice;
bool fQuit =false;
Shape *sp;
while( 1) {
std::cout<<”(1)Circle(2)Rectangle(3)Square (0)Quit:”;
std::cin>>choice;
switch(choice){
case 1:sp=new Circle(5); break;
case 2:sp=new Rectangle(4,6); break;
case 3:sp=new Square(5); break;
default: fQuit=true; break; }
if (fQuit) break;
sp->draw( ); std::cout<<”\n”;
} return 0;}
基类中只要有一个函数被声明为纯虚函数,这个类就是ADT;
有时候,会从ADT派生出其他ADT。你可能想将一些继承而来的纯虚函数变成非纯虚函数,而保留其他的不变。
在从ADT派生而来的类中,继承的纯虚函数仍是纯虚的,要实例化这种类的对象,必须重写每个纯虚函数。试图实例化没有重写的派生类将导致编译错误,因为他们都是抽象的数据类型!
在一个程序中,基类可能是抽象的,而在另一个程序中可能不是!什么因素决定应将类声明为抽象的呢?
如要编写一个描述农场或动物园的程序,可能将基类声明为抽象类!但希望能实例化某个派生类对象;;另一方面,如果要描述各种派生类中的具体的类别,可能将派生类也声明为抽象类,且只实例化派生类中某个具体的类型。抽象层次取决于需要如何细分类型!
总结! 向上提升功能是什么意思?
答:这指的是将共享功能向上提升,将其放到基类中。对于多个类都需要的函数,最好将其放在一个合适的类中。
向上提升功能是否总是好事儿?
如果提升的是共享功能,就是好事儿;如果提升的是接口,就是坏事儿。对于并非所有派生类都使用的成员函数,将其移到基类就是错误,如果这样做,就必须检查对象的运行阶段类型,以判断能否调用该函数。
为什么动态强制类型转换时糟糕的?
使用虚函数旨在让虚函数表(而不是程序员)来判断对象的运行阶段类型;
为什么要创建抽象数据类型?为何不将其声明为非抽象的,并避免创建这种类型的对象?
C++的很多规则都旨在让编译器帮助查找bug,以避免出现运行阶段bug。通过在类中包含纯虚函数,让类变成抽象的,编译器将把创建这类对象的代码视为错误。这还意味着你可以与其他应用程序或程序员共享抽象数据类型。
第十九章 《使用链表存储信息》
数组可以看成是一个容器它的大小是固定的;链表是一种数据结构,由连接在一起的小容器组成。在这里容器是类,它们包含要存储在链表中的对象。就是编写一个存储数据对象的类(如Cat或Rectangle),他能够指向链表中的下一个容器,您为需要存储的每个对象创建一个容器,并将他们连接起来。
这些容器有节点。链表中的第一个节点称为头结点,最后一个节点称为尾节点。
链表有:单链表;双链表;树;
在单链表中,每个节点都指向下一个节点,但不能指向前一个节点。要查找特定的节点,从链表头开始,逐个节点往下找。双链表让你能够向前向后移动到下一个节点和前一个节点。树是一种由节点组成的复杂数据结构,每个节点都可能指向多个节点
注意每个类只负责自己的任务,但协同工作造就了一个能正常运行的程序!
链表的组成:节点类本身是一个抽象类,将使用3个子类来完成工作。链表包含一个头结点和一个尾节点,他们负责管理链表的组成部分,还包含零或多个内部节点。内部节点用于记录存储在链表中的实际数据。
注意数据和链表是一个不同的概念。可在链表中存储任何类型的数据,连接在一起的不是数据而是存储数据的节点。程序并不知道节点,它只是使用链表。链表完成的工作很少,它将工作委托给节点。
在面向对象编程中,赋予每个对象已明确而有限的职责。链表负责维护头结点,而HeadNode将新数据传递给它当前指向的节点,而不考虑它指向的是那个节点 。
每当收到数据后,TailNode都创建一个新节点并将其插入到链表中。TailNode只知道一点:只要有数据传来,便将它插入到我前面。InternalNode(内部节点)要复杂些,它们命令自己包含的对象与新对象进行比较,并根据比较结果决定是插入还是往下传,然而他又不知道如何比较,所以这份工作只有交给对象自己去完成。
通过动态分布内存,链表很小时使用的内存很少,随着链表不断增加,它将使用更大的内存空间。链表只占用足以存储当前数据内存,而数组分配的内存是固定的,这既浪费内存,又受到限制;
注意!!!链表是一种顺序存取方式,这就意味着必须遍历整个链表,直到找到所需要的对象,这种存取速度相对较慢。
总结!!!为何要将数据对象和节点分开?
答:让节点对象能够正常工作后,就可重用其代码,将其用于要存储到链表中的任何对象。
如果要在链表中存储其他类型对象,必须创建新的链表和新节点吗? 必须的
《第六部分 特殊主题》
第20章 《使用特殊的类、函数和指针》
Static 静态成员变量只能在类的外部直接初始化。不能在类内部进行初始化
静态成员函数的调用与对象无关;
不能对类的对象的值进行修改;除非将其成员修饰成static的静态成员变量:
静态成员变量是同一个类的所有实例共享的变量,它们是全局数据(可供程序所有部分使用)和成员数据(通常只供一个对象使用)的折衷。可将静态成员视为属于类而不是对象。通常成员数据是每个对象一个,而静态成员数据是每个类一个。
class Cat { public:
Cat(int newAge=1):age(newAge){howManyCats++;}
virtual ~Cat( ){howManyCats--;}
virtual int getAge( ){return age;}
vrttual void setAge(int newAge) {age=newAge;}
static int howManyCats;
private: int age;
};
int Cat::howManyCats=0;
int main( ){ const int maxCats=5;
Cat *catHouse[maxCats];
int i;
for(i=0;i<maxCats;i++)
catHouse[i]=new Cat(i);
for(i=0;i<maxCats;i++){
std::cout<<”there are”;
std::cout<<Cat::howManyCats;
std::cout<<”cats left!\n”;
std::cout<<”Deleting the one which is:”;
std::cout<<catHouse[i]->getAge();//用Cat对象访问
std::cout<<”years old \n”;
delete catHouse[i];
catHouse[i]=0; }
return 0; }
howManyCats 的声明并没有定义一个int变量,所有没有分配内存空间!。不同于非静态成员变量,实例化Cat对象不会为成员变量howManyCats分配存储空间,因为他不在对象中。注意,howManyCats变量是公有的,可在main()中直接访问。只要总是通过Cat实例来访问数据,就应将该成员变量及其他成员变量声明为私有的,并提供公有的存取器函数。另一方面,如果你想在没有Cat对象的情况下直接访问该数据:那么就得将他声明为公有变量或者提供一个静态成员函数。
静态成员函数类似于静态成员变量:它们不属于某个对象而属于整个类。因此不通过对象也能调用它们:Cat :: getHowMany( ); 可以直接这样使用静态成员函数
静态成员函数没有this指针,因此不能将它们声明为const。另外由于在成员函数中是通过this指针来访问成员数据变量的,因此静态成员函数不能访问非静态成员变量!
正如你在前面看到的,一个类的成员数据可以是另一个类的对象。c++程序员说外部类包含内部类,Employee类可能包含用于表示员工姓名的String以及用于表示员工薪水的整型。
??????????P227
按值传递Employee对象时,将复制它包含的所有String对象,这将调用String的复制构造函数。这种代价非常高,需要占用内存和处理时间。
使用指针或引用按引用传递Employee对象时,可避免所有这些代价。这就是对于超过多个字节的对象,C++程序员总是竭尽全力,不按值传递他们的原因。
有时候您要创建成对的类,他们需要能够彼此访问对象的私有成员,但是你又不想让这些信息变成公有的。
要将私有成员数据或函数暴露给另一个类,必须将其声明为友元类。这扩展了类接口,使其包含友元类。
友元类不能传递,不能继承,也不可交换,将Class1声明为Class2的友元并不能让Class2成为Class1的友元。
友元:能直接在类的外部使用;破坏了类的封装性,
友元关系是单向性的,不具有交换性,
友元不具有传递性;
#include<iostream> #include<cmath> using namespace std;
class B{ public: void setval( ){ }
private: int x; };
class A{public: void getval( ){ }
friend class B; };
int main ( ){
B b;
b.getval( ); b.setval( ); return 0;} //不能这样操作
#include<iostream>
using namespace std;
class B;
class A{ public: int getval();
private: static int y; };
class B{ public: friend class A; //A是B的友元,A能用B,而B不能用A
void setval(); //A是我的朋友,资源对他开放
private: static int x; };
int A::getval() {
int x = B::x++; return x;} //A中能通过B对象访问B类
void B::setval(){ //B中不能用A的成员变量
// A::y++;//友元具有单向性,故A不一定把B当朋友,故不能使用
}
int A::y=5; int B::x=10;
int main(int argc,char *argv[]){ A a; B b;
cout<<endl; cout<<a.getval()<<endl;
return 0; }
long * func(int);接受一个int参数且返回类型为long指针的函数
long (*func)(int);指向的函数接受一个int参数且返回类型为long;
void (*pFunc)(int&,int&); 指向返回值为void且接受两个int引用参数的函数;
明白指向什么的!!!!!
就像可以声明int指针数组一样,也可以声明函数指针数组,其中的指针指向返回特定类型和具有特定签名的函数。
函数指针(和函数指针数组)可传递给其他函数,后者可执行操作,然后使用传入的指针调用相应的函数。
结构void(*)(int &,int &)有点繁琐,可以使用typedef来简化,方法是声明类型VPF,它是一个指针,指向返回值类型为void且接受两个引用参数的函数:
typedef void(*VPF) (int &,int &);
然后,将变量pFunc的类型声明为VPF;
VPF pFunc;
再将成员函数printVals( )声明为接受三个参数(一个VPF和两个int引用):
void printVals(VPF pFunc,int &,int &);
记住,typedef创建同义词,唯一的差别就是可读性更强;
目前为止,创建的所有函数指针都指向非成员函数,其实也可以创建指向类成员函数的指针。
要创建成员函数指针:可使用与创建函数指针相同的语法,但需要包含类名和作用域运算符(::)。因此,如果pFunc指向类Shape的一个成员函数,它接受两个int参数且返回类型为void,那么pFunc的声明如下:
void (Shape::*pFunc)(int,int);
用法和函数指针相同,只是需要通过相应的对象来调用。
这里通过指针ptr来访问对象;通过指针
pFunc来访问函数;
delete ptr;没有理由这样做,因为他是一个指向代码的指针,而不是指向堆中对象的指针。delete反而会出错
像函数指针一样,成员函数指针也可以存储在数组中。可以用不同成员函数的地址来初始化数组,且可以用下标表示法来调用这些函数。
总结: 静态成员变量用于存储有关整个类的信息,还可用作同一个类的不同对象交换
信息的手段
友元函数让一个类能够将其私有成员变量和函数暴露给另一个类。虽然这些问题通常是通过基类和派生类之间的继承关系来处理的,但是使用友元函数可赋予继承层次结构外的类以访问特权。
答:静态数据的作用域为其所属类,因此,只有通过类的对象、通过使用类名的显式调用(如果静态数据时公有的)或通过静态成员函数,才能访问静态数据。静态数据在访问方面的限制和强制类型特征,使其比全局数据更安全。
在使用全局函数的情况下,为何还要使用静态成员函数?
答:静态成员函数的作用域为其所属的类,只有通过类的对象或显式全限定才能调用它们:ClassName::FunctionName();
为什么不将所有的类声明为它们使用的所有类的友元?
答:将类声明为另一个类的友元暴露了实现细节,降低封装程度。
每个静态成员有多少个备份? 只有一个备份供类的所有对象使用。
第二十一章 《使用C++0x新增的功能》
使用指针时一定要给他赋值,未初始化的指针可能指向内存的任何位置,这边是野指针。创建指针时应该讲空赋值给指针。可将它设置为0或者NULL(nullptr)。
最好不要讲指针初始化为0,如果对于依赖于函数重载的类来说便会导致二义性。nullptr不会隐式地转换为整数,但可能隐式地转换为布尔值(false)。
C++0X新增了常量表达式,这是使用了关键字constexpr实现的:
constexpr int getCentury( ){ return 100;}
Microsoft Visual Studio 10不支持这种关键字 constexpr;
任何使用constexpr声明的变量或函数都被隐式的视为常量;
auto并非C++新增的数据类型。变量的数据类型由编译器决定;
auto声明自动确定类型的变量时,必须对它初始化:不能使用它来声明数组的类型,也不能将其用作函数参数或返回值的类型,可将函数的返回值赋给使用auto声明的变量。关键字auto用于定义类或结构的成员变量,除非他是静态成员;用auto定义多个变量时,要求这些变量的数据类型相同。auto a=5; auto b=10.5; auto c=a+b; c=>>15.5;
这种for语句由两个由冒号(:)分割的部分组成。第一部分是一个引用,用于存储列表元素,而第二部分是列表名;
int str[5]={1,2,3,4,5};
for(int &p:str){ p *=3; std::cout<<p<<”\n”;} //每个元素都将乘以3;
这被称为基于范围(range-based)的for循环,可用于数组、初始化列表以及这样的类:包含返回迭代器的函数begin( )和end( );
第23章 《创建模板》
模板是相对较新的C++功能,为这种问题提供了解决方案。不同于老式宏,模板是C++的邮寄组成部分,它是类型安全的,并且非常灵活。模板让你能够创建通用类,通过将类型作为参数传递给模板,可创建实例!
模板让你能够告诉编译器如何创建任何数据类型的链表,而不是创建一组特定类型的链表。模板的实例化是:根据类创建对象或根据模板创建特定的类型;每个具体的类称为模板的实例
定义:要声明模板类List,可使用关键字template》》》》》
template<class T>
class List { public:
List( ); //全类的声明在此处//
};
所有模板类的声明和定义都以关键字template打头,接下来是模板的参数,它们是碎木板实例而已;
总结:在使用宏可行时为何要使用模板?
模板是类型安全的且是C++语言内置的。
模板函数的参数化类型和常规函数的参数有何不同?
常规函数对其接受的参数进行处理。模板函数让你能够参数化函数参数的类型。
除了类处理的元素类型外,所有行为或几乎所有行为都不变时使用模板。如果需要复制代码,且只修改一个或多个成员的类型,应考虑使用继承。
当对象的类型不影响类中函数的行为时,使用模板。如:常见的堆、栈、队列;
对象的类型不影响类中函数的行为:堆、栈这些无论对象是什么,都少补了入栈,出栈等操作。并不改变函数的行为。
当对象的类型影响类中函数的行为时,就要使用继承。如:猫、人等;
对象的类型影响类中函数的行为:对于猫、人这些类来讲,每只猫品种不同,都有自己特定的行为习惯,总会跟其他种类的有所不同,
这也就导致模板不能满足每一种情况,所以,使用继承。用虚函数来实现每种猫在拥有猫的共性的同时,又具有自己的特性。
第24章 《处理异常和错误》
异常是一个对象,从发生问题的代码传递给处理问题的代码。发生异常称为引发,处理异常称为捕获。
异常的基本理念:(1)资源分配(如分配内存或者锁定文件)通常是在程序的底 层进行的;
(2)操作失败(无法分配内存或者不能锁定文件)的处理逻辑 通常在程序高层,包含于用户交互的代码;
(3)异常提供一条从资源分配代码跳转到错误处理代码的快捷 路径不一定包含处理错误的代码。
如何使用异常》》》》》
对于可能发生问题的代码,应将其放在try块中。try块是一个用大括号括起来的代码块,其中的代码可能发生异常;
try { someDangerousFunction ();}
catch块紧跟在try块湖面,负责对异常进行处理:
try{ someDangerousFunction();}
catch(outOfMemory){采取行动,从低内存状态恢复}
catch(fileNotFound){take action when a file is not found}
使用异常的基本步骤:
(1)找出程序中可能引发异常的代码,并将其放在try块中;
(2)创建捕获这些异常的catch块,在其中执行清理工作,并将发生的情况告知用户。
引发异常后,程序将立即转到当前try块后面的catch块执行;
在确定try块的位置时,考虑在何处分配内存或资源。要监视的其他异常包括越界、无法输入等;
捕获异常》》》
引发异常后,将检查调用栈。调用栈是在程序的一部分调用另一个函数时创建的函数调用列表。
栈解退:异常沿调用向上传递给每个封闭快;栈解退时,将对栈中的局部对象调用析构函数, 将对象销毁。
注意:每个try块后面都有一个或者多个catch语句。如果异常与某个catch语句匹配,将执行该catch语句并认为异常已得到处理。若没有匹配到相应的catch语句,就将继续解退栈。
附件!! 《答案》
http://cplusplus.cadenhead.org