C++:多态的详细剖析


概念

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

在这里插入图片描述

比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。

定义及实现

  • 多态的构成条件

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

在继承中构成多态的两个条件(缺一不可):

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
    在这里插入图片描述

虚函数

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

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

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

#include <iostream>
using namespace std;

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. 协变(基类与派生类虚函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。原来的返回类型是指向基类的指针或引用,新的返回类型是指向派生类的指针或引用,覆盖的方法就可以改变返回类型。这样的类型称为协变返回类型(Covariant returns type).

覆盖的返回值不区分基类或派生类。从语意上理解,一个派生类也是一个基类。如下:

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;
    }
};
  1. 析构函数的重写(基类与派生类析构函数的名字不同)

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

class Person {
public:
	virtual ~Person() {
		cout << "~Person()" << endl;
	}
};
class Student : public Person {
public:
	virtual ~Student() {
		cout << "~Student()" << endl;
	}
};

// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,
// 才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;	// ~Person()
	delete p2;	// ~Student() ~Person()
	return 0;
}

C++11 override 和 final

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

  1. final:修饰虚函数,表示该虚函数不能再被继承
class Car
{
public:
	virtual void Drive() final 
	{}
};
class Benz :public Car
{
public:
	virtual void Drive() { 
		cout << "Benz-舒适" << endl; 
	}
};
  1. override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car {
public:
	virtual void Drive() 
	{}
};
class Benz :public Car {
public:
	virtual void Drive() override { 
		cout << "Benz-舒适" << endl; 
	}
};
重载、覆盖(重写)、隐藏(重定义)的对比

在这里插入图片描述

重载(overload):

特征: 函数名相同 、函数参数不同、 必须位于同一个域(类)中;

覆盖(override):

特征: 函数名相同 、函数参数相同、 分别位于派生类和基类中、virtual(虚函数);

隐藏(hide):

即:派生类中函数隐藏(屏蔽)了基类中的同名函数。
情形1: 函数名相同、 函数参数相同、 分别位于派生类和基类中 – 为 隐藏;(即跟覆盖的区别是基类中函数是否为虚函数)
情形2 : 函数名相同、 函数参数不同、 分别位于派生类和基类中 – 为 隐藏;(即与重载的区别是两个函数是否在同一个域(类)中)

抽象类(纯虚函数)

在虚函数的后面写上 =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;
	}
};

int main() {
	Car* pBenz = new Benz;
	pBenz->Drive();	// Benz-舒适
	Car* pBMW = new BMW;
	pBMW->Drive();	// BMW - 操控
	return 0;
}

接口继承和实现继承:

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

多态的原理

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

int main() {
	cout << sizeof(Base);	// 8
	return 0;
}

通过观察测试我们发现b对象是8bytes,除了_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;
	cout << sizeof(b) << endl;	// 8
	cout << sizeof(d) << endl;	// 12

	return 0;
}

在这里插入图片描述

通过观察和测试,我们发现了以下几点问题:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。

  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现 Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但不是虚函数,所以不会放进虚表。

  4. 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。

  5. 这里还有很容易混淆的问题:虚函数存在哪的?虚表存在哪的?

答:注意虚表存的是虚函数指针,不是虚函数虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?vs下是存在代码段的(不代表所有编译器在实现细节上的处理都是完全一致)

  • 多态的原理

上面分析了这个很多了,那么多态的原理到底是什么?

还记得这里Func函数传Person调用的 Person::BuyTicket,传Student调用的是Student::BuyTicket

在这里插入图片描述

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
}; 
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main() {
	Person Mike;
	Func(Mike);
	Student Johnson;
	Func(Johnson);
	return 0;
}

在这里插入图片描述

  1. 观察上图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是 Person::BuyTicket。

  2. 观察上图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是 Student::BuyTicket。

  3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

  4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。

  5. 满足多态的函数调用,不是在编译时确定的,是运行起来以后到对象中寻找的。不满足多态的函数调用时编译时确认好的。

重点总结:

同一个类的不同对象共享一份虚表

