C++中的继承

继承的概念大家应该都很熟悉,最近看《effective C++》的时候发现有一些有意思的特性以前没有了解过,以此为记。

公有继承
首先,最常用的就是公有继承:

class ExampleA {
public:
    virtual ~ExampleA() { std::cout << "A~" << std::endl; }
    virtual void func() { std::cout << "A" << std::endl; }
    int a = 0;
protected:
    int b = 1;
private:
    int c = 2;
};
class ExampleB : public ExampleA {
public:
    ~ExampleB() { std::cout << "B~" << std::endl; }
    void func() { std::cout << "B" << std::endl; }
    void test() { a; b; c; }                        // 无法使用基类的私有成员c
};
class ExampleC : public ExampleB {
public:
    ~ExampleC() { std::cout << "C~" << std::endl; }
    void func() { std::cout << "C" << std::endl; }
    void test() { a; b; c; }                        // 无法使用基类的私有成员c
};

int main()
{
    ExampleB* ex1 = new ExampleC;
    ExampleA* ex2 = new ExampleC;
    ExampleA* ex3 = new ExampleB;
    ExampleA* ex4 = new ExampleA;
    ex1->func();       // C
    ex2->func();       // C
    ex3->func();       // B
    ex4->func();       // A

    delete ex1;        // C~ B~ A~
    delete ex1;        // C~ B~ A~
    delete ex1;        // B~ A~
    delete ex1;        // A~

    return 0;
}

上面的代码有几个关键点:
- 在公有继承中,继承基类之后能够在类中使用基类的public和protected成员。
- 使用了虚函数,虚函数也是实现动态多态的关键。当继承了基类并重写了基类的虚函数,那么对象指针将会有动态多态的特性,就如同上面的代码,无论指针本身指向的是什么类型,都会调用到new的时候的类型成员函数,当然前提是类本身重写了该虚函数,如果没有重写,会自动向下调用到最先实现的那个类的成员函数。
- 虚析构函数,可以看到使用了虚析构后,在析构时会调用真实类型所继承的所有父类的析构函数,这样可以保证资源完整释放。而如果不定义虚析构,只会调用指针本身类型的析构函数和其父类的析构函数。
- 多重继承,“孙子类”同样继承了“爷爷类”的内容。

另外,还有纯虚函数virtual void func() = 0;,纯虚函数不允许实现,只能在派生类中进行重写,另外含有纯虚函数的类不能实例化。

那么,虚函数是怎么实现这种调用规则的呢?
我们先了解一下函数指针的概念:指针指向对象称为对象指针,指针除了指向对象还可以指向函数,函数的本质就是一段二进制代码,我们可以通过指针指向这段代码的开头,计算机就会从这个开头一直往下执行,直到函数结束,并且通过指令返回回来。函数的指针与普通的指针本质上是一样的,存储着内存的地址,这个地址就是函数的首地址。

而当类有虚函数时,就会有对应的虚函数表。虚函数表指针:类中除了定义的函数成员,还有一个成员是虚函数表指针,这个指针指向一个虚函数表的起始位置,这个表会与类的定义同时出现,这个表存放着该类的虚函数指针,调用的时候可以找到该类虚函数表,通过虚函数表的偏移找到函数的入口地址,从而找到要使用的虚函数。
当实例化一个该类的子类对象的时候,如果该类的子类并没有定义虚函数,在实例化该类子类对象的时候也会产生一个虚函数表,这个虚函数表是子类的虚函数表,但是记录的子类的虚函数地址与父类的是一样的。所以,此时通过子类对象调用虚函数就会调用到父类的函数。如果我们在子类中定义了从父类继承来的虚函数,对于父类来说情况是不变的,对于子类来说它的虚函数表与之前的虚函数表是一样的,但是此时它的虚函数表中这个函数的指针就会覆盖掉原有的指向父类函数的指针的值,此时再调用该函数就会调用自己的函数了。

然后,再讨论一下公有继承的意义和使用场景。公有继承能够调用基类的非私有成员并且能重载基类的虚函数实现动态多态,《effective C++》中将其描述为”是一个…”、”是一种…”,挺形象的。例如:

class Student{
public:
    virtual void learn(){}
protect:
    int height;
}
class Tom:public Student{   // 汤姆是一个学生
public:
    void learn(){}
}

class Bird{
public:
    virtual void egg() = 0;
}
class Sparrow:public Bird{  // 麻雀是一种鸟
public:
    virtual void egg(){}
}

