1.C++的多态如何实现

1.C++的多态如何实现

1.多态基本概念

多态是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征。

多态性(polymorphism)提供接口与具体实现之间的另一层隔离,从而将”what”和”how”分离开来。多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性,项目不仅在最初创建时期可以扩展而且当项目在需要有新的功能时也能扩展

C++支持编译时多态(静态多态)和运行时多态(动态多态),运算符重载和函数重载就是编译时多态,而派生类和虚函数实现运行时多态。

静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)。如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就是说地址是早绑定的。而如果函数的调用地址不能编译不能在编译期间确定,而需要在运行时才能决定,这这就属于晚绑定(动态多态,运行时多态)。

2.多态的定义及实现

2.1多态的构成条件

在继承中要构成多态还有两个条件:

①. 必须通过基类的指针或者引用调用虚函数。

②. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

2.2虚函数

在C++语言中,当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定。因为我们直到运行时才能知道到底调用了哪个版本的虚函数,所以所有虚函数都必须有定义。通常情况下,如果我们不使用某个函数,则无须为该函数提供定义。但是我们必须为每一个虚函数都提供定义,而不管它是否被用到了,这是因为连编译器也无法确定到底会使用哪个虚函数。

(1)虚函数的定义

在实现C++多态时会用到虚函数。虚函数使用的其核心目的是通过基类访问派生类定义的函数。所谓虚函数就是在基类定义一个未实现的函数名,为了提高程序的可读性,建议后代中虚函数都加上virtual关键字。一般格式:

class base
{
public:
	base();
	virtual void test(); //定义的一个虚函数
private:
	char* basePStr;
};

上述代码在基类中定义了一个test的虚函数,所有可以在其子类重新定义父类的做法这种行为成为覆盖(override),或者为重写。

常见用法:声明基类指针,利用指针指向任意一个子类对象,调用相关的虚函数,动态绑定,由于编写代码时不能确定被调用的是基类函数还是那个派生类函数,所以被称为“”虚“”函数。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。

#include<iostream>  
using namespace std;

class A
{
public:
    void foo()
    {
        printf("1\n");
    }
    virtual void fun()
    {
        printf("2\n");
    }
};

class B : public A
{
public:
    void foo()  //隐藏:派生类的函数屏蔽了与其同名的基类函数
    {
        printf("3\n");
    }
    void fun()  //多态、覆盖
    {
        printf("4\n");
    }
};

int main(void)
{
    A a;
    B b;
    A* p = &a;
    p->foo();  //输出1
    p->fun();  //输出2
    p = &b;
    p->foo();  //取决于指针类型,输出1
    p->fun();  //取决于对象类型,输出4,体现了多态

    return 0;
}

(2)对虚函数的调用可能在运行时才被解析

当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数。被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。

//计算并打印销售给定数注的某种书符所得的费用
double print_total(ostream& os, const Quote& item, size_t n)
{
    //根据传入item形参的对象类型调用Quote::net _price 
    //或者Bulk_quote::net_price 
    double ret = item.net_price(n);
    os << "ISBN: " << item.isbn() //调用Quote::isbn
        << " # spld:" << n << " total due: " << ret << endl;
    return ret;
}

print_total函数通过其名为item的参数来进一步调用net_price,其中item的类型是&Quote。因为item是引用而且net_price是虚函数,所以到底调用net_price的哪个版本完全依赖于运行时绑定到item的实参的实际(动态)类型:

Quote base("0-201-82470-1", 50);
print_total(cout, base, 10);//调用Quote::net_price
Bulk_quote derived("0-201-82470-1", 50, 5, .19);
print_total(cout, derived, 10);//调用derived::net_price

在第一条调用语句中,item绑定到Quote类型的对象上,因此当print_total调用net_price时,运行在Quote中定义的版本。在第二条调用语句中,item绑定到Bulk_quote类型的对象上,因此print_total调用Bulk_quote定义的net_price。

必须要搞清楚的一点是,动态绑定只有当我们通过指针或引用调用虚函数时才会发生。

base = derived;//把derived的Quote部分拷贝给base
base.net_price(20);//调用Quote::net_price

当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来。例如,如果我们使用base调用net_price,则应该运行net_price的哪个版本是显而易见的。我们可以改变base表示的对象的值(即内容),但是不会改变该对象的类型。因此,在编译时该调用就会被解析成Quote的net_price。

(3)派生类中的虚函数

当我们在派生类中稷盖了某个虚函数时,可以再一次使用virtual关键字指出该函数的性质。然而这么做并非必须,因为一旦某个函数被声明成虚函数,则在所有派生类中它都是虚函数。
一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。
同样,派生类中虚函数的返回类型也必须与基类函数匹配。该规则存在一个例外,当类的虚函数返回类型是类本身的指针或引用时,上述规则无效。也就是说,如果D由B派生得到则基类的虚函数可以返回B*而派生类的对应函数可以返回D*,只不过这样的返回类型要求从D到B的类型转换是可访问的。

