继承和多态
分类:
IT文章
•
2024-05-14 17:13:30
继承
public、private、protected
继承方式和属性
(1) 公有继承(public)
公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。
(2)私有继承(private)
私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
(3)保护继承(protected)
保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。


请看我的的另一篇:
https://blog.****.net/qq_38504396/article/details/78658824
继承的基本测试
// public 修饰的成员 变量,方法,在类的内部和外部都可以访问
// protected 修饰的成员 变量,方法,在类的内部使用,在继承的子类中使用,不能在类的外部使用
// private 修饰的成员 变量,方法, 只能在类的内部使用 不能再类的外部使用
// 1 preotected 修饰的成员 变量,函数,是为了在家族中使用,为了继承
// 2 项目开发中 一般使用 public
//
// Created by lk on 18-6-11.
//
#include <iostream>
using namespace std;
class Parent1
{
public:
void print()
{
cout << "a = " << a << " b = " << b << endl;
}
int a;
protected:
private:
int b;
};
// class Child: private Parent
// class Child: protected Parent
// 一般都是public继承
class Child1: public Parent1
{
public:
protected:
private:
int c;
};
int main()
{
Child1 c1;
c1.a = 1;
// c1.b = 2;
cout << "Hello world!" << endl;
return 0;
}
继承最基本的用法
#include <iostream>
using namespace std;
class Parent {
public:
int a;
void print() {
cout << "print T" << endl;
}
protected:
int b;
private:
int c;
};
// 保护继承
class Child24 : protected Parent {
public:
void useVar() {
a = 0; // ok
b = 0; // ok
// c = 0; err
}
protected:
private:
};
int main() {
Child24 c1;
// c1.a = 10; // 都是错的因为继承是protected
// c1.b = 20;
// c1.c =30;
return 0;
}
protected继承
#include <iostream>
using namespace std;
class Parent {
public:
int a;
void print() {
cout << "print T" << endl;
}
protected:
int b;
private:
int c;
};
// 私有继承
class Child : private Parent {
public:
void useVar() {
a = 0; // ok
b = 0; // ok
// c = 0; err
}
protected:
private:
};
int main() {
Child c1;
// c1.a = 10; 都是错的因为继承是private
// c1.b = 20;
// c1.c =30;
return 0;
}
private继承
#include <iostream>
using namespace std;
class Parent {
public:
int a;
void print() {
cout << "print T" << endl;
}
protected:
int b;
private:
int c;
};
/*
三看原则
c++中的继承方式(public, protected, private)会影响子类对外的访问属性
判断一句话能否被访问
1> 看调用语句,是写在子类的内部,还是外部
2> 看子类的继承方式 (public, protected, private)
3> 看父类的 访问级别 (public, protected, private)
*/
// 公有继承
class Child2 : public Parent {
public:
void useVar() {
a = 0; // 正确
b = 0; // 正确
// c = 0; // 错误
}
protected:
private:
};
int main() {
Parent p1, p2;
p1.a = 10; // ok
// p1.b = 100; //err
// p1.c = 1000; //err
cout << "Hello world!" << endl;
return 0;
}
public继承
类型兼容性原则 重要
类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。
通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员。
这样,公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决。
类型兼容规则中所指的替代包括以下情况:
子类对象可以当作父类对象使用
子类对象可以直接赋值给父类对象
子类对象可以直接初始化父类对象
父类指针可以直接指向子类对象
父类引用可以直接引用子类对象
在替代之后,派生类对象就可以作为基类的对象使用,但是只能使用从基类继承的成员。
类型兼容规则是多态性的重要基础之一。
子类就是特殊的父类 (base *p = &child;)
#include <iostream>
using namespace std;
class Parent{
public:
void printP() {
cout << "我是父类" << endl;
}
Parent() {
cout << "构造函数" << endl;
}
Parent(const Parent&rho) {
cout << "拷贝构造函数" << endl;
}
protected:
private:
int a;
};
class Child : public Parent{
public:
void printC() {
cout << "我是子类" << endl;
}
protected:
private:
int b;
};
/*
兼容规则中所指的替代 包括以下情况
子类对象可以当做父类对象使用
子类对象可以直接赋值给父类对象
子类对象可以直接初始化父类对象
父类指针可以直接指向子类对象
父类引用可以直接引用子类对象
*/
// c++编译器不会报错
void howToPrint(Parent*base) {
base->printP(); // 只能执行父类的成员(调用子类和父类都有的成员)
}
void howToPrint(Parent&base) {
base.printP(); // 只能执行父类的成员
}
int main() {
Parent p1;
p1.printP();
Child c1;
c1.printC();
c1.printP();
// 赋值兼容性原则,第一层含义
// 1-1 父类指针 (引用) 指向子类对象 重要
Parent*p = NULL;
p = &c1;
p->printP();
// 1-2指针做函数参数
howToPrint(&p1);
howToPrint(&c1);
// 1-3引用做函数参数
howToPrint(p1);
howToPrint(c1);
// 赋值兼容性原则,第二层含义
// 可以让子类对象 初始化 父类对象
// 子类对象是特殊的父类对象
Parent p3 = c1;
cout << "p1 = " << sizeof(p1) << " c1 = " << sizeof(c1) << endl;
cout << "Hello world!" << endl;
return 0;
}
示例
当兼容性原则遇到函数重写(virtual 函数重写,发生多态)
#include <iostream>
using namespace std;
class Parent {
public:
Parent(int a) {
this->a = a;
// cout <<"parent a = " << a << endl;
}
virtual void print() {
cout << "Parent 打印 a = " << a << endl;
}
protected:
private:
int a;
};
class Child : public Parent {
public:
Child(int b) : Parent(10) {
this->b = b;
// cout <<"Child b = " << b << endl;
}
// 如果父类写了 子类的virtual 可写可不写, 最好写上
// 子类和父类的名字一样
virtual void print() {
cout << "Child 打印 b = " << b << endl;
}
protected:
private:
int b;
};
void howToPrint(Parent *base) {
base->print(); // 一种调用语句,有多种表现形态
}
void howToPrint(Parent &base) {
base.print();
}
int main() {
// print()函数不写virtual时
Parent *base = NULL;
Parent p1(20);
Child c1(10);
base = &p1;
base->print(); // 执行父类的打印函数
base = &c1;
base->print(); // 执行谁的函数? 子类 // 面向对象新需求
// 执行父类的函数
cout << endl;
{
Parent &base2 = p1;
base2.print(); // 父类
Parent &base3 = c1; // base3是c1的别名
base3.print(); // 打印的还是子类
}
cout << endl;
{
// 函数调用
howToPrint(&p1); // 调用父类
howToPrint(&c1); // 调用子类
cout << endl;
howToPrint(p1); // 调用父类
howToPrint(c1); // 调用子类
}
return 0;
}
兼容性原则,遇到函数重写
继承中的构造和析构
问题:如何初始化父类成员?父类与子类的构造函数有什么关系
在子类对象构造时,需要调用父类构造函数对其继承得来的成员进行初始化
在子类对象析构时,需要调用父类析构函数对其继承得来的成员进行清理
继承中构造和析构的顺序
1、子类对象在创建时会首先调用父类的构造函数
2、父类构造函数执行结束后,执行子类的构造函数
3、当父类的构造函数有参数时,需要在子类的初始化列表中显示调用
4、析构函数调用的先后顺序与构造函数相反
就是: 构造函数: 由内到外
: 析构函数: 由外到内
#include <iostream>
using namespace std;
// 结论
// 先调用父类构造函数,在调用子类构造函数
// 析构顺序:先调用子类析构函数,在调用父类析构函数
class Parent
{
public:
void printP(int a = 0, int b = 0)
{
this->a = a;
this->b = b;
cout << " 我是父类" << endl;
}
Parent(int a = 0, int b = 0)
{
this->a = a;
this->b = b;
cout << " 父类构造函数" << endl;
}
Parent(const Parent &rho)
{
cout << " 拷贝构造函数"<< endl;
}
~Parent()
{
cout << " 父类析构函数" << endl;
}
protected:
private:
int a;
int b;
};
class Child: public Parent
{
public:
void printC ()
{
cout << " 我是子类" << endl;
}
Child(int a, int b,int c): Parent(a, b)
{
this->c = c;
cout << " 子类构造函数" << endl;
}
~Child()
{
cout << " 子类析构函数" << endl;
}
protected:
private:
int c;
};
void playObj()
{
Parent p1(1, 2);
Child c1(3, 4, 5);
}
int main()
{
playObj();
return 0;
}
//父类构造函数
//父类构造函数
//子类构造函数
//子类析构函数
//父类析构函数
//父类析构函数
构造和析构的顺序测试
#include <iostream>
using namespace std;
// 这个程序不能调试
// 构造顺序:先调用父类对象,如果父类还有父类(老祖宗) 先调用老祖宗构造函数, 然后父类,最后子类
// 析构函数:先析构子类,然后父类,最后老祖宗
class Object {
public:
Object(int a, int b) {
this->a = a;
this->b = b;
cout << " Object构造函数 a = " << a << " b = " << b << endl;
}
~Object() {
cout << " Object析构函数" << endl;
}
protected:
int a;
int b;
private:
};
class Parent : public Object {
public:
Parent(char *p) : Object(1, 2) {
this->m_p = p;
cout << " Parent构造函数:p " << p << endl;
}
~Parent() {
cout << " Parent析构函数" << endl;
}
void printP() {
cout << " 我是父类" << endl;
}
protected:
char *m_p;
private:
};
class Child : public Parent {
public:
// 构造函数调用顺序
Child(char *p) : Parent(p), obj1(3, 4), obj2(5, 6) {
this->myp = p;
cout << " Child构造函数 myp " << myp << endl;
}
~Child() {
cout << " Child析构函数" << endl;
}
void printC() {
cout << " 我是子类" << endl;
}
protected:
char *myp;
Object obj1;
Object obj2;
private:
};
void play() {
Child c1("继承测试");
}
int main() {
play();
cout << " Hello world!" << endl;
return 0;
}
继承组合混搭,有点小迷
//调用顺序:Parent(p)构造时一看是继承来的,向上找到parent,parent一看自己还有其他的是继承的,所以向上找
//然后找到object没了(调用第一次a=1..),然后回到parent那调用char*构造,
//parent一看自己还有继承的构造,然后obj1(3,4)和(5,6),
//调用完成后返回到child类完成构造函数
//开始析构函数
//Object构造函数 a = 1 b = 2
//Parent构造函数:p 继承测试
//Object构造函数 a = 3 b = 4
//Object构造函数 a = 5 b = 6
//Child构造函数 myp 继承测试
// Child析构函数
//Object析构函数
// Object析构函数
//Parent析构函数
// Object析构函数
//Hello world!
继承组合混带结果
继承中重名成员的调用方法



