C++进阶篇:继承与多态一篇拿下

目录

1. 继承

1. 继承的概念及定义

1.1 继承的概念

1.2 继承的定义

1.2.1 格式

1.2.2 继承关系和访问限定符                

 1.2.3 继承基类成员访问方式的变化

 2. 基类和派生类对象赋值转换

 3. 继承中的作用域

4. 派生类的默认成员函数

5. 继承与友元

6. 继承与静态成员

7. 菱形继承与菱形虚拟继承 

8. 继承和组合 

9. 笔试面试题

2. 多态 

1. 多态的概念 

2. 多态的定义及实现

2.1 多态的构成条件

2.2 虚函数 

2.3虚函数的重写

虚函数重写的两个例外: 

1. 协变(基类与派生类虚函数返回值类型不同)

2. 析构函数的重写(基类与派生类析构函数的名字不同)

2.4 C++11 override 和 final

1. final

2. override

2.5 重载、覆盖(重写)、隐藏(重定义)的对比

​编辑

3. 抽象类

3.1 概念

注意!!!: 

3.2 接口继承和实现继承

4.多态的原理

4.1虚函数表

​编辑

 4.2多态的原理

 注意!!!:

 4.3 动态绑定与静态绑定

5.单继承和多继承关系的虚函数表

5.1 单继承中的虚函数表

 5.2 多继承中的虚函数表

6. 继承和多态常见的面试问题 

6.1 概念查考

6.2 问答题


1. 继承

1. 继承的概念及定义


1.1 继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保
持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象
程序设计的
层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,
承是
类设计层次的复用

class A
{
public:
	void Print()
	{
		cout << "a:" << a << endl;
	}
protected:
	int a = 1;
};
/* 继承后父类的A的成员(成员函数 + 成员变量)都会变成子类的一部分。*/
class B : public A
{
protected:
	int b = 2; 
};
class C : public A
{
protected:
	int c = 3; 
};
int main()
{
	B b;
	C c;
	b.Print();
	c.Print();
	return 0;
}

监视窗口:

1.2 继承的定义
1.2.1 格式

结合上面的例子:

1.2.2 继承关系和访问限定符                

 1.2.3 继承基类成员访问方式的变化
类成员/继承方式 public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected
成员
派生类的private
成员
基类的protected
成员
派生类的protected
成员
派生类的protected
成员
派生类的private
成员
基类的private成
在派生类中不可见在派生类中不可见在派生类中不可

总结:
1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私
有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面
都不能去访问它

2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在
派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的

3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。

        基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式)

                                        (public > protected> private)

其实和const成员的权限问题差不多:权限缩小,可以平移,但不能放大!!!
4. 使用关键字class时默认的继承方式是private使用struct时默认的继承方式是public,不过
最好显示的写出继承方式。
5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强。

 2. 基类和派生类对象赋值转换

1. 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片
或者切割
。寓意把派生类中父类那部分切来赋值过去。
2. 基类对象不能赋值给派生类对象!!!
3. 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类
的指针是指向派生类对象时才是安全的
。这里基类如果是多态类型,可以使用RTTI(Run-
Time Type Information)的dynamic_cast来进行识别后进行安全转换。

                             

class Person
{
protected:
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};
class Student : public Person
{
public:
	int _No; // 学号
};
void Test()
{
	Student sobj;
	// 1.子类对象可以赋值给父类对象/指针/引用
	Person pobj = sobj;
	Person* pp = &sobj;
	Person& rp = sobj;

	//2.基类对象不能赋值给派生类对象
	sobj = pobj;

	// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
	pp = &sobj
	Student * ps1 = (Student*)pp; // 这种情况转换时可以的。
	ps1->_No = 10;

	pp = &pobj;
	Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
	ps2->_No = 10;
}

 3. 继承中的作用域

