一文读懂C++的类和对象以及多态的原理

        现实生活中,关于类和对象最好的例子是自然界的动物类,本文将以此为场景逐步引入C++的概念,达到学习的目的。因为C++这门语言本身有很多繁杂的内容,而网上的资源也是参差不齐,有的人见山谈山遇水聊水,有多人故弄玄虚,好比孔乙己说茴香豆的回字有多少种写法,这更加让读者越来越晕。

一、类和对象的基本概念:

      场景引入: 基于动物都有年龄、性别,多数的动物都会吃东西的现实,可以设计一个动物类如下: 

#include<iostream>
using namespace std;

class Animal
{
public:
    int age;//年龄
    bool sex;//0:男,1:女
public:
    Animal(int age,bool sex)
    {
        this->age=age;
        this->sex=sex;
    }
    void eating()//吃东西
    {
        cout<<"aninmal is eating"<<endl;
    }
};
void test_fun1()
{
    Animal animal1(10,0);
    Animal animal2(10,1);
    animal1.eating();
    animal2.eating();
}

      成员分析:使用微软的开发者调试工具,在命令行输入:cl /EHsc -d1reportSingleClassLayoutAnimal main.cpp,查询单个类的布局如下:

       基于上面的认识,我猜想动物类在内存空间的布局如下:

        


    静态成员:
        现实生活中,总有一些属性属于所有对象共有,因此需要使用static关键字加以声明,因此动物类的定义修改如下:        

#include<iostream>
using namespace std;

class Animal
{
public:
    int age;//年龄
    bool sex;//0:男,1:女
    static int group_num;//公用静态成员
public:
    Animal(int age,bool sex)
    {
        this->age=age;
        this->sex=sex;
        group_num++;
    }
    void eating()//吃东西
    {
        cout<<"aninmal is eating"<<endl;
    }
    static void output_group();//静态成员函数

};
/*静态成员只能在类的外部进行初始化*/
int Animal::group_num =0;//公用的的静态成员在类外初始化
void Animal::output_group()
{
   cout<<"group num is:"<<Animal::group_num<<endl;
}

void test_fun1()
{
    Animal animal1(10,0);
    Animal animal2(10,1);
    animal1.eating();
    animal2.eating();
    Animal::output_group();
}

        使用微软的开发者调试工具,在命令行输入:cl /EHsc -d1reportSingleClassLayoutAnimal main.cpp,查询单个类的布局如下,对象的空间占用还是8个字节,但是动物1和动物2有一个共同的group_num变量。

 基于上面的认识,我猜想动物类在内存空间的布局如下:

二、类的继承和对象多态:

        普通继承:老虎和狮子都属于动物类,它们的生殖方式是自然受孕,区别于其他动物,因此需要增加一个成员变量。其次他们进食的方式也很有特点,喜欢吃其他的动物,因此增加独有的成员函数,如下:

#include<iostream>
using namespace std;
/*************************动物类**************************/
class Animal
{
public:
    int age;//年龄
    bool sex;//0:男,1:女
    static int group_num;//公用静态成员,种群数量
public:
    Animal(int age,bool sex)
    {
        this->age=age;
        this->sex=sex;
        group_num++;
    }
    void eating()//吃东西
    {
        cout<<"aninmal is eating"<<endl;
    }
    static void output_group();//静态成员函数

};
/*静态成员只能在类的外部进行初始化*/
int Animal::group_num =0;//公用的的静态成员在类外初始化
void Animal::output_group()
{
   cout<<"group num is:"<<Animal::group_num<<endl;
}
/*************************老虎类+狮子类**************************/
class Tiger:public Animal
{
public:
    int breed_type;//0:自然受孕
public:
    Tiger(int age,bool sex,int breed):Animal(age,sex)//初始化列表形式构造父类
    {
        this->breed_type =breed;
    }
    void eating()//吃东西
    {
        cout<<"Tiger is eating"<<endl;
    }
};

class Lion:public Animal
{
public:
    int breed_type;//0:自然受孕
public:
    Lion(int age,bool sex,int breed):Animal(age,sex)//初始化列表形式构造父类
    {
        this->breed_type =breed;
    }
    void eating()//吃东西
    {
        cout<<"Lion is eating"<<endl;
    }
};

