面向对象程序设计
学习方法
理解基本原理+掌握程序设计方法+加强动手实践
课程目标
-
理解面向对象程序设计的基本原理,掌握面向对象技术的基本概念和封装性、继承性和多态性,能够具有面向对象程序设计思想。掌握C++语言面向对象的基本特性和C++语言基础知识,能够使用C++语言进行计算机工程领域复杂工程问题的表述,能够进行C++程序阅读和分析。
-
理解C++语言中的面向对象机制,掌握C++程序设计的一般流程、基本方法和技能。能够综合应用基础知识对复杂的计算机系统工程问题进行用户需求分析,并能够采用面向对象方法进行程序设计。
-
熟练掌握VC++集成开发工具的使用方法和技巧,能正确编辑、编译和调试C++语言程序,能够具有良好的程序调试和动手编程能力,并能够利用计算机软件对计算机领域的复杂工程问题进行面向对象分析,建立面向对象的系统模型,并对分析阶段建立的对象模型进行面向对象设计。
课程内容
C++语言基础
类和对象
友元和运算符重载
继承
运行时的多态性和抽象类
模板
异常处理
I/O流类库
MFC基础编程
第1章 C++语言基础
主要内容:
数据类型,基本语句
变量,函数
指针和引用
自定义语句,程序预处理
名字空间
new和delete运算符
输入和输出
基本数据类型
7种基本数据类型
整型:byte(1)、short(2)、int(4)、long(8)
浮点型:float(4)、double(8)
字符型:char(1)
字符串: 字符数组
空类型void
void—指“无类型”。
void *—指“无类型指针”,void *可以指向任何类型的数据,无需进行强制类型转换。
void作用:对函数返回的限定,对函数参数的限定。
枚举类型
C++枚举类型的一般形式:
enum<标识符>{<枚举元素列表>};
枚举元素列表:定义该枚举类型的所有枚举值。
枚举元素的值是从0开始的整数值。
例如:
enum weekday{sun, mon, tue, wed, thu, fri, sat};
枚举类型
枚举类型变量的操作只有赋值操作。
weekday workday;
workday = mon;
一个整数不能直接赋给一个枚举变量。如:
workday = 2; //error, 不属于同一类型
workday = (enum weekday)2;//强制类型转换
结构体
结构体是一个具有不同类型的相关数据的有机整体。
结构体的定义格式
struct <结构体名>
{
<成员列表>
};
例如:
struct Student
{
long number;
char name[10];
char sex[3];
int age;
};
结构体
结构体变量的定义格式
<结构体名> <结构体变量标识符列表>;
例如:Student stu1={100,“张三”,“男”,26}, stu2, *p;
结构体变量的引用
整体引用
stu2 = stu1; p= &stu1;
按分量引用
stu2.age = stu1.age; stu2.age = p->age;
1.3 变量
变量的定义方法
在C中,局部变量必须置于可执行代码段之前。
C++允许在程序的任何位置定义局部变量。
例如:
f()
{
int i; i = 10;
int j; j = 25;
//…
}
在C中编译指示出错,中止对函数的编译。
在C++中编译不会出错。
变量的定义方法
块
C++语言把用花括号{ }括起来的一块区域称为块。
块变量
定义在某个块中的变量。
块作用域
块变量的作用域,即由花括号{}括起来的范围。块变量在其作用域内是可见的,在其作用域外是不可见的。
void main(void)
{ int ii, jj, tt, v(6);
for(int i = 0; i<10; i++)
{ for(int j = 0; j<i; j++)
{ }
int t = 1; }
ii = i;
// jj = j; //变量 j 超出作用域,error
// tt = t; //变量 t 超出作用域,error
}
### 局部变量在什么位置说明较好?
通常认为:在大函数中,在最靠近使用变量的位置说明变量较为合理;
而在较短的函数中,把局部变量集中在函数开始处说明较好。
内联函数
内联函数也称内置函数,就是在编译时把该函数的程序代码插入到调用该函数的语句之处,以便在程序运行时不再进行函数调用。(这样会增加目标程序的代码量。)
设置内联函数的目的
为了解决函数调用的效率问题,消除函数调用时的系统开销,以提高运行速度。(是一种用空间换时间的措施。)
内联函数的标识符
在函数说明前,冠以关键字inline,该函数就被声明为内联函数。
#include<iostream.h>
inline float circle(float r) //内联函数
{ return 3.1416*r*r; }
void main()
{ for(int i=1; i<=3; i++)
cout<<”r=”<<i
<<” area=” <<circle(i)<<endl;
}
内联函数说明
内联函数的函数代码一定不能太长,如果函数代码太长,该内联函数在程序中又被多次调用,则可能引起严重不良后果。
内联函数在被调用之前必须进行完整的定义,否则编译器将无法知道应该插入什么代码。内联函数通常写在主函数的前面。
内联函数说明
内联函数内不能有静态变量、循环语句、switch语句和goto语句,不能有递归和数组说明等。
类的成员函数的内联函数有两种表示方法:
定义函数时,在函数名前加inline;
把函数体直接写在类定义内。
带缺省参数的函数
C++在说明函数原型时,可为一个或多个参数指定缺省参数值。例如:
int Fun1(int x=5, float y=5.3);
当进行函数调用时,编译器按从左向右顺序将实参与形参结合,当调用语句给出函数的参数时,就按该参数调用该函数。例如:
Fun1(100, 79.8); //x=100, y=79.8
若未指定足够的实参,则编译器按顺序用函数原型中的缺省值来补足缺少的实参。例如:
Fun1(25); //x=25, y=5.3
Fun1(); //x=5, y=5.3
带缺省参数的函数
说明
在函数原型中(设计函数缺省参数时),所有取缺省值的参数都必须出现在不取缺省值的参数的右边。即一旦开始定义取缺省值的参数,就不可再说明非缺省的参数。
例如:
int Fun2(int i, int j=5, int k); //错误
函数(名)重载
在C语言中,函数名必须是唯一的。
例如:
Isquare(int i);
Fsquare(float i);
Dsquare(double i);
C++支持函数(名)重载,即在同一个作用域内多个函数可以共用一个函数名。被重载的函数称为重载函数。例如:
square(int i);
square(float i);
square(double i);
第一章小结:
主要内容
重点
引用类型
const类型限定符
输出型参数的设计与引用
内联函数
带缺省参数的函数
函数重载
new和delete运算符
难点
#include<iostream>
using namespace std;
int sum(int *p, int n);
int main()
{
int a[3][3] = {1, 2, 3, 4, 5, 6 ,7, 8, 9};
cout<<sum(a[0], 3)<<endl;
//cout<<sum(*a, 3)<<endl;
return 0;
}
/*
int sum(int *p, int n)
{
int s = 0;
for(int i=0; i<n; i++)
s += *(p+i+i*n);
return s;
}
*/
int sum(int *p, int n)
{
int s = 0, *q;
q = p + n*n;
while(p < q)
{
s += *p;
p += n+1;
}
return s;
}
#include<iostream.h>
void converse(int *a, int *b, int n);
void main()
{
int a[9] = {1, 2, 3, 4, 5, 6 ,7, 8, 9}, b[9];
converse(a, b, 9);
for(int i = 0; i < 9; i++)
cout<<b[i]<<' ';
}
void converse(int *a, int *b, int n)
{
int *p = a+n-1;
while(p >= a)
{
*b = *p;
p--;
b++;
}
}
第2章 类和对象
面向对象技术的基本概念
抽象性和封装性
类
对象
对象成员变量
内部类
static成员
自引用对象指针this
string类
设计举例
设计实例对比
例:设计完成复数加法运算的C语言和C++语言程序。
class Complex{
float real;
float imag;
public:
Complex(float x=0, float y=0) : real(x), imag(y){}
Complex Add(Complex x){
Complex z;
z.real = real + x.real;
z.imag = imag + x.imag;
return z;
}
void Show(){
cout<<"The Complex is: (%.1f+%.1fi)\n", real, imag);
}
};
int main()
{
Complex x(1, 2), y(3, 4), z;
z = x.Add(y);
z.Show();
return 0;
}
面向过程程序设计方法的特点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MfNT7zel-1681720636448)(2023-03-30-23-58-09.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1UP8CJgB-1681720636449)(2023-03-30-23-59-49.png)]
面向对象程序设计方法的6个优点
可控制程序的复杂性
可增强程序的模块性
可提高程序的重用性
可改善程序的可维护性
能对现实世界的分类系统进行自然的描述
能很好地适应新的硬件环境
对象
在现实世界中有两类对象
有形事物和抽象概念。如:一本书,一座楼、学校校规等。
事件。如:一场足球比赛,一次就医过程等。
对象的特征
名字:用来唯一标识该对象
属性或状态:用来描述对象的特征
行为或操作:用来实现对象的功能
在面向对象方法中,对象是系统中用来描述客观事物的一个实体,是构成系统的一个基本单元。对象是既包括属性(状态)数据,又包括作用于属性数据的一组操作(行为、方法、服务)的封装体。
一个对象由属性和服务组成。在C++语言中,属性称作数据成员,服务称作成员函数。
类
在面向对象方法中,类是对具有相同属性和相同服务的一组相似对象的抽象。
三个特性
模块性:类是相同属性和服务的封装体。
抽象性:类是对具有相同属性和服务的一个或多个对象的抽象描述。
继承性:子类可以在继承父类所有属性和服务的基础上增加自己特有的属性和服务,或在某些操作中与父类有不同的操作。
消息
在面向对象方法中, 一个对象向另一个对象发出的服务请求被称为“消息”。对象之间通过消息传递进行交互。
消息的组成
接收消息的对象
消息名
零个或若干个参数
如:MyCircle.Draw(1, 2, 3, Red);
### 消息传递机制与函数调用机制的区别
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2CexvwzF-1681720636450)(2023-03-31-00-01-57.png)]
2.2 抽象性和封装性
抽象性
在面向对象技术中,抽象是通过对一个系统进行分析和认识,强调系统中的某些本质的特征,从而对系统进行的简化描述。
抽象包括两个方面:数据抽象和行为抽象。
在面向对象技术中,抽象性是指提取一个类或对象与众不同的特征,而不对该类或对象的所有信息进行处理。
封装性
封装性是把对象的属性和行为结合成一个独立的单位,尽可能隐藏对象的内部细节。
封装机制将对象的设计者与使用者分开,使用者只需要使用类的外部接口,从而降低了系统的复杂性和开发难度,提高了代码的重用性。
封装使得对象与外界形成一个边界,只保留有限的对外接口使之与外部发生联系。(如:售报亭)
类
类的定义
类是对具有相同属性和相同行为的客观世界的一组相似对象的抽象。
类的定义
class 类名 {
private:
<私有成员变量和成员函数>
protected:
<保护成员变量和成员函数>
public:
<公有成员变量和成员函数>
};
访问权限关键字
private—说明的成员只能在该类中使用,即在类的外面不能直接访问 private 成员,类的默认访问权限为private。
public—说明的成员不但可以在该类中使用,而且其他的函数或类可以直接访问 public成员。
protected—说明的成员可以在该类中使用,也可以在其派生类中使用,但其他函数或类不能直接访问 protected 成员。
class FirstClass
{
private:
int x; //只能在 FirstClass类中使用
public:
void set(int xx){ x=xx; }
int get(){ return x; }
};
例:设计复数类
class Complex{
float real, imag;
public:
Complex(float x=0, float y=0){real = x; imag = y;}
~Complex(){}
Complex Add(Complex x)
{ Complex z;
z.real = real + x.real; z.imag = imag + x.imag;
return z;
} ……
};
成员变量
成员变量的定义
成员变量可用任何C++的基本数据类型、用户自定义数据类型、C++的基本类类型或用户自定义的类类型定义。
成员变量定义不能递归,即不能用自身类的实例化对象作为该类的成员变量。如下定义类A是错误的:
class A{
private:
A x;
…
};
成员函数
成员函数的定义
内联函数形式1
class A{
…
返回类型 成员函数名(形参表)
{
函数体;
}
};
class Complex
{
float real;
float imag;
public:
Complex(float x=0, float y=0){real = x; imag = y;}
~Complex(){}
Complex Add(Complex x)
{ Complex z;
z.real = real + x.real;
z.imag = imag + x.imag;
return z;
}
};
成员函数
成员函数的定义
内联函数形式2
class A{
…
返回类型 成员函数名(形参表);
};
inline 返回类型 类名:: 成员函数名(形参表)
{
函数体;
}
class Complex
{
float real;
float imag;
public:
Complex(float x=0, float y=0){real = x; imag = y;}
~Complex(){}
Complex Add(Complex x);
};
inline Complex Complex::Add(Complex x)
{…}
成员函数
成员函数的定义
外联函数形式:
class A{
…
返回类型 成员函数名(形参表);
};
返回类型 类名:: 成员函数名(形参表)
{
函数体;
}
成员函数重载
基本概念
成员函数重载就是允许多个功能类似的成员函数使用同一个函数名。
当一个对象收到两个或多个相同消息时,则因调用成员函数时的参数个数或参数类型不同,而有不同的操作结果。
设计要求
重载的多个成员函数之间参数个数或类型要有所不同。
函数返回值类型不同不能作为重载依据。
构造函数和所有成员函数都可以重载。
构造函数
基本概念
构造函数是一个特殊的成员函数,主要用于为对象分配空间。
每当创建对象时,系统都自动调用构造函数,用来设置该对象的成员变量的初始数据值。
构造函数
构造函数声明
构造函数的访问权限一定是public。
构造函数名一定要和类名相同。
构造函数不能有返回类型,也不能是void类型。
例:
class Complex{
float real;
float imag;
public:
Complex(float x=0, float y=0)
{ real = x; imag = y; }
…
};
构造函数
构造函数重载
一个类中可以有多个参数个数或参数类型不同的构造函数。
缺省参数的构造函数——无参数或参数全为缺省值.
例:
Complex();
Complex(double r = 0.0, double i = 0.0);
析构函数
基本概念
自动对象
当用户定义时系统自动建立的对象,当程序运行到超出对象的作用域时,系统会自动收回自动对象的存储空间。
动态对象
用户根据需要向系统申请的,因此当动态对象超出作用域时必须由用户撤销动态对象,释放存储空间。C++使用析构函数来实现。
析构函数
析构函数声明
~类名();
析构函数不能带任何参数,不能有返回类型(void也不允许)。
一个类中只能有一个析构函数。
例:设计一个简单的动态数组类。
class Array
{
double *arr;
int size;
public:
Array(int sz=100);
~Array(void);
};
Array::Array(int sz)
{ if(sz<=0)
{ cout<<“参数错!”<<endl;
exit(0);
}
size = sz;
arr = new double[size];
}
Array::~Array(void)
{ delete[]arr; }
void main()
{ Array a(10); }
类与对象的关系
在C++中,类是创建对象时的模板,对象是类的实际变量,也称为类的实例。
对象是一个引用类型,对象名是指向系统所分配内存空间首地址的引用(或别名)。
例如:complex x(1, 2);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nkLCwMH2-1681720636451)(2023-03-31-00-22-43.png)]
对象的定义
利用构造函数创建对象有两种方法:
<类名> <对象名>[(<实参表>)]
<类名> *<指针> = new <类名>[(<实参表>)]
例:创建复数类Complex的一个对象x
Complex x(1, 2); 或
Complex *x = new Complex(1, 2);
对象的引用
对象的引用是指对对象成员的引用,外部程序(外部函数)只能通过向该对象发消息才能访问该对象的公有成员变量和成员函数。
一般引用格式如下:
使用点运算符(.):对象名.成员
Complex x(1, 2);
x.setReal(10);
使用指针运算符(->):指向对象的指针->成员
Complex *x = new Complex(1, 2);
Complex y(3, 4), z;
z = x->Add(y);
对象的引用
对象数组
数组中的每个元素都是同类对象。
声明格式:类名 数组名[维数]={初始化表};
使用格式:数组名[下标].对象成员名;
对象指针数组
数组中的每个元素都是指向对象的指针。
声明格式:
类名* 数组名[维数]={new 类名(初始化表1),… ,
new 类名(初始化表n)};
十个 C++ 运算符重载示例
https://blog.csdn.net/lu_embedded/article/details/121599696?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168085813916800226537145%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=168085813916800226537145&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_positive~default-2-121599696-null-null.142v82koosearch_v1,201v4add_ask,239v2insert_chatgpt&utm_term=%E8%BF%90%E7%AE%97%E7%AC%A6%E9%87%8D%E8%BD%BD&spm=1018.2226.3001.4187
数组类设计 (P101)
C++语言系统类库中提供的数组类Array 的设计方法:
可自定义下界下标和上界下标。
可对下标越界进行检查。
可重置数组大小。
可知道数组的长度。
数组类的对象可使用赋值运算符(=)和下标运算符([])。
class Array{
int size, low, up;
double* arr;
public:
Array(int sz=100);
Array(int l, int u);
Array(const Array& a);
~Array();
void operator=(const Array& a);
double& operator[](int ndx) const;
void Resize(int sz);
void Resize(int ll, int hh);
int ArraySize(void) const;
};
Array::Array(int sz)
{
if(sz<=0) exit(0);
low = 0;
up = sz-1;
size = sz;
arr = new double[size];
}
Array::Array(int l, int u)
{
low = l;
up = u;
size = up-low+1;
arr = new double[size];
}
Array::Array(const Array& a)
{
low = a.low;
up = a.up;
int n = a.size;
size = n;
arr = new double[size];
double* sourcePtr = a.arr;
double* destPtr = arr;
while(n--)
*destPtr++ = *sourcePtr++;
}
重点
定义友元类和友元函数的方法
运算符重载为类的成员函数
运算符重载为类的友元函数
难点
运算符重载函数设计
派生类的三种继承方式
继承方式表明派生类中新定义的成员函数对基类成员的访问权限。
public
基类的public和protected成员将分别成为派生类的public成员和protected成员。
private
基类的public和protected成员继承到派生类后,都成为private成员。
protected
基类的public和protected成员继承到派生类后,都成为protected成员。
在private继承方式下,外部程序中定义的派生类对象可以访问派生类中的公有成员,但不可以访问基类中的公有成员。
总结
在公有派生情况下,基类中所有成员的访问特性在派生类中维持不变;
在私有派生情况下,基类中所有成员在派生类中成为私有成员。
三种继承形式
派生类对基类成员函数的继承
派生类对基类中属性的继承是全部继承,但派生类对基类中服务的继承分为三种形式:
完全继承
覆盖继承
扩充继承
虚基类
基本概念
所谓虚基类,是说在一个类层次中,如果某个派生类存在一个公共基类,则系统只考虑最原始的那一个公共基类。
当一个基类有多于一个派生类时,为了防止产生重复继承,在定义派生类时,C++使用关键字virtual将基类声明为虚基类,即共享继承。
虚基类的使用
虚基类的声明格式
class 派生类名:virtual 继承方式 基类名
{
派生类成员变量定义;
派生类成员函数定义;
};
重点
继承的概念
派生类的三种继承方式
派生类的基类子对象
派生类的构造函数与析构函数
基类构造(析构)函数和派生类构造(析构)函数的调用顺序
赋值兼容规则
派生类对基类成员函数的继承
二义性问题及其解决方法
虚基类的概念和使用
第5章 运行时的多态性和抽象类
多态性
运行时的多态性
虚函数
抽象类
设计举例
5.1 多态性
- 基本概念
多态性是指相同的对象收到相同的消息时,或不同的对象收到相同的消息时,产生不同的行为方式。
通俗地说,多态性是指用一个名字定义不同的函数,这些函数执行不同却类似的接口、多种方法。即用同样的接口访问功能不同的函数,从而实现多态性。
- 多态性的分类
重载多态性(函数重载和运算符重载)
class Point{
int x, y;
public:
Point(int a, int b):x(a), y(b)
{ cout<<“类Point构造函数调用”<<endl; }
Point(Point &p):x(p.x), y(p.y) //拷贝构造函数
{ cout<<“类Point拷贝构造函数调用”<<endl; }
void Show(string s){ cout<<s<<endl; }
void Show(){ cout<<“(“<<x<<“, “<<y<<“)”<<endl; }
};
Point p(1, 2);
p.Show(“hello”);
p.Show();
继承多态性(派生类)
运行时的多态性的基本概念
对一个类层次来说,同样一个成员函数的调用,如果程序中动态确定的对象不同,则调用结果不同。
系统在运行时才具体确定对象所属类层次中的某个类,从而确定外部程序所发消息的匹配对象。
运行时的多态性的实现机制使得函数调用与函数体之间的联系在运行时才建立,即所谓的滞后联编(动态链接)。
滞后联编
联编(binding, 绑定或束定)
联编是将一个标识符名和一个存储地址联系在一起。
对一个函数调用,要在编译时或在运行时确定将链接上相应的函数体的代码,这一过程称为函数联编(简称联编)。
C++的两种联编方式
早期联编(静态联编)
滞后联编(动态联编)
滞后联编
静态多态性的实现
在C++中,静态多态性主要通过重载函数实现。
包括两种情况:
1)在一个类中说明的重载;
2)基类成员函数在派生类中重载。
参数有所差别的重载。
函数所带参数完全相同,只是属于不同的类。
- 滞后联编
滞后联编(动态联编)
在面向对象的语言中,联编是把每一条消息和相应对象的方法相结合。
滞后联编是指系统在运行时根据当时指针对象的具体取值确定对象的具体类型,或匹配重载函数的具体某一个。
滞后联编支持的多态性称为运行时的多态性,也称为动态多态性。
主要优点:提供了更好的灵活性、高度问题抽象性和程序易维护性。
动态多态性的实现
以公有继承为基础;
以虚函数为重要条件;
使用对象的引用或指针。
5.3 虚函数
- 虚函数的概念
C++语言中,函数声明前边标有关键字virtual的成员函数称做虚函数。
滞后联编的标记是虚函数标记virtual。
- 虚函数的定义
在基类中声明虚函数
virtual 成员函数原型;
- 在派生类中重载
必须保持函数原型完全不变。
否则等同于普通函数的重载,不能实现动态联编。
重载时关键字virtual可以省略。
虚函数的使用方法
例:使用虚函数计算三角形、矩形和圆的面积。
class figure{
protected:
double x, y;
public:
figure(double a, double b):x(a), y(b){}
virtual void show_area()
{
cout<<"No area defined"<<endl;
}
};
class triangle : public figure{
public:
triangle(double a, double b) : figure(a,b){}
void show_area()
{ cout<<"Triangle's area: "<<x*y*0.5<<endl; }
};
class square : public figure{
public:
square(double a, double b) : figure(a,b){}
void show_area()
{ cout<<" square 's area: "<<x*y<<endl; }
};
class circle : public figure{
public:
circle(double a):figure(a, a){}
void show_area()
{ cout<<"Circle's area: "<<x*x*3.1416<<endl; }
};
figure *p;
triangle t(10.0, 6.0);
square s(10.0, 6.0);
circle c(10.0);
p = &t; p->show_area();
p = &s; p->show_area();
p = &c; p->show_area();
说明:
虽然三个类计算面积的方法不同,但具有相同的界面接口:show_area(),显示了C++的“同一接口,多种方法”的多态性机制。
应用程序不必为每一个派生类编写功能调用。即“以不变应万变”,可大大提高程序的可复用性。
虚函数与覆盖继承的设计区别
共同点
派生类成员函数和基类成员的参数个数和参数类型完全一样。
区别
对于覆盖继承,接收消息的对象在编译时就已确定,要么是基类对象,要么是派生类对象,在程序运行时不会改变。
对于虚函数实现的运行时的多态性,接收消息的对象(基类的指针对象)在编译时并不确定,而是在运行时根据基类指针对象所指对象的不同执行相应的虚函数。
class A{
public:
A(void){}
virtual void vf1(void) const
{ cout<<"f1 function of base."<<endl; }
void f2(void) const
{ cout<<"f2 function of base."<<endl; }
};
class AA : public A{
public:
AA(void){}
void vf1(void) const
{ cout<<“vf1 function of derive."<<endl; }
void f2(void) const
{ cout<<"f2 function of derive."<<endl; }
};
void main(void)
{
A myA, *pa;
AA myAA;
pa = &myA;
pa->vf1();
pa->f2();
pa = &myAA;
pa->vf1();
pa->f2();
myA.f2();
myAA.f2();
}
虚析构函数
基本概念
类的构造函数可以重载,析构函数不能重载。
C++标准不支持虚构造函数,析构函数能定义为虚函数。
虚析构函数声明
virtual ~<类名>();
虚析构函数作用
基类析构函数被设置为虚函数后,当把动态申请的派生类对象地址赋给基类对象指针,delete运算符对析构函数的调用可以采用滞后联编,从而实现运行时的多态性。
析构函数执行时首先调用派生类的析构函数,其次调用基类的析构函数。
例:虚析构函数的作用(P155)
class A{
public:
A(void){}
virtual ~A(void)
{
cout<<"基类析构函数"<<endl;
}
};
class AA : public A{
char *aa;
int length;
public:
AA(char *messaeg)
{ length = strlen(message) + 1;
aa = new char(length);
strcpy(aa, message);
}
~AA(void)
{ delete aa;
cout<<"派生类析构函数"<<endl;
}
};
void main()
{
A *pa = new AA(“Hello”);
delete pa;
}
虚析构函数结论推广
凡是派生类的构造函数或其它成员函数中,有用运算符new动态申请了内存空间,需要派生类的析构函数用运算符delete动态释放的情况,必须设计其基类的析构函数为virtual,从而保证派生类对象动态申请的内存空间被自动释放。
如果一个类的析构函数是虚函数,由它派生而来的所有子类的析构函数也是虚函数。
5.4 抽象类
- 基本概念
抽象类
若某个类往往表示一种抽象的概念,它并不与具体的事物相联系,即没有具体对象存在,则该类称做抽象类。
如: Shape类。
纯虚函数
在某些情况下,基类需要一个虚函数被所有派生类覆盖。
该虚函数在基类中的定义无意义。
如:void Shape::Draw();
- 纯虚函数的定义与应用
定义格式
virtual type func_name(参数表) = 0;
一旦基类中定义了纯虚函数,则在该基类的任何派生类中都必须定义自己的与虚函数同名的重载函数。
例: (P157)
#include<iostream>
class Shape{
public:
virtual void draw() = 0;
};
class Rec : public Shape{
public:
void draw() { cout<<“draw Rec”<<endl; }
};
class Circle : public Shape{
public:
void draw()
{
cout<<“draw circle”<<endl;
}
};
int main(void)
{
Shape *p
p = new Rec();
p->draw();
delete p;
p = new Circle();
p->draw();
delete p;
}
抽象类
如果一个类中至少有一个纯虚函数,则称该类为抽象类。
抽象类的使用规定
抽象类不能建立抽象类对象。
抽象类不能作为参数类型、函数返回类型或显式转换的类型。
如果在抽象类的派生类中没有重新定义纯虚函数,则这个派生类仍是一个抽象类。抽象类只能定义为一个类层次中的基类,不能定义为派生类。(如下例)
class shape{
protected:
int color;
public:
shape(int c) : color(c){}
virtual void area()=0;
virtual void print()=0;
};
class figure : public shape{
protected:
int x, y;
public:
figure(int a, int b, int c)
:x(a), y(b), shape(c){}
void print()
{ cout<<color<<endl; }
};
figure ff(1,2,3); //error
class triangle : public figure{
public:
triangle(int a, int b, int c):figure(a, b, c){}
void area()
{
cout<<"Triangle's area: "
<<x*y*0.5<<endl;
}
};
triangle tt(1,2,3);
5.5 设计举例
基本概念
同质单链表
单链表中所有结点都属同样的数据类型或类类型。
异质单链表
单链表中的结点可以属于一个类层次中的不同类类型。
例:大学人员问题
教学人员:姓名,年龄,专业编号
非教学人员:姓名,年龄,业绩评级
要求:
设计一个能同时存储学校教学人员和非教学人员的异质单链表类,并设计一个测试程序进行测试。
设计分析
抽取教学人员和非教学人员的共同信息,生成基类Person。
生成公有派生类Professor,包括教学人员的特殊信息。
生成公有派生类Staff,包括非教学人员的特殊信息。
把基类Person中的成员函数CreateNode()设计为空虚函数,将Print()设计成虚函数。
在基类Person中声明异质单链表类DLinList是它的友元类。
异质单链表类DLinList中的成员变量head和成员函数Insert(p, pos)中的参数p设计成基类Person的指针类型,从而根据赋值兼容规则和虚函数方法实现运行时的多态性。
本章小结
重点
多态性概念
滞后联编概念
运行时多态性实现方法
虚析构函数
抽象类的设计与应用
难点
滞后联编概念
运行时多态性实现方法
虚析构函数