【C++复习】多态{深入理解多态底层}

时间:2024-09-30 11:45:36

文章目录

  • 介绍
  • windows下堆栈相对位置
  • 析构函数
  • 复习override和final和重载/重定义/重写
  • 抽象类
  • 多态原理
    • 回顾虚基表指针
    • 单继承多态底层
    • 打印虚函数表
    • 多继承多态底层
    • c++输出类成员函数地址
    • 再次理解多态
    • 早期绑定/晚期绑定

介绍

什么是多态

  1. 多态(Polymorphism)是面向对象编程中的一个重要概念,指的是同一种操作或方法可以在不同的对象上产生不同的行为。具体来说,多态是通过继承和虚函数实现的。多态:为不同数据类型的实体提供统一的接口
  2. 多态可以提高代码的灵活性和可扩展性。通过多态,我们可以编写通用的代码,而不必考虑对象的具体类型。这样可以使代码更加简洁、易于维护和扩展。
  3. 例如:同样是买票这种行为,普通人是全价买票,学生是半价买票,军人则是优先买票。这就是一种多态的体现

多态的构成条件

  1. 必须通过基类的指针或者引用调用虚函数
  2. 派生类必须对基类的虚函数进行重写(子类重写时virtual可有可无 建议有)

虚函数重写

派生类中有一个跟基类完全相同的虚函数(返回值类型、函数名字、参数列表完全相同,缺省值可以不同)

虚函数

  1. 虚函数用于实现运行时多态。虚函数在运行时根据对象的实际类型调用相应的函数。虚函数通过使用虚函数表来实现动态绑定。

  2. 在C++中,如果一个成员函数被声明为虚函数,那么它会被编译器标记为虚函数,并且在类的内存布局中会包含一个指向虚函数表的指针。虚函数表是一个存储虚函数地址的表格,它是在编译时由编译器生成的,用于实现动态绑定。每个包含虚函数的类都有自己的虚函数表,虚函数表中存储着该类的虚函数地址。

  3. 多态的实现原理:当一个对象调用虚函数时,编译器会通过对象的虚函数表指针找到该对象所属类的虚函数表,然后根据虚函数在类中的位置,找到对应的虚函数地址。这个过程称为动态绑定,它是在运行时确定的,而不是在编译时确定的。

  4. 虚函数可以被派生类重写,也可以被派生类继承并保留为虚函数。

  5. virtual只能用于修饰普通成员函数,不能修饰静态成员函数,virtual和static不能共用。(最后解释)

  6. virtual关键字只在声明时加上,虚函数在类外实现时不加virtual。(这点和static相同)

  7. 在重写基类虚函数时,派生类的虚函数不加virtual关键字,也可以构成重写。因为基类虚函数的接口被继承下来,在派生类中依旧保持虚函数属性。但是该种写法不是很规范,不建议这样使用。

  8. 可以被继承;可以被隐藏(重定义);可以被访问控制符修饰;

  9. 可以被声明为纯虚函数;可以被重写(覆盖);可以被动态绑定(进虚函数表);

两个例外:不严格按照重写条件也被认为是重写

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

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(甚至可以是其他父子关系的指针或引用 )

//  1.1 (基类与派生类虚函数返回值类型不同)
class Animal
{
public:
    virtual Animal* express()
    {
        cout << "我在疯狂动物叫" << endl;
        return this;
    }

};

class Dog :public Animal
{
public:
    virtual Dog* express()
    {
        cout << "我在疯狂狗叫" << endl;
        return this;
    }

};
void func(Animal& animal)
{
    animal.express();
}
int main()
{
    Animal animal;
    func(animal);
    Dog dog;
    func(dog);
    return 0;
}

// 1.2(甚至可以是其他父子关系的指针或引用)
class Ox  //牛
{

};
class Bull :public Ox//公牛
{

};
class Animal
{
public:
    virtual Ox* express()
    {
        cout << "我在疯狂动物叫" << endl;
        return nullptr;
    }

};

