C++多态

前言

继承和多态这两节内容比较杂,我也不喜欢整理,但是为了知识,我还是继续努力吧

1. 多态的构成条件

1.1 两个构成条件

class person
{
public:
	virtual void buy()
	{
		cout << "买票全价" << endl;
	}
};

class student:public person
{
public:
	virtual void buy()
	{
		cout << "买票半价" << endl;
	}
};

class military :public person
{
public:
	virtual void buy()
	{
		cout << "买票优先" << endl;
	}
};

void func(person& x)
{
	x.buy();
}
int main()
{
	person a;
	student b;
	military c;
	func(a);
	func(b);
	func(c);
	return 0;
}

在这里插入图片描述
按照常理来说,应该是三个买票全价的,但是是三个对应的,这就是多态
多态有两个构成条件
1.必须要通过基类的指针或者引用来调用虚函数(有virtual)(必须在类内)
2.派生类要对虚函数进行重写
什么是虚函数呢,有virtual的就是虚函数
什么叫做对虚函数的重写呢,就是在派生类类中,有一个和基类函数名,参数,还有返回值相同,只是函数体的实现不同的虚函数,这就是叫做对虚函数的重写
还有就是这个多态的访问不关private什么事,就算是私有,也可以多态
在这里插入图片描述
在这里插入图片描述
在虚函数的重写中,派生类可以不用写virtual,只要函数名,参数,返回值相同,同样是重写,同样可以多态
但是基类不能不写virtual,不写就不是重写,就不是多态
在这里插入图片描述

在这里插入图片描述
就是普通的函数,该调用谁就调用谁
然后就是必须是基类的指针或者引用,如果只是对象赋值是不行的
在这里插入图片描述
在这里插入图片描述

1.2 协变

虚函数重写的两个例外
第一个就是协变,就是基类和派生类的返回值可以不同,但是必须对应分别为子类父类,还有就是必须要是子类或者父类的指针或者引用
在这里插入图片描述
看,如果只是简单的参数不同,那是不构成重写的
在这里插入图片描述
而且必须构成对应,基类的返回值就是对应基类,派生类的返回值就是对应派生类
在这里插入图片描述

1.3 析构函数的重写

第二个便是析构函数的重写
在此之前,我们先看一个例子

class person
{
public:
	virtual void buy()
	{
		cout << "买票全价" << endl;
	}
	~person()
	{
		cout << "~person()" << endl;
	}
};

class student:public person
{
public:
	virtual void buy()
	{
		cout << "买票半价" << endl;
	}
	~student()
	{
		cout << "~student()" << endl;
	}
};

class military :public person
{
public:
	virtual void buy()
	{
		cout << "买票优先" << endl;
	}
	~military()
	{
		cout << "~military()" << endl;
	}
};
	person* x =new military;
	delete x;

在这里插入图片描述
delete x就会调用x类型的析构函数,但是这样只调用了person的析构,但我们开辟的是military的空间,按理说应该调用military的析构的,因为这样编译器还可以调用person的析构,但没有,可能还会内存泄漏,怎么弄呢,这时就要用多态了

	virtual ~person()
	{
		cout << "~person()" << endl;
	}

在这里插入图片描述
所以对析构函数进行重写,再加上是基类的指针引用,所以就可以多态了,为什么加上virtual就是重写了呢,因为我们之前就说过,在继承中,析构函数会被统一处理为destructor(),所以我们之前实现派生类的析构函数,如果要显示调用基类的析构函数的话,还要指定类域,不然就是隐藏了。因为统一被处理为destructor(),所以就满足了重写,满足了多态,就OK了

1.4 C++11 override 和 final

final:修饰虚函数,表示该虚函数不能再被重写
在这里插入图片描述
如图,我们已经在基类那里buy函数的后面写上了final就表示这个函数无法重写,如果你还要重写的话,就会报错
这里再额外提一句,其实函数重写就是一种特殊的隐藏
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
在这里插入图片描述
如果我们没有实现buy的重写,就是取消了virtual,加上override的话就会报错

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

在这里插入图片描述

2. 抽象类

class person
{
public:
	virtual void buy()=0
	{
		cout << "买票全价" << endl;
	}
	virtual ~person()
	{
		cout << "~person()" << endl;
	}
};

在这里插入图片描述
在虚函数的后面加上=0就是纯虚函数,含有纯虚函数的类叫做抽象类,抽象类不能实例化出对象,继承下来抽象类它还是抽象类,不能实例化出对象,除非将那个纯虚函数重写,这样就可以了

class student:public person
{
public:
	virtual void buy()
	{
		cout << "买票半价" << endl;
	}
	virtual ~student()
	{
		cout << "~student()" << endl;
	}
};