注意:基类中的虚函牧在派生类中隐含地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配。

(4)final 和 override 说明符

派生类如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同,这仍然是合法的行为。编译器将认为新定义的这个函数与基类中原有的函数是相互独立的。这时,派生类的函数并没有覆盖掉基类中的版本。就实际的编程习惯而言,这种声明往往意味着发生了错误,因为我们可能原本希望派生类能覆盖掉基类中的虚函数, 但是一不小心把形参列表弄错了。

要想调试并发现这样的错误显然非常困难。在C++11新标准中我们可以使用 override关键字来说明派生类中的虚函数。这么做的好处是在使得程序员的意图更加清晰的同时让编译器可以为我们发现一些错误,后者在编程实践中显得更加重要。如果我们 使用override标记了某个函数,但该函数并没有狻盖已存在的虚函数,此时编译器将报错:

struct B
{
    virtual void f1(int) const;
    virtual void f2();
    void f3();
};

struct D1 : B
{
    void f1(int) const override;//正确:f1于基类的f1匹配
    void f2(int) override;//错误:B没有形如f2(int)的函数
    void f3() override;//错误:f3不是虚函数
    void f4() override;//错误:B没有名为f4的函数
};

使用override所表达的意思是我们希望能覆盖基类中的虚函数而实际上并未做到,所以编译器会报错。

因为只有虚函数才能被覆盖,所以编译器会拒绝D1的f3。该函数不是B中的能函数,因此它不能被覆盖。类似的,f4的声明也会发生错误,因为B中根本就没有名为f4的函数。

还能把某个函数指定为final,如果我们已经把函数定义成final了,则之后 任何尝试覆盖该函数的操作都将引发错误:

struct D2 : B
{
    //从B继承f2()和f3(),覆盖f1(int)
    void f1(int) const final;//不允许后续的其他类覆盖f1(int)
};

struct D3 : D2
{
    void f2();//正确:覆盖从间接基类B继承而来的f2
    void f1(int) const;//错误:D2已经将f2声明成final
};

final和override说明符出现在形参列表(包括行何const或引用修饰符)以及尾置返回类型之后。

(5)虚函数与默认实参

和其他函数一样,虚函数也可以拥有默认实参。如果某次函数调用使用了默认实参,则该实参由本次调用的静态类型决定。
换句话说,如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。此时,传入派生类函数的将是基类函数定义的默认实参。如果派生类函数依赖不同的实参,则程序结果将与我们的预期不符。

建议:如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。

例子:

#include <iostream>
using namespace std;
class A
{
public:
    virtual void Display(int i = 0)
    {
        cout << "A:  " << i << endl;
    }
};

class B : public A
{
public:
    virtual void Display(int i = 1)
    {
        cout << "B:  " << i << endl;
    }
};

int main()
{

    B b;
    A* p = &b;

    p->Display(4);
    p->Display();  //virtual是动态绑定,而缺省参数值却是静态绑定

    return 0;
}

输出:

B:  4
B:  0

(6)回避虚函数的机制

在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本。仗用作用域运算符可以实现这一目的,例如下面的代码:

//强行调用基类中定义的面数版本而不管baseP的动态类型到底是什么 
double undiscounted = baseP->Quote::net_price(42);

该代码强行调用Quote的net_price函数,而不管baseP实际指向的对象类型到底是什么。该调用将在编译时完成解析。

通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。

什么时候我们需耍回避虚函数的默认机制呢?通常是当一个派生类的虚函数调用它覆盖的基类的虚函数版本时。在此情况下,基类的版本通常完成继承层次中所有类型都要做的共同任务,而派生类中定义的版本虚要执行一些与派生类本身密切相关的操作。

警告:如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。

(7)基类和派生类的虚函数表是否为同一个

  • 派生类实现基类的虚函数时,基类中虚函数表和派生类的虚函数表地址不同,基类虚函数表中的虚函数地址和派生类虚函数表中的虚函数地址不同;
  • 派生类不实现基类的虚函数时,基类中虚函数表和派生类中虚函数表地址不同,基类虚函数表中的虚函数地址和派生类虚函数表中的虚函数地址相同。

(8)为什么基类的析构函数需要定义为虚函数?

在C++实现多态里,有一个关于 析构函数的重写问题:基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数。这里他们的函数名不相同,看起来违背了重写的规则,但实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。那么为什么要把基类中的析构函数写成虚函数呢?

当使用多态特性,让基类指针指向派生类对象时,如果析构函数不是虚函数,通过基类指针销毁派生类对象时,会调用静态绑定的析构函数,也就是基类的析构函数,从而只能销毁属于基类的元素,导致派生类析构不完全,程序就会出现资源泄露或未定义行为。

当派生类中不存在使用动态资源或其他自定义析构行为时,可以不写为虚析构函数,来提高程序效率。但为了程序的可扩展性和健壮性,在使用多态特性时,一般都建议将基类的析构函数定义为虚函数。

