C++ +小结2 继承, 多态和引用

来源:互联网 发布:淘宝专卖店是正品吗 编辑:程序博客网 时间:2024/05/22 13:16
#include <iostream>using namespace std;// 继承机制class Animal {public:// 如果在父类中只定义了一个有参数的构造函数,而没有显示定一个无参数的构造函数// 那么当继承的子类实例化对象来调用相应的方法时, 则会发生因找不到一个合适的构造函数来实例化// 而发生错误, 因为子类调用时, 会先调用父类的构造函数,之后再调用子类的构造函数// 之后再调用相应的方法来完成相应的动作, 现在是只定义了一个有参数的构造函数// 所以找不到一个合适的无参数的构造函数来初始化, 那怎么办? // 那么此时可以在子类的构造函数后面加上父类的构造函数,并填入一些参数帮助其初始化即可//填入的这些参数只要符合其数据类型即可// 例如: Fish(): Animal(300,300) 这样即可Animal( int height, int weight){cout<<"Animal construct"<<endl;}/*Animal(){cout<<"Animal construct"<<endl;}*/// 当子类调用相应的方法时, 先调用子类的析构函数 再调用父类的构造函数~Animal(){cout<<"Animal deconstruct"<<endl;}void eat();//protected:void sleep(){cout <<"animal sleep"<<endl;}virtual void breathe(){cout<<"animal breathe"<<endl;}// 纯虚函数 test()//virtual void test()=0; // 那么此时Animal 就是一个拥有纯虚函数的类, 则不能被实例化了, //并且此时如果继承它的子类没有实现这个test()纯虚函数的话, 则子类也变成了纯虚函数不能被实例化//纯虚函数被标明为这种不具体实现的虚成员函数, 纯虚函数可以让类先具有一个操作的名称, 而没有具体的操作内容//让派生类在继承的时候,再具体给出它的定义};class Fish : public Animal  // : public Animal 表示fish 是按public 方式继承Animal{/*void eat(){}void sleep(){} void breathe(){}*///void test()//{//sleep();//breathe();//}public:Fish():Animal(300,33),a(1){cout<<"Fish construct"<<endl;}~Fish(){cout<<"Fish deconstruct"<<endl;}// 如果一个类中有常量的话, 则必须进行初始化, 那么初始化是在构造函数的后面进行//初始化即可void breathe()  // 这里称之为函数的覆盖即重写(override) 发生在父类和子类之间的 ,而函数重载(overload)是发生在一个类中{//那么现在子类 override父类的函数, 但是如果要想还拥有父类中这个方法的函数功能,那怎么办呢?  加入下面这一句即可 Animal :: breathe();sAnimal::breathe();cout<<"fish bubble"<<endl;}private :const int a;};// 继承机制中, class  Fish : public Animal 则是表示子类Fish 按public访问权限继承父类Animal// 子类可以继承父类中 public 和protected的成员和函数, 不能继承父类中的private成员和函数// 类中的protected 和private 成员和函数在类的外部不能通过实例化变量来访问void fn(Animal *pAn){pAn->breathe();}void Animal::eat(){cout<<"animal eat"<<endl;}//void change(int *pA, int *pB)//{////}//void change(int &a,int &b)//{}//void main()//{//Animal an(200,100);//an.eat();//Fish fh;////fh.sleep();//fh.sleep();//fh.breathe();//////Animal *pAn;//pAn=&fh; //这里将fish的地址隐式转换为Animal 了//fn(pAn); // 这里最终调用了Animal 的breathe(), why?//// 如果要调用fish中的breathe()怎么办/ z可以在Animal的breathe()前面加入一个virtual  这个叫做多态性////这个多态性是什么意思? 如果传递的是子类的地址,当在调用方法时 发现父类中的这个方法是virtual 方法时 则查看子类中是否有这个方法,////如果有则调用子类中的这个方法, 如果子类中没有这个方法则调用父类中的这个方法。////纯virtual函数 当含有纯虚函数的类是不能被实例化的//// fh.sleep() fh 调用sleep()时, 则先调用父类的构造函数,再调用子类的构造函数再调用子类的方法////////引用 就相当一个变量的别名///*int a = 6; //int &b = a;  */// 这里的int &b  是定一个了int型的引用b 引用在定义的时候就需要被初始化,这里//// int &b = a 即 让b 成为a的引用, 特别注意这个引用必须是在定义的时候进行初始化,不能这样://// int &b ; b =a; 这样是错误的////b = 5;  // 此时a也变成了5////int c = 7;////b =c;  // 这里不是说让c成为b的引用, 引用一旦在定义的时候进行初始化它就维系在一个特定的目标上面//// b=c; 这操作仅仅是将c的值赋值给b, 此时a的值也变成了7.//// 指针变量存储的是一个地址值, 指针变量本身是需要内存空间来存放的, 而引用是不需要额外的内存空间//// 引用不需要占据内存, 就相当变量的别名//// 此外用引用有时候比用指针在概念上更加清晰一些 ////int x = 3;//int y = 4;////change(&x, &y); // 这里是到底是x,y 的值发生了改变还是x,y地址发生了改变///*change(x,y);*/  //这里采用了引用 则概念更加清晰些, void change(int &a, int &b)//getchar();//}

