一、类及其实例化
1、定义类
类要先声明后使用;不能声明两个名字相同的类,类是具有唯一标识符的实体;在类中声明的任何成员不能使用extern、auto、register关键字进行修饰;类中声明的变量属于该类,在某些情况下,变量也可以被该类的不同实例所共享;类中有数据成员和成员函数,不有在类声明中对数据成员使用表达式进行初始化。
⑴ 声明类
声明类以class开始,其后跟类名,类所声明的内容用花括号括起来,右括号后的分号作为类声明结束的标志。
类成员具有访问权限,通过它前面的关键字来定义,关键字private后的成员叫私有成员、public后的成员叫公有成员、protected后的成员叫受保护的成员,访问权限用于控制对象的成员在程序中的可访问性。如果没有使用关键字,则所有成员默认声明为private权限。
class Point{//类名Point
private://声明为私有访问权限
int x,y;//私有的数据成员
public://声明为公有的访问权限
void setXY(int a,int b);
void move(int a,int b);
void display();
int getX();
int getY();
};//声明以分号为结尾
⑵ 定义成员函数
类中声明的成员函数用来对数据成员进行操作,还必须在程序中实现这些成员函数。
#include <iostream>
using namespace std;
class Point{//类名Point
private://声明为私有访问权限
int x,y;//私有的数据成员
public://声明为公有的访问权限
Point(){};//没有参数的构造函数
Point(int a,int b){//两个有参的构造函数
x = a;
y = b;
}
void setXY(int a,int b);//函数声明
void display();
inline int getX();//声明为内联函数
int getY(){//在类体中定义函数,如果不包含循环或switch语句则默认为内联函数
return y;
}
};//声明以分号为结尾
//在类体外定义函数
void Point :: setXY(int a,int b){
x = a;
y = b;
}
void Point :: display(){
cout << x << "," << y;
}
inline int Point :: getX(){//类体外定义内联函数
return x;
}
一般在类体中给出简单成员函数的定义,在类中定义的函数如果不包含循环或switch语句会被系统默认当作内联函数来处理,函数体内容较多的函数则在类外定义,系统并不会把它们默认为内联函数,如果想把它们指定为内联函数,则应该使用inline进行显示声明。如果在类体外定义内联函数,需要把函数声明和函数定义放在同个源文件中才能编译成功。
在类体中直接定义函数时,不需要在函数名前加上类名,只有在类体外定义时才需要。其中,“::”是作用域运算符,它用于表明其后的成员函数是属于这个特定的类 。
⑶ 数据成员的赋值
不能在类体内给数据成员赋值,数据成员的具体值是用来描述对象属性的,只有产生了一个具体的对象,这些数据值才有意义。如果产生对象时就使对象的数据成员具有指定值,则称为对象的初始化。注意,初始化和赋初值是两个不同的概念,初始化是使用与Point同名的构造函数来实现的,赋初值是在有了对象A之后,对象A调用自己的数据成员或成员函数实现赋值操作。
int main(){
//构造函数初始化
Point a(10,20);
//对象赋值
Point b;
b.setXY(10,20);
return 0;
}
2、使用类的对象
只有产生类的对象,才能使用这些数据和成员函数。类不仅可以声明为对象,还可以声明为对象的引用和对象的指针。
//定义print函数的重载,分类使用类指针和类对象作为参数
void print(Point *a){//类指针作为参数重载print函数
a -> display();
}
void print(Point &b){//类引用作为参数重载print函数
b.display();
}
int main(){
//构造函数初始化
Point a(10,20);
//对象赋值
Point b;
b.setXY(10,20);
Point *p1 = &a;//声明对象a的对象指针
Point &p2 = b;//声明对象b的对象引用
print(p1);//10,20
print(p2);//10,20
return 0;
}
总结:
① 类的成员函数可以直接使用类的私有成员;
② 类外的函数不能直接使用类的私有成员;
③ 类外的函数只能通过类的对象使用该类的公有成员函数;
④ 对象的成员函数代码都是一样的,对象的区别只是属性的取值;
⑤ 在程序运行时,通过为对象分配内存来创建对象,为了节省内存,在创建对象时,只分配用于保存数据的内存,代码为每个对象共享,类中定义的代码被放在计算机内的一个公共区中供该类的所有对象共享;
⑥ 对象和引用在访问对象的成员时,使用运算符“.”,而指针则使用“->”运算符。
3、数据封装
面向对象程序设计是通过为数据和代码建立分块的内存区域,以便提供对程序进行模块化的程序设计方法,这些模块可以被用作样板,在需要时再建立副本。而对象是计算机内存中的一块区域,通过将内存分块,每个对象在功能上保持相对独立。这些内存块中不但存储数据,也存储代码,只有对象中的代码才可以访问存储于这个对象中的数据,这将保护它自己不受未知外部事件的影响,从而使自己的数据和功能不会遭到破坏。
在面向对象的程序中,只有向对象发送消息才能引用对象的行为,所以面向对象是消息处理机制,对象之间只能通过成员函数相互调用来实现相互通信。这样,对象之间相互作用的方式是受控制的,一个对象外部的代码就没有机会通过直接修改对象的内存区域妨碍对象发挥其功能。
面向对象就是将世界看成是一组彼此相关并能相互通信的实体即对象组成的,程序中的对象映射现实世界中的对象。
C++对其对象的数据成员和成员函数的访问是通过访问控制权限来限制的,一般情况下将数据成员说明为私有的,以便隐藏数据,将部分成员函数说明为公有的,用于提供外界和这个类的对象相互作用的接口,从而使得其他函数也可以访问和处理该类的对象。
二、构造函数
1、默认构造函数
没有定义构造函数,却可以使用类直接产生对象,原因是当没有为一个类定义任何构造函数的情况下,C++编译器总要自动建立一个不带参数的构造函数。默认的构造函数函数名与类名相同,函数体是空的,没有参数,也没有返回值,如果它有返回值,编译器就必须知道如何处理返回值,这样会大大增加编译器的工作,降低了效率。如果我们在程序中定义了自己的构造函数,系统就不再提供默认的构造函数。如果我们需要使用到无参的构造函数,则需要在显式的声明并定义一个无参的构造函数。
2、定义构造函数
#include <iostream>
using namespace std;
class Point{
private:
int x,y;
public:
Point();//声明一个无参的构造函数
Point(int,int);//声明一个有两个参数的构造函数
};
Point :: Point(){//定义无参的构造函数
cout << "默认初始化对象" << endl;
}
Point :: Point(int a,int b):x(a),y(b){//定义两个参数的构造函数,x(a)相当于x = a,它跟下面的声明方式是等价的
cout << "初始化对象,属性x:" << a << ",属性y:" << b <<endl;
}
/*Point :: Point(int a,int b){
x = a;
y = b;
}*/
int main(){
Point a;//使用无参构造函数产生对象
Point b(10,20);//使用有参构造函数产生对象
Point c[2];//使用无参构造函数产生对象数组
Point d[2]={Point(15,25),Point(20,30)};//使用有参构造函数产生对象数组
/**
* 初始化对象,属性x:10,属性y:20
* 默认初始化对象
* 默认初始化对象
* 初始化对象,属性x:15,属性y:25
* 初始化对象,属性x:15,属性y:25
*/
}
3、构造函数和运算符new
运算符new用于建立生存期可控的对象,new返回这个对象的指针。当使用new建立一个动态的对象时,new将首先分配保证类的一个对象所需要的内存,然后自动调用构造函数来初始化这块内存,再返回这个动态对象的地址。
使用new建立的动态对象只能使用delete删除,以便释放所占空间。
Point *p1 = new Point();
Point *p2 = new Point(5,8);
delete p1;
delete p2;
/**
* 默认初始化对象
* 初始化对象,属性x:5,属性y:8
*/
4、构造函数的默认参数
class Point1{
private:
int x,y;
public:
Point1(int=0,int=0);//声明一个默认参数的构造函数,使用默认参数的构造函数就不能再声明无参的构造函数
};
Point1 :: Point1(int a,int b):x(a),y(b){//定义两个参数的构造函数
cout << "初始化对象,属性x:" << a << ",属性y:" << b <<endl;
}
int main(){
Point1 a;
Point1 b(10,25);
}
5、复制构造函数
引用在类中可以用在复制构造函数中,编译器建立一个默认复制构造函数,然后采用拷贝式的方法使用已有的对象来建立新对象。复制构造函数必须使用对象的引用为形式参数,为了安全起见,建议使用const限定符。
class Point2{
private:
int x,y;
public:
Point2();
Point2(const Point2&);//声明带const限定符的复制构造函数
};
Point2 :: Point2():x(12),y(20){
cout << "初始化对象,属性x:" << x << ",属性y:" << y <<endl;
}
Point2 :: Point2(const Point2 &p){
x = p.x;//一个类中定义的成员函数可以访问该类任何对象的私有成员
y = p.y;
cout << "初始化对象,属性x:" << x << ",属性y:" << y <<endl;
};
int main(){
Point2 a;
Point2 b(a);
/**
* 初始化对象,属性x:12,属性y:20
* 初始化对象,属性x:12,属性y:20
*/
}
三、析构函数
在对象消失时,应使用析构函数释放由构造函数分配的内存。构造函数、复制构造函数和析构函数是构造型成员函数的基本成员。
1、定义析构函数
析构函数的函数名称与类名一样,为了与构造函数进行区分,在析构函数的前面加一个“~”号。在定义析构函数时,不能指定任何返回类型,也不能指定任何参数,但是可以显式的声明参数为void,即A::~A(void),一个类只能定义一个析构函数。
void example3();
class Point3{
private:
int x,y;
public:
Point3(int,int);//声明两个参数的构造函数
~Point3();//声明析构函数
};
Point3 :: Point3(int a,int b):x(a),y(b){//定义两个参数的构造函数
cout << "Initializing" << endl;
}
Point3 :: ~Point3(){
cout << "Destructor is active" << endl;
}
int main(){
example3();
return 0;
}
void example3(){
Point3 a(10,20);//通过构造函数实例化一个对象
cout << "Exiting main function" << endl;
/**
* Initializing //创建对象时调用构造函数
* Exiting main function //在程序结束之前调用析构函数
* Destructor is active //程序自动调用构造函数
*/
}
当对象的生命周期结束时,程序为这个对象自动调用析构函数,然后回收这个对象占用的内存。全局对象和静态对象的析构函数在程序运行结束之前调用。类的对象数组的每个元素调用一次析构函数。全局对象的析构函数在程序结束之前被调用。
如果在定义类时没有定义析构函数,C++编译器也要为它产生一个函数体为空的默认析构函数。
2、析构函数和运算符delete
运算符delete与析构函数一起工作,当使用运算符delete删除一个动态对象时,它首先为这个动态对象调用析构函数,然后再释放这个动态对象占用的内存,这与使用new建立动态对象的过程刚好相反。
void example4();
class Point3{
private:
int x,y;
public:
Point3(int=0,int=0);//声明两个参数的构造函数
~Point3();//声明析构函数
};
Point3 :: Point3(int a,int b):x(a),y(b){//定义两个参数的构造函数
cout << "Initializing" << a << "," << b << endl;
}
Point3 :: ~Point3(){
cout << "Destructor is active" << endl;
}
int main(){
example4();
return 0;
}
void example4(){
Point3 *p = new Point3[2];//创建对象数组
delete [] p;//动态删除对象
/**
* Initializing0,0
* Initializing0,0
* Destructor is active
* Destructor is active
*/
}
当使用delete释放动态对象数组时,必须告诉它这个动态对象数组有几个元素对象,C++使用“[]”来实现 。然后,delete将为动态数组的每个对象调用一次析构函数,并释放内存。
当程序先后创建几个对象时,系统将按照先创建后析构的原则进行析构对象,当使用delete调用析构函数时,则按delete的顺序析构。
四、调用复制构造函数的综合实例
void example5();
void example6();
void example7();
void example8();
class Point4{
private:
int x,y;
public:
Point4(int=0,int=0);//默认参数构造函数
Point4(const Point4&);//复制构造函数
~Point4();//析构函数
void show(){
cout << "x=" << x << ",y=" << y << endl;
}
};
Point4 :: Point4(int a,int b):x(a),y(b){//定义默认构造函数
cout << "使用默认参数构造函数初始化对象!x=" << x << ",y=" << y << endl;
}
Point4 :: Point4(const Point4 &p){//定义复制构造函数
x = p.x;
y = p.y;
cout << "使用复制构造函数初始化对象!x=" << x << ",y=" << y << endl;
}
Point4 :: ~Point4(){//定义析构函数
cout << "析构对象,x=" << x << ",y=" << y << endl;
}
void show1(Point4 p){//使用对象作为参数的方法
cout << "使用对象作为参数,";
p.show();
}
void show2(Point4 &p){//使用对象引用作为参数的方法
cout << "使用对象引用作为参数,";
p.show();
}
Point4 getPoint4(){//返回值为对象的方法
Point4 p(10,20);
return p;
}
int main(){
example8();
return 0;
}
//测试返回对象时的函数调用流程
void example8(){
Point4 a = getPoint4();
show2(a);
/**
* 使用默认参数构造函数初始化对象!x=10,y=20
* 使用对象引用作为参数,x=10,y=20
* 析构对象,x=10,y=20
*/
}
//测试函数的形参为对象引用时函数调用流程
void example7(){
Point4 a(17,27);
show2(a);
/**
* 使用默认参数构造函数初始化对象!x=17,y=27
* 使用对象引用作为参数,x=17,y=27
* 析构对象,x=17,y=27
*/
}
//测试函数的形参为对象时的函数调用流程
void example6(){
Point4 a(16,26);
show1(a);
/**
* 使用默认参数构造函数初始化对象!x=16,y=26
* 使用复制构造函数初始化对象!x=16,y=26 //创建临时对象
* 使用对象作为参数,x=16,y=26
* 析构对象,x=16,y=26 //先析构临时对象
* 析构对象,x=16,y=26
*/
}
//测试用一个类的对象去初始化另一个对象
void example5(){
Point4 a(15,25);//调用构造函数初始化对象a
Point4 b(a);//使用对象a初始化对象b
/**
* 使用默认参数构造函数初始化对象!x=15,y=25
* 使用复制构造函数初始化对象!x=15,y=25
* 析构对象,x=15,y=25
* 析构对象,x=15,y=25
*/
}
从上面的程序执行情况来看,当函数的形参是类的对象时,调用函数时需要调用复制构造函数。而传对象就是传值的方式,所以需要构造一个临时对象,在退出时再析构该对象。相比较而言,传引用是传地址的方式,所以不需要构造临时对象,所以传对象的引用比传对象要好。
六、this指针
在定义Point类的对象a之后,当执行语句“a.getX()”时,计算是怎么知道获取哪个对象的值呢?其实成员函数getX()有一个隐藏参数,名为this指针,当源程序被编译后,getX()的实际形式如下:
int Point :: getX((Point*)this){
return this -> x;
}
C++规定,当一个成员函数被调用时,系统自动向它传递一个隐藏的参数,该参数是一个指向调用该函数的对象的指针,从而使用成员函数知道该对哪个对象进行操作。在程序中可以使用this关键字来引用该指针。
七、一个类的对象作为另一个类的成员
void example9();
class Desk{
private:
int num;//数量
public:
void setNum(int a){
num = a;
}
int getNum(){
return num;
}
};
class Bed{
private:
int num;//数量
public:
void setNum(int a){
num = a;
}
int getNum(){
return num;
}
};
class House{
private:
Desk d;
Bed b;
public:
void setHouse(Desk &d,Bed &b){
this -> d = d;
this -> b = b;
}
int getTotal(){
return d.getNum() + b.getNum();
}
};
int main(){
example9();
return 0;
}
void example9(){
Bed b;
b.setNum(2);
Desk d;
d.setNum(5);
House h;
h.setHouse(d,b);
cout << "屋子里一共有" << h.getTotal() << "件家具!";
/**
* 屋子里一共有7件家具!
*/
}
八、类和对象的性质
1、对象的性质
① 同一类的对象之间可以相互赋值,如:
Point a,b;
a.setXY(10,20);
b = a;
② 可以使用对象数组,如:
Point a[2];
③ 可以使用对象的指针,如:
Point a(10,20);
Point *p = &a;
p -> display();//使用对象指针调用成员函数
④ 对象可以用作函数参数。如果传对象值,调用函数时对形参的改变不会影响调用函数中实参的对象;如果传的是对象的引用(地址),当参数对象被改变时,相应的实参对象也会被修改;如果传的是对象的地址值,可以使用对象指针作为参数,可以达到传引用相同的效果。为了避免被调用函数修改原来对象的数据成员,可以使用const修饰符。
⑤ 对象作为函数参数时,可以使用对象、对象引用和对象指针;
⑥ 一个对象可以用作另一个对象的成员。
2、类的性质
⑴ 使用类的权限
① 类本身的成员函数可以使用类的所有成员(包括公有和私有);
② 类的对象只能访问公有成员函数;
③ 其他函数不能使用类的私有成员,也不能使用公有成员函数,它们只能通过类的对象使用类的公有成员函数;
④ 虽然一个类可以包含另外一个类的对象,但这个类只能通过被包含类的对象使用那个类的成员函数,再通过成员函数使用数据成员。
⑵ 不完全的类声明
类不是内存中的物理实体,只有当一个类产生对象时,才会分配内存,这种对象建立的过程被称为实例化。类必须在其使用之间先进行声明,有时也需要将类作为一个整体来使用,而不存取成员。如下:
class Point;//不完全类声明
Point *p;//全局的类指针
void main(){}//主函数
class Point{//类体};//完全定义类
不完全类声明,用于在类没有完全定义之前就引用该类的情况,比如:引用另一文件中定义的类,由于类标识符Point通过类声明进入作用域,所以就可以声明全局变量指针p,当编译器执行到该指针声明处,只了解指针所指类型是一个叫Point的类,而不了解其他情况。
不完全类声明的类不能实例化,也不能存取没有完全声明的类成员,否则会编译错误。
⑶ 空类
尽管类的目的是封装代码和数据,但它也可以不包括任何声明。如:
class Point{};
这种类没有任何行为,但可以产生空类对象。
⑷ 类的使用域
声明类时所使用的一对花括号形成所谓的类作用域,在类作用域中声明的标识符只在类中可见。
九、面向对象的标记图
1、类和对象的UML标记图
类图如下:
2、对象的结构和连接
只有定义和描述了对象之间的关系,各个对象才能构成一个整体的、有机的系统模型,这就是对象的结构与连接关系。对象结构是指对象之间的分类(继承)关系和组成(聚合)关系,统称为关联关系。对象之间的静态关系是通过对象属性之间的连接反映的,称之为实例连接。对象行为之间的动态关系是通过对象行为(消息)之间的依赖关系表现的,称之为消息连接,实例连接和消息连接统称为连接。
⑴ 分类关系及其表示
C++中的分类结构是继承结构。UML使用一个空三角形表示继承关系,三角形指向基类。如下:
⑵ 对象组成关系及其表示
组成关系说明的结构是整体与部分的关系。C++中的聚合隐含了两种实现方式,第一种是独立的定义,可以属于多个整体对象,并具有不同的生存期,它用空心菱形表示。第二种方式是用一个类的对象作为一种广义的数据类型来定义整体对象的一个属性,构成一个嵌套对象,它用实心菱形来表示。如下:
⑶ 实例连接及其表示
实例连接反映对象之间的静态关系。简单的实例连接是对象实例之间的一种二元关系,实例连接用一条实线表示。比如:老师指导学生论文。
⑷ 消息连接及其表示
消息连接描述对象之间的动态关系。即一个对象在执行自己的操作时,需要通过消息请求另一个对象为它完成某种服务,也就是说两个对象之间存在着消息连接。消息连接是有方向的,使用一个带箭头的实线表示,从消息的发送者指向消息的接收者。
3、对象、类和消息
对象的属性是指描述对象的数据成员,对象的属性集合又称为对象的状态。对象的行为是定义在对象属性上的一组操作的集合,操作是响应消息而完成的算法,表示对象的内部实现细节,对象的操作集合体现了对象的行为能力。
对象一般具有以下特征:
① 有一个状态,由与其关联的属性值集合所表示;
② 有唯一标识名,可以区别其他对象;
③ 有一组操作方法,每个操作决定对象的一种行为;
④ 对象的状态只能被自己的行为所改变;
⑤ 对象的操作包括自身操作和施加于其他对象的操作;
⑥ 对象之间以消息传递的方式进行通信;
⑦ 一个对象的成员仍可以是一个对象。
消息是向对象发出的服务请求,它是面向对象系统中实现对象之间的通信和请求任务的操作。一个对象所能接受消息及其所带的参数,构成该对象的外部接口。对象接收它能识别的消息,并按照自己的方式来解释和执行。一个对象可以同时向多个对象发送消息,也可以接收多个对象发来的消息。对象传递的消息一般由三个部分组成:接收对象名、调用操作名和必要的参数。向对象发送一个消息,就是引用一个方法的过程。
消息协议是一个对象对外提供服务的规定格式说明,外界对象能够向该对象发送协议中所提供的消息,请求该对象服务。消息分为公有和私有消息,私有消息只供类的内部使用的消息,公有消息是对外的接口,协议则是一个对象所能接受的所有公有消息的集合。
十、面向对象编程的文件规范
在编程一般要求将类的声明放在头文件中,非常简单的成员函数可以在声明中定义(默认是内联函数),实现放在.cpp文件中,然后在.cpp文件中将头文件包含进去。主程序可单独使用一个文件,这是多文件编程规范。
对于规模较大的类,一般会为每个类设立一个头文件和实现文件。但函数模板和类模样比较特殊,如果使用头文件说明,则同时完成它们的定义,为了避免重复包含头文件,可使用条件编译的方法。
1、编译指令
C++的源程序可包含各种编译指令,以指示编译器对源代码进行编译之前先对其进行预处理。所有的编译指令都以#开始,每条指令单独占用一行,同一行不能有其他编译指令和C++语句(注释除外)。
⑴ 嵌入指令
嵌入指令#include指示编译器将一个源文件嵌入到带有#include指令的源文件中该指令所在的位置处。尖括号或双引号中的文件名可含有路径信息。例:#include <iostream> #include "e:\app\myfile.h"
⑵ 宏定义
#define定义一个标识符及串,在源程序中每次遇到该标识符时,编译器均用定义的串代替之。该标识符称为宏名,而将替换过程称之为宏替换。宏定义由新行结束,而不是以分号结束,如果要写多行的宏定义,则除最后一行外,每行末尾要加上一个“\”,表示宏定义继续到下一行。例:
#define MAX(a,b)(a>b)?\
a:b
如果不再使用定义的宏,可以使用#undef删除。
⑶ 条件编译指令
条件编译指令是#if、#else、#elif、#endif,它们构成类似于C++的if选择结构,其中#endif表示一条指令结束。编译指令#error用于输出出错信息,使用形式如下:
#error 出错信息
⑷ defined操作符
关键字defined不是指令,而是一个预处理操作符,用于判定一个标识符是否已经被#define定义过。如下:
defined (MAX)//结果为布尔值
2、在头文件中使用条件编译
在多文件设计中,由于文件包含指令可以嵌套使用,可能会出现不同包含同一个头文件的情况,这就会引起变量及类的重复定义,为了避免这种情况,可对这个类头文件使用条件编译。如下:
//Point.h
#if !defined(POINT_H)
#define POINT_H
class Point{
};
#endif