多继承和虚继承
// 多继承的二义性:子类继承父类,如果有多个父类,并且父类中相同的属性,就会产生二义性
// 二义性解决方案:在继承关系时加上virtual关键字: class b : virtual public b1, virtual b2;
// 但是解决不彻底(多继承始终存在问题)
// 多继承在项目开发中一般不会用到
// class A{int a}; B : virtual A {int b};, C:public A {int c};;
// sizeof(A) = 4, B = 12, C = 8;
// 在虚继承中 编译器会偷偷的增加属性
#include <iostream>
using namespace std;
class Base1
{
public:
Base1(int b = 0)
{
b1 = b;
cout << "B1有参构造函数" << endl;
}
void printB1()
{
cout << "b1 = " << b1 << endl;
}
protected:
private:
int b1;
};
class Base2
{
public:
Base2(int b = 0)
{
b2 = b;
cout << "B2有参构造函数" << endl;
}
void printB2()
{
cout << "b2 = " << b2 << endl;
}
protected:
private:
int b2;
};
class B: public Base1, public Base2
{
public:
B(int b1, int b2, int c):Base1(b1),Base2(b2)
{
this->c = c;
cout << "B的构造函数" << endl;
}
void printC()
{
cout << "c = " << c << endl;
}
protected:
private:
int c;
};
int main()
{
B b1(1, 2, 3);
b1.printB1();
b1.printB2();
b1.printC();
return 0;
}
// 多继承的二义性:子类继承父类,如果有多个父类,并且父类中相同的属性,就会产生二义性
// 二义性解决方案:在继承关系时加上virtual关键字: class b : virtual public b1, virtual b2;
// 但是解决不彻底(多继承始终存在问题)
// 多继承在项目开发中一般不会用到
// class A{int a}; B : virtual A {int b};, C:public A {int c};;
// sizeof(A) = 4, B = 12, C = 8;
// 在虚继承中 编译器会偷偷的增加属性
了解
继承总结
继承是面向对象程序设计实现软件重用的重要方法。程序员可以在已有基类的基础上定义新的派生类。
单继承的派生类只有一个基类。多继承的派生类有多个基类。
派生类对基类成员的访问由继承方式和成员性质决定。
创建派生类对象时,先调用基类构造函数初始化派生类中的基类成员。调用析构函数的次序和调用构造函数的次序相反。
C++提供虚继承机制,防止类继承关系中成员访问的二义性。
多继承提供了软件重用的强大功能,也增加了程序的复杂性。
多态
函数重载发生在同一个类中
在继承中:
注意:虚函数发生重写(多态发生)必须有virtual关键字,
非虚函数重写(又叫重定义)(父类有一个函数, 但是子类想要重新定义这个函数 条件: 有继承关系,函数名相同,不能有virtual关键字(有的话就是虚函数重写了))
什么是多态:
同种形态的不同表现形式
多态的分类:
静态多态(静态联编)
动态多态(动态联编)
静态多态和动态多态的区别其实只是在什么时候将函数实现和函数调用关联起来,是在编译时期还是运行时期,即函数地址是早绑定还是晚绑定的?
静态多态是指在编译期间就可以确定函数的调用地址,并生产代码,这就是静态的,也就是说地址是早早绑定的,静态多态也往往被叫做静态联编。
动态多态则是指函数调用的地址不能在编译器期间确定,必须需要在运行时才确定,这就属于晚绑定,动态多态也往往被叫做动态联编。
//父类中被重写的函数依然会继承给子类
//默认情况下子类中重写的函数将隐藏父类中的函数
//通过作用域分辨符::可以访问到父类中被隐藏的函数
https://blog.****.net/xy913741894/article/details/52939323
多态发生的条件:
必须有继承,虚函数重写, virtual关键字, 实参传入指针或者引用(形参必须是父类指针)..搭建舞台
总结来说:
1要有继承
2要有虚函数重写
3用父类指针(父类引用)指向子类对象....(搭建舞台和调用)
多态的引出
#include <iostream>
// 我自己 想的,不一定对
using namespace std;
class Parent {
public:
Parent(int a) {
this->a = a;
}
void print() {
cout << "Parent 打印 a = " << a << endl;
}
protected:
private:
int a;
};
class Child : public Parent {
public:
Child(int b) : Parent(10) {
this->b = b;
}
void print() {
cout << "Child 打印 b = " << b << endl;
}
protected:
private:
int b;
};
int main() {
Parent p1(10);
Child c1(20);
Parent *p2 = &c1;
p2->print(); // 调用父类函数, 需要调用子类时 用作用域运算符来写(麻烦) 引出多态
return 0;
}
引出多态
多态的案例:
#include <iostream>
using namespace std;
// HeroFighter AdvHeroFighter EnemyFighter
class HeroFighter
{
public:
virtual int power() // 看到virtual关键字 编译器会对power特殊处理
{
return 10; // 自己的战斗机
}
protected:
private:
};
class EnemyFighter
{
public:
int attack() // 敌人的战斗机
{
return 15;
}
protected:
private:
};
class AdvHeroFighter: public HeroFighter
{
public:
virtual int power()
{
return 20;
}
protected:
private:
};
class AdvAdvHeroFighter: public HeroFighter
{
public:
virtual int power()
{
return 30;
}
protected:
private:
};
// 多态 准换平台,要想发生多态 注意最好是这里接受基类的形参
// 并且 必须是 指针,或者 引用时 才能发生多态
//多态的威力
// 1 playObj给对象搭建舞台 看成一个框架
void PlayObj(HeroFighter *const hf, EnemyFighter *const ef)
{
// 如果这个power函数不写virtual
// 静态联编,c++编译器根据HeroFight类型 去执行这个power函数,编译阶段就已经决定了函数的调用
// 动态联编 : 迟绑定 :在运行时根据具体的对象(类型) 执行不同对象的函数,表现成多态0
if (hf->power() > ef->attack()) // hf->power()函数调用将会有多态发生
{
cout <<"主角win" << endl;
}
else
{
cout << "主角挂掉"<< endl;
}
}
// 引用中使用多态
void PlayObj(HeroFighter &hf, EnemyFighter &ef)
{
if (hf.power() > ef.attack())
{
cout <<"主角win" << endl;
}
else
{
cout << "主角挂掉"<< endl;
}
}
// 面向对象的思想
// 封装: 突破c函数的概念...用类做函数参数的时候,可以使用对象的属性 和对象的方法
// 继承:A B 代码的复用
// 多态:可以使用未来...
// 多态很重要
// 多态发生的条件
// c语言 间接赋值 指针存在的最大意义
// 是c语言特有的现象(1:定义一个变量,2:建立关联,3:*p在被调用函数中去间接修改函数的值)
// 实现多态的三个条件
// 1要有继承
// 2要有虚函数重写
// 3用父类指针(父类引用)指向子类对象....(搭建舞台和调用)
int main()
{
HeroFighter hf;
AdvHeroFighter Adv_hf;
EnemyFighter ef;
// 指针
PlayObj(&hf, &ef); // 调用父类
PlayObj(&Adv_hf, &ef); // 调用子类
// 引用
PlayObj(hf, ef); // 调用父类
PlayObj(Adv_hf, ef); // 调用子类
AdvAdvHeroFighter AdvAdv_hf;
PlayObj(&AdvAdv_hf, &ef); // 这个框架能把 后来人写的代码,给调用起来
return 0;
}
// 并没有使用多态
int main1401()
{
HeroFighter hf;
AdvHeroFighter Adv_hf;
EnemyFighter ef;
if (hf.power() > ef.attack())
{
cout <<"主角win" << endl;
}
else
{
cout << "主角挂掉"<< endl;
}
if (Adv_hf.power() > ef.attack())
{
cout <<"Adv主角win" << endl;
}
else
{
cout << "Adv主角挂掉"<< endl;
}
return 0;
}
多态的案例
比较大的例子,推荐看看
多态练习, 企业信息管理
https://files.cnblogs.com/files/xiaokang01/%E5%A4%9A%E6%80%81%E6%A1%88%E4%BE%8B-%E4%BC%81%E4%B8%9A%E5%91%98%E5%B7%A5%E4%BF%A1%E6%81%AF%E7%AE%A1%E7%90%86.zip
虚析构函数
#include <iostream>
#include <cstring>
using namespace std;
// 虚析构函数
class A {
public:
A() {
p = new char[20];
strcpy(p, "obja");
cout << "A()" << endl;
}
virtual ~A() {
delete[]p;
cout << "~A()" << endl;
}
protected:
private:
char *p;
};
class B : public A {
public:
B() {
p = new char[20];
strcpy(p, "objb");
cout << "B()" << endl;
}
virtual ~B() {
delete[]p;
cout << "~B()" << endl;
}
protected:
private:
char *p;
};
class C : public B {
public:
C() {
p = new char[20];
strcpy(p, "objc");
cout << "C()" << endl;
}
~C() {
delete[]p;
cout << "~C()" << endl;
}
protected:
private:
char *p;
};
// 在这个场景下,只执行了父类的析构函数
// 面试题
// 想通过父类指针,把所有的子类对象的析构函数 都执行一边
// 想父类指针,释放所有子类资源
// 特别注意 在父类不加virtual关键字和加的区别
void howToDelete(A *base) {
delete base; // 这句话不会表现为多态,为静态
}
int main() {
C *myC = new C; // new delete匹配
// delete myC;
// 直接通过子类对象释放资源,不需要写virtual,
// 但是有时候不能这样写,所以还是最好加virtual
howToDelete(myC);
return 0;
}
虚析构函数
重载重写重定义
函数重载发生在同一个类中
在继承中:
注意:虚函数发生重写(多态发生)必须有virtual关键字,
非虚函数重写(又叫重定义)(父类有一个函数, 但是子类想要重新定义这个函数 条件: 有继承关系,函数名相同,不能有virtual关键字(有的话就是虚函数重写了))
// 重载 重写重定义
// 重写发生在两个类之间
// 重载必须在一个类之间
// 重写分为两类
// 1虚函数重写 将发生多态
// 2非虚函数重写 (重定义)
#include <iostream>
using namespace std;
// 重载 重写重定义
// 重写发生在两个类之间
// 重载必须在一个类之间
// 重写分为两类
// 1虚函数重写 将发生多态
// 2非虚函数重写 (重定义)
class Parent {
// 这三个函数都是重载关系
public:
void abc() {
cout << "Parent abc" << endl;
}
virtual void func() {
cout << "func() do..." << endl;
}
virtual void func(int i) {
cout << "func() do... " << i << endl;
}
virtual void func(int i, int j) {
cout << "func() do..." << i << " " << j << endl;
}
virtual void func(int i, int j, int k, int f) {
cout << "func()4个参数" << endl;
}
protected:
private:
};
class Child : public Parent {
public:
void abc() // abc函数将发生重定义,如果在父类中的abc前加一个virtual则是虚函数重写(会发生多态)
{
cout << "Child abc" << endl;
}
void abc(int i) // 发生重载 和上面的abc无参数的
{
cout << "abc()" << i << endl;
}
virtual void func(int i, int j) // 虚函重写
{
cout << "func(int i, int j) do..." << i << " " << j << endl;
}
virtual void func(int i, int j, int k) //
{
cout << "func(int i, int j, int k) do..." << endl;
}
protected:
private:
};
// 重载重写和重定义
int main() {
Child c1;
// c1.func(); // 不能用的原因是:虽然子类继承了父类的func(),
// 但是子类的func(int i, int j)将无参的进行了覆盖,要想调用必须显示调用
// 子类无法重载父类的函数,父类同名函数将被名称覆盖
//c1.func() 如果想用
c1.Parent::func();
// 1 c++编译器看到func名字,因子类中func名字已经存在(名称覆盖),所以不会再去父类找4个参数的func了
// 2c++编译器只会在子类里查找func函数,找到了两个,但是都没有4个参数的所以报错
// 若想调用父类的4个参数的函数,则指出作用域
// c1.func(1,2,3,4);
// func函数的名字,在子类中发生了名称覆盖,子类的函数的名字,占用了父类的函数的名字的位置
// 因为子类的中已经有了func函数的名字的重载形式。。。
// 编译器开始在子类中找func函数。。。但是没有0个参数的func函数
return 0;
}
重载,重写(虚函数和非虚函数)
多态理论基础
01静态联编和动态联编
1、联编是指一个程序模块、代码之间互相关联的过程。
2、静态联编(static binding),是程序的匹配、连接在编译阶段实现,也称为早期匹配。
重载函数使用静态联编。
3、动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定)。
switch 语句和 if 语句是动态联编的例子。
4、理论联系实际
1、C++与C相同,是静态编译型语言
2、在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象;所以编译器认为父类指针指向的是父类对象。
3、由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象
从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数。这种特性就是静态联编。
多态实现的原理探究
#include <iostream>
using namespace std;
// 多态发生的条件
// 要有继承,虚函数重写,父类指针(引用)指向子类对象
class Parent
{
public:
Parent(int a = 0)
{
this->a = a;
}
virtual void print()
{
cout <<"我是父类"<< endl;
}
protected:
private:
int a;
};
class Child : public Parent
{
public:
Child(int b = 0, int a = 0):Parent(a)
{
this->b = b;
}
virtual void print() // 1动手脚 virtual关键字 会特殊处理 // 虚函数表
{
cout <<"我是子类"<< endl;
}
protected:
private:
int b;
};
void playObj(Parent * base)
{
base->print(); // 这里会发生多态 2 动手脚
// 效果:传来子类对象,执行子类的print函数,传来父类对象,执行父类。。。
// c++编译器根本不需要区分是子类对象,还是父类对象
// 父类对象和子类对象都有vptr指针 ==>虚函数表 ==> 函数入口地址
// 实现了:迟邦定(运行时,c++编译器才会判断)
}
int main()
{
Parent p1; // 3 提前布局 动手脚
// 用类定义对象的时候 c++编译器,会在对象中添加vptr指针
Child c1; // 子类对象也有一个vptr指针
playObj(&p1);
playObj(&c1);
cout << "Hello world!" << endl;
return 0;
}
多态原理探究
C++多态实实现的原理
当类中声明虚函数时,编译器会在类中生成一个虚函数表
虚函数表是一个存储类成员函数指针的数据结构
虚函数表是由编译器自动生成与维护的
virtual成员函数会被编译器放入虚函数表中
存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针)





