第七章 类的继承
7.1 继承的基本概念和语法
继承与派生概述:
- 继承与派生是同一过程从不同的角度看
- 保持已有类的特性而构造新类的过程称为继承
- 在已有类的基础上新增自己的特性而产生新类的过程称为派生。
- 被继承的已有类称为基类(或父类)
- 派生出的新类称为派生类(或子类)
- 直接参与派生出某类的基类称为直接基类
- 基类的基类甚至更高层的基类称为间接基类
继承与派生的目的
- 继承的目的:实现设计与代码的重用
- 派生的目的:当新的问题出现,原有程序无法解决(或不能完全解决)时,需要原有程序进行改造。
单继承时派生类的定义
// 标准语
class 派生类名:继承方式 基类名
{
成员声明;
}
// 例子
class Derived: public Base
{
public:
Derived ();
~Derived ();
};
多继承时派生类的定义:
//标准语法:
class 派生类名:继承方式1 基类名1,继承方式2 基类名2,...
{
成员声明;
}
//注意:每一个“继承方式”,只用于限制对紧随其后之基类的继承。
//例子:
class Derived: public Base1, private Base2
{
public:
Derived ();
~Derived ();
}
派生类的构成:
- 吸收基类成员
改造基类成员:
添加新的成员:
7.2 继承方式简介及公有继承
不同继承方式的影响主要体现在:
- 派生类成员对基类成员的访问权限
- 通过派生类对象对基类成员的访问权限
三种继承方式:
- 公有继承
- 私有继承
- 保护继承
公有继承(public)
例7-1 公有继承举例
//Point.h
#ifndef _POINT_H
#define _POINT_H
class Point { //基类Point类的定义
public: //公有函数成员
void initPoint(float x = 0, float y = 0)
{ this->x = x; this->y = y;}
void move(float offX, float offY)
{ x += offX; y += offY; }
float getX() const { return x; }
float getY() const { return y; }
private: //私有数据成员
float x, y;
};
#endif //_POINT_H
//Rectangle.h
#ifndef _RECTANGLE_H
#define _RECTANGLE_H
#include "Point.h"
class Rectangle: public Point { //派生类定义部分
public: //新增公有函数成员
void initRectangle(float x, float y, float w, float h) {
initPoint(x, y); //调用基类公有成员函数
this->w = w;
this->h = h;
}
float getH() const { return h; }
float getW() const { return w; }
private: //新增私有数据成员
float w, h;
};
#endif //_RECTANGLE_H
#include <iostream>
#include <cmath>
using namespace std;
#include “Rectangle.h”
int main() {
Rectangle rect; //定义Rectangle类的对象
//设置矩形的数据
rect.initRectangle(2, 3, 20, 10);
rect.move(3,2); //移动矩形位置
cout << "The data of rect(x,y,w,h): " << endl;
//输出矩形的特征参数
cout << rect.getX() <<", "
<< rect.getY() << ", "
<< rect.getW() << ", "
<< rect.getH() << endl;
return 0;
}
7.3 私有继承和保护继承
私有继承(private)
- 继承的访问控制
- 基类的public和protected成员:都以private身份出现在派生类中;
- 基类的private成员:不可直接访问。
访问权限
- 派生类中的成员函数:可以直接访问基类中的public和protected成员,不能直接访问基类的private成员;
- 通过派生类的对象:不能直接访问从基类继承的任何成员。
例7-2 私有继承举例
//Point.h
#ifndef _POINT_H
#define _POINT_H
class Point { //基类Point类的定义
public: //公有函数成员
void initPoint(float x = 0, float y = 0)
{ this->x = x; this->y = y;}
void move(float offX, float offY)
{ x += offX; y += offY; }
float getX() const { return x; }
float getY() const { return y; }
private: //私有数据成员
float x, y;
};
#endif //_POINT_H
//Rectangle.h
#ifndef _RECTANGLE_H
#define _RECTANGLE_H
#include "Point.h"
class Rectangle: private Point { //派生类定义部分
public: //新增公有函数成员
void initRectangle(float x, float y, float w, float h) {
initPoint(x, y); //调用基类公有成员函数
this->w = w;
this->h = h;
}
void move(float offX, float offY) { Point::move(offX, offY); }
float getX() const { return Point::getX(); }
float getY() const { return Point::getY(); }
float getH() const { return h; }
float getW() const { return w; }
private: //新增私有数据成员
float w, h;
};
#endif //_RECTANGLE_H
#include <iostream>
#include <cmath>
using namespace std;
int main() {
Rectangle rect; //定义Rectangle类的对象
rect.initRectangle(2, 3, 20, 10); //设置矩形的数据
rect.move(3,2); //移动矩形位置
cout << "The data of rect(x,y,w,h): " << endl;
cout << rect.getX() <<", " //输出矩形的特征参数
<< rect.getY() << ", "
<< rect.getW() << ", "
<< rect.getH() << endl;
return 0;
}
保护继承(protected):
- 继承的访问控制
- 基类的public和protected成员:都以protected身份出现在派生类中;
- 基类的private成员:不可直接访问。
- 访问权限
- 派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员;
- 通过派生类的对象:不能直接访问从基类继承的任何成员。
protected 成员的特点与作用:
- 对建立其所在类对象的模块来说,它与 private 成员的性质相同。
- 对于其派生类来说,它与 public 成员的性质相同。
- 既实现了数据隐藏,又方便继承,实现代码重用。
//protected 成员举例(补7-1)
class A
protected:
int x;
};
int main() {
A a;
a.x = 5; //错误
}
class A {
protected:
int x;
};
class B: public A{
public:
void function();
};
void B:function() {
x = 5; //正确
}
- 如果派生类有多个基类,也就是多继承时,可以用不同的方式继承每个基类。
//多继承举例(补7-2)
class A {
public:
void setA(int);
void showA() const;
private:
int a;
};
class B {
public:
void setB(int);
void showB() const;
private:
int b;
};
class C : public A, private B {
public:
void setC(int, int, int);
void showC() const;
private const:
int c;
};
void A::setA(int x) {
a=x;
}
void B::setB(int x) {
b=x;
}
void C::setC(int x, int y, int z) {
//派生类成员直接访问基类的
//公有成员
setA(x);
setB(y);
c = z;
}
//其他函数实现略
int main() {
C obj;
obj.setA(5);
obj.showA();
obj.setC(6,7,9);
obj.showC();
// obj.setB(6); 错误
// obj.showB(); 错误
return 0;
}
7.4 类型转换
- 公有派生类对象可以被当作基类的对象使用,反之则不可。
- 派生类的对象可以隐含转换为基类对象;
- 派生类的对象可以初始化基类的引用;
- 派生类的指针可以隐含转换为基类的指针。
- 通过基类对象名、指针只能使用从基类继承的成员。
//例7-3 类型转换规则举例
#include <iostream>
using namespace std;
class Base1 { //基类Base1定义
public:
void display() const {
cout << "Base1::display()" << endl;
}
};
class Base2: public Base1 { //公有派生类Base2定义
public:
void display() const {
cout << "Base2::display()" << endl;
}
};
class Derived: public Base2 { //公有派生类Derived定义
public:
void display() const {
cout << "Derived::display()" << endl;
}
};
void fun(Base1 *ptr) { //参数为指向基类对象的指针
ptr->display(); //"对象指针->成员名"
}
int main() { //主函数
Base1 base1; //声明Base1类对象
Base2 base2; //声明Base2类对象
Derived derived; //声明Derived类对象
fun(&base1); //用Base1对象的指针调用fun函数
fun(&base2); //用Base2对象的指针调用fun函数
fun(&derived); //用Derived对象的指针调用fun函数
return 0;
}
7.5 派生类的构造函数
默认情况
-
基类的构造函数不被继承
-
派生类需要定义自己的构造函数。
C++11 规定 -
可用using语句继承基类构造函数。
-
但是只能初始化从基类继承的成员。(派生类新增成员可以通过类内初始值进行初始化。)
-
语法形式:
using B::B;
建议
- 如果派生类有自己新增的成员,且需要通过构造函数初始化,则派生类要自定义构
造函数。
若不继承基类的构造函数 - 派生类新增成员:派生类定义构造函数初始化;
- 继承来的成员:自动调用基类构造函数进行初始化;
- 派生类的构造函数需要给基类的构造函数传递参数。
单继承 - 派生类只有一个直接基类的情况,是单继承。单继承时,派生类的构造函数只需要给一个直接基类构造函数传递参数。
单继承时构造函数的定义语法
派生类名::派生类名(基类所需的形参,本类成员所需的形参):
基类名(参数表), 本类成员初始化列表
{
//其他初始化;
};
//单继承时的构造函数举例(补7-3)
#include<iostream>
using namespace std;
class B {
public:
B();
B(int i);
~B();
void print() const;
private:
int b;
};
B::B() {
b=0;
cout << "B's default constructor called." << endl;
}
B::B(int i) {
b=i;
cout << "B's constructor called." << endl;
}
B::~B() {
cout << "B's destructor called." << endl;
}
void B::print() const {
cout << b << endl;
}
class C: public B {
public:
C();
C(int i, int j);
~C();
void print() const;
private:
int c;
};
C::C() {
c = 0;
cout << "C's default constructor called." << endl;
}
C::C(int i,int j): B(i), c( j){
cout << "C's constructor called." << endl;
}
C::~C() {
cout << "C's destructor called." << endl;
}
void C::print() const {
B::print();
cout << c << endl;
}
int main() {
C obj(5, 6);
obj.print();
return 0;
}
多继承
- 多继承时,有多个直接基类,如果不继承基类的构造函数,派生类构造函数需要给所有基类构造函数传递参数。我们来看一下语法规定。
多继承时构造函数的定义语法
派生类名::派生类名(参数表) :
基类名1(基类1初始化参数表),
基类名2(基类2初始化参数表),
...
基类名n(基类n初始化参数表),
本类成员初始化列表
{
//其他初始化;
};
派生类与基类的构造函数
多继承且有对象成员时派生的构造函数定义语法
派生类名::派生类名(形参表):
基类名1(参数), 基类名2(参数), ..., 基类名n(参数),
本类成员(含对象成员)初始化列表
{
//其他初始化
};
构造函数的执行顺序
7.6 派生类构造函数举例
// 例7-4 派生类构造函数举例
#include <iostream>
using namespace std;
class Base1 {//基类Base1,构造函数有参数
public:
Base1(int i)
{ cout << "Constructing Base1 " << i << endl; }
};
class Base2 {//基类Base2,构造函数有参数
public:
Base2(int j)
{ cout << "Constructing Base2 " << j << endl; }
};
class Base3 {//基类Base3,构造函数无参数
public:
Base3()
{ cout << "Constructing Base3 *" << endl; }
};
class Derived: public Base2, public Base1, public Base3 {
public:
Derived(int a, int b, int c, int d): Base1(a), member2(d), member1(c),
Base2(b)
//此处的次序与构造函数的执行次序无关
{ }
private:
Base1 member1;
Base2 member2;
Base3 member3;
};
int main() {
Derived obj(1, 2, 3, 4);
return 0;
}
7.7 派生类复制构造函数
派生类未定义复制构造函数的情况
-
编译器会在需要时生成一个隐含的复制构造函数;
-
先调用基类的复制构造函数;
-
再为派生类新增的成员执行复制。
派生类定义了复制构造函数的情况 -
一般都要为基类的复制构造函数传递参数。
-
复制构造函数只能接受一个参数,既用来初始化派生类定义的成员,也将被传递给基类的复制构造函数。
-
基类的复制构造函数形参类型是基类对象的引用,实参可以是派生类对象的引用
-
例如:
C::C(const C &c1): B(c1) {…}
7.8 派生类的析构函数
- 析构函数不被继承,派生类如果需要,要自行声明析构函数。
- 声明方法与无继承关系时类的析构函数相同。
- 不需要显式地调用基类的析构函数,系统会自动隐式调用。
- 先执行派生类析构函数的函数体,再调用基类的析构函数。
//例7-5 派生类对象析构举例
#include <iostream>
using namespace std;
class Base1 {
public:
Base1(int i)
{ cout << "Constructing Base1 " << i << endl; }
~Base1() { cout << "Destructing Base1" << endl; }
};
class Base2 {
public:
Base2(int j)
{ cout << "Constructing Base2 " << j << endl; }
~Base2() { cout << "Destructing Base2" << endl; }
};
class Base3 {
public:
Base3() { cout << "Constructing Base3 *" << endl; }
~Base3() { cout << "Destructing Base3" << endl; }
};
class Derived: public Base2, public Base1, public Base3 {
public:
Derived(int a, int b, int c, int d): Base1(a), member2(d), member1(c),
Base2(b)
{ }
private:
Base1 member1;
Base2 member2;
Base3 member3;
};
int main() {
Derived obj(1, 2, 3, 4);
return 0;
}
7.9 访问从基类继承的成员
作用域限定
- 当派生类与基类中有相同成员时:
- 若未特别限定,则通过派生类对象使用的是派生类中的同名成员。
- 如要通过派生类对象访问基类中被隐藏的同名成员,应使用基类名和作用域操作符(::)来限定。
//例7-6 多继承同名隐藏举例
#include <iostream>
using namespace std;
class Base1 {
public:
int var;
void fun() { cout << "Member of Base1" << endl; }
};
class Base2 {
public:
int var;
void fun() { cout << "Member of Base2" << endl; }
};
class Derived: public Base1, public Base2 {
public:
int var;
void fun() { cout << "Member of Derived" << endl; }
};
int main() {
Derived d;
Derived *p = &d;
//访问Derived类成员
d.var = 1;
d.fun();
//访问Base1基类成员
d.Base1::var = 2;
d.Base1::fun();
//访问Base2基类成员
p->Base2::var = 3;
p->Base2::fun();
return 0;
}
7.10 二义性问题
- 如果从不同基类继承了同名成员,但是在派生类中没有定义同名成员,“派生类对
象名或引用名.成员名”、“派生类指针->成员名”访问成员存在二义性问题 - 解决方式:用类名限定
二义性问题举例
class A {
public:
void f();
};
class B {
public:
void f();
void g()
};
class C: public A, piblic B {
public:
void g();
void h();
};
/*
如果定义:C c1;
则 c1.f() 具有二义性
而 c1.g() 无二义性(同名隐藏)
*/
// 例7-7 多继承时的二义性和冗余问题
//7_7.cpp
#include <iostream>
using namespace std;
class Base0 { //定义基类Base0
public:
int var0;
void fun0() { cout << "Member of Base0" << endl; }
};
class Base1: public Base0 { //定义派生类Base1
public: //新增外部接口
int var1;
};
class Base2: public Base0 { //定义派生类Base2
public: //新增外部接口
int var2;
};
class Derived: public Base1, public Base2 {
public:
int var;
void fun()
{ cout << "Member of Derived" << endl; }
};
int main() { //程序主函数
Derived d;
d.Base1::var0 = 2;
d.Base1::fun0();
d.Base2::var0 = 3;
d.Base2::fun0();
return 0;
}
Derived 类对象d 的存储结构示意图示意图
虚基类
需要解决的问题
当派生类从多个基类派生,而这些基类又共同基类,则在访问此共同基类中
的成员时,将产生冗余,并有可能因冗余带来不一致性
虚基类声明
以virtual说明基类继承方式
例:
class B1:virtual public B
作用
主要用来解决多继承时可能发生的对同一基类继承多次而产生的二义性问题
为最远的派生类提供唯一的基类成员,而不重复产生多次复制
注意:
在第一级继承时就要将共同基类设计为虚基类。
#include <iostream>
using namespace std;
class Base0 {
public:
int var0;
void fun0() { cout << "Member of Base0" << endl; }
};
class Base1: virtual public Base0 {
public:
int var1;
};
class Base2: virtual public Base0 {
public:
int var2;
};
class Derived: public Base1, public Base2 {
//定义派生类Derived
public:
int var;
void fun() {
cout << "Member of Derived" << endl;
}
};
int main() {
Derived d;
d.var0 = 2; //直接访问虚基类的数据成员
d.fun0(); //直接访问虚基类的函数成员
return 0;
}
虚基类及其派生类构造函数
- 建立对象时所指定的类称为最远派生类。
- 虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。
- 在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中为虚基类的构造函数列出参数。如果未列出,则表示调用该虚基类的默认构造函数。
- 在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数,其他类对虚基类构造函数的调用被忽略。
//有虚基类时的构造函数举例(补7-4)
#include <iostream>
using namespace std;
class Base0 {
public:
Base0(int var) : var0(var) { }
int var0;
void fun0() { cout << "Member of Base0" << endl; }
};
class Base1: virtual public Base0 {
public:
Base1(int var) : Base0(var) { }
int var1;
};
class Base2: virtual public Base0 {
public:
Base2(int var) : Base0(var) { }
int var2;
};
//有虚基类时的构造函数举例(补7-4)
class Derived: public Base1, public Base2 {
public:
Derived(int var) : Base0(var), Base1(var), Base2(var)
{ }
int var;
void fun()
{ cout << "Member of Derived" << endl; }
};
int main() { //程序主函数
Derived d(1);
d.var0 = 2; //直接访问虚基类的数据成员
d.fun0(); //直接访问虚基类的函数成员
return 0;
}
第八章 多态性
8.1 多态的实现
8.2 运算符重载
- 与函数重载的原理一样,可以通过定义运算符函数,为类重载运算符
- 这一章我们要介绍运算符重载的语法,并且以复数类、时钟类为例,演示如何重载运算符:复数加减运算、时钟自增1秒(前置、后置++)、用cout和插入运算符输出整个对象。
- 我们可以让自定义的复数类也能像整数和浮点数一样用+号和-号来进行运算、让我们熟悉的Clock类能通过前置、后置++来自增1秒、更神奇的是用cout和插入运算符来输出整个对象!这一切都是通过运算符重载来实现的。
8.3 虚函数
- 这一章就为大家接开这个谜底:如何让程序能在运行时根据指针指向的实际对象,找到该对象的函数。
- 这就要用到虚函数和动态绑定,也就是C++的动态多态性。
- 这并不复杂,其实就是“virtual”关键字。
8.4 运算符重载的规则
- 思考:用“+”、“-”能够实现复数的加减运算吗?
- 实现复数加减运算的方法
——重载“+”、“-”运算符 - 运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时导致不同的行为。
- C++ 几乎可以重载全部的运算符,而且只能够重载C++中已经有的。
不能重载的运算符:“.”、“.*”、“::”、“?:” - 重载之后运算符的优先级和结合性都不会改变。
- 运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。例如:
- 重载为类的非静态成员函数;
- 重载为非成员函数。
8.5 运算符重载为成员函数
重载为类成员的运算符函数定义形式
函数类型 operator 运算符(形参)
{
......
}
//参数个数=原操作数个数-1 (后置++、--除外)
双目运算符重载规则
- 如果要重载 B 为类成员函数,使之能够实现表达式 oprd1 B oprd2,其中 oprd1为A 类对象,则 B 应被重载为 A 类的成员函数,形参类型应该是 oprd2 所属的类型。
- 经重载后,表达式 oprd1 B oprd2 相当于 oprd1.operator B(oprd2)
/*
例8-1 复数类加减法运算重载为成员函数
要求:
将+、-运算重载为复数类的成员函数。
规则:
实部和虚部分别相加减。
操作数:
两个操作数都是复数类的对象。
*/
//源代码:
#include <iostream>
using namespace std;
class Complex {
public:
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { }
//运算符+重载成员函数
Complex operator + (const Complex &c2) const;
//运算符-重载成员函数
Complex operator - (const Complex &c2) const;
void display() const; //输出复数
private:
double real; //复数实部
double imag; //复数虚部
};
Complex Complex::operator+(const Complex &c2) const{
//创建一个临时无名对象作为返回值
return Complex(real+c2.real, imag+c2.imag);
}
Complex Complex::operator-(const Complex &c2) const{
//创建一个临时无名对象作为返回值
return Complex(real-c2.real, imag-c2.imag);
}
void Complex::display() const {
cout<<"("<<real<<", "<<imag<<")"<<endl;
}
int main() {
Complex c1(5, 4), c2(2, 10), c3;
cout << "c1 = "; c1.display();
cout << "c2 = "; c2.display();
c3 = c1 - c2; //使用重载运算符完成复数减法
cout << "c3 = c1 - c2 = "; c3.display();
c3 = c1 + c2; //使用重载运算符完成复数加法
cout << "c3 = c1 + c2 = "; c3.display();
return 0;
}
前置单目运算符重载规则
如果要重载 U 为类成员函数,使之能够实现表达式 U oprd,其中 oprd 为A类对
象,则 U 应被重载为 A 类的成员函数,无形参。
经重载后,
表达式 U oprd 相当于 oprd.operator U()
后置单目运算符 ++和–重载规则
- 如果要重载 ++或–为类成员函数,使之能够实现表达式 oprd++ 或 oprd-- ,其中 oprd 为A类对象,则 ++或-- 应被重载为 A 类的成员函数,且具有一个 int 类型形参。
- 经重载后,表达式 oprd++ 相当于 oprd.operator ++(0)
例8-2 重载前置++和后置++为时钟类成员函数
前置单目运算符,重载函数没有形参
后置++运算符,重载函数需要有一个int形参
操作数是时钟类的对象。
实现时间增加1秒钟。
源代码:
#include
using namespace std;
class Clock {//时钟类定义
public:
Clock(int hour = 0, int minute = 0, int second = 0);
void showTime() const;
//前置单目运算符重载
Clock& operator ++ ();
//后置单目运算符重载
Clock operator ++ (int);
private:
int hour, minute, second;
};
Clock::Clock(int hour, int minute, int second) {
if (0 <= hour && hour < 24 && 0 <= minute && minute < 60
&& 0 <= second && second < 60) {
this->hour = hour;
this->minute = minute;
this->second = second;
} else
cout << “Time error!” << endl;
}
void Clock::showTime() const { //显示时间
cout << hour << “:” << minute << “:” << second << endl;
}
Clock & Clock::operator ++ () {
second++;
if (second >= 60) {
second -= 60; minute++;
if (minute >= 60) {
minute -= 60; hour = (hour + 1) % 24;
}
}
return *this;
}
Clock Clock::operator ++ (int) {
//注意形参表中的整型参数
Clock old = *this;
++(*this); //调用前置“++”运算符
return old;
}
int main() {
Clock myClock(23, 59, 59);
cout << "First time output: ";
myClock.showTime();
cout << "Show myClock++: ";
(myClock++).showTime();
cout << "Show ++myClock: ";
(++myClock).showTime();
return 0;
}
运算符重载为非成员函数
有些运算符不能重载为成员函数,例如二元运算符的左操作数不是对象,或者是不
能由我们重载运算符的对象
运算符重载为非成员函数的规则
函数的形参代表依自左至右次序排列的各操作数。
重载为非成员函数时
参数个数=原操作数个数(后置++、–除外)
至少应该有一个自定义类型的参数。
后置单目运算符 ++和–的重载函数,形参列表中要增加一个int,但不必写形参名。
如果在运算符的重载函数中需要操作某类对象的私有成员,可以将此函数声明为该
类的友元。
运算符重载为非成员函数的规则
双目运算符 B重载后,
表达式oprd1 B oprd2
等同于operator B(oprd1,oprd2 )
前置单目运算符 B重载后,
表达式 B oprd
等同于operator B(oprd )
后置单目运算符 ++和–重载后,
表达式 oprd B
等同于operator B(oprd,0 )
例8-3 重载Complex 的加减法和“<<”运算符为非成员函数
• 将+、‐(双目)重载为非成员函数,并将其声明为复数类的友元,两个操作数都是
复数类的常引用。
• 将<<(双目)重载为非成员函数,并将其声明为复数类的友元,它的左操作数是
std::ostream引用,右操作数为复数类的常引用,返回std::ostream引用,用以
支持下面形式的输出:
cout << a << b;
该输出调用的是:
operator << (operator << (cout, a), b);
源代码:
//8_3.cpp
#include
using namespace std;
class Complex {
public:
Complex(double r = 0.0, double i = 0.0) : real®, imag(i) { }
friend Complex operator+(const Complex &c1, const Complex &c2);
friend Complex operator-(const Complex &c1, const Complex &c2);
friend ostream & operator<<(ostream &out, const Complex &c);
private:
double real; //复数实部
double imag; //复数虚部
};
Complex operator+(const Complex &c1, const Complex &c2){
return Complex(c1.real+c2.real, c1.imag+c2.imag);
}
Complex operator-(const Complex &c1, const Complex &c2){
return Complex(c1.real-c2.real, c1.imag-c2.imag);
}
ostream & operator<<(ostream &out, const Complex &c){
out << “(” << c.real << ", " << c.imag << “)”;
return out;
}
int main() {
Complex c1(5, 4), c2(2, 10), c3;
cout << "c1 = " << c1 << endl;
cout << "c2 = " << c2 << endl;
c3 = c1 - c2; //使用重载运算符完成复数减法
cout << "c3 = c1 - c2 = " << c3 << endl;
c3 = c1 + c2; //使用重载运算符完成复数加法
cout << "c3 = c1 + c2 = " << c3 << endl;
return 0;
}
虚函数
问题:还记得第7章的例子吗?
例7-3 类型转换规则举例
#include
using namespace std;
class Base1 { //基类Base1定义
public:
void display() const {
cout << “Base1::display()” << endl;
}
};
class Base2: public Base1 { //公有派生类Base2定义
public:
void display() const {
cout << “Base2::display()” << endl;
}
};
class Derived: public Base2 { //公有派生类Derived定义
public:
void display() const {
cout << “Derived::display()” << endl;
}
};
void fun(Base1 *ptr) { //参数为指向基类对象的指针
ptr->display(); //“对象指针->成员名”
}
int main() { //主函数
Base1 base1; //声明Base1类对象
Base2 base2; //声明Base2类对象
Derived derived; //声明Derived类对象
fun(&base1); //用Base1对象的指针调用fun函数
fun(&base2); //用Base2对象的指针调用fun函数
fun(&derived); //用Derived对象的指针调用fun函数
return 0;
}
例8-4 通过虚函数实现运行时多态
现在我们来改进一下第7章的程序
#include
using namespace std;
class Base1 {
public:
virtual void display() const; //虚函数
};
void Base1::display() const {
cout << “Base1::display()” << endl;
}
class Base2::public Base1 {
public:
virtual void display() const;
};
void Base2::display() const {
cout << “Base2::display()” << endl;
}
class Derived: public Base2 {
public:
virtual void display() const;
};
void Derived::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);
return 0;
}
初识虚函数
用virtual关键字说明的函数
虚函数是实现运行时多态性基础
C++中的虚函数是动态绑定的函数
虚函数必须是非静态的成员函数,虚函数经过派生之后,就可以实现运行过程中的
多态。
一般成员函数可以是虚函数
构造函数不能是虚函数
析构函数可以是虚函数
一般虚函数成员
虚函数的声明
virtual 函数类型 函数名(形参表);
虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。
在派生类中可以对基类中的成员函数进行覆盖。
虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数
的处理是静态的。
virtual 关键字
派生类可以不显式地用virtual声明虚函数,这时系统就会用以下规则来判断派生类
的一个函数成员是不是虚函数:
该函数是否与基类的虚函数有相同的名称、参数个数及对应参数类型;
该函数是否与基类的虚函数有相同的返回值或者满足类型兼容规则的指针、
引用型的返回值;
如果从名称、参数及返回值三个方面检查之后,派生类的函数满足上述条件,就会
自动确定为虚函数。这时,派生类的虚函数便覆盖了基类的虚函数。
派生类中的虚函数还会隐藏基类中同名函数的所有其它重载形式。
一般习惯于在派生类的函数中也使用virtual关键字,以增加程序的可读性。
虚析构函数
为什么需要虚析构函数?
可能通过基类指针删除派生类对象;
如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正
常的),就需要让基类的析构函数成为虚函数,否则执行delete的结果是不确定的。
一个不使用虚析构函数的例子
源代码:
#include
using namespace std;
class Base {
public:
~Base(); //不是虚函数
};
Base::~Base() {
cout<< “Base destructor” << endl;
}
class Derived: public Base{
public:
Derived();
~Derived(); //不是虚函数
private:
int *p;
};
#include
using namespace std;
class Base {
public:
virtual ~Base();
};
Base::~Base() {
cout<< “Base destructor” << endl;
}
class Derived: public Base{
public:
Derived();
virtual ~Derived();
private:
int *p;
};
虚表与动态绑定
虚表
每个多态类有一个虚表(virtual table)
虚表中有当前类的各个虚函数的入口地址
每个对象有一个指向当前类的虚表的指针(虚指针vptr)
动态绑定的实现
构造函数中为对象的虚指针赋值
通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找
到所调用的虚函数的入口地址
通过该入口地址调用虚函数
虚表示意图
抽象类
纯虚函数
纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,
要求各派生类根据实际需要定义自己的版本,纯虚函数的声明格式为:
virtual 函数类型 函数名(参数表) = 0;
带有纯虚函数的类称为抽象类
抽象类
带有纯虚函数的类称为抽象类:
class 类名
{
virtual 类型 函数名(参数表)=0;
//其他成员……
}
抽象类作用
抽象类为抽象和设计的目的而声明
将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。
对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。
注意
抽象类只能作为基类来使用。
不能定义抽象类的对象。
例8-6 抽象类举例
//8_6.cpp
#include
using namespace std;
class Base1 {
public:
virtual void display() const = 0; //纯虚函数
};
class Base2: public Base1 {
public:
virtual void display() const; //覆盖基类的虚函数
};
void Base2::display() const {
cout << “Base2::display()” << endl;
}
class Derived: public Base2 {
public:
virtual void display() const; //覆盖基类的虚函数
};
void Derived::display() const {
cout << “Derived::display()” << endl;
}
void fun(Base1 *ptr) {
ptr->display();
}
int main() {
Base2 base2;
Derived derived;
fun(&base2);
fun(&derived);
return 0;
}
C++11:override 与 final
override
多态行为的基础:基类声明虚函数,继承类声明一个函数覆盖该虚函数
覆盖要求: 函数签名(signatture)完全一致
函数签名包括:函数名 参数列表 const
下列程序就仅仅因为疏忽漏写了const,导致多态行为没有如期进行
显式函数覆盖
C++11 引入显式函数覆盖,在编译期而非运行期捕获此类错误。
在虚函数显式重载中运用,编译器会检查基类是否存在一虚拟函数,与派生类中带
有声明override的虚拟函数,有相同的函数签名(signature);若不存在,则会回
报错误。
final
C++11提供的final,用来避免类被继承,或是基类的函数被改写
例:
struct Base1 final { };
struct Derived1 : Base1 { }; // 编译错误:Base1为final,不允许被继承
struct Base2 {
virtual void f() final;
};
struct Derived2 : Base2 {
void f(); // 编译错误:Base2::f 为final,不允许被覆盖
};
小结
主要内容
多态性的概念、运算符重载、虚函数、纯虚函数、抽象类、override 和
final
达到的目标
掌握运算符重载原理和方法
理解动态多态性的原理,掌握通过虚函数实现的多态性的方法
掌握纯虚函数和抽象类的概念和设计方法