新建一个Win32 console App Empty Project, Add new Items  Main.cpp

首先定义一个Animal 的类,因为需要用到io所以

#include <iostream>

using namespace std;

 

class Animal

{

         public:

void eat()

{

         cout<<”Animal eat”<<endl;

}

void sleep()

{

         cout<<”Animal”<<endl;

}

void breathe()

{

         cout<<”animal breathe”<<endl;

}

}; // 注意要加分号

 

再定义一个类

class Fish

{

         public:

void eat()

{}

void sleep()

{}

void breathe()

{}

};

 

因为Animal的动作, Fish都需要,所以可以用Fish继承Animal,这样就可以不用写void eat(),void sleep() void breathe()就可以在Fish中调用Animal的这些方法了

Class Fish : public Animal

{}

进入main()来调用这些方法

void main()

{

         Animal an;

an.eat();

Fish fh;

Fish.breathe();

}

那么输出结果是: Animal eat , Animal breathe此时Fish.breathe()可以调用Animal中的breathe,Fish继承了Animal了的breathe()方法。

那么Animal 被称之为基类,父类, Fish 称之为派生类,和子类

如果在Animal 中声明一个protected的访问方式后,之后的成员函数和变量都是保护的,那么这些保护的函数和成员在子类中是可以调用,但是在子类之外的外部是不能够被访问的

class Animal{

public:

void eat()

{}

protected

void sleep()

{}

private:

void breathe()

{}

};

此时 void sleep()是受保护的,void breathe()则是Animal私有的只能在Animal这个类中可以访问,其他任务地方都不能被访问,但是说protectedsleep()方法是可以在子类中被访问的,例如下面的访问是可以的

Class Fish : public Animal

{

         void test()

{       

void  sleep();

//但是breathe()private所以在Fish这里是不能被访问的

}

 

public的成员函数和变量是可以在任何地方被访问的,而protected的函数和变量只能在类Animal本身和其子类Fish中被访问,在这些类之外的其他地方都是不可访问的, Animal类中的private成员函数和变量则只能在Animal类中被访问,出了Animal这个类则不能被访问

}

 

void main()

{

         Animal an;

         an. eat(); // 因为eat()public类型,所以在任何地方都可以被访问,即使是在Animal类的外部也是可以的

 an.sleep(); // 这是错误的,因为sleepprotected的类型,只能在其类Animal中和继承Animal的子类Fish这两个类中被访问,其它地方都是不充许被访问的

an.breathe(); // 这个错误的,因为breathe()Private只能在其类Animal内部被访问其它地方都是不可被访问的

Fish fh;

fh.eat();  // 这个是可行的,因为Fish继承了Animal类,则可以访问Animal类中的publicprotected类的成员变量和函数,而public类型的变量和函数则可以在任何地方被访问

fh.sleep(); // 这个是错误的,因为sleepprotected的类型,所以只能在其在的类和继承其所在的类的子类中被访问, Fish虽然继承了Animal但是Fish只能在自己Fish这个类中可以访问其父类Animal中的protected的成员函数和变量,在Fish类的外部则不能访问其父类的protected的成员变量和函数,那么如果Fish类需要在自己的外部去访问其父类的protected的成员函数和变量,则可以通过将父类的protected的成员函数和变量封装到自己的public成员函数和变量中,再从外部来访问,例如Fish要在外部来访问其父类Animalprotected的成员函数sleep()可以通过访问fh.test()函数来间接访问sleep()函数。

}