在 C++ 中,只需要在基类中定义虚析构函数,派生类会自动继承这个虚属性。也就是说,如果基类的析构函数被声明为虚函数,那么所有派生类的析构函数都将自动成为虚函数。

如果你在派生类中显式地声明析构函数,无论你是否将其声明为虚函数,它都将是虚函数。因此,在派生类中声明虚析构函数并非必要,但是为了代码的清晰性,很多开发者仍然会在派生类中显式地声明虚析构函数。

这样的话,当我们看到派生类的代码时,我们就能立刻知道其析构函数是虚函数,这使得代码更易于理解。这也是一种被称为 "programming by contract" 的编程习惯,意味着类的设计者通过接口明确地表明了类的使用方式。

总结来说,基类的析构函数声明为虚函数后,派生类的析构函数自动成为虚函数,不论是否显式声明为虚函数。但为了代码清晰,有时候我们仍然会在派生类中显式地声明虚析构函数。

举个例子:

  • 当基类析构函数不是虚函数时:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;

class A 
{
public:
	A() { cout << "A的构造" << endl; }
	~A() { cout << "A的析构" << endl; }
	void Work() 
	{
		cout << "A工作" << endl;
	}
};//基类

class B :public A
{
public:
	B() { cout << "B的构造" << endl; }
	~B() { cout << "B的析构" << endl; }
	void Work() { cout << "B工作" << endl; }
}; //派生类

int main()
{
	A* p = new B;  //派生类对象赋给基类指针
	p->Work();//此时调用的是基类的成员函数,因为基类的成员函数覆盖了派生类的同名成员函数
	delete p;

	system("pause");
	return EXIT_SUCCESS;
}

输出:

A的构造
B的构造
A工作
A的析构
请按任意键继续. . .

可以看到在delete p的时候只调用了基类A的析构函数,并没有调用派生类B的析构函数,导致内存释放并不完全,出现内存泄漏的问题。

  • 然后将基类析构函数写为虚函数时
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;

class A
{
public:
	A() { cout << "A的构造" << endl; }
	virtual ~A() { cout << "A的析构" << endl; }
	void Work()
	{
		cout << "A工作" << endl;
	}
};//基类

class B :public A 
{
public:
	B() { cout << "B的构造" << endl; }
	~B() { cout << "B的析构" << endl; } //在派生类中重写的成员函数可以不加virtual关键字
	void Work() { cout << "B工作" << endl; }
};//派生类

int main()
{
	A* p = new B;  //派生类对象赋给基类指针
	p->Work();//此时调用的是基类的成员函数,因为基类的成员函数覆盖了派生类的同名成员函数
	delete p;

	system("pause");
	return EXIT_SUCCESS;
}

输出:

A的构造
B的构造
A工作
B的析构
A的析构
请按任意键继续.

可以看到这次在delete p的时候调用了派生类的析构函数,因为在调用派生类的析构函数后会自动调用基类的析构函数,这样整个派生类的对象被完全释放。

另外上面两个过程中我们发现执行 "p->Work();" 时,也就是p在调用同名成员函数的时候,调用的始终是基类的成员函数,这是因为基类的成员函数覆盖了派生类的同名成员函数,如果想要调用派生类的成员函数,同样将Work()设置为虚函数即可。

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;

class A 
{
public:
	A() { cout << "A的构造" << endl; }
	virtual ~A() { cout << "A的析构" << endl; }
	virtual void Work() 
	{
		cout << "A工作" << endl;
	}
};

class B :public A
{
public:
	B() { cout << "B的构造" << endl; }
	~B() { cout << "B的析构" << endl; }
	void Work() { cout << "B工作" << endl; }
};

int main()
{
	A* p = new B;  //派生类指针转化成基类指针
	p->Work();
	delete p;
	system("pause");
	return EXIT_SUCCESS;
}

输出:

A的构造
B的构造
B工作
B的析构
A的析构
请按任意键继续. . .

内存切割:

2.3多态的切片示意图

(1)示例1:给一个student的子类对象(临时对象也行),然后把这个对象赋给一个父类指针,通过这个父类指针就可以访问student子类的虚拟函数

(2)示例2:假设B是子类,A是父类,new一个B类的临时对象,然后把这个临时对象赋给一个父类指针A* p2,通过这个父类指针p2就可以访问子类B的虚拟函数func

 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*p1 = new B;
	//p1->test();	这个是多态调用,下有讲解 二->6
	p1->func();	//普通调用
 
	A*p2 = new B;
	p2->func();	//多态调用
 
	return 0;
}

输出:

B->0
B->1
请按任意键继续. . .

2.4多态演示

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;

class Person
{
public:
	Person(const char* name)
		:_name(name)
	{}

	// 虚函数
	virtual void BuyTicket()
	{
		cout << _name << "Person:买票-全价 100¥" << endl;
	}

protected:
	string _name;
	//int _id;
};

class Student : public Person 
{
public:
	Student(const char* name)
		:Person(name)
	{}

