c++自学笔记

 

第一章节   《入门》 4

G++程序的执行步骤》》》》》 4

G++面向对象编程》》》》》 4

第四章 《表达式、语句、运算符》 4

第五章 《函数的调用》 4

函数的重载:要求》》》》》 4

内联函数: 5

预处理的优点》》》 5

第六章  《控制程序流》 5

高级for 5

如何在whilefor之间做出选择? 5

第七章  《使用数组和字符串存储信息》 5

解决方案》》》cin 》》》getline() 6

《第二部分  类》 6

第八章   《创建基本类》 6

类和成员:》》》 6

访问类成员》》》 6

实现成员函数》》 7

创建和删除对象》》》 7

默认构造函数 7

编译器提供的构造函数》》 7

第九章   《高级类》 8

const  成员函数 8

内联函数的实现: 8

《第三部分  内存管理》 8

第十章   《创建指针》 8

指针》》》》》》 8

栈和堆:(堆有很多的内存) 9

new关键字 9

使用关键字delete 9

避免内存泄漏: 9

第十一章 《开发高级指针》 10

在堆中创建对象》》》》》》》》》 10

删除对象》》》》》》》 10

使用指针访问数据成员》》》》》》 10

堆中的数据成员》》》》》》 10

this  指针》》》》》》 10

悬摆指针:》》》》》也称野指针或者迷失指针 11

const指针》》》》》 11

constconst成员函数》》》》》》 11

Java不需支持指针,为什么c++需要指针? 11

第十二章 《创建引用》 11

引用》》》》 11

将地址运算符用于引用》》》》》 11

可引用的目标》》》》》 12

空指针和空引用》》》》》》一级bug 12

按引用传递函数参数》》》》》》 12

按引用返回值》》》》》 12

第十三章 《高级引用和指针》 12

按引用传递以提高效率》》》》》》 12

传递const指针》》》》》》 12

作为指针替代品的引用》》》》》 12

不要返回不在作用域内的引用》》》》》》》 13

返回指向堆中对象的引用》》》》》》》》 13

谁拥有指针》》》》》》 13

《第四部分  高级C++ 14

第十四章 《高级函数》 14

重载成员函数》》》》》 14

使用默认参数值》》》》》 14

初始化对象》》》》》 15

复制构造函数》》》》》 15

  ::重载函数可以有默认参数吗? 16

第十五章 《运算符重载》 16

重载运算符》》》》》》 16

编写递增方法》》》》》 16

重载后缀运算符》》》》》 17

重载加法运算符》》》》》 17

对运算符重载的限制》》》》》 17

赋值运算符》》》》》 17

转换运算符》》》》》 18

《第五部分  继承和多态》 19

第十六章 《使用继承扩展类》 19

什么是继承》》》》》 19

继承和派生》》》》》 19

派生语法》》》》》 19

私有和保护》》》》》 19

构造函数和析构函数》》》》》 20

将参数创递给基类构造函数》》》》》 20

16.5重写函数》》》》》 21

16.5.1重载和重写》》》》》 22

16.5.2隐藏基类的方法》》》》》 22

16.5.3调用基类方法》》》》》 22

第十七章 《使用多态和派生类》 23

17.1使用虚函数实现多态 23

17.2虚成员函数的工作原理》》》》》 24

17.2.1不能通过基类指针访问派生类特有的方法》》》》》 24

17.2.2切除》》》》》 25

17.2.3.1构造函数》》》》》 26

17.2.3.2虚析构函数》》》》》 26

17.2.4虚复制构造函数》》》》》 26

17.2.5使用虚成员函数的代价》》》》》 26

第十八章 《使用高级多态》 27

18.1单继承存在的问题》》》》》 27

18.2 抽象数据类型》》》》》(ADT) 28

18.2.1纯虚函数 28

18.2.2实现纯虚函数 28

18.2.3复杂的抽象函数层次结构》》》》》 30

18.2.4哪些类是抽象的》》》》》 30

第十九章 《使用链表存储信息》 30

19.1链表和其他结构》》》》》 30

19.2链表案例研究》》》》》 31

19.3作为对象的链表》》》》》 31

《第六部分  特殊主题》 31

20章 《使用特殊的类、函数和指针》 31

20.1  Static静态成员函数》》》》》 31

20.2静态成员函数》》》》》 32

20.3将其他类对象作为成员》》》》》 33

20.3.3按引用还是按值复制》》》》》 33

20.4友元类和友元函数》》》》》 33

20.5函数指针》》》》》 34

20.5.1函数指针数组》》》》》 34

20.5.2将函数指针传递给其他函数》》》》》 35

20.5.3  typedef用于函数指针》》》》》 35

20.5.4成员函数指针》》》》》 36

20.5.5成员函数指针数组》》》》》 37

第二十一章  《使用C++0x新增的功能》 37

21.2空指针常量》》》》》 37

21.3编译阶段常量表达式》》》》》 37

21.4自动确定类型的变量》》》》》 37

21.5新的for循环》》》》》 37

23章  《创建模板》 38

23.1什么是模板》》》》》 38

23.2模板实例》》》》》 38

24章 《处理异常和错误》 38

24.1 异常》》》》》 38

24.4使用try块和catch块》》》》》 39

附件!!  《答案》 39

 

第一章节   《入门》

G++   --version  >>>检查是否安装gcc

http//developer.apple.com》》》注册成为APPle开发人员

PATH》》》计算机》》高级系统设置》》》环境变量》》》在变量值文本框中添加:PATH》》》OK

G++源代码文件:扩展名>>>>> .cpp  ||  .cxx  ||  .cp  ||  .c 一般选择 .cpp

G++程序的执行步骤》》》》》

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

 

#符号支出当前代码行是一个编译指令,需要在程序编译器处理的命令;

函数签名由函数名、参数及其排列顺序组成;

#defineconst定义的常量有何不同:

(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 doubleint);

注意事项:  1):递归函数不能定义为内联函数

2):内联函数一般适合于不存在whileswitch等复杂函数结构中,并且只有1~5条语句的小函数上,否则编译器将该函数视为普通函数;