void test_fun2()
{
    Lion lion(10,0,0);//公狮子
    Tiger tiger(10,0,1);//母老虎
    lion.eating();
    tiger.eating();
    Animal::output_group();
}

        使用微软的开发者调试工具,在命令行输入:cl /EHsc -d1reportSingleClassLayoutTiger main.cpp,查询单个类的布局如下,对象的空间占用都是12个字

节。

        菱形继承:在动物园里看到长的既像老虎,又像狮子的猛兽,俗称彪的动物,是母老虎和公狮子的杂交物种,用代码的表示方式如下,同时继承了老虎和狮子的基因。

        

#include<iostream>
using namespace std;
/*************************动物类**************************/
class Animal
{
public:
    int age;//年龄
    bool sex;//0:男,1:女
    static int group_num;//公用静态成员,种群数量
public:
    Animal(int age,bool sex)
    {
        this->age=age;
        this->sex=sex;
        group_num++;
    }
    void eating()//吃东西
    {
        cout<<"aninmal is eating"<<endl;
    }
    static void output_group();//静态成员函数

};
/*静态成员只能在类的外部进行初始化*/
int Animal::group_num =0;//公用的的静态成员在类外初始化
void Animal::output_group()
{
   cout<<"group num is:"<<Animal::group_num<<endl;
}
/*************************老虎类+狮子类**************************/
class Tiger:public Animal
{
public:
    int breed_type;//0:自然受孕
public:
    Tiger(int age,bool sex,int breed):Animal(age,sex)//初始化列表形式构造父类
    {
        this->breed_type =breed;
    }
    void eating()//吃东西
    {
        cout<<"Tiger is eating"<<endl;
    }
};

class Lion:public Animal
{
public:
    int breed_type;//0:自然受孕
public:
    Lion(int age,bool sex,int breed):Animal(age,sex)//初始化列表形式构造父类
    {
        this->breed_type =breed;
    }
    void eating()//吃东西
    {
        cout<<"Lion is eating"<<endl;
    }
};
/*************************狮虎兽类**************************/
class Biao:public Tiger,public Lion
{
public:
    int breed_type;//1:杂交
public:
    Biao(int age,bool sex,int breed):Tiger(age,sex,breed),Lion(age,sex,breed)//初始化列表形式构造父类
    {
        this->breed_type =breed;
    }
    void eating()//吃东西
    {
        cout<<"Biao is eating"<<endl;
    }
};
void test_fun3()
{
    Biao biao(10,0,0);//狮虎兽
    biao.eating();
    Animal::output_group();
}

        使用微软的开发者调试工具,在命令行输入:cl /EHsc -d1reportSingleClassLayoutBiao main.cpp,查询单个类的布局如下,对象的空间占用是28个字节,这个狮虎兽同时从老虎和狮子身上继承了age和sex,这产生了数据的冗余。另一方面来说,一个动物不可能有两个年龄,因此这样的继承方式有点不恰当。

        为了解决上述不恰当的继承方式,把老虎类和狮子类从动物身上继承的方式改成虚继承的方式,代码如下:

#include<iostream>
using namespace std;
/*************************动物类**************************/
class Animal
{
public:
    int age;//年龄
    bool sex;//0:男,1:女
    static int group_num;//公用静态成员,种群数量
public:
    Animal()//子类虚继承父类的时候,必须显示的定义一个无参构造函数
    {

    }

    Animal(int age,bool sex)
    {
        this->age=age;
        this->sex=sex;
        group_num++;
    }
    void eating()//吃东西
    {
        cout<<"aninmal is eating"<<endl;
    }
    static void output_group();//静态成员函数

};
/*静态成员只能在类的外部进行初始化*/
int Animal::group_num =0;//公用的的静态成员在类外初始化
void Animal::output_group()
{
   cout<<"group num is:"<<Animal::group_num<<endl;
}
/*************************老虎类+狮子类**************************/
class Tiger:virtual public Animal
{
public:
    int breed_type;//0:自然受孕
public:
    Tiger(int age,bool sex,int breed):Animal(age,sex)//初始化列表形式构造父类
    {
        this->breed_type =breed;
    }
    void eating()//吃东西
    {
        cout<<"Tiger is eating"<<endl;
    }
};