	// 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
	virtual void BuyTicket()
	{
		cout << _name << " Student:买票-半价 50 ¥" << endl;
	}
};

void Pay(Person& ptr)
{
	ptr.BuyTicket();
}

int main()
{
	string name;
	cin >> name;
	Student s(name.c_str());
	Pay(s);

	system("pause");
	return EXIT_SUCCESS;
}

输出:

悟空
悟空 Student:买票-半价 50 ¥
请按任意键继续. . .

买票场景下的多态 完整代码

普通人 买票时,是全价买票; 学生 买票时,是半价买票; 军人 买票时是优先买票。

class Person
{
public:
	Person(const char* name) : _name(name)
	{
	}

	// 虚函数
	virtual void BuyTicket() { cout << _name << "Person:买票-全价 100¥" << endl; }

protected:
	string _name;
	//int _id;
};

class Student : public Person 
{
public:
	Student(const char* name) : Person(name)
	{
	}

	// 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
	virtual void BuyTicket() { cout << _name << " Student:买票-半价 50 ¥" << endl; }
};

class Soldier : public Person
{
public:
	Soldier(const char* name) : Person(name)
	{
	}

	// 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
	virtual void BuyTicket() { cout << _name << " Soldier:优先买预留票-88折 88 ¥" << endl; }
};

// 多态两个要求:
// 1、子类虚函数重写的父类虚函数 (重写:三同(函数名/参数/返回值)+虚函数)
// 2、父类指针或者引用去调用虚函数。

//void Pay(Person* ptr)
//{
//	ptr->BuyTicket();
//}

void Pay(Person& ptr)
{
	ptr.BuyTicket();
}

// 不能构成多态
//void Pay(Person ptr)
//{
//	ptr.BuyTicket();
//}

int main()
{
	int option = 0;
	cout << "=======================================" << endl;
	do
	{
		cout << "请选择身份:";
		cout << "1、普通人 2、学生 3、军人" << endl;
		cin >> option;
		cout << "请输入名字:";
		string name;
		cin >> name;
		switch (option)
		{
		case 1:
		{
			Person p(name.c_str());
			Pay(p);
			break;
		}
		case 2:
		{
			Student s(name.c_str());
			Pay(s);
			break;
		}
		case 3:
		{
			Soldier s(name.c_str());
			Pay(s);
			break;
		}
		default:
			cout << "输入错误,请重新输入" << endl;
			break;
		}
		cout << "=======================================" << endl;
	} while (option != -1);

	return 0;
}

输出:

=======================================
请选择身份:1、普通人 2、学生 3、军人
2
请输入名字:悟空
悟空 Student:买票-半价 50 ¥
=======================================
请选择身份:1、普通人 2、学生 3、军人
1
请输入名字:菩提老祖
菩提老祖Person:买票-全价 100¥
=======================================
请选择身份:1、普通人 2、学生 3、军人
3
请输入名字:唐僧
唐僧 Soldier:优先买预留票-88折 88 ¥
=======================================
请选择身份:1、普通人 2、学生 3、军人
4
请输入名字:222
输入错误,请重新输入
=======================================
请选择身份:1、普通人 2、学生 3、军人

解释 2:父类指针或者引用去调用虚函数,传值调用不构成多态。

用子类也不行,必须用父类,比如你用个student,那么你的Person或者Soldier就传不进形参

void Pay(Person* ptr)     //指针调用可以
{
	ptr->BuyTicket();
}
 
void Pay(Person& ptr)    //引用调用可以
{
	ptr.BuyTicket();
}
 
// 不能构成多态
//void Pay(Person ptr)    //传值调用不可以
//{
//	ptr.BuyTicket();
//}

2.5虚函数重写的例外

2.5.1协变(父类与子类虚函数返回值类型不同)

子类重写父类虚函数时,与父类虚函数返回值类型不同 称为协变。

虚函数重写对返回值要求有一个例外:协变,协变是子类虚函数与父类虚函数返回值类型不同,但子类和父类的返回值类型也必须是父子关系指针和引用。

子类虚函数没有写virtual,f依旧是虚函数,因为子类先继承了父类函数接口声明(接口部分是virtual A* f() ),重写是重写父类虚函数的实现部分( 重写函数实现部分是用子类虚函数的{ }里面的函数实现替代父类虚函数的{ }里面的函数实现 ) ps:我们自己写的时候子类虚函数也写上virtual

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;

class A {};
class B : public A {};

// 虚函数重写对返回值要求有一个例外:协变,父子关系指针和引用
// 
class Person
{
public:
	virtual A* f()
	{
		cout << "virtual A* Person::f()" << endl;
		return nullptr;
	}
};

class Student : public Person
{
public:
	// 子类虚函数没有写virtual,f依旧时虚函数,因为先继承了父类函数接口声明
	// 重写父类虚函数实现
	// ps:我们自己写的时候子类虚函数也写上virtual
	// B& f() { 
	virtual B* f()
	{
		cout << "virtual B* Student::f()" << endl;
		return nullptr;
	}
};

