课程《C++语言程序设计进阶》清华大学 郑莉老师)

基本概念

多态性

具体的讲,在面向对象程序设计中,指同样的方法被不同对象执行时会有不同的执行效果。

多态的实现

  1. 绑定机制

    绑定是将一个标识符名和一个存储地址联系在一起的过程

  2. 静态多态性:编译时的多态通过静态绑定实现,例如 函数的重载

    绑定工作在编译连接阶段完成

  3. 动态多态性:运行时的多态通过动态绑定实现,例如 虚函数

    绑定工作在程序运行阶段完成

运算符重载

函数重载就体现了静态多态性。运算符也是同样的符号经过重载可以作用于不同的数据类型,可以为类重载不同的操作符,使同一个运算符作用于不同类型的数据时导致不同的行为。例如为复数类重载+,-,*,/等基本操作符,实现复数基本运算法则。

  • C++ 几乎可以重载全部的运算符,而且只能够重载C++中已经有的。不能重载的运算符:“.”、“.*”、“::”、“?:”

  • 重载之后运算符的优先级和结合性都不会改变

  • 重载为类的非静态成员函数

  • 重载为非成员函数

重载为成员函数

函数类型 operator 运算符(形参)
{
函数定义
}

双目运算符重载规则:

运算所需变量为两个的运算符叫做双目运算符,例如:+,-,*,等等

//B为双目运算符,obj1,obj2为类对象
obj1 B obj2
/*等效于*/
obj1.operator B(obj2)

例如:

#include <iostream>
using namespace std;
class Complex {
private:
double real, image;
public:
Complex(double r = 0., double i = 0.) :real(r), image(i) {}
Complex operator +(const Complex& c2) const; //重载+
Complex operator -(const Complex& c2) const; //重载-
friend ostream& operator <<(ostream& os, const Complex& c2);
};
Complex Complex::operator +(const Complex& c2) const {
return Complex(this->real + c2.real, this->image + c2.image);
}
Complex Complex::operator -(const Complex& c2) const {
return Complex(this->real - c2.real, this->image - c2.image);
}
ostream& operator <<(ostream& os, const Complex& c2) {
os << "(" << c2.real << "," << c2.image << ")" << endl;
return os;
} int main() {
Complex c1(1, 1), c2(2, 3);
Complex c3 = c1 + c2;
cout << c1 << c2 << c3;
return 0;
}

单目运算符重载

运算所需变量为一个的运算符叫做单目运算符,例如:++,--,等等

前置单目运算符

//U为单目运算符,obj1为类对象
U obj1
/*等效于*/
obj1.operator U()

后单目运算符

与前置相比,为了加以区分,后置单目运算符会具有一个int类型形参

//U为单目运算符,obj1为类对象
U obj1
/*等效于*/
obj1.operator U(0)

例如:

#include <iostream>
using namespace std;
class Count {
private:
int num;
public:
Count(int _num) :num(_num) {}
Count& operator ++();//前置++
Count operator ++(int);//后置++
friend ostream& operator << (ostream& os, const Count c);
};
Count& Count::operator ++ () {
num++;
return *this;
}
Count Count::operator ++ (int) {
Count old = *this;
++(*this); //调用前置“++”运算符
return old; //返回++之前的值
}
ostream& operator<<(ostream& os, const Count c){
os << "Current Num = " << c.num << endl;
return os;
}
int main() {
Count myCount(10);
cout << "myCount : ";
cout << myCount;
cout << "myCount++: ";
cout << myCount++;
cout << "++myCount: ";
cout << ++myCount;
return 0;
}
/*
myCount : Current Num = 10
myCount++: Current Num = 10
++myCount: Current Num = 12
*/

运算符重载为非成员函数

双目运算符的左操作数不是对象,比如,对于以上复数类,需要实现double+Complex类型运算的功能,则需要在类外实现运算符重载函数。

例如:在complex类中,将+与<<重载为类外操作符。

#include <iostream>
using namespace std;
class Complex {
private:
double real, image;
public:
Complex(double r = 0., double i = 0.) :real(r), image(i) {}
friend Complex operator + (double c1, const Complex& c2); //重载类外+
friend ostream& operator <<(ostream& os, const Complex& c2);
};
Complex operator+(double c1, const Complex& c2) {
return Complex(c1 + c2.real, c2.image);
}
ostream& operator <<(ostream& os, const Complex& c2) {
os << "(" << c2.real << "," << c2.image << ")" << endl;
return os;
}
int main() {
double c1 = 10;
Complex c2(1, 1);
Complex c3 = c1 + c2;
cout << "double " << c1 << endl;
cout << "complex " << c2 << "double +complex " << c3;
return 0;
}
/*
double 10
complex (1,1)
double +complex (11,1)
*/

虚函数

通过virtual关键词,让程序能在运行时根据指针指向的实际对象,找到该对象的函数。使用虚函数实现动态绑定,也就是C++的动态多态性。

虚函数特点

  • 用virtual关键字说明的函数

  • 虚函数是实现运行时多态性基础

  • C++中的虚函数是动态绑定的函数

  • 虚函数必须是非静态的成员函数,虚函数经过派生之后,就可以实现运行过程中的多态。

  • 一般成员函数可以是虚函数

  • 构造函数不能是虚函数

  • 析构函数可以是虚函数