3):内联函数只能先定义后使用,否则编译器也会把他认为是普通函数;

4):不能进行异常的接口声明;

5)内联函数的功能和预处理宏的功能很相似》》》》》

预处理的优点》》》

函数调用要有一定的时间和空间方面的开销,于是影响其效率,然而宏只是在预处理的地方把宏展开,不需要额外的时间和空间方面的开销,效率也很高;

缺点是:不能访问私有的成员;而宏的定义很容易产生二义性;

内联函数和宏的区别在于:宏是预处理器对宏替换;然而内联函数是通过编译器控制来实现 的,它是真正的函数;只是在用到的时候,内联函数像宏一样展开,所以消除了函     数的参数压栈,减少了调用的开销;

inline tablefunctionint 1{return 1*1}  》》》内联函数

在类的内部定义的函数默认是内联函数;比普通函数的效率要高;  

内联函数不能是virtual虚函数;

内联函数是一个静态行为,而虚函数是个动态的行为;  

第六章  《控制程序流》

高级for

Forint x=0,y=0;x<10;x++,y++{std::cout<<x*y<<\n;}

如何在whilefor之间做出选择?

如果要初始化计数变量,且每次循环迭代都检查并递增该变量,应该考虑使用for循环;如果变量已经初始化,或者无需每次循环迭代都递增它,while循环可能是更好的选择;

 

第七章 《使用数组和字符串存储信息》

 Std::cin>>boy;

存在两个大问题》》》首先,如果用户输入的字符超过了缓冲区的长度,cin写入时跨国缓冲区边界,导致程序不能正确运行,还可能导致安全问题;其次是如果用户输入了空格,cin》》》将认为字符串就此结束,不在将接下来的内容写入缓冲区;

解决方案》》》cin 》》》getline()

(1) 要填充的缓冲区     2)最多读取多少个字符;

Std::cin.getline(boy,18);   //最多提取18个字符(包括空字符)

Std::cin.getline(boy,18, ‘  ‘ ); //这条语句在遇到空格后停止读取输入,

 

 

对于只有24个元素的数组,如果写入第25个元素,结果如何:》》》》》》

这将写入其他变量占据的内容,给程序带来灾难,这可能覆盖程序使用的其他内容,导致程序不能正常运行,据安全专家说:恶意程序员利用最多的软件漏洞是,写入数据时超越缓冲区边界,并利用这种错误来执行新代码,新代码通常可执行任何操作,如修改或者删除文件、将系统特权授予不信任的用户以及复制病毒;

《第二部分  类》

第八章   《创建基本类》

类和成员:》》》

C++类是一个模板,用于创建对象。定义类后,便可像使用其他类型那样使用根据它创建的对象。类是一系列捆绑在一起的变量和函数,其中的变量可以是任何其他类型包括其他类;变量构成了类的数据,而函数使用这些数据来执行任务,将变量和函数捆绑在一起成为封装;

类中的变量称为成员变量:成员变量也被称为数据成员或者实例变量,他们是类的组成部分;类中的函数使用和修改成员变量,他们被称为类的成员函数或者方法,与成员变量一样,成员函数也是类的组成部分,它决定了类的对象能做什么;

声明一个类:  class  Tricycle{ public

unsigned int  speedunsigned int wheelsize

pedal();break(); }

创建类对象》》》 Tricycle    Wichita

访问类成员》》》

创建对象后。可使用句点运算符(.)来访问其成员函数和成员变量。如前面的》》要使用Tricycle类中名为speed的成员变量,要设置这个变量,可使用(.);

例如》》》》》   Tricycle    WichitaWichita.speed=6Wichita.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 ::backint  initialSpeed{   setSpeed(initialSpeed);  }

    上面的构造函数作用为设置speed的初始值;注意如果声明了一个构造函数,那么就得声明一个析构函数。它们的关系为》》》》》构造函数创建并且初始化对象,而析构函数则是执行清理工作并且释放分配给对象的内存,析构函数的名称总是由腭化符号(~)和类名组成。析构函数不接受任何参数,也没有返回值back的析构函数

 ~ back::back(){  //do  nothing } //析构函数无需执行特殊的操作来释放内

默认构造函数

创建对象的时候有多种调用构造函数的方式;一种方式是在括号内指定一个或者多个参数》》》》》  back Wichita5);

        这些参数将传递给构造函数,上述示例设置了一个初始值,

也可以在创建对象的时候不指定参数:back  Wichita();默认调用没有参数的构造函数

编译器提供的构造函数》》

没有声明构造函数时,将由编译器自动创建一个默认构造函数(不执行任何操作);

注意!1>默认的构造函数是没有参数的构造函数,你可以自己定义,也可以让编译器提供;

      2>只要你定义了构造函数(无论是否接受参数),编译器就不会为你提供默认构造函数,在这种情况下,如果需要默认构造函数,你必须自己定义;

  如果你没有定义析构函数,编译器也会自己提供一个。其函数体也是空的,无操作;

只要定义了构造函数就必须定义一个析构函数,哪怕它什么也不执行;

例子》》》》》》 class back{  public:  int  getSpeed( ); void setSpeed(int speed);  void  pedal();

private:  int  speed;};

            back ::backint   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:注意应该尽可能的将函数声明为常量函数,这是一种好的编程习惯。通常这样做,可以让编译器发现您对成员函数变量的无意修改, 避免这些错误出现在运行阶段;

const  修饰函数时,表示不能使用该函数来修改对象;

      修饰对象时,表示对象中的成员不可以被修改;     

修饰变量时,表示对象的值不可以被修改,只能通过构造函数的参数初始化表进行初始化操作;

 

内联函数的实现:

可将常规函数声明为内联函数,也可以将成员函数声明为内联,为此,只需要在返回类型的前面加上关键字inline;或者直接将函数定义放在类声明中,这样函数将自动变成内联函数;

将其他类用作成员数据的类:

《第三部分  内存管理》

第十章   《创建指针》

指针》》》》》》

变量是可存储一个值得对象:整型变量存储一个数字,字符变量存储一个字母,而指针是存储地址变量;

