第4章 类与对象
4.1 面向对象程序设计的基本特点:抽象、封装、继承、多态
抽象:面向对象方法中的抽象,是指对具体问题进行概括,抽出一类对象的公有性质并加以描述的过程。
封装:将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体,也就是将数据与操作数据的代码进行有 机的结合,形成“类”,其中的数据和函数都是类的成员。
继承:在一般概念上派生出特殊概念,使得一般概念中的属性和行为可以被特殊概念共享,摆脱重复性、重复开发的困境。允许程序员在保持原有特性不变的基础上,进行更具体、更详细的说明。
多态:一段程序能处理多种类型的能力。
多态的四种实现方式:强制多态、重载多态、包含多态、类型参数化多态。
又可分为:特殊多态性(只是表面的多态性)(包括强制多态、重载)和一般多态性(真正的多态性)(包括包含多态、类型参数化多态)。
强制多态:通过将一种类型的数据转换成另一种类型的数据来实现,也就是数据类型转换(隐式或显式)。
重载:指给同一个名字赋予不同的含义(函数重载、运算符重载)。
包含多态:C++中采用虚函数实现包含多态。
类型参数化多态:C++中采用模板实现类型参数化多态,包括函数模板和类模板。
4.2 类和对象
4.2.1 类的定义
定义类:
class 类名称{
public:
外部接口
private:
私有类型成员
protected:
保护类型成员
};
4.2.2 类成员的访问控制
对成员访问权限的控制,是通过设置成员的访问控制属性来实现的。访问控制属性包括三种:公有类型(public)、私有类型(private)、保护类型(protected)。
公有类型成员(public)定义了类的外部接口。
私有类型成员(private)只能被本类的成员函数访问,来自类外部的任何访问都是非法的。
保护类型成员(protected)的性质类似私有成员,其差别在于继承过程中对产生新类的影响不同(见第7章)。
4.2.3 对象
类的对象:就是该类的某一特定实体(实例)。
声明一个对象:
类名 对象名;
注意:对象所占据的内存空间只用于存放数据成员,而函数成员不在每一对象中存储副本,每个函数的代码在内存中只占据一份空间。
类内成员互访:直接使用成员名
类外访问数据成员:
对象名.数据成员名
类外调用函数成员:
对象名.函数成员名(参数表)
4.2.4 类的成员函数
4.2.4.1 成员函数的实现
类的成员函数:用于描述类的行为。
类的成员函数一般先在类体中作原型声明,然后在类外定义,有利于封装,供别人调用接口。虽然成员函数在类的外部定义,但在调用成员函数时会根据在类中声明的函数原型找到函数的定义(函数代码),从而执行该函数。
如果一个函数,其函数体只有2-3行,可在声明类时在类体中定义(变成隐式的内联成员函数,见4.2.4.4)。多于3行的函数一般在类体中声明,在类外定义。
在类体中直接定义函数时,不需要在函数名前面加上类名来限定,因为函数属于哪一个类是不言而喻的。
而在类外定义(实现)成员函数时,类的成员函数名要用**类名加作用域运算符(限定符)::
**来限制:
返回值类型 类名::函数成员名(参数表)
{
函数体
}
4.2.4.2 成员函数调用中的目的对象
调用成员函数,需要使用**“.”操作符**指出调用所针对的对象,这一对象在本次调用中称为目的对象。
在成员函数中可以不使用“.”操作符而直接引用目的对象的数据成员。
在成员函数中引用其他对象的属性和调用其他对象的方法时,都需要使用“.”操作符。
注意:在成员函数中,既可以访问目的对象的私有成员,又可以访问当前类的其他对象的私有成员。
4.2.4.3 带默认形参值的成员函数
类的成员函数(如果赋)默认形参值,一定要写在类定义中,而不能写在类定义之外的函数实现中。
class Clock{
public:
void setTime(int newH=0, int newM=0, int newS=0);
...
};
4.2.4.4 内联成员函数
如果有的函数成员需要被频繁调用,而且代码比较简单,这个函数可以定义为内联函数(inline function)。
与普通内联函数相同,内联成员函数的函数体会在编译时插入到每一个调用它的地方。这样可以减少调用的开销,提高执行效率,但会增加编译后代码的长度,所以只有对相当简单的成员函数才可以被声明为内联函数。
内联成员函数的两种声明方式:隐式声明、显式声明。
隐式的内联成员函数:将函数体直接放在类体中,在类体中实现(不加inline
),比如将时钟类的showTime()
函数声明为内联函数:
class Clock{
pubilc:
void setTime(int newH, int newM, int newS);
void showTime(){
cout<<hour<<":"<<minute<<":"<<second<<endl;
}
private:
int hour, minute, second;
};
显式的内联成员函数:在类外的函数体实现时,在函数返回值类型前面显式地加上inline
(类定义中showTIme()
不加函数体):
inline void Clock::showTime(){
cout<<hour<<":"<<minute<<":"<<second<<endl;
}
4.3 构造函数和析构函数
在定义对象的时候进行数据成员的设置,称为对象的初始化。
4.3.1 构造函数
构造函数的作用:(分配内存空间,赋初始值)在对象被创建时利用特定的值构造对象,将对象初始化为一个特定的状态。
构造函数也是类的一个成员函数,通常被声明为公有函数。
构造函数的特殊性质:函数名与类名相同,无返回值。
自定义的构造函数带有形参,那么建立对象时就必须给出初始值,用来作为调用构造函数时的实参。
class Clock{
public:
//构造函数的函数名与类名相同,前面无返回值类型
Clock(int newH, int newM, int newS);
void setTime(int newH, int newM, int newS);
void showTime();
private:
int hour, minute, second;
};
//构造函数的实现:
Clock::Clock(int newH, int newM, int newS){
hour=newH;
minute=newM;
second=newS;
}
构造函数的执行方式:构造函数在对象被创建时被自动调用,只要类中有构造函数,编译器就会在建立新对象的地方自动插入对构造函数调用的代码。
默认构造函数:调用时无需提供参数的构造函数。
如果类中没有写构造函数,编译器会自动生成一个隐含的默认构造函数,该构造函数的参数列表和函数体皆为空。如果类中声明了构造函数(无论是否有参数),编译器便不会再为之生成隐含的构造函数。
class Clock{
public:
Clock(){} //编译系统生成的隐含的默认构造函数
};
构造函数可以重载。(重载:函数名相同,形参个数或类型不同)
class Clock{
public:
//构造函数可以重载
Clock(int newH, int newM, int newS); //有参数的构造函数
Clock(){ //无参数的构造函数
hour=0;
minute=0;
second=0;
}
void setTime(int newH, int newM, int newS);
void showTime();
private:
int hour, minute, second;
};
//有参数的构造函数的实现
Clock::Clock(int newH, int newM, int newS){
hour=newH;
minute=newM;
second=newS;
}
//调用构造函数
int main(){
Clock c1(0, 0, 0); //调用有参数的构造函数
Clock c2; //调用无参数的构造函数
}
4.3.2 复制构造函数(拷贝构造函数)
复制构造函数是一种特殊的构造函数,具有一般构造函数的所有性质。
复制构造函数的形参是本类的对象的引用。
复制构造函数的作用:使用一个已经存在的(由复制构造函数的参数指定的)对象,去初始化同类的一个新对象。 本质:类对象对类对象赋值。
如果程序员没有定义类的复制构造函数,系统就会在必要时自动生成一个隐含的复制构造函数。这个隐含的复制构造函数的功能是,把初始值对象的每个数据成员的值都复制到新建立的对象中。
声明和实现复制构造函数:
class 类名{
public:
类名(形参表); //构造函数
类名(类名 & 对象名); //复制构造函数
...
};
类名::类名(类名 & 对象名){ //复制构造函数的实现
函数体
}
普通构造函数在对象创建时被调用,而复制构造函数在以下三种情况下都会被调用:
(1)当用类的一个对象取初始化该类的另一个对象时。
int main(){
Point a(1, 2); //创建Point类的对象a并赋初始值,构造函数被调用
Point b(a); //用对象a初始化对象b,复制构造函数被调用
Point c=a; //用对象a初始化对象c,复制构造函数被调用
}
(2)如果函数的形参是类的对象,调用函数时,进行形参和实参的结合。
void f(Point p){ //函数f的形参是Point类的对象
cout<<p.getX()<<endl;
}
int main(){
Point a(1, 2);
f(a); //函数f的形参是Point类的对象a,调用函数f时复制构造函数被调用
return 0;
}
注意:只有把对象用值传递时,才会调用复制构造函数,如果对象为引用传递,则不会调用复制构造函数。由于这一原因,传递比较大的对象时,传递引用会比传值的效率高很多。
(3)如果函数的返回值是类的对象,函数执行完成返回调用者时。
Point g(){
Point a(1, 2);
return a; //函数g的返回值a是Point类的对象,返回函数值时调用复制构造函数
}
int main(){
Point b;
b=g();
return 0;
}
上例中,a
是g()
的局部对象,离开建立它的函数g
以后就消亡了,无法在返回主函数后继续生存。为了处理这种情况,编译系统会在主函数中创建一个无名临时对象,该对象的生存只在函数调用所处的表达式b=g()
中。执行语句return a;
时,实际上是调用复制构造函数将a
的值复制到临时对象中。
注意:当类的数据成员中有指针类型时,默认的复制构造函数实现的只是浅复制。浅复制会带来数据安全方面的隐患,要实现正确复制,也就是深复制,程序员必须编写复制构造函数。
4.3.3 析构函数
析构函数的作用:完成对象被删除前的一些清理扫尾工作。
析构函数的执行方式:在对象的生存期即将结束的时刻被自动调用。
在析构函数调用完成后,对象也就消失了,相应的内存空间也被释放。
与构造函数一样,析构函数通常也是类的一个公有函数成员,函数名与类名也相同,但前面加~
,也无返回值。
与构造函数不同,析构函数不接收任何参数,但可以是虚函数。
如果不进行显式说明,系统也会自动生成一个函数体为空的隐含析构函数。
析构函数不能被重载,一个类中只能有一个析构函数。
析构函数的声明:
class Clock{
public:
Clock(); //构造函数
void setTime(int newH, int newM, int newS);
void showTime();
~Clock(){} //空的内联析构函数,功能与系统自动生成的隐含构造函数相同
private:
int hour, minute, second;
};
一般来讲,如果希望程序在对象被删除之前的时刻自动(不需要人为进行函数调用)完成某些事情,就可以把它们写到析构函数中。
4.4 类的组合
4.4.1 组合
类的组合:利用已有类的对象来构建新的类,描述的是一个类内嵌其他类的对象作为成员的情况,它们之间的关系是包含与被包含的关系。
当创建类的对象时,如果这个类具有内嵌对象成员,那么各个内嵌对象将首先被自动创建。
创建对象时,既要对本类的基本类型数据进行初始化,又要对内嵌对象成员进行初始化。
组合类构造函数定义的一般形式:
类名::类名(形参表):内嵌对象1(形参表),内嵌对象2(形参表),...
{类的初始化}
初始化列表:上述组合类构造函数定义中的“内嵌对象1(形参表),内嵌对象2(形参表),...
”称为初始化列表,作用是对内嵌对象进行初始化。
对基本类型的数据成员也可以这样初始化,比如Circle类的构造函数可以写为:
Circle::Circle(float r):radius(r){}
等同于
Circle::Circle(float r){
radius=r;
}
创建一个组合类的对象时,不仅它自身的构造函数的函数体将被执行,而且还将调用其内嵌对象的构造函数。这时,构造函数的调用顺序如下:
(1)调用内嵌对象的构造函数,调用顺序按照内嵌对象在组合类的定义中出现的顺序。注意,内嵌对象在构造函数初始化列表中出现的顺序与内嵌对象构造函数的调用顺序无关。
(2)执行本类构造函数的函数体。
注意:有些数据成员的初始化,必须在构造函数的初始化列表中进行。这些数据成员包括两类,一类是没有默认构造函数的内嵌对象——因为这类对象初始化时必须提供参数,另一类是引用类型的数据成员——因为引用类型的变量必须在初始化时绑定引用的对象。如果一个类包括这两类成员,那么编译器不能够为这个类提供隐含的默认构造函数,这时必须编写显式的构造函数,并且在每个构造函数的初始化列表中必须至少为这两类数据成员初始化。
析构函数的调用执行顺序与构造函数正好相反。析构函数的函数体被执行完毕后,内嵌对象的析构函数被一一执行,这些内嵌对象的析构函数调用顺序与它们在组合类的定义中出现的次序刚好相反。
当存在类的组合关系时,如果程序员没有编写复制构造函数,编译系统会在必要时自动生成一个隐含的复制构造函数,这个隐含的复制构造函数会自动调用内嵌对象的复制构造函数,为各个内嵌对象初始化。
当存在类的组合关系时,如果程序员要编写复制构造函数,则需要为内嵌对象的复制构造函数传递参数。比如假设C类中包含B类的对象b作为成员,则C类的复制构造函数如下:
C::C(C &c1):b(c1.b){...}
类的组合示例:线段Line类,其中,用Point类的两个对象p1和p2作为数据成员来表示端点,Line具有计算线段长度的功能,在构造函数中实现。
#include <iostream>
#include <cmath>
using namespace std;
class Point{
public:
Point(int xx=0,int yy=0){ //内联构造函数
x=xx;
y=yy;
}
Point(Point &p); //复制构造函数,类内声明
int getX(){return x;}
int getY(){return y;}
private:
int x,y;
};
Point::Point(Point &p){ //复制构造函数,类外实现
x=p.x;
y=p.y;
cout<<"Calling the copy constructor of Point"<<endl;
}
//类的组合
class Line{
public:
Line(Point xp1, Point xp2); //组合类的构造函数
Line(Line &l); //组合类的复制构造函数
double getLen(){return len;}
private:
Point p1, p2; //Point类的对象p1,p2
double len;
};
//组合类的构造函数
Line::Line(Point xp1, Point xp2):p1(xp1),p2(xp2){
cout<<"Calling the constructor of Line"<<endl;
double x=static_cast<double>(p1.getX()-p2.getX());
double y=static_cast<double>(p1.getY()-p2.getY());
len=sqrt(x*x+y*y);
}
//组合类的复制构造函数
Line::Line(Line &l):p1(l.p1),p2(l.p2){
cout<<"Calling the copy constructor of Line"<<endl;
len=l.len;
}
//主函数
int main(){
Point myp1(1, 1),myp2(4, 5); //建立Point类的对象
Line line(myp1, myp2); //建立Line类的对象
Line line2(line); //利用复制构造函数建立一个新对象
cout<<"The length of the line is: ";
cout<<line.getLen()<<endl;
cout<<"The length of the line2 is: ";
cout<<line2.getLen()<<endl;
return 0;
}
4.4.2 前向引用声明
在使用一个类之前,必须首秀按定义该类。
前向引用声明的作用:在引用未定义的类之前,将该类的名字告诉编译器,使编译器知道那是一个类名。
class B; //前向引用声明
class A{
public:
void f(B b); //成员函数f的形参是B类的对象b
};
class B{
public:
void g(A a);
};
注意:当使用前向引用声明时,只能使用被声明的符号,而不能涉及类的任何细节。
4.6 结构体和联合体
4.6.1 结构体
C语言有结构体,而没有类。
C语言的结构体中只允许定义数据成员,不允许定义函数成员。
C语言没有访问控制属性的概念,结构体的成员全都是公有的。
C++中引入结构体以保持和C程序的兼容性。
在C++中,结构体是一种特殊类型的类,和类一样,可以有自己的数据成员和函数成员,构造函数和析构函数,可以控制访问权限,可以继承,支持包含多态等。
在C++中,结构体与类的唯一区别:在类中,对于未指定访问控制属性的成员,其访问控制属性为私有类型(private);在结构体中,对于未指定访问控制属性的成员,其访问控制属性为公有类型(public)。因此,在结构体的定义中,如果把公有成员放在最前面,则最前面的“public:
”可以省略:
struct 结构体名称{
公有成员
protected:
保护型成员
private:
私有成员
};
如果一个结构体的全部数据成员都是公有成员,而且没有用户定义的构造函数,没有基类和虚函数,这个结构体的变量可在结构体外用以下语法形式连续赋初值:
类型名 变量名={数据成员1的初值,数据成员2的初值,...};
在C++中,什么时候使用结构体?有时在程序中需要定义一些数据类型,它们并没有什么操作,定义它们的唯一目的只是将一些不同类型的数据组合成一个整体,从而方便地保存数据,这样的类型不妨定义为结构体。如果用类来定义,为了遵循“将数据成员设置为私有类型(private)”的习惯,需要为每个私有数据成员编写专门的函数成员来读取和改写各个属性,反而比较麻烦。
4.6.2 联合体
与结构体一样,联合体也是从C语言继承而来的,因此它的默认访问控制属性也是公有类型的。
在C++中,类似结构体,联合体也是一种特殊类型的类,可以有自己的数据成员和函数成员,构造函数和析构函数,可以控制访问权限。
但联合体不能继承,不支持包含多态。
联合体的各个对象成员,以及这些对象成员的对象成员,以此类推,都不能有自定义的构造函数、自定义的析构函数、重载的复制赋值运算符(见第8章)。
联合体的全部数据成员共享一组内存单元。因此,联合体变量中的成员同时至多只有一个是有意义的。
联合体定义的语法形式:
union 结构体名称{
公有成员
protected:
保护型成员
private:
私有成员
};
在C++中,什么时候使用联合体?有时,一组数据中,任何两个数据不会同时有效,例如,如果需要存储一个学生的各门课程成绩,有些成绩是等级制,需要用一个字符来存储它的等级,有些课程只记“通过”和“不通过”,需要用一个布尔值来表示是否通过,而另一些课程的成绩是百分制的,需要用一个整数来存储它的分数,这个分数就可以用一个联合体来表示:
union Mark{
char grade;
bool pass;
int percent;
};
无名联合体:不声明名称(如上例中的名称“Mark
”)的联合体,只声明一个成员项的集合,这些成员具有相同的内存地址,可以由成员项的名字直接访问。
无名联合体通常用作类或结构体的内嵌成员。