第11章 类的继承和派生 继承是面向对象程序设计方法的四个基本特征之一,是程序代码可重用性的具体体现。 第11章 类的继承和派生 继承 继承是面向对象程序设计方法的四个基本特征之一,是程序代码可重用性的具体体现。 在C++面向对象程序设计中,所谓类的继承就是利用现有的类创建一个新的类。新类继承了现有类的属性和行为。 为了使新类具有自己所需的功能,它可以扩充和完善现有类的属性和行为,使之更具体。 微软基础类MFC就是通过类的继承来体现类的可重用性和可扩充性。 发扬
11.1 基类和派生类 1. 问题的提出 在现实世界中,一类事物的对象常常也属于另一类事物。 11.1 基类和派生类 1. 问题的提出 在现实世界中,一类事物的对象常常也属于另一类事物。 在面向对象程序设计方法中,一个类的对象也常常是另一个类的对象,即一个类具有了另一个类的属性和方法。 在定义一个类时,根据类的继承性,我们能够且应尽可能地利用现有的类来定制新的类,而不必重新设计新的类。
在继承关系中,新定义的类称为被继承类的派生类或子类,而被继承的类称为新定义类的基类或父类。派生类继承了基类的所有成员。 2. 基类和派生类的概念 在继承关系中,新定义的类称为被继承类的派生类或子类,而被继承的类称为新定义类的基类或父类。派生类继承了基类的所有成员。 一个派生类也可以作为另一个派生类的基类。 3. 派生类的定义 class <派生类名> : [<派生方式>] <基类名> { . . . // 派生类新增加的成员声明列表 };
说明: 派生方式决定了基类的成员在派生类中的访问权限。派生方式共有三种:public、private和protected(缺省值为private)。 虽然派生类继承了基类的所有成员,但为了不破坏基类的封装性,无论采用哪种派生方式,基类的私有成员在派生类中都是不可见的,即不允许在派生类的成员函数中访问基类的私有成员。
三种派生方式的区别: 采用public派生,基类成员的访问权限在派生类中保持不变,即基类所有的公有或保护成员在派生类中仍为公有或保护成员。public派生最常用。 (1) 可以在派生类的成员函数中访问基类的非私有成员; (2) 可通过派生类的对象直接访问基类的公有成员。 采用private私有派生,基类所有的公有和保护成员在派生类中都成为私有成员,只允许在派生类的成员函数中访问基类的非私有成员。private派生很少使用。 采用protected保护派生,基类所有的公有和保护成员在派生类中都成为保护成员,只允许在派生类的成员函数和该派生类的派生类的成员函数中访问基类的非私有成员。
例 定义类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坐标 };
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坐标 };
程序运行结果: 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 ="<<c.getUpperLeftY()<<endl; } 公有派生类的对象可以直接访问基类Point的公有成员 程序运行结果: X=200,Y=250,Radius=100 UpperLeft X=100,UpperLeft Y=350
说明: 派生类Circle通过public派生方式继承了基类Point的所有成员(除私有成员外所有成员的访问权限不变),同时还定义了自己的成员变量和成员函数。 若将类Circle的派生方式改为private或protected,则下述语句是非法的:c.setPoint(200, 250); 无论哪种派生方式,派生类都继承了基类的所有成员,包括私有成员。我们虽然不能在派生类Circle中直接访问私有数据成员x和y,但可以通过继承的公有成员函数getX()、getY()和setPoint()访问或设置它们。 ! 容易 混淆
最后一个问题: ? 利用类继承定义类可能带来一个问题:派生类会继承它不需要的基类中的数据成员和成员函数,这时,基类中不适合于派生类的成员可以在派生类中重新加以定义。 例 派生类成员函数对基类成员函数的覆盖。 #include <iostream.h> class A { public: void Show( ) { cout<<"A::Show\n"; }; };
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();};
? 程序运行结果: A::Show B::Show 请问:如果在派生类B中没有对成员函数Show()重新进行定义,程序运行结果如何? 从本例可以看出,虽然派生类继承了基类的所有成员函数,但如果派生类某个成员函数的名称和参数与基类成员函数一致(即在派生类中对该成员函数重新进行了定义),则在派生类中调用的成员函数是派生类的成员函数。 请问:如果在派生类B中没有对成员函数Show()重新进行定义,程序运行结果如何? ?
为什么我们经常在现有类的基础上采用继承的方法来定制新类,而不通过直接修改现有类来设计自己的类?除了代码重用的优越性,其主要原因是可能得不到基类的实现源码。 重要性! 在利用微软基础类MFC派生自己的类时,我们只需要MFC类声明的头文件(利用#include指令将头文件包含)和含有成员函数目标代码的OBJ文件,并不需要整个MFC类库的实现源码。
11.2 基类和派生类的构造函数 1. 问题的提出 一个派生类对象也属于其基类,因此当程序创建一个派生类对象时,系统首先自动创建一个基类对象。 在调用派生类的构造函数构建派生类对象时,系统首先调用基类的构造函数构建基类对象。当派生类对象的生存期结束时,首先调用派生类的析构函数,然后调用基类的析构函数。 编译器在对程序编译时,首先生成基类构造函数的调用代码,然后生成派生类构造函数的调用代码。
! 2. 基类构造函数的调用方式 隐式调用和显式调用两种方式: 2. 基类构造函数的调用方式 隐式调用和显式调用两种方式: (1)隐式方式是指在派生类的构造函数中不指定对应的基类的构造函数,调用的是基类的默认构造函数(即含有缺省参数值或不带参数的构造函数)。 (2)显式方式是指在派生类的构造函数中指定要调用的基类构造函数,并将派生类构造函数的部分参数值传递给基类构造函数。 注意:除非基类有默认的构造函数,否则必须采用显式调用方式。 !
设类B是类A的派生类,则派生类B显式方式构造函数的定义形式如下: 6. 显式方式构造函数的定义 设类B是类A的派生类,则派生类B显式方式构造函数的定义形式如下: 形参声明中的部分参数,传递给基类构造函数 B::B( <形参声明> ) : A( <参数表> ) { . . . // 类B构造函数的实现代码 } 派生类构造函数形参的名称和类型 派生类构造函数既初始化派生类的数据成员,又通过基类构造函数初始化其基类的数据成员。 参数表中参数的个数和类型要与基类某个构造函数的形参声明一致。
当基类有多个构造函数时,编译器根据派生类构造函数为基类构造函数提供的参数表来确定调用基类的哪一个构造函数。 注意: 当基类有多个构造函数时,编译器根据派生类构造函数为基类构造函数提供的参数表来确定调用基类的哪一个构造函数。 例 首先定义类Point,然后定义类Point的派生类Circle,再定义类Circle的派生类Cylinder。 (x, y) h r (x, y) r (x, y) Point Circle Cylinder
#include <iostream.h> class Point // 定义基类Point { protected: int x, y; public: Point(int a=0, int b=0) // 含有缺省参数值的构造函数也是默认的构造函数 x=a; y=b; cout<<"Pointconstructor:"<<'['<<x<<','<<y<<']'<<endl; }; ~Point() cout<<"Pointdestructor:"<<'['<<x<<','<<y<<']'<<endl;
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<<"Circledestructor:"<<'['<<radius<<']'<<'['<<x<<','<<y<<']'<<endl;
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<<']'<<'[‘
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]
构造函数的执行顺序: 析构函数的执行顺序: Point( ) ~ Cylinder( ) Circle( ) ~ Circle( ) 当程序结束时
6.6.3 多重继承 1. 单继承和多重继承的概念 class A class A class B class B class C 6.6.3 多重继承 1. 单继承和多重继承的概念 class A class A class B class B class C class C 一个派生类同时从多个基类派生而来,即有多个直接基类 —— 多重继承 每个派生类只有一个直接基类 —— 单继承
设类B是类A1、A2、…、An的派生类,多重继承的派生类的定义形式为: 2. 多重继承派生类的定义 设类B是类A1、A2、…、An的派生类,多重继承的派生类的定义形式为: class <B> : [<派生方式1>] <A1>, [<派生方式2>] <A2>, … , [<派生方式3>] <An> { . . . // 派生类新增加的成员声明列表 }; 多重继承的派生方式也有private、public和protected三种,各基类的派生方式可以不同
例 定义一个派生类MultiDerived,它是类BaseA和BaseB的派生类。 class BaseB // 定义基类 { protected: int b; public: void setB(int); }; class BaseA // 定义基类 { protected: int a; public: void setA(int); }; 定义两个基类
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属性成员
main() { MultiDerived md; // 声明派生类的对象 md.setA(30); // 调用基类BaseA的成员函数 md.setB(70); // 调用基类BaseB的成员函数 cout<<"a+b="<<md.getAB()<<endl; // 调用派生类MultiDerived自定义的成员函数 } 程序运行结果: a+b=100
? 6.6.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; } 6.6.4 虚基类 1. 多重继承中的 二义性问题 class A { public: int a; }; class B : public A int b; 二义性错误: 编译器无法确定数据成员a是哪一个副本 ?
派生类D的对象中存在间接基类A的两份副本 class A D B A C class B class C class D 派生类D的对象中存在间接基类A的两份副本
利用作用域限定符(::)把基类的成员与下一层基类关联起来: d1.B::a=100; 或: d1.C::a=100 2. 解决方法 利用作用域限定符(::)把基类的成员与下一层基类关联起来: d1.B::a=100; 或: d1.C::a=100 从路径D→B→A继承而来 从路径D→C→A继承而来 缺点: 浪费了存储空间; 在访问基类的成员时,要求指明访问路径。 大部分情况下不需要保存基类多个相同的副本。
6. 使用虚基类 虚基类并不是一种新的类型的类,而是一种派生方式。采用虚基类方式定义派生类,在创建派生类的对象时,类层次结构中虚基类的成员只出现一次,即基类的一个副本被所有派生类对象所共享。 class B class C class D class A D A B C
√ 虚基类派生方式的定义: 采用虚基类方式定义派生类的方法是在基类的前面加上关键字virtual,而定义基类时与一般基类完全一样。 class B : virtual public A { public: int b; }; class C : virtual public A { public: int c; }; 主函数中: d1.a=100; √
使用虚基类派生方式的好处: 节约内存空间; 避免在多重派生类中类成员的不明确性。
6.4 多态性和虚函数 何谓多态性? 多态性也是面向对象程序设计方法的一个重要特征,它主要表现在函数调用时实现“一种接口、多种方法”。 6.4 多态性和虚函数 何谓多态性? 多态性也是面向对象程序设计方法的一个重要特征,它主要表现在函数调用时实现“一种接口、多种方法”。 两种多态性:编译时多态性和运行时多态性。 编译时多态性:在函数名或运算符相同的情况下,编译器在编译阶段就能够根据函数参数类型的不同来确定要调用的函数 —— 通过重载实现。 运行时多态性:在函数名、函数参数和返回类型都相同的情况下,只能在程序运行时才能确定要调用的函数 —— 通过虚函数实现。
6.4.1 用基类指针指向派生类对象 声明一个派生类的对象的同时也自动声明了一个基类的对象。 —— 6.3小节内容 6.4.1 用基类指针指向派生类对象 声明一个派生类的对象的同时也自动声明了一个基类的对象。 —— 6.3小节内容 派生类的对象可以认为是其基类的对象。C++允许一个基类对象的指针指向其派生类的对象 —— 这是实现虚函数的关键 不允许派生类对象的指针指向其基类的对象。 即使将一个基类对象的指针指向其派生类的对象,通过该指针也只能访问派生类中从基类继承的公有成员,不能访问派生类自定义的成员,除非通过强制类型转换将基类指针转换为派生类指针。 例
例 基类指针与派生类指针之间的相互转换。 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'; }; };
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
? 6.4.2 虚函数 1. 为什么要引入虚函数 class A { public: void Show( ) 6.4.2 虚函数 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() ? 如果想通过基类指针调用派生类中覆盖的成员函数,只有使用虚函数。
要将一个成员函数声明为虚函数,只需在定义基类时在成员函数声明的开始位置加上关键字virtual。 2. 虚函数的声明 要将一个成员函数声明为虚函数,只需在定义基类时在成员函数声明的开始位置加上关键字virtual。 class A { public: virtual void Show() { cout<<"A::show\n"; }; }; class B : public A void Show() { cout<<"B::show\n"; };
程序运行结果: 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所指的对象。
6. 联编的概念 即将函数调用语句与函数代码相关联。 两种联编方式:静态联编和动态联编。静态联编是指编译器在编译阶段就确定了要调用的函数,即早期绑定。动态联编是指在程序执行过程中根据具体情况再确定要调用的函数,即后期绑定。 重载采用静态联编方式:虽然函数名相同,但编译器能够根据函数参数类型的不同确定要调用的函数。重载体现出一种静态多态性或编译时多态性。 当通过基类指针调用虚函数时,C++采用动态联编方式。虚函数体现出一种动态多态性或运行时多态性。
基于构造函数的特点,不能将构造函数定义为虚函数。 4. 构造函数、析构函数与虚函数 基于构造函数的特点,不能将构造函数定义为虚函数。 声明派生类对象时自动调用基类的构造函数 当撤消派生类的对象时,先调用派生类析构函数,然后自动调用基类析构函数,如此看来析构函数没必要定义为虚函数。但是,假如使用基类指针指向其派生类的对象,而这个派生类对象是用new运算创建的。当程序使用delete运算撤消派生类对象时,这时只调用了基类的析构函数,而没有调用派生类的析构函数。 如果使用虚析构函数,无论指针所指的对象是基类对象还是派生类对象,程序执行时都会调用对应的析构函数。 例
例 虚析构函数的使用。 class A { public: A() { }; // 构造函数不能是虚函数 例 虚析构函数的使用。 class A { public: A() { }; // 构造函数不能是虚函数 virtual ~A() { cout<<"A::destructor\n"; }; // 析构函数是虚函数 }; class B : public A B() { }; ~B() { cout<<"B::destructor\n"; }; // 虚析构函数
? 程序运行结果: B::destructor A::destructor void main() { A *pA=new B; // . . . . . . delete pA; // 先调用派生类B的构造函数,再调用基类A的构造函数 } 如果析构函数不是虚函数,则得不到下面的运行结果。请读者思考会是什么结果 ? 程序运行结果: B::destructor A::destructor 总结:由于使用了虚析构函数,当撤消pA所指派生类B的对象时,首先调用派生类B的析构函数,然后再调用基类A的析构函数。
6.4.3 抽象类和纯虚函数 1. 何谓抽象类 抽象类是类的一些行为(成员函数)没有给出具体定义的类,即纯粹的一种抽象。 6.4.3 抽象类和纯虚函数 1. 何谓抽象类 抽象类是类的一些行为(成员函数)没有给出具体定义的类,即纯粹的一种抽象。 抽象类只能用于类的继承,其本身不能用来创建对象,抽象类又称为抽象基类。 抽象基类只提供了一个框架,仅仅起着一个统一接口的作用,而很多具体的功能由派生出来的类去实现。 虽然不能声明抽象类的对象,但可以声明指向抽象类的指针。 在一般的类库中都使用了抽象基类,如类CObject就是微软基础类库MFC的抽象基类。
一个类如果满足以下两个条件之一就是抽象类: 至少有一个成员函数不定义具体的实现; 定义了一个protected属性的构造函数或析构函数。 2. 抽象类的定义 一个类如果满足以下两个条件之一就是抽象类: 至少有一个成员函数不定义具体的实现; 定义了一个protected属性的构造函数或析构函数。 纯虚函数 6. 纯虚函数 不定义具体实现的成员函数称为纯虚函数。纯虚函数不能被调用,仅起提供一个统一接口的作用。 纯虚函数的声明: virtual <数据类型> <成员函数名>(<形参表>)= 0 ; 当基类是抽象类时,只有在派生类中重新定义基类中的所有纯虚函数,该派生类才不会再成为抽象类。
例 纯虚函数和抽象类的使用。 // 定义具体的派生类 // 定义抽象基类 class CCircle : public CShape 例 纯虚函数和抽象类的使用。 // 定义具体的派生类 class CCircle : public CShape { public: CCircle(double x):CShape(x) { }; // 重新定义虚函数 void Area() { s=6.14159*r*r; }; }; // 定义抽象基类 class CShape { public: double r ; double s ; CShape(double x) { r=x; } // 声明纯虚函数 virtual void Area()=0; };
main() { CCircle circle(48.52); circle.Area(); cout<<"Area="<<circle.s<<endl; }
第10章 重载 重载是C++提供的一个新特性。C++重载分为函数重载和运算符重载,这两种重载的实质是一样的,因为进行运算可以理解为是调用一个函数。 Add(x, y) Add(x, y, z) x + y X + Y 通过使用重载机制,可以对一个函数名(或运算符)定义多个函数(或运算功能),只不过要求这些函数的参数(或参加运算的操作数)的类型有所不同。 重载使C++程序具有更好的可扩充性。
10.1 函数重载 函数重载:指一组功能类似但函数参数类型(个数)不同的函数可以共用一个函数名。 10.1 函数重载 函数重载:指一组功能类似但函数参数类型(个数)不同的函数可以共用一个函数名。 当C++编译器遇到重载函数的调用语句时,它能够根据不同的参数类型或不同的参数个数选择一个合适的函数。 例 通过函数参数类型的不同实现函数重载。 int abs(int val) { return val<0 ? –val : val; } float abs(float val) { return (val<0) ? –val : val; }
不能利用函数返回类型的不同进行函数重载。因为在没有确定调用的是哪个函数之前,不知道函数的返回类型。 main() { int i=100; cout<<abs(i)<<endl; // int型 float f=-125.78F; cout<<abs(f)<<endl; // float型 } 在程序中,求绝对值函数的名称相同,但参数类型不同,这时C++编译器自动按参数表的不同来分别联编不同的求绝对值函数。 不能利用函数返回类型的不同进行函数重载。因为在没有确定调用的是哪个函数之前,不知道函数的返回类型。 long abc(int); float abc(int);
同样,不能利用引用进行函数重载: void fun(int&); void fun(int); 因为对于下面的调用语句,编译器无法决定调用哪一个函数: fun(i); // i是一个整型变量 从上面可以看出,一般函数的重载使C++程序具有更好的可扩充性。此外,类的成员函数也可以重载,特别是构造函数的重载给C++程序设计带来很大的灵活性。
例 构造函数的重载。 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; } };
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
10.2 运算符重载 运算符重载:指对于不同数据类型的操作数,同一个运算符所代表的运算功能可以不同。 10.2 运算符重载 运算符重载:指对于不同数据类型的操作数,同一个运算符所代表的运算功能可以不同。 一个运算符定义了一种操作,一个函数也定义了一种操作,其本质是相同的,当程序遇到运算符时会自动调用相应的运算符函数。 虽然重载运算符完成的功能都能够用一个真正的成员函数来实现,但使用运算符重载使程序更易于理解。 与函数重载类似,编译器是根据参加运算的操作数的类型来识别不同的运算。
我们可以将字符串operator+看成一个运算符函数名,这些同名的运算符函数根据不同类型的操作数完成不同的加法运算。 例: 对于表达式:10+20 编译器把它看成如下函数调用: int operator+(10, 20); 对于表达式:10.0+20.0 float operator+(10.0, 20.0); 参加运算的数是整数 参加运算的数是单精度实型数 我们可以将字符串operator+看成一个运算符函数名,这些同名的运算符函数根据不同类型的操作数完成不同的加法运算。
重载一个运算符,就是编写一个运算符函数,重载运算符(函数)的原型为: 重载运算符的形式: 重载一个运算符,就是编写一个运算符函数,重载运算符(函数)的原型为: <数据类型> operator<运算符>(<形参表>); 运算结果的类型 参加运算的操作数 要重载的运算符 例 定义复数类型,重载运算符“+”。 例如:c3=c1+c2
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; } 利用普通函数重载运算符
void main() { Complex complex1(6. 34f, 4. 8f), complex2(12. 8f, 5 void main() { Complex complex1(6.34f, 4.8f), complex2(12.8f, 5.2f); Complex complex; complex=complex1+complex2; // 进行两个复数的相加运算 cout<<complex.r<<'+'<<complex.i<<'i'<<endl; } 说明: 本例采用普通函数的形式重载运算符。 可以采用成员函数的形式重载运算符。并且如果运算符函数要求直接访问类的非公有成员时,运算符函数不能定义为非成员函数,除非将它声明为该类的友元函数。
例 利用成员函数进行运算符重载。 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; }; // 输出实部和虚部 };
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(6.34f, 4.8f), complex2(12.8f, 5.2f); Complex complex; complex=complex1+complex2; complex.Display(); }
说明: 当利用非成员函数重载双目运算符时,运算符函数的第一个参数代表运算符左边的操作数,运算符函数第二个参数代表运算符右边的操作数。 当利用成员函数重载双目运算符时,运算符左边的操作数就是对象本身,不能再将它作为运算符函数的参数,运算符函数只需要一个函数参数。 运算符重载与函数重载的区别: 同一个重载运算符的参数个数是相同的。 不能定义新的运算符,只能重载现有的运算符。 运算符重载后仍然保持原来的优先级和结合性。
10.3 重载单目运算符 由于单目运算符只有一个操作数,因此运算符重载函数只有一个参数,如果运算符重载函数作为成员函数,则还可省略此参数。
例10.5 有一个Time类,包含数据成员minute(分)和sec(秒),模拟秒表,每次走一秒,满60秒进一分钟,此时秒又从0开始算。要求输出分和秒的值。 #include <iostream> using namespace std; class Time { public: Time( ){minute=0;sec=0;} //默认构造函数 Time(int m,int s):minute(m),sec(s){ } //构造函数重载 Time operator++( ); //声明运算符重载函数 void display( ){cout<<minute<<″:″<<sec<<endl;} //定义输出时间函数 private: int minute; Int sec; };
运行情况如下: Time Time∷operator++( ) //定义运算符重载函数 { if(++sec>=60) ++minute; } return *this; //返回当前对象值 int main( ) Time time1(34,0); for (int i=0;i<61;i++) {++time1; time1.display( );} return 0; 运行情况如下: 34:1 34:2 ┆ 34:59 35:0 35:1 (共输出61行)
注:“++”和“--”运算符有两种使用方式,前置自增运算符和后置自增运算符。对于后缀情况, C++约定: 在自增(自减)运算符重载函数中,增加一个int型形参。 Time operator++(int); Time Time∷operator++(int) //定义后置自增运算符“++”重载函数 { Time temp(*this); sec++; if(sec>=60) sec-=60; ++minute; } return temp; //返回的是自加前的对象
6.6 C++模板 什么是模板? 模板是一个将数据类型参数化的工具,它把“一般性的算法”和其“对数据类型的实现”区分开来。 模板分为函数模板和类模板两种。 采用模板方式定义函数或类时不确定某些函数参数或数据成员的类型,而将它们的数据类型作为模板的参数。在使用模板时根据实参的数据类型确定模板参数(数据类型)的数据类型。 模板提高了软件的重用性。当函数参数或数据成员可以是多种类型而函数或类所实现的功能又相同时,使用C++模板在很大程度上简化了编程。
6.6.1 函数模板 1. 函数重载与函数摸板 函数模板扩展了函数重载:利用函数重载可以让多个函数共享一个函数名,只要所重载的函数的参数类型必须有所不同。但是,由于参数的类型不一样,虽然这些函数所完成的功能完全一样,也必须为每一个重载函数编写代码。 一个函数模板可用来生成多个功能相同但参数和返回值的类型不同的函数。 函数工厂 2. 什么是函数模板 函数模板是一种不指定某些参数的数据类型的函数,在函数模板被调用时根据实际参数的类型决定这些函数模板参数的类型。
以下定义了一个可对任何类型变量进行操作(求绝对值)的函数模板: 6. 函数模板的定义举例 以下定义了一个可对任何类型变量进行操作(求绝对值)的函数模板: template < class T > T abs( T val ) { return val<0 ? -val : val; } 类型参数T作用: 定义函数的参数和返回值; 在函数体中用来声明变量。 模板定义以关键字template开头; 关键字class后面的标识符T由用户自定义,称为类型参数,是函数模板abs()中没有确定数据类型的参数val的类型。 模板定义的下面是模板函数abs()的定义。
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; }
5. 函数模板的实例化 函数模板将数据类型参数化,这使得在程序中能够用不同类型的参数调用同一个函数(模板函数)。在调用模板函数时即创建函数模板的一个实例,这个过程称为函数模板的实例化。 函数模板的实例化由编译器完成:编译时函数模板本身并不产生可执行代码,只有在函数模板被实例化时,编译器才按照实参的数据类型进行类型参数的替代,生成新的函数。 编译器 函数模板 函 数
例 函数模板的定义和使用。 #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=-125.78F; cout<<abs(f)<<endl; 类型参数T 替换为int 类型参数T 替换为long 类型参数T 替换为float
6.6.2 类模板 1. 类模板与函数模板 函数模板只能用于定义非成员函数,它是模板的一个特例。类模板实际上是函数模板的推广,它是一种不确定类的某些数据成员的类型或成员函数的参数及返回值的类型的类。 2. 类模板与类 类是对问题的抽象,而类模板是对类的抽象,即更高层次上的抽象。 类模板称为带参数(或参数化)的类,也称为类工厂,它可用来生成多个功能相同而某些数据成员的类型不同或成员函数的参数及返回值的类型不同的类。
6. 类模板的定义 为了起到模板的作用,与函数模板一样,定义一个类模板时必须将某些数据类型作为类模板的类型参数。 模板类的实现代码与普通类没有本质上的区别,只是在定义其成员时要用到类模板的类型参数。 定义举例
例如,以下定义了含有一个类型参数的类模板: template < class T > class MyTemClass { private: T x; // 类型参数T用于声明数据成员 public: void SetX( T a ) { x=a; }; // 类型参数T用于声明成员函数的参数 T GetX( ) { return x; }; // 类型参数T用于声明成员函数的返回值 };
注意: 如果在模板类的外部定义模板类的成员函数,必须采用如下形式: template < class T > // 不能省略模板声明 void MyTemClass < T > :: SetX( T a ) { x=a; } 编译时由编译器完成 4. 类模板的实例化 与函数模板不同,类模板不是通过调用函数时实参的数据类型来确定类型参数具体所代表的类型,而是通过在使用模板类声明对象时所给出的实际数据类型确定类型参数。
例如,以下使用类模板声明了一个类型参数为int的模板类的对象: MyTemClass < int > intObject; 对于上面的对象声明: 编译器首先用int替代模板类定义中的类型参数T,生成一个所有数据类型已确定的类class; 然后再利用这个类创建对象intObject 。 5. 含有多个参数类模板的定义 template < class T1,int i,class T2 > class MyTemClass { . . . } 使用
例如,声明模板类的对象应采用如下形式: 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; }; };
MyTemClass< int, float > mt(a, b); // 声明模板类的对象 mt.ShowMax(); } void main() { int a=100; float b=126.45F; MyTemClass< int, float > mt(a, b); // 声明模板类的对象 mt.ShowMax(); } 类模板 的实例化
6.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++语法进行了一些扩充。
6.7.1 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 任意类型
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位数值,代表一个颜色值
6.7.2 Visual C++运行库 运行库(Run-Time Library)是存放一些常用函数执行代码的文件库,它由LIB文件组成,进行链接时将需要的LIB文件与程序链接在一起。 Visual C++ 6 可以使用的运行库包括C运行库、标准C++库(存放新的iostream函数和其它标准函数)和旧的iostream库,但标准C++库和旧的iostream库是不兼容的,因此,链接时除了链接C运行库,只能链接标准C++库或旧的iostream库中的一个库。
根据程序中使用的头文件就可以确定是链接标准C++库还是旧的iostream库。 以下文件包含指令包含一个标准C++库头文件,在编译时Visual C++将自动链接一个标准C++库: #include < iostream> 以下文件包含指令包含一个旧的iostream库头文件(本章的例子都是使用该库),在编译时Visual C++将自动链接一个旧的iostream库: #include < iostream .h > 使用标准C++库(新的isotherm函数)的例子: #include <iostream> std::cout<< “AAAAAA\n ”; // 输出
6.7.3 运行时类型识别RTTI 运行时类型识别RTTI(Run-Time Type Information)是这样一种机制:在程序运行时可以确定对象的类型。RTTI主要有以下两种应用: (1)使用dynamic_cast运算符检查一个基类指针是否指向其派生类对象; (2)使用typeid运算符识别指针所指类型。
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运算符检查一个基类指针是否指向其派生类对象。
#include <iostream #include <iostream.h> class A { public: // 多态性类(使用虚函数) // 才能使用dynamic_cast运算符 virtual void f1() { }; }; class B : public A void f1() { };
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项。
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;
运行时类型识别RTTI机制在较先进的编译器如Visual C++和Borland C++中才得到支持,但微软基础类MFC并未使用Visual C++所支持的RTTI,它有自己的一套办法。MFC提供了有关运行时类型识别的宏,其详细内容请参看第6章的6.5和6.6节。 作业: P115 3-69,3-70
6.7.4 编程规范 为了阅读理解源程序,Visual C++源程序中变量的取名一般采用匈牙利表示法则。该法则要求每一个变量名都有一个前缀,用于表示变量的类型,后面是代表变量含义的一串字符。 例如:前缀n表示整形变量,前缀sz表示以0结束的字符串变量,前缀lp表示指针变量。这些前缀还可以组合起来使用。前缀一般是小写字母,前缀后的第一个字符要大写。如:nWidth表示一个整形变量,lpszMyname表示一个字符串的指针。 在给类和成员变量取名时也使用特定的前缀,如CView是一个类(视图类),m_xStart是一个类的整形成员变量(起点的X坐标)。
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_