int main()
{
	Person p;
	Student s;
	Person* ptr = &p;
	ptr->f();

	ptr = &s;
	ptr->f();

	system("pause");
	return EXIT_SUCCESS;
}

输出:

virtual A* Person::f()
virtual B* Student::f()
请按任意键继续. . .

2.5.2析构函数的重写-析构函数名统一会被处理成destructor()

基类与派生类析构函数的名字不同

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

只有派生类 Student 的析构函数重写了 Person 的析构函数,下面的 delete 对象调用析构函数,才能构成多态,才能保证 p1 和 p2 指向的对象正确的调用析构函数。
函数名处理成destructor() 才能满足多态:
如果父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加virtual关键字,都与父类的析构函数构成重写,虽然父类与子类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;

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

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

int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	delete p2;

	system("pause");
	return EXIT_SUCCESS;
}

输出:

~Person()
~Student()
~Person()
请按任意键继续. . .

2.注意:期望delete ptr调用析构函数是一个多态调用, 如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;

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

class Student : public Person
{
public:
	// Person析构函数加了virtual,关系就变了
	// 重定义(隐藏)关系 -> 重写(覆盖)关系
	virtual ~Student()    //这里virtual加不加都行
	{
		cout << "~Student()" << endl;
		delete[] _name;
		cout << "delete:" << (void*)_name << endl;
	}

private:
	char* _name = new char[10] { 'j', 'a', 'c', 'k' };
};

int main()
{
	// 对于普通对象是没有影响的
	//Person p;
	//Student s;

	// 期望delete ptr调用析构函数是一个多态调用
	// 如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数
	Person* ptr = new Person;
	delete ptr; // ptr->destructor() + operator delete(ptr)

	ptr = new Student;
	delete ptr;  // ptr->destructor() + operator delete(ptr)

	system("pause");
	return EXIT_SUCCESS;
}

输出:

~Person()
~Student()
delete:000001D5368ECEA0
~Person()
请按任意键继续. . .

这是一个关于C++虚函数(尤其是虚析构函数)的示例。下面是程序的运行过程:

首先,程序定义了两个类,一个是基类Person,另一个是派生类Student。在这两个类中,都有一个析构函数。在Person类中,析构函数是虚函数,这意味着它可以在派生类中被重写。

在main()函数中,首先创建了一个指向Person类对象的指针,并分配了内存。然后,该指针被删除,调用Person类的析构函数,并释放内存。输出的结果是"~Person()"。

然后,该指针被重新赋值为一个新的Student类对象的地址,并再次分配内存。当删除这个指针时,会首先调用Student类的析构函数,然后调用Person类的析构函数(因为Student类是从Person类派生的)。Student类的析构函数输出"~Student()"和对应的被删除的内存地址,然后Person类的析构函数输出"~Person()"。最后,释放内存。

输出的结果是:

~Person()
~Student()
delete:<address>
~Person()

请注意,虚析构函数在这里非常重要,因为它允许在删除指向派生类对象的基类指针时,正确地调用派生类的析构函数。如果Person类的析构函数不是虚函数,那么删除指向Student类对象的Person类指针时,只会调用Person类的析构函数,不会调用Student类的析构函数,这可能会导致内存泄漏。

所以,如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数,这样可以避免在删除基类指针(指向派生类对象)时产生的问题。

2.6接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
所以就有了 子类虚函数没有写virtual,依旧是虚函数;子类虚函数使用的是父类虚函数的缺省参数,只是重写了实现
多态的坑题目(考接口继承)
子类虚函数没有写virtual,func 依旧是虚函数,因为子类先继承了父类函数接口声明(接口部分是virtual A* f() ),重写是重写父类虚函数的实现部分( 重写函数实现部分是用子类虚函数的{ }里面的函数实现替代父类虚函数的{ }里面的函数实现 ) ps:我们自己写的时候子类虚函数也写上virtual

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

(只有重写要求原型相同,原型相同就是指 函数名、参数、返回值都相同)

函数重载:在同一个作用域中,两个函数的函数名相同,参数个数,参数类型,参数顺序至少有一个不同,函数返回值的类型可以相同,也可以不相同。

重写(也叫做覆盖)是指在继承体系中子类定义了和父类函数名,函数参数,函数返回值完全相同的虚函数。此时构成多态,根据对象去调用对应的函数。

重定义(也叫做隐藏)是指在继承体系中,子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) ,此时子类的函数会屏蔽掉父类的那个同名函数。

3.抽象类

3.1概念

在面向对象编程中,抽象类是一种特殊的类,它不能被实例化。相反,你通常会创建一个或多个派生类,继承自抽象类,并实现抽象类中的所有纯虚函数。抽象类是一种对某一类对象的通用性质的抽象表示,这些性质在具体的实现(派生类)中有所不同。

在C++中,抽象类通过至少包含一个纯虚函数来定义。纯虚函数是在基类中声明但没有实现的函数。