1. 在继承体系中基类派生类都有独立的作用域
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。
(在子类成员函数中,可以使用 基类::基类成员 显示访问
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员

4. 派生类的默认成员函数

6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类
中,这几个成员函数是如何生成的呢?
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认
的构造函数,则
必须在派生类构造函数的初始化列表阶段显示调用
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同,那么编译器会对析构函数名进行特殊处理,处理成destrutor()(在操作系统底层析构函数都被统一命名 destrutor()),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。

 

class Person
{
public:
	Person(const char* name = "peter")
		: _name(name)
	{
		cout << "Person()" << endl;
	}

	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}

	Person& operator=(const Person& p)
	{
		cout << "Person operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;

		return *this;
	}

	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name; // 姓名
};
class Student : public Person
{
public:
	Student(const char* name, int num)
		: Person(name)
		, _num(num)
	{
		cout << "Student()" << endl;
	}
	Student(const Student& s)
		: Person(s)
		, _num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	}
	Student& operator = (const Student& s)
	{
		cout << "Student& operator= (const Student& s)" << endl;
		if (this != &s)
		{
			Person::operator =(s);
			_num = s._num;
		}
		return *this;
	}
	~Student()
	{
		cout << "~Student()" << endl;
	}
protected:
	int _num; //学号
};
void Test()
{
	Student s1("jack", 18);
	Student s2(s1);
	Student s3("rose", 17);
	s1 = s3;
}

总结一下就是: 想构造基类,再构造派生类。先析构派生类,再析构基类。

5. 继承与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员 

6. 继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例 。

class Person
{
public:
	Person() { ++_count; }
protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。
};

int Person::_count = 0;
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
string _seminarCourse; // 研究科目
};

void TestPerson()
{
	Student s1;
	Student s2;
	Student s3;
	Graduate s4;
	cout << " 人数 :" << Person::_count << endl;
	Student::_count = 0;
	cout << " 人数 :" << Person::_count << endl;
}

7. 菱形继承与菱形虚拟继承 

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

菱形继承:菱形继承是多继承的一种特殊情况。

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
在Assistant的对象中Person成员会有两份。
 

class Person
{
public:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _num; //学号
};
class Teacher : public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};
void Test()
{
	// 这样会有二义性无法明确知道访问的是哪一个
	Assistant a;
	a_name = "peter";
	// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
}

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地
方去使用。

class Person
{
public:
	string _name; // 姓名
};
class Student : virtual public Person
{
protected:
	int _num; //学号
};
class Teacher : virtual public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};
void Test()
{
	Assistant a;
	a._name = "peter";
}

虚拟继承解决数据冗余和二义性的原理 

首先来看对象成员模型:

直接继承:

class A
{
public:
	int _a;
};
 class B : public A
//class B : virtual public A
{
public:
	int _b;
};
 class C : public A
//class C : virtual public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

下图是菱形继承的内存对象成员模型:这里可以看到数据冗余 

下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下
面,这个A同时属于B和C,这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。

8. 继承和组合 

【1】public继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象。
【2】组合是一种 has-a 的关系。假设B组合了A,每个B对象中都有一个A对象。

【3】优先使用对象组合,而不是类继承

【4】继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称
        为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的
        内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很
        大的影响。派生类和基类间的依赖关系很强,耦合度高。
【5】对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象
        来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复
        用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。
        组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
        封装。
【6】实际尽量多去用组合。
组合的耦合度低,代码维护性好。不过继承也有用武之地的,
        些关系就适合继承那就用继承,另外要实现多态,也必须要继承
。类之间的关系可以用
        继承,可以用组合,就用组合。

// Car和BMW Car和Benz构成is-a的关系
 class Car{
 protected:
 string _colour = "白色"; // 颜色
 string _num = "陕ABIT00"; // 车牌号
 };
 class BMW : public Car{
 public:
 void Drive() {cout << "好开-操控" << endl;}
 };
 class Benz : public Car{
 public:
 void Drive() {cout << "好坐-舒适" << endl;}
 };
 // Tire和Car构成has-a的关系
 class Tire{
 protected:
   string _brand = "Michelin";  // 品牌
   size_t _size = 17;     // 尺寸
 };
 class Car{
 protected:
 string _colour = "白色"; // 颜色
 string _num = "陕ABIT00"; // 车牌号
  Tire _t; // 轮胎
 }; 

9. 笔试面试题

1. 什么是菱形继承?菱形继承的问题是什么?
2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的
3. 继承和组合的区别?什么时候用继承?什么时候用组合?

2. 多态 

1. 多态的概念 

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态