在这里插入图片描述
这样就可以正常使用了
再额外提几句,只有当你不想显示实例化对象时,才定义抽象类,或者这个类本身就很抽象,无法实例化
还有就是只有你想用多态的时候才是弄虚函数,不然最好不要平白无故使用虚函数,因为这样会有损耗

3. 多态的原理

在这里插入图片描述
在这里插入图片描述
这里我们计算基类的大小,为什么不计算派生类的大小呢,因为派生类相当于是拷贝了基类的了,所以大小会有点区别,有点难算,所以我们先计算基类的大小
这里我们发现基类的大小为4,为什么呢,因为person这个对象里面有一个指针,指针指向一个表,叫做虚函数表,这个表存放了各个虚函数的地址

class person
{
public:
	virtual void buy1()
	{
		cout << "virtual void buy1()" << endl;
	}
	virtual void buy2()
	{
		cout << "virtual void buy2()" << endl;
	}
	void buy3()
	{
		cout << "void buy3()" << endl;
	}
};

class student :public person
{
public:
	virtual void buy1()
	{
		cout << "virtual void buy1()" << endl;
	}
};

class military :public person
{
public:
	virtual void buy1()
	{
		cout << "virtual void buy1()" << endl;
	}
};
person s;
	student s1;

在这里插入图片描述

加上代码分析,我们可以得到以下图
注意不是每创建一个对象就会创建一个虚函数表,而是同一个类的所有对象共用同一个虚函数表
在这里插入图片描述
从这幅图我们就可以看出,每个类只要有虚函数,就会有一个虚函数表,虚函数表存放了每个虚函数的地址,当虚函数进行了重写,那么表就会指向两个不同的虚函数,为了多态区分开,没有重写的虚函数,就都指向它
接下来我们来讲一下,为什么基类的指针指向重写的虚函数就可以实现多态呢
因为基类的指针或者引用都是指向的派生类的一部分,同时也包含了虚函数表,所以就能够访问到不同的属于自己的虚函数,这样就实现了多态
然后对于普通函数,比如buy3,这个就是通过符号表存放着地址,编译时来找函数地址的
而虚函数表是运行时来找的
然后我们来讲一下这些东西都存放在哪里

	int a;
	static int b;
	int* c = new int;
	cout << "栈区:" << &a<<endl;
	cout << "静态区:" << &b<<endl;
	cout << "堆区:" << c<<endl;
	cout <<  & person::buy1 << endl;

在这里插入图片描述
注意如果是打印类外的函数的地址,那么就只需要写个函数名就可以了,但是在类里面,除了要写明类域,还要写上&才算是取到了地址
但是这里为什么会打印1呢,因为cout的原因,这个类里面的地址,就相当于当年的char*,是无法正常打印地址的,而且类里面的函数地址还无法强制类型转换为void*,但是char就可以,所以char可以用cout打印地址,类里面的函数却不可以,所以只能用printf了

	int a;
	static int b;
	int* c = new int;
	cout << "栈区:" << &a<<endl;
	cout << "静态区:" << &b<<endl;
	cout << "堆区:" << c<<endl;
	printf("函数:%p\n", &person::buy1);
	printf("函数:%p\n", &person::buy2);
	printf("函数:%p\n", &person::buy3);

在这里插入图片描述
看得出来,类里面的函数,不管是虚函数还是普通函数都是存放在静态区的,也就是代码段

	int a;
	static int b;
	int* c = new int;
	const char* d = "qqqqq";
	cout << "栈区:" << &a<<endl;
	cout << "静态区:" << &b<<endl;
	cout << "堆区:" << c<<endl;
	cout << "常量区:" << (void*)d<<endl;
	person p;
	cout << (void*)*((int*)&p) << endl;

在这里插入图片描述
因为指向虚函数表的指针在类的最前面四个字节,所以只需要得到前四个字节就可以了,但是不能直接将person类型强制类型转换为int,因为类型转换必须具有相关性,无关的不能转换,所以只能这样。
&p指向的是整个person对象,强制为int*,在解引用就得到了地址,再变为void*,规范
所以我们可以看出,虚函数表是存在于常量区的

所以虚函数在代码段在静态区,虚函数表在常量区,指向虚函数的指针和对象一起在栈区

广义来说,其实多态还分为静态多态(静态绑定)和动态多态(动态绑定)