抽象类特点:

①在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象,但可以new别的对象来定义指针,例如

Car* pBMW = new BMW;  

②子类继承后也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。

③父类的纯虚函数强制了派生类必须重写,才能实例化出对象(跟override异曲同工,override是放在子类虛函数,检查重写。功能有一些重叠和相似),另外纯虚函数更体现出了接口继承。

④纯虚函数也可以写实现{ },但没有意义,因为是接口继承,{ }中的实现会被重写;父类没有对象,所以无法调用纯虚函数

/*
 抽象类  -- 在现实一般没有具体对应实体
 不能实例化出对象
 间接功能:要求子类需要重写,才能实例化出对象
*/

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;

class Car
{
public:
	virtual void Drive() = 0;
	//	// 实现没有价值,因为没有对象会调用他
	//	/*virtual void Drive() = 0
	//	{
	//		cout << " Drive()" << endl;
	//	}*/
};

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();
}

int main()
{
	Test();

	system("pause");
	return EXIT_SUCCESS;
}

3.2接口继承和实现继承

  • 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
  • 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
  • 如果不实现多态,不要把函数定义成虚函数。
  • 子类虚函数没有写virtual,依旧是虚函数;子类虚函数使用的是父类虚函数的缺省参数,只是重写了实现

多态的坑题目(考接口继承)
子类虚函数没有写virtual,func 依旧是虚函数,因为子类先继承了父类函数接口声明(接口部分是virtual A* f() ),重写是重写父类虚函数的实现部分( 重写函数实现部分是用子类虚函数的{ }里面的函数实现替代父类虚函数的{ }里面的函数实现 ) ps:我们自己写的时候子类虚函数也写上virtual

p->test(),调用test中的this指针类型是A*,但指向的是对象B* p中的内容,类B中继承的test函数中又调用func函数,func函数没有写virtual 但依旧是虚函数,只要是虚函数重写就是接口继承,子类先继承了父类函数接口声明(父类接口部分是virtual void func(int va1=1) ),重写是重写父类虚函数的实现部分( 即使用子类的函数的实现部分{}内容 ),所以缺省函数用的是父类的1,实现用的子类的函数实现,打印结果是 B->1

4.多态的原理

4.1虚函数表指针

这里常考一道笔试题:sizeof(Base)是多少?

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;


class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
	char ch = 'A';
};

int main()
{
	Base bb;
	cout << sizeof(Base) << endl;
	
	system("pause");
	return EXIT_SUCCESS;
}

可能刚看到这个题目的时候,都会觉得答案是8个字节,但是我们在打印后却发现是12个字节;这是为什么呢?

因为有了虚函数,这个对象里面就多了一个成员,虚函数表指针__vfptr。

64为系统中为16字节

4.2虚函数表

和菱形虚拟继承的虚基表不一样,那个存的是偏移量

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

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

4.3虚表存储

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;

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;
	}

	void Func3()
	{
		cout << "Derive::Func3()" << endl;
	}
private:
	int _d = 2;
};

int main()
{
	cout << sizeof(Base) << endl;
	Base b;

	cout << sizeof(Derive) << endl;
	Derive d;

	Base* p = &b;
	p->Func1();
	p->Func3();

	p = &d;
	p->Func1();
	p->Func3();

	//	/*Base& r1 = b;    引用也是多态调用
	//	r1.Func1();
	//	r1.Func3();
	//
	//	Base& r2 = d;
	//	r2.Func1();
	//	r2.Func3();*/

	system("pause");
	return EXIT_SUCCESS;
}

输出:

16
24
Base::Func1()
Base::Func3()
Derive::Func1()
Base::Func3()
请按任意键继续. . .

(1)虚函数重写/覆盖 语法与原理层解释

--语法层的概念: 派生类对继承基类虚函数实现进行了重写
--原理层的概念: 子类的虚表,拷贝父类虚表进行了修改,覆盖重写那个虚函数

(2)虚表存储解释

无论是子类还是父类中只要有虚函数都会多存一个指针,这个指针叫虚表指针,他指向一个指针数组,指针数组中存着各个虚函数的地址。

Func1是重写的函数,Base[0]中存的地址并非真正的Derive中Func1的地址,而是通过call这个地址,找到这个地址的内容,这个地址的内容指令又是jump到地址2,地址2存的才是真正的Derive中Func1的地址

多态调用和普通调用底层解释(编译时多态/运行时多态)

C++语言的多态性分为编译时的多态性和运行时的多态性

①运行时多态是动态绑定,也叫晚期绑定;运行时的多态性可通过虚函数实现。

②编译时多态是静态绑定,也叫早期绑定,主要通过重载实现;编译时的多态性可通过函数重载和模板实现。