私有继承
私有继承和公有继承的区别在于,继承的时候声明为private:class A:private B。再看个例子:

class ExampleA {
public:
    virtual void func() {}
    int a = 0;
protected:
    int b = 1;
private:
    int c = 2;
};
class ExampleB : private ExampleA {
public:
    void func() {}
    void test() { a; b; c; }                        // 无法使用基类的私有成员c
};
class ExampleC : public ExampleB {
public:
    void test() { a; b; c; }                        // 无法使用a、b、c
};

int main()
{
    ExampleA* a = new ExampleB;                    // 编译失败,B无法转换成A
    return 0;
}

上面的代码有几个关键点:
- 使用私有继承后,基类所有访问权限都会变成private。
- 私有继承无法转换成基类类型。也就是说,虽然A声明了一个虚函数且B重写了虚函数,但是在私有继承中无法实现动态多态。

私有继承能够调用基类的非私有成员但是不能实现动态多态,由此可见,私有继承和公有继承不同,并不是”是一个…”、”是一种…”的意义。《effective C++》中将其描述为”用….来实现”,也就是说私有继承只是使用了基类中的部分或者全部的成员来实现自身的功能。其实,把类作为成员也能使用类的部分成员,那么为什么非要用私有继承呢?
总结了几种情况,一是需要调用类中的protected成员,包含类无法调用。这种情况也能用来隐藏底层类,只要把底层类的成员都写成protected类型,再进行私有继承,这样就只能使用派生类无法使用基类。

class ExampleA {
public:
    virtual void func() {}
    int a = 0;
protected:
    int b = 1;
private:
    int c = 2;
};
class ExampleB : {
public:
    void func() { exa.b; }                         // 无法使用类成员的protected成员      
private:
    ExampleA exa;
};
class ExampleC : private ExampleA  {
public:
    void func() { b; }                            // 可以使用私有继承父类的protected成员    
};

二是,有时候需要重写类的虚函数,包含类无法重写:

class ExampleA {
public:
    virtual std::string random() { return "1"; }      // 实现某种算法,返回数字结果字符串
    virtual int number()                              // 将字符串转化成int
    {
        return atoi(random().c_str());
    }
};

class ExampleB {
public:
    virtual void print_string() { std::cout << "1" << std::endl; }  // 打印字符串
};

class ExampleC : private ExampleA, public ExampleB{                 // 这里用到了多继承,在下面的内容中会说明
public:
    std::string random() { return "2"; }                            // 重写ExampleA的算法,返回不同结果
    virtual void print_string() { std::cout << number() << std::endl; }    // 打印出算法算出的数字
};

int main()
{
    ExampleC c;
    c.print_string();

    return 0;
}