动态多态就是我们这一节讲的
静态多态就是函数模版,函数重载那些,因为模版会根据类型不同选择不同,重载会根据参数不同选择不同,和我们这一节的虚函数不同选择不同很类似

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

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
这道题应该选什么呢这道题应该选C
在这里插入图片描述
首先在上一节中我们就知道了,派生类相当于拷贝了基类,所以基类就相当于派生类的成员变量,但不是的,因为如果是成员变量是无法访问被保护内容的,但是可以这样理解
因为编译器默认的是基类先构造,所以基类在派生类的成员变量的前面,又因为base1先继承,所以base1也在前面
p3肯定指向整个对象的,所以指向前面
因为p1要访问到base1,而且base1也在面前,所以p1也指向最前面,但是访问的内容却是只有base1那么大
因为p2要访问base2,所以指向base2,所以选c

 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: 以上都不正确
因为test没有重写,所以会访问到virtual void test()
这时候就关键了,我们说过,成员函数会隐藏的传自己的地址给this指针
然后test是A类的,所以就是A类型的this指针
所以就是这样
A*this=p
virtual void test(){ func();}
然后基类指针去访问重写函数,就会多态,因为原来是父类的,所以就会跳转到void func(int val=0){ std::cout<<“B->”<< val <<std::endl; }
但是你以为就选D了吗,还不是,这道题是选B
因为只是缺省值不同还是满足函数重写的,所以说函数重写,重写的是函数体,而函数名返回值参数都是没有重写的,在调用重写后的函数时,函数体还是调用重写后的函数体,而函数名那些东西就是调用的以前的虚函数,就是这样的
void func(int val=0){ std::cout<<“B->”<< val <<std::endl; }

virtual void func(int val = 1)
+
{ std::cout<<“B->”<< val <<std::endl; }
所以选B

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

在这里插入图片描述
这个会经历三次test,第一次是B在构造时会调用A的构造函数,这里是在构造A,所以this指针就是A,所以是virtual void func() { std::cout<<m_iVal<<‘ ’;}
第二次是在B的构造函数内部调用test,这时的this指针指的是B,然后跳到A中就会多态,就是 virtual void func()
{
++m_iVal;
std::cout<<m_iVal<<‘ ’;
}
第三次就不用说了,反正就是构造函数才是最最初的this指针,它的祖先就是自己

总结

下一节或者后面会讲树相关内容

C++中的多态(Polymorphism)是指在父类和子类之间的相互转换,以及在不同对象之间的相互转换。 C++中的多态性有两种:静态多态和动态多态。 1. 静态多态 静态多态是指在编译时就已经确定了函数的调用,也称为编译时多态C++中实现静态多态的方式主要有函数重载和运算符重载。 函数重载是指在同一作用域内定义多个同名函数,但它们的参数列表不同。编译器根据传递给函数的参数类型和数量来确定调用哪个函数。例如: ```c++ void print(int num) { std::cout << "This is an integer: " << num << std::endl; } void print(double num) { std::cout << "This is a double: " << num << std::endl; } int main() { int a = 10; double b = 3.14; print(a); // 调用第一个print函数 print(b); // 调用第二个print函数 } ``` 运算符重载是指对C++中的运算符进行重新定义,使其能够用于自定义的数据类型。例如: ```c++ class Complex { public: Complex(double real, double imag) : m_real(real), m_imag(imag) {} Complex operator+(const Complex& other) const { return Complex(m_real + other.m_real, m_imag + other.m_imag); } private: double m_real; double m_imag; }; int main() { Complex a(1.0, 2.0); Complex b(3.0, 4.0); Complex c = a + b; // 调用Complex类中重载的+运算符 } ``` 2. 动态多态 动态多态是指在运行时根据对象的实际类型来确定调用哪个函数,也称为运行时多态C++中实现动态多态的方式主要有虚函数和纯虚函数。 虚函数是在父类中定义的可以被子类重写的函数,使用virtual关键字声明。当一个对象的指针或引用指向一个子类对象时,调用虚函数时会根据实际的对象类型来确定调用哪个函数。例如: ```c++ class Shape { public: virtual void draw() { std::cout << "Drawing a shape." << std::endl; } }; class Circle : public Shape { public: void draw() override { std::cout << "Drawing a circle." << std::endl; } }; int main() { Shape* shape_ptr = new Circle(); shape_ptr->draw(); // 调用Circle类中重写的draw函数 } ``` 纯虚函数是在父类中定义的没有实现的虚函数,使用纯虚函数声明(如virtual void func() = 0;)。父类中包含纯虚函数的类称为抽象类,抽象类不能被实例化,只能作为基类来派生子类。子类必须实现父类的纯虚函数才能实例化。例如: ```c++ class Shape { public: virtual void draw() = 0; }; class Circle : public Shape { public: void draw() override { std::cout << "Drawing a circle." << std::endl; } }; int main() { Shape* shape_ptr = new Circle(); shape_ptr->draw(); // 调用Circle类中重写的draw函数 } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值