Download presentation
Presentation is loading. Please wait.
1
版权所有 复制必究 第 3 章 C++面向对象程序设计
2
与传统的面向过程的程序设计语言相比,C++语言的最大特征是支持面向对象程序设计OOP(Object Oriented Programming),它引入了类、继承、多态和重载等面向对象的新机制。通过本章的学习,使我们系统地介绍C++面向对象设计的基本方法。 本章主要内容: 类和对象 类继承 多态性:虚函数,重载,模板
3
3.1 面向对象程序设计概述 3.1.1 结构化程序设计 结构化程序设计的特点: 是一种自上而下、逐步细化的模块化程序设计方法。
3.1 面向对象程序设计概述 结构化程序设计 结构化程序设计的特点: 是一种自上而下、逐步细化的模块化程序设计方法。 Wirth N的观点:算法 + 数据结构 = 程序 是一种面向过程程序设计方法,即一个程序是由多个过程(在C++中为函数)模块组成,过程之间通过函数参数和全局变量进行相互联系。
4
结构化程序设计的特点: 与非结构化程序相比,结构化程序在调试、可读性和可维护性等方面都有很大的改进。 代码重用性不高:以过程为中心设计新系统,除了一些标准函数,大部分代码都必须重新编写。 由于软、硬件技术的不断发展和用户需求的变化,按照功能划分设计的系统模块容易发生变化,使得开发出来的模块的可维护性欠佳。 面向过程模式将数据与过程分离,若对某一数据结构做了修改,所有处理数据的过程都必须重新修订,这样就增加了很多的编程工作量。
5
面向对象程序设计方法及特征 什么是对象: 现实世界是由各种各样的事物组成,包括真实的事物和抽象的事物。例如,人、动物、汽车(真实的事物)和程序、直线(抽象的事物)等。 每一类事物都有自己特定的属性(如大小、形状、重量等)和行为(如生长、行走、转弯、运算等),人们通过研究事物的属性和行为而认识事物。 在计算机科学中将这些现实世界中的事物称之为对象。对象是包含现实世界中事物特征的抽象实体,它反映了系统为之保存信息和与之交互的方法。 在程序设计领域,可以用如下公式表示: 对象 = 数据 + 作用于这些数据上的操作
6
什么是类: 为了描述属性和行为相同的一类对象,引入了类(class)的概念。 类是具有相同数据结构(属性)和相同操作功能(行为)的对象的集合,它规定了这些对象的公共属性和行为方法。 对象是类的一个实例,例如,汽车是一个类,而行驶在公路上的一辆汽车则是一个对象。 对象和类的关系相当于程序设计语言中变量和变量类型的关系。
7
面向对象程序设计的特点: OOP围绕现实世界的概念来组织模块,采用对象描述问题空间的实体,用程序代码模拟现实世界中的对象,使程序设计过程更自然、更直观。 SP是以功能为中心来描述系统,而OOP是以数据为中心来描述系统。相对于功能而言,数据具有更强的稳定性。 OOP模拟了对象之间的通信。就象人们之间互通信息一样,对象之间也可以通过消息进行通信。这样,我们不必知道一个对象是怎样实现其行为的,只需通过对象提供的接口进行通信并使用对象所具有的行为功能。
8
面向对象程序设计的特点: OOP把一个复杂的问题分解成多个能够完成独立功能的对象(类),然后把这些对象组合起来去完成这个复杂的问题。 一个对象可由多个更小的对象组成,如汽车由发动机、传送系统和排气系统等组成。这些对象(类)可由不同的程序员来设计,可在不同程序中使用,就象一个汽车制造商使用许多零部件去组装一辆汽车,而这些零部件可能不是自己生产的。 采用面向对象模式就象在流水线上工作,我们最终只需将多个零部件(已设计好的对象)按照一定关系组合成一个完整的系统。
9
一个简单例子: class Time { private: int hour; // 数据成员,表示小时 int minute; // 数据成员,表示分钟 int second; // 数据成员,表示秒 public: void setTime(int h, int m, int s) // 成员函数,设置时间 { hour=(h>=0 && h<24) ? h:0; minute=(m>=0 && m<60) ? m:0; second=(s>=0 && s<60) ? s:0; } void showTime() // 成员函数,输出时间 { cout<<hour<<':'<<minute<<':'<<second<<endl; } };
10
main() { Time EndTime; // 声明对象EndTime // 设置对象EndTime的时间(属性,数据成员) EndTime.setTime(12, 23, 36); cout<<"The time is:"; // 显示对象EndTime的时间 EndTime.showTime(); } 运行结果: The time is:12 : 23 : 36
11
面向对象程序设计方法的基本特征 面向对象程序设计方法具有四个基本特征: 抽象 封装 继承 多态性 1. 抽象 抽象是人类认识问题的最基本手段之一。抽象是指对具体问题(对象)进行概括,抽出一类对象的公共属性和行为并加以描述的过程。抽象包括数据抽象和代码抽象(或行为抽象)。
12
2. 封装 封装是把每个对象的数据(属性)和操作(行为)包装在一个类中。一旦定义了对象的属性和行为,则必须决定哪些属性和行为只用于表示内部状态,哪些属性和行为在外部是可见的。 一般限制直接访问对象的属性,而应通过操作接口访问,这样使程序中模块之间关系更简单、数据更安全。对程序的修改也仅限于类的内部,使得由于修改程序所带来的影响局部化。
13
3. 继承 继承是指一个新类可以从现有的类派生而来。新类继承了现有类的特性,包括一些属性和行为,并且可以修改或增加新的属性和行为,使之适合具体的需要。 例如,所有的Windows应用程序都有一个窗口,它们可以看作都是从一个窗口类派生出来的,但有的应用程序用于文字处理,有的应用程序用于绘图,这是由于派生出了不同的类,它们增加了不同的属性和行为。 继承很好地解决了软件的可重用性问题。
14
4. 多态性 多态性是指类中具有相似功能的不同函数使用同一个名称来实现,并允许不同类的对象对同一消息作出的响应不相同。 例如,同样的“编辑|粘贴”操作,在字处理程序和绘图程序中有不同的结果;同样的加法,把两个时间值相加和把两个整数相加的要求肯定不同。 多态性使程序设计灵活、抽象,具有行为共享和代码共享的优点,很好地解决了程序的函数同名问题。
15
3.2 C++类 为了支持面向对象程序设计,C++在C语言结构(struct)数据类型的基础上引入了类这种抽象数据类型。
16
类的定义与实现 C++类将对象的属性抽象为数据成员,将对象的行为抽象为成员函数,并对它们进行封装。数据成员又称成员变量,成员函数又称为方法。 C++类在形式上类似于C语言中用户自定义的结构类型,但定义类时规定了成员的访问控制权限。对象只能访问所属类的公有成员,而类的私有成员只能在类的成员函数中被访问。 C++类定义的基本形式
17
C++类定义的基本形式: class <类名> { private: <私有数据成员和私有成员函数的声明列表>; public: <公有数据成员和公有成员函数的声明列表>; protected: <保护数据成员和保护成员函数的声明列表>; };
18
说明: 类的定义由关键字class开始,其后为用户定义的类名,花括号括起来的部分称为类体。 关键字private、public和protected称为访问权限控制符,用来设置数据成员和成员函数的访问属性,其默认值为private。 private属性表示数据成员和成员函数是类的私有成员,它们只允许被本类的成员函数访问或调用,数据成员一般定义为private属性;
19
说明: public属性表示数据成员和成员函数是类的公有成员,它们允许被本类或其它类的成员函数(通过对象)访问或调用,是类的外部接口,成员函数一般定义为public属性; protected属性表示数据成员和成员函数是类的保护成员,它们允许被本类的成员函数和派生类的成员函数访问或调用。 例:
20
例 定义类Time(表示时间)。 私有数据成员hour、minute 和second只能在类的成员 函数中被访问或赋值; class Time{ private: // 最好不要省略private int hour; // 数据成员,表示小时 int minute; // 数据成员,表示分钟 int second; // 数据成员,表示秒 public: void setTime(int, int, int); // 成员函数,设置时间 void showTime(); // 成员函数,输出时间 }; 公有成员函数setTime、showTime 可在外部被调用,但必须通过一个 对象作为对象的成员使用。
21
类的实现: 利用C++类进行面向对象编程,定义类的成员只是完成了工作的第一步,最重要的工作是实现定义的类。 类的实现实质上是类的成员函数的实现,即定义类的成员函数。 成员函数的定义形式与一般函数的定义形式基本相同,但必须在成员函数名前加上类名和作用域限定符(::)。 成员函数的定义也可放在类体内(该函数声明之处),这时成员函数将变成内联函数。 例:
22
例 类Time的实现。 void Time::setTime(int h, int m, int s) { hour=(h>=0 && h<24) ? h:0; // 设置时间 minute=(m>=0 && m<60) ? m:0; second=(s>=0 && s<60) ? s:0; } void Time::showTime() cout<<hour<<':'<<minute<<':'<<second<<endl; private成员hour、minute和 second不允许外界存取, 所以为类Time增加两个 public成员函数,供外界 设置或显示private成员。
23
C++面向对象编程约定之一: 一般将类的定义放在头文件(.h)中,类的实现放在源文件(.cpp)中,而main主函数可以放在另一个源文件中。在源文件中用#include编译预处理指令包含头文件。 利用类声明对象: 对象是类的一个实例,定义并实现了类,就可以利用定义好的类来声明对象,即创建对象。声明对象的形式与声明普通变量类似,例如: Time t1, start ; point *pt1=&t1
24
成员的访问: 声明对象后,就可以通过成员运算符“ . ”或指向运算符“->”访问对象的公有成员,但不能访问对象的私有成员。 例如,公有成员函数调用: t1.setTime(); start.showTime(); pt1->setTime(); 而任何形如t1.hour、t1.minute、start.second等私有成员变量的直接访问都是非法的。 例:
25
例 类Time的使用,声明对象并设置对象属性。
main() { Time EndTime; // 声明对象EndTime EndTime.setTime(12, 23, 36); // 设置对象EndTime的时间 cout<<"The time is:"; EndTime.showTime(); // 显示对象EndTime的时间 }
26
? 3.2.2 构造函数和析构函数 在定义类时不能对成员变量进行初始化,因为无法确定成员变量属于哪一个对象。
构造函数和析构函数 如何进行成员 变量的初始化? 在定义类时不能对成员变量进行初始化,因为无法确定成员变量属于哪一个对象。 成员变量一般都定义为私有属性,也不能在声明对象后利用赋值运算对成员变量进行初始化。 成员变量的初始化一般是利用一个名为构造函数的成员函数来完成。 ?
27
什么是构造函数: 构造函数是一种特殊的成员函数,它是在创建对象时(声明或new动态创建)系统自动调用的成员函数。 什么是析构函数: 析构函数也是一种特殊的成员函数,它是在对象生存期结束时系统自动调用的成员函数。 构造函数的名称与类名相同,析构函数的名称必须在类名前加上“~”符号。注意,构造函数和析构函数不能指定任何返回值类型,包括void返回类型。
28
例 为类Time添加构造函数和析构函数。 #include <iostream.h> class Time{ private: int hour; int minute; int second; public: Time(int, int, int); // 构造函数 ~Time(); // 析构函数 };
29
Time::Time(int h, int m, int s) { hour=h; // 对私有成员变量初始化 minute=m;
构造函数和析构函数的实现: Time::Time(int h, int m, int s) { hour=h; // 对私有成员变量初始化 minute=m; second=s; cout<<"The constructor be called: "<<hour<<':’ <<minute<<':'<<second<<endl; } 功能与成员函数 Time::setTime()类似 Time::~Time() { cout<<"The destructor be called: "<<hour<<':’ <<minute<<':'<<second<<endl; }
30
? 构造函数和析构函数的自动调用: void main(void) { Time t1(10, 35, 55) ; // 自动调用构造函数
为什么是 这个结果? 程序运行结果为: The constructor be called:10:35:55 The constructor be called:16:53:9 The destructor be called:16:53:9 The destructor be called:10:35:55 ?
31
结果分析: 当创建一个对象时,系统先根据类定义的成员变量为对象分配内存空间,然后自动调用对象的构造函数对这段内存空间进行初始化处理,从而完成对象的初始化。 当撤消一个对象时,系统先自动调用对象的析构函数,然后释放对象所占内存空间。 从程序的运行结果可以看出,析构函数的调用顺序一般与构造函数的调用顺序相反。 栈:后进先出表
32
补充说明: 与一般数据类型的变量相比,对象在它的生存期会有大量的操作,有时这些操作的结果必须在对象的生存期结束时加以清理。因此可以在析构函数中进行动态分配的内存清理工作。 如果定义类时没有提供构造函数和析构函数,编译系统将会自动为类分别添加一个缺省的构造函数和析构函数。如果用户加上自定义的构造函数和析构函数,编译系统将不会再添加缺省的构造函数和析构函数。 若构造函数无参数,则声明对象时也不能给出参数。
33
3.2.3 this指针 this指针是一个特殊的隐藏在对象中的指针,每一个处于生存期的对象都有一个this指针,用于指向对象本身。
例:
34
下面定义的成员函数并没有声明this参数:
void Time::showTime() { cout<<hour<<':'<<minute<<':'<<second<<endl; } 编译器会把this指针作为成员函数的参数: void Time::showTime(Time* this); { cout<<this->hour<<':'<<this->minute<<':’ <<this->second<<endl; }
35
调用时: 当程序中调用某个成员函数时,编译器会把该对象的地址赋值给this指针,并将该地址值加入到参数表中,如下所示: EndTime.showTime(&EndTime); 作用: 在一个成员函数中经常需要调用其它函数(非本类的成员函数),而有时需要把对象本身(即对象的地址)作为参数传递给被调用函数,这时必须使用this指针。 例:
36
例 this指针的使用。 #include <iostream.h> #include <string.h> class Person{ public: // 可在外部直接访问public属性的数据成员 char m_strName[20]; char m_ID[18]; public: Person(char* strName, char* ID) // 内联构造函数 {strcpy(m_strName, strName); strcpy(m_ID, ID);}; void Show(); };
37
void Display(Person* pObj) // 非成员函数
{ cout<<"Name:"<<pObj->m_strName<<endl <<"ID:"<<pObj->m_ID<<endl; } void Person::Show() Display(this); // 以this指针作为参数调用其它函数 void main(void) Person *pPerson = // new运算也调用构造函数 new Person("LiMing"," "); pPerson->Show(); // 通过调用Show调用Display
38
静态成员 静态成员的概念: 一般情况下,同一个类不同对象的数据成员所占用的内存空间是不同的(体现了不同对象具有不同的属性值)。在有些情况下,类的数据成员的值对每个对象都是相同的,如当前已创建对象的数量,这时可以将该数据成员声明为静态数据成员(占有相同的存储单元)。 静态成员的声明: 在声明成员时以关键字static开头,例如: public: static int m_nCount;
39
静态成员的初始化:放在类定义的外部 int Person :: m_nCount=0; 说明: 静态成员分为静态数据成员和静态成员函数。 静态数据成员类似于一般的static静态变量,它具有全局性。静态数据成员属于整个类,为类的所有对象共享。 无论类的对象有多少,类的静态数据成员只有一份,存储在同一个内存空间。即使没有创建类的一个对象,类的静态数据成员也是存在的。 使用静态数据成员保证了该数据成员值的唯一性。
40
静态成员的访问: 公有静态成员:三种方式 (1)通过对象访问,如: person1.m_nCount=100; (2)利用类名和作用域限定符(::)访问,如: int Person::m_nCount=100; // 初始化 (3)在成员函数中访问,如: m_nCount++; 私有和保护静态成员:只能在成员函数中访问
41
静态成员函数: 成员函数也可以是静态的,其声明方式与静态成员变量类似。如: public: static int GetCount(); // 获取静态数据成员 静态成员函数也与一个类相关联,而不只与一个特定的对象相关联。 区别非静态成员函数,静态成员函数没有this指针,因为类的静态成员函数只有一个运行实例。 成员函数一般是公有属性,可以通过对象、类名和作用域限定符、在成员函数中三种方式调用静态成员函数。
42
注 意 静态成员函数只能访问类的静态成员(成员变量和成员函数),而不能访问类的非静态成员。因为当通过类名和运算符“::”调用一个静态成员函数时,不能确定函数中所访问的非静态成员属于哪一个对象。 解决方法: 将对象作为静态成员函数的参数,然后在静态成员函数中通过对象访问它的非静态成员。 例
43
例 静态成员变量和静态成员函数的使用。 #include <iostream.h> #include <string.h> class Person{ public: char m_strName[20]; long m_ID; static int m_nCount; // 静态成员变量,表示已创建对象的数量 Person(char*, long); // 构造函数 static int GetCount(); // 静态成员函数 static long GetID(Person); // 对象作为静态成员函数的参数 };
44
Person::Person(char* strName, long ID)
{ strcpy(m_strName, strName); m_ID=ID; m_nCount++; // 对象数目加1 } int Person::GetCount() return m_nCount; // 访问静态成员变量 long Person::GetID(Person x) return x.m_ID; // 不能直接访问非静态成员m_ID
45
int Person::m_nCount=0; // 初始化静态成员变量
void main() { Person e1("LiuJun", ); cout<<Person::m_nCount<<" , "<<e1.m_nCount <<'\n'; // 通过类或对象访问静态成员变量 cout<<Person::GetCount()<<" , ” <<Person::GetID(e1)<<'\n'; // 通过类调用静态成员函数 cout<<e1.GetCount()<<" , "<<e1.GetID(e1)<<'\n'; // 通过对象调用静态成员函数
46
Person e2("WangXiaogang",1101058);
cout<<Person::GetCount()<<" , ” <<Person::GetID(e2)<<'\n'; cout<<e2.GetCount()<<" , "<<e2.GetID(e2)<<'\n'; cout<<e1.GetCount()<<" , "<<e1.GetID(e1)<<'\n'; // e1和e2共享静态成员变量m_nCount } 程序运行结果为: 1,1 1, 2, 2,
47
作业: P ,3-37(1),3-42
48
友元 类具有封装性,类的私有成员一般只能通过该类的成员函数访问,这种封装性隐藏了对象的数据成员,保证了对象的安全,但有时带来了编程的不方便。 友元函数: C++提供了一种函数,它虽然不是一个类的成员函数,但可以象成员函数一样访问该类的所有成员,包括私有成员和保护成员。这种函数称为友元(friend)函数。
49
友元函数本身的定义没有什么特殊要求,可以是一般函数,也可以是另一个类的成员函数。
友元函数的声明: 一个函数要成为一个类的友员函数,需要在类的定义中声明该函数,并在函数声明的前面加上关键字friend。 友元函数本身的定义没有什么特殊要求,可以是一般函数,也可以是另一个类的成员函数。 为了能够在友元函数中访问并设置类的私有数据成员,一个类的友元函数一般将该类的引用作为函数参数。 例
50
例如: class A { friend void display(A); // 友元函数是一个一般函数 friend void B::BMemberFun(A&); // 友元函数是另一个类B的成员函数 public: }
51
友元类: 友元的另一种类型是友元类,一个类可以声明另一个类为其友元类,这个友元类的所有成员函数都可以访问声明其为友元的类的所有成员。 由于访问权限控制符不影响友元声明,友元声明可放在类体中任何地方,建议把友元声明放在类体的开始位置。 例 友元(一般友元函数、友元成员函数和友元类)的声明和使用。 P78-79,例3-7。
52
作业: P114,3-43 说明: 友元关系是单方向的,不具有交换性和传递性。
使用友元虽然简化了编程,并可避免调用成员函数的开销,但破坏了类的封装性,建议谨慎使用。 作业: P114,3-43
53
3.3 类的继承 继承是面向对象程序设计方法的四个基本特征之一,是程序代码可重用性的具体体现。
3.3 类的继承 继承 继承是面向对象程序设计方法的四个基本特征之一,是程序代码可重用性的具体体现。 在C++面向对象程序设计中,所谓类的继承就是利用现有的类创建一个新的类。新类继承了现有类的属性和行为。 为了使新类具有自己所需的功能,它可以扩充和完善现有类的属性和行为,使之更具体。 微软基础类MFC就是通过类的继承来体现类的可重用性和可扩充性。 发扬
54
3.3.1 基类和派生类 1. 问题的提出 在现实世界中,一类事物的对象常常也属于另一类事物。
基类和派生类 1. 问题的提出 在现实世界中,一类事物的对象常常也属于另一类事物。 在面向对象程序设计方法中,一个类的对象也常常是另一个类的对象,即一个类具有了另一个类的属性和方法。 在定义一个类时,根据类的继承性,我们能够且应尽可能地利用现有的类来定制新的类,而不必重新设计新的类。
55
2. 基类和派生类的概念 在继承关系中,新定义的类称为被继承类的派生类或子类,而被继承的类称为新定义类的基类或父类。派生类继承了基类的所有成员。 一个派生类也可以作为另一个派生类的基类。 3. 派生类的定义 class <派生类名> : [<派生方式>] <基类名> { // 派生类新增加的成员声明列表 };
56
说明: 派生方式决定了基类的成员在派生类中的访问权限。派生方式共有三种:public、private和protected(缺省值为private)。 虽然派生类继承了基类的所有成员,但为了不破坏基类的封装性,无论采用哪种派生方式,基类的私有成员在派生类中都是不可见的,即不允许在派生类的成员函数中访问基类的私有成员。
57
三种派生方式的区别: 采用public派生,基类成员的访问权限在派生类中保持不变,即基类所有的公有或保护成员在派生类中仍为公有或保护成员。public派生最常用。 (1) 可以在派生类的成员函数中访问基类的非私有成员; (2) 可通过派生类的对象直接访问基类的公有成员。 采用private私有派生,基类所有的公有和保护成员在派生类中都成为私有成员,只允许在派生类的成员函数中访问基类的非私有成员。private派生很少使用。 采用protected保护派生,基类所有的公有和保护成员在派生类中都成为保护成员,只允许在派生类的成员函数和该派生类的派生类的成员函数中访问基类的非私有成员。
58
例 定义类Point,然后定义类Point的派生类Circle。
#include <iostream.h> class Point // 定义基类,表示点 { private: int x; int y; public: void setPoint(int a, int b) { x=a; y=b; }; // 设置坐标 int getX() { return x; }; // 取得X坐标 int getY() { return y; }; // 取得Y坐标 };
59
class Circle : public Point // 定义派生类,表示圆
{ private: int radius; public: void setRadius(int r) { radius=r; }; // 设置半径 int getRadius() { return radius; }; // 取得半径 int getUpperLeftX() { return getX()-radius; }; // 取得外接正方形左上角的X坐标 int getUpperLeftY() { return getY() + radius; }; // 取得外接正方形左上角的Y坐标 };
60
程序运行结果: X=200,Y=250,Radius=100 UpperLeft X=100,UpperLeft Y=350
main() { Circle c; c.setPoint(200, 250); c.setRadius(100); cout<<"X="<<c.getX()<<", Y="<<c.getY() <<", Radius="<<c.getRadius()<<endl; cout<<"UpperLeft X="<<c.getUpperLeftX() <<", UpperLeft Y="<<c.getUpperLeftY()<<endl; } 公有派生类的对象可以直接访问基类Point的公有成员 程序运行结果: X=200,Y=250,Radius=100 UpperLeft X=100,UpperLeft Y=350
61
说明: 派生类Circle通过public派生方式继承了基类Point的所有成员(除私有成员外所有成员的访问权限不变),同时还定义了自己的成员变量和成员函数。 若将类Circle的派生方式改为private或protected,则下述语句是非法的:c.setPoint(200, 250); 无论哪种派生方式,派生类都继承了基类的所有成员,包括私有成员。我们虽然不能在派生类Circle中直接访问私有数据成员x和y,但可以通过继承的公有成员函数getX()、getY()和setPoint()访问或设置它们。 ! 容易 混淆
62
最后一个问题: ? 利用类继承定义类可能带来一个问题:派生类会继承它不需要的基类中的数据成员和成员函数,这时,基类中不适合于派生类的成员可以在派生类中重新加以定义。 例 派生类成员函数对基类成员函数的覆盖。 #include <iostream.h> class A { public: void Show( ) { cout<<"A::Show\n"; }; };
63
class B : public A { public: void Show( ) { cout<<"B::Show\n"; }; // 在派生类中重新定义成员函数 void Display() { Show( ); }; // 调用派生类B的成员函数Show() }; void main() A a; B b; a.Show(); // 调用基类A的成员函数Show() b.Show(); // 调用派生类B的成员函数Show() b.Display(); } 如果想调用基类A的成员函数Show(),可以使用作用域限定符“::”:{A:: Show();};
64
? 程序运行结果: A::Show B::Show 请问:如果在派生类B中没有对成员函数Show()重新进行定义,程序运行结果如何?
从本例可以看出,虽然派生类继承了基类的所有成员函数,但如果派生类某个成员函数的名称和参数与基类成员函数一致(即在派生类中对该成员函数重新进行了定义),则在派生类中调用的成员函数是派生类的成员函数。 请问:如果在派生类B中没有对成员函数Show()重新进行定义,程序运行结果如何? ?
65
为什么我们经常在现有类的基础上采用继承的方法来定制新类,而不通过直接修改现有类来设计自己的类?除了代码重用的优越性,其主要原因是可能得不到基类的实现源码。
重要性! 在利用微软基础类MFC派生自己的类时,我们只需要MFC类声明的头文件(利用#include指令将头文件包含)和含有成员函数目标代码的OBJ文件,并不需要整个MFC类库的实现源码。
66
基类和派生类的构造函数 1. 问题的提出 一个派生类对象也属于其基类,因此当程序创建一个派生类对象时,系统首先自动创建一个基类对象。 在调用派生类的构造函数构建派生类对象时,系统首先调用基类的构造函数构建基类对象。当派生类对象的生存期结束时,首先调用派生类的析构函数,然后调用基类的析构函数。 编译器在对程序编译时,首先生成基类构造函数的调用代码,然后生成派生类构造函数的调用代码。
67
! 2. 基类构造函数的调用方式 隐式调用和显式调用两种方式:
2. 基类构造函数的调用方式 隐式调用和显式调用两种方式: (1)隐式方式是指在派生类的构造函数中不指定对应的基类的构造函数,调用的是基类的默认构造函数(即含有缺省参数值或不带参数的构造函数)。 (2)显式方式是指在派生类的构造函数中指定要调用的基类构造函数,并将派生类构造函数的部分参数值传递给基类构造函数。 注意:除非基类有默认的构造函数,否则必须采用显式调用方式。 !
68
设类B是类A的派生类,则派生类B显式方式构造函数的定义形式如下:
3. 显式方式构造函数的定义 设类B是类A的派生类,则派生类B显式方式构造函数的定义形式如下: 形参声明中的部分参数,传递给基类构造函数 B::B( <形参声明> ) : A( <参数表> ) { // 类B构造函数的实现代码 } 派生类构造函数形参的名称和类型 派生类构造函数既初始化派生类的数据成员,又通过基类构造函数初始化其基类的数据成员。 参数表中参数的个数和类型要与基类某个构造函数的形参声明一致。
69
当基类有多个构造函数时,编译器根据派生类构造函数为基类构造函数提供的参数表来确定调用基类的哪一个构造函数。
注意: 当基类有多个构造函数时,编译器根据派生类构造函数为基类构造函数提供的参数表来确定调用基类的哪一个构造函数。 例 首先定义类Point,然后定义类Point的派生类Circle,再定义类Circle的派生类Cylinder。 (x, y) h r (x, y) r (x, y) Point Circle Cylinder
70
#include <iostream.h>
class Point // 定义基类Point { protected: int x, y; public: Point(int a=0, int b=0) { // 含有缺省参数值的构造函数也是默认的构造函数 x=a; y=b; cout<<"Point constructor:"<<'['<<x<<','<<y<<']'<<endl; }; ~Point() { cout<<"Point destructor:"<<'['<<x<<','<<y<<']'<<endl;
71
class Circle : public Point // 定义类Point的派生类
{ protected: int radius; public: // 显式调用基类的构造函数 Circle(int a=0, int b=0, int r=0) : Point(a, b) radius=r; cout<<"Circle constructor:"<<'['<<radius<<']'<<'[' <<x<<','<<y<<']'<<endl; }; ~Circle() { cout<<"Circle destructor:"<<'['<<radius<<']'<<'['<<x<<','<<y<<']'<<endl;
72
class Cylinder : public Circle // 定义类Circle的派生类
{ protected: int height; public: // 显式调用基类的构造函数 Cylinder(int a=0, int b=0, int r=0, int h=0) : Circle(a, b, r) height=h; cout<<"Cylinder constructor:"<<'['<<height<<']'<<'[‘ <<radius<<']‘<<'['<<x<<','<<y<<']'<<endl; }; ~Cylinder() { cout<<"Cylinder destructor:"<<'['<<height<<']'<<'[‘
73
main() { Cylinder cylinder(200, 300, 100, 400); // 调用了类Point、Circle和Cylinder的构造函数 } 程序运行结果: Point constructor:[200,300] Circle constructor:[100] [200,300] Cylinder constructor:[400] [100] [200,300] Cylinder destructor:[400] [100] [200,300] Circle destructor:[100] [200,300] Point destructor:[200,300]
74
构造函数的执行顺序: 析构函数的执行顺序: Point( ) ~ Cylinder( ) Circle( ) ~ Circle( )
当程序结束时
75
作业: P (2),3-45(上机),3-46
76
3.3.3 多重继承 1. 单继承和多重继承的概念 class A class A class B class B class C
多重继承 1. 单继承和多重继承的概念 class A class A class B class B class C class C 一个派生类同时从多个基类派生而来,即有多个直接基类 —— 多重继承 每个派生类只有一个直接基类 —— 单继承
77
设类B是类A1、A2、…、An的派生类,多重继承的派生类的定义形式为:
2. 多重继承派生类的定义 设类B是类A1、A2、…、An的派生类,多重继承的派生类的定义形式为: class <B> : [<派生方式1>] <A1>, [<派生方式2>] <A2>, … , [<派生方式3>] <An> { // 派生类新增加的成员声明列表 }; 多重继承的派生方式也有private、public和protected三种,各基类的派生方式可以不同
78
例 定义一个派生类MultiDerived,它是类BaseA和BaseB的派生类。
class BaseB // 定义基类 { protected: int b; public: void setB(int); }; class BaseA // 定义基类 { protected: int a; public: void setA(int); }; 定义两个基类
79
class MultiDerived : public BaseA , public BaseB // 定义多重继承的派生类 {
int getAB(); // 添加成员函数 }; 成员函数的实现 void BaseA::setA(int x) { a=x; } void BaseB::setB(int x) b=x; int MultiDerived::getAB() { return a+b; } 可以直接访问基类中protected属性成员
80
main() { MultiDerived md; // 声明派生类的对象 md.setA(30); // 调用基类BaseA的成员函数 md.setB(70); // 调用基类BaseB的成员函数 cout<<"a+b="<<md.getAB()<<endl; // 调用派生类MultiDerived自定义的成员函数 } 程序运行结果: a+b=100
81
? 3.3.4 虚基类 1. 多重继承中的 二义性问题 class C : public A { public: int c; };
class D : public B, public C { // 类D派生于类B和类C int d; main() D d1; d1.a=100; } 虚基类 1. 多重继承中的 二义性问题 class A { public: int a; }; class B : public A int b; 二义性错误: 编译器无法确定数据成员a是哪一个副本 ?
82
派生类D的对象中存在间接基类A的两份副本
class A D B A C class B class C class D 派生类D的对象中存在间接基类A的两份副本
83
利用作用域限定符(::)把基类的成员与下一层基类关联起来: d1.B::a=100; 或: d1.C::a=100
2. 解决方法 利用作用域限定符(::)把基类的成员与下一层基类关联起来: d1.B::a=100; 或: d1.C::a=100 从路径D→B→A继承而来 从路径D→C→A继承而来 缺点: 浪费了存储空间; 在访问基类的成员时,要求指明访问路径。 大部分情况下不需要保存基类多个相同的副本。
84
3. 使用虚基类 虚基类并不是一种新的类型的类,而是一种派生方式。采用虚基类方式定义派生类,在创建派生类的对象时,类层次结构中虚基类的成员只出现一次,即基类的一个副本被所有派生类对象所共享。 class B class C class D class A D A B C
85
√ 虚基类派生方式的定义: 采用虚基类方式定义派生类的方法是在基类的前面加上关键字virtual,而定义基类时与一般基类完全一样。
class B : virtual public A { public: int b; }; class C : virtual public A { public: int c; }; 主函数中: d1.a=100; √
86
使用虚基类派生方式的好处: 节约内存空间; 避免在多重派生类中类成员的不明确性。 作业: P (3),3-48
87
3.4 多态性和虚函数 何谓多态性? 多态性也是面向对象程序设计方法的一个重要特征,它主要表现在函数调用时实现“一种接口、多种方法”。
3.4 多态性和虚函数 何谓多态性? 多态性也是面向对象程序设计方法的一个重要特征,它主要表现在函数调用时实现“一种接口、多种方法”。 两种多态性:编译时多态性和运行时多态性。 编译时多态性:在函数名或运算符相同的情况下,编译器在编译阶段就能够根据函数参数类型的不同来确定要调用的函数 —— 通过重载实现。 运行时多态性:在函数名、函数参数和返回类型都相同的情况下,只能在程序运行时才能确定要调用的函数 —— 通过虚函数实现。 下节讲授的内容
88
3.4.1 用基类指针指向派生类对象 声明一个派生类的对象的同时也自动声明了一个基类的对象。 —— 3.3小节内容
用基类指针指向派生类对象 声明一个派生类的对象的同时也自动声明了一个基类的对象。 —— 3.3小节内容 派生类的对象可以认为是其基类的对象。C++允许一个基类对象的指针指向其派生类的对象 —— 这是实现虚函数的关键 不允许派生类对象的指针指向其基类的对象。 即使将一个基类对象的指针指向其派生类的对象,通过该指针也只能访问派生类中从基类继承的公有成员,不能访问派生类自定义的成员,除非通过强制类型转换将基类指针转换为派生类指针。 例
89
例 基类指针与派生类指针之间的相互转换。 class B : public A class A { { private: private:
例 基类指针与派生类指针之间的相互转换。 class B : public A { private: int b; public: void setB(int i) { b=i; }; void showB() { cout<<"b="<<b<<'\n'; }; } ; class A { private: int a; public: void setA(int i) { a=i; }; void showA() { cout<<"a="<<a<<'\n'; }; };
90
pa=&b; // 基类指针pa指向派生类对象b // 通过基类指针pa访问B中从基类A继承的公有成员 pa->setA(100);
void main() { A a, *pa; // pa为基类对象的指针 B b, *pb; // pb为派生类对象的指针 pa=&b; // 基类指针pa指向派生类对象b // 通过基类指针pa访问B中从基类A继承的公有成员 pa->setA(100); pa->showA(); pb=(B*)pa; // 将基类指针强制转化为派生类指针 // 不能通过基类指针pa访问派生类自己定义的成员 pb->setB(200); pb->showB(); } pb=&a pa->setB() pa->showB() 程序运行结果为: a=100 b=200
91
? 3.4.2 虚函数 1. 为什么要引入虚函数 class A { public: void Show( )
虚函数 1. 为什么要引入虚函数 class A { public: void Show( ) { cout<<"A::Show\n"; }; }; class B : public A { public: void Show( ) { cout<<"B::Show\n"; }; }; void main() { A *pa; B b; pa=&b; pa->Show(); } 调用哪一个Show() ? 如果想通过基类指针调用派生类中覆盖的成员函数,只有使用虚函数。
92
要将一个成员函数声明为虚函数,只需在定义基类时在成员函数声明的开始位置加上关键字virtual。
2. 虚函数的声明 要将一个成员函数声明为虚函数,只需在定义基类时在成员函数声明的开始位置加上关键字virtual。 class A { public: virtual void Show() { cout<<"A::show\n"; }; }; class B : public A void Show() { cout<<"B::show\n"; };
93
程序运行结果: A::Show B::Show void main() { A a, *pa; B b; pa=&a; pa->Show(); // 调用函数A::Show() pa=&b; pa->Show(); // 调用函数B::Show() } 总结:利用虚函数可以在基类和派生类中使用相同的函数名和参数类型,但定义不同的操作。这样,就为同一个类体系中所有派生类的同一类行为(其实现方法可以不同)提供了一个统一的接口。 例如,在一个图形类继承结构中,设类CShape是所有具体图形类(如矩形、三角形或圆等)的基类,则函数调用语句“pShape->Draw()”可能是绘制矩形,也可能是绘制三角形或圆。具体绘制什么图形,取决于pShape所指的对象。
94
3. 联编的概念 即将函数调用语句与函数代码相关联。 两种联编方式:静态联编和动态联编。静态联编是指编译器在编译阶段就确定了要调用的函数,即早期绑定。动态联编是指在程序执行过程中根据具体情况再确定要调用的函数,即后期绑定。 重载采用静态联编方式:虽然函数名相同,但编译器能够根据函数参数类型的不同确定要调用的函数。重载体现出一种静态多态性或编译时多态性。 当通过基类指针调用虚函数时,C++采用动态联编方式。虚函数体现出一种动态多态性或运行时多态性。
95
基于构造函数的特点,不能将构造函数定义为虚函数。
4. 构造函数、析构函数与虚函数 基于构造函数的特点,不能将构造函数定义为虚函数。 声明派生类对象时自动调用基类的构造函数 当撤消派生类的对象时,先调用派生类析构函数,然后自动调用基类析构函数,如此看来析构函数没必要定义为虚函数。但是,假如使用基类指针指向其派生类的对象,而这个派生类对象是用new运算创建的。当程序使用delete运算撤消派生类对象时,这时只调用了基类的析构函数,而没有调用派生类的析构函数。 如果使用虚析构函数,无论指针所指的对象是基类对象还是派生类对象,程序执行时都会调用对应的析构函数。 例
96
例 虚析构函数的使用。 class A { public: A() { }; // 构造函数不能是虚函数
例 虚析构函数的使用。 class A { public: A() { }; // 构造函数不能是虚函数 virtual ~A() { cout<<"A::destructor\n"; }; // 析构函数是虚函数 }; class B : public A B() { }; ~B() { cout<<"B::destructor\n"; }; // 虚析构函数
97
? 程序运行结果: B::destructor A::destructor void main() { A *pA=new B;
// delete pA; // 先调用派生类B的构造函数,再调用基类A的构造函数 } 如果析构函数不是虚函数,则得不到下面的运行结果。请读者思考会是什么结果 ? 程序运行结果: B::destructor A::destructor 总结:由于使用了虚析构函数,当撤消pA所指派生类B的对象时,首先调用派生类B的析构函数,然后再调用基类A的析构函数。
98
3.4.3 抽象类和纯虚函数 1. 何谓抽象类 抽象类是类的一些行为(成员函数)没有给出具体定义的类,即纯粹的一种抽象。
抽象类和纯虚函数 1. 何谓抽象类 抽象类是类的一些行为(成员函数)没有给出具体定义的类,即纯粹的一种抽象。 抽象类只能用于类的继承,其本身不能用来创建对象,抽象类又称为抽象基类。 抽象基类只提供了一个框架,仅仅起着一个统一接口的作用,而很多具体的功能由派生出来的类去实现。 虽然不能声明抽象类的对象,但可以声明指向抽象类的指针。 在一般的类库中都使用了抽象基类,如类CObject就是微软基础类库MFC的抽象基类。
99
一个类如果满足以下两个条件之一就是抽象类: 至少有一个成员函数不定义具体的实现; 定义了一个protected属性的构造函数或析构函数。
2. 抽象类的定义 一个类如果满足以下两个条件之一就是抽象类: 至少有一个成员函数不定义具体的实现; 定义了一个protected属性的构造函数或析构函数。 纯虚函数 3. 纯虚函数 不定义具体实现的成员函数称为纯虚函数。纯虚函数不能被调用,仅起提供一个统一接口的作用。 纯虚函数的声明: virtual <数据类型> <成员函数名>(<形参表>)= 0 ; 当基类是抽象类时,只有在派生类中重新定义基类中的所有纯虚函数,该派生类才不会再成为抽象类。
100
例 纯虚函数和抽象类的使用。 // 定义具体的派生类 // 定义抽象基类 class CCircle : public CShape
例 纯虚函数和抽象类的使用。 // 定义具体的派生类 class CCircle : public CShape { public: CCircle(double x):CShape(x) { }; // 重新定义虚函数 void Area() { s= *r*r; }; }; // 定义抽象基类 class CShape { public: double r ; double s ; CShape(double x) { r=x; } // 声明纯虚函数 virtual void Area()=0; };
101
作业: P111-114 3-38(1)、(2),3- 49,3-52(上机) main() {
CCircle circle(48.52); circle.Area(); cout<<"Area="<<circle.s<<endl; } 作业: P (1)、(2),3- 49,3-52(上机)
102
3.5 重载 重载是C++提供的一个新特性。C++重载分为函数重载和运算符重载,这两种重载的实质是一样的,因为进行运算可以理解为是调用一个函数。 Add(x, y) Add(x, y, z) x + y X + Y 通过使用重载机制,可以对一个函数名(或运算符)定义多个函数(或运算功能),只不过要求这些函数的参数(或参加运算的操作数)的类型有所不同。 重载使C++程序具有更好的可扩充性。
103
3.5.1 函数重载 函数重载:指一组功能类似但函数参数类型(个数)不同的函数可以共用一个函数名。
函数重载 函数重载:指一组功能类似但函数参数类型(个数)不同的函数可以共用一个函数名。 当C++编译器遇到重载函数的调用语句时,它能够根据不同的参数类型或不同的参数个数选择一个合适的函数。 例 通过函数参数类型的不同实现函数重载。 int abs(int val) { return val<0 ? –val : val; } float abs(float val) { return (val<0) ? –val : val; }
104
不能利用函数返回类型的不同进行函数重载。因为在没有确定调用的是哪个函数之前,不知道函数的返回类型。
main() { int i=100; cout<<abs(i)<<endl; // int型 float f= F; cout<<abs(f)<<endl; // float型 } 在程序中,求绝对值函数的名称相同,但参数类型不同,这时C++编译器自动按参数表的不同来分别联编不同的求绝对值函数。 不能利用函数返回类型的不同进行函数重载。因为在没有确定调用的是哪个函数之前,不知道函数的返回类型。 long abc(int); float abc(int);
105
同样,不能利用引用进行函数重载: void fun(int&); void fun(int); 因为对于下面的调用语句,编译器无法决定调用哪一个函数: fun(i); // i是一个整型变量 从上面可以看出,一般函数的重载使C++程序具有更好的可扩充性。此外,类的成员函数也可以重载,特别是构造函数的重载给C++程序设计带来很大的灵活性。
106
例 构造函数的重载。 class Box { private: int height, width, depth; public: Box() { height=0; width=0; depth=0; } // 避免给成员变量赋不安全的值 Box(int ht, int wd, int dp) // 重载构造函数 { height=ht; width=wd; depth=dp; } int Volume() { return height*width*depth; } };
107
cout<<"Volume1="<<box1.Volume()
void main() { Box box1; Box box2(10, 15, 20); cout<<"Volume1="<<box1.Volume() <<", Volume2="<<box2.Volume()<<endl; } 程序运行结果: Volume1=0,Volume2=3000
108
3.5.2 运算符重载 运算符重载:指对于不同数据类型的操作数,同一个运算符所代表的运算功能可以不同。
运算符重载 运算符重载:指对于不同数据类型的操作数,同一个运算符所代表的运算功能可以不同。 一个运算符定义了一种操作,一个函数也定义了一种操作,其本质是相同的,当程序遇到运算符时会自动调用相应的运算符函数。 虽然重载运算符完成的功能都能够用一个真正的成员函数来实现,但使用运算符重载使程序更易于理解。 与函数重载类似,编译器是根据参加运算的操作数的类型来识别不同的运算。
109
我们可以将字符串operator+看成一个运算符函数名,这些同名的运算符函数根据不同类型的操作数完成不同的加法运算。
例: 对于表达式:10+20 编译器把它看成如下函数调用: int operator+(10, 20); 对于表达式: float operator+(10.0, 20.0); 参加运算的数是整数 参加运算的数是单精度实型数 我们可以将字符串operator+看成一个运算符函数名,这些同名的运算符函数根据不同类型的操作数完成不同的加法运算。
110
重载一个运算符,就是编写一个运算符函数,重载运算符(函数)的原型为:
重载运算符的形式: 重载一个运算符,就是编写一个运算符函数,重载运算符(函数)的原型为: <数据类型> operator<运算符>(<形参表>); 运算结果的类型 参加运算的操作数 要重载的运算符 例 定义复数类型,重载运算符“+”。 例如:c3=c1+c2
111
class Complex { public: // 公有成员,以便运算符函数(非成员函数)访问 float r; // 实部 float i; // 虚部 public: Complex(float x=0, float y=0) { r=x; i=y; } }; Complex operator+(Complex c1 , Complex c2) Complex temp; temp.r=c1.r+c2.r; temp.i=c1.i+c2.i; return temp; } 利用普通函数重载运算符
112
void main() { Complex complex1(3. 34f, 4. 8f), complex2(12. 8f, 5
void main() { Complex complex1(3.34f, 4.8f), complex2(12.8f, 5.2f); Complex complex; complex=complex1+complex2; // 进行两个复数的相加运算 cout<<complex.r<<'+'<<complex.i<<'i'<<endl; } 说明: 本例采用普通函数的形式重载运算符。 可以采用成员函数的形式重载运算符。并且如果运算符函数要求直接访问类的非公有成员时,运算符函数不能定义为非成员函数,除非将它声明为该类的友元函数。
113
例 利用成员函数进行运算符重载。 class Complex { private: // 私有成员能够在成员函数(运算符函数)中访问
例 利用成员函数进行运算符重载。 class Complex { private: // 私有成员能够在成员函数(运算符函数)中访问 float r; // 实部 float i; // 虚部 public: Complex(float x=0, float y=0) { r=x; i=y; }; Complex operator+(Complex); void Display() { cout<<r<<'+'<<i<<'i'<<endl; }; // 输出实部和虚部 };
114
Complex Complex::operator+(Complex other) { Complex temp;
temp.r=this->r+other.r; temp.i=this->i+other.i; // 可以省略this指针 return temp; } 利用成员函数重载运算符 void main() { Complex complex1(3.34f, 4.8f), complex2(12.8f, 5.2f); Complex complex; complex=complex1+complex2; complex.Display(); }
115
说明: 当利用非成员函数重载双目运算符时,运算符函数的第一个参数代表运算符左边的操作数,运算符函数第二个参数代表运算符右边的操作数。 当利用成员函数重载双目运算符时,运算符左边的操作数就是对象本身,不能再将它作为运算符函数的参数,运算符函数只需要一个函数参数。 运算符重载与函数重载的区别: 同一个重载运算符的参数个数是相同的。 不能定义新的运算符,只能重载现有的运算符。 运算符重载后仍然保持原来的优先级和结合性。
116
作业: P (3)、(4),3-55,3-57(上机)
117
3.6 C++模板 什么是模板? 模板是一个将数据类型参数化的工具,它把“一般性的算法”和其“对数据类型的实现”区分开来。
模板分为函数模板和类模板两种。 采用模板方式定义函数或类时不确定某些函数参数或数据成员的类型,而将它们的数据类型作为模板的参数。在使用模板时根据实参的数据类型确定模板参数(数据类型)的数据类型。 模板提高了软件的重用性。当函数参数或数据成员可以是多种类型而函数或类所实现的功能又相同时,使用C++模板在很大程度上简化了编程。
118
函数模板 1. 函数重载与函数摸板 函数模板扩展了函数重载:利用函数重载可以让多个函数共享一个函数名,只要所重载的函数的参数类型必须有所不同。但是,由于参数的类型不一样,虽然这些函数所完成的功能完全一样,也必须为每一个重载函数编写代码。 一个函数模板可用来生成多个功能相同但参数和返回值的类型不同的函数。 函数工厂 2. 什么是函数模板 函数模板是一种不指定某些参数的数据类型的函数,在函数模板被调用时根据实际参数的类型决定这些函数模板参数的类型。
119
以下定义了一个可对任何类型变量进行操作(求绝对值)的函数模板:
3. 函数模板的定义举例 以下定义了一个可对任何类型变量进行操作(求绝对值)的函数模板: template < class T > T abs( T val ) { return val<0 ? -val : val; } 类型参数T作用: 定义函数的参数和返回值; 在函数体中用来声明变量。 模板定义以关键字template开头; 关键字class后面的标识符T由用户自定义,称为类型参数,是函数模板abs()中没有确定数据类型的参数val的类型。 模板定义的下面是模板函数abs()的定义。
120
4. 含义多个类型参数的函数模板 定义函数模板时可以使用多个类型参数,每个类型参数前面只需加上关键字class,用逗号分隔: template <class T1,class T2,class T3> 例如: template <class T1,class T2> T1 Max( T1 x, T2 y) { return x>=y ? x : (T1)y; }
121
5. 函数模板的实例化 函数模板将数据类型参数化,这使得在程序中能够用不同类型的参数调用同一个函数(模板函数)。在调用模板函数时即创建函数模板的一个实例,这个过程称为函数模板的实例化。 函数模板的实例化由编译器完成:编译时函数模板本身并不产生可执行代码,只有在函数模板被实例化时,编译器才按照实参的数据类型进行类型参数的替代,生成新的函数。 编译器 函数模板 函 数
122
例 函数模板的定义和使用。 #include <iostream
例 函数模板的定义和使用。 #include <iostream.h> template <class T> // 定义模板 T abs(T val) // 定义模板函数 { return val<0 ? -val : val; } void main() int i=100; cout<<abs(i)<<endl; long l=-12345L; cout<<abs(l)<<endl; float f= F; cout<<abs(f)<<endl; 类型参数T 替换为int 类型参数T 替换为long 类型参数T 替换为float
123
类模板 1. 类模板与函数模板 函数模板只能用于定义非成员函数,它是模板的一个特例。类模板实际上是函数模板的推广,它是一种不确定类的某些数据成员的类型或成员函数的参数及返回值的类型的类。 2. 类模板与类 类是对问题的抽象,而类模板是对类的抽象,即更高层次上的抽象。 类模板称为带参数(或参数化)的类,也称为类工厂,它可用来生成多个功能相同而某些数据成员的类型不同或成员函数的参数及返回值的类型不同的类。
124
3. 类模板的定义 为了起到模板的作用,与函数模板一样,定义一个类模板时必须将某些数据类型作为类模板的类型参数。 模板类的实现代码与普通类没有本质上的区别,只是在定义其成员时要用到类模板的类型参数。 定义举例
125
例如,以下定义了含有一个类型参数的类模板:
template < class T > class MyTemClass { private: T x; // 类型参数T用于声明数据成员 public: void SetX( T a ) { x=a; }; // 类型参数T用于声明成员函数的参数 T GetX( ) { return x; }; // 类型参数T用于声明成员函数的返回值 };
126
注意: 如果在模板类的外部定义模板类的成员函数,必须采用如下形式: template < class T > // 不能省略模板声明 void MyTemClass < T > :: SetX( T a ) { x=a; } 编译时由编译器完成 4. 类模板的实例化 与函数模板不同,类模板不是通过调用函数时实参的数据类型来确定类型参数具体所代表的类型,而是通过在使用模板类声明对象时所给出的实际数据类型确定类型参数。
127
例如,以下使用类模板声明了一个类型参数为int的模板类的对象: MyTemClass < int > intObject;
对于上面的对象声明: 编译器首先用int替代模板类定义中的类型参数T,生成一个所有数据类型已确定的类class; 然后再利用这个类创建对象intObject 。 5. 含有多个参数类模板的定义 template < class T1,int i,class T2 > class MyTemClass { } 使用
128
例如,声明模板类的对象应采用如下形式: MyTemClass < int, 100, float > MyObject ;
例 使用多个类型参数的类模板。 template <class T1, class T2> // 使用2个类型参数 class MyTemClass // 定义模板类 { private: T1 x; T2 y; public: MyTemClass(T1 a, T2 b) { x=a; y=b; }; void ShowMax() { cout<<"MaxMember="<<(x>=y?x:y)<<endl; }; };
129
作业: P115 3-63,3-65 void main() { int a=100; float b=123.45F;
MyTemClass< int, float > mt(a, b); // 声明模板类的对象 mt.ShowMax(); } 类模板 的实例化 作业: P ,3-65
130
3.7 Microsoft Visual C++的语法扩充
经过多年的发展,C++有很多版本,微软公司就推出了不少C++编译器。微软公司最早推出的C++编译器是Microsoft C++(1.0版到8.0版)。1993年,微软推出了第一个可视化编译器即Visual C++ 1.0,以后不断推出它的新版本,2001年推出了Visual C++ 7.0。1998年,美国国家标准化协会ANSI和国际标准组织ISO联合正式制定了C++国际标准。Visual C++编译器除了遵循一般的C++标准,还结合自己的开发环境、工具和MFC类对C++语法进行了一些扩充。
131
Visual C++自定义数据类型 数据类型 意义 FAR 对应于far NEAR 对应于near CONST 对应于const BOOL 布尔类型,值为TRUE(真)或FALSE(假) UINT 32位无符号整形,对应于unsigned int BYTE 8位无符号整形,对应于unsigned char WORD 16位无符号整形,对应于unsigned short int DWORD 32位无符号长整形,对应于unsigned long int SHORT 短整形 LONG 32位长整形,对应于long LONGLONG 64位长整形 FLOAT 浮点型,对应于float CHAR Windows字符 VOID 任意类型
132
LPCSTR 32位字符串指针,指向一个常数字符串 LPSTR 32位字符串指针 LPVOID 32位指针,指向一个未定义类型的数据 LPARAM 32位消息参数,作为窗口函数或回调函数的参数 LPRESULT 32位数值,作为窗口函数或回调函数的返回值 LPCRECT 32位指针,指向一个RECT结构的常量 PROC 指向回调函数的指针 WNDPROC 32位指针,指向一个窗口函数 WPARAM 16位或32位数值,作为窗口函数或回调函数的 参数 HANDLE 对象句柄,其它还有HPEN、HWND、 HCURSOR、HDC等 CONST 常量 COLORREF 32位数值,代表一个颜色值
133
Visual C++运行库 运行库(Run-Time Library)是存放一些常用函数执行代码的文件库,它由LIB文件组成,进行链接时将需要的LIB文件与程序链接在一起。 Visual C++ 6 可以使用的运行库包括C运行库、标准C++库(存放新的iostream函数和其它标准函数)和旧的iostream库,但标准C++库和旧的iostream库是不兼容的,因此,链接时除了链接C运行库,只能链接标准C++库或旧的iostream库中的一个库。
134
根据程序中使用的头文件就可以确定是链接标准C++库还是旧的iostream库。
以下文件包含指令包含一个标准C++库头文件,在编译时Visual C++将自动链接一个标准C++库: #include < iostream> 以下文件包含指令包含一个旧的iostream库头文件(本章的例子都是使用该库),在编译时Visual C++将自动链接一个旧的iostream库: #include < iostream .h > 使用标准C++库(新的isotherm函数)的例子: #include <iostream> std::cout<< “AAAAAA\n ”; // 输出
135
运行时类型识别RTTI 运行时类型识别RTTI(Run-Time Type Information)是这样一种机制:在程序运行时可以确定对象的类型。RTTI主要有以下两种应用: (1)使用dynamic_cast运算符检查一个基类指针是否指向其派生类对象; (2)使用typeid运算符识别指针所指类型。
136
1. dynamic_cast运算符语法结构如下:
dynamic_cast < Type-ID > ( EXP ) 该运算符的功能是将EXP转换成Type-ID类型,要求Type-ID必须是类的指针、引用或void*类型,EXP必须是一个具体的指针或引用。如果EXP是Type-ID类型的基类指针,程序运行时该运算符检查EXP是否指向Type-ID类型(派生类)的对象,如果是,运算结果是该Type-ID类的对象的指针,否则为 NULL(空)。 例 使用dynamic_cast运算符检查一个基类指针是否指向其派生类对象。
137
#include <iostream
#include <iostream.h> class A { public: // 多态性类(使用虚函数) // 才能使用dynamic_cast运算符 virtual void f1() { }; }; class B : public A void f1() { };
138
void main() { A. pAA=new A; // 基类指针pAA指向基类类对象 A
void main() { A *pAA=new A; // 基类指针pAA指向基类类对象 A *pAB=new B; // 基类指针pAB指向派生类对象 B *pB1=dynamic_cast<B*>(pAA); // pB1==NULL B *pB2=dynamic_cast<B*>(pAB); // pB2!=NULL cout<<"pB1="<<pB1<<", pB2="<<pB2<<endl; } 要使用RTTI,应对Visual C++ IDE进行如下设置:执行菜单命令 “Project→Settings→C/C++ →Category→C++ Language”,选择Enable Run-Time Type Information项。
139
2. typeid 运算: 利用typeid运算符不仅可以确定一个对象是否属于类继承层次中的某个类,还可以识别程序运行时一个对象的真实类型。
CMyClass my; cout<<“Class Name of my: ”<<typeid(my).name()<<endl; int i=12345; if(typeid(i)!=typeid(my)) cout<<"The type of i is not CMyClass !"<<endl; if(typeid(i)!=typeid(float)) cout<<"The type of i is not float !"<<endl; if(typeid(i) = = typeid(123)) cout<<"The type of i is int !"<<endl;
140
运行时类型识别RTTI机制在较先进的编译器如Visual C++和Borland C++中才得到支持,但微软基础类MFC并未使用Visual C++所支持的RTTI,它有自己的一套办法。MFC提供了有关运行时类型识别的宏,其详细内容请参看第6章的6.5和6.6节。 作业: P ,3-70
141
编程规范 为了阅读理解源程序,Visual C++源程序中变量的取名一般采用匈牙利表示法则。该法则要求每一个变量名都有一个前缀,用于表示变量的类型,后面是代表变量含义的一串字符。 例如:前缀n表示整形变量,前缀sz表示以0结束的字符串变量,前缀lp表示指针变量。这些前缀还可以组合起来使用。前缀一般是小写字母,前缀后的第一个字符要大写。如:nWidth表示一个整形变量,lpszMyname表示一个字符串的指针。 在给类和成员变量取名时也使用特定的前缀,如CView是一个类(视图类),m_xStart是一个类的整形成员变量(起点的X坐标)。
142
Visual C++中的前缀及说明 前缀 表示的类型 例 子 a 数组变量 aScore[50] b 布尔变量 bFlag,bIsEnd c 字符变量 cSex n,i 整形变量 nWidth,iNum x、y 无符号整形变量(X、Y坐标) xStart,yPos s 字符串变量(不常使用) sMyName sz 以0结束的字符串变量 szMyName p 指针变量 pszString,pMyDlg lp 长指针变量 lpszMyname h 句柄 hWnd,hPen,hDlg fn 函数 FnCallBack() m_ 类的成员变量 m_xStart C 类和结构 CDialog,CView,CMysdiApp,CRuntimeClass Afx,afx,AFX 应用程序框架 AfxGetApp(),afx_msg ID*_ 资源标识 ID_,IDD_,IDC_,IDB_,IDI_
Similar presentations