当前位置: 首页 > news >正文

【C++】继承与多态

目录

  • 前言
  • 1. 继承
    • 1.1 继承的概念
    • 1.2 继承的定义
    • 1.3 切片赋值
    • 1.4 继承中的作用域
    • 1.5 派生类的默认成员函数
    • 1.6 继承与友元、静态成员
    • 1.7 多继承、菱形继承、菱形虚拟继承
      • 1.7.1 区分单继承与多继承
      • 1.7.2 菱形继承
      • 1.7.3 菱形虚拟继承
      • 1.7.4 菱形虚拟继承的原理
  • 2. 多态
    • 2.1 概念
    • 2.2 多态的定义和实现
    • 2.3 C++11中的两个关键字
    • 2.4 区分重载、重写、重定义
    • 2.5 抽象类
    • 2.6 多态的原理
      • 2.6.1 虚函数表
      • 2.6.2 多态如何实现
    • 2.7 静态绑定和动态绑定


前言

💭面向对象程序设计的三大特征是封装、继承、多态。封装在类和对象模块已经基本掌握了,而本文将着重介绍继承和多态。

1. 继承

1.1 继承的概念

继承(inheritance)是面向对象软件技术当中的一个概念。如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”。继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。另外,为子类追加新的属性和方法也是常见的做法。 一般静态的面向对象编程语言,继承属于静态的,意即在子类的行为在编译期就已经决定,无法在执行期扩展。

选自《维基百科》

⭕继承是面向对象编程中实现代码复用的重要手段,它可以在保存原有类(基类、也称父类)特性的基础上,增加新特性,产生新的类,称为派生类(也称子类)。之前我们实现代码复用的手段都是在函数层面的,而继承是类层面的代码复用。

🌰举个栗子

class A
{	
public:void func1(){}
protected:int _a;
}class B: public A
{
public:void func2(){}
private:int _b;
}

如这段代码,我们称B类继承了A类,B是A的派生类(子类),A是B的基类(父类)下文均用基类、派生类的叫法

在这里插入图片描述

如图是B类的继承模型。可以看到,B类不仅有自己的方法fun2和数据_b,同时也继承了A类的fun1和_a。


1.2 继承的定义

在这里插入图片描述

继承方式和类内的访问限定符用的关键字相同,都是publicprotectedprivate,但意义不同。不同继承方式和基类不同访问限定符结合,派生类会继承不同的访问限定的成员。

📝如下表

基类成员\继承方式publicprotectedprivate
基类中的public成员派生类的public成员派生类的protected成员派生类的private成员
基类中的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类中的private成员在派生类中不可见在派生类中不可见在派生类中不可见

记忆方法:

基类中的private成员,无论派生类以哪种方式继承都不可见。其他的,只需记住各个关键字的权限大小关系:public > protected > private,假设派生类以X(继承方式)方式继承基类,那么基类中的Y(访问限定符)成员就是派生类中的min(X,Y)成员。

🔎总结与一些注意事项:

  • 区分类中privateprotected成员的区别,private成员在类外无论任何场景都无法访问,protected在类外也无法访问,但是可在派生类中访问。保护限定符protected为继承而生。

  • 没有显式写出访问限定符和继承方式的情况下:
    class默认访问限定符和默认继承方式private
    struct默认访问限定符和默认继承方式public
    最好显式写出访问限定符和继承方式

  • 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强


1.3 切片赋值

💭不同类型的普通对象之间的赋值通常会发生类型转换,中间还会生成一个具有常性的临时变量,如下:

	int a = 3.14;double f = a;const int& ref = f;

💡派生类与基类之间的赋值,并不会发生类型转化,即便它们是两个不同的类,而是一种特殊的机制——切片赋值。派生类对象赋值给基类对象/基类引用,派生类对象的地址可以赋值给基类指针,切片赋值就是将派生类中的基类部分切割下来赋值给基类。

注意:基类不能赋值给派生类,只能向上转换。

💬实例

class Person
{
public:string name = "张三";int age = 18;
};class Student :public Person
{
public:string stuID;int score = 100; 
};void test()
{Student s;Person p = s; // 1Person& rp = s; // 2Person* pp = &s; // 3
}

1.对象s中的基类部分赋值给对象p

在这里插入图片描述

  1. rp是对象s中基类部分的别名

在这里插入图片描述

3.pp指向对象s中的父类部分

在这里插入图片描述

测试,改变rp和pp的指向内容,s对象的成员发生变化,证明以上特性

在这里插入图片描述

  • 基类指针可以通过强制类型转化赋值给派生类指针,但是要注意可能存在越界问题。
void test()
{Student s;Person p = s;Person* pp = &s;Student* ps = (Student*)pp;//可以,没问题ps = (Student*)&p;//可以,但可能会发生越界访问
}