侯捷视频 面向对象下的vptr和vtbl 17中讲的


在一个继承体系中每出现一个虚函数,在其结构中都会有一个vptr指针,该指针指向一个vtabl表
在vtable表中存放的是 虚函数, 具体调用那一个,看传过来的是那一个类的指针(就是上图的P)
证明vptr指针的存在
#include <iostream>
using namespace std;
class A
{
public:
void printf()
{
cout<<"aaa"<<endl;
}
protected:
private:
int a;
};
class B
{
public:
virtual void printf()
{
cout<<"aaa"<<endl;
}
protected:
private:
int a;
};
int main()
{
//加上virtual关键字 c++编译器会增加一个指向虚函数表的指针 。。。
printf("sizeof(a):%d, sizeof(b):%d
", sizeof(A), sizeof(B));
cout<<"hello..."<<endl;
return 0;
}
证明vptr的存在
在构造函数中调用虚函数,为什么不能实现多态
1)对象中的VPTR指针什么时候被初始化?
#include <iostream>
using namespace std;
class Parent
{
public:
Parent(int a = 0)
{
this->a = a;
print();
}
virtual void print()
{
cout <<"我是父类"<< endl;
}
protected:
private:
int a;
};
class Child : public Parent
{
public:
Child(int b = 0, int a = 0):Parent(a)
{
this->b = b;
}
virtual void print()
{
cout <<"我是子类"<< endl;
}
protected:
private:
int b;
};
int main()
{
Parent p1(1); // 正常来说,构造函数会调用虚函数
Child c1(1,2); // 定义一个子类对象,在这个过程中,在父类构造函数中调用虚函数print,能发生多态吗,
// 不能, 如果能的话,结果就会打印我是子类
// c1.print();
return 0;
}
多态的分布初始化
对象在创建的时,由编译器对VPTR指针进行初始化
只有当对象的构造完全结束后VPTR的指向才最终确定
父类对象的VPTR指向父类虚函数表
子类对象的VPTR指向子类虚函数表