基类的虚表生成:

  • 虚表中肯定放置的都是虚函数
  • 按照虚函数在类中声明的先后次序,依次添加到虚表中

派生类的虚表生成:

  • 先将基类中的虚表内容拷贝一份到派生类虚表中
  • 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
  • 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

静态多态与动态多态

  1. 静态多态(静态绑定、前期绑定、早绑定):在程序编译期间确定了程序的行为(具体调用哪个函数)。比如:函数重载、模版

  2. 动态多态(动态绑定、后期绑定、晚绑定):在程序运行期间,根据基类指针或者引用指向不同类的对象,调用对应的虚函数(在程序运行时,确定函数的具体行为)

虚函数表

  • 打印虚表中的函数
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;
}

代码生成图:
在这里插入图片描述

要点总结

一、覆盖/重写

虚函数:
virtual
可以被重写的函数

当派生类继承基类后,如果基类中含有虚函数,子类可以对虚函数执行重写(覆盖),方法是写一个跟它完全相同的函数(函数名、参数列表、返回值都相同),就会覆盖掉原来的函数。
覆盖后,无论用基类指针还是派生类指针,都会调用覆盖后的函数。

特例:
协变:
假如B继承自A,D继承自C,那么A中虚函数返回了C类指针,B中虚函数返回了D类指针,这种情况也构成重写。

二、虚析构函数:

所有析构函数在底层的函数名都是相同的。

虚析构函数是用来解决子类对象转化为父类对象进行析构的问题的。

三、final&override
C++11
final:不能被继承的类或者不能被重写的虚函数(父类)
override:声明子类的某个函数必须重写父类的某个虚函数(子类)

四、重载/重写/隐藏

重载:
1、针对函数
2、重名
3、参数列表不同
重写/覆盖:
1、针对继承
2、完全相同(协变例外)
3、虚函数/纯虚函数
隐藏/重定义:
1、针对继承
2、重名
3、不能是虚函数

※重写是换了一个函数,隐藏是藏一个函数,所以重写是一个函数,隐藏是两个函数

五、抽象类

纯虚函数:只有接口,没有实现的函数。

包含纯虚函数的类叫做抽象类,抽象类不能定义对象。

六、虚表

虚表是一个二级函数指针,只要类中包含虚函数,就会在对象的头部包含一个虚表指针(vfptr)

虚表相当于一个函数指针数组,里面存放的就是虚函数的地址。

当子类继承父类时,会继承虚表,当子类有新的虚函数时,会在虚表后面新增新的项,当子类重写父类的虚函数时,会把虚表中原有的某一项覆盖。

多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

多态常见的面试问题

  1. 什么是多态?

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

  3. 多态的实现原理?

  4. inline函数可以是虚函数吗?

不能,因为inline函数没有地址,无法把地址放到虚函数表中。

  1. 静态成员可以是虚函数吗?

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

  1. 构造函数可以是虚函数吗?

虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数。 因此,构造函数不应该被定义为虚函数。

  1. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

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

虚析构函数是为了解决这样的一个问题:基类的指针指向派生类对象,并用基类的指针删除派生类对象。

  • 基类指针可以指向派生类的对象(多态性),如果删除该指针delete []p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。
  • 如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。
  1. 对象访问普通函数快还是虚函数更快?

普通函数快,因为地址在编译期间指定,单纯的寻址调用。
虚函数调用时,首先找虚函数表,然后找偏移地址进行调用。

  1. 虚函数表是在什么阶段生成的,存在哪的?

虚函数是在编译阶段就生成的;一般情况下存在代码段(常量区)的:因为虚表中的内容是不允许被修改的。

  1. C++菱形继承的问题?虚继承的原理?

答案请点击 <----

  1. 什么是抽象类?抽象类的作用?

抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。

多态习题
  1. 关于虚函数的描述正确的是()

A. 派生类的虚函数与基类的虚函数具有不同的参数个数和类型
B. 内联函数不能是虚函数
C. 派生类必须重新定义基类的虚函数
D. 虚函数可以是一个static型的函数