class Lion:virtual public Animal
{
public:
    int breed_type;//0:自然受孕
public:
    Lion(int age,bool sex,int breed):Animal(age,sex)//初始化列表形式构造父类
    {
        this->breed_type =breed;
    }
    void eating()//吃东西
    {
        cout<<"Lion is eating"<<endl;
    }
};
/*************************狮虎兽类**************************/
class Biao:public Tiger,public Lion
{
public:
    int breed_type;//1:杂交
public:
    Biao(int age,bool sex,int breed):Tiger(age,sex,breed),Lion(age,sex,breed)//初始化列表形式构造父类
    {
        this->breed_type =breed;
    }
    void eating()//吃东西
    {
        cout<<"Biao is eating"<<endl;
    }
};
void test_fun3()
{
    Biao biao(10,0,0);//狮虎兽
    biao.eating();
    Animal::output_group();
}

        使用微软的开发者调试工具,在命令行输入:cl /EHsc -d1reportSingleClassLayoutBiao main.cpp,查询单个类的布局如下,对象的空间占用是28个字节,此时就没有了数据冗余,因为从父类继承的是一个vbptr【虚基类指针】,这个指针指向了vbtable【虚基类表】,这个基类表记录基类数据的偏移量,此时不管以老虎的身份去访问,还是以狮子的身份去访问,得到的是同一份东西age和sex。

        对象的多态:在定义的类中,都有一个成员函数吃饭可是每个动物都要吃饭,作为工作人员真的很累,我喜欢他们就一起吃饭省事。        

#include<iostream>
using namespace std;
/*************************动物类**************************/
class Animal
{
public:
    int age;//年龄
    bool sex;//0:男,1:女
    static int group_num;//公用静态成员,种群数量
public:
    Animal()//子类虚继承父类的时候,必须显示的定义一个无参构造函数
    {

    }

    Animal(int age,bool sex)
    {
        this->age=age;
        this->sex=sex;
        group_num++;
    }
    void eating()//吃东西
    {
        cout<<"aninmal is eating"<<endl;
    }
    static void output_group();//静态成员函数

};
/*静态成员只能在类的外部进行初始化*/
int Animal::group_num =0;//公用的的静态成员在类外初始化
void Animal::output_group()
{
   cout<<"group num is:"<<Animal::group_num<<endl;
}
/*************************老虎类+狮子类**************************/
class Tiger:virtual public Animal
{
public:
    int breed_type;//0:自然受孕
public:
    Tiger(int age,bool sex,int breed):Animal(age,sex)//初始化列表形式构造父类
    {
        this->breed_type =breed;
    }
    void eating()//吃东西
    {
        cout<<"Tiger is eating"<<endl;
    }
};

class Lion:virtual public Animal
{
public:
    int breed_type;//0:自然受孕
public:
    Lion(int age,bool sex,int breed):Animal(age,sex)//初始化列表形式构造父类
    {
        this->breed_type =breed;
    }
    void eating()//吃东西
    {
        cout<<"Lion is eating"<<endl;
    }
};
/*************************狮虎兽类**************************/
class Biao:public Tiger,public Lion
{
public:
    int breed_type;//1:杂交
public:
    Biao(int age,bool sex,int breed):Tiger(age,sex,breed),Lion(age,sex,breed)//初始化列表形式构造父类
    {
        this->breed_type =breed;
    }
    void eating()//吃东西
    {
        cout<<"Biao is eating"<<endl;
    }
};

void Animal_eating(Animal *p)
{
     p->eating();
}

void test_fun4()
{
    Lion lion(10,0,0);//公狮子
    Tiger tiger(10,0,1);//母老虎
    Biao biao(10,0,0);//狮虎兽
    Animal_eating(&lion);
    Animal_eating(&tiger);
    Animal_eating(&biao);
    /*运行结果
     * aninmal is eating
     * aninmal is eating
     * aninmal is eating
     */
}

        但是程序运行的结果都是动物在吃饭,这不是我们想要看到的。此时只要在动物类的成员函数前面增加一个关键字 virtual void eating(),然后查看单个类布局,发现多了一个vfptr【虚函数指针】,这个指针指向了vftable【虚函数表】。C++规定,当父类的成员函数是虚函数的时候,子类实例化对象的时候必须要重新父类的函数,并且是以指针的方式调用。这是后程序运行正确了。