1.4 继承中的作用域

  1. 在继承体系中基类和派生类都有独立的作用域。
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。
class A
{
public:void func(){cout << "A::func()" << endl;}int _n = 10;
};class B:public A
{
public:int func(int val = 0){cout << "B::func()" << endl;return val;}int _n = 20;
};void test2()
{B b;// b对象中派生类部分和基类部分存在同名成员,默认调用派生类成员cout << b._n << endl;b.func();// 若想调用基类部分成员,应指定作用域cout << b.A::_n << endl;b.A::func();
}

⭕测试结果

在这里插入图片描述

1.5 派生类的默认成员函数

默认成员函数有:构造函数,析构函数,拷贝构造,赋值重载和取地址重载。普通类具有这些成员函数,派生类也不例外。但是派生类的默认成员函数的“职责”会比普通类的多一项,那就是还要对基类部分进行处理。

  1. 派生类的构造函数必须先调用基类的构造函数来初始化基类部分的成员。派生类会在构造函数的初始化列表调用基类的构造函数,如果不显式写则是调用基类的默认构造。
class Person
{
public:Person(string name = "张三",int age = 18):_name(name),_age(age){}
public:string _name;int _age;
};class Student :public Person
{
public:// 构造函数Student(string name, int age, int score):Person(name, age) // 此处调用了基类Person的构造函数, _score(score){}
public:int _score;
};

⭕构造完毕,符合预期
在这里插入图片描述

  1. 派生类的拷贝构造函数必须调用基类的拷贝构造,完成基类部分的拷贝初始化。
class Person
{
public:Person(string name = "张三",int age = 18):_name(name),_age(age){}Person(const Person& pn):_name(pn._name),_age(pn._age){}
public:string _name;int _age;
};class Student :public Person
{
public:// 拷贝构造Student(const Student& st):Person(st) // 调用基类的拷贝构造,参数发生切片赋值, _score(st._score){}public:int _score;
};

⭕ 拷贝构造成功
在这里插入图片描述

❓小问题:当派生类的拷贝构造函数的初始化列表不显示地调用基类拷贝构造,会发生什么?

class Person
{
public:Person(string name = "张三",int age = 18):_name(name),_age(age){}Person(const Person& pn):_name(pn._name),_age(pn._age){}
public:string _name;int _age;
};
class Student :public Person
{
public:Student(string name, int age, int score):Person(name, age), _score(score){}Student(const Student& st):/*Person(st) // 不显示地调用基类拷贝构造函数, */_score(st._score){}
public:int _score;
};

⭕调试发现s1的基类Person部分调用了基类的默认构造函数去初始化

在这里插入图片描述
💡得出结论,当派生类的拷贝构造函数的初始化列表不显示地调用基类拷贝构造,会调用基类的默认构造函数来初始化基类部分,而非调用基类的默认拷贝构造函数。但这并不符合“拷贝构造”的需求,因此我们自己写派生类的拷贝构造函数时,必须在初始化列表显式地调用基类拷贝构造函数。

  1. 派生类的operator=函数(赋值重载)同样需要调用基类的operator=来处理基类部分的成员。
