Visual C++面向对象编程语言
第三章 C++面向对象程序设计
第三章 C++面向对象程序设计
• 面向对象程序设计概述
• 传统的结构化程序设计
• 面向对象程序设计方法及特征
• C++类
• 类的定义与实现
• 构造函数和析构函数
• this指针
• 静态成员
• 友元
• 类的继承
• 基类和派生类
• 基类和派生类的构造函数
• 多重继承
• 虚基类
• 多态性和虚函数
• 用基类指针指向派生类对象
• 虚函数
• 抽象类和纯虚函数
• 重载
• 函数重载
• 运算符重载
• C++模板
• 函数模板
• 类模板
• Microsoft Visual C++的语法扩充
• Visual C++自定义数据类型
• Visual C++运行库
• 运行时类型识别
• 编程规范
• 结构化程序设计:将系统分解为若干模块
(即函数);
• 缺点:维护困难,特别是数据结构的修改;
• 传统的结构化程序设计
• 面向对象程序设计方法及特征
• 对象=数据+作用于这些数据上的操作;
• 类是具有相同数据结构和相同操作功能的
对象的集合,对象是类的实例;
• 对象与类的关系类似于鞋和生产鞋的模具;
• 面向对象程序设计把一个复杂的问题分解
为多个能完成独立功能的对象,分开独立
设计,再将其整合完成复杂问题;
• 其理想目标是希望编写程序就像搭积木;
面向过程和面向对象的区别
面向过程就是分析出解决问题所需要的
步骤,然后用函数把这些步骤一步一步
实现,使用的时候一个一个依次调用就
可以了。
面向对象是把构成问题事务分解成各个
对象,建立对象的目的不是为了完成一
个步骤,而是为了描叙某个事物在整个
解决问题的步骤中的行为。
例如五子棋,面向过程的设计思路就是
首先分析问题的步骤:
1、开始游戏,2、黑子先走,3、绘制
画面,4、判断输赢,5、轮到白子,6、
绘制画面,7、判断输赢,8、返回步骤2
,9、输出最后结果。
把上面每个步骤用分别的函数来实现,
问题就解决了。
而面向对象的设计则是从另外的思路来解决问
题。整个五子棋可以分为:
1、黑白双方,这两方的行为是一模一样的,
2、棋盘系统,负责绘制画面,3、规则系统,
负责判定诸如犯规、输赢等。
第一类对象(玩家对象)负责接受用户输入,
并告知第二类对象(棋盘对象)棋子布局的变
化,棋盘对象接收到了棋子的变化就要负责在
屏幕上面显示出这种变化,同时利用第三类对
象(规则系统)来对棋局进行判定。
可以明显地看出,面向对象是以功能来划分问
题,而不是步骤。
同样是绘制棋局,这样的行为在面向过程的设
计中分散在了总多步骤中,很可能出现不同的
绘制版本,因为通常设计人员会考虑到实际情
况进行各种各样的简化。而面向对象的设计中,
绘图只可能在棋盘对象中出现,从而保证了绘
图的统一。
功能上的统一保证了面向对象设计的可扩展性。
比如我要加入悔棋的功能,如果要改动面向过
程的设计,那么从输入到判断到显示这一连串
的步骤都要改动,甚至步骤之间的循序都要进
行大规模调整。
如果是面向对象的话,只用改动棋盘对象就行
了,棋盘系统保存了黑白双方的棋谱,简单回
溯就可以了,而显示和规则判断则不用顾及,
同时整个对对象功能的调用顺序都没有变化,
改动只是局部的。
• 在类的定义中,数据称为成员变量,函数称
为方法;
• 成员变量按访问控制权限分为公有和私有;
• 定义的格式:
class <类名>
{
private:<私有数据成员和成员函数的声明>
public:<私有数据成员和成员函数的声明>
protected:<保护数据成员和成员函数的声明>
}
类的定义与实现
• private属性的成员只允许被本类的成员函数
访问和调用;
• protected属性的成员允许被本类的成员函数
和派生类的成员函数访问和调用;
• 例:Time类的定义
class Time
{
private:
int hour;
int minute;
int second;
public:
void setTime( int, int, int );
void showTime();
}
为什么需要这两个
函数???
对于成员函数来讲,成员变量就像全局变量;
• 类的定义和一般函数定义类似,但必须在成
员函数名前加类名和作用域限定符(::)
• 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;
}
• 对象的声明:就像变量的声明(用模具生产
一个产品);
• 格式:<类名> <对象名>;
• 例:Time t1,today;
Time *pt1=&t1;
• 比较 int i;
• 成员函数的调用:
(1,2,3);
();
pt1->setTime(1,2,3);
• 思考:=10;
在面向对象程序设计中,程序设计范型
变成了:确定你需要哪些类型,为每个
类型提供完整的一组操作。
• 思考:如果对于Time类,在未调用setTime
前调用showTime会怎样?
• 我们需要对成员变量初始化;
• 构造函数是一种特殊的成员函数,函数名和
类名相同,提供了对成员变量初始化的方法,
使得声明对象时自动初始化对象,当声明对
象时,自动调用构造函数;
• 析构函数也是成员函数,用来在系统释放对
象前做一些清理工作,当对象注销时,自动
调用析构函数;
构造函数和析构函数
•构造函数的名称与类名相同,而析构函数必
须在类名前加“~”,都没有返回值;
•构造函数可以有参数,析构函数没有参数;
•系统有默认的构造函数和析构函数;
•可以有多个构造函数(参数类型不同),使
得创建的对象根据数据类型的不同(整型和
实型)进行不同的处理,这就是函数重载;
• public:
Time(int,int,int);
~Time()
……
};
Time::Time(int h,int m,int s)
{
hour=h;
minute=m;
second=s;
}
Time::~Time()
{……
• 例
Void main(void)
{
Time t1(10,20,30); //调用构造函数
……
……
} //调用析构函数
理解面向对象设计的思想
例:计算一个矩形的面积和周长
计算一个矩形的面积和周长
从面向过程的角度出发思考这个问题;
先确定面积和周长的算法(这个很简单,
小学知识) 编写2个方法分别计算面积和
周长,在一个类里面 考虑到计算周长和
面积需要至少2个参数,长和宽;
程序如下:
int perimeter (int length, int width)
{
return 2*(length + width);
}
int area (int length, int width)
{
return length*width;
}
void main()
{
cout<<perimeter(4,5);
cout<<“\n”
cout<<area (4,5);
}
下面再从面向对象的角度出发,重新考
虑这个问题:
一个长方形可以看成一个长方形对象。
既然是对象,那么它就有属性(长和宽)
和行为(周长和面积) 抽取长方形的共
性,设计一个类 通过对象的行为,就可
以求出某个具体的长方形对象的周长和
面积
class Rectangle()
{
public:
int length, width;
int perimeter() {return 2*(length+width);}
int area() {return length*width;}
}
void main()
{
Rectangle rect1=new Rectangle();
=10;
=5;
cout<<();
cout<<();
}
• 在C方案中扩展困难,原因就在于没有一个
合适的位置来存储辅助的状态信息,因为在
原来的情况下根本不需要状态信息;
• 对象提供了一个理想的位置来放置状态信息,
并且作为对象的一部分是显示可用的,而不
是隐藏在幕后的东西;
继续思考面向对象的特点
在面向过程的思想里,一个程序一般都
是由一个个函数组成的,这些函数之间
相互调用, 就形成了一个完整的程序,
解决了一个问题。它的基本结构是:
函数一()
{
……;
}
函数二()
{
……;
}
……
主函数()
{
……;
}
在面向对象的思想中,是把这些都封装
起来,形成一个类。
class 类名
{
函数一()
{
……;
}
函数二()
{
……;
}
……
}
然后由这个类产生一个对象,比如:我
们把一个类的类名起为 student
那么产生对象的方法就是: student s1;
这里s1是一个对象,产生了这个对象以
后,才可以进行相关操作。
操作方法如: s1.函数一();
假设我们要做一个小游戏,类似于网游
里面的打怪,为简单起见,认为怪不打
人,只让人打,他的血不断减少,直到
死亡。
把所有怪物的共同特征抽象出来,于是
我们得到一个类:怪物
class 怪物
{
char 怪物名;
int 血;
}
怪物::怪物(怪物名n,血n) /*构造函数
*/
{
怪物名=怪物名n;
血=血n;
}
怪物::被打函数()
{
血减少;
}
怪物::判断函数()
{
if (血==0)
cout<<“你胜利了!”;
}
怪物::玩游戏()
{
while (血 > 0)
{
被打函数();
判断函数();
}
main主函数
void main()
{
/******产生对象******/
怪物 怪物1("狼",5000);
怪物 怪物2("虎",8000);
怪物 怪物3("蛇",3000);
怪物1.玩游戏();
怪物2.玩游戏();
怪物3.玩游戏();
}
• 指向对象本身的指针,作为隐含参数传给每个
成员函数;
• 例:
void Time::showTime()
{
cout<<hour<<’:’<<minute<<‘:’<<second<<endl;
}等同于
void Time::showTime(Time *this);
{
cout<<this->hour<<‘:”<<this->minute……
}
this指针
Time::showTime
{
Display(this);
}
Display(Time *pObj)
• This指针对于在非本类成员函数中使用对象成
员变量
• 需要让多个对象共享相同值的成员时使用;
• 成员变量定义:
static int m_ncount;
• 使用和一般成员变量一样,也有公有、私有之
分;
• 初始化必须在类定义的外部:
int Person::m_nCount=0;
静态成员
• 成员函数也可以是静态的;
• 定义
static int GetCount();
• 没有this指针;
• 可以通过类调用:Person::GetCount();
也可以通过对象调用:();
• 静态成员函数只能访问类的静态成员;
• 如需访问非静态成员,需将对象作为参数传入;
long Person::GetID(Person x);
友元
• 友元函数:访问其他类的私有成员变量的函数;
• 一个类可以声明一个函数为其友元函数;
• 作用:编程方便;
• 定义:
class A
{
friend void display(A);
private:
int a;
}
•void display(A e)
{
cout<<
}
void main()
{
……
display(a1);
}
• class B
{
public:
void fun1(A&)
}
class A
{
friend void B::fun1(A&);
private:
int a;
}
void B::fun1(A& e)
{
=10; ……
}
一个类的成员函
数作为另一个类
的友元函数
• void main()
{
A a1;
B b1;
(a1);
}
• 友元类:类A可以声明类B为其友元类,类B的
所有成员函数都可以访问A的所有成员;
• 定义
class C
{
public:
void fun2(A&);
}
class A
{
friend C;
private:
int a;
}
• void C::fun2(A& e)
{
=10;
}
void main()
{
A a1;
C c1;
(a1);
}
• class B
{
public:
void fun1(A&);
}
void B::fun1(A& e)
{
=10;
}
void main()
{
A a1;
B b1;
(a1);
void B::fun1(A e)
{
=10;
}
void main()
{
A a1;
B b1;
(a1);
class B
{
public:
void fun1(A)
}
• class B
{
public:
void fun1(A&);
}
void B::fun1(A& e)
{
=10;
}
void main()
{
A a1;
B b1;
(a1);
void B::fun1(A e)
{
=10;
}
void main()
{
A a1;
B b1;
(a1);
class B
{
public:
void fun1(A)
}
类的继承
• 实现代码重用和扩充;
• 拷贝代码受到版权的各方面因素影响;
基类和派生类
• 在继承关系中,新定义的类称为被继承类的
派生类或子类,被继承的类称为新定义类的
基类或父类;
• 格式:
class <派生类名>:[<派生方式>] <基类名>
{
……
};
• 派生方式:public、private和protected(默认
为private);
• 无论何种派生方式,基类的私有成员都不能在
派生类中使用;
• 例
class A
{
private:
int a;
public:
int Geta(){return a};
}
class B:public A
{
}
void main()
{
B b1;
();
:=3;
}
class B
{
private:
int a;
public:
int Geta(){return a};
}
• 思考:如果不采用继承定义B类?
class B:public A
{
}
• public派生:基类成员访问权限不变,可以通
过派生类对象访问基类公有成员;
• private派生:基类所有成员成为私有成员,只
能通过派生类的成员函数访问基类非私有成员;
• protected派生:和private差不多;
• 例
class A
{
private:
int a;
public:
int Geta(){return a};
}
class B: A
{
……
}
void main()
{
B b1;
();//非法
}
• 基类中不适合派生类的成员可以在派生类中重
新定义,称为覆盖(override);
• 在派生类中定义与基类成员函数名称和参数表
相同的函数;
• 例
class A
{
public:
void show(){……}
};
class B:public A
{
public:
void
show(){……}
};
……
B b1;
();
• 若需要在派生类中调用基类中同名的成员函数,
可以使用作用域限定符“::”;
class B:public A
{
public:
void show(){……};
void display(){A::show();};
};
……
B b1;
();
();
基类和派生类的构造函数
• 当程序创建一个派生类对象时,系统首先自动
创建一个基类对象,即在调用派生类的构造函
数构建派生类对象时,系统首先调用基类的构
造函数构建基类对象;
• 当派生类对象的生存期结束时,首先调用派生
类的析构对象,然后调用基类的析构函数;
• 派生类中必须显示调用基类的构造函数,除非
基类有默认的构造函数;
• 例:
B::B(<形参声明>):A(<参数表>)
{
……
}
• 形参时指明派生类构造函数形参的名称和类型,
参数表时形参说明中所给出的部分参数,是要
传递给基类构造函数的参数;
Point(int a,int b)
Circle(int a,int b,int
r):Point(a,b)
Cylinder(int a,int
b,int r,int
h):Circle(a,b,r)
多重继承
• 单继承:每个派生类只有一个直接基类;
• 多重继承:一个派生类可以同时从多个基类派
生而来;
• 格式:
class <B>:[<派生方式>]<A1>,[<派生方式
>]<A2>,…,[<派生方式>]<An>
{
……
};
• P86 例3-11 GetAB(){ return a+b; }
虚基类
• 解决二义性问题;
• 每个派生类对象含有基类的数据成员的一个副
本,因此派生类对象可能存在间接基类的两份
副本。
• 采用虚基类解决;
• 格式(派生类定义)
class B:public virtual A
• 基类定义不变;
多态性和虚函数
• 多态性:根据数据不同进行不同的运算处理,
如加法,对于实数或整数就是将两数相加,对
于复数就是将对应的实部和虚部相加;
• “一种接口、多种方法”;
• 多态性分为编译时多态性和运行时多态性;
• 编译时多态性又称静态多态性,用来处理函数
名相同,但参数类型不同的情况,通过重载机
制来实现;
• 运行时多态性:处理函数名、参数和返回值都
相同的情况,通过虚函数机制实现;
• 派生类的对象也可以认为是其基类的对象,但
基类的对象不是其派生类的对象;(儿子像老
爸)
• 允许基类对象的指针指向其派生类的对象,通
过该指针只能访问派生类中从基类继承的公有
成员;(老爸的东西也是儿子的东西,老爸只
能拿老爸自己的东西)
• 可以通过强制类型转换将基类指针和派生类指
针互相转换;
用基类指针指向派生类对
象
• 例:
……
class B:public A
……
void setB()
……
A a,*pa;
B b,*pb;
pa=&b; //基类指针指向派生类对象
pb=(B *)pa; //强制类型转换
• pa->setB(); //通过基类指针访问派生类自定义
的成员
• pb=&a //派生类指针指向基类对象
• pa=&a;
pb=(B*)pa;
pb->setB(); //对象a中不存在setB()
虚函数
• 派生类成员函数和基类成员函数具有相同的函
数名和参数类型,派生类的成员函数将覆盖基
类成员函数;
• 例:
class A
{
void show()
……
class B:public A
{
void show()
……
B b;
();
• 若用基类指针指向派生类对象,结果一样;
A *pa;
B b;
pa=&b;
pa->show();
• 在基类对象中使用虚函数,可以使指向派生类
对象的基类指针调用派生类中的覆盖函数(和
基类对象成员函数同名);
• 派生类对象的覆盖函数各不相同,但基类对象
的虚函数机制,使我们可以通过指向派生类对
象的基类指针调用同个函数名,实现不同派生
类对象的不同功能,这就是类的多态性;
• 格式:
class A
{
public :
virtual void
show(){……};
}
class B:public A
{
public:
void show(){……};
}
void main
{
•
A *pa;
B b;
pa=&b;
pa->show();
}
• 利用虚函数机制,我们可以在基类和派生类中
使用相同的函数名和参数类型,而定义不同的
操作;
• 为同一个类体系中所有派生类的同一类行为提
供了统一的接口;
• 如果用new运算创建派生类对象;当delete运算
撤销派生类对象时,只调用基类的析构函数,
没有调用派生类的析构函数;
• 定义基类的析构函数为虚函数可解决问题;
• 格式:
class A
{
virtual ~A(){……};
}
class B:public A
{
~B(){……}; }
void main()
{
A *pa=new B;
……
delete pa;
}
• 开发程序中,常将基类中功能容易变化的成员
函数设计成虚函数,然后根据需要设计派生类
的虚函数,实现“一种接口、多种方法”;
抽象类和纯虚函数
• 我们可以让基类的一些成员函数没有具体的定
义,这种基类只能用于类的继承,其本身不能
用来创建对象,这样的类称为抽象类;
• 抽象类只能是基类,它提供了一个框架,仅仅
起着统一接口的作用;
• 不能声明抽象类的对象,但可以声明抽象类的
指针;
• 抽象类:1、至少有一个成员函数不定义具体
实现;2、定义了一个protected访问属性的构
造函数或析构函数;
• 不定义具体实现的成员函数称为纯虚函数;
• 纯虚函数不能调用;
• 格式:
virtual<数据类型><成员函数名>(<形参表
>)=0;
• 例:
class A
{
public:
virtual void fun1()=0;
};
class B:public A
{
public:
void fun1(){……};
}
重载
• C++重载分为函数重载和运算符重载;
• 运算可以理解为是调用一个函数;
• 使用重载机制可以对一个函数名(或运算符)
定义多个函数(或运算),只不过函数的参数
(或运算的数)类型不同;
• 如加法,对于实数或整数就是将两数相加,对
于复数就是将对应的实部和虚部相加;
函数重载
• 我们可以通过函数重载机制使一组功能相似但
函数参数类型不同的函数共用一个函数名;
• C++编译器根据参数类型和个数选择正确的函
数;
• P95例3-18求绝对值函数abs()重载;
• 不能利用函数返回类型的不同进行函数的重载;
• 不能利用引用来重载;
对于void fun(int&);和void fun(int);
fun(i)无法选择调用哪个函数;
• 利用重载可以设计多个构造函数,为对象提供
多种初始化方式;
• P96例3-19:
class Box
{
public:
Box(){……};
Box(int,int,int){……};
}……
Box box1;
Box Box2(10,15,20);
运算符重载
• 运算符重载指对于不同数据类型的操作数,同
一个运算符所代表的运算功能可以不同;
• 可以把运算符看成调用运算符函数,如10+20
看成int +(10,20);
• 重载运算符就是编写运算符函数;
• 格式
<数据类型> operator<运算符>(<形参表>);
• P97例3-20非成员函数重载运算符:
class Complex
{
……
}
Complex operator+(Complex c1,Complex c2)
{
……
}
void main()
{……
complex=complex1+complex2;
}
• 利用非成员函数重载双目运算符时,运算符函
数的第一个参数代表运算符左边的操作数,运
算符函数的第二个参数代表运算符右边的操作
数;
• Complex operator+(Complex c1,Complex c2)
• 如果运算符函数要求访问一个类的非公有成员,
则运算符函数不能定义为非成员函数,除非将
它声明为该类的友元函数;
• P98例3-21成员函数重载运算符:
class Complex
{
Complex operator+(Complex);
}
Complex Complex ::operator+(Complex other)
{
……
}
void main()
{……
complex=complex1+complex2;
}
• 利用成员函数重载双目运算符时,运算符左边
的操作数就是对象本身,不能再将它作为运算
符函数的参数,因此,这时运算符函数只需一
个函数参数;
• Complex operator+(Complex);
• 同一个重载运算符的参数个数是相同的;
• 用户不能定义新的运算符,只能对现有的进行
重载;
• 重载后的优先级和结合性不变;
• 下列运算符不能重载
“.”(成员运算符)
“::”(作用域限定符)
“?:”(条件运算符)
“*”(成员指针选择符);
C++模板
• 模板:将数据类型参数化的工具;
• 使用模板时根据实参的数据类型确定模板参数
的数据类型;
• 避免代码重复编写;
函数模板
• 例:
int abs(int val)
{
return val<0?-val:val;
}
float abs(float val)
{
return val<0?-val:val;
}
• 为了避免这种代码的重复编写,C++提供函数
模板;
• 例:
template <class T> //T是自己定义的类型参数
T abs(T val)
{
return val<0?-val:val;
}
• abs(-1);
abs();
• 类型参数定义格式:
template <class 类型参数名>
template < class T1, class T2, class T3>
• 编译时函数模板本身并不产生实际的可执行代
码,只有在函数模板被实例化时,编译器才按
照实参的数据类型进行类型参数的替代,生成
新的函数,然后编译这个函数;
类模板
• 类模板:不确定类的某些数据成员的类型或成员
函数的参数及返回值的类型的类;
• 例
template <class T> // //T是自己定义的数据类型
class MyTemClass
{
private: T x;
public:
void SetX(T a){x=a;};
T GetX(){return x;};
};
• 在模板类的外部定义模板类的成员函数:
template <class T>
void MyTemClass<T>::SetX(T a)
{
x=a;
}
• 根据使用模板类声明对象时所给出的实际类型
确定类型参数;
• 例:MyTemClass <int> a1;
• 编译器先用int 替代模板类定义中的类型参数T
,生成一个所有数据类型已确定的类,再利用
这个类声明对象;
• 定义类模板时可以使用多个类型参数,还可以
含有已确定类型的参数;
• 例:
template <class T1,int i,class T2>
class MyTemClass
{
……
}
声明:MyTemClass <int,100,float> MyObject;
Microsoft Visual C++的语
法扩充
• Visual C++自定义数据类型
• Visual C++运行库
运行时类型识别
• RTTI:在程序运行时确定对象的类型
• dynamic_cast运算符:检查一个基类指针是否指
向其派生类对象;
• 语法:
dynamic_cast<Type-ID> (EXP)
Type-ID必须是类的指针、引用或void *;
EXP必须是一个具体的指针或引用;
• 作用:检查基类指针EXP是否指向其派生类
Type-ID类型的对象,若是,运算结果是该Type
-ID类的对象的指针,否则为NULL(空);
• 例
class A
……
class B:public A
……
A *p1=new A;
A *p2=new B;
B *pb1=dynamic_cast<B*>(p1);
B *pb2=dynamic_cast<B*>(p2);
• 结果:
pb1==NULL
pb2!=NULL
• typeid运算
• 输出类名:typeid(对象名).name()
• 比较两种变量(对象)类型是否相同:
typeid(变量)
• 例:
int i=0;
if (type(i)!=typeid(float))
cout<<“not float”;