目录
一、多态和虚函数
1、通过案例学多态
(1)案例:父类Animal,2个子类Dog和Cat,实现speak方法
(2)用父类指针指向各对象,调用各方法看效果,记下来
#include <iostream>
using namespace std;
//基类
class Animal
{
public:
void speak(void);
};
void Animal::speak(void)
{
cout << "Speaking!" << endl;
}
//子类
class Dog:public Animal
{
public:
void speak(void);
};
void Dog::speak(void)
{
cout << "wang wang wang!" << endl;
}
class Cat:public Animal
{
public:
void speak(void);
};
void Cat::speak(void)
{
cout << "miao miao miao!" << endl;
}
int main(int argc, char *argv[])
{
Animal a;
Dog d;
Cat c;
a.speak();
d.speak();
c.speak();
Animal *p;
p = &a;
p->speak();
p = &d;
p->speak();
p = &c;
p->speak();
return 0;
}
(3)将父类speak方法声明为virtual,再用父类指针调用各方法看效果,记下来
//基类
class Animal
{
public:
virtual void speak(void);
};
(4)对比差异,理解什么叫多态
2、什么是多态
(1)polymorphism,多态,面向对象的三大特征之一。
(2)从宏观讲,多态就是要实现一套逻辑多种具体适配的执行结果。猫就应该是猫的叫声,狗就应该是狗的叫声.
(3)从微观讲,多态就是要一套代码在运行时根据实际对象的不同来动态绑定/跳转执行相匹配的具体函数.
(4)函数声明前加virtual的即是虚函数
(5)虚函数是C++实现多态特性的基础,从语法上讲多态特性的基类方法必须是虚函数
3、多态中的override
(1)基类中方法声明为virtual,派生类中重新实现同名方法以实现多态,这就叫override(中文为覆盖,或重写)
(2)注意区分override和redefining,微观上最大区别就是是否有virtual,宏观上最大区别就是是否表现为多态
4、多态一定要通过面向对象和override来实现吗?
(1)宏观上的多态是一种编程效果,微观上的多态是一种C++支持的编程技术,微观是为了去实现宏观
(2)不用C++的virtual和override,也可以实现宏观上的多态,C中我们就经常这么干。
(3)C中实现多态的案例:
#include <stdio.h>
enum Animal
{
Cat,
Dog
};
void Dog_speak(void)
{
printf("wang wang wang!\n");
}
void Cat_speak(void)
{
printf("miao miao miao!\n");
}
void speak(int type)
{
if (type == Dog)
{
Dog_speak();
}
if (type == Cat)
{
Cat_speak();
}
}
int main(int argc, char *argv[])
{
int tmp;
scanf("%d", &tmp);
speak(tmp);
return 0;
}
(4)C++源生支持多态,实现起来更容易,后续修改和维护更容易,架构复杂后优势更大。
5、对比重载、重定义、重写三个概念
(1)overload,重载 同一个类里面的多个方法,函数名相同但参数列表不同
(2)redifining,重定义,隐藏 继承中子类再次实现父类中同名方法然后把父类方法隐藏掉
(3)override,覆盖,重写 继承中子类去实现父类中同名virtual方法然后实现多态特性
二、纯虚函数与抽象类
1、纯虚函数
(1)纯虚函数就是基类中只有原型没有实体的一种虚函数
(2)纯虚函数形式:virtual 函数原型=0;
纯虚函数在实现多态时与虚函数一样,可当作虚函数使用。
(3)代码实践:在基类Animal中使用纯虚函数
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void speak(void)=0;
};
class Cat:public Animal
{
public:
void speak(void);
};
void Cat::speak(void)
{
cout << "miao miao miao!" << endl;
}
int main(int argc, char *argv[])
{
Animal *p;
Cat c;
p = &c;
p->speak();
return 0;
}
(4)纯虚函数为什么没有实体?
因为语义上不需要
(5)纯虚函数是否占用内存?
不会,因为纯虚函数所在的类根本无法实例化对象!
2、抽象类(abstract type)
(1)带有纯虚函数的类成为抽象类。抽象类只能作为基类来派生新类,不可实例化对象(即抽象类不可用来定义对象)。
(2)派生类 必须实现 基类的纯虚函数 后 才能用于实例化对象。
(3)抽象类的作用:将有关的数据和行为组织在一个继承层次结构中,保证派生类必须具有所要求的方法。对应暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。这种机制可以让语法和语义保持一致。
(4)抽象类的子类必须实现基类中的纯虚函数,这样子类才能创建对象,否则子类就还是个抽象类
3、接口(interface)
(1)接口是一种特殊的类,用来定义一套访问接口,也就是定义一套规约
(2)接口类中不应该定义任何成员变量
(3)接口类中所有成员函数都是公有且都是纯虚函数
(4)有些高级语言中直接提供关键字interface定义接口,接口其实就是个纯粹的抽象基类
三、虚析构函数
1、什么是虚析构函数
(1)析构函数前加virtual,则析构函数变为虚析构函数
(2)规则:基类有1个或多个虚函数时(注意不要求是纯虚函数),则其析构函数应该声明为virtual
2、为什么需要虚析构函数
(1)代码演示:父子类各自添加析构函数,用2种分配和回收对象的方式分别实验,观察析构函数被调用的规律
//测试代码1
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void speak(void)=0;
virtual ~Animal();
//~Animal();//这个和上面的执行结果相同
};
Animal::~Animal()
{
cout << "~Animal()" << endl;
}
class Cat:public Animal
{
public:
void speak(void);
~Cat();
};
Cat::~Cat()
{
cout << "~cat()" << endl;
}
void Cat::speak(void)
{
cout << "miao miao miao!" << endl;
}
int main(int argc, char *argv[])
{
Animal *p;
Cat c;
p = &c;
p->speak();
return 0;
}
上面这种情况实际析构时只执行了子类Cat的析构函数,并没有执行父类Animal的析构函数,是子类的析构函数调用了父类的析构函数.
//测试代码2
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void speak(void)=0;
virtual ~Animal();
//~Animal();//这个和上面的执行结果不相同
};
Animal::~Animal()
{
cout << "~Animal()" << endl;
}
class Cat:public Animal
{
public:
void speak(void);
~Cat();
};
Cat::~Cat()
{
cout << "~cat()" << endl;
}
void Cat::speak(void)
{
cout << "miao miao miao!" << endl;
}
int main(int argc, char *argv[])
{
Animal *p = new Cat();//对象是Cat类对象,分配在堆上
p->speak();
delete p;
return 0;
}
上面这种情况实际析构时只执行了父类Animal的析构函数,并没有执行子类Cat的析构函数。
(2)结论:虚析构函数在各种情况下总能调用正确的(和对象真正匹配的)析构函数。
3、分析和总结
(1)其实虚函数的virtual的价值,就是让成员函数 在运行时 动态解析 和 绑定 具体执行的函数,这是RTTI机制(动态运行时类型识别机制)的一部分。
(2)析构函数也是成员函数,加virtual的效果和普通成员函数加virtual没什么本质差异
(3)加virtual是有开销的,运行时动态绑定不如编译时静态绑定效率高资源消耗优,但是可以多态。
四、using重新定义继承时访问权限
1、using关键字 在非public继承时的 权限重开作用
(1)父类的public方法在private/protected继承时,到了子类就成了private/protected而不是public了,无法用子类对象来调用了
(2)解决方法1:是改为public继承,有用但是有时候不得不protected或者private继承时就没办法了
(3)解决方法2:是在子类中再实现一个public的方法,内部调用父类继承而来的那个方法,能用但是有点麻烦而且有额外开销
(4)解决方法3:是在子类中使用using关键字将该方法声明为public访问权限,本质上类似于权限打洞
(5)用法:在子类public声明中使用 using Base::func; 即可,不带返回值类型不带参数列表。
(6)注意:using只用于private/protected继承中的权限损失找回,如果方法在父类中本来就是private的子类中没法using后访问。
#include <iostream>
using namespace std;
class father
{
public:
void work(void);
void live(void);
};
void father::work(void)
{
cout << "I am working." << endl;
}
void father::live(void)
{
cout << "I am living." << endl;
}
class son:private father
{
public:
void way(void);
using father::live;
} ;
void son::way(void)
{
this->work();
}
int main(int argc, char *argv[])
{
son s;
s.way();
s.live();
return 0;
}
权限 本身是 编译器在编译时的一种设定。
3、C++继承和多态特性总结
(1)本部分主要讲了C++的2个特性:继承和多态,其中继承和权限管控、继承中的构造和析构、隐藏和覆盖、虚函数和多态、纯虚函数和抽象类、接口、静态和动态绑定等是本课程的重点。
(2)面向对象三大特征中:封装是基础、继承是关键、多态是延伸。
(3)本部分内容很重要,属于C++知识体系中关键基础零件,对于以后理解复杂代码和架构意义重大,必须彻底理解和熟悉。
(4)要求要见过、认识、会写、理解原理、熟练运用。
注:本文章参考了《朱老师物联网大讲堂笔记》,并结合了自己的实际开发经历以及网上他人的技术文章,综合整理得到。如有侵权,联系删除!水平有限,欢迎各位在评论区交流。