c++符号表的作用、继承方法、多态

15 篇文章 0 订阅

问题1:如何通过变量名知道其地址?在引用的时候,我们知道那是nickname,那么两个变量,如何知道对应同一地址空间?

答:网上搜了几篇博客,有一篇讲的不错,推荐给大家:http://lishicongli.blog.163.com/blog/static/1468259020115154859706/。摘录其中比较关键的点:

编译器会生成一个叫做“符号表”的数据结构来维护变量名和内存地址直接的对应关系。它会搜集变量名,比如我们定义了一个全局的 int a; 那么编译器会为程序预留4个字节(32位平台)的空间,比如起始地址23456788(长度为4),并把变量名“a”和地址88888888保存进符号表,这样程序中对a进行相关操作时,它就会根据符号表找到变量的真正的物理位置(23456788),进行相关操作。 在机器执行程序的时候,会把变量名替换为内存地址(和长度),而不存在任何名称。

  • 符号表  存储  变量名和地址

 

问题2:C++ 继承方法

答:同样可以参考博客:https://www.cnblogs.com/33debug/p/6666939.html。摘录其中比较关键的点:

原始类称为基类,继承类称为派生类,它们是类似于父亲和儿子的关系,所以也分别叫父类和子类。而子类又可以当成父类,被另外的类继承。继承的方式有三种分别为公有继承(public),保护继承(protect)私有继承(private)

  • 定义格式如下:

  • 继承方式及访问属性

补充一个很重要的点:protected 成员和 private 成员类似,也不能通过对象访问,都得通过成员函数返回。但是当存在继承关系时,protected 和 private 就不一样了:基类中的 protected 成员可以在派生类中使用,而基类中的 private 成员不能在派生类中使用。

 

问题3:C++多态

可以参考博客:https://www.jianshu.com/p/02183498a2c2。这一块的内容讲的很细,建议看一下原作博客:

  • 面向对象的三大特性是封装继承多态
  • C++多态基于虚函数虚继承实现。

代码比较直观,所以把“多态的引入”这节的代码理解一下:

C++继承可以让子类继承另基类所包含的属性和方法,有时,子类虽继承了基类,却有些方法存在自己的实现。我们看下面这样一个例子,两个类动物(Animal)和人(Human)。Human继承了Animal,Animal有呼吸方法,Human也有呼吸方法。代码如下:

#include <iostream>

using namespace std;

class Animal {
public:
    char *name;
    void breathe() {
        cout << "Animal breathe" << endl;
    }
    virtual void eat() {
        cout << "Animal eat" << endl;
    }
};

class Human: public Animal {
public:
    int race;
    void breathe() {
        cout << "Human breathe" << endl;
    }
    void eat() {
        cout << "Human eat" << endl;
    }
};

int main(void) {
    // 用实例调用
    Animal a;
    Human h;
    a.breathe();
    a.eat();
    h.breathe();
    h.eat();

    cout << endl;

    // 用基类指针调用
    Animal *aPtr = NULL;
    aPtr = &a;
    aPtr->breathe();
    aPtr->eat();
    aPtr = &h;
    aPtr->breathe();
    aPtr->eat();
    return 0;
}

输出结果:

Animal breathe
Animal eat
Human breathe
Human eat

Animal breathe
Animal eat
Animal breathe
Human eat

首先我们对一个Animal实例和一个Human实例分别调用breathe方法和eat方法,结果如我们所想要的,各自调用了各自的实现。

但我们知道,基类的指针可以指向子类,因为有时候我们为了让代码更通用,会用一个更通用的基类指针来指向不同的实例。在例子中,我们发现,对breathe方法,基类指针并没有调用具体实例所属Human类的实现,两次输出都是“Animal breathe”,而对eat方法,基类指针调用了所指向的实例所属Human类的实现,两次输出分布是“Animal eat”和“Human eat”。这就是引入虚函数的基本情况。

对于没有被声明成虚函数的方法,比如这里的breathe,代码中对于breathe方法的调用在编译时就已经被绑定了实现,绑定的是基类的实现,此为早绑定。对于被声明成虚函数的方法,比如这里的eat,代码中对于eat方法的调用是在程序运行时才去绑定的,而这里的基类指针指向了一个Human类的实例,它会调用Human类的eat方法实现。那么它是如何做到调用具体类的实现而非基类的实现呢?

虚函数表

我们来观察一下类的内存分布,大部分编译器都提供了查看C++代码中类内存分布的工具,在Visual Studio中,右击项目,在属性(Properties)-> C/C++ -> 命令行(Command Line)-> 附加选项(Additional Options)中输入/d1 reportAllClassLayout即可在输出窗口中查看类的内存分布。对于上述代码中的Animal类和Human类,内存的分布如下:

1>  class Animal    size(8):
1>      +---
1>   0  | {vfptr}
1>   4  | name
1>      +---
1>
1>  Animal::$vftable@:
1>      | &Animal_meta
1>      |  0
1>   0  | &Animal::eat
1>
1>  class Human size(12):
1>      +---
1>   0  | +--- (base class Animal)
1>   0  | | {vfptr}
1>   4  | | name
1>      | +---
1>   8  | race
1>      +---
1>
1>  Human::$vftable@:
1>      | &Human_meta
1>      |  0
1>   0  | &Human::eat

对于有虚函数的类,它在类内存的开始有一个指针指向虚函数表,虚函数表中包含了基类中以virtual修饰的所有虚函数。在基类Animal中,虚函数表中的eat指向的是Animal::eat,而在子类Human中,虚函数表中的eat指向的是Human::eat,因而在使用基类指针调用实例方法时,会调用虚函数表中的函数,也就是具体实例所属类的实现。

由上面的内存分布可以看出:

  1. 一个类中的某个方法被声明为虚函数,则它将放在虚函数表中。
  2. 当一个类继承了另一个类,就会继承它的虚函数表,虚函数表中所包含的函数,如果在子类中有重写,则指向当前重写的实现,否则指向基类实现。若在子类中定义了新的虚函数,则该虚函数指针在虚函数表的后面(如Human类中的breathe,在eat的后面)。
  3. 在继承或多级继承中,要用一个祖先类的指针调用一个后代类实例的方法,若想体现出多态,则必须在该祖先类中就将需要的方法声明为虚函数,否则虽然后代类的虚函数表中有这个方法在后代类中的实现,但对祖先类指针的方法调用依然是早绑定的。(如用Animal指针调用Asian实例中的breathe方法,虽然在Human类中已经将breathe声明为虚函数,依然无法调用Asian类中breathe的实现,但用Human指针调用Asian实例中的breathe方法就可以)。

多继承

现在假设这样一个例子,有LandAnimal(陆生动物)类和Mammal(哺乳动物)类,它们都有breathe和eat方法,都被声明成虚函数。Human类继承了LandAnimal类和Mammal类,同时Human类重写了eat方法。代码如下:

#include <iostream>

using namespace std;

class LandAnimal {
public:
    int numLegs;
    virtual void run() {
        cout << "Land animal run" << endl;
    }
};

class Mammal {
public:
    int numBreasts;
    virtual void milk() {
        cout << "Mammal milk" << endl;
    }
};

class Human: public Mammal, public LandAnimal {
public:
    int race;
    void milk() {
        cout << "Human milk" << endl;;
    }
    void run() {
        cout << "Human run" << endl;
    }
    void eat() {
        cout << "Human eat" << endl;
    }
};

int main(void) {
    Human human;

    cout << "用LandAnimal指针调用human实例的方法" << endl;
    LandAnimal *laPtr = NULL;
    laPtr = &human;
    laPtr->run();

    cout << "用Mammal指针调用human实例的方法" << endl;
    Mammal *mPtr = NULL;
    mPtr = &human;
    mPtr->milk();

    return 0;
}

运行的结果如下,可以看出,对于重写了的milk和run方法,通过基类指针的调用会指向实例所属类的实现:

用LandAnimal指针调用human实例的方法
Human run
用Mammal指针调用human实例的方法
Human milk

类的内存结构如下:

1>  class LandAnimal    size(8):
1>      +---
1>   0  | {vfptr}
1>   4  | numLegs
1>      +---
1>
1>  LandAnimal::$vftable@:
1>      | &LandAnimal_meta
1>      |  0
1>   0  | &LandAnimal::run
1>
1>  class Mammal    size(8):
1>      +---
1>   0  | {vfptr}
1>   4  | numBreasts
1>      +---
1>
1>  Mammal::$vftable@:
1>      | &Mammal_meta
1>      |  0
1>   0  | &Mammal::milk
1>
1>  class Human size(20):
1>      +---
1>   0  | +--- (base class Mammal)
1>   0  | | {vfptr}
1>   4  | | numBreasts
1>      | +---
1>   8  | +--- (base class LandAnimal)
1>   8  | | {vfptr}
1>  12  | | numLegs
1>      | +---
1>  16  | race
1>      +---
1>
1>  Human::$vftable@Mammal@:
1>      | &Human_meta
1>      |  0
1>   0  | &Human::milk
1>
1>  Human::$vftable@LandAnimal@:
1>      | -8
1>   0  | &Human::run

可见,对于多继承的情况,子类会包含多个基类的内存结构,包括多个虚函数表,若子类中重写了基类种被定义为虚函数的方法,则虚函数表中的函数指针指向子类的实现,否则指向基类的实现。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值