关于对象那些事儿 ~~ 继承
Table of Contens
关于继承
随着软件功能的日益完善,随之而来的就是软件的也越来越大,变得难以维护。传统的面向 过程的编程范式难以满足日益丰富的用户需求,因此需要一种新的编程范式来应对这种 复杂的需求。面向对象在这个时候出现,很好的解决了这些难题。它是一种软件开发方法, 利用对世界关系的分析方法来分析复杂软件体系关系,实践证明这是可行的。
何为对象
世界的所有事物都可以抽象为一个对象,一个对象拥有基本的属性以及作用在这些属性上 的功能。举个简单的例子:人为一个对象,有基本属性(身高、体重等),以及功能 (吃饭、说话等)。在面向对象的软件开发方法中,有三大特性:继承、封装及多态。今天 的主角是继承。
何为继承
每个对象的可以抽象为不同的级别,我们将有某些共同属性或功能抽象成一个基类对象,然 后让派生类来继续延用(继承)公用的属性或功能。同样举个简单例子:父亲是一个基类或 父类,小孩就是派生类或子类,小孩会继承父亲遗传的某些性状(如血型)。
继承是实现代码重用的重要手段,直接使用基类对象的属性和方法,呈现面向对象程序设计
的层次结构,体现由简单到复杂、一般到特殊的认知过程。继承的方式有三种: public
继承、
protected
继承、 private
继承。编译器通过三种方式对应的限定符进行区分。
public 继承
公有继承是比较常见的继承方式,通过 public
关键字来表明。
public 继承权限
- 父类的
public
成员继承为子类的public
成员,子类可以访问。 - 父类的
protected
成员继承为子类的protected
成员,子类成员函数可以访问。 - 父类的
private
成员仍为 父类 的private
成员,子类不可以访问。
实例
#include <iostream>
class Base {
public:
int int_pub = 0;
protected:
int int_pro = 0;
private:
int int_pri = 0;
};
class Derive : public Base {
public:
void print() {
std::cout << "print:" << int_pub << std::endl;
std::cout << "print:" << int_pro << std::endl;
// std::cout << "print:" << int_pri << std::endl;
}
};
int main() {
Derive d;
std::cout << "main:" << d.int_pub << std::endl;
// std::cout << "main:" << d.int_pro << std::endl;
d.print();
return 0;
}
在子类的成员函数种访问父类的 private
会出错,错误信息也会提示这是 Base
的私
有成员。子类直接访问继承的 protected
成员也会报错。
protected 继承
保护继承通过 protected
关键字限定表明。
protected 继承权限
- 父类的
public
成员继承为子类的protected
成员,可通过成员函数访问。 - 父类的
protected
成员继承为子类的protected
成员,可通过成员函数访问。 - 父类的
private
成员仍为 父类 的private
成员,子类不可以访问。
实例
#include <iostream>
class Base {
public:
int int_pub = 0;
protected:
int int_pro = 0;
private:
int int_pri = 0;
};
class Derive : protected Base {
public:
void print() {
std::cout << "print:" << int_pub << std::endl;
std::cout << "print:" << int_pro << std::endl;
// std::cout << "print:" << int_pri << std::endl;
}
};
int main() {
Derive d;
// std::cout << "main:" << d.int_pub << std::endl;
// std::cout << "main:" << d.int_pro << std::endl;
d.print();
return 0;
}
通过对象直接访问继承自父类的 public
成员会出错。
private 继承
私有继承通过 private
关键字限定表明。
protected 继承权限
- 父类的
public
成员继承为子类的private
成员,可通过成员函数访问。 - 父类的
protected
成员继承为子类的private
成员,可通过成员函数访问。 - 父类的
private
成员仍为 父类 的private
成员,子类不可以访问。
实例
#include <iostream>
class Base {
public:
int int_pub = 0;
protected:
int int_pro = 0;
private:
int int_pri = 0;
};
class Derive : protected Base {
public:
void print() {
std::cout << "print:" << int_pub << std::endl;
std::cout << "print:" << int_pro << std::endl;
// std::cout << "print:" << int_pri << std::endl;
}
};
int main() {
Derive d;
// std::cout << "main:" << d.int_pub << std::endl;
// std::cout << "main:" << d.int_pro << std::endl;
d.print();
return 0;
}
访问和 protected
是一样。这里说明 private
和 protected
的异同:继承权限不
一样,前者不可继承后者可以;访问权限是一样的,都必须通过成员函数才能访问。
虚继承
虚继承的出现是用来解决多继承的对象多次被继承带来的问题。普通继承在不同途径继承来 自同一基类的子孙类中存在多份拷贝。这将存在两个问题:其一,浪费存储空间;第二,存 在二义性问题,通常可以将派生类对象的地址赋值给基类对象,实现的具体方式是,将基类 指针指向继承类(继承类有基类的拷贝)中的基类对象的地址,但是多重继承可能存在一个 基类的多份拷贝,这就出现了二义性。
虚继承可以解决多种继承前面提到的两个问题:
虚继承底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子 类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的 存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而 已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指 向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过 偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类 (虚基类)的两份同样的拷贝,节省了存储空间。
在这里我们可以对比虚函数的实现原理:他们有相似之处,都利用了虚指针(均占用类的 存储空间)和虚表(均不占用类的存储空间)。
虚基类依旧存在继承类中,只占用存储空间;虚函数不占用存储空间。
虚基类表存储的是虚基类相对直接继承类的偏移;而虚函数表存储的是虚函数地址。
实例
- 普通继承
#include<iostream>
using namespace std;
class A //大小为4
{
public:
int a;
};
class B :public A //大小为8,变量a,b共8字节
{
public:
int b;
};
class C :public A //与B一样8
{
public:
int c;
};
class D :public B, public C //大小为20,变量 2xa + b + c + d = 20
{
public:
int d;
};
int main()
{
A a;
B b;
C c;
D d;
cout << "A " << sizeof(a) << endl;
cout << "B " << sizeof(b) << endl;
cout << "C " << sizeof(c) << endl;
cout << "D " << sizeof(d) << endl;
return 0;
}
- 虚继承
#include<iostream>
using namespace std;
class A //大小为4
{
public:
int a;
};
class B :virtual public A //大小为16,变量a,b共8字节,虚基类表指针8
{
public:
int b;
};
class C :virtual public A //与B一样16
{
public:
int c;
};
class D :public B, public C
//大小为40,变量a,b,c,d共16,B的虚基类指针8,C的虚基类指针8,D类的虚指针8
//也可以这样算: B, C 一共 32, d占4字节,D虚指针8,一共44, 减去B,C重复的A,得 40
{
public:
int d;
};
int main()
{
A a;
B b;
C c;
D d;
cout << "A " << sizeof(a) << endl;
cout << "B " << sizeof(b) << endl;
cout << "C " << sizeof(c) << endl;
cout << "D " << sizeof(d) << endl;
return 0;
}
从上面的两个例子中可以看出,在普通继承中的通过不同途径继承自同一继承祖先类的子孙 类中包含多份拷贝,在大型的软件系统中,这很浪费资源。
虚继承与虚函数
这两者是完全不同的概念,虚继承是解决多重继承带来的两个问题;而虚函数是为了实现多 态。
总结
这篇是对面向对象三大特性之一进行简单的介绍,重点还是要理解继承权限的关系以及虚继 承和普通继承的内存布局,不要把虚继承和虚函数的弄混淆。