正确答案:

B

答案解析

虚函数是为了实现动态绑定,不能声明为虚函数的有:

1、静态成员函数
2、类外的普通函数
3、构造函数
4、友元函数

虚函数只能是类中的一个成员函数,但不能是静态成员函数

此外,还有一些函数可以声明为虚函数,但是没有意义,但编译器不会报错,如:
1、赋值运算符的重载成员函数:
因为复制操作符的重载函数往往要求形参与类本身的类型一致才能实现函数功能,故形参类型往往是基类的类型,因此即使声明为虚函数,也把虚函数当普通基类普通函数使用。
2、内联函数:
内联函数目的是在代码中直接展开(编译期),而虚函数是为了继承后能动态绑定执行自己的动作(动态绑定),因此本质是矛盾的,因此即使内联函数声明为虚函数,编译器遇到这种情况是不会进行inline展开的,而是当作普通函数来处理。因此声明了虚函数不能实现内敛的,即内敛函数可以声明为虚函数,但是毫无了内联的意义

  1. 以下关于纯虚函数的说法,正确的是()

A. 声明纯虚函数的类不能实例化
B. 声明纯虚函数的类成虚基类
C. 子类必须实现基类的
C. 纯虚函数必须是空函数

正确答案:

A

答案解析:

纯虚函数可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再去具体地给出定义。凡是含有纯虚函数的类叫做抽象类 。这种类不能声明对象,只是作为基类为派生类服务。除非在派生类中完全实现基类中所有的的纯虚函数,否则,派生类也变成了抽象类,不能实例化对象。

基类被虚继承才是虚基类

空函数的概念:
比如int fun(int x ,int y){}这叫做空函数,也就是花括号中为空;
而纯虚函数的定义是virtual int fun(int x int y)=0.和空函数还是有点不一样的

  1. “引用”与多态的关系?

A.两者没有关系
B.引用可以作为产生多态效果的手段
C.一个基类的引用不可以指向它的派生类实例
D.以上都不正确

正确答案:

B

答案解析:

引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。

class A; 
class Bpublic A{……}; 
B b; 
A &Ref = b; // 用派生类对象初始化基类对象的引用 

Ref 只能用来访问派生类对象中从基类继承下来的成员,是基类引用指向派生类。如果A类中定义有虚函数,并且在B类中重写了这个虚函数,就可以通过Ref产生多态效果。

  1. 下列关于多态性的描述,错误的是( )。

A.C++语言的多态性分为编译时的多态性和运行时的多态性
B.编译时的多态性可通过函数重载实现
C.运行时的多态性可通过模板和虚函数实现
D.实现运行时多态性的机制称为动态绑定

正确答案

C

答案解析

C++中的多态性分为两类:编译时的多态性和运行时的多态性。编译时的多态性是通过函数重载和模板体现的,其实现机制称为静态绑定;运行时的多态性是通过虚函数体现的,其实现机制称为动态绑定。

  1. 以下程序输出结果是____
class A
{
public:
    A ():m_iVal(0){test();}
    virtual void func() { std::cout<<m_iVal<<‘ ’;}
   void test(){func();}
public:
int m_iVal;
};
class B : public A
{
public:
    B(){test();};
    virtual void func()
    {
        ++m_iVal;
        std::cout<<m_iVal<<‘ ’;
}
};

int main(int argc ,char* argv[])
{
    A*p = new B;
    p->test();
    return 0;
}

A. 1 0
B. 0 1
C. 0 1 2
D. 2 1 0
E. 不可预期
F. 以上都不对

正确答案

C

答案解析:

本问题涉及到两个方面:

  1. C++继承体系中构造函数的调用顺序。
  2. 构造函数中调用虚函数问题。

在父类的构造函数中调用虚函数,还是会执行父类的构造函数,不会跑到子类中去,即使有vitual,因为此时父类都还没有构造完成,子类也就还没有构造。本题的输出顺序为:父类构造函数、子类构造函数、指向子类的父类指针(覆盖、多态),所以结果为012.