class Dog :public Animal
{
public:
    virtual Bull* express()
    {
        cout << "我在疯狂狗叫" << endl;
        return nullptr;
    }

};
void func(Animal& animal)
{
    animal.express();
}
int main()
{
    Animal animal;
    func(animal);
    Dog dog;
    func(dog);
    return 0;
}

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

基类与派生类析构函数名字不同构成重写的原因是,编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

基类的析构函数不为虚函数,派生类与基类的析构函数构成隐藏/重定义(子类与父类某函数名相同)。

基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。

windows下堆栈相对位置

(35 封私信 / 8 条消息) 堆、栈的地址高低? 栈的增长方向? - 知乎 (zhihu.com)

Linux下进程地址空间

在这里插入图片描述

windows的进程地址空间

在这里插入图片描述

很明显,windows下栈的位置并不是严格按照Linux的!他甚至有时候还会比代码区低!

总结

  1. Windows的栈和Linux不一样。
  2. windwos的栈向哪个方向增长取决于编译器。解释看一下代码
void test()
{
    int arr[3]{0, 1, 2};
}
int main()
{
    test();
    return 0;
}
被调用函数(callee)test的栈帧相对调用函数(caller)main的栈帧的位置反映了栈的增长方向:
    如果被调用函数test的栈帧比调用函数main的栈帧在更低的地址,那么栈就是向下增长;反之则是向上增长。
而在一个栈帧内,局部变量是如何分布到栈帧里的(所谓栈帧布局,stack frame layout),这完全是编译器的*。
即不是严格按照a[0] a[1] a[0] 创建的

析构函数

一些场景析构函数需要构成重写,重写的条件之一是函数名相同。一般情况下,编译器会对析构函数名进行特殊处理,处理成 destrutor(),所以父类析构函数不加 virtual 的情况下,子类析构函数和父类析构函数构成隐藏关系。


在这里插入图片描述


没有虚析构导致的问题

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

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

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

int main()
{
    // 1.0 s生命周期结束 调用student析构 而student的析构会自动调用父类析构 正确释放空间
	Student s;
    
    // 2.0.1 ptr1正确释放
	Person* ptr1 = new Person;
	delete ptr1;
	// 2.0.2 ptr2 只调用person的析构 error!
	Person* ptr2 = new Student;
	delete ptr2;
    
	return 0;
}

虚析构存在的必要性

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

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

	//int* _ptr;
};

class Student : public Person 
{
public:
	Student()
	{
		cout << "Student()" << endl;
	}
	// 析构函数名底层为:destructor -- 构成虚函数重写
	virtual ~Student() 
	{
		cout << "~Student()" << endl;
	}
};

int main()
{
	Person* ptr1 = new Person;
	delete ptr1;

    // ptr2 是一个指向子类的父类指针 调用子类重写的析构函数 形成多态
    // delete ptr2时会调用子类的析构函数 而子类的析构函数会自动调用父类的析构函数 ok!!
	Person* ptr2 = new Student;
	delete ptr2;                  //ptr2 -> destructer();
                                  //operator delete(ptr2);
	return 0;
}

纯虚析构

class Dad 
{
public:
    Dad()
    {
        cout << "Dad 构造函数调用!" << endl;
    }
	virtual void Name() = 0;
    
    virtual ~Dad() = 0;
};

Dad::~Dad()
{
    cout << "Dad 纯虚析构函数调用!" << endl;
}

class Son : public Dad
{
public:
    Son(string name)
    {
        cout << "Son 构造函数调用!" << endl;
        _name = new string(name);
    }
   
    virtual void Name()
    {
        cout << *_name << "是son的名字" << endl;
    }
    
    ~Son()
    {
        cout << "Son 析构函数调用!" << endl;
        if (this->_name != NULL) 
        {
            delete _name;
            _name = NULL;
        }
    }
public:
    string* _name;
};

int main() 
{

    Dad* dad = new Son("Mike");
    dad->Name();

    delete dad;

    return 0;
}