计算机内存是存储变量值得地方,计算机内存被划分成顺序编号的内存单元,每个内存单元都有地址,在内存中,每个变量都位于特定的地址处,而不管其类型如何。

声明指针:   》》》新增变量nullptr  表示空指针;

它用于存储地址。指针是一种特殊的变量,用于存储对象在内存中的地址;声明指针的时候,务必指定它将指向的变量类型,这告诉编译器如何处理指针指向的内存单元;

        一定要明确指向》》》  int  howold=550int *pAge=nullptr;   pAge=&howold;

      unsigned  short  int  howold=50;  unsigned  short  int  * pAge=&howold;

简介运算符(*)  》》》也被称为解除引用运算符

对指针引用的时候,将获取指针存储的地址处的值,如两个变量之间的赋值:

unsigned  short  int  howold=55unsigned  short  int* pAge=&howold

unsigned  short  int  yourAge;   yourAge=*pAge//存储在.......处的值

注意:声明的时候:*  >>表示这是指针,而不是普通的变量;

      解引用的时候:*  >>表示这是指针指向的内存单元中的值,而不是指针的地址本身;

指针、地址、变量:

  区分哪个是指针,该指针存储的是那个变量的地址,该地址处的值是多少;

使用指针操作数据:

查看存储在指针中的地址:

为何使用指针:

  1>管理堆中的数据;2>访问类的成员数据和成员函数;3>按引用将变量传递给函数;

栈和堆:(堆有很多的内存)

  通常处理需要5个内存区域;

      >全局名称空间;------全局变量;

>寄存器;      ------用于内部管理,如跟踪栈顶和指令指针;

>代码空间;    ------代码

>栈;          ------局部变量和函数参数

>堆;(自由存储空间)  ------余下的内存都在堆中

局部变量:不会持久化函数返回,将丢失    全局变量解决了这个问题(易Bug

放在堆中能解决这种bug :必须询问预留的地址,将其存储到指针中,然后丢掉;

注意*  每当函数返回的时候,都会清理栈;然而只有当程序结束的时候才会清理堆,所以在使用完预留的内存后,要将其释放;让不需要的信息留在堆中成为内存泄漏

堆的优点:在显式释放前,你预留的内存始终可用。如果在函数中预留堆中的内存,在函数返回后,该内存任然可用;只有有权访问指针的函数才能访问它所指向的数据;这提供了控制严密的数据接口,消除了函数意外修改数据的问题;故就需要使用能够创建指向堆中内存区域的指针,------>>>>>>   new  关键字

new关键字

返回一个内存地址,必须将其赋给指针;如>>>>>>

unsigned  short  int * pPointer

pPointer = new unsigned  short  int ;

*pPointer=72//72 赋值给pPointer指向的堆内存区域;

同:unsigned short  int* pPointer=new unsigned short int

如果不能从堆中分配内存(因为内存资源有限),将引发异常;

使用关键字delete

  使用分配完的内存区域后,必须对指针调用delete,将内存归还给堆;指针本身为局部变量,这不同于它指向的内存;当声明指针的函数返回时,指针将不再在作用域中,因此被丢弃,然而,使用new运算符分配的内存不会自动释放,这些内存将不可用,这被称为内存泄漏,因此在程序结束前,内存不会归还给堆,就像内存从计算机中泄漏了;删除指针时,应将其设置为NULL,对空指针调用delete是安全的;若不NULL则两次调用将造成崩溃;

如何给一个变量分配堆内存》》》》

int * pHeap = new  int ;

ifpHeap==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 Friskreturn0}

堆中的数据成员》》》》》》

类可能有一个或者多个数据成员指针,指向堆中的对象(内存)。可在构造函数或成员函数中分配内存,并在析构函数中释放内存;自己编写析构函数将不会造成内存泄漏;因为编译器只删除指向堆内存的指针,而不会删除堆中的内容;

#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 Friskyreturn0}

 

this  指针》》》》》》

每个类成员函数都有一个隐藏的参数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 Friskreturn0}

悬摆指针:》》》》》也称野指针或者迷失指针

导致Bug的罪魁祸首之一;对指针调用delete(释放它所指向的内存)后,如果没有重新赋值就是用它,将导致悬摆指针;注意对指针调用delete后千万不要使用它。该指针仍指向原来的内存区域,但是编译器可能在这里存储了其他的数据;使用该指针可能导致程序崩溃,故delete后一定要nullptr,解除武装!

const指针》》》》》

const  int   *pOne;  >>>>>>>>>不能修改它指向的值  *pOne=5;  (X

int   const * pOne

int * const  pTwo;    >>>>>>可修改指向的变量的值,指向不能更改;

const  int  * const  pThree;   >>>>>>>都不能修改

constconst成员函数》》》》》》

 注意:将对象声明为常量时,实际上是将this声明为指向常量对象的指针,常量this指针只能用于调用常量成员函数;

   类中的数据成员可以是指向堆中对象的指针,可在类的构造函数或其他函数中分配内存,并在析构函数中将其释放;

Java不需支持指针,为什么c++需要指针?

  Java创始人认为对大多数任务,对不需要指针,可用其他方法得到相同的结果,他认为指针太麻烦了,就排除了!在C++中》》仅当需要直接使用硬件(如需要编写设备驱动程序)时,指针才有绝对的作用;

第十二章 《创建引用》

 引用》》》》

指针是存储另一个对象的地址的变量;而引用是对象的别名

int  someInt;  int   &rSomeRef=someInt;  操作rSomeRef就等于操作someInt

注意:引用运算符和地址运算符是同一个符号 &,用于声明中;别忘了,声明指针时,星号(*)声明的变量是指针;在语句中与指针一起使用时,星号表示间接运算符;

将地址运算符用于引用》》》》》         

也可以声明一个类:如现有类President  Back_Obama President  &Obama= Back_Obama

这里只有President一个类,对Obama执行的任何操作都将影响Back_Obama

可引用的目标》》》》》

可引用任何对象,包括用户定义的对象。请注意,您创建的是指向对象的引用,而不是指向类或者数据类型的引用(如int)的对象。不能写成>>>> int  &rone=int ;

    可以像使用对象那样使用指向对象的引用:访问成员函数和成员数据时,使用类成员访问符(.),与内置类型的引用一样,指向对象的引用也是对象的别名;

空指针和空引用》》》》》》一级bug

指针未初始化或被删除时都应该赋值NULL;对于引用却不是,引用不能为空,让引用指向空对象的程序是非法的,可能正常运行或者删除硬盘重要的数据;

按引用传递函数参数》》》》》》

通过按引用传递对象,可让函数修改指向的对象;指针和引用语法不同效果一样;

swapint * a,int * b;指针>>>地址   swapint &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::SimpleCatSimpleCat &);//复制构造函数结束后自动调用;