构造函数中调用虚函数,虚函数表现为该类中虚函数的行为,即在父类构造函数中调用虚函数,虚函数的表现就是父类定义的函数的表现。原因如下:
假设构造函数中调用虚函数,表现为普通的虚函数调用行为,即虚函数会表现为相应的子类函数行为,并且假设子类存在一个成员变量int a;子类定义的虚函数的新的行为会操作a变量,在子类初始化时根据构造函数调用顺序会首先调用父类构造函数,那么虚函数回去操作a,而因为a是子类成员变量,这时a尚未初始化,这是一种危险的行为,作为一种明智的选择应该禁止这种行为。所以虚函数会被解释到基类而不是子类。

  1. 求输出结果
#include <iostream>
 
using namespace std;
 
class A
{
public:
    virtual void print()
    {
        cout << "A::print()" << "\n";
    }
};
 
class B: public A
{
public: virtual void print()
    {
        cout << "B::print()" << "\n";
    }
};
 
class C: public A
{
public: virtual void print()
    {
        cout << "C::print()" << "\n";
    }
};
 
void print(A a)
{
    a.print();
}
 
int main()
{
    A a, *aa, *ab, *ac;
    B b;
    C c;
    aa = &a;
    ab = &b;
    ac = &c;
    a.print();
    b.print();
    c.print();
    aa->print();
    ab->print();
    ac->print();
    print(a);
    print(b);
    print(c);
}

A. C::print() B::print() A::print() A::print() B::print() C::print() A::print() A::print() A::print()
B. A::print() B::print() C::print() A::print() B::print() C::print() A::print() A::print() A::print()
C. A::print() B::print() C::print() A::print() B::print() C::print() B::print() B::print() B::print()
D. C::print() B::print() A::print() A::print() B::print() C::print() C::print() C::print() C::print()

正确答案

A

答案解析

C++中的多态性是通过将父类的指针或者引用传递给子类来实现的。
如果是直接传递一个对象,那么这个对象都是会直接访问父类而实现不了多态。

虚函数会具有动态绑定功能,会按照实际类型调用相关的函数。
1, a.print(); b.print(); c.print();
分别输出A::print() B::print() C::print(),
2,aa->print(); ab->print(); ac->print();
由于是虚函数,所以输出实际对象类型对应的print,因此输出A::print() B::print() C::print(),
3,void print(A a){ a.print();}
函数声明的形参为A类型的,相当于强制类型转换,因此调用print(A a)函数的输出都是A::print()(调用print(b)和print©的时候产生了切割,虚表指针产生了重新指向)

  1. 若char是一字节,int是4字节,指针类型是4字节,代码如下:
class CTest
{
    public:
        CTest():m_chData(‘\0),m_nData(0)
        {
        }
        virtual void mem_fun(){}
    private:
        char m_chData;
        int m_nData;
        static char s_chData;
};
char CTest::s_chData=’\0;

问:
(1)若按4字节对齐sizeof(CTest)的值是多少?
(2)若按1字节对齐sizeof(CTest)的值是多少?
请选择正确的答案。

A. 16 4
B. 16 10
C. 12 9
D. 10 10

正确答案: C

答案解析:

1 先找有没有virtual 有的话就要建立虚函数表,+4
2 static的成员变量属于类域,不算入对象中 +0
3 神马成员都没有的类,或者只有成员函数 +1
4 对齐法则,对大家都没有问题

  1. 下面有关继承、多态、组合的描述,说法错误的是?

A. 封装,把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏
B. 继承可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展
C. 隐藏是指派生类中的函数把基类中相同名字的函数屏蔽掉了
D. 覆盖是指不同的函数使用相同的函数名,但是函数的参数个数或类型不同

正确答案:D

答案解析:

重载:

只有在 同一类定义中的同名成员函数才存在重载关系 ,主要特点是 函数的参数类型和数目有所不同 ,但 不能出现函数参数的个数和类型均相同 ,仅仅依靠返回值类型不同来区分的函数,这和普通函数的重载是完全一致的。另外,重载和成员函数是否是虚函数无关