例如:例子中的fun()函数,传递不同类型的对象,调用特定对象的display()函数

#include <iostream>
using namespace std; class Base1 {
public:
virtual void display() const { cout << "Base1::display()" << endl; }; //虚函数
};
class Base2 :public Base1 {
public:
virtual void display() const { cout << "Base2::display()" << endl; }
};
class Derived : public Base2 {
public:
virtual void display() const { cout << "Derived::display()" << endl; }
};
void fun(Base1* ptr) {
ptr->display();
}
int main() {
Base1 base1;
Base2 base2;
Derived derived;
fun(&base1);
fun(&base2);
fun(&derived);
derived.Base1::display();
derived.Base2::display();
derived.display();
return 0;
}
/*
Base1::display()
Base2::display()
Derived::display()
Base1::display()
Base2::display()
Derived::display()
*/

虚函数

虚函数的声明

virtual 函数类型 函数名(形参表);
  • 虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。
  • 在派生类中可以对基类中的成员函数进行覆盖
  • 虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的。

virtual 关键字

  • 派生类可以不显式地用virtual声明虚函数,这时系统就会用以下规则来判断派生类的一个函数成员是不是虚函数:
  • 该函数是否与基类的虚函数有相同的名称、参数个数及对应参数类型
  • 该函数是否与基类的虚函数有相同的返回值或者满足类型兼容规则的指针、引用型的返回值;
  • 如果从名称、参数及返回值三个方面检查之后,派生类的函数满足上述条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了基类的虚函数
  • 派生类中的虚函数还会隐藏基类中同名函数的所有其它重载形式。
  • 一般习惯于在派生类的函数中也使用virtual关键字,以增加程序的可读性。

虚析构函数

为什么需要虚析构函数?

  • 可能通过基类指针删除派生类对象
  • 如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),就需要让基类的析构函数成为虚函数,否则执行delete的结果是不确定的。

例子:虚析构函数

#include <iostream>
using namespace std;
class Base {
public:
virtual ~Base() { cout << "Base destructor" << endl; }
}; class Derived : public Base {
public:
Derived() { p = new int(0); }
virtual ~Derived() { cout << "Derived destructor" << endl; delete p; }
private:
int* p;
}; void fun(Base* b) { delete b;} int main() {
Base* base = new Base();
Base* derived = new Derived();
fun(base);
fun(derived);
return 0;
}
/*
Base destructor
Derived destructor
Base destructo
*/

虚函数的实现

虚表与动态绑定

虚表

  1. 每个多态类有一个虚表(virtual table)

    虚表中有当前类的各个虚函数的入口地址

    每个对象有一个指向当前类的虚表的指针(虚指针vptr)

  2. 动态绑定的实现

    构造函数中为对象的虚指针赋值

    通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址

    通过该入口地址调用虚函数纯虚类,定义统一的函数接口

抽象类

纯虚函数

纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本,纯虚函数的声明格式为:

virtual 函数类型 函数名(参数表) = 0;

抽象类

带有纯虚函数的类称为抽象类,纯虚类作为基类,一般用于定义统一的函数对外接口。

class  类名
{
     virtual 类型 函数名(参数表)=0;
     //其他成员……
}

抽象类作用

抽象类为抽象和设计的目的而声明

对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现

注意

抽象类只能作为基类来使用。

不能定义抽象类的对象。(纯虚函数没有函数具体定义,无法创建对象)

例如:

#include <iostream>
using namespace std;
class Base1 {
public:
virtual void display() const = 0; //纯虚函数
};
class Base2 : public Base1 {
public:
virtual void display() const { cout << "Base2::display()" << endl; } //覆盖基类的虚函数
};
class Derived : public Base2 {
public:
virtual void display() const { cout << "Derived::display()" << endl; } //覆盖基类的虚函数
};
void fun(Base1* ptr) { //定义引用的通用接口
ptr->display();
}
int main() {
Base2 base2;
Derived derived;
fun(&base2);
fun(&derived);
return 0;
}
/*
Base2::display()
Derived::display()
*/

override 与 final关键字

override

  • 多态行为的基础:基类声明虚函数,继承类声明一个函数覆盖该虚函数

  • 覆盖要求: 函数签名(signatture)完全一致

  • 函数签名包括:函数名 参数列表 const(通常可能会忽略const,导致虚函数没有被覆盖)

下列程序就仅仅因为疏忽漏写了const,导致多态行为没有如期进行

final

C++11提供的final,用来避免类被继承,或是基类的函数被改写

//类限定final
struct Base1 final { };
struct Derived1 : Base1 { }; // 编译错误:Base1为final,不允许被继承
//类成员函数限定final
struct Base2 {
virtual void f() final;
};
struct Derived2 : Base2 {
void f(); // 编译错误:Base2::f 为final,不允许被覆盖
};

