C++类公有继承、私有继承、保护继承、构造函数、虚函数的工作原理


从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。

class A :public B{
...
}

冒号指出A的基类是B,为公有继承。基类的公有成员将成为派生类的公有成员基类的私有部分也将成为派生类的一部分。但只能通过基类的公有和保护方法来访问。

  • 派生类对象存储了基类的数据成员(派生类继承了基类的实现)
  • 派生类对象可以使用基类的方法(派生类继承了基类的接口)
  • 派生类需要自己的构造函数
  • 派生类可以根据需要添加数据成员和成员函数。

构造函数:访问权限的考虑

派生类不能直接访问基类的私有成员。而必须通过基类的方法来进行访问。所以派生类构造函数必须使用基类的构造函数。创建派生类对象时,程序首先创建基类对象。这意味着,基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表语法来完成这种工作。

A::A(int a,int b,int c) : B(b,c){
...
}

A的构造函数先将b和c传递给B的构造函数。如果省略成员初始化列表,将调用默认构造函数。

  1. 首先创建基类对象
  2. 派生类构造函数通过成员初始化列表将参数传递给基类的构造函数
  3. 派生类构造函数初始化派生类新增的数据对象。

释放对象的顺序和创建对象的顺序相反,所以先执行派生类的析构函数,然后自动调用基类的析构函数。

基类和派生类之间的特殊关系

  1. 派生类对象可以调用基类的非私有方法
  2. 基类指针可以在不进行显式类型转换的情况下指向派生类对象
  3. 基类引用可以在不进行显式类型转换的情况下引用派生类对象
A a;
B & rb = a;
B * pb = &a;
rb.b();
pb->b();//b是基类B中的方法

基类指针只能用于调用基类方法。

多态公有继承

如果希望同一个方法在派生类和基类中的行为是不同的。换句话说,方法的行为应取决于调用方法的对象。这种行为成为多态。一个方法,多种形态。方法的行为随上下文而异。有两种方法实现多态公有继承:

  • 在派生类中重新定义基类的方法
  • 使用虚方法
    如果使用了virtual,程序将根据引用或指向的对象的类型来选择方法。
    方法在基类中被声明为虚后,它在派生类中将自动成为虚方法。

    所以在派生类中可以不使用virtual关键字。
    如果要在派生类中重新定义基类的方法,通常应将基类的方法声明为虚,这样程序将通过对象类型来选择方法。为基类声明一个虚析构函数也是一种惯例。使用虚析构函数可以确保正确的析构函数序列被调用。因为析构函数是虚的,它将选择相应的对象类型的析构函数。

虚函数的工作原理

通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针。这种数组被称为虚函数表一个类有一个虚函数表。 虚函数表中存储了为类对象进行声明的虚函数地址。

  • 如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址。
  • 如果没有,将保存原始版本函数的地址。
  • 如果派生类定义了新的虚函数,则该函数的地址也将被添加进虚函数表。

使用虚函数的成本:

  • 每个对象都将增大,增大量为存储地址的空间
  • 对于每个类,编译器都创建一个虚函数地址表(数组)
  • 对于每个函数调用,都要执行一项额外的操作,即到表中查找地址。

注意事项:

  • 在基类中使用virtual关键字声明方法,可以使该方法在基类以及所有的派生类中是虚的。

  • 如果使用指向对象的引用或者指针来调用虚方法。程序将使用为对象类型定义的方法。

  • 如果定义的类将被用作基类,则应该将要在派生类中重新定义的方法定义为虚的。

  • 构造函数不能是虚函数
    原因:构造函数是用来创建并初始化对象的,在调用构造函数之前虽然分配了内存空间,但此时还没有实例化,没有虚函数指针。
    虚函数的调用过程:
    1.找到虚函数指针
    2.通过虚函数指针找到虚函数表
    3.在虚函数表中找到该虚函数的地址,并调用

  • 析构函数应当是虚函数,除非类不用做基类。 给类定义一个虚析构函数并非错误,即使这个类不用做基类,这是效率方面的问题。

  • 友元函数不能是虚函数

  • 如果派生类没有重新定义,将使用函数的基类版本

  • 如果在派生类中重新定义函数,将隐藏基类版本,无论函数的参数列表是否相同。