上面的继承是采用的public方式来继承的

class Fish : public Animal  那么如果采用protected private 来继承会怎么样?

基类的访问权限

类的继承特性

子类的访问权限

Public

Protected

Private

Public

Public

Protected

No access

Public

Protected

Private

Protected

Protected

Protected

No access

Public

Protected

Private

Private

Private

Private

No access

 

如果在Animal中增加一个Animal的构造函数

Animal()

{

         cout<<”Animal construct”<<endl;

}

 

然后在Fish 中也构造一个构造函数

class Fish : public Animal

{

public :

         Fish()

         {

                   cout<<”Fish construct”<<endl;

}

}

 

void main()

{

         Fish fh;

}

运行的时候是什么结果呢? Fish实例化一个对象时,回去调用自己的构造函数,那么它会不会去调用其父类的构造函数呢?会的话,是先调用父类的构造函数还是先调用自己的狗构造函数呢?

结构是 animal construct , fish construct

Fish 实例化一个对象是会先调用其父类的构造函数而后再调用自己的构造函数

因为Fish 继承了Animal内部的成员变量和方法,那么当然在构造的时候这个基类就要首先先构造,因为构造函数的重要作用就是创建对象,并为对象分配内存,也可以负责初始化值,那么Fish继承了Animal内的成员变量和方法,如果不先构造基类的构造函数,那么子类Fish要调用Animal类中的函数和变量时,则因为基类还没有构造,没有创建对象,分配内存,没有初始化变量,则无法被子类来调用。所以要先构造基类的构造函数,再构造子类的构造函数,也就是说没有父亲就没有孩子。构造函数负责创建对象,并为其分配内存,那么这些变量,函数生命周期结束时就应该收回其先前申请的这些内存,则要用到析构函数来回收,分配在AnimalFish类中定义其各自的析构函数

public:

~Animal()

{

         cout<<”Animal deconstruct”<<endl;

}

 

public:

~Fish()

{

         cout<<”Fish deconstruct”<<endl;

}

void main()j

{

         Fish fh;

}

 

那么析构的时候是子类先析构,父类后析构

即:

Animal Construct

Fish construct

Fish deconstruct

Animal deCosntruct

 

现在在Animal构造函数中加入带参数的值

Animal(int height , int weight)

{

         cout<<”Animal construct”<<endl;

}

 

其它都不变,运行会报错:

‘Animal’ : no appropriate default constructor available

这是什么原因呢?

对于一个类来说,当自己构建一个构造函数,不管是带参还是不带参,C++编译器都不会再给提供默认的无参构造函数了,此时当构造Fish时, Fish是从Animal派生出来的,所以当Fish实例化对象时,要先构造基类的构造函数,再构造自己的构造函数,那么当它去构造基类的构造函数时,它会去调用这个缺省的Animal的构造函数,这个缺省的Animal的构造函数就是不带参数的构造函数,可是上面在Animal中提供了一个带参数的构造函数,而因为有了这么一个带参数的构造函数, C++编译器不再提供缺省的无参构造函数,所以当Fish 要来构造这个基类的无参构造函数,则找不到这个无参构造函数,因此报错提示没有适合的缺省的构造函数可以利用,那此时怎办?不可能再到Animal中加上一个不带参数的构造函数,因为就是想要在构造Animal时传递它的height, weight参数。那么此时可以在子类Fish的构造函数后面加上一个冒号,写上Animal的构造函数并传入两个参数,这两个参数只要是int型就型,即:

public:

Fish() : Animal(300,300)

{

         cout<<”Fish construct”<<endl;

}

这也是从子类中向基类传递参数的一种方式

Fish 去构造Fish()这个构造函数时,它就会去调用其后面的那个Animal(300,300)构造函数来构造基类,从而给带参数的Animal构造函数传递300300。这样就可以成功构造Animal的对象,Animal对象成功构造之后,Fish对象也可以成功构造了。那么这种方式也用在一个类中定义了常量时,需要去初始化它们可以使用的方式,例如在Fish中定义了一个常量,那怎么去初始化呢?

class Fish : public Animal

{

         public:

         Fish() : Animal(200,200),a(1)

{

         cout<<”Fish construct”<<endl;

}

private:

const  int a;

};

那么在Fish中定义的常量a可以在Fish()构造函数后面来初始化其值即在Animal(30,30)后面加一个逗号,赋值即可,如:Animal(20,20) , a(1)