关于多态的面试题
面试题1:请谈谈你对多态的理解
多态的实现效果
多态:同样的调用语句有多种不同的表现形态;
多态实现的三个条件
有继承、有virtual重写、有父类指针(引用)指向子类对象。
多态的C++实现
virtual关键字,告诉编译器这个函数要支持多态;不是根据指针类型判断如何调用;而是要根据指针所指向的实际对象类型来判断如何调用
多态的理论基础
动态联编PK静态联编。根据实际的对象类型来判断重写函数的调用。
多态的重要意义
设计模式的基础 是框架的基石。
实现多态的理论基础
函数指针做函数参数
C函数指针是C++至高无上的荣耀。C函数指针一般有两种用法(正、反)。
多态原理探究
与面试官展开讨论
面试题2:谈谈c++编译器是如何实现多态的
c++编译器多态实现原理
面试题3:谈谈你对重写,重载的理解
函数重载
必须在同一个类中进行
子类无法重载父类的函数,父类同名函数将被名称覆盖
重载是在编译期间根据参数类型和个数决定函数调用
函数重写
必须发生于父类与子类之间
并且父类与子类中的函数必须有完全相同的原型
使用virtual声明之后能够产生多态(如果不使用virtual,那叫重定义)
多态是在运行期间根据具体对象的类型决定函数调用
#include <cstdlib>
#include <iostream>
using namespace std;
class Parent01
{
public:
Parent01()
{
cout<<"Parent01:printf()..do"<<endl;
}
public:
virtual void func()
{
cout<<"Parent01:void func()"<<endl;
}
virtual void func(int i)
{
cout<<"Parent:void func(int i)"<<endl;
}
virtual void func(int i, int j)
{
cout<<"Parent:void func(int i, int j)"<<endl;
}
};
class Child01 : public Parent01
{
public:
//此处2个参数,和子类func函数是什么关系
void func(int i, int j)
{
cout<<"Child:void func(int i, int j)"<<" "<<i + j<<endl;
}
//此处3个参数的,和子类func函数是什么关系
void func(int i, int j, int k)
{
cout<<"Child:void func(int i, int j, int k)"<<" "<<i + j + k<<endl;
}
};
void run01(Parent01* p)
{
p->func(1, 2);
}
int main()
{
Parent01 p;
p.func();
p.func(1);
p.func(1, 2);
Child01 c;
//c.func(); //这个函数调用能运行吗? 为什么
c.Parent01::func();
c.func(1, 2);
run01(&p);
run01(&c);
return 0;
}
// 不能,因为在这里发生的函数名称覆盖 在子类中又找不到无参的func,
// 想要调用必须指明调用的类名 c.Parent01::func();
View Code
//问题1:child对象继承父类对象的func,请问这句话能运行吗?why
//c.func(); //因为名称覆盖,C++编译器不会去父类中寻找0个参数的func函数,只会在子类中找func函数。
//1子类里面的func无法重载父类里面的func
//2当父类和子类有相同的函数名、变量名出现,发生名称覆盖(子类的函数名,覆盖了父类的函数名。)
//3//c.Parent::func();
//问题2 子类的两个func和父类里的三个func函数是什么关系?
面试题4:是否可类的每个成员函数都声明为虚函数,为什么。
c++编译器多态实现原理
面试题5:构造函数中调用虚函数能实现多态吗?为什么?
c++编译器多态实现原理
面试题6:虚函数表指针(VPTR)被编译器初始化的过程,你是如何理解的?
c++编译器多态实现原理
面试题7:父类的构造函数中调用虚函数,能发生多态吗?
c++编译器多态实现原理
面试题8:为什么要定义虚析构函数
在什么情况下应当声明虚函数
构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数
析构函数可以是虚的。虚析构函数用于指引 delete 运算符正确析构动态对象