class Person
{
public:Person& operator=(const Person& pn){if (this != &pn){_name = pn._name;_age = pn._age;}return *this;}
public:string _name;int _age;
};class Student :public Person
{
public:Student& operator=(const Student& st){if (this != &st){// 先调用基类的operator=// 三种调用方法//Person::operator=(st);//*((Person*)this) = st;Person& tmp = *this;tmp = st;// 再处理派生类成员_score = st._score;}return *this;}
public:int _score;
};
  1. 派生类析构函数会在调用结束后自动调用基类的析构函数以清理基类部分的成员。保证了派生类对象先清理派生类成员再清理基类成员的顺序。

💬验证:

class Person
{
public:Person(string name = "张三",int age = 18):_name(name),_age(age){}~Person() // 基类的析构函数{cout << "~Person()" << endl;}
public:string _name;int _age;
};class Student :public Person
{
public:Student(string name, int age, int score):Person(name, age), _score(score){}~Student() // 派生类的析构函数{cout << "~Student()" << endl;}
public:int _score;
};void test()
{Student s("李四", 22, 90);
}

⭕ 验证了先调用派生类的析构函数,再自动调用基类的析构函数
在这里插入图片描述


💬最终的继承模型中默认成员函数代码一览

class Person
{
public:Person(string name = "张三",int age = 18):_name(name),_age(age){}Person(const Person& pn):_name(pn._name),_age(pn._age){}~Person(){cout << "~Person()" << endl;}Person& operator=(const Person& pn){if (this != &pn){_name = pn._name;_age = pn._age;}return *this;}
public:string _name;int _age;
};class Student :public Person
{
public:Student(){}Student(string name, int age, int score):Person(name, age), _score(score){}Student(const Student& st):Person(st), _score(st._score){}~Student(){cout << "~Student()" << endl;}Student& operator=(const Student& st){if (this != &st){Person::operator=(st);_score = st._score;}return *this;}public:int _score;
};

1.6 继承与友元、静态成员

💭友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。可以理解为“父亲的朋友不一定是儿子的朋友”。

class A
{friend void func();
protected:int _a;string _str;void funcA(){}
};class B :public A
{
protected:void funcB(){}
private:int _b;
};void func()
{A a;a._a = 0;a._str = "hello world";a.funcA();B b;b._b = 1; // err,无法访问b.funcB(); // err,无法访问
}

💭基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。

class A
{
public:A(){_a++;}
public:static int _a;
};int A::_a = 0;class B :public A
{
public:B(){_a++;}
};class C :public A
{
public:C(){_a++;}
};void test_static()
{A a;B b;C c;//注意b、c的实例化会调用A的构造函数cout << A::_a << endl;
}

⭕ 测试结果,说明A、B、C类的不同对象,共用一个static成员_a;

在这里插入图片描述

1.7 多继承、菱形继承、菱形虚拟继承

1.7.1 区分单继承与多继承

  1. 单继承:一个派生类只有一个直接基类

在这里插入图片描述

  1. 多继承:一个派生类有两个或两个以上直接基类

在这里插入图片描述

1.7.2 菱形继承

💭菱形继承是由多继承和单继承混用引发的问题,是多继承的一种特殊模式,其模型如下:

在这里插入图片描述

💬代码实现

class Person
{
public:int _age;
};class Teacher :public Person
{
public:int _salary;
};class Student :public Person
{
public:int _score;
};class Assistant :public Teacher, public Student
{
public:int _majorNum;
};

此时class Assistant的类模型:

在这里插入图片描述

🚫可以发现,菱形继承引发的问题:

  1. 数据冗余,一个Assistant类有两份Person类的数据,浪费空间。
  2. 二义性,当你访问Assistant::_age时,编译器无法判断是哪个_name。
void test2()
{Assistant x;// 存在二义性//cout << x._age << endl;//err// 指定类域可解决二义性问题cout << x.Student::_age << endl;cout << x.Teacher::_age << endl;// 但是数据冗余问题不好解决cout << sizeof(x) << endl;
}

🔎 通过调试发现,对象x确实有两份Person类成员

在这里插入图片描述

🔎因此,C++提供了菱形虚拟继承的方法,用以解决数据冗余和二义性问题。

1.7.3 菱形虚拟继承

1️⃣ 菱形虚拟继承的使用

在菱形继承模型的腰部作修改。两个类的继承方法前加上关键字virtual,称为虚拟继承。

在这里插入图片描述

💬 代码实现(此处给各个类的成员变量一个缺省值,方便后续观察)

class Person
{
public:int _age = 20;
};class Teacher :virtual public Person
{
public:int _salary = 1;
};class Student :virtual public Person
{
public:int _score = 2;
};class Assistant :public Teacher, public Student
{
public:int _majorNum = 10;
};

⭕ 解决了刚才的问题

在这里插入图片描述

1.7.4 菱形虚拟继承的原理

🔎通过调试,观察菱形虚拟继承的派生类模型。

在这里插入图片描述

💭很奇怪,这里的派生类在普通基础的基础上,甚至多了一个Person类,说好的解决数据冗余问题呢?怎么不减反增了?其实这里的vs监视窗口是不准确的,我们可以通过内存窗口来观察。

🔎下面对比菱形继承和菱形虚拟继承的派生类的内存布局,以探究菱形虚拟继承的原理:

  1. 菱形继承
    在这里插入图片描述

  2. 菱形虚拟继承
    在这里插入图片描述

原理:菱形虚拟继承实际是将基类中冗余的部分独立出来,成为各个直接基类的共享数据,然后每个基类中多出一个指针,称为虚基表指针。虚基表指针指向一张虚基表,表中存储着一个偏移量,能够让直接基类找到共享部分的数据。

在这里插入图片描述

上图验证了Tercher和Student部分的Person数据是共享的

💡如图,Teacher类和Student类的Person类被抽离出来,放到Assistant内存的最下面,此时的Person是Teacher和Student共享的。Teacher和Student都有一个虚基表指针,指向的虚基表中存有一个偏移量,可以让其找到最下面的Person。

在这里插入图片描述

Q: 为什么要搞一个指针供Tercher部分、Student部分找到属于自己的Person呢?
A: 在切片赋值时,需要通过虚基表中的偏移量,找到基类的数据,完成赋值。如以下情形

void test2()
{Assistant a;Teacher t = a;Student s = a;
}

⭕除了菱形虚拟继承的场景,不要在其它地方运用虚拟继承,很容易出错。



2. 多态

2.1 概念

💭 多态,即多种形态,通俗理解就是不同对象完成同一个行为时,会产生多种不同的结果。

在现实生活中,存在着许多形式的“多态”,例如:车站买票,普通人买全价票,学生买半价学生票,军人可以优先购票;医院排队挂号,普通人要排队,有特殊情况的可走绿色通道,军人优先…

在C++中,多态的具体表现形式主要有两种:函数重载类继承中的多态调用

  • 先试着以函数重载理解多态的概念
class Person
{};class Student :public Person
{};void Buyticket(Person p)
{cout << "全价票" << endl;
}void Buyticket(Student s)
{cout << "学生票" << endl;
}void test2()
{Buyticket(Person());Buyticket(Student());
}

代码中函数Buyticket构成重载,传入不同的类对象(这里是Person类和Student)会有不同的行为,产生不同的结果。

在这里插入图片描述

而下文将着重介绍类继承中的多态。


2.2 多态的定义和实现

🔎 类继承中的多态,是指在一个继承模型中,调用不同类对象的同一个成员函数,会产生不同的结果。比如Student和Soldier继承了Person,并且它们都有成员函数Buyticket,那么当它们分别调用Buyticket时会用不同的结果。

实现多态的条件:

  1. 必须通过基类的引用或指针调用虚函数。
  2. 派生类的虚函数必须对基类的虚函数完成重写(覆盖)

💬示例如下:

class Person
{
public:virtual void Buyticket(){cout << "全价票" << endl;}
};class Student :public Person
{
public:virtual void Buyticket(){cout << "学生票" << endl;}
};class Soldier :public Person
{
public:virtual void Buyticket(){cout << "军人优先购票" << endl;}
};void test1()
{Person* p1 = new Person();Person* p2 = new Student();Person* p3 = new Soldier();p1->Buyticket();p2->Buyticket();p3->Buyticket();
}

在这里插入图片描述

🔎 下面介绍几个概念

  • 虚函数:被virtual关键字修饰的函数称为虚函数。这种函数可以被派生类继承并重写。

在这里插入图片描述

  • 重写:又称覆盖,即派生类对基类的一些函数进行重新定义,使之成为派生类特有的方法。

派生类对基类虚函数的重写需满足三个相同:
函数名相同
参数列表相同
返回值类型相同

在这里插入图片描述

而如下派生类函数不加 virtual 也是可行的,因为派生类继承了基类的函数接口,其virtual特性也被继承了下来。但这种写法并不规范,一般不建议使用。

class Person
{
public:virtual void Buyticket(){cout << "全价票" << endl;}
};class Student :public Person
{
public:void Buyticket(){cout << "学生票" << endl;}
};

虚函数重写存在两个特例:

  1. 协变,即基类虚函数与派生类虚函数的返回值类型不同,也能完成虚函数重写。但也不能是任意的两种不同类型,只有当基类虚函数返回基类对象(可以是其他类)指针/引用,派生类虚函数返回派生类对象(可以是其他类)指针/引用时,才构成协变。
class A
{
public:virtual A* func(){cout << "I am A" << endl;return this;}
};class B :public A
{
public:virtual B* func(){cout << "I am B" << endl;return this;}
};void test3()
{A a;B b;A& ref1 = a;ref1.func();A& ref2 = b;ref2.func();
}

在这里插入图片描述

  1. 析构函数的重写,基类和派生类的析构函数的函数名不同,但能够完成重写。

⭕下面这段代码会出现内存泄漏的问题(析构函数没有重写)

A* p = new B();
delete p;

🔎解释
new B()在堆上开辟了一个B类对象的空间,而p指针指向的是B类对象中的基类A部分。当执行delete p 时,调用的也是A类的析构函数,因此B类的派生类成员没有被释放,造成内存泄漏。

在这里插入图片描述

重写析构函数可以解决这个问题,使得在释放空间时,指针指向基类就释放基类,指向派生类就释放整个派生类。

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

class A
{
public:virtual ~A(){cout << "~A" << endl;}
};class B :public A
{
public:~B(){cout << "~B" << endl;}
};void test4()
{A* pa = new A();delete pa;cout << endl;A* pb = new B();delete pb;
}

在这里插入图片描述


2.3 C++11中的两个关键字

  1. final
  • 修饰类:该类不能再被继承,可理解为“最终类”
class C final
{};class D :public C // err, 不能将final类型用作基类
{};
  • 修饰虚函数:该虚函数不能被派生类重写
class C
{
public:virtual void func() final{cout << "class C" << endl;}
};class D :public C
{
public:virtual void func() // err, 无法重写final函数{cout << "class B" << endl;}
};
  1. override
    检查派生类虚函数是否重写了某个基类的虚函数,若否则编译时报错。
class C
{
public:virtual void func(){cout << "class C" << endl;}
};class D :public C
{
public:virtual void func1() override // func1没有重写,编译报错{cout << "class B" << endl;}
};

2.4 区分重载、重写、重定义

在这里插入图片描述


2.5 抽象类

💭 抽象类是一种不能实例化出对象的类,只能作为基类被继承。 抽象类中包含纯虚函数(在虚函数的声明后面加上 =0称为纯虚函数)。继承了抽象类的派生类也不能实例化出对象,只有重写了纯虚函数才可以实例化。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class AbstractClass
{
public:virtual void func() = 0 ; // 纯虚函数
};class Next :public AbstractClass
{
public:virtual void func(){cout << "cover successfully" << endl;}
};void test6()
{//AbstractClass x;//err, 抽象类不能实例化对象Next* pn = new Next();pn->func();
}

💡 接口继承和实现继承

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

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


2.6 多态的原理

2.6.1 虚函数表

🔎 提问,只有一个虚函数的类的大小为多少?

class A
{
public:virtual void func1(){cout << "func1() of A" << endl;}
};
cout << sizeof(A) << endl;

验证得到,其大小为4个字节

在这里插入图片描述

💭其实,每个包含虚函数的类都至少会有一个虚函数表(本质是一个函数指针数组,又称虚表),用于存放它的虚函数。因此,类对象中会存放一个虚函数表指针,指向其虚函数表。

在这里插入图片描述

💬看下面代码

class A
{
public:virtual void func1(){cout << "func1() of A" << endl;}virtual void func2(){cout << "func2()" << endl;}
};

💡通过调试可以很清楚地观察虚函数表指针和虚函数表的存在

在这里插入图片描述


2.6.2 多态如何实现

  1. 派生类虚函数实现重写的过程

⭕派生类实例化时,会生成属于自己的虚表,总共分三步:

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

如图:

在这里插入图片描述

💬通过代码观察

class A
{
public:virtual void func1(){cout << "func1() of A" << endl;}virtual void func2(){cout << "func2()" << endl;}void func3(){cout << "func3()" << endl;}
};class B :public A
{
public:virtual void func1(){cout << "func1() of B" << endl;}virtual void func4(){cout << "func4()" << endl;}void func5(){cout << "func5()" << endl;}
};

在这里插入图片描述

调试可以很清楚地发现,A::func1被重写为B::func1,func2无重写。但是却看不到派生类B自己的虚函数func4。这是因为vs监视窗口本身的问题,我们可以通过内存窗口更深入地观察虚函数表。

在这里插入图片描述

可以看到虚函数表中,func1和func2的指针后还有一个指针,那正是B类自己的虚函数func4。为了验证其准确性,我们也可以通过如下代码,取出虚函数表中每个虚函数并调用观察。

typedef void(*VFT)();void CheckVFTable(VFT* ptr, int n)
{cout << "虚表地址:" << ptr << endl;for (int i = 0; i < n; ++i){printf("[%d]:%p->", i, ptr[i]);ptr[i]();}cout << endl;
}class A
{
public:virtual void func1(){cout << "func1() of A" << endl;}virtual void func2(){cout << "func2()" << endl;}void func3(){cout << "func3()" << endl;}
};class B :public A
{
public:virtual void func1(){cout << "func1() of B" << endl;}virtual void func4(){cout << "func4()" << endl;}void func5(){cout << "func5()" << endl;}
};
void test5()
{B b;CheckVFTable((VFT*)(*(void**)&b), 3); //取出b对象空间中前4/8个字节,再强制成虚表指针类型
}

💡 测试结果证明,func4的指针确实在B类对象的虚表中

在这里插入图片描述

  1. 多态调用的动态绑定

💭前面我们说过,实现多态,必须通过基类的引用或指针调用虚函数,这是有原因的。基类的引用/指针可以指向基类对象,也可以指向派生类对象(可见基类范围)。多态就是要达到指向基类时执行基类行为,指向派生类时执行派生类行为。经过前面的分析,我们知道基类和派生类都有虚表,在运行时,程序会根据当前指向的对象的虚表中来找到将要调用的虚函数。这种在运行时确认程序的具体行为,称为动态绑定

class Person
{
public:virtual void Buyticket(){cout << "全价票" << endl;}// 其它成员...
};class Student :public Person
{
public:virtual void Buyticket(){cout << "学生票" << endl;}// 其它成员...
};void test1()
{Person* p1= new Student();p1->Buyticket();Person* p2= new Person();p2->Buyticket();
}

如图,p1指向Student类对象时,可以找到Student::Buyticket;p2指向Person类对象时,可以找到Person::Buyticket

在这里插入图片描述

2.7 静态绑定和动态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,
    比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体
    行为,调用具体的函数,也称为动态多态。

可以通过反汇编观察动态绑定的运动机理,及其与普通调用的区别。

在这里插入图片描述

相关文章:

【C++】继承与多态

目录前言1. 继承1.1 继承的概念1.2 继承的定义1.3 切片赋值1.4 继承中的作用域1.5 派生类的默认成员函数1.6 继承与友元、静态成员1.7 多继承、菱形继承、菱形虚拟继承1.7.1 区分单继承与多继承1.7.2 菱形继承1.7.3 菱形虚拟继承1.7.4 菱形虚拟继承的原理2. 多态2.1 概念2.2 多…...

每日分享(微信社区小程序/h5/圈子论坛贴吧交友/博客/社交)

1.Java单元测试实战 高清PDF中文版 Java单元测试实战来自于作者多年来的单元测试实践&#xff0c;最初发表在阿里内网的ATA上&#xff0c;成为了很多阿里同学单元测试学习的必读文章。很多程序员认为单元测试会花费大量的时间&#xff0c;因此他们写单元测试的意愿比较低&…...

第51章 初识第3方阿里云短信验证发送服务

1 第3方阿里云短信验证发送服务准备工作 2 其它第3方短信发送服务平台 当前常用第3方短信发送服务平台还有容联云和凯信通过&#xff0c;在配置上来说阿里云和容联云配置都比较复杂&#xff0c;网上程序集成示例完善&#xff0c;资费灵活。凯信通配置十分简单只要在程序中指定其…...

CSGO社区服搭建服务器架设游戏服务端教程

CSGO社区服搭建服务器架设游戏服务端教程 我是艾西&#xff0c;上一篇说了搭建CSGO服务器需要准备服务器以及安装好所需要的环境&#xff0c;那么今天说一下CSGO社区私人服务器怎么搭建游戏服务端 搭建CSGO服务器比较简单&#xff0c;Valve开发者社区wiki也给出了安装指导&…...

【Linux学习】基础IO——系统调用 | 文件描述符fd | 重定向

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《Linux学习》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 基础IO&#x1f34e;文件操作&#x1f349;使用C接口进行文件操作&#x1f349;文件操作的系统调…...

PLC实验—西门子S7 1200读取超声波传感器数据

PLC实验—西门子S7 1200读取超声波传感器数据 US-016超声波测距模块 实验箱上是US-016超声波测距模块&#xff0c;其有关信息可以看实验室的博客 US-016超声波测距模块 1号Pin&#xff1a;接VCC电源&#xff08;直流5V&#xff09; 2号Pin&#xff1a;量程设置引脚&#xff…...

一次Linux系统密码修改失败事件

一、事件描述 某业务系统采用移动云主机&#xff0c;某次因误操作导致移动云内嵌密码管理相关Pga进程导致页面无法修改密码&#xff0c;东移动云主机web终端登录也无法修改&#xff0c;密码错误次数最大已无法登录&#xff0c;无奈只能重启主机&#xff0c;修改密码&#xff1b…...

C语言循环控制语句Break,goto,continue语句讲解

循环控制语句改变你代码的执行顺序。通过它你可以实现代码的跳转。 C 语言中 break 语句有以下两种用法&#xff1a; 当 break 语句出现在一个循环内时&#xff0c;循环会立即终止&#xff0c;且程序流将继续执行紧接着循环的下一条语句。 它可用于终止 switch 语句中的一个 …...

Zabbix“专家坐诊”第183期问答汇总

问题一 Q&#xff1a;老师&#xff0c;请问一下zabbix采集的数据怎么过滤&#xff0c;获取数据是nottime20:30 notafter3&#xff0c;怎么过滤出netafter3 &#xff1f;谢谢。 A&#xff1a;过滤器设置如下图。 问题二 Q&#xff1a;大佬&#xff0c;请问一下被管节点部署了…...

以太网协议和DNS

目录 &#x1f415;今日良言:跨过困难,去迎接更好的自己. &#x1f433;一、以太网桢格式 &#x1f433;二、DNS &#x1f415;今日良言:跨过困难,去迎接更好的自己. &#x1f433;一、以太网桢格式 数据链路层考虑的是相邻两个节点(通过网线/光纤/无线直接相连的两个设备…...

【设计模式】原型模式与建造者模式

原型模式 原型模式是指通过原型实例指定创建对象的种类&#xff0c;然后通过拷贝的方式创建新的对象。属于创建型模式 原型模式的核心在于拷贝原型对象&#xff0c;主要用于对对象的复制。当你需要通过一大段get/set方法去构建对象的时候&#xff0c;就可以考虑使用原型模式了…...

QT之Http请求

今天我们介绍一下QT的http请求&#xff0c;这里有post和get两种方式 一、post请求方式&#xff1a;参数分为网址和具体请求值两个部分&#xff0c;有时候还需要设置头信息&#xff0c;看具体情况定 1 void MainWindow::sendPostRequest()2 {3 QNetworkAccessManager *m_pH…...

何谓dB , dB怎么理解?

dB 是什么单位 ?愈低愈好吗?对于声频 ( 声学及电子声学 ) 方面的单位&#xff0c;它是以分贝(decibel &#xff0c;dB ) 来做结果的。斯多里一生专注于科学,1876 发明电话&#xff0c;我们都知道贝尔发明了电话&#xff0c;然而重要的是&#xff0c;他发现我们人类耳朵对声音…...

【数据聚类|深度聚类】Unsupervised Deep Embedding for Clustering Analysis(DEC)论文研读

DEC算法由两部分组成 第一部分会预训练一个SDAE模型;第二部分选取SDAE模型中的Encoder部分,加入聚类层,然后最小化KL散度进行训练聚类Absratct 提出了一种利用深度神经网络同时进行表征学习和聚类分配的方法,称之为深度嵌入聚类。该方法学习从数据空间到低纬空间的映射,并…...

Nuxt.js项目开发过程遇到的问题以及对Nuxt.js的学习与总结

文章目录&#x1f4cb;前言&#x1f4bb;Nuxtjs3快速了解&#x1f3af;nuxtjs是什么&#xff1f;官网是这样介绍它的。&#x1f3af;关于nuxtjs的SSR开发&#x1f9e9;SSR应用场景&#x1f9e9;nuxtjs的特性&#x1f4bb;nuxtjs的初始目录结构&#x1f3af;关于各个目录的解释&…...

JavaSE:异常机制

异常概念异常发生的原因用户输入非法数据要打开的文件不存在网络中断/JVM内存溢出&#xff08;JVM是JRE的一部分。它是一个虚构出来的计算机&#xff09;除0、非法参数等三种类型的异常&#xff0c;用以理解异常检查性异常【otherException】&#xff1a;程序员无法预见的&…...

Go mockito 使用说明 (github/mockey)

GitHub - bytedance/mockey: a simple and easy-to-use golang mock library Go mockito 是什么? mockey是一个简单易用的golang mock库&#xff0c;可以快速方便的mock函数和变量。目前广泛应用于字节跳动服务的单元测试编写。底层是monkey patch&#xff0c;通过在运行时重…...

Spring Boot+Vue前后端分离项目练习01之网盘项目的搭建

1.前期设计 一般看来&#xff0c;网盘系统主要实体包括&#xff1a;用户、存储信息、用户文件、文件、回收文件等基本功能点。 各实体对应的表结构如下所示&#xff1a; 表名&#xff1a;user&#xff08;用户表&#xff09; 字段名属性说明userIdbigint主键usernamevarcha…...

超详细MySQL(免安装版)安装与配置

一、MySQL下载 首先打开MySQL官网&#xff0c;官网首页地址为 MySQL官网首页地址 进入官网后如下图所示&#xff0c;点击DOWNLOADS进入下载页面 下滑页面找到MySQL Community&#xff08;GPL&#xff09;Downloads>>并点击 接下来点击MySQL Community Server 若想要安…...

STM32F1,F4,L1系列禁止JTAG和SW引脚方法

STM32F1系列 程序中在使用到JTAG、SWD的某个IO 时&#xff0c;需要禁用掉相关调试方法后&#xff0c;再配置相应的IO方式。在需要相应的接口配置前使用这些代码。 对于F1系列&#xff0c;调用函数进行专门的禁止。 标准库配置方式&#xff1a; RCC_APB2PeriphClockCmd(RCC_A…...

NVIDIA CUDA初级教程视频学习笔记1

周斌老师 课程链接&#xff1a; 目录第一课内容生态环境第一课 CPU体系架构的概述什么是CPU指令存储器架构CPU内部的并行性第一课 预修课程&#xff1a; Cuda c programming guide 参考内容 1,2&#xff0c;3查找一下。 内容 CPU体系架构概述 并行程序设计概述 CUDA开发环境…...

CEC2005:星雀优化算法(Nutcracker optimizer algorithm,NOA)求解CEC2005(提供MATLAB代码)

一、星雀优化算法NOA 星雀优化算法(Nutcracker optimizer algorithm,NOA)由Mohamed Abdel-Basset等人于2023年提出&#xff0c;该算法模拟星雀的两种行为&#xff0c;即&#xff1a;在夏秋季节收集并储存食物&#xff0c;在春冬季节搜索食物的存储位置。 星鸦单独或成对活动&…...

工作实战之密码防重放攻击

目录 前言 一、登录认证密码加密 二、bcrypt加密密码不一样&#xff0c;匹配原理 1.程序运行现象 2.原理解释 三、密码防重放 总结 前言 密码重放攻击&#xff1a;请求被攻击者获取&#xff0c;并重新发送给认证服务器&#xff0c;从而达到认证通过的目的 一、登录认证密…...

如何编写测试用例?

编写测试用例流程 1、确定测试目标&#xff1a;首先&#xff0c;需要明确测试目标&#xff0c;即测试人员需要测试什么&#xff0c;这有助于测试人员了解需要测试哪些方面&#xff0c;以及如何测试。 2、识别测试场景&#xff1a;在确定测试目标后&#xff0c;测试人员需要识…...

5.排序算法之二:选择排序

选择排序&#xff08;select sort&#xff09;在无序列表中&#xff0c;把无序列表分成有序区&#xff08;刚开始有序区元素个数为0&#xff09;和无序区&#xff08;刚开始无序区元素个数为n&#xff09;&#xff0c;循环n-1趟&#xff0c;每一趟找到最小或最大的那个元素&…...

Ubuntu18系统安装:node及node版本管理工具nvm部署前端项目

注意在安装之前先安装好Git 如何在Ubuntu 上安装Git与入门教程_ubuntu安装git_飞鹰雪菲的博客-CSDN博客 1、把nvm远程镜像克隆到指定目录 git clone https://gitee.com/mirrors/nvm 1.1在终端指定的文件夹下 drciZwz91oq31508figapkas0Z:~/qiang/tools$ git clone https://…...

统计学 假设检验

文章目录假设检验假设检验的基本原理提出假设作出决策表述决策结果一个总体参数的检验总体均值的检验总体比例的检验总体方差的检验两个总体参数的检验两个总体均值之差的检验两个总体比例之差的检验两个总体方差比的检验总体分布的检验正态性检验的图示法Shapiro-Wilk 和 K-S …...

【C++】哈希

哈希一、unordered系列关联式容器二、哈希原理2.1 哈希映射2.2 哈希冲突2.2.1 闭散列—开放地址法2.2.2 代码实现2.2.3 开散列—拉链法2.2.4 代码实现三、哈希封装unordered_map/unordered_set3.1 基本框架3.2 迭代器实现3.2.3 operator*和operator->和operator!3.2.4 opera…...

「TCG 规范解读」PC 平台相关规范(3)

可信计算组织&#xff08;Ttrusted Computing Group,TCG&#xff09;是一个非盈利的工业标准组织&#xff0c;它的宗旨是加强在相异计算机平台上的计算环境的安全性。TCG于2003年春成立&#xff0c;并采纳了由可信计算平台联盟&#xff08;the Trusted Computing Platform Alli…...

这篇教你搞定Android内存优化分析总结

一、内存优化概念1.1 为什么要做内存优化&#xff1f;内存优化一直是一个很重要但却缺乏关注的点&#xff0c;内存作为程序运行最重要的资源之一&#xff0c;需要运行过程中做到合理的资源分配与回收&#xff0c;不合理的内存占用轻则使得用户应用程序运行卡顿、ANR、黑屏&…...

wordpress登录密码错误也不报错/百度推广竞价是什么意思

2019年高考结束最后一门英语考试的时候&#xff0c;王恒杰这个懂得感恩的大男孩被我们记住了。在高考英语结束后&#xff0c;所有人都放下了心中的担心&#xff0c;轻轻松松的走出了考场。此刻王恒杰也是万千学子中的一名&#xff0c;但是&#xff0c;他之后的举动却让他在众多…...

jsp和.net做网站的区别/全国唯一一个没有疫情的城市

新手入门CSDN欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也…...

客服外包在哪个平台接业务/苏州seo报价

算法流程 优化算法 优化1&#xff1a;Center-Symmetric Census Transform (CSCT) CSCT并行 MC并行 优化2&#xff1a;代价聚合并行 实验结果 除了在集成了 8 个 ARM 内核和 2 个 Maxwell SM 且 TDP 为 10W 的 NVIDIA Tegra X1 上执行之外&#xff0c;为了进行比较&#xff…...

做淘宝客的的网站有什么要求吗/seo教学平台

刚开始接触java的时候感觉要学习太多的东西了&#xff0c;而且听别人说很难&#xff0c;就有点畏惧感&#xff0c;可是慢慢的就感觉并没有那么难&#xff0c;和c语言有很多类似的地方&#xff0c;也有很多互通的知识&#xff0c;感觉慢慢去深入的学习java&#xff0c;还是可以一…...

做网站需求文档/南宁整合推广公司

代码如下2113&#xff1a; 1). xml可能的中文编码错误处理 def xml_Error_C(filename): fp_xmlopen(filename) fp_x#中文乱5261码改正 for i in range(os.path.getsize(filename)): i1 afp_xml.read(1) if a&: fp_xml.seek(-1,1) if fp_xml.read(6) : i5 continue else: fp…...

手机网站开发者模式/什么是整合营销概念

阿里篇&#xff08;仅有问题&#xff0c;没有答案需要大家共同学习探讨&#xff09; 如何实现一个高效的单向链表逆序输出&#xff1f;已知 sqrt (2)约等于 1.414&#xff0c;要求不用数学库&#xff0c;求 sqrt (2)精确到小数点后 10 位。给定一个二叉搜索树(BST)&#xff0c…...