面向对象程序设计概念
VC++应用程序是采用C++语言编写的。C++是在C语言的基础上进行改进与扩充,是既面向对象又面向过程的一种混合程序设计语言。
传统的程序设计思维方式是基于“算法+数据结构=程序”的模式,在这类程序中,数据和施加于数据的操作(算法过程)总是分离的,所有程序均有一组被动的数据和一组能动的过程所组成。通常把这类程序设计称为面向过程的程序设计,把支持这类程序设计的语言称为面向过程的语言(procedure-oriented language ,简称POL)。C语言就是其中的一种。C++保留了C语言的基本语法和流程,C程序几乎都可以在C++环境中不作修改被运行。
面向对象程序设计(object-oriented programming,简称OOP)将数据及对数据的操作放在一起,作为一相互依存、不可分割的整体来处理,它采用数据抽象和信息隐藏技术。它将对象及对象的操作抽象成一种新的数据类型——类,并且考虑不同对象之间的联系和对象类的重用性。概括为“对象+消息=面向对象的程序”。
本章将在C语言基础上对比介绍C++的基本语法,并扩充到面向对象程序设计的基本概念,使读者能够对在VC++应用程序开发中遇到的语法现象有个初步的理解。
2.1 C++与C的基本语法区别
2.1.1 一般语法区别
1.
注释
C++支持的注释方法有两种:
(1) /*……*/
(2) //……
“/*……*/”是C语言中所使用的注释方法,在C++中继续延用,一般在连续多行注释时使用。“//……”只能单行注释,进行注释的内容从“//”后的第一个字符直到本行结束。在使用AppWizard生成的MFC应用框架中会默认自动插入大量注释如程序清单2-1所示,“// TODO……”注释行提醒程序员添加代码的位置和通常所作的操作。
程序清单2-1: 加注释的C++程序
|
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
if( !CFrameWnd::PreCreateWindow(cs) )
return FALSE;
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
return TRUE;
}
|
注释一行不要太长,一般60个字符以内(保证在VC++集成编辑环境的可见区域之内),如有超过,建议换行处理。
2.
常量的定义
在C语言中,符号常量的定义是通过宏定义#define来实现的,其实质是字符串替换。例如下面语句定义了一个符号常量PI:
#define PI 3.1415926
在C++中保留#define定义符号常量的方法,但更多地主张采用关键字const来定义符号常量。例如PI的定义可以用下面语句表示:
const float PI=3.1415926;
注意:用关键字const定义的符号常量必须指定其数据类型,而用宏定义的符号常量是没有数据类型的;用关键字const定义的符号常量在编译时为其分配相应的内存空间。规定了符号常量的数据类型,便于编译系统检查参数的数据类型,C++的编译系统检查比C更为严格。
3.
变量的定义
在c语言的函数体中,定义部分和执行语句部分是相对分离的,对局部变量的定义必须出现在函数体或分程序内所有执行语句之前。C++的函数体不再区分定义部分和执行部分,用户可在函数体的任意位置定义一个局部变量。因此一个变量尤其是中间变量,在需要时再定义,这样便于阅读与维护。例如可以在for语句中定义循环变量:
int c=1;
for(int i=0;i<10 ;i++)
{ c*=2;
……
}
在C++程序中,要注意区分各种变量的作用范围和生命周期,变量的类型有:
(1) 局部变量(local variable)
在一个函数体内定义的变量,只在本函数范围内有效,在本函数外不能使用。
(2) 形参变量(formal parameter)
向函数传送的变量,同时又能象其他局部变量一样使用。
(3) 全局变量(global variable)
在函数体外定义的变量,其有效范围为:从变量定义处开始直到本源文件结束。
(4) 成员变量(member variable):
在一个类中定义的变量,对类中的所有成员函数都是有效的。在一个继承体系中,根据访问类型和继承类型的不同,对派生类的作用范围会有不同,参见后面章节对继承的讨论。
4.
强制类型转换
C++保留了C语言的显式强制类型转换法:
(类型名)表达式
同时还提供了函数表示法:
类型名(表达式)
例如:
int i=int(1.35); //给i赋一个初始值
函数表示法的函数名只能使用简单类型名称,即int、float、double、char。
5.
动态变量创建
C语言中用函数malloc()动态分配存储空间,用函数dispose()动态释放已分配的存储空间。C++中提供了两个类似该函数的运算符new和delete,它们能完成malloc()和dispose()的功能,而且更为方便和优越。
(1) new 运算符
使用new 运算符向内存的“堆”申请某个大小的存储空间,返回存储空间的首地址。空间大小由数据类型决定,分配失败返回空指针。使用new的语句格式如下:
指针
=new数据类型(初始化值)
指针
=new数据类型[算术表达式]
//分配拥有多个连续空间的动态数组
例如:
int *pi
,*parray;
pi=new int(85);
//分配一个int型的内存单元,赋初值为85,首地址赋给int型指针pi
parray=new int[10];
//分配10个int型(数组)的内存单元,首地址赋给int型指针parray
(2) delete运算符
delete运算符用来释放通过new运算符得到的内存空间。使用delete的语句格式如下:
delete 指针
delete [] 指针
//释放整个数组占用的空间
例如:
delete pi;
//释放p指向的内存单元
delete [] parray; //释放parray指向的动态数组内存块,方括号内不要写元素个数
6.
输入输出语句
在C++中继续支持使用C的外部库函数(如printf和scanf等)进行输入输出,以兼容C程序的执行。C++本身引入了流的概念来处理数据的输入输出。
所谓的流就是指数据之间的传输操作,包括输入流和输出流。输入流表示数据从载体(如数据文件)或设备(如键盘)流向内存的数据结构,输出流表示数据从内存的数据结构流向载体或设备(如显示器)。流的操作由C++的标准库iostream.h提供,也就是说,如果要使用流操作必须包含库文件“iostream.h”。
输入输出流操作包含两个流对象:输出设备cout和输入设备cin;两个操作符:插入操作符“<<”和抽取操作符“>>”。进行输出时的语句格式为:
cout<<输出内容[<<输出内容...];
例如:cout<<”a=”<<a<<endl;
//endl表示换行,等价于“/n”
进行输入时的语句格式为:
cin>>变量1[>>变量2...];
例如:cin>>x>>y;
程序清单2-2演示了两种不同的输入输出方法的使用:
程序清单2-2:C与C++的输入输出比较
|
(1)#include “stdio.h”
(2)#include “iostream.h”
void main() void main()
{ int a,b,sum; { int a,b,sum;
printf(“input a b:”); cout<<“input a b:”;
scanf(“%d%d” ,&a,&b);
cin>>a>>b;
sum=a+b;
sum=a+b;
printf(“sum is %d/n”,sum);
cout<<”sum is ”<<sum<<endl;
} }
|
cout和cin的优点是可以自动调整输出和输入格式,而不必象printf和scanf都要由使用者一个个指定;另外cout和cin支持用户自定义的数据类型(printf和scanf不支持)。
2.1.2指针和引用
引用是
C
++的一个特征,可以把它看作是一个常量指针,并自动被编译器逆向引用。指针是指对象(变量)的地址,而
引用是指给对象的地址取一个别名。引用通常应用于函数参数表中作为参数实现双向传值,但也可以作为一个引用变量使用。
1.
引用变量
引用变量的说明方式是:
<类型> &引用名=变量名或对象名;
例如:
int a;
int &b=a;
//合法定义引用类型
int &c;
//错!非法定义引用类型
b++;
这里b是a合法的引用变量,b是a的别名,不再为b分配空间,a,b共用同一存储单元,对b的操作与对a的操作效果相同,执行b++等同于执行a++。
引用变量定义时必须初始化,对此可以这样理解,指针变量的指向可以改变,但作为常量指针的引用,它的指向是不能改变的,所以在创建一个引用时,它必须指向一个特定的合法的存储空间,并且在程序运行过程中不能改变。
2.
引用参数
最常看到引用的场合是在函数的参数表中。C与C++函数参数传送是采用单向传值,形参得到实参的拷贝值。如果需要逆向传值时,即将修改过的参数值反向传递回函数调用处,必须采用传地址的方法,即使用指针变量,直接操作内存空间。而在C++中更好的方法是使用引用参数,即把形参定义为引用变量,编译器会自动地实现逆向传值,引用具有更清晰的语法。
程序清单2-3:引用参数举例
|
#include "iostream.h"
void AutoInc1(int*x);
void AutoInc2(int &y);
void main()
{int a(10);
AutoInc1(&a);
cout<<"a="<<a<<endl;
AutoInc2(a);
cout<<"a="<<a<<endl;
}
void AutoInc1(int*x)
{ (*x)++;
}
void AutoInc2(int &y)
{y++;
}
运行结果:
a=11
a=12
|
在上面的示例中,定义两个函数AutoInc1和AutoInc2的作用都是增1操作。AutoInc1形参采用的是指针,调用时需传递地址,而AutoInc2形参采用的是引用。可以看到AutoInc2的实现代码更为简单,调用时隐藏性好,即调用代码不能确定是传值还是传地址。
2.1.3函数的改进
1.
函数原型
C++中的函数原型其实就是C语言中的函数声明的概念,用于告诉编译系统使用函数的信息,包括函数的返回类型、参数个数和参数类型。
函数原型通常放在程序的开始部分或者包含在头文件中,确保编译器在该函数被调用以前就已经“认识”该函数,其语法形式为:
返回类型
函数名(参数表);
例如:
char* substring (char* str, int start ,int count);
也可写成:
char* substring(char*,int, int);
C的类型检查的机制较为薄弱,在很多情况下函数声明都可以省略,但在类型检查更为严格的C++中,所有函数皆必须声明后才可使用,这便是C++所特别要求的函数原型。编译系统对下面情况会给出错误信息:
(1) 实参和形参的类型不匹配,且实参向形参自动类型转换非法。
(2) 实参与形参的个数不一致。
(3) 函数没有按函数的类型返回数据。
例如:在C++中main函数前必须指明返回类型为void,否则会出现编译错误,main()函数的一般定义如下:
void main()
{}
2.
函数重载
函数重载是指同一个函数名可以对应着多个函数的实现。函数重载允许一个程序内声明多个名称相同的函数,这些函数通常执行相同的功能,但是带有不同类型、不同数目的参数或不同类型的返回值。例如,要写一个求两个整数之和与两个浮点型数之和的函数如下:
int plus (int x, int y){return x+y; }
//整数求和函数
double plus(double x, double y){return x+y; } //浮点数求和函数
以上实现的是两个不同参数类型的重载。这样在调用时可用同一个函数名plus调用,程序会自动完成正确的调用。如:
int z=plus(2,3); //调用整数求和函数plus
double z= plus(2.0,3.5); //调用浮点数求和函数plus
编译系统解读函数原型,识别重载。为了使编译系统能确定采用哪一个函数的实现,要求重载函数的参数个数和参数类型必须有所不同,否则产生二义性错误。
3.
设置默认参数
调用函数时,若所用的参数有一个经常用的值,在声明函数原型时,可以用该值作为函数参数的默认值。凡有默认值的参数,在函数调用时如果提供实参,形参则接受实参传递的数值,如果没有给出实参,形参则接受默认值。例如,下面程序代码中的延时函数,第四次调用 delay()时没有给出实参,就按默认值1000延时,其它调用都给出了实参值,就按给定的数值延时。
程序清单2-4:函数参数默认值举例
|
#include “iostream.h”
void delay(int t)
{
if(t==0) return;
for(int i=0; i<t;i++);
cout<<”i=”<<i<<endl;
}
void main()
{
void delay(int=1000); //声明默认值参数
delay(0);
delay(50);
delay (500);
delay();
delay(5000);
}
运行结果为:
i=50
i=500
i=1000
i=5000
|
注意:
(1) 默认参数只能在函数原型中给出,不能在函数定义中给出
(2) 由于实参和形参的匹配顺序是从左到右进行的,当不完全给出默认参数时,默认参数应集中置于形参序列的右端。
下面的语句是错误的:
int func(int x=3,int y=5,int z);
int func(int x=3;int z;int y=5);
4.
内联函数
函数为程序设计的模块化作出了重要贡献,但函数的调用也会带来降低程序运行效率的问题,原因在于调用函数时,系统必须先保护现场,即保存当前的运行状态和代码地址,而后传递参数给形参,执行被调用函数,执行完毕返回时,需要恢复现场,即要恢复调用函数前的运行状态,根据保存的代码地址继续执行下一条指令。可见调用函数增加了时间和空间方面的开销,影响了程序运行效率。特别是当频繁调用一个小函数时,执行函数代码的时间可能很短,而大量的时间花费在调用过程中的保护现场和恢复现场的工作上。
C++允许把代码少的小函数设置成内联函数,编译器为内联函数创建代码,在每次碰到该函数的调用都用相应的一段代码替换,避免了保护现场和恢复现场的开销,提高了程序的执行速度。从用户的角度看,调用内联函数和调用一般函数没有任何区别。
内联函数有两种定义方法:
(1) 在类的外部定义时,把关键字inline加在函数定义之前
(2) 把函数原型声明和函数定义合并,放在类定义的内部,则不需要关键字inline
程序清单2-5中所示的程序,在说明函数原型时前面加inline关键字,声明max函数为内联函数。
程序清单2-5:内联函数举例
|
#include "iostream.h"
void main()
{
int a(8),b(16),c;
inline int max(int x,int y);
c=max(a,b);
cout<<"c="<<c<<endl;
}
int max(int x,int y)
{
return(x>y?x:y);
}
|
内联函数与宏在概念上是不同的,它是真正的函数。编译系统会进行类型检查,执行时会进行参数传递,与函数不同之处是在适当的时候能够象宏一样展开,取消函数调用的开销,所以在C++中应该只使用内联函数,而不再使用宏定义。
内联函数的实现通常是在结构和规模比较简单的函数,如果过于复杂的函数,由于多处调用多处复制,反而会引起代码膨胀,就得不偿失了,另外
内联函数不支持递归调用。
2.2 类和对象
类是面向对象程序设计的核心,类描述了具有一组相同特性(数据成员)和相同行为(成员函数)的对象,是对一组对象的抽象,它的实质是一种新的数据类型。通过类的结构可以规范一组对象的行为,这些行为是通过操作接口来描述的。使用者只需关心接口的功能,而不需要知道它是如何实现的。所以一个写的好的C++程序比C程序更简单,更容易理解,通常面向对象程序需要较少的手工代码,因为问题的解决大部分都可以用已存在的库代码。
2.2.1 类的定义
类的一般定义形式为:
class 类名
{
private:
私有段数据及函数;
protected:
保护段数据及函数;
public:
公有段数据及函数;
};
程序清单2-6给出了一个点(Point)类的定义。
程序清单2-6:Point类的设计(一)
|
class Point
{
int x;
int y;
public:
void set_x(int k){x=k;}
void set_y(int k){y=k;}
int get_x(){return x;}
int get_y(){return y;}
};
|
其中Point是类名标识符。在此类中,x和y是两个私有数据成员,分别表示点的横坐标和纵坐标。set_x, set_y, get_x, get_y是四个公有成员函数的定义。set_x和set_y函数分别用于设置x与y,即点的坐标值;get_x和get_y均是一个无参函数,分别返回点的坐标值。
1.
成员的访问类型
上述定义形式中,数据成员或成员函数前面冠以private或protected或public,
分别表示私有的、保护的、公有的。缺省时在类中定义成员都是私有的,所以上例中x和y的访问类型是private。
(1) private
私有的,是指只有类中所提供的成员函数才可以直接使用这些私有数据或调用这些私有函数,任何类以外的函数企图去使用这些私有数据或调用这些私有函数皆将导致编译时的错误。
(2) protected
保护段的成员除了被本类中的成员函数存取及调用外,还可以被派生类的成员函数访问。但同样不能被其它类的函数访问。
(3) public
公有段的成员是可以被类内和类外的任何函数访问。
一般情况下数据成员都定义为私有的,进行封装,不允许直接使用。成员函数定义为公有的,用来提供类与外界的操作接口,从而规范对象的行为,保护数据成员不被随意修改。而保护段成员的定义主要是从派生类和继承性的角度来考虑的。
2.
成员的组成
类的成员分成两大类:数据成员和成员函数。
(1) 数据成员
数据成员可以是简单类型的变量,也可以是用户自定义类型的变量,甚至可以是已定义类型的一个对象。但是不能是寄存器类型或者是外部类型,另外不能定义自身类的对象作为数据成员。
(2) 成员函数
类的成员函数的实现可以放在类定义内,也可以放在类定义外。一般当函数体的内容较小时,成员函数的实现放在类内,C++
编译系统将在类内实现的成员函数按内联函数来处理,可以提高执行速度,定义时不必加inline关键字。而当函数体的大小不适合设定为inline函数时,则应将成员函数的实现放在类外,但在类外实现的成员函数必须在类内提供函数原型。这样的类
的定义分成了两个部分,即类的定义部分和成员函数的实现部分。在
VC
++应用程序中对类的组织也在应分为两个文件:类的头文件(
.h
)存放类的定义部分,类的实现文件(
*.cpp
)存放成员函数的实现部分。
当类的成员函数的定义是在类的外部实现时,在定义方法时必须把类名放在方法名之前,中间用作用域运算符“::”隔开,用来指明其所属的类。例如程序清单2-7所示为修改上述Point类的定义:
程序清单2-7:Point类的设计(二)
|
class Point
{
int x;
int y;
public:
void set_x(int k);
void set_y(int k);
int get_x();
int get_y();
};
void Point::set_y(int k)
{
y=k;
}
void Point::set_x(int k)
{
x=k;
}
int Point::get_x()
{
return x;
}
int Point::get_y()
{
return y;
}
|
从功能上来分,成员函数可分为构造函数、析构函数和普通成员函数。除了析构函数不能重载之外,其它成员函数都能重载。
2.2.2 对象的定义
对象的实质就是变量,对象的定义也就类似变量的定义,通常我们说对象是一个类的实例(instance)化。遵循同样的规则,同样存在着普通对象、对象数组、对象指针和对象指针数组,以及它们的定义和引用。例如使用定义好的Point类定义一组对象:
Point p1,*p, pa[100],*parray[100];
p1是一个Point类的对象,p是一个指向Point类的对象指针,pa是一个可以存放100个point的对象数组。
对象定义后就可以访问对象中的成员,访问方法类似于C语言中结构体成员的访问。不同之处是,类的成员规定了访问类型,私有类型和保护类型的成员只能被类的成员函数访问。所以通过对象只能访问公有数据成员和公有成员函数,如Point类中只能访问设定为公有类型的四个成员函数。
成员引用的格式如下:
对象名. 公有数据成员名
对象名. 公有成员函数名(实参表)
对象指针->公有数据成员名
对象指针->公有成员函数名(实参表))
成员引用示例如程序清单2-8所示。
程序清单2-8:实现Point类应用的main函数
|
#include “iostream.h”
void main()
{
Point p1,*p;
int x,y;
p1.set_x(100);
p1.set_y(100);
p=&p1;
x=p->get_x();
y=p->get_y();
cout<<”x=”<<x<<endl<<”y=”<<y<<endl;
}
运行结果为:
x=100
y=100
|
2.2.3构造函数和析构函数
1.
构造函数和析构函数
构造函数和析构函数是类的特殊的成员函数。展开VC++应用程序框架中任意一个类,都可以看到两个特殊的函数,一个完全与类名同名,是类的构造函数,另一个在类名前多一个符号“~”,是析构函数,如图2-1所示。一个类可以看到一个或多个构造函数,但只能有一个析构函数。
析构函数
|
构造函数
|
图2-1 类的构造函数和析构函数
构造函数的作用是定义对象时,分配内存空间,使用给定的值初始化对象。析构函数的作用恰恰相反,在释放对象之前作一些必要的清理工作,主要清理系统分配的对象内存。
构造函数的特点是它的函数名和类名相同,既可以在类内定义,也可以在类外定义;它可以有任意类型的参数,可以重载,但不能有返回类型说明,即不返回任何值。析构函数的特点是它的函数名和类名相同;前面有“~”符,它没有参数,也没有返回值,而且也不能重载。
构造函数和析构函数的调用都不需要用户干涉,当创建一个对象时,系统自动调用该类的构造函数,当对象退出作用域时,系统自动调用该类的析构函数。
若用户没有定义构造函数和析构函数,系统将自动产生缺省的构造函数和析构函数。缺省的构造函数将成员变量简单的设置为0或空,缺省的析构函数完成内存释放工作。对于大多数类来说,缺省的析构函数就能满足要求。但如果类的构造函数使用了new分配的内存,就必须自定义一个析构函数,在析构函数中使用delete来释放new分配的内存。而对象定义时能否进行初始化完全由构造函数决定,要由用户重载构造函数,指定初始化形式。
2.
构造函数和对象初始化
简单变量定义时,可以直接设置初值,如:
int i=1;
在定义类的成员变量时,不能在类体中对成员变量进行初始化,只能在对象定义时初始化成员变量的值。对象的初始化实质就是调用构造函数,要为成员变量设初值,就要为构造函数设定相应的形参,在定义对象时,以实参的形式传值给构造函数,构造函数就能使用给定值对成员变量设初值,方法如下:
(1) 定义对象时使用函数运算符给出初值,如:
Point p(100,150);
(2) 定义带参构造函数,如:
Class Point
{ int x,y;
public:
Point();
Point(int a ,int b);
};
注意:构造函数决定对象定义的形式,一般在类定义时要给出无参构造函数,以允许在定义对象时不进行初始化,否则,若只有带参构造函数就必须进行初始化。
(3) 构造函数给成员变量赋值
构造函数给成员变量赋值有两种方式,一种是直接在函数体中使用赋值语句,如:
Point::Point(int a,int b)
{ x=a;
y=b;
}
另一种方式是在构造函数参数表与函数体之间,以“:”和“成员名(形参)”的方式给出,如:
Point::Point(int a,int b):x(a),y(b)
{}
3.
拷贝初始化构造函数
有时在创建一个对象时,希望用一个已经存在的对象来初始化这个对象,这时就需要拷贝初始化构造函数去完成初始化工作。拷贝构造函数的作用是完成同类对象间相互拷贝。
拷贝构造函数是通过一个同类型的引用参数来完成的,它的格式为:
T(const T&)
例如,一个Point类使用拷贝构造函数的定义和使用如下:
程序清单2-9:使用拷贝构造函数
|
#include "iostream.h"
class Point{
int x,y;
public:
Point(){};
Point(int a ,int b);
Point(const Point& pa);
int get_x(){return x;}
int get_y(){return y;}
};
Point::Point(int a,int b):x(a),y(b)//实现成员变量赋值
{}
Point::Point(const Point& pa)// 实现对象拷贝
{x=pa.x;
y=pa.y;
}
void main()
{Point p1(100,150); //调用带参构造函数初始化p1
Point p2(p1);
//以p1的值初始化p2
cout<<"x="<<p2.get_x()<<endl;
cout<<"y="<<p2.get_y()<<endl;
}
运行结果:
x=100
y=150
|
拷贝构造函数的另一个应用场合是一个数据成员是另一类的对象,对其进行初始化时,需要进行对象拷贝,该数据成员所使用的类需要提供拷贝构造函数。例如:
class T1
{....
T1(const T1&);
}
class T2
{T1 a;
int x;
T2 (T1 b ,int x );
}
在类T2中有一个数据成员a是T1类的对象,T2的构造函数中,直接用一个参数值b对a进行初始化,这就要求T1类必须提供拷贝构造函数,支持对象拷贝。
2.2.4 this指针
this
是一个指向调用该成员函数的对象的常量指针。成员函数可通过this指针直接使用同类的其它成员,而不需要在每个成员之前加“对象名”和成员运算符“.”。如图2-2所示,在CExam1_1View类的成员函数OnDraw中输入this,这时this指针表示的是CExam1_1View类,当输入箭头时,就会弹出一个下拉列表,显示CExam1_1View类的成员供选用。
图2-2 this指针的使用
实际上,this指针在C++中纯粹是一个概念,实际编程时并不需要使用this,因为它是隐含的。理解this指针的目的是理解this指针的作用,即在
成员函数中使用某一数据成员时并不需要指出该数据成员是属于哪一个对象的,每一个数据成员之前都隐含有“
this->
”。
2.2.5程序实例
【例2-1】定义一个正方形类,该类包括:
(1) 保护类数据成员,表示正方形的边长(值的范围为1-30个‘*’),
(2) 四个成员函数,功能分别为:①取边长;②设置边长;③画正方形;④在构造该类对象时,初始化正方形的边长。
编制main函数实现:
(1) 创建一个边长为5的正方形;
(2) 调用成员函数打印正方形的形状;
(3) 由用户输入正方形边长,修改边长的值;
(4) 重新打印正方形。
按照题目要求正方形Square类包括保护类数据成员len,公有类成员函数:带参构造函数,取边长函数GetLen(),设置边长函数SetLen(),画正方形函数DrawSquare()。实现代码如程序清单2-10所示。
程序清单2-10:实现正方形类及应用
|
#include "iostream.h"
class Square
{
protected:
int len;
public:
Square(){}
Square(int x){len=x;}
int SetLen(int x);
int GetLen(){return len;}
void DrawSquare();
};
int Square::SetLen(int x)
{
if(x<1 ||x>30 )
//检查x的值在1-30之间
return 0;
len=x;
//设置边长
return 1;
}
void Square::DrawSquare()
{ int i,j;
for(i=0;i<len;i++)
{for(j=0;j<len;j++)
cout<<'*';
cout<<endl;
}
}
void main()
{ Square s(5);
cout<<"draw the square,len="<<s.GetLen()<<endl;
s.DrawSquare();
cout<<"input the new side length:";
int x;
cin>>x;
if (!s.SetLen(x))
cout<<"error, length should be between 1 and 30! ";
else
{cout<<"draw the square,len="<<s.GetLen()<<endl;
s.DrawSquare();
}
}
|
程序运行结果如下:
程序运行时定义了一个Square类的对象s,并定义边长为5个‘*’,然后调用s的成员函数GetLen获得s的边长并输出边长信息,调用s的成员函数DrawSquare画出图形。允许用户输入新的正方形边长,例如3个‘*’,重复开始的输出步骤输出边长信息和图形。
2.3 继承
继承描述的是类与类之间的关系问题。把在已有类的基础上定义新类的过程称为继承。继承的本质是实现代码重用,因而,继承机制能缩短软件的开发周期,加快编程速度。
2.3.1基类和派生类
被继承的类成为基类(或父类),基于父类并增加新特性从而派生出的类称为派生类(或子类)。派生类继承了基类中的部分或全部方法,也可修改基类中的方法,增加基类中没有的方法。
定义派生类的一般格式如下:
class 派生类名:继承方式
基类名
{
... //本派生类新定义的成员
};
其中,派生类名是新定义的类名;继承方式有public、private、protected三种,默认为private方式。继承方式决定了派生类对基类的访问性质。派生类对基类的继承情况如表2-1所示。
表2-1不同继承方式下派生类对基类的访问权限
继承方式
|
基类成员访问权限
|
派生类访问权限
|
public
(公有继承)
|
public
|
public
|
protected
|
protected
| |
private
|
不可访问
| |
protected
(保护继承)
|
public
|
protected
|
protected
|
protected
| |
private
|
不可访问
| |
private
(私有继承)
|
public
|
private
|
protected
|
private
| |
private
|
不可访问
|
(1) public继承方式
基类中的私有成员派生类不可访问,不可继承,基类中其他类型的成员可以被派生类继承,而且继承后的访问权限不变。
(2) protected继承方式
基类中的私有成员派生类不可访问,不可继承,基类中其他类型的成员可以被派生类继承,但是继承后的访问权限都变成保护的。
(3) private继承方式
基类中的私有成员派生类不可访问,不可继承,基类中其他类型的成员可以被派生类继承,但是继承后的访问权限都变成私有的,即不可再向下派生。
public继承方式揭示了父类与子类在概念上的相关性,子类应是父类的特例。当描述相关的两个实体时,一般采用public继承方式。
构造函数不能被继承,因此,
派生类的构造函数必须调用基类的构造函数来初始化基类的子对象。所以,在定义派生类的构造函数时,除了对自己的数据成员进行初始化外,还必须(先)调用基类的构造函数,初始化基类的数据成员。析构函数也不能被继承。所以,执行派生类的析构函数时,基类的析构函数也被调用,执行顺序是先执行派生类的析构函数,再执行基类的析构函数。
【例2-2】设计两个类,一个类描述点,另一个类描述圆,圆由圆心和半径构成,圆类由点类派生而来,其中圆心的特性描述由点类继承下来。要求圆类提供(
1)求圆面积的成员函数(2)取圆心坐标的两个函数(3)支持初始化的带参构造函数。
如程序清单2-11所示,Point类为Circle类的基类,Circle类继承了Point类的点的特性,用于描述圆心,还继承取得点的坐标的两个成员函数get_x()和get_y()。Circle类在基类的基础上进行了扩展,增加了半径的特性,并增加了四个成员函数:Circle类的构造函数的实现调用Point类的构造函数;获取圆心坐标的两个函数get_centreX()和get_centreY(),函数的实现直接调用get_x()和get_y()实现;求圆的面积函数Area()。
程序清单2-11:派生类的实现和应用
|
#include "iostream.h"
class Point
// 定义基类
{
int m_x, m_y;
public:
Point(int x,int y){ m_x=x; m_y=y; }
int get_x(){ return m_x; }
int get_y() { return m_y;}
};
class Circle : public Point
//定义派生类,公有继承
{
double radius;
public:
Circle(int x,int y,double r) : Point(x,y)//派生类没有继承基类的构造函数,而是
{ radius=r;} //通过访问基类的构造函数初始化基类的数据成员。
double Area(void) {return 3.14*radius*radius; } //计算圆面积
int get_centreX() {return get_x();} //调用基类中的成员函数访问基类数据,获得
int get_centreY() {return get_y();} //圆心坐标
};
void main()
{
int x,y;
double r;
cout<<"x="; cin>>x;
cout<<"y="; cin>>y;
cout<<"r="; cin>>r;
Circle c(x,y,r);
cout<<"the centre of the Circle is "<<c.get_centreX()<<" and "<<c.get_centreY()<<endl;
cout<<"the Area of the Circle is "<<c.Area()<<endl;
}
运行结果:
x=100
y=100
r=50
the centre of the Circle is 100 and 100
the Area of the Circle is 7850
|
程序运行时,由用户输入圆心的坐标[x,y]的值,及半径r的值,例如x为100,y为100,半径为5。定义一个Circle对象c,以用户的输入值初始化这个对象。接着调用Circle类的成员函数get_centreX()和get_centreY()输出圆心位置,调用成员函数Area()输出圆面积。
2.3.2多继承
C++允许派生类有两种方式继承基类:即单继承与多继承。单继承指派生类只从一个基类派生,上一节中的Circle类即为单继承派生类;多继承指派生类从多个基类派生,派生类具有多个基类的特性。
多继承派生类的定义格式如下:
class 派生类名:继承方式1 基类名1,…继承方式n 基类名n
{
... //本派生类新定义的成员
};
【例2-3】如图
2-3所示,一个图形是由一个圆和一个矩形构成,要求求解图形的面积。设计三个类:其中两个是基类,一个基类描述圆,一个基类描述矩形;第三个派生类是由一个圆和一个矩形构成的图形类,如下图所示。圆类包含数据成员半径和求圆面积的成员函数,矩形类包含数据成员长和宽,求矩形面积的成员函数。派生的图形类提供输出图形面积的函数,及支持初始化的带参构造函数。
圆类
矩形类
图形类
图2-3 图形类的构成
|
程序清单2-12:多继承示例
|
#include "iostream.h"
const double PI =3.14;
class Circle
//定义基类
{
double radius;
public:
Circle(double r)
{ radius=r;}
double CircleArea(void) {return PI*radius*radius; } //求圆面积
};
class Rectangle //定义基类
{
double length,width;
public:
Rectangle(double x,double y)
{length=x,width=y;}
double RecArea(void){return length*width;} //求矩形面积
};
class Graph:public Circle,public Rectangle //定义多继承派生类
{
public:
Graph(double r,double x,double y):Circle(r),Rectangle(x,y)
{}
void ShowArea(void) //求图形面积,调用基类成员函数
{double TotalArea;
TotalArea= CircleArea()+RecArea();
cout<<"the Area of Graph is "<<TotalArea<<endl;
}
};
void main()
{
double x,y,r;
cout<<"r="; cin>>r;
cout<<"x="; cin>>x;
cout<<"y="; cin>>y;
Graph g(r,x,y);
g.ShowArea ();
}
运行结果:
r=10
x=20
y=50
the Area of Graph is 1314
|
程序中定义了两个基类:
Circle类表示圆类,包含一个数据成员半径radius,一个带参构造函数,和一个求圆面积的成员函数CircleArea();Rectangle类表示矩形类,包含两个数据成员长length和width,一个带参构造函数,和一个求矩形面积的成员函数RecArea()。程序中定义的派生类Graph 公有继承两个基类,派生类的带参构造函数访问基类的构造函数为基类的数据成员初始化,所以两个基类中必须提供带参构造函数。派生类的成员函数ShowArea调用基类的公有类成员函数CircleArea()和RecArea()获得圆面积和矩形面积,求出图形面积并输出。
程序运行时,由用户输入圆的半径,矩形的长和宽,定义一个
Graph对象g,由用户输入的数值初始化对象g。对象g调用成员函数ShowArea输出图形面积。
2.4 虚函数
2.4.1多态性
前面我们介绍了一个类中的普通成员函数的重载,在一个继承的体系中,应该允许
在派生类中对基类的函数进行重载,这样可以在基类和派生类中使用同样的函数名而定义不同的操作,就出现了在一个继承体系中,多个类中的函数重载。能够实现“一种接口,多种方法”的技术,就是多态性。
区分不同实现的重载函数有静态联编和动态联编两种方式。
静态联编的解决方式有:
(1) 根据函数的参数类型和个数等参数特性的不同,来确定重载函数的归属。
如:double Area(double radius);
double Area(double length,double width)
(2) 通过类名和作用符指定要执行的重载函数的归属。
如:Circle::Area();
Rectangle::Area();
(3) 通过对象或对象指针调用函数,对象或对象指针的类型决定重载函数的归属。
如:Circle cir; cir.Area();
Rectangle rec; rec.Area();
静态联编的实现都是在编译的时候就确定下来的,而动态联编方式不同,C++允许在程序运行时才确定重载函数的归属。要实现动态联编,首先要介绍一种基类指针在派生类与基类转换中的作用。
2.4.2派生类与基类的转换
在继承的关系下,每一个派生类的对象是包含有一个基类的公有继承部分,例如基类为CA,派生类为CB,定义一个派生类对象B,它的内存结构如图2-4所示,在对象B的首部是基类CA的内容,接下来才是CB类的增加的成员。
CA
类公有继承部分
|
CB
类新增加的成员
|
图2-4派生类对象的构成
所以派生类对象和基类对象存在以下赋值关系:
(1) 派生类对象可以直接赋值给其public基类的对象,其实质就是将派生类中所包含的与基类相符的内容赋给基类对象。
(2) 若将以基类对象赋值给派生类对象,必须明确使用强制类型转换方式来实现赋值。如:假设已定义基类对象A,使用A对B赋值,要进行强制类型转换:
B=(CB)A;
C++虽允许这种转换,可以通过编译,但是在使用上却是危险的。此时B虽然是派生类CB的对象,但通过赋值仅仅得到了基类部分的数据,若在使用中需要调用其派生类中增加部分的成员,将发生错误。
(3) 指向派生类对象的指针或引用可以直接赋值给其public基类的对象指针或引用。
(4) 同样的道理可以用一个基类指针访问派生类对象,但反之则不行,不能用指向派生类的指针指向一个基类的指针。如果希望用指向基类的指针访问其公有派生类的对象的特定成员,只需将基类指针用强制类型转换方式转换为派生类指针。
在上面所示的关系中,最重要的一点是一个
指向基类的指针可用来指向从基类公有派生的任何对象,这是C++实现虚函数与多态性的关键途径。下面程序给出了使用指向基类的指针调用派生类函数的示例。
程序清单2-13:派生类与基类的转换
|
#include "iostream.h"
class Sharp
{double x,y;
public :
void SetValue(double i,double j){x=i,y=j;}
double GetX(){return x;}
double GetY(){return y;}
};
class Rectangle:public Sharp
{
public:
void Area(){cout<<"the area of rectangle is "<<GetX()*GetY()<<endl;}
};
void main()
{
Sharp *bs;
Rectangle rec;
rec.SetValue(3.0,4.0);
bs=&rec;
//通过地址赋值,使基类指针指向派生类对象
((Rectangle*)bs)->Area(); //基类指针调用派生类的成员函数
}
|
2.4.3虚函数
从上一节的实例可以看到,使用基类指针访问派生类的成员,必须对基类指针执行强制类型转换。如果在继承体系中引入虚函数,只要
在基类中将需要重载的函数定义为虚函数,派生类重载该函数后,只需对基类指针赋予不同对象的地址,不需进行强制类型转换,系统将根据运行时指针所指向的实际对象来确定调用哪一个类的成员函数版本。设置虚函数是指在需要设置的成员函数前加上关键字“virtual”。
请看下面的例子,Sharp类是Circle类和Rectangle类的基类,在Sharp类中定义了虚函数Area(),该函数功能是输出一行文本串,Circle类和Rectangle类中各自重载了Area函数,分别进行求圆的面积和矩形的面积。在main函数中,定义了一个基类指针 bs,一个Circle对象 和一个Rectangle对象。当bs指针指向不同类型的对象时,系统自动识别调用不同类型的Area函数的实现版本。
程序清单2-14:虚函数的实例
|
#include "iostream.h"
const double PI =3.1415926;
class Sharp
{protected:
double x,y;
public:
void set_value(double i,double j=0)
{x=i;y=j;}
virtual void Area(){cout<<" this is a sample of virtual fuction/n";}//设置虚函数
};
class Circle:public Sharp
{
public:
void Area(void) {
cout<<"the area of Circle is "<<PI*x*x<<endl; } //求圆面积
};
class Rectangle:public Sharp
{
public:
void Area(void){cout<<"the area of Rectanle is "<<x*y<<endl;} //求矩形面积
};
void main()
{
Sharp *bs;Circle cir;Rectangle rec;
bs=○ bs->set_value(10);bs->Area(); //基类指针访问Circle类成员函数
bs=&rec; bs->set_value(3.5,5.0);bs->Area();//基类指针访问Rectangle类成员函数
}
运行结果:
the area of Circle is 314.159
the area of Rectanle is 17.5
|
上述结果说明,通过虚函数和指向不同对象的基类指针,C++系统能自动判断应该调用哪一个类对象的成员函数。由于所调用函数的版本是在程序运行时确定的,也称为运行时的多态性。
在继承体系下,用虚函数实现运行时的多态性有三要素:首先,在基类定义中,必须把成员函数定义为虚函数,即在正常函数定义之前加关键字“virtual”;其次,在派生类的定义中,对虚函数的重新定义只能修改函数体内容,而函数名、返回类型、参数个数、参数类型及参数顺序必须与基类的定义完全相同;最后,必须用指向基类的指针(或引用)访问虚函数。
虚函数必须是类的成员函数。析构函数可以是虚函数,但构造函数不能为虚函数。一旦一个函数被说明为虚函数,不管经历了多少派生类层次,都将保持其虚函数的特性。在一个派生类中,可以通过基类名和作用域运算符来使用其直接基类的虚函数版本。例如,在程序清单2-14中Circle类的定义中增加调用基类成员函数Area的语句如下。
程序清单2-15:虚函数的实例
|
class Circle:public Sharp
{
public:
void Area(void) {
Sharp::Area();
cout<<"the area of Circle is "<<PI*x*x<<endl; } //求圆面积
};
再次运行这个程序,运行结果为:
this is a sample of virtual fuction
the area of Circle is 314.159
the area of Rectanle is 17.5
|
2.4.4抽象类
1.
纯虚函数
设计面向对象的程序时,并非必须使用虚函数。但利用虚函数,为一个类体系中所有子类的同一行为提供了统一的接口,使所设计的软件系统更加灵活。
如果基类的虚函数在派生类中没有重定义,那么指向派生类对象的指针调用该函数时,调用的肯定是基类中定义的版本。然而在许多情况下,基类的虚函数是无法定义的。这时有两种处理方法:一是在函数中进行异常处理,警告必须在派生类中重新定义该函数后再使用;一是使用纯虚函数。
纯虚函数是在基类中声明但没有定义的虚函数。如果基类中包含纯虚函数,那么任何派生类都必须重定义该函数,因为不能直接使用从基类继承下来的虚函数。
纯虚函数是一种特殊的虚函数,它的一般格式如下:
class <类名>
{
virtual <
类型
><
函数名
>(<
参数表
>)=0;
…
};
在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它说明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。
下面给出一个纯虚函数的例子。Point类是Line类和Circle类的基类,在Point类中定义了纯虚函数set()和draw(),含义为:给出位置并画出图形。由于在基类中,无法确定线或圆的位置,也无法提供画出线和圆的具体方法,所以,将set()和draw()说明成纯虚函数,以便在派生类中进一步说明。
程序清单2-16:纯虚函数的实例
|
#include <iostream.h>
class Point
{
public:
Point(int i=0, int j=0)
{
a0=i; b0=j;
}
virtual void set() = 0;
virtual void draw() = 0;
protected:
int a0, b0;
};
class Line : public Point
{
public:
Line(int i=0, int j=0, int m=0, int n=0):Point(i, j)
{
a1=m; b1=n;
}
void set() { cout<<"Line::set() be called./n"; }
void draw() { cout<<"Line::draw() be called./n"; }
protected:
int a1, b1;
};
class Circle : public Point
{
public:
Circle(int i=0, int j=0, int p=0, int q=0):Point(i, j)
{
a2=p; b2=q;
}
void set() { cout<<"Circle::set() be called./n"; }
void draw() { cout<<"Circle::draw() be called./n"; }
protected:
int a2, b2;
};
void setobj(Point *p)
{
p->set();
}
void drawobj(Point *p)
{
p->draw();
}
void main()
{
Line *line1 = new Line;
Circle *circle1 = new Circle;
drawobj(line1);
drawobj(circle1);
cout<<endl;
setobj(line1);
setobj(circle1);
cout<<"/nRedraw the object.../n";
drawobj(line1);
drawobj(circle1);
}
运行结果:
Line::draw( ) be called.
Circle::draw( ) be called.
Line::set( ) be called.
Circle::set( ) be called.
Redraw the object…
Line::draw( ) be called.
Circle::draw( ) be called
|
2.
抽象类
如果
一个类中至少有一个纯虚函数,则这个类被称为抽象类(
abstract class
)。抽象类有一个重要的特点:
必须用作派生其他类的基类,而不能用于直接创建对象实例。原因是其中有一个或多个函数没有定义,但仍可使用指向抽象类的指针支持运行时的多态性。
抽象类是一种特殊的类,它是为抽象和设计的目的而建立的,处于继承层次结构的较上层。由它来提供一个公共的根,相关的子类是从这个根派生出来的。
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。
如果派生类没有重新定义纯虚函数,只是继承基类的纯虚函数,则这个派生类仍是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了。
实验
实验1:类的概念
1. 定义一个角度类CAngle,包含的内容有:
(1) 私有成员变量value ,表示角度值,类型为double;
(2) 公有成员函数SetValue,实现功能是使用形参为value赋值。声明为:
void SetValue(double);
(3) 公有成员函数GetCos,实现功能是计算余弦值,并作为返回值返回。声明为:
double GetCos();
编程实现:
定义一个CAngle类的对象deg,调用成员函数设置deg的角度为30,调用成员函数计算输出deg的余弦值。
2. 定义一个网上购书定单类BookOrder,包含的内容有:
(1) 私有成员变量BookID,表示图书编号,类型char[6];
(2) 私有成员变量Quantity,表示图书的数量,类型int;
(3) 公有成员变量Price,表示图书的单价,类型double;
(4) 公有成员函数BookInit,实现功能是使用参数为成员变量赋值。声明为:
void BookInit(char*,int ,double);
(5) 公有成员函数BookInfo,实现功能是显示订单信息:图书编号、数量、单价及总价。声明为:
void BookInfo();
编程实现:
(1) 创建图书订单类的一个对象bkorder;
(2) 交互式由用户输入订单信息:图书编号、数量、单价;
(3) 调用成员函数BookInit设置bkorder的成员变量的值;
(4) 调用成员函数BookInfo显示图书订单的内容;
(5) 修改图书的单价为现有单价的80%;
(6) 再次调用成员函数BookInfo显示图书订单的内容。
实验2:构造函数和析构函数
1. 定义一个日期类Date,包含的内容有:
(1) 三个私有成员变量:year,month,day表示年、月、日,类型为int;
(2) 公有成员函数无参构造函数,实现功能为将三个成员变量的值初始为2003-1-1,输出“Constructor be called.”;
(3) 公有成员函数带参构造函数,实现功能为使用参数值为三个成员变量赋值,输出“Constructor be called.”;
(4) 公有成员函数析构函数,实现功能为输出“Destructor be called”;
(5) 公有成员函数打印输出函数PrintDate输出具体的年、月、日。
编程实现:
(1) 创建Date类的对象d1;
(2) 调用 PrintDate输出d1的内容;
(3) 创建Date类的对象,并按日期2003-7-12对d2初始化;
(4) 调用 PrintDate输出d1的内容。
2. 修改Date类的定义:
(1) 增加三个公有函数设置三个成员变量的值;
(2) 增加拷贝构造函数支持对象拷贝。
编程实现:
(1) 由用户输入当天的年、月、日;
(2) 创建Date类的对象today ,以用户输入的日期初始化today;
(3) 创建Date类的对象tomorrow,使用对象today为tomorrow初始化;
(4) 修改tomorrow的成员变量的值,使之为today第二天的值,考虑日期表示的合理性。例如6月份不能有第31天;
(5) 输出tomorrow的值。
实验3:继承和虚函数
1. 定义一个时间类Time,Time类的内容包含:
(1) 保护类型成员变量Hour,表示小时,类型为int;
(2) 保护类型成员变量Min,表示分钟,类型为int;
(3) 保护成员变量Sec,表示秒,类型为int;
(4) 公有成员函数SetTime,功能为用参数值为Hour,Min,Sec赋值。声明为:
void SetTime(int ,int ,int);
(5) 公有成员函数ShowTime,功能为以24小时制显示时间值。声明为:
void ShowTime();
2. 定义派生类NewTime,公有继承Time类。在派生类中定义两个重载成员函数:
(1) 声明为void ShowHour(),实现功能为以24小时制显示当前的Hour值;
(2) 声明为void ShowHour(int flag),实现功能为当flag为0时,以12小时制显示当前的Hour值;当flag 为其它值时,以24小时制显示当前Hour。
3. 在main 函数中实现:
(1) 定义Time类的对象time,以时间“23:34:45”为time赋值;
(2) 调用成员函数ShowTime显示时间;
(3) 定义NewTime类对象nt,并在Time类和NewTime类中定义适当的构造函数,实现用对象time的时间值为nt初始化;
(4) 分别以24小时制和12小时制输出nt的Hour值。
4. 设置Time类的成员函数ShowTime()为虚函数,在NewTime中重载该函数,以12小时制显示时间值,并给出上下午信息。在main函数定义基类指针Time类指针basetime,用此指针分别指向对象time和newtime,依次调用ShowTime进行输出。
自测题
1.函数重载是指
,在VC++中,要求重载函数的
和
必须有所不同,否则产生二义性。
2.类的成员的访问类型有
种,分别为
、
、
。
3.构造函数的作用是
,析构函数的作用是
。
4.派生类继承了基类中的
,也可修改
,增加基类中没有的
。
5.在继承体系下,用
实现运行时的多态性。通过
和指向不同对象的基类指针,C++系统能自动判断应该调用哪一个类对象的成员函数。由于所调用函数的版本是在程序运行时确定的,也称为
。
6.观察实验2的输出,讨论构造函数和析构函数在何时被系统自动调用?
7.阅读Base类、FstDerive类、SecDerive类的定义,回答问题。
(1) 派生类FstDerive中成员函数PubMemb2()能否访问基类Base中的成员:PubMemb1(),ProMemb1和PriMemb1呢?
(2) 派生类SecDerive中的成员函数PubMemb3()能否访问直接基类FstDerive中的成员:PubMemb2()和ProMemb2呢?能否访问间接基类Base中的成员PubMemb1(),ProMemb1和PriMemb1呢?
class Base
{ public:
void PubMemb1( );
protected:
int ProMemb1;
private:
int PriMemb1;};
class FstDerive:public Base
{ public:
void PubMemb2( );
protected:
int ProMemb2;
private:
int PriMemb2;};
class SecDerive: public FstDerive
{ public:
void PubMemb3 ( );};
8.在例2-3中,如果两个基类Circle和Rectangle的求面积的成员函数都命名为Area,派生类Graph的成员函数要如何调用基类的成员函数,才能正确输出图形面积?
小结
1. C++支持单行注释“//”和多行注释“/* ...*/”两种方式。
2. 在C++中可以在任意地方进行变量定义。
3. 在C++中使用const进行符号常量的定义,在常量定义时必须指明数据类型。
4. C++提供了强制转换函数进行强制类型转换,函数名为简单数据类型的名称。
5. C++提供了操作符new和delete完成动态变量或动态数组的创建和释放。
6. 引用变量的实质是一个常量指针,在定义引用变量时必须指定引用变量的指向地址,并且在程序运行的时候不能发生改变指向。引用变量通常在函数调用时作为参数进行值的双向传递。使用引用变量比使用指针的程序可读性和隐蔽性更强。
7. C++的编译系统对数据类型匹配的检查更为严格,所以对函数原型的要求也更加严格,所有在函数定义之前进行函数调用的场合下,函数原型必不可少。
8. C++允许一个程序声明多个名称相同的函数,这就是函数重载,这些函数通常执行相同的功能,但是带有不同的类型、不同数目的参数及不同类型的返回值,编译系统根据参数或返回值的差异自动判断在不同的调用场合下执行不同的函数实现。
9. 在C++中可以对函数参数设置默认值,函数调用时对一些通用参数的省略实参输入,但又允许在某些场合中对这些参数作修改,增加了函数使用的灵活性。
10. 类的实质是一种新的数据结构,是一组对象的抽象,描述了具有一组相同特性(数据成员)和相同行为(成员函数)的对象,类的定义格式为:
class 类名
{
private:
私有段数据及函数;
protected:
保护段数据及函数;
public:
公有段数据及函数;
};
11. 一个对象是类的一个实例化,其实质就是变量,所以对象的定义也就类似变量的定义,同样存在普通对象、对象数组、对象指针和对象指针数组的概念,以及它们的定义和引用。
12. 对成员的访问类型有三种:private、protected和public。与结构体类型设置相反,不作访问类型设置的成员默认为private类型。类的封装性体现在:一般情况下数据成员的访问类型为private,成员函数的访问类型为public,这样就拒绝外部对象属性的任意更改,同时public类型的成员函数实质是提供给外部的访问接口,可以通过这些成员函数有限地访问对象属性,也就是说使用类能够很好地控制外部对数据的操作行为。protected类型用于一个继承体系中,对于外部来说,与private类型一样protected类型的成员是拒绝访问的。对于派生类来说,基类中protected类型的成员是可以访问的,但是基类中private类型的成员是不可访问的。
13. 构造函数和析构函数是类中用于初始化和清理的两个特殊函数,它们与类同名,由系统自动调用。构造函数是可以重载的函数,构造函数一般包括三种类型:无参构造函数,带参构造函数和拷贝构造函数,以适合不同的初始化场合。析构函数不能重载,一般系统默认的析构函数就能完成常规的清理工作。
14.
this
指针是一个特殊的指针,它在成员函数中出现,表示成员函数所属的类。
15. C++支持由已有的一个或多个类生成新的类,并可以修改和扩展新类,以实现代码重用。被继承的类称为基类(或父类),基于父类并增加新特性从而派生出的类称为派生类(或子类)。
16. 继承的方式有三种,分别是:private、protected和public。通常情况下采用的是public继承,派生类完全继承基类的成员。当要阻止派生类继续向下派生时,使用private继承。当继承的所有基类成员只允许派生类成员访问,而不允许派生类对象调用时,采用protected继承。
17. 多态性是指能够实现“一种接口,多种方法”的技术,它的实现是依靠函数重载。区分不同实现的重载函数有静态联编和动态联编两种方式。静态联编的解决方式有:
(1) 根据函数的参数类型和个数等参数特性的不同,来确定重载函数的归属。
(2) 通过类名和作用符指定要执行的重载函数的归属。
(3) 通过对象或对象指针调用函数,对象或对象指针的类型决定重载函数的归属。
18. 动态联编,是在程序运行时实现重载函数匹配,实现方法为设置基类函数为虚函数,再使用基类函数指向不同类的对象,调用不同类的重载函数。