按引用传递避免了创建备份以及调用复制构造函数,构造函数和析构函数效果更好;

传递const指针》》》》》》const  SimpleCat  * const  Functionconst SimpleCat *const theCat);

使用const关键字避免了对象在按引用传递时候暴露在被修改的风险中。

经典的一句话按值传递类似与将作品的照片提供给博物馆,即使在照片上做记号也不会损坏原作;而按引用传递就好比让参观者到博物馆直接与作品接触;故要用const

作为指针替代品的引用》》》》》  const  SimpleCat  &Functionconst 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();

int  main(){    

                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=&rCatdelete 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{  publicRectangleint widthint height); ~Rectangle(){}

      void  drawShape()const

void drawShapeint  widthint  heightconst

privateint widthint  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”;} }

  int main( ) {  

  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,0true):\n”;

box.drawShape(0,0true);   std::cou<<”\ndrawShape(40,2):\n”;

box.drawShape(40,2);   return  0;}//默认为false

初始化对象》》》》》

  与成员函数一样,构造函数也可以重载。重载构造函数功能强大》》》

    一个接受参数的构造函数;一个不接受参数;单数要注意的是不能重载析构函数,析构函数:名称为类名之前加(~),且不接受任何参数;

析构函数由初始化部分和函数体组成;可在初始化部分设置成员变量,也可以在构造函数的函数体内设置:如下所示初始化成员变量》》》》》

Tricycle::Tricyclespeed5),wheelSize12{//body  of  constructor}

警告!!由于不能给阴影和常量赋值,因此必须以这种方式给他们初始化;

复制构造函数》》》》》

除了默认构造函数和析构函数外,还有一种就是默认的复制构造函数,每当创建对象的备份时,都将调用复制构造函数;

按值将对象传入或者传出函数时,都将创建对象的一个临时备份。如果对象是用户自定义的,就将调用相应类的复制构造函数。所有的复制构造函数都接受一个参数:一个引用,它指向所属类的对象,最好将该引用声明为常量,因为复制构造函数不用修改传入的对象::Tricycleconst 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{ publicCounter( );~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   var1var2var3var3=var2+var1

class  Counter{ publicCounter( );  ~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;

    gamma=alpha + beta;

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

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来派生而来的;

派生语法》》》》》

要从一个类派生出另一个类,可在声明中的类名后面加上冒号,在指定类的访问控制(publicprotected、或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受保护的数据成员和函数对派生类来说是可见的,但其他方面与私有成员完全相同。

对于三个限定符:publicprivateprotected。只要有类对象函数就能访问该类的所有成员数据和成员函数;然而,成员函数可以访问基类的所有私有数据和函数。

enum   BREED {YORKIECAIRNDANDIESHETLANDDOBERMANLAB}

class  Mammal{ publicMammal( ):age(2),weight(5){}  ~Mammal( ){ };

   //accessors  数据的获取与设置

 int  getAge() const {return age}

     void  setAgeint newAge{age=newAge}

 int  getWeight()const{return  weight}

 void  setWeightint 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;   //具有MammalDog的所有特征

  fido.speak( );

fido.wagTail( );

 std::cout<<”Fidois“<<fido.getAge()<<”years old\n”;

 return 0;}

构造函数和析构函数》》》》》

对于C++的继承,要明白创建一个派生类的对象时,将调用多个构造函数。

如上面的程序当派生类的对象是基类时,在派生类中创建fido时,将首先调用基类构造函数创建一个Mammal对象,然后调用Dog类的构造函数完成对象的创建,由于创建的fido没有指定任何参数,因此在这两个阶段都将调用默认构造函数。

在销毁fido对象的时候,将首先调用Dog类的析构函数,然后调用Mammal类的析构函数,这让每个函数都有机会清理对象的相应部分。构造函数按继承顺序调用,而析构函数按相反的顺序调用;

将参数创递给基类构造函数》》》》》

您可能想重载MammalDog的构造函数,使其分别设置agebreed。如果将参数ageweight传递给Mammal的构造函数;要在派生类的初始化阶段进行基类初始化,可指定基类名称,并在后面跟基类构造函数需要的参数,如上面的

      Mammal基类中的ageweight要在派生类中初始化:

Dog( );

Dogint  age);

Dogint  ageint  weight);

Dogint  ageBREED  breed);

Dogint  ageint  weightBREED  breed);

  

Dog::Dog():Mammal(),breedYORKIE{std::cout<<”hello,,,\n”;}

Dog::Dogint age):Mammalage),breedYORKIE{std::cout<<”hello,,,\n”;}

。。。。。。。。。。。

 

16.5重写函数》》》》》

Dog这个派生类不但可以访问基类的所有成员,还可以访问自身的任何函数;另外还可以重写基类函数,重写基类函数意味着在派生类中修改基类函数的实现。创建派生类的对象时,将调用正确的函数;

  如果派生类创建了一个返回类型和签名都与基类成员函数相同的函数,但是提供了新的实现,就叫做重写该函数。

重写函数时返回类型和签名必须与基类函数相同。签名是指除了返回类型外的函数原型,这包括函数名,参数列表和关键字const(若使用)。函数的签名由其名称以及参数的数量和类型组成,但是不包括返回类型

如》》》》》》

enum   BREED {YORKIECAIRNDANDIESHETLANDDOBERMANLAB}

class  Mammal{ publicMammal( ){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;}

 