Lion is eating
Tiger is eating
Biao is eating

        多态分析:动物类中的虚函数,在动物实例中产生了一个虚函数指针,这个指针指向的时动物类的吃饭函数。老虎继承动物后,相同的成员变量都继承一份包括vfptr,但是这个vfptr赋值的时候给的时老虎类的吃饭函数。当用基类的指针指向子类的时,依然可以用父类的套路玩转成员函数,虽然看起来时玩的动物类,但是实际上玩的是老虎类。【这里不使用狮虎兽为例进行分析,原因就是复杂一点】

三、C++多态的拓展

        多态归纳:实现多态有3个条件:一是父类定义虚函数,二是子类重写虚函数,三是父类指针指向子类对象。实际上,采用引用的方式也是可以实现多态,下面的代码时等价的:

        

//method 1
void Animal_eating(Animal *p)
{
     p->eating();
}

//method 2
void Animal_eating(Animal &p)
{
     p.eating();
}

void test_fun4()
{
    Lion lion(10,0,0);//公狮子
    Tiger tiger(10,0,1);//母老虎
    Biao biao(10,0,0);//狮虎兽
    Animal_eating(&lion);
    Animal_eating(&tiger);
    Animal_eating(&biao);

    Animal_eating(lion);
    Animal_eating(tiger);
    Animal_eating(biao);
    Animal A;
    /*运行结果
        Lion is eating
        Tiger is eating
        Biao is eating
        Lion is eating
        Tiger is eating
        Biao is eating
     */
}

        引用调用:引用是一个别名,或者看成常量指针。当子类对象赋值给父类的引用对象时,就相当于完成了一个常量指针的初始化,只不过这个指针的类型或者外壳时父类的。因此在吃饭函数里面,虽然玩的父类,但是本质是玩的子类,原因在于在构造对象时期给vfptr赋予的是子类的函数地址。

void test_fun5()
{
     Animal InstA;
     Animal &QuoteA =InstA;
     Animal * const PointerA =&InstA; 
     /*
         InstA是一个对象;
         QuoteA是这个对象的别名,两者属于绑定的关系,打他就是打我。
         PointerA是一个常量指针,一旦赋值该对象就不允许许配给他人。            
     */       
}

        指针和引用:【参考https://www.cnblogs.com/haoyul/p/7282613.html】

        指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。

        而在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。

        引用传递和指针传递是不同的,虽然它们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针的指针,或者指针引用。

        注意事项:在发生多态的时候,父类指针是以父类的玩法,来玩转子类的成员函数,因此子类对象开辟在栈上面的内存,不会进行释放,也就是不会执行子类的析构函数。父类指针有点不负责任,只负责干事,就是个渣男。代码如下:

void test_fun6()
{
    Animal *p_tiger =new Tiger(10,0,1);
    p_tiger->eating();
    /*
      运行结果:
        tiger is eating
      严重后果:
        对象Tiger的内存空间没有释放
    */
}
class Tiger:virtual public Animal
{
public:
    int breed_type;//0:自然受孕
public:
    Tiger(int age,bool sex,int breed):Animal(age,sex)//初始化列表形式构造父类
    {
        this->breed_type =breed;
    }
    void eating()//吃东西
    {
        cout<<"Tiger is eating"<<endl;
    }
    ~Tiger()
    {
        cout<<"Call Tiger's desconstruc Fun"<<endl;
    }
};

        改进措施:此时将父类的析构函数改成虚函数的形式,那么就会先调用子类的析构函数,再调用父类的析构函数。

#include<iostream>
using namespace std;
/*************************动物类**************************/
class Animal
{
public:
    int age;//年龄
    bool sex;//0:男,1:女
    static int group_num;//公用静态成员,种群数量
public:
    Animal()//子类虚继承父类的时候,必须显示的定义一个无参构造函数
    {

    }

    Animal(int age,bool sex)
    {
        this->age=age;
        this->sex=sex;
        group_num++;
    }
    virtual ~Animal()//多态发生时,会先调用子类析构函数,再调用父类的析构函数
    {
        cout<<"Call Animal's desconstruc Fun"<<endl;
    }
    virtual void eating()//吃东西
    {
        cout<<"aninmal is eating"<<endl;
    }