覆盖:

在派生类中覆盖基类中的同名函数,要求两个函数的参数个数、参数类型、返回类型都相同,且基类函数必须是虚函数。

隐藏:

派生类中的函数屏蔽了基类中的同名函数,
2个函数参数相同,但基类函数不是虚函数(和覆盖的区别在于基类函数是否是虚函数)。2个函数参数不同,无论基类函数是否是虚函数,基类函数都会被屏蔽(和重载的区别在于两个函数不在同一类中)。

  1. 下面关于多态性的描述,错误的是:

A. C++语言的多态性分为编译时的多态性和运行时的多态性
B. 编译时的多态性可通过函数重载实现
C. 运行时的多态性可通过模板和虚函数实现
D. 实现运行时多态性的机制称为动态绑定

正确答案: C

答案解析:

A,正确,分为编译时多态和运行时多态
B,编译时多态可以通过函数重载实现,具体表现在根据参数的个数和类型不同选择合适的同名函数
C,运行时多态通过虚函数实现,就是运行时根据对象类型自动选择正确的调用接口。模板属于编译时多态性,因为编译时自动根据模板生成模板函数。
D,运行时多态是根据对象类型自动选择正确的调用函数,也叫动态绑定。

  1. C++将父类的析构函数定义为虚函数,下列正确的是哪个?

A. 释放父类指针时能正确释放子类对象
B. 释放子类指针时能正确释放父类对象
C. 这样做是错误的
D. 以上全错

正确答案: A

答案解析:

C++中假设有基类为fa,它的派生类为son,如果有fa = new son();在delete fa或者释放fa的时候将只会调用基类的析构函数;如果基类的析构函数为虚函数,在delete fa或者释放*fa的时候会先调用派生类(这里也就是son)的析构函数,再调用基类的析构函数。

  1. 在继承虚基类的时候,必须重写基类中所有的纯虚函数

正确答案:

答案解析:

虚函数为了重载和多态的需要,在基类中是由定义的,即便定义是空,所以子类中可以重写也可以不写基类中的函数!
纯虚函数在基类中是没有定义的,必须在子类中加以实现

  1. 关于重载和多态正确的是

A. 如果父类和子类都有相同的方法,参数个数不同,将子类对象赋给父类后,由于子类继承于父类,所以使用父类指针
调用父类方法时,实际调用的是子类的方法
B. 选项全部都不正确
C. 重载和多态在C++面向对象编程中经常用到的方法,都只在实现子类的方法时才会使用

D.

class A{
	void test(float a){cout<<"1";}
};
class B:public A{
	void test(int b){cout<<"2";}
};
A *a=new A;
B *b=new B;
a=b;
a.test(1.1);
结果是1

正确答案: B

答案解析:

class默认属性为private,其中所有函数都不能在外部被调用,其为一;a.test(1.1)应该为a->test(1.1),其为二。

  1. 下面程序的输出是()
class A
{
public:
    void foo(){
        printf("1");
    }
    virtual void fun(){
        printf("2");
    }
};
class B: public A
{
public:
    void foo(){
        printf("3");
    }
    void fun(){
        printf("4");
    }
};
int main(void)
{
    A a;
    B b;
    A *p = &a;
    p->foo();
    p->fun();
    p = &b;
    p->foo();
    p->fun();
    A *ptr = (A *)&b;
    ptr->foo();
    ptr->fun();
    return 0;
}

A. 121434
B. 121414
C. 121232
D. 123434

正确答案: B

答案解析:

  1. 首先声明为A类型的指针指向实际类型为A的对象,调用的肯定是A的方法,输出1 2,
  2. 然后声明为A类型的指针指向实际类型为B的对象,则非虚函数调用A的方法,输出1,虚函数调用实际类型B的方法,输出4
  3. 声明类型为A的指针指向实际类型为B的对象,进行一个强制类型转换,其实这种父类指针指向子类会自动进行类型转换,所以是否强制类型转换都不影响结构,原理同上一步,结果输出1 4
  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值