       out:     Mammal constructor...   

Mammal constructor...   

Dog constructor...

Mammal sound!

Woof... ...

Dog destructor...

Mammal destructor...

Mammal destructor...

16.5.1重载和重写》》》》》

本语重载和重写相似,C++中的功能也相似。重载成员函数时,创建了多个名称相同但是签名不同的函数;而重写成员函数时,在派生类中创建了一个名称和签名都与基类函数相同的函数。

16.5.2隐藏基类的方法》》》》》

在上一个函数的例子中,Dog类的成员函数speak( )隐藏了基类的同名函数,这正是您希望的;这样可能导致意外的结果。比如Mammal有一个重载了的move( )方法,而Dog重写了该函数,那么Dogmove( )方法将隐藏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; }

16.5.3调用基类方法》》》》》

即使重写了基类的方法,仍然可以使用全限定名来调用它。可指定基类名、冒号和方法名:  Mammal::move( );

总结! 继承的数据和函数会传递给后代吗?如果Dog是从Mammal派生而来,而Mammal是从Animal派生而来!那么Dog能否继承Animal的函数和数据?

答:会!连续派生时,派生类将继承其所有基类的函数和数据;

在派生类中,可将公有的基类函数改为私有的吗?

答:可以!在接下来的派生中,它始终保持为私有的!

为何在派生类中隐藏基类的成员函数?

答:已禁止使用它。有时候派生类与基类的行为差别很大,导致有些基类成员函数不合适。鉴于并非总是可以修改基类。

创建派生类对象时,将以继承顺序调用构造函数!

第十七章  《使用多态和派生类》

17.1使用虚函数实现多态

多态让你能够将派生对象视为基类对象,例如:假设你创建了从Mammal派生出的DogCatHorse等类,而Mammal类包含很多适用于这些派生类的成员函数,其中之一就是speak( ),它能实现所有哺乳动物都能发出声音的功能;

你如果想让每个派生类都发出特殊的声音,如猫叫、犬吠..... 每个派生类都必须能够重写speak( )方法的实现;

与此同时,如果有一系列Mammal对象,如有DogCatHorseCow对象的农场,你希望让这些对象发出声音,而无需知道或者关心它们的speak( )实现。将这些对象都视为哺乳动物而调用方法Mammal.speak( )时,便使用了多态。

多态是个不寻常的单词,意味着具有多种形态。它起源于polymorph,前者表示多,而后者表示形态;

你将使用Mammal的多种形态:使用多态需要声明一个Mammal指针,并将在堆中创建的Dog对象的地址赋给它(基类)。由于Dog是一个Mammal,因此下面的代码完全合法;   Mammal  * pMammal=new  Dog;

然后可以使用该指针对Mammal调用任何成员。你希望调用重写了的函数时,将根据指针指向的对象类型,调用正确的函数;虚成员函数让你能够实现这个目标。为实现多态,你使用Mammal指针来调用方法,不用知道也不关心该指针指向的是那种对象以及该对象的方法是如何实现的!下例就是用虚函数来实现多态》》》》》

class Mammal{public

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》》》》》

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 ...!\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 Mammalbreak;  }

array[i]=ptr;     }  

for(i=0;i<5;i++)  array[i]->speak( );

return  0;}

注意!!!在编译阶段,无法知道将创建什么类型的对象!因此无法知道将调用那个speak()方法;ptr指向的对象是在运行阶段确定的,这被称为后期绑定或运行阶段绑定!与此相对的是静态绑定或者编译阶段绑定!

17.2虚成员函数的工作原理》》》》》

创建派生对象(如Dog对象)时,首先调用基类的构造函数,然后调用派生类的构造函数。注意基类部分和派生类部分在内存中是相邻的!

  在类中创建虚函数后!这个对象必须跟踪它。很多编译器

创建虚函数表(V-table)。每个类都有一个虚函数表,而每个对象都有一个

指向虚函数表的指针(V-ptrV-pointer)。

虽然实现方式不同,但是所有编译器都必须完成这项工作!如果在基类表明某个函数被定义为virtual(虚函数)后!使用基类指针时,将根据基类指针指向的对象的实际类型指向正确的函数!这样调用speak( )时,将调用正确的函数!

17.2.1不能通过基类指针访问派生类特有的方法》》》》》

如果Dog有成员函数WagTail( ),但是Mammal没有!就不能通过Mammal指针访问它(除非将其转换为派生类指针 )。由于WagTail( )不是虚函数,且基类没有它,因此如果没有Dog对象或者Dog指针,就不能访问它;

虽然可以将基类指针转换为派生类指针,但是通常有更好、更安全的调用WagTail( )的方法。C++不赞成强制类型转换!因为这样容易出错。1824章有介绍!

17.2.2切除》》》》》

