C++基本概念复习之一:枚举、引用、指针、类、多态

C++基本概念复习之一:枚举、引用、指针、类、多态

原文http://www.cnblogs.com/qzhforthelife/archive/2012/11/04/2753470.html

一、枚举

enum COLOR {RED,BLUE,YELLOW}

COLOR代表一种新类型,COLOR类型变量只能取上述四种值

COLOR c,现在看来,c既是COLOR类型,也是unsigned int。举例:

enum Days {Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Staturday};

Days today=Wednesday;

fi(today==Wednesday)

{

blahblah

}

不能给枚举变量直接赋unsigned int值,却能用枚举变量(常量)与任何整型值比较。

应用举例:


enum COLOR {BLACK,BLUE,YELLOW};
    COLOR c=BLACK;
    int i=3;
    if(c==i)//由于i为3不与任一枚举常量值相等,不打印3hahaha
    {
        cout<<"3hahaha"<<endl;
    }
    if(c==0)//虽然不能直接将整型值赋值给它,但能用它与数值进行比较
    {
        cout<<"0hahaha"<<endl;
    }
    i=3;
    switch(i)//不会有任何打印,因为枚举常量值都不与i匹配
    {
        case BLACK: cout<<"black color..."<<endl;break;
        case BLUE:  cout<<"blue color..."<<endl;break;
        case YELLOW:cout<<"yellow color..."<<endl;break;
    }
    i=3;
    switch(i)//当i=1或2时,都会找到匹配的枚举常量值,而i为其他值时我们默认选择黑颜色
    {
        case BLUE:  cout<<"blue color..."<<endl;break;
        case YELLOW: cout<<"yellow color..."<<endl;break;
        default:cout<<"black color..."<<endl;break;
    }
    //c=2;
    return 0;

二、引用

创建引用时必须给其赋值:int &r=someInt,之后对r和someInt的赋值操作都会互相影响,二者一致。

获取一个引用的地址:&r发现它和&someInt的值一样,这点和指针不同,我们可以用&获取一个指针本身的地址,而对引用这样操作则不能获取引用变量自身的地址,所获取的只是它所关联的变量的地址。引用一经初始化就不能再改变。当我们试图给引用重新赋值时,总是在操作目标变量:r=anotherInt,实际将anotherInt的值赋给了someInt,而并非将r与anotherInt关联起来——如前所言,引用一经初始化,就只能与一个变量关联。

按引用传递能提高效率,因为一般的按值传递会导致复制,当然指针的按址传递也能起到同样的作用。

如果可能,当优先使用引用,代码更简单易读。但是由于引用不能为空,且一经初始化就不能更改,所以使用时要注意:

Person *p=new Person(“张三”,23)

if(p!=null)

{

Person & rp=*p

………

}

三、指针

const int *pOne=&oneInt;//不能通过指针来修改oneInt(*pOne=12),但oneInt可以通过别的方式来修改

int * const pTwo=&twoInt;//指针本身不能修改即不能指向别的变量

const int * cont pThree=&threeInt//上述二者结合:指针不能修改,也不能通过该指针来修改其指向的变量,但变量依然能通过其他方式修改

应用:

void f(Person *p)

{

……….

}

对于上边的函数,在函数体内通过指针完全可以更改其指向的Person对象,而这又不是我们希望发生的,我们要确定该函数对我们的Person对象来说是只读的,为了起到保护作用,可以限制指针的行为:

void f( const Person *p)

{

……….

}

同理,const引用也能起到类似作用:

void f(const Person &refp)

{

……….

}

回头再说指针,同时,为了防止在函数体内p指向别处,可以更进一步限制指针的性质:

void f(const Person * const p)

{

……….

}

注意,引用一经初始化便不能再指向别的对象,所以对引用前加const限制是没意义的,这一点与指针不同,我们只需要限制引用的行为就行了:防止通过引用来更改其关联的对象。

四、类

声明类:

class Cat

{

unsigned int age;

unsigned int weight;

void meow();

};

上述代码仅仅是一个声明,并没有Cat类分配内存,仅仅是在告诉编译器Cat是什么,也即当用Cat定义对象时该对象会包含哪些数据具备什么功能会占多少内存。对于一个Cat对象,函数meow所占有的内存是meow指针,一个函数指针,32位平台占4个字节。