-在运行期间,通过传递不同类的对象,编译器选择调用不同类的虚函数:编译期间,编译器主要检测代码是否违反语法规则,此时无法知道基类的指针或者引用到底引用那个类的对象,也就无法知道调用哪个类的虚函数。在程序运行时,才知道具体指向那个类的对象,然后通过虚表调用对应的虚函数,从而实现多态。

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;

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

	void Buy() { cout << "Person::Buy()" << endl; }
};

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

	void Buy() { cout << "Student::Buy()" << endl; }
};

void Func1(Person* p)
{
	//跟对象有关,指向谁调用谁 -- 运行时确定函数地址
	p->BuyTicket();
	//跟类型有关,p类型是谁,调用就是谁的虚函数  -- 编译时确定函数地址
	p->Buy();
}

int main()
{
	Person p;
	Student s;

	Func1(&p);
	Func1(&s);

	system("pause");
	return EXIT_SUCCESS;
}

输出:

买票-全价
Person::Buy()
买票-半价
Person::Buy()
请按任意键继续. . .

重点总结:
多态调用:运行时决议-- 运行时确定调用函数的地址(不管对象类型,查对应的虚函数表,如果是父类的对象,就查看父类对象中存的虚表;如果是子类切片后的对象,就查看子类切片后对象中存的虚表)
普通调用:编译时决议-- 编译时确定调用函数的地址(只看对象类型去确定调用哪个对象中的函数

(3)父类赋值给子类对象,也可以切片。为什么实现不了多态?

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;

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;
	}

	void Func3()
	{
		cout << "Derive::Func3()" << endl;
	}
private:
	int _d = 2;
};

int main()
{
	cout << sizeof(Base) << endl;
	Base b;

	cout << sizeof(Derive) << endl;
	Derive d;


	// 父类赋值给子类对象,也可以切片。为什么实现不了多态?
	Base r1 = b;
	r1.Func1();
	r1.Func3();

	Base r2 = d;
	r2.Func1();
	r2.Func3();

	system("pause");
	return EXIT_SUCCESS;
}

输出:

16
24
Base::Func1()
Base::Func3()
Base::Func1()
Base::Func3()
请按任意键继续. . .

我们发现r2没有拷贝子类d的虚表,则r2虚表中存的还是父类的虚表,调用时还是调用父类的func1,而不是子类切片后的func1

(4)静态和动态的多态(了解)

(5)非多态的虚函数Func4在监视窗口被隐藏了,看不到,只能通过内存看到

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;

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;
	}

	void Func3()
	{
		cout << "Derive::Func3()" << endl;
	}

	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}
private:
	int _d = 2;
};

// 取内存值,打印并调用,确认是否是func4
//typedef void(*)() V_FUNC; // 不支持这种写法
typedef void(*V_FUNC)();    // 只能这样定义函数指针

// 打印虚表
//void PrintVFTable(V_FUNC a[])
void PrintVFTable(V_FUNC* a)
{
	printf("vfptr:%p\n", a);

	for (size_t i = 0; a[i] != nullptr; ++i)    //VS下的虚表以空指针结束
	{
		printf("[%d]:%p->", i, a[i]);    //打印虚表中的所有函数的地址
		V_FUNC f = a[i];                //调用函数中打印函数,可以知道是哪个func函数
		f();
	}
}

int c = 2;

int main()
{
	Base b;
	Derive d;
	PrintVFTable((V_FUNC*)(*((intptr_t*)&d)));    //下有解释

	system("pause");
	return EXIT_SUCCESS;
}

PrintVFTable((V_FUNC)(((int*)&d))); 解释:

因为对象中存虚表指针,虚表指针中存的是虚表(一个指针数组),则需要先解引用访问到这个对象的前四个字节内容(存的就是虚表指针),此时的虚表指针 ((int)&d)是一个int类型,再把虚表指针类型强转成指针数组类型才能传参

监视窗口和内存窗口:

(6)同一类型对象,共用一个虚表

一个类型公共一个虚表,所有这个类型对象都存这个虚表指针

(7)虚表存在常量区/代码段

不可能存在栈,栈区是建立栈帧,出作用域栈帧销毁, 虚表是一个永久的存在,排除栈。

不可能存在堆区,堆区是动态申请,最后动态释放

可以存在静态区或者常量区,最可能存在常量区。通过下面打印地址可见虚表存储地址离常量区地址最近


4.4多继承,虚表的存储(一个子类继承两个父亲时)

大体的结论就是:func1是重写的函数,在子类的两个父类的虚表中存储的func1地址不相同,但是通过一系列的call这个地址,这个地址的内容又是jump到另一个指令,最终都会跳到子类重写的func1地址上

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()
{
	printf("%p\n", &Derive::func1);
 
	Derive d;
	//PrintVTable((VFPTR*)(*(int*)&d));
	PrintVTable((VFPTR*)(*(int*)&d));    
	PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1))));
}
PrintVTable((VFPTR*)(*(int*)&d)); 

因为对象中存虚表指针,虚表指针中存的是虚表(一个指针数组),则需要先解引用访问到这个对象的前四个字节内容(存的就是虚表指针),此时的虚表指针 *((int*)&d)是一个int类型,再把虚表指针类型强转成指针数组类型才能传参