仅当通过指针和引用进行调用时,才能发挥虚函数的魔力;按值传递对象时不能发挥虚函数的魔力!如下所示》》

                class  Mammal{public:   Mammalage(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!

 

 

17.2.3.1构造函数》》》》》

从存储空间角度,虚函数对应一个指向v-table虚函数表的指针,这大家都知道,可是这个指向vtable的指针其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。

从使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。

17.2.3.2虚析构函数》》》》》

在需要积累指针的地方使用指向派生类对象的指针是一种合法和常见的做法。当指向派生类对象的指针被删除时将发生什么情况?(回收资源的)如果析构函数是virtual,将执行:调用派生类的析构函数。由于派生类的析构函数会自动调用基类的析构函数,因此整个对象将被正确的销毁;

类中任何一个函数时虚函数,那么析构函数也应该是虚函数!!

如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。

 

17.2.4虚复制构造函数》》》》》

构造函数不能使虚函数,然而有时候程序非常需要通过传递一个指向基类对象的指针,创建派生类对象的备份。解决方案一:在基类中创建一个clone( )成员函数,并将其设置为虚函数。clone( )函数创建当前对象的备份,并返回该对象!由于每个派生类都重写clone( ) 方法,因此使其创建派生类对象的备份。

17.2.5使用虚成员函数的代价》》》》》

使用虚成员函数的类就必须维护一个v-table,因此使用虚函数会带来一些开销。如果类很小,不打算从它派生出其他类,就根本不需要用到虚函数;

将任何函数都声明为虚函数之后,便付出了V-table的大部分代价(每增加一个表项都会增加一些内存开销);故应将析构函数声明为虚函数!并假设其他所有函数也可能是虚函数。

总结!!!为什么不将类函数都声明为虚函数?

创建V-table表的开销伴随第一个虚函数的创建而发生。在此之后,创建其他虚函数带来的开销将很小!如果一个函数为虚函数!那么其他所有函数也应该为虚函数!又得程序员又不认为是这样!

如果一个基类的一个函数是虚函数,且被重载以便能接受一个或者两个int参数!而派生类重写了接受一个int参数的版本!那么通过指向派生类对象的指针调用接受两个int参数的函数时,将调用那个函数?

重写接受一个int参数的版本将隐藏基类中所有同名函数,因此将出现变异错误!指出该函数只接受一个int参数。

在运行阶段将指针绑定到对象成为什么?

晚期绑定发生在运行阶段;而静态绑定发生在编译阶段!

第十八章  《使用高级多态》

18.1单继承存在的问题》》》》》

如果基类有成员函数speak( ),并在派生类中重写了它,将派生类对象赋给基类指针时,可使用该指针来完成正确的工作;

class Mammal{publicMammal( ):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,将执行:调用派生类的析构函数。由于派生类的析构函数会自动调用基类的析构函数,因此整个对象将被正确的销毁;

18.2 抽象数据类型》》》》》(ADT)

经常需要创建层次结构!如可能创建Shape类,然后将其作为基类,派生出RectangleCircle。从Rectangle类又可能派生出Square类,将其作为Rectangle的特例;

每个派生类都将重写draw( )getArea( )方法等。CircleShape(基类)派生而来,并重写了基类的虚成员函数,就没有理由在使用virtual关键字。因为这是其继承性的一部分;如果要写也没有坏处!如逐层继承!

18.2.1纯虚函数

C++通过提供纯虚函数来支持创建抽象数据类型!纯虚函数是必须在派生类中重写的虚函数。通过将虚函数初始化为0来将其声明为纯虚的virtual void draw()=0

任何包含一个或多个纯虚函数的类都是DAT,不能对其实例化!试图这样做将导致编译器错误。将纯虚函数放在类中向其客户指出了以下两点:

(1)不要创建这个类的对象,而应该从其派生。

(2)务必重写这个类继承的纯虚函数。

 在从ADT派生而来的类中,继承的纯虚函数仍是纯虚的要实例化这种类的对象,必须重写每个纯虚函数。此,如果RectangleShape派生而来,而Shape有三个纯虚函数,那么Rectangle必须重写这三个纯虚函数,否则它也将是ADT

要将虚函数声明为抽象的,可在函数声明后面添加=0virtual  long  getArea=0

下面重写了Shape类,使其成为抽象数据类型:

  class  Shape{  public:    Shape(){ }

  virtual  ~Shape(){ }

  virtual  long  getArea()=0

  virtual  long  getPerim()=0

virtual  void  draw()=0

       private}

18.2.2实现纯虚函数

通常不实现抽象基类的纯虚函数。由于不能创建抽象的对象,因此没有理由提供实现,另外,ADT只用作其派生而来的类的接口定义。

然而可以给纯虚函数提供实现。这样,就可以通过从ADT派生而来的对象调用该函数,该函数可能旨在给所有重写函数提供通用功能。

下面的程序Shape2Shape声明为ADT,并提供了纯虚函数draw( )的实现。Circle类重写了draw( )方法(必须这样做),并调用了基类的draw( )函数以提供额外的功能。

 #include<iostream>  

class  Shape{ publicShape(){  }

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

18.2.3复杂的抽象函数层次结构》》》》》

有时候,会从ADT派生出其他ADT。你可能想将一些继承而来的纯虚函数变成非纯虚函数,而保留其他的不变。

 在从ADT派生而来的类中,继承的纯虚函数仍是纯虚的要实例化这种类的对象,必须重写每个纯虚函数。试图实例化没有重写的派生类将导致编译错误,因为他们都是抽象的数据类型!

18.2.4哪些类是抽象的》》》》》

在一个程序中,基类可能是抽象的,而在另一个程序中可能不是!什么因素决定应将类声明为抽象的呢?

如要编写一个描述农场或动物园的程序,可能将基类声明为抽象类!但希望能实例化某个派生类对象;;另一方面,如果要描述各种派生类中的具体的类别,可能将派生类也声明为抽象类,且只实例化派生类中某个具体的类型。抽象层次取决于需要如何细分类型!

总结!    向上提升功能是什么意思?

答:这指的是将共享功能向上提升,将其放到基类中。对于多个类都需要的函数,最好将其放在一个合适的类中。

  向上提升功能是否总是好事儿?

     如果提升的是共享功能,就是好事儿;如果提升的是接口,就是坏事儿。对于并非所有派生类都使用的成员函数,将其移到基类就是错误,如果这样做,就必须检查对象的运行阶段类型,以判断能否调用该函数。

   为什么动态强制类型转换时糟糕的?

  使用虚函数旨在让虚函数表(而不是程序员)来判断对象的运行阶段类型;

为什么要创建抽象数据类型?为何不将其声明为非抽象的,并避免创建这种类型的对象?

   C++的很多规则都旨在让编译器帮助查找bug,以避免出现运行阶段bug。通过在类中包含纯虚函数,让类变成抽象的,编译器将把创建这类对象的代码视为错误。这还意味着你可以与其他应用程序或程序员共享抽象数据类型。

 

第十九章  《使用链表存储信息》

19.1链表和其他结构》》》》》

数组可以看成是一个容器它的大小是固定的;链表是一种数据结构,由连接在一起的小容器组成。在这里容器是类,它们包含要存储在链表中的对象。就是编写一个存储数据对象的类(如CatRectangle),他能够指向链表中的下一个容器,您为需要存储的每个对象创建一个容器,并将他们连接起来。

这些容器有节点。链表中的第一个节点称为头结点,最后一个节点称为尾节点。

链表有:单链表;双链表;树;

在单链表中,每个节点都指向下一个节点,但不能指向前一个节点。要查找特定的节点,从链表头开始,逐个节点往下找。双链表让你能够向前向后移动到下一个节点和前一个节点。是一种由节点组成的复杂数据结构,每个节点都可能指向多个节点

19.2链表案例研究》》》》》

注意每个类只负责自己的任务,但协同工作造就了一个能正常运行的程序!

链表的组成:节点类本身是一个抽象类,将使用3个子类来完成工作。链表包含一个头结点和一个尾节点,他们负责管理链表的组成部分,还包含零或多个内部节点。内部节点用于记录存储在链表中的实际数据。

注意数据和链表是一个不同的概念。可在链表中存储任何类型的数据,连接在一起的不是数据而是存储数据的节点。程序并不知道节点,它只是使用链表。链表完成的工作很少,它将工作委托给节点。

19.3作为对象的链表》》》》》

 在面向对象编程中,赋予每个对象已明确而有限的职责。链表负责维护头结点,而HeadNode将新数据传递给它当前指向的节点,而不考虑它指向的是那个节点 。

每当收到数据后,TailNode都创建一个新节点并将其插入到链表中。TailNode只知道一点:只要有数据传来,便将它插入到我前面。InternalNode(内部节点)要复杂些,它们命令自己包含的对象与新对象进行比较,并根据比较结果决定是插入还是往下传,然而他又不知道如何比较,所以这份工作只有交给对象自己去完成。

通过动态分布内存,链表很小时使用的内存很少,随着链表不断增加,它将使用更大的内存空间。链表只占用足以存储当前数据内存,而数组分配的内存是固定的,这既浪费内存,又受到限制;

注意!!!链表是一种顺序存取方式,这就意味着必须遍历整个链表,直到找到所需要的对象,这种存取速度相对较慢。

总结!!!为何要将数据对象和节点分开?

  答:让节点对象能够正常工作后,就可重用其代码,将其用于要存储到链表中的任何对象。

如果要在链表中存储其他类型对象,必须创建新的链表和新节点吗? 必须的

 《第六部分  特殊主题》

第20章  《使用特殊的类、函数和指针》

20.1  Static静态成员函数》》》》》

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对象的情况下直接访问该数据:那么就得将他声明为公有变量或者提供一个静态成员函数。

20.2静态成员函数》》》》》

静态成员函数类似于静态成员变量:它们不属于某个对象而属于整个类。因此不通过对象也能调用它们:Cat  :: getHowMany( );   可以直接这样使用静态成员函数

静态成员函数没有this指针,因此不能将它们声明为const。另外由于在成员函数中是通过this指针来访问成员数据变量的,因此静态成员函数不能访问非静态成员变量!

20.3将其他类对象作为成员》》》》》

正如你在前面看到的,一个类的成员数据可以是另一个类的对象。c++程序员说外部类包含内部类,Employee类可能包含用于表示员工姓名的String以及用于表示员工薪水的整型。

??????????P227

20.3.3按引用还是按值复制》》》》》

按值传递Employee对象时,将复制它包含的所有String对象,这将调用String的复制构造函数。这种代价非常高,需要占用内存和处理时间。

使用指针或引用按引用传递Employee对象时,可避免所有这些代价。这就是对于超过多个字节的对象,C++程序员总是竭尽全力,不按值传递他们的原因。

20.4友元类和友元函数》》》》》

有时候您要创建成对的类,他们需要能够彼此访问对象的私有成员,但是你又不想让这些信息变成公有的。

要将私有成员数据或函数暴露给另一个类,必须将其声明为友元类。这扩展了类接口,使其包含友元类。

友元类不能传递,不能继承,也不可交换,将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; }

20.5函数指针》》》》》

long * funcint);接受一个int参数且返回类型为long指针的函数

long *func)(int);指向的函数接受一个int参数且返回类型为long

void (*pFunc)(int&,int&);  指向返回值为void且接受两个int引用参数的函数;

明白指向什么的!!!!!

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

20.5.1函数指针数组》》》》》

就像可以声明int指针数组一样,也可以声明函数指针数组,其中的指针指向返回特定类型和具有特定签名的函数。

 

 

 

 

                                       

 

 

 

 

 

 

 

20.5.2将函数指针传递给其他函数》》》》》

函数指针(和函数指针数组)可传递给其他函数,后者可执行操作,然后使用传入的指针调用相应的函数。

 

      

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

20.5.3  typedef用于函数指针》》》》》

结构void*)(int &int &)有点繁琐,可以使用typedef来简化,方法是声明类型VPF,它是一个指针,指向返回值类型为void且接受两个引用参数的函数:

typedef  void(*VPF) (int &,int &);

然后,将变量pFunc的类型声明为VPF

    VPF  pFunc

再将成员函数printVals( )声明为接受三个参数(一个VPF和两个int引用):

void  printValsVPF pFuncint &int &);

记住,typedef创建同义词,唯一的差别就是可读性更强;

 

20.5.4成员函数指针》》》》》

目前为止,创建的所有函数指针都指向非成员函数,其实也可以创建指向类成员函数的指针。

要创建成员函数指针:可使用与创建函数指针相同的语法,但需要包含类名和作用域运算符(::)。因此,如果pFunc指向类Shape的一个成员函数,它接受两个int参数且返回类型为void,那么pFunc的声明如下:

void  Shape::*pFunc)(intint);

用法和函数指针相同,只是需要通过相应的对象来调用。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

                              这里通过指针ptr来访问对象;通过指针

  pFunc来访问函数;

  delete  ptr;没有理由这样做,因为他是一个指向代码的指针,而不是指向堆中对象的指针。delete反而会出错

 

20.5.5成员函数指针数组》》》》》

像函数指针一样,成员函数指针也可以存储在数组中。可以用不同成员函数的地址来初始化数组,且可以用下标表示法来调用这些函数。

 

总结:  静态成员变量用于存储有关整个类的信息,还可用作同一个类的不同对象交换

信息的手段

