继承与多态

本文详细介绍了C++中的继承概念,包括派生类中从基类继承的成员访问限定、初始化方式及内存分配。讨论了重载、隐藏和覆盖的区别,重点解析了虚函数的动态绑定机制,以及虚函数表的工作原理,帮助读者深入理解C++的继承和多态特性。
摘要由CSDN通过智能技术生成

继承

一个类是另一个类的一种(a kind of)

// 人
class People
{
public:
	void eat(string foot)
	{
		cout << "eat:" << foot << endl;
	}

	void sleep()
	{
		cout << "sleep" << endl;
	}
private:
};

//学生
class Student:public People
{
public:
	void learn()
	{
		cout << "learn" << endl;
	}
private:
};

//对于Student来说,People 的每一个特征Student都要拥有,
//不仅是Student 如果现在需要一个teacher类,对于People 的特征也要拥有,
//这样我们可以将People定义为一个基类,所有拥有这些特征的都可以继承这个类,
//这样就减少了不必要的代码重复
int main()
{
	Student st;
	st.eat("红烧肉");
	return 0;
}

派生类中从基类中继承来的成员的访问限定

  • 派生类公有继承基类
基类派生类类外
publicpublic可访问
protectedprotected不可访问
private不可访问不可访问
  • 派生类保护继承基类
基类派生类类外
publicprotected不可访问
protectedprotected不可访问
private不可访问不可访问
  • 派生类私有继承基类
基类派生类类外
publicprivate不可访问
protectedprivate不可访问
private不可访问不可访问

派生类怎么初始化从基类继承来的成员

class People
{
public:
	People(int m):ma(m){ cout << "People()" << endl; }

	~People(){ cout << " ~People()" << endl; }

	void eat(string foot) { cout << "eat:" << foot << endl; }

	void sleep() { cout << "sleep" << endl; }
private:
	int ma;
};


class Student:public People
{
public:
	//派生类必须通过调用基类的构造函数对基类的成员进行初始化
	//如果基类不存在默认的构造函数,则在派生类的初始化列表中进行初始化
	//如果基类存在默认的构造函数,如果不在初始化列表中初始化,
	//则默认调用基类的默认构造函数
	Student(int m):People(m)
	{ cout << "Student()" << endl; }

	~Student(){ cout << "~Student" << endl; }

	void learn() { cout << "learn" << endl; }
private:
};

int main()
{
	Student st(1);
	st.eat("红烧肉");
	return 0;
}

在这里插入图片描述
上图可以看出,先调用基类的构造函数,再调用派生类的构造函数,
析构的时候顺序相反。

基类对象能不能赋给派生类对象?不可以
派生类对象能不能赋给基类对象?可以
基类指针指向派生类对象?可以
派生类指针指向基类对象?不可以

派生类与基类的内存

class Base
{
public:
	void Show() { cout << "Base::Show()" << endl; }
	void Show(int a) { cout << "Base::Show(int)" << endl; }
private:
	int ma;
	int mb;
};

class Derived:public Base
{
public:
	void Show() { cout << "Derived::Show()" << endl; }
private:
	int mc;
};

int main()
{
	cout << "Base size:" << sizeof(Base) << endl;
	cout << "Derived size:" << sizeof(Derived) << endl;
	return 0;
}

输出结果
在这里插入图片描述
同一个类的所有对象都有自己的成员变量,所有对象共享一套成员方法。
所以sizeof()计算类的大小的时候,只计算成员变量的大小,在上面的例子中,派生类继承了基类的所有的成员变量域成员方法,所以计算派生类的大小的时候,要加上基类的大小。

重载、隐藏、覆盖

  1. 重载

如果两个函数的 函数名相同,参数列表不同,在同一个作用域中,则这两个函数叫重载函数

class Base
{
public:
	//这两个函数为重载函数
	void Show() { cout << "Show()" << endl; }
	void Show(int a) { cout << "Show(int)" << endl; }
};

问题: 为什么c++ 中支持重载,c语言中不支持重载?重载实现的原理是什么?
因为在c语言中,编译阶段产生符号表的时候,每个函数产生一个符号,符号的产生是根据函数名来确定的,而c++ 中,符号的产生是根据函数名和参数列表共同确定的,所以,函数名相同,参数列表不同在编译阶段中产生的符号是不同的,在链接的过程中连接器根据符号表查找函数的的定义,不会产生冲突。

  1. 隐藏
    在基类和派生类中,函数名相同,叫做隐藏
class Base
{
public:
	void Show() { cout << "Base::Show()" << endl; }
	void Show(int a) { cout << "Base::Show(int)" << endl; }
};

class Derived:public Base
{
public:
	void Show() { cout << "Derived::Show()" << endl; }
};

int main()
{
	Derived d;
	d.Show();					//访问派生类的Show()
	d.Show(10);					//编译错误,派生类中没有函数参数的Show()函数
	d.Base::Show();				//调用基类的Show()
	d.Base::Show(10);			//调用基类的Show(int)
	return 0;
}

因为派生类中Show() 将基类中的Show函数隐藏了,所以通过对象访问只能访问派生类的Show ,要访问基类的Show只能通过加上作用域。

  1. 覆盖
    基类与派生类中,函数名,函数的返回值与参数列表都相同,基类的函数的是虚函数
    (覆盖虚函数表)
    先来了解一下c++ 中的静态绑定域动态绑定
  • 静态类型:对象在声明时采用的类型,在编译期既已确定;
  • 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
  • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
  • 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;
    我们来看一个例子