虚析构/纯虚析构

  1. 二者目的皆是能够【delete指向子类对象的父类指针】时正确调用析构函数。
  2. 纯虚析构适用于:当前基类作为一个抽象类,不想要实例化对象,只作为子类的父类,并且可以强制子类重写析构函数。
  3. 但是使用虚析构和纯虚析构需要注意:二者必须有函数实现–虚析构在类内即可完成函数实现–纯虚析构需要在类外完成。

复习override和final和重载/重定义/重写

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

final:修饰虚函数,表示该虚函数不能再被重写(不常用)【final修饰类 标识该类不能被继承】

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

在这里插入图片描述

抽象类

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

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。

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

理解子类的虚函数表和接口继承

class A
{
public:
	virtual void func(int value = 1) 
	{ 
		cout << "A->" << value << endl; 
	}
	virtual void test() 
	{ 
		func();
	}
};
class B : public A
{
public:
	virtual void func(int value = 0)  override
	{ 
		cout << "B->" << value << endl; 
	}
};
int main()
{
	//B* pb = new B;  //B -> 1
	//A* pb = new B;  //B -> 1
	//pb->test();

	A* pa = new A;
	pa->test(); // A->1

	return 0;
}

B::func的缺省值和A::func不同 是否构成重写?此处构成重写!

函数重写(override)是指子类提供一个与父类相同的方法名、返回类型以及参数列表(包括参数的个数和类型)的实现。缺省值不同:参数缺省值属于接口内容,会被继承下来。

输出解释

B* pb = new B;
pb->test();

此时调用的是 B 对象中的 test 函数,而 test 函数的实现是从类 A 继承而来的:

virtual void test(A* this) // A* this = pb;
{
	func();// this -> func();
}

参数缺省值属于接口内容,会被继承下来。==》默认参数绑定

深入理解

子类不重写test B对象虚基表里面:重写的func 未重写的test 
如果子类不重写父类的所有虚函数 那么父类虚表指针指向父类的虚函数表 子类虚表指针也指向父类的虚函数表
但是vs下 不管是否重写 子类跟父类虚表都不是同一个
这样实现的理由:即便子类没有重写 但是子类有自己的虚函数时 单独创建一个虚表和父类分隔开 更有条理
子类虚函数表存储:重写的父类虚函数func 没有重写的父类虚函数test 自己的虚函数

在这里插入图片描述

结果相同,只不过切片赋值操作在定义基类指针p就已经发生了。调用test函数时是同类指针的普通赋值。

实际到底调用的是谁,不是看传的是父类指针还是子类指针,而是指针指向的对象是父类还是子类。指向谁调用的就是谁

多态原理

回顾虚基表指针

在这里插入图片描述

单继承多态底层

class Dad 
{
public:
	virtual void Cook() 
	{ 
		cout << "佛跳墙" << endl; 
	}

	virtual void Work() 
	{ 
		cout << "Work" << endl; 
	}
	int _a = 0;
};

class Son : public Dad 
{
public:
	virtual void Cook()
	{ 
		cout << "方便面" << endl; 
	}

	int _b = 0;
};

void Test(Dad& p)
{
	p.Cook();
}

int main()
{
	Dad dad;
	Test(dad);

	Son son;
	Test(son);

	return 0;
}

在这里插入图片描述

打印虚函数表

class Dad
{
public:
	virtual void BuyCar()
	{
		cout << "Dad::买车-宾利" << endl;
	}

	virtual void Func1()
	{
		cout << "Dad::Func1()" << endl;
	}
};

class Son : public Dad 
{
public:
	virtual void BuyCar()
	{
		cout << "Son::买车-奔驰" << endl;
	}

	virtual void Func2()
	{
		cout << "Son::Func2()" << endl;
	}
};