const成员函数:

一旦某成员函数这样声明了:

class Cat

{

void meow() const;

};

就意味着该成员函数不会修改任何成员变量,如果确实在方法实现内部这样做了,将会得到编译期的错误。一般而言,如果设计初衷是该成员函数不会也不应当修改成员变量,那么就应该将其声明为const,这样就会让编译期来维护我们的初衷,免得我们在实现函数的过程中遗忘此初衷带来的误操作。

类声明及方法实现的存放:

标准的做法是将类声明放在头文件中,类方法的实现放在同名,扩展名为cpp的实现文件中。比如,对于上述的类,会有两个文件Cat.h和Cat.cpp。要注意的是,在实现文件的开始要#include “Cat.h”,效果等同于将头文件的代码复制到此处。可能会奇怪,既然如此为何还要分成两个文件呢?注意,这不过是一种组织代码的方式,目的在于将实现分离到另一个文件。我们的类(包括声明及方法实现)写好之后,拿给客户使用的时候,客户并不想关心具体实现,他只需要看头文件便能知道该类的作用及意义。

防止重复包含:

看如下的文件结构:

****

基类所在文件Amimal.h:(类声明代码略)

****

子类Cat所在文件Cat.h:

#include “Animal.h”

class Cat:public Animal

{…};

****

子类Dog所在文件Dog.h:

#include “Amimal.h”

class Dog:public Animal

{…};

****

而main所在cpp文件同时使用到了上述两个子类,将会导致基类头文件被重复包含(因为上述两个子类都包含了基类头文件),又各个类的实现文件也都会包含头文件,从而增加了重复包含的可能。对于我们这个例子,重复包含导致的就是这样:使得最终的源代码中基类Animal声明了多次。重复包含是编译期错误。

解决办法:

将上述每个头文件都必定,举例,比如基类头文件当这样:

#ifndef ANIMAL_H(该符号名称并不重要,不过让预处器参考用的,但要保证多个头文件的这种符号名称不冲突)

#define ANIMAL_H

#endif

预处理器和编译器:

编译器,不过也是一段程序,用来编译我们的代码。在正式编译我们的代码之前,编译器有一部分代码的功能是这样的:对我们的代码进行预处理,然后再正式编译。程序员可以通过预处理指令(#开头)来控制代码在编译期仍可修改,修改后再正式编译。比如#include “Cat.h”就告诉编译器:先把Cat.h里的代码进来,然后编译器再读取这个被修改过的源代码文件再编译。

避免默认内联:

将方法的实现定义在类之外(定义在另一个文件中当然也就定义在了类体之外)有一个好处:避免了直接在类体内实现,从而使得成员函数自动成为内联的。要知道内联函数的致命之处在于:每次执行它时虽然能避免指令跳转的开销,但这是以复制代码为代价的:内联函数的每次调用,都会将函数的代码复制高用处,虽然避免了跳转,但要知道函数的,成员函数,的调用频率是很高的,如此一来,exe可能会变得极大。比如你在程序第1行、第12345行都调用了一个内联实现的成员函数,那么在编译期,编译器会提前将代码复制到这两个调用处,从而当程序运行,两处调用执行的都是各自的函数代码,避免了指令跳转带来的CPU开销。问题是,如果函数体很大呢?如果函数调用语句很多呢?后果无法想象。所以要避免内联函数的使用。

派生:

一个类继承另一个类时,父类当先声明,类实现(方法实现)可以放在后面。

给基类构造传参(调用基类构造):

Dog::Dog(int age,float speed):Mamal(age)

{

//这里仅仅是Dog子类的一个构造的简单实现

}

覆盖和隐藏:

基类已经有了一个函数,子类重写了该函数的实现。必须要保证子类的这个函数与基本的函数:名称,参数,const特性都完全一致,这才能保证是对父类的相应函数的覆写(覆盖),若有一个不一样,则仅仅是为子类增加了另一个不同的函数而已,比如仅仅函数名相同,则不过是一种重载而已。注意,不能通过函数返回类型来区别两个函数。

C++基本概念复习之一:枚举、引用、指针、类、多态

18行会打印出dog speak,说明覆盖确实发生了。第22行虽然是通过指针调用,但仍然不是多态。而20行,打印的是mamal speak——这一点,与java中的覆盖不太一样,java直接通过覆写就实现了多态:

C++基本概念复习之一:枚举、引用、指针、类、多态

 

接着说C++。当基类中有重载方法时,而我们只覆盖了其中一个,则其他同名的重载方法就都被隐藏了,后果是,通过子类对象来调用这些方法将会导致编译错误。如果也想通过子类来调用这些方法,必须对每个重载方法都覆盖。另一个解决办法是这样来调用:d.Mamal::speak(5)——假设父类有重载方法void speak(int)和void speak(),而子类只覆盖了无参的那个。

五、多态

在java中,只需要简单的覆盖(覆写),就能实现动态绑定和多态,而对C++ 实现多态的关键是:指针、引用、覆盖虚方法。

C++基本概念复习之一:枚举、引用、指针、类、多态

运行期间,根据pm指向的对象,去调用该对象的方法。引入虚方法后,使普通的覆盖发生了魔幻般的变化:调用谁的函数,能在运行期间决定!注意,必须要保证调用时使用的是父类类型的指针或者引用,才能保证多态!想通过父类类型的变量来达到多态多用,是不可能的,因为将子类对象赋值给父类变量的时候,有一个过程叫切除,即仅仅子类中属于父类的那部分留下了,子类所覆写的方法也压根不存在了,多态就更不可能了。另外,不能直接通过基类指针来访问子类的非虚方法——意思,只有像上面的代码那样,才能实现通过基类指针动态调用子类的函数,该函数(speak)是覆写父类虚函数的产物,当然也是一个虚函数,可以被子类的子类覆写下去。

父类的虚方法对应的子类的覆盖方法也是虚方法,一般而言,为了使这一点更明显增加代码可读性,在虚方法的覆盖方法前也加virtual声明。

虚析构函数的原理:

我们知道对一个指针执行delete操作时,会释放其指向的堆内存,即释放当初new操作请求的内存。比如Dog *pm=new Dog;当整个程序结束(比如上面的代码,当main结束的时候,pm指向的内存也释放了)或者对pm执行delete操作时,会释放pm指向的对象所占用的内存。我们知道,一个对象生命周期结束的时候,会调用自己的析构函数,在函数内部执行清理动作。将一个类的析构函数声明为虚函数的好处在于:

Mamal *m=new Dog;

delete m;

执行delete m,根据多态原理,会执行Dog类的析构函数,由于子类析构函数会自动父类析构函数,从而整个Dog对象会得到合理清理。非虚析构函数不能做到这一点。对于析构函数的多态,要求没那么严格:不需要类体系中的虚方法们同名——实际上也根本不可能,因为每个类的析构函数名称与类名相同。但必须满足一点:析构函数必须是虚函数,这样才能使得在delete一个指向后代类对象的父类指针时,利用多态,调用后代的析构函数,使其指向的对象得到合理清理——同时,还要确保父类的析构也得是虚方法。否则,即父类析构不是虚函数时,则delete一个指向子类对象的父类型指针的时候,得到调用的永远是父类析构,使得子类对象得不到合理清理——为了增加可读性,这时往往将子类也加上virtual声明,且子类加上virtual声明还有另一层意义:子类也可能作其他类的父类,这种情况下,其为虚函数是极其必要的,因为一旦涉及该子类的子类对象析构,子类析构当为虚方法。说了一大堆,总而言之,为了万无一失,一定要保证父类的析构是虚函数!——子类的析构默认都是虚函数,为了增加可读性,干脆将父类及子类的析构都声明为虚方法。

C++基本概念复习之一:枚举、引用、指针、类、多态

注意,上面代码中最终子类YellowDog的析构并未显示声明为虚的,但它也是虚析构,因为父类是虚析构函数。

编程经验:往往一个类中有虚函数,则意味着将来可能会动态绑定来使用,那么就会出现“父类指针(或者引用)指向子类对象”的情况,这种情况下为了子类能正常销毁,必须要保证继承链中的类的析构函数也得是虚函数。——所以我们的经验是,如果一个类中有虚函数,那么也将其析构函数定义为虚函数。对于有虚函数的类来说,及时释放内存是很有意义的,毕竟它里面维护着一个很占开销的v-table。

让(复制)构造函数有动态性:

严格说来,复制构造函数不能为虚函数,但是通过适当处理,也能达到动态调用的效果,即,用一个父类类型的指针,然后用该指针调用虚方法,通过动态绑定,则会调用相应子类对象的方法,该子类对象方法内部再调用自己的构造,从而,间接的实现了复制构造函数的动态调用。示例:


#include <iostream>

using namespace std;
class Mammal
{
    public:
        Mammal():itsAge(1){cout<<"Mammal constructor..."<<endl;}
        virtual ~Mammal(){cout<<"Mammal destructor..."<<endl;}
        Mammal (const Mammal & rm){cout<<"Mammal copy constructor..."<<endl;}
        virtual void speak() const {cout<<"Mammal speak!"<<endl;}
        virtual Mammal* clone(){return new Mammal(*this);}
        int getAge()const {return itsAge;}
    protected:
        int itsAge;
};
class Dog:public Mammal
{
    public:
        Dog(){cout<<"Dog constructor..."<<endl;}
        virtual ~Dog(){cout<<"Dog desctructor..."<<endl;}
        Dog(const Dog & rd){cout<<"Dog copy constructor..."<<endl;}
        void speak() const {cout<<"dog speak!"<<endl;}
        virtual Mammal* clone(){return new Dog(*this);}
};
class Cat:public Mammal
{
    public:
        Cat(){cout<<"Cat constructor..."<<endl;}
        virtual ~Cat(){cout<<"Cat desctructor..."<<endl;}
        Cat(const Cat & rd){cout<<"Cat copy constructor..."<<endl;}
        void speak() const {cout<<"Cat speak!"<<endl;}
        virtual Mammal* clone(){return new Cat(*this);}
};
enum ANIMALS {MAMMAL,DOG,CAT};
const int NumAnimalTypes=3;

int main()
{
    Mammal *theArray[NumAnimalTypes];
    Mammal *ptr;
    int choice,i;
    for(i=0;i<NumAnimalTypes;++i)
    {
        cout<<"0:Mammal,1:Dog,2:Cat>>";//输入非{0,1,2}默认选择Mammal
        cin>>choice;
        switch(choice)
        {
            case DOG:   ptr=new Dog;//1
                        break;
            case CAT:   ptr=new Cat;//2
                        break;
            default:    ptr=new Mammal;//0.3.4...
                        break;
        }
        theArray[i]=ptr;//最终theArray数组是一些Mammal类型的指针,指向的对象可能为三种类型
    }
    cout<<"*******************************"<<endl;
    Mammal *otherArray[NumAnimalTypes];
    for(i=0;i<NumAnimalTypes;++i)
    {
        theArray[i]->speak();//根据多态(动态绑定),会调用各自的speak

        /*根据多态,都会调用各自的clone方法,该方法调用复制构造,生成一个对象,并将
        该对象的指针返回,该指针赋给了父类类型的指针(虚函数的clone的返回类型是父类类型的指针)
        这样一来,otherArray数组里保存的指针是:父类类型的指针,但指向(可能)三种不同类型对象
        */
        otherArray[i]=theArray[i]->clone();
    }
    cout<<"*******************************"<<endl;
    for(i=0;i<NumAnimalTypes;++i)
    {
        otherArray[i]->speak();
    }
    return 0;
}



私有继承的意义:

有时候,子类和父类之间不存在“is-a”关系,但是又想让子类有父类的功能,私有继承便派上了用场:


#include <iostream>
using namespace std;
class God
{
    public:
        void createLight(){cout<<"上帝说要有光,于是有了光..."<<endl;}
    private:
        void dd(){cout<<"父类私有方法..."<<endl;}
};
class Puppet:private God
{
    public:
        void doSthByGod()
        {
            cout<<"傀儡不能直接干活...\t";
            createLight();
            //dd();父类中本来该方法就是私有的,子类代码完全不能访问
        }
};
int main()
{
    Puppet p;
    p.doSthByGod();
    //p.createLight();这样调用是不对的,私有继承虽然将父类的方法继承过来了,
    //也能在子类的代码中调用,但在客户端代码,通过子类对象是不能调用的
    //God *p=new Puppet;一个Puppet并非是一个God,私有继承会保证这一点,这行代码编译错误
    return 0;
}

(参考C++、java隐藏、覆盖、动态绑定,参考http://hi.baidu.com/cassietony/item/84ff14c98bf7e410ad092fb4