父类指针和子类指针的步长
这个会报错,
#include <iostream>
using namespace std;
class Parent
{
public:
Parent(int a = 0)
{
this->a = a;
// this->c = c;
}
virtual void print()
{
cout <<"我是父类"<< endl;
}
protected:
private:
int a;
};
class Child : public Parent
{
public:
Child(double b = 0, int a = 0):Parent(a)
{
this->b = b;
}
virtual void print()
{
cout <<"我是子类"<< endl;
}
protected:
private:
double b; // 如果将b换成int不会报错.....,感觉int也会报错,
// 因为两个类大小不能,所以不能在用子类对象初始化的父类指针++,按理说会报错
};
// 结论
// 多态的发生是,父类指针指向子类对象 和父类指针++,是两个不同概念
// 这里如果在子类加上b属性,两个类的大小不同,所以指针++就会报错
int main()
{
Parent p1(1); // 正常来说,构造函数会调用虚函数
Child c1(1,2); // 定义一个子类对象,在这个过程中,在父类构造函数中调用虚函数print,能发生多态吗, 不能
Parent *pP = NULL;
Child *cC = NULL;
Parent p;
Child c;
cout << sizeof(p) <<" " <<sizeof(c) << endl;
Child arr[] = { Child(1), Child(2), Child(3) };
pP = arr;
cC = arr;
pP->print();
cC->print(); // 发生多态
pP++;
cC++;
pP->print();
cC->print(); // 发生多态
pP++;
cC++;
pP->print();
cC->print(); // 发生多态
return 0;
}
探究步长
多态内存图:

1)铁律1:指针也只一种数据类型,C++类对象的指针p++/--,仍然可用。
2)指针运算是按照指针所指的类型进行的。
p++《=》p=p+1 //p = (unsigned int)basep + sizeof(*p) 步长。
结论:父类p++与子类p++步长不同;不要混搭,不要用父类指针++方式操作