友元函数让一个类能够将其私有成员变量和函数暴露给另一个类。虽然这些问题通常是通过基类和派生类之间的继承关系来处理的,但是使用友元函数可赋予继承层次结构外的类以访问特权。

问题! 在使用全局数据的情况下,为何还要使用静态数据?

答:静态数据的作用域为其所属类,因此,只有通过类的对象、通过使用类名的显式调用(如果静态数据时公有的)或通过静态成员函数,才能访问静态数据。静态数据在访问方面的限制和强制类型特征,使其比全局数据更安全。

在使用全局函数的情况下,为何还要使用静态成员函数?

        答:静态成员函数的作用域为其所属的类,只有通过类的对象或显式全限定才能调用它们:ClassName::FunctionName();

        为什么不将所有的类声明为它们使用的所有类的友元?

答:将类声明为另一个类的友元暴露了实现细节,降低封装程度。

每个静态成员有多少个备份?  只有一个备份供类的所有对象使用。

第二十一章  《使用C++0x新增的功能》

21.2空指针常量》》》》》

使用指针时一定要给他赋值,未初始化的指针可能指向内存的任何位置,这边是野指针。创建指针时应该讲空赋值给指针。可将它设置为0或者NULL(nullptr)

最好不要讲指针初始化为0,如果对于依赖于函数重载的类来说便会导致二义性。nullptr不会隐式地转换为整数,但可能隐式地转换为布尔值(false)。

21.3编译阶段常量表达式》》》》》

C++0X新增了常量表达式,这是使用了关键字constexpr实现的:

constexpr  int getCentury( ){  return  100;}

Microsoft  Visual Studio  10不支持这种关键字 constexpr;

任何使用constexpr声明的变量或函数都被隐式的视为常量;

21.4自动确定类型的变量》》》》》

auto并非C++新增的数据类型。变量的数据类型由编译器决定;

auto声明自动确定类型的变量时,必须对它初始化:不能使用它来声明数组的类型,也不能将其用作函数参数或返回值的类型,可将函数的返回值赋给使用auto声明的变量。关键字auto用于定义类或结构的成员变量,除非他是静态成员;用auto定义多个变量时,要求这些变量的数据类型相同。auto   a=5;  auto  b=10.5;  auto  c=a+b;   c=>>15.5;

21.5新的for循环》》》》》

这种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章  《创建模板》

23.1什么是模板》》》》》

模板是相对较新的C++功能,为这种问题提供了解决方案。不同于老式宏,模板是C++的邮寄组成部分,它是类型安全的,并且非常灵活。模板让你能够创建通用类,通过将类型作为参数传递给模板,可创建实例!

23.2模板实例》》》》》

模板让你能够告诉编译器如何创建任何数据类型的链表,而不是创建一组特定类型的链表。模板的实例化是:根据类创建对象或根据模板创建特定的类型;每个具体的类称为模板的实例

定义:要声明模板类List,可使用关键字template》》》》》

template<class  T>

class  List {  public:

List( );   //全类的声明在此处//

         };

所有模板类的声明和定义都以关键字template打头,接下来是模板的参数,它们是碎木板实例而已;

总结:在使用宏可行时为何要使用模板?

模板是类型安全的且是C++语言内置的。

模板函数的参数化类型和常规函数的参数有何不同?

常规函数对其接受的参数进行处理。模板函数让你能够参数化函数参数的类型

  什么时候使用模板?什么时候使用继承?

除了类处理的元素类型外,所有行为或几乎所有行为都不变时使用模板。如果需要复制代码,且只修改一个或多个成员的类型,应考虑使用继承。

当对象的类型不影响类中函数的行为时,使用模板。如:常见的堆、栈、队列;

对象的类型不影响类中函数的行为:堆、栈这些无论对象是什么,都少补了入栈,出栈等操作。并不改变函数的行为。

当对象的类型影响类中函数的行为时,就要使用继承。如:猫、人等;

对象的类型影响类中函数的行为:对于猫、人这些类来讲,每只猫品种不同,都有自己特定的行为习惯,总会跟其他种类的有所不同,

这也就导致模板不能满足每一种情况,所以,使用继承。用虚函数来实现每种猫在拥有猫的共性的同时,又具有自己的特性。

 

第24章 《处理异常和错误》

24.1 异常》》》》》

异常是一个对象,从发生问题的代码传递给处理问题的代码。发生异常称为引发,处理异常称为捕获。

异常的基本理念:1)资源分配(如分配内存或者锁定文件)通常是在程序的底 层进行的;

    2)操作失败(无法分配内存或者不能锁定文件)的处理逻辑 通常在程序高层,包含于用户交互的代码;

    3)异常提供一条从资源分配代码跳转到错误处理代码的快捷 路径不一定包含处理错误的代码。

如何使用异常》》》》》

对于可能发生问题的代码,应将其放在try块中。try块是一个用大括号括起来的代码块,其中的代码可能发生异常;

try {  someDangerousFunction ();}

catch块紧跟在try块湖面,负责对异常进行处理:

try{  someDangerousFunction();}

catchoutOfMemory{采取行动,从低内存状态恢复}

catchfileNotFound{take action when a file is not found}

使用异常的基本步骤:

(1)找出程序中可能引发异常的代码,并将其放在try块中;

(2)创建捕获这些异常的catch块,在其中执行清理工作,并将发生的情况告知用户。

  引发异常后,程序将立即转到当前try块后面的catch块执行;

24.4使用try块和catch块》》》》》

在确定try块的位置时,考虑在何处分配内存或资源。要监视的其他异常包括越界、无法输入等;

捕获异常》》》

引发异常后,将检查调用栈。调用栈是在程序的一部分调用另一个函数时创建的函数调用列表。

栈解退:异常沿调用向上传递给每个封闭快;栈解退时,将对栈中的局部对象调用析构函数,   将对象销毁。

注意:每个try块后面都有一个或者多个catch语句。如果异常与某个catch语句匹配,将执行该catch语句并认为异常已得到处理。若没有匹配到相应的catch语句,就将继续解退栈。

 

 

 

 

 

 

 

 

 

附件!!  《答案》

      http://cplusplus.cadenhead.org

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值