typedef void(*vftptr)();
void PrintVftable(vftptr* pt)  //void PrintVftable(vftptr pt[])
{
	for (size_t i = 0; *(pt + i) != nullptr; ++i)
	{
		printf("vft[%d]:%p->", i, pt[i]);
        
		//1.直接访问
		pt[i]();
        
		//2.间接访问
		//vftptr pf = pt[i]; f();
	}
	cout << endl;
}
int main()
{
	Dad p1;
	Dad p2;

    Son s1;
	Son s2;

	//打印子类虚表
	PrintVftable((vftptr*)*(int*)&s1);
	PrintVftable((*(vftptr**)&s1));

	//打印父类虚表
	PrintVftable((vftptr*)*(int*)&p1);
	PrintVftable((*(vftptr**)&p1));
	
	return 0;
}
/*
typedef void(*VFPTR)();
void PrintVFTable(VFPTR *table, size_t n)
{
  for(size_t i = 0; i<n; ++i)                        
  {    
    printf("vftable[%lu]:%p -> ", i, table[i]);    
    table[i](); //函数指针强转成VFPTR,无视函数原型调用函数。    
  }    
}                                                                

void Test1()
{ 
  //打印Base和Derive两个类的虚函数表
  Base b;           
  Derive d;    
  
  printf("Base虚函数表:%p\n", (int*)*(long long*)&b);    
  printf("Derive虚函数表:%p\n", (int*)*(long long*)&d);  
  cout << endl;    
  
  PrintVFTable((VFPTR*)*(long long*)&b, 2);//取出对象中的虚函数表指针传参      
  cout << endl;    
  PrintVFTable((VFPTR*)*(long long*)&d, 3);      
}    

*/

在这里插入图片描述

在这里插入图片描述

vs监视窗口存在bug,虚函数表中不能显示派生类自己定义的虚函数指针func2。

样例Ⅱ

class Person{
  virtual void Buyticket(){
    cout << "Person::Buyticket()" << endl;
  }
  virtual void Func1(){
    cout << "Person::Func1()" << endl;
  }                     
};

class Student:public Person{
  virtual void Buyticket(){
    cout << "Student::Buyticket()" << endl;
  }
  virtual void Func2(){
    cout << "Student::Func2()" << endl;
  }
};

typedef void(*VFPTR)();
void PrintVFTable(VFPTR *table, size_t n){
  for(size_t i = 0; i<n; ++i)                        
  {
    printf("vftable[%lu]:%p -> ", i, table[i]);
    table[i](); //函数指针强转成VFPTR,无视函数原型调用函数。
  }
}

int main(){
  Person p;    
  Person p1;    
  Student s;                   
  Student s1;    
  //测试一:打印各对象虚函数表的地址
  cout << "p: " << (VFPTR*)*(long long*)&p << endl;     
  cout << "p1: " << (VFPTR*)*(long long*)&p1 << endl;     
  cout << "s: " << (VFPTR*)*(long long*)&s << endl;     
  cout << "s1: " << (VFPTR*)*(long long*)&s1 << endl;     
  cout << endl;    
  
  //测试二:打印虚函数表中的虚函数地址,并调用虚函数
  PrintVFTable((VFPTR*)*(long long*)&p, 2); //取对象开头的虚函数表指针传参    
  cout << endl;    
  PrintVFTable((VFPTR*)*(long long*)&s, 3);    
  return 0;   
}

32位

在这里插入图片描述

64位

在这里插入图片描述

  1. 虚函数表的指针位于对象空间的开头前8个字节(64下),一个long long的大小;

  2. 同类型的对象p1,p2共用一个虚函数表(输出结果可见);不管是否完成重写,子类和父类的虚函数表都不是同一个。

  3. 单继承只有一个虚函数表。派生类对象的虚函数表指针保存在基类部分,派生类会继承基类的虚函数表(拷贝基类虚函数的地址);如果构成重写,就覆盖重写后的虚函数地址;

  4. 派生类自己定义的虚函数地址也要存入虚函数表。

多继承多态底层

class Base1{
    virtual void Func1(){
        cout << "Base1::Func1()" << endl;
    }
    virtual void Func2(){
        cout << "Base1::Func2()" << endl;
    }
};

class Base2{
    virtual void Func1(){
        cout << "Base2::Func1()" << endl;
    }
    virtual void Func2(