C++学习笔记:08 多态性的更多相关文章

  1. 机器学习实战(Machine Learning in Action)学习笔记————08.使用FPgrowth算法来高效发现频繁项集

    机器学习实战(Machine Learning in Action)学习笔记————08.使用FPgrowth算法来高效发现频繁项集 关键字:FPgrowth.频繁项集.条件FP树.非监督学习作者:米 ...

  2. C++ GUI Qt4学习笔记08

    C++ GUI Qt4学习笔记08   qtc++signal图形引擎文档 本章介绍Qt的二维图形引擎,Qt的二维图形引擎是基于QPainter类的.<span style="colo ...

  3. CSS学习笔记08 浮动

    从CSS学习笔记05 display属性一文中,我们知道div是块元素,会独占一行,即使div的宽度很小,像下面这样 应用display属性的inline属性可以让div与div共享一行,除了这种方法 ...

  4. [Golang学习笔记] 08 链表

    链表(Linked list)是一种常见数据结构,但并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针. 由于不必须按顺序存储,链表在插入的时候可以达到O(1),比顺序表快得多,但是查 ...

  5. [原创]java WEB学习笔记08:HttpServletRequest & ServletRequest

    本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ...

  6. Bash脚本编程学习笔记08:函数

    官方资料:Shell Functions (Bash Reference Manual) 简介 正如我们在<Bash脚本编程学习笔记06:条件结构体>中最后所说的,我们应该把一些可能反复执 ...

  7. AMQ学习笔记 - 08. Spring-JmsTemplate之发送

    概述 JmsTemplate提供了3组*3,共计9个发送用的方法.   发送的方法有3组: 基本的发送 转换并发送 转换.后处理再发送 必需的资源 必需的资源有: javax.jms.Connecti ...

  8. knockoutJS学习笔记08:表单域绑定

    前面的绑定都是用在基本标签上,这章主要讲表单域标签的绑定. 一.value 绑定 绑定标签:input text.textarea. <p>用户名:<input type=" ...

  9. Python学习笔记08

      正则表达式包re match,search,sub re.match(pattern, string, flags=0) re.search(pattern, string, flags=0) r ...

  10. 【EF学习笔记08】----------加载关联表的数据 显式加载

    显式加载 讲解之前,先来看一下我们的数据库结构:班级表 学生表 加载从表集合类型 //显示加载 Console.WriteLine("=========查询集合===========&quo ...

随机推荐

  1. 给MediaWiki增加看板娘

    我们想给我们的mediawiki增加个像我博客里这样的看板娘,那么怎么做才好呢? 其实很简单,只要在相应的模板文件里增加指定代码就好了! 修改模板文件 找到模板文件skins/Vector/Vecto ...

  2. 解决移动端click事件300ms延迟的问题

    方法1.部分浏览器的<meta>标签加上width=device-width就能解决. 方法2.引入fastclick.js库 <!DOCTYPE html> <html ...

  3. 细说Typescript类型检查机制

    上一篇文章我介绍了Typescript的基础知识,回顾一下,有基础数据类型.数组.函数.类.接口.泛型等,本节内容将述说一下Typescript为方便我们开发提供了一些类型检查机制. 类型检查机制 类 ...

  4. VSCode 在.vscode/launch.json中设置启动时的参数

    如下脚本设置启动参数,如题,在.vscode/launch.json文件中,红色部分设置运行参数 { // Use IntelliSense to learn about possible attri ...

  5. MySQL修改配置文件 避免中文乱码

    MySQL修改配置文件 避免中文乱码 MySQL安装后默认的服务器字符集是拉丁文,也就是说默认 character_set_server = latin1 ,这是造成 MySQL 中文乱码的主要原因之 ...

  6. RHEL7.2下DHCP服务器的搭建

    DHCP(?Dynamic Host Configuration Protocol)是一种帮助计算机从指定的DHCP服务器获取配置信息的自举协议 请求配置信息的计算机叫做DHCP客户端,而提供信息的叫 ...

  7. 解决方案-问题001:物理机、虚机等等Linux操作系统/usr/bin目录权限误操作,导致无法切换root

    导语:平常运维人员会误操作一些目录权限,导致一些问题,那么如何恢复呢? 问题:物理机.虚机等等Linux操作系统/usr/bin目录权限误操作,导致无法切换root? 实验环境: ip地址 是否目录正 ...

  8. Java HashMap工作原理:不仅仅是HashMap

    前言: 几乎所有java程序员都用过hashMap,但会用不一定会说. 近年来hashMap是非常常见的面试题,如何为自己的回答加分?需要从理解开始. "你用过hashMap吗?" ...

  9. Python - 面向对象编程 - 小实战(2)

    需求 小明和小美都爱跑步 小明体重 75 公斤 小美体重 45 公斤 每次跑步会减肥 0.5 公斤 每次吃东西体重增加 1 公斤 需求分析 小明.小美都是一个具体的对象,他们都是人,所以应该抽象成人类 ...

  10. Docker 面试宝典

    Docker 是什么? 是实现容器技术的一种工具 是一个开源的应用容器引擎 使用 C/S 架构模式,通过远程API 来管理 可以打包一个应用及依赖包到一个轻量级.可移植的容器中 容器是什么? 对应用软 ...