class Base
{
public:
	void Show(){ cout << "Base::Show()" << endl;}
	virtual void Display(){ cout << "Base::Display()" << endl;}
};

class Derived:public Base
{
public:
	void Show(){ cout << "Derived::Show()" << endl;}
	virtual void Display(){ cout << "Derived::Display()" << endl;}
};

int main()
{
	Derived *d = new Derived;
	Base *b = d;
	b->Show();
	d->Show();
	cout << "=====================" << endl;
	b->Display();
	d->Display();
}

在这里插入图片描述
虽然b和d都指向同一个对象。因为函数Show是一个no-virtual函数,它是静态绑定的,也就是编译器会在编译期根据对象的静态类型来选择函数。d的静态类型是Derived*,那么编译器在处理d->Sow()的时候会将它指向Derived::Show()。同理,b的静态类型是Base*,那b->Show()调用的就是Base::Show();

因为Display是一个虚函数,它动态绑定的,也就是说它绑定的是对象的动态类型,b和d虽然静态类型不同,但是他们同时指向一个对象,他们的动态类型是相同的,都是Derived*,所以,他们的调用的是同一个函数:Derived::Display()。

对于虚函数特别要注意的地方
虚函数+缺省参数
虚函数是动态绑定的,但是为了执行效率,缺省参数是静态绑定的。

class Base
{
public:
	virtual void Display(int a = 10)
	{ cout << "Base::Display() a = " << a <<endl;}
};

class Derived:public Base
{
public:
	virtual void Display(int a = 20)
	{ cout << "Derived::Display() a = " << a << endl;}
};
int main()
{
	Derived *d = new Derived;
	Base *b = d;
	b->Display();
	d->Display();
}

在这里插入图片描述
可以看到因为缺省参数属于静态绑定,所以当b->Display() 默认的参数是10;
d->Display() 默认的参数是20 这是我们不愿意看到的,在使用虚函数的时候,一定要考虑号缺省参数的问题。

我们来看一下虚函数是怎样实现动态绑定的?

先来看一下这样一个例子

class Animal // 有纯虚函数的类  =》  抽象类
{
public:
	Animal(string name) :_name(name) {}
	virtual void bark() = 0;// 纯虚函数
protected:
	string _name;
};

class Cat : public Animal
{
public:
	Cat(string name):Animal(name){}
	void bark() { cout << _name << " bark:喵喵!" << endl; }
};

class Dog : public Animal
{
public:
	Dog(string name) :Animal(name) {}
	void bark() { cout << _name << " bark:旺旺!" << endl; }
};

int main()
{
	Animal *p1 = new Cat("猫");
	Animal *p2 = new Dog("二哈");
	int *p11 = (int*)p1;
	int *p22 = (int*)p2;
	int tmp = p11[0];
	p11[0] = p22[0];
	p22[0] = tmp;
	//Cat 与 Dog 都继承了 Animal 类,而Animal 有一个虚函数bark 两个派生类中都重写了
	//bark函数,两个派生类中的bark自动声明为虚函数
	//按我们的理解输出的结果为:
	/*
	猫 bark:喵喵
	二哈 bark:旺旺
	calse Cat
	*/
	p1->bark();
	p2->bark();
	cout << typeid(*p1).name() << endl;
	return 0;
}

实际的输出结果
在这里插入图片描述
分析一下这段代码

	Animal *p1 = new Cat("猫");
	Animal *p2 = new Dog("二哈");
	//p11 指向 p1 的首地址
	int *p11 = (int*)p1;
	//p22 指向 p2 的首地址
	int *p22 = (int*)p2;
	//交换了p11指向的内容与p22 指向内容的前四个字节
	int tmp = p11[0];
	p11[0] = p22[0];
	p22[0] = tmp;

对象的前四个字节中存储了什么?交换之后对象的方法都交换了,而且对象的类型都变了 ?

每个类的虚函数都有一个对应的虚函数表,该表存储在只读数据段,即.rodata段,在每个含有虚函数类中就多了一个数据成员,vfptr 指向了虚函数表。通过vfptr 可以动态访问类中的虚函数

class Base
{
public:
	virtual void Display(int a = 10)
	{ cout << "Base::Display() a = " << a <<endl;}

private:
	int ma;
};

int main()
{
	cout << " Base size " << sizeof(Base) << endl;
	return 0;
}

在这里插入图片描述
可以看到类中不止有int ma,这就验证了上面我们说的。

虚函数表的内容
在这里插入图片描述
其中 RTTI (Run-Time Type Identification) ,RTTI 指向了.rodata 段的一个常量字符换,该常量字符串表示类型,例如上面的Base 类的虚函数表中的RTTI指向的就是"Base" 字符串,偏移量表示虚函数在对象中的位置,一般都是0 ,即在对象的起始位置。这样对于这段代码就好理解了

	Animal *p1 = new Cat("猫");
	Animal *p2 = new Dog("二哈");
	//p11 指向 p1 的首地址
	int *p11 = (int*)p1;
	//p22 指向 p2 的首地址
	int *p22 = (int*)p2;
	//交换了p11指向的内容与p22 指向内容的前四个字节
	//也就是交换了两个对象的虚函数表的地址,
	int tmp = p11[0];
	p11[0] = p22[0];
	p22[0] = tmp;
	
	//p1此时指向存储的是p2的虚函数表的地址,所以调用p2的bark
	p1->bark();
	//p2指向的p1 的虚函数表的地址,所以调用的p1 的bark
	p2->bark();
	//p1 此时的RTTI是 原来p2的类型。
	cout << typeid(*p1).name() << endl;
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值