#include <iostream>
using namespace std;
class A
{
public:
    int i;
    virtual void func() {}
    virtual void func2() {}
};
class B : public A
{
    int j;
    void func() {}
};
int main()
{
    cout << sizeof(A) << ", " << sizeof(B);  //输出 8,12
    return 0;
}

在 32 位编译模式下,程序的运行结果是:
8, 12

如果将程序中的 virtual 关键字去掉,输出结果变为:
4, 8

任何有虚函数的类及其派生类的对象都包含这多出来的 4 个字节,这 4 个字节就是实现多态的关键——它位于对象存储空间的最前端,其中存放的是虚函数表的地址。

每一个有虚函数的类(或有虚函数的类的派生类)都有一个虚函数表,该类的任何对象中都放着该虚函数表的指针(可以认为这是由编译器自动添加到构造函数中的指令完成的)。

虚函数表是编译器生成的,程序运行时被载入内存。一个类的虚函数表中列出了该类的全部虚函数地址。例如,在上面的程序中,类 A 对象的存储空间以及虚函数表(假定类 A 还有其他虚函数)如图 所示。
在这里插入图片描述

类 B 对象的存储空间以及虚函数表(假定类 B 还有其他虚函数)如图
在这里插入图片描述
多态的函数调用语句被编译成根据基类指针所指向的(或基类引用所引用的)对象中存放的虚函数表的地址,在虚函数表中查找虚函数地址,并调用虚函数的一系列指令。
假设 pa 的类型是 A*,则 pa->func() 这条语句的执行过程如下:

  1. 取出 pa 指针所指位置的前 4 个字节,即对象所属的类的虚函数表的地址(在 64 位编译模式下,由于指针占 8 个字节,所以要取出 8 个字节)。如果 pa 指向的是类 A 的对象,则这个地址就是类 A 的虚函数表的地址;如果 pa 指向的是类 B 的对象,则这个地址就是类 B 的虚函数表的地址。
  2. 根据虚函数表的地址找到虚函数表,在其中查找要调用的虚函数的地址。不妨认为虚函数表是以函数名作为索引来查找的,虽然还有更高效的查找方法。
    如果 pa 指向的是类 A 的对象,自然就会在类 A 的虚函数表中查出 A::func 的地址;如果 pa 指向的是类 B 的对象,就会在类 B 的虚函数表中查出 B::func 的地址。
    类 B 没有自己的 func2 函数,因此在类 B 的虚函数表中保存的是 A::func2 的地址,这样,即便 pa 指向类 B 的对象,pa->func2();这条语句在执行过程中也能在类 B 的虚函数表中找到 A::func2 的地址。
  3. 根据找到的虚函数的地址调用虚函数。

由以上过程可以看出,只要是通过基类指针或基类引用调用虚函数的语句,就一定是多态的,也一定会执行上面的查表过程,哪怕这个虚函数仅在基类中有,在派生类中没有。

多态机制能够提高程序的开发效率,但是也增加了程序运行时的开销。虚函数表各个对象中包含的 4 个字节的虚函数表的地址都是空间上的额外开销;而查虚函数表的过程则是时间上的额外开销。

访问控制:protected

关键字private和protected相似,在类外只能用公有类成员来访问protected部分的类成员。唯一区别在于基类的派生类中才会体现。派生类的成员可以直接访问protected成员,而不能直接访问基类的private成员。最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派生类能够访问私有成员。

抽象基类(abstract base class ABC)

纯虚函数

virtual int test() const = 0;//纯虚函数声明

包含纯虚函数的的类只能用做基类。在C++中,允许纯虚函数有定义。
当类声明中包含纯虚函数时,则不能创建该类的对象。

私有继承

使用私有继承,基类的公有成员和保护成员都将称为派生类的私有成员。这意味着,基类方法不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们。

公有继承、私有继承和保护继承

有public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性。

基类成员访问属性的改变基类public成员基类protected成员基类private成员
public继承publicprotectedprivate
protected继承protectedprotectedprivate
private继承privateprivateprivate