多继承
C++支持继承多个基类,就是所谓的多继承(multiple inheritance)。多继承能够让类拥有多个类的特性,在一篇文章(https://blog.youkuaiyun.com/qcyfred/article/details/53440536)中看到了一个有意思的例子,大概是这样:

class Bed {
public:
    virtual void sleep() {}
};
class Sofa {
public:
    virtual void sit() {}
};
class Sofabed {
public:
    void sleep() {}
    void sit() {}
};

但是,多继承同样会引起一些问题,例如成员的二义性,就是在两个基类中有同名的成员时,这个时候必须要指明使用的是哪个基类的成员:

class Bed {
public:
    virtual void sleep() {}
    virtual std::string get_name() { return name; }
private:
    std::string name = "Bed";
};
class Sofa {
public:
    virtual void sit() {}
    virtual std::string get_name() { return name; }
private:
    std::string name = "Sofa";
};
class Sofabed : public Sofa, public Bed {
public:
    void sleep() {}
    void sit() {}
};

int main()
{
    Sofabed* sofabed = new Sofabed;
    sofabed->get_name();              // 二义性,ambiguous access of 'get_material'
    sofabed->Sofa::get_name();
    sofabed->Bed::get_name();
    return 0;
}

这样做了之后,Sofabed 就失去了虚函数的动态多态特性,只能调用到指定的基类成员。

class MySofabed : public Sofabed {
public:
    std::string get_name() { return "MySofabed"; }
};

int main()
{
    Sofabed* sofabed = new MySofabed;                             
    std::cout << sofabed->get_name() << std::endl;   // 二义性,sofabed虚表中的对应的虚指针不能确定,因此失去了动态多态特性

    return 0;
}

同时还有一个问题,那就是无法同时重写两个基类的同名函数,因为一个类中只能有一个同名、同参数、同返回值的函数。有一种方法可以解决这个问题,就是继承基类后重新定义一个不同名但是功能相同的函数,然后进行重写:

class Bed {
public:
    virtual void sleep() {}
    virtual std::string get_name() { return name; }
private:
    std::string name = "Bed";
};
class OtherBed: public Bed {
public:
    virtual std::string get_bedname() = 0;             // 定义另一个名字的同功能接口
    std::string get_name() { return get_bedname(); }
};

class Sofa {
public:
    virtual void sit() {}
    virtual std::string get_name() { return name; }
private:
    std::string name = "Sofa";
};
class OtherSofa : public Sofa {
public:
    virtual std::string get_sofaname() = 0;            // 定义另一个名字的同功能接口
    std::string get_name() { return get_sofaname(); }
};

class Sofabed : public OtherSofa, public OtherBed {
public:
    std::string get_sofaname() { return "OtherSofa"; }    // 重写同功能接口
    std::string get_bedname() { return "OtherBed"; }      // 重写同功能接口
};

int main()
{
    Sofabed* sofabed = new Sofabed;
    Sofa* sofa = sofabed;
    Bed* bed = sofabed;
    std::cout << sofa->get_name() << std::endl;   // OtherSofa
    std::cout << bed->get_name() << std::endl;    // OtherBed

    getchar();
    return 0;
}

这个方法的确解决了无法重写的问题,但是也引入了在程序设计中没有实际意义的类,它的存在只是为了解决问题。看到这里,我们已经可以看到多继承可能会遇到的一部分问题,实际上还会有另外的问题:菱形继承结构。

菱形继承结构

class A {
public:
    int a;
};
class B : public A {
public:
    B() { a = 1; }
    int b;
};
class C : public A {
public:
    C() { a = 2; }
    int c;
};
class D : public B, public C {
public:
    int d;
};

这种结构会导致类D拥有两个类A的实例,如果希望只有一个A的实例怎么办?这时候需要使用继承,让类B和类C虚继承于类A。使用虚继承之后,D的对象就只会有一个A的实例,同时类D对象的内存分部也会有所改变。原来是A+B+A+C+D的连续分布,使用虚继承之后就可能会变成B+A指针+C+A指针+D(视编译器不同,不过大体思路就是这样),这样自然会牺牲一部分的空间。

class A {
public:
    int a;
};
class B : virtual public A {    // 虚继承类A
public:
    B() { a = 1; }
    int b;
};
class C : virtual public A {    // 虚继承类A
public:
    C() { a = 2; }
    int c;
};
class D : public B, public C {
public:
    int d;
};

看起来这样挺完美的,但是实际类设计中我们很难预测到是否会有人同时继承B和C,也就很难去决定是否使用虚继承。这样看起来,多继承有很多很多的问题,那么是不是干脆不用?但是在上面的私有继承中的一个例子,用到了多继承来解决一些问题,所以还是有一些场景还是有用的,不过需要我们小心谨慎的使用。

保护继承
保护继承的作用是让外部无法访问基类的公有和保护成员,但是从保护继承派生的类能够访问基类的公有和保护成员。就像下面的代码,ExampleC能够访问ExampleA中的成员a和b,但是类外就不行。

class ExampleA {
public:
    virtual void func() {}
    int a = 0;
protected:
    int b = 1;
private:
    int c = 2;
};
class ExampleB : protected ExampleA {
public:
    void func() {}
    void test() { a; b; }                        // 能够使用基类的公有和保护成员
};
class ExampleC : public ExampleB {
public:
    void test() { a; b; }                       // 能够使用基类的公有和保护成员
};

int main()
{
    ExampleA* a = new ExampleB;                    // 编译失败,B无法转换成A
    ExampleB b;
    b.a; b.b; b.c;                                 // 无法使用a、b、c
    return 0;
}
  • 和公有继承的差别:禁止了类外访问基类的公有成员,禁止了对象的转化为基类。
  • 和私有继承的差别:保护继承的派生类能够访问基类的公有和保护成员。

    《effective C++》中只说了保护继承还没有发现用处,根据以上两个差别进行猜测,可能可以用于不希望外部访问公有成员但是派生类能访问的情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值