Fish 类中再定义一个成员函数 void breathe()

void breathe()

{

cout<< “Fish bubble”<<endl;

}

 

void main()

{

         Fish fh;

fh.breathe();

}

Fish对象fh来调用breathe()时会输出Fish bubble

此时 Fish Animal这两个类中breathe()方法,不过是从返回值还是参数来讲都是一样的,这个在C++中叫做函数的覆盖(override

函数的覆盖(override)是发生在父类和子类之间的,函数的重载(overload)是发生在一个类中的。

Fish 中的breathe()方法既要实现父类Animal中的breathe()功能,又要自己创新一下,则可以将Animal的中breathe()方法写入Fishbreathe()中即可,例如:

void breathe()

{

         Animal breathe();

         cout<<”Fish bubble”<<endl;

}

 

现在来构建一个全局的函数 void fn(Animal *pAn),然后再来调用breathe()方法

void fn(Animal *pAn)

{

         pAn->breath();

}

 

然后在main()中构造一个Animal的指针*pAn,同时构造一个Fish的对象fh,并且让fh的地址赋值给Animal的这个指针 pAn,然后把这个pAn指针传入fn(Animal *pAn)中。

void main()

{

Fish fh;

Animal *pAn;

pAn = &fh;

fn(pAn);

getchar();

}

那么结果会是什么呢?结果会输出:Animal breathe.为什么会这样呢?

首先在这里, pAn= &fh是将Fish的地址转换为一个Animal的指针,那么类型转换是否能够成功,主要是看这个内存模型。例如一个char类型和一个int类型的转换

一个char类型占用一个Byte,一个int类型占用4Byte,例如:

char ch; int i;

那么ch是占据一个Byte的内存空间,模型如下: ch

1Byte

i占据4Byte的内存空间,模型如下:i

1Byte

1Byte

1Byte

1Byte

现在如果想把i转换为char ch = (char) i;

那么就会把i的这个内存模型往ch的内存模型上扣,但是只能将i中的第一个Byte放到ch中,其后面的三个Byte放不下,就会发生数据类型的截断,造成丢失,这样就是所谓的精度丢失。

如果要把char型转换为int型也是将其内存模型往上扣,但是只用了一个Byte,所以char型转为int不会造成丢失。下面来Fish对象的内存布局

1.       this 指针指向Fish 对象的地址

2.       Fish 对象的内存:

Animal对象内存

Fish 继承部分

因为Fish 是继承Animal类,所以在产生一个Fish的对象fh时,它首先要去构造一个Animal对象,即Fish的内存布局部分是先构造Animal的内存对象,紧接着去构造Fish继承的其他部分,那么指向Fish这个对象的隐含的指针this所指向的地址既是Animal对象的地址,又是Fish对象的地址,(因为Fish构造时要先构造Animal对象),所以要将Fish的地址取出来转换为Animal对象的地址是可以的,因为他们的内存模型是匹配的。pAn = &fh 虽然要人感觉是把fh地址赋值给了pAn,但实际上,但是从Fish的内存布局来看,同时也是Animal的对象的地址,所以当调用breathe()方法时,则调用了Animal对象的breathe()方法。所以结果输出的是Animal breathe ,

但是有些情况下就是想要调用Fishbreathe()方法,那怎么办呢?可以在Animal类中的breathe()方法前面加上一个virtual关键字,将其定义为一个虚函数。此时再编译则会调用Fish breathe()方法,这就叫做做多态性。这个多态性是什么意思?即如果传递是子类的地址,当调用方法时,发现父类中的这个方法是virtual方法时,则查看子类中是否有这个方法,如果子类中有这个方法的实现则调用子类的这个方法,如果没有则调用父类中的这个方法。

即:当C++编译器在编译的时候,发现Animal类的breathe()方法是虚函数,这个时候C++就会采用迟绑定技术(late binding),在运行时,依据对象的类型(在本程序中,传递的是Fish 类对象的地址)来确认调用哪个函数,这就叫做C++的多态性。

C++中还有一种函数叫做纯虚函数(没有函数体),含有纯虚函数的这中类叫做抽象类,抽象类是不能实例化的,如:

Class Animal

{

         public:

         virtual void breathe()=0;

};

那么含有breathe()这个纯虚函数,则Animal这个类是抽象类,如果继承它的子类也没有实现它父类中的这些纯虚函数,则子类也成为了纯虚函数,也就不能实例化对象。

纯虚函数被标明为不具体实现的这种虚成员函数。纯虚函数可以让类先具有一个操作的名称,而没有具体的操作内容,让派生类在继承的时候再具体的去实现。

 

引用部分

一个引用实际上就是一个变量的别名

定义一个变量a, 再定义一个引用b ,并让b成为a的一个引用,当改变b时,a的值也就改变了

int a =6;

int &b =a;

b = 3; 那么此时a的值也变成了3,引用需要一个变量或者说一个对象来初始化它自己,引用必须在定义的时候被初始化。

int c = 7;

b = c; 此时并不是说让b成为c的引用,而只是把c的值赋值给了b,此时b的值也编程了c的值也就是7 a的值也变成了7,引用一旦在定义的时候进行了初始化,就维系在一个特定的目标上。

引用和指针的内存模型的区别

&号只有在定义的时候才是定义引用,在其他地方是取址符。

b成为a的引用,在引用的内存模型中可以看到, a=5的内存地址为0012FF7C,那么引用b所指向的地址也是这个地址,但是注意引用b不需要自己的内存地址,也就是说不需要额外的内存来存放引用b,而指针本身就是地址,指针变量要存储地址值,所以指针变量本身需要有内存空间,通常引用是在函数传递参数这个环节中,引用的使用可以避免内存的拷贝,此外用引用可以是概念更加清晰

例如:要交换x,y的值,那么要想让它们交换之后生效,怎要么传递一个指针,要么传递一个引用

int change (int *pA, int *pB)

{

        

}

那么此时要传入地址

change(&x, &y);

那么此时到底是x,y的地址发生了改变了还是x,y的值发生了交换?不是含清晰

如果传入引用,

int change(int &a, int &b);

change(x, y)

那么就可以清晰的知道是交换x,y的值。

 

从上面可以看到在Main.cpp中定义了两个类Animal Fish ,然后其函数的实现和调用都放在了这个Main.cpp中,如果要定义的类多又复杂的时候,则不便于维护,那怎么办呢?通常的做法是一个类定义自己的文件,来各自管理,然后定义一个主函数文件来集中调用,而每个类在定义的时候,为了化复杂为更简单呢,通常把声明部分发在头文件中,把定义部分放在主文件中

例如上面我们可以新建Animal.h来声明Animal的中成员函数和变量,然后再新建一个Animal.cpp来实现其成员函数和初始化其值。例如:

Animal.h

 

class Animal

{

         void eat();

};

 

Animal.cpp

#include “Animal.h”

Animal::Animal(int height,int weight)

{

         cout<<”Animal construct”<<endl;

}

void Animal:: eat()

{

         cout<<”Animal eat”<<endl;

}

 

Fish.h

因为要继承Animal所以要包含Animal.h

#include “Animal.h”

class Fish : public Animal

{

public:

         Fish();

        

};

 

Fish.cpp

#include”Fish.h”

这里要注意初始化基类中的有参构造函数

Fish ::Fish():Animal(200,200)

{}

void Fish:: breathe()

{

         cout<<”fish bubble”<<endl;

}

 

然后定义一个主文件,来集中调用,因为要用到Fish , Animal中的相关方法,所以要

#include “Ani mal.h”

#include”Fish.h”

#include <iostream>

usin g namespace stdl;

 

void main()

{

         Animal an;

Fish fh;

}

这样在运行的时候会报错说重复定义,这是因为在主文件中,同时#includeAnimal.hFish.h这两个头文件,那么在运行时,执行完#include “Animal.h”时,是把Animal.h中的代码拷贝到了这个主文件#include”Animal.h”这个位置替换了#include”Animal.h”,

那么当执行#include”Fish.h”时,因为Fish.h中也#include”Animal.h”所以又会去拷贝一份来,所以就会报运行重复定义的错误,可以通过下面的预编译指令来避免发生

#ifndef   XXXX

#define   XXXX

class Animal

{};

#endif

那么把Animal.h改成:

#ifndef ANIMAL_H_H

#define ANIMAL_H_H

class Animal

{};

#endif

 

Fish.h改成

#include “Animal.h”

 

#ifndef FISH_H_H

#define FISH_H_H

class Fish : public Animal

{};

#endif

就可以解决此问题。

原创粉丝点击