总结:

  1. 基类的private成员始终是private,不能被派生类直接访问
  2. private成员只能被本类成员(类内)和友元访问,不能被派生类访问;
  3. protected成员可以被派生类访问。
  4. private和protected成员在类外都不可以直接访问

公有继承:

#include<iostream>
#include<assert.h>
using namespace std;

class A {
public:
	int a;
	A() {
		publicMem = 1;
		protectedMem = 2;
		privateMem = 3;
		a = 4;
	}
	void fun() {
		cout << a << endl;    //正确
		cout << publicMem << endl;   //正确
		cout << protectedMem << endl;   //正确
		cout << privateMem << endl;   //正确
	}
public:
	int publicMem;
protected:
	int protectedMem;
private:
	int privateMem;
};
class B : public A {
public:
	int a;
	B(int i) {
		A();
		a = i;
	}
	void fun() {
		cout << a << endl;       //正确,public成员
		cout << publicMem << endl;       //正确,基类的public成员,在派生类中仍是public成员。
		cout << protectedMem << endl;       //正确,基类的protected成员,在派生类中仍是protected可以被派生类访问。
		cout << privateMem << endl;       //错误,基类的private成员不能被派生类访问。
	}
};
int main() {
	B b(10);
	cout << b.a << endl;
	cout << b.publicMem << endl;   //正确
	cout << b.protectedMem << endl;   //错误,类外不能访问protected成员
	cout << b.privateMem << endl;   //错误,类外不能访问private成员
	system("pause");
	return 0;
}

保护继承:

#include<iostream>
#include<assert.h>
using namespace std;

class A {
public:
	int a;
	A() {
		publicMem = 1;
		protectedMem = 2;
		privateMem = 3;
		a = 4;
	}
	void fun() {
		cout << a << endl;    //正确
		cout << publicMem << endl;   //正确
		cout << protectedMem << endl;   //正确
		cout << privateMem << endl;   //正确
	}
public:
	int publicMem;
protected:
	int protectedMem;
private:
	int privateMem;
};
class B : protected A {
public:
	int a;
	B(int i) {
		A();
		a = i;
	}
	void fun() {
		cout << a << endl;       //正确,public成员
		cout << publicMem << endl;       //正确,基类的public成员,在派生类中变成protected成员。
		cout << protectedMem << endl;       //正确,基类的protected成员,在派生类中仍是protected可以被派生类访问。
		cout << privateMem << endl;       //错误,基类的private成员不能被派生类访问。
	}
};
int main() {
	B b(10);
	cout << b.a << endl;
	cout << b.publicMem << endl;   //错误,类外不能访问protected成员
	cout << b.protectedMem << endl;   //错误,类外不能访问protected成员
	cout << b.privateMem << endl;   //错误,类外不能访问private成员
	system("pause");
	return 0;
}

私有继承:

#include<iostream>
#include<assert.h>
using namespace std;

class A {
public:
	int a;
	A() {
		publicMem = 1;
		protectedMem = 2;
		privateMem = 3;
		a = 4;
	}
	void fun() {
		cout << a << endl;    //正确
		cout << publicMem << endl;   //正确
		cout << protectedMem << endl;   //正确
		cout << privateMem << endl;   //正确
	}
public:
	int publicMem;
protected:
	int protectedMem;
private:
	int privateMem;
};
class B : private A {
public:
	int a;
	B(int i) {
		A();
		a = i;
	}
	void fun() {
		cout << a << endl;       //正确,public成员
		cout << publicMem << endl;       //正确,基类的public成员,在派生类中变成private成员。
		cout << protectedMem << endl;       //正确,基类的protected成员,在派生类中是private,可以被派生类的成员函数访问。
		cout << privateMem << endl;       //错误,基类的private成员不能被派生类访问。
	}
};
int main() {
	B b(10);
	cout << b.a << endl;
	cout << b.publicMem << endl;   //错误,类外不能访问private成员
	cout << b.protectedMem << endl;   //错误,类外不能访问private成员
	cout << b.privateMem << endl;   //错误,类外不能访问private成员
	system("pause");
	return 0;
}
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SOC罗三炮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值