2. 多态的定义及实现

2.1 多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了
Person。Person对象买票全价,Student对象买票半价。

那么在继承中要构成多态还有两个条件
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

2.2 虚函数 

虚函数:即被virtual修饰的 类成员函数称为虚函数。

class Person {
public:
    virtual void BuyTicket() { cout << "买票-全价" << endl;}
};    
2.3虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的
返回值类型、函数名字、参数列表完全相同
)
,称子类的虚函数重写了基类的虚函数。

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
	为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
	这样使用*/
	/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

虚函数重写的两个例外: 
1. 协变(基类与派生类虚函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指
针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变

class A{};

class B : public A {};

class Person {
public:
    virtual A* f() {return new A;}
};

class Student : public Person {
public:
    virtual B* f() {return new B;}
};
2. 析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
理,编译后析构函数的名称统一处理成destructor

2.4 C++11 override 和 final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数
名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有
得到预期结果才来debug会得不偿失,因此:C++11提供了 overridefinal 两个关键字,可以帮
助用户检测是否重写。

1. final

修饰虚函数,表示该虚函数不能再被重写 

class Car
{
public:
    virtual void Drive() final {}
};

class Benz :public Car
{
public:
    virtual void Drive() {cout << "Benz" << endl;}
};
2. override

 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Car{
public:
    virtual void Drive(){}
};

class Benz :public Car {
public:
    virtual void Drive() override {cout << "Benz" << endl;}
};
2.5 重载、覆盖(重写)、隐藏(重定义)的对比

3. 抽象类

3.1 概念

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象
。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生
类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。 

class Car
{
public:
	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz" << endl;
	}
};
class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW" << endl;
	}
};
void Test()
{
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
}
注意!!!: 

1:抽象类:不能实例化出对象

2:间接强制派生类重写虚函数

3:override 已经重写了,帮助检查语法是否有问题

3.2 接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实
现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成
多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

4.多态的原理

4.1虚函数表
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

通过观察测试我们发现b对象是8bytes32位平台下),除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表(和虚基表不是一种东西)。那么派生类中这个表放了些什么呢?我们接着往下分析

// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

通过观察和测试,我们发现了以下几点问题:
【1】 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚
表指针也就是存在部分的另一部分是自己的成员。
【2】 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表
中存的是重写的Derive::Func1,所以
虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
【3】 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函
数,所以不会放进虚表。
【4】 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
【5】 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生
类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己
新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
【6】 这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在
虚表,虚表存在对象中。注意上面的回答的错的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。虚表存在vs下是存在代码段。

简单来说就是:虚函数跟普通函数一样,都是存在代码段,不是存在虚表的虚表中存的仅仅是虚函数的地址。

 4.2多态的原理

 多态的原理到底是什么?还记得这里Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket ?为什么?

 【1】 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚
函数是Person::BuyTicket。
【2】 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中
找到虚函数是Student::BuyTicket。
【3】 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
【4】 反过来思考我们要达到多态,有两个条件一个是虚函数覆盖一个是对象的指针或引用调
用虚函数

【5】 再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行
起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的

 注意!!!

 多态调用:运行时,到指向对象的虚表中找虚函数调用,指向父类调用父类的虚函数,指向子类                       调用子类的虚函数。

 普通调用:编译时,调用对象是哪个类型,就调用他的函数。

 虚表:       虚函数表,存的虚函数,目标实现多态。

 虚基表:    存的当前位置距离虚基类部分的偏移量,解决菱形继承数据冗余和二义性。

 4.3 动态绑定与静态绑定

【1】 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态
比如:函数重载
【2】 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体
行为,调用具体的函数,也称为动态多态

5.单继承和多继承关系的虚函数表

5.1 单继承中的虚函数表
class Base {
public :
virtual void func1() { cout<<"Base::func1" <<endl;}
virtual void func2() {cout<<"Base::func2" <<endl;}
private :
int a;
};
class Derive :public Base {
public :
virtual void func1() {cout<<"Derive::func1" <<endl;}
virtual void func3() {cout<<"Derive::func3" <<endl;}
virtual void func4() {cout<<"Derive::func4" <<endl;}
private :
int b;
};

下面我们使用代码打印出虚表中的函数。

typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Base b;
	Derive d;
	/* 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数
	指针的指针数组,这个数组最后面放了一个nullptr*/
		// 1.先取b的地址,强转成一个int*的指针
		// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
		// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
		// 4.虚表指针传递给PrintVTable进行打印虚表
		// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最
		// 后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决                
        // 方案,再编译就好了。
	VFPTR * vTableb = (VFPTR*)(*(int*)&b);
	PrintVTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	PrintVTable(vTabled);
	return 0;
}

 5.2 多继承中的虚函数表
class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	return 0;
}

观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中 

6. 继承和多态常见的面试问题 

6.1 概念查考

【1】 下面哪种面向对象的方法可以让你变得富有( )
        A: 继承 B: 封装 C: 多态 D: 抽象


【2】 ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,
        而对方法的调用则可以关联于具体的对象。

        A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定


【3】 面向对象设计中的继承和组合,下面说法错误的是?()
        A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复
              用,也称为白盒复用。
        B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动
              态复用,也称为黑盒复用。
        C:优先使用继承,而不是组合,是面向对象设计的第二原则
        D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封
              装性的表现。


【4】以下关于纯虚函数的说法,正确的是( )
        A:声明纯虚函数的类不能实例化对象         B:声明纯虚函数的类是虚基类
        C:子类必须实现基类的纯虚函数                D:纯虚函数必须是空函数


 【5】 关于虚函数的描述正确的是( )
  A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型 

 B:内联函数不能是虚函数
 C:派生类必须重新定义基类的虚函数

  D:虚函数可以是一个static型的函数


 【6】 关于虚表说法正确的是( )
        A:一个类只能有一张虚表
        B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
        C:虚表是在运行期间动态生成的
        D:一个类的不同对象共享该类的虚表


【7】 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
        A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
        B:A类对象和B类对象前4个字节存储的都是虚基表的地址
        C:A类对象和B类对象前4个字节存储的虚表地址相同
        D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表

【8】下面程序输出结果是什么? ()

#include<iostream>
using namespace std;
class A {
public:
	A(char* s) { cout << s << endl; }
	~A() {}
};
class B :virtual public A
{
public:
	B(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
	C(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:
	D(char* s1, char* s2, char* s3, char* s4) 
		:B(s1, s2)
		,C(s1, s3)
		,A(s1)
	{
		cout << s4 << endl;
	}
};

int main() {
	D* p = new D("class A", "class B", "class C", "class D");
	delete p;
	return 0;
}

 A:class A class B class C class D         B:class D class B class C class A
C:class D class C class B class A         D:class A class C class B class D

[9] 多继承中指针偏移问题?下面说法正确的是( ) 

class Base1 {  public:  int _b1; };
class Base2 {  public:  int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main(){
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}

A:p1 == p2 == p3         B:p1 < p2 < p3         C:p1 == p3 != p2         D:p1 != p2 != p3

10. 以下程序输出结果是什么()

class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
	virtual void test() { func(); }
};
class B : public A
{
public:
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
	B* p = new B;
	p->test();
	return 0;
}

A: A->0         B: B->1         C: A->1         D: B->0         E: 编译出错         F: 以上都不正确

6.2 问答题

【1】 什么是多态?

答:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态


【2】什么是重载、重写(覆盖)、重定义(隐藏)?

答:(一张图拿下)

【3】多态的实现原理?

答:虚函数表


【4】 inline函数可以是虚函数吗?

答:可以,不过编译器就忽略inline属性,这个函数就不再是
inline,因为虚函数要放到虚表中去。


【5】 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数
的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。


【6】 构造函数可以是虚函数吗?

答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。


【7】 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

答:可以,并且最好把基类的析构函数定义成虚函数。


【8】 对象访问普通函数快还是虚函数更快?

答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查表。


【9】 虚函数表是在什么阶段生成的,存在哪的?

答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。


【10】 C++菱形继承的问题?虚继承的原理?

答:菱形继承有数据冗余和二义性的问题。虚基表(要注意和虚表(虚函数表)区分开)。


【11】 什么是抽象类?抽象类的作用?

答:在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象
。抽象类强制重写了虚函数,另外抽象类体现出了接口继承系。 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值