PrintVTable((VFPTR)((int*)((char*)&d+sizeof(Base1))));

是找到Base2的虚表地址后再解引用找到虚表(直接加2个int字节也能找到base2,考虑Base1可能不单单是2个int大小,这里建议用sizeof(Base1) )



结论: Derive对象Base2虚表中func1时,是Base2指针ptr2去调用。但是这时ptr2发生切片指针偏移,需要修正。中途就需要修正存储this指针ecx的值

4.5再次理解多态构成的条件

为什么必须要是父类的指针或引用来调用虚函数,为什么不能是对象调用?

 指针和引用调用、对象调用本质上是子类切片完成的,指针和引用的切片只针对子类中父类的那一部分,其他都不管,只管与父类一样的内容(虚表中)切片过去;如果是对象切片,就是将虚表里的内容全部切片过去(因为都是要调用构造函数的),那么就有可能父类也指向了子类的虚表,那就乱套了;

 可以理解为:指针和引用是将虚表的指针拷贝过去了,对象是将虚表中的内容拷贝过去了;

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

5.1单继承中的虚函数表

class Base
{
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a = 1;
};
 
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 = 1;
};
 
int main()
{
	Base b;
	Derive d;
	return 0;
}

5.1.1监视窗口与内存查看

5.1.2使用代码查看

观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。

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;
	VFPTR* vTableb = (VFPTR*)(*(int*)&b);
	PrintVTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	PrintVTable(vTabled);
	return 0;
}

思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr

先取b的地址,强转成一个int*的指针
再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
虚表指针传递给PrintVTable进行打印虚表
需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。

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;
};

5.2.1监视窗口与内存查看

5.2.2使用代码查看

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 )
    A: 继承 B: 封装 C: 多态 D: 抽象
  2. ( D )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的 调用则可以关联于具体的对象。
    A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
  3. 面向对象设计中的继承和组合,下面说法错误的是?( C )
    A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
    B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
    C:优先使用继承,而不是组合,是面向对象设计的第二原则
    D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现
  4. 以下关于纯虚函数的说法,正确的是( A )
    A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类
    C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数
  5. 关于虚函数的描述正确的是( B )
    A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型
    B:内联函数不能是虚函数
    C:派生类必须重新定义基类的虚函数
    D:虚函数可以是一个static型的函数
  6. 关于虚表说法正确的是( D )
    A:一个类只能有一张虚表
    B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
    C:虚表是在运行期间动态生成的
    D:一个类的不同对象共享该类的虚表
  7. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则(D )
    A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
    B:A类对象和B类对象前4个字节存储的都是虚基表的地址
    C:A类对象和B类对象前4个字节存储的虚表地址相同
    D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表
  8. 下面程序输出结果是什么? ( A )
    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
#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;
}
  1. 多继承中指针偏移问题?下面说法正确的是( C )
    A:p1 == p2 == p3 B:p1 < p2 < p3
    C:p1 == p3 != p2 D:p1 != p2 != p3
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;
}
  1. 以下程序输出结果是什么( B )
    A: A->0 B: B->1 C: A->1
    D: B->0 E: 编译出错 F: 以上都不正确
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;
}

6.2问答题

1.什么是多态?
多态首先是建立在继承的基础上的,先有继承才能有多态。多态是指不同的子类在继承父类后分别都重写覆盖了父类的方法,即父类同一个方法,在继承的子类中表现出不同的形式。

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

3.多态的实现原理?
当子类继承了父类的虚函数并完成重写,则就构成了多态,其底层是父类和子类都有一个虚表指针指向了一个虚表,这个虚表是用来存放虚函数的地址的(不是真正的地址,可以理解为间接地址),当父类的指针和引用来调用虚函数时,取决于对象本身(即接受的父类就调用父类,接受的是子类就调用子类),父类和子类就会分别去各自的虚表指针里找到相应的虚函数。

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

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

6.构造函数可以是虚函数吗?
不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

7.析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
可以,并且最好把基类的析构函数定义成虚函数。

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

9.虚函数表是在什么阶段生成的,存在哪的?
虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

10.C++菱形继承的问题?虚继承的原理?
菱形继承的问题是子类对象当中会有两份父类的成员,因此会导致数据冗余和二义性的问题。
虚继承对于相同的虚基类在对象当中只会存储一份,若要访问虚基类的成员需要通过虚基表获取到偏移量,从而找到相应的虚基类成员,解决了数据冗余和二义性的问题。

11.什么是抽象类?抽象类的作用?
含 有纯虚拟函数的类称为抽象类,它不能生成对象;抽象类强制子类必须重写虚函数,否则无法实例化对象;另外抽象类体现出了接口继承关系。

参考:

【C++】—— 多态

C++:多态 详解

C++:基类析构函数为什么要定义为虚函数

posted @ 2023-08-03 07:58  CodeMagicianT  阅读(88)  评论(0编辑  收藏  举报