    static void output_group();//静态成员函数
};
/*静态成员只能在类的外部进行初始化*/
int Animal::group_num =0;//公用的的静态成员在类外初始化
void Animal::output_group()
{
   cout<<"group num is:"<<Animal::group_num<<endl;
}
/*************************老虎类+狮子类**************************/
class Tiger:virtual public Animal
{
public:
    int breed_type;//0:自然受孕
public:
    Tiger(int age,bool sex,int breed):Animal(age,sex)//初始化列表形式构造父类
    {
        this->breed_type =breed;
    }
    void eating()//吃东西
    {
        cout<<"Tiger is eating"<<endl;
    }
    ~Tiger()
    {
        cout<<"Call Tiger's desconstruc Fun"<<endl;
    }
};

class Lion:virtual public Animal
{
public:
    int breed_type;//0:自然受孕
public:
    Lion(int age,bool sex,int breed):Animal(age,sex)//初始化列表形式构造父类
    {
        this->breed_type =breed;
    }
    void eating()//吃东西
    {
        cout<<"Lion is eating"<<endl;
    }
};
/*************************狮虎兽类**************************/
class Biao:public Tiger,public Lion
{
public:
    int breed_type;//1:杂交
public:
    Biao(int age,bool sex,int breed):Tiger(age,sex,breed),Lion(age,sex,breed)//初始化列表形式构造父类
    {
        this->breed_type =breed;
    }
    void eating()//吃东西
    {
        cout<<"Biao is eating"<<endl;
    }
};

void test_fun6()
{
    Animal *p_tiger =new Tiger(10,0,1);
    p_tiger->eating();
    delete p_tiger;
    /*
      运行结果:
        Tiger is eating
        Call Animal's desconstruc Fun
      严重后果:
        对象Tiger的内存空间没有释放
    */
    /*
     *       运行结果:
        Tiger is eating
        Call Tiger's desconstruc Fun
        Call Animal's desconstruc Fun

    */
}


void test_fun5()
{
     Animal InstA;
     Animal &QuoteA =InstA;
     Animal * const PointerA =&InstA;
     /*
         InstA是一个对象;
         QuoteA是这个对象的别名,两者属于绑定的关系,打他就是打我。
         PointerA是一个常量指针,一旦赋值该对象就不允许许配给他人。
     */
}


//method 1
void Animal_eating(Animal *p)
{
     p->eating();
}

//method 2
void Animal_eating(Animal &p)
{
     p.eating();
}

void test_fun4()
{
    Lion lion(10,0,0);//公狮子
    Tiger tiger(10,0,1);//母老虎
    Biao biao(10,0,0);//狮虎兽
    Animal_eating(&lion);
    Animal_eating(&tiger);
    Animal_eating(&biao);

    Animal_eating(lion);
    Animal_eating(tiger);
    Animal_eating(biao);
    Animal A;
    /*运行结果
        Lion is eating
        Tiger is eating
        Biao is eating
        Lion is eating
        Tiger is eating
        Biao is eating
     */
}

void test_fun3()
{
    Biao biao(10,0,0);//狮虎兽
    biao.eating();
    Animal::output_group();
}

void test_fun2()
{
    Lion lion(10,0,0);//公狮子
    Tiger tiger(10,0,1);//母老虎
    lion.eating();
    tiger.eating();
    Animal::output_group();
}

void test_fun1()
{
    Animal animal1(10,0);
    Animal animal2(10,1);
    animal1.eating();
    animal2.eating();
    Animal::output_group();
}

int main()
{
     test_fun6();
//     test_fun4();
//    test_fun3();
//    test_fun2();
//    test_fun1();

   return 0;
}

        因为只要是虚函数,就会产生虚函数表,这才是问题的本质。但是为何调用了子类的析构函数后,还会继续调用父类的析构函数,这是我不明白的地方,或许认为是类控制中心增添了一个特殊的标记,偷偷的干了这个事情,暂且只能这样解释。

四、类的继承和组合的区别

        例如一个动物园有多只动物,此时设计一个动物园类,那么动物园类是动物类的多个组合。两者不存在继承关系。但是有的人说动物园是动物的父类,这是让人很费解的一种说法。

class Zoo
{
public:
    Animal animal1;
    Animal animal2;

public:
    Zoo():animal1(),animal2()
    {}
};

欢迎志同道合的朋友们指正,谢谢。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值