7.1 类和对象的定义
7.1.1 基本概念
和结构体一样,在程序设计语言中,一个类是一种新的数据类型。
在c语言中,struct 只能包含成员变量,不能包含成员函数,而在c++中,struct 类似于 class,既可以包含成员变量,又可以包含成员函数。
c++中的 struct 和 class 基本是通用的,唯有几个细节不同:
•使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的。
•class 继承默认时 private 继承,而 struct 继承默认时 public 继承。
•class 可以使用模板,而 struct 不能。
•class 有默认无参构造函数和默认析构函数,而 struct 没有。
•class 表现为行为,struct常用来存储数据。
7.1.2 类的声明
类的声明用于具体说明类的组成,一般将类的声明单独用一个扩展名为.h的头文件来保存。
另外,还需要对所声明的成员函数定义功能。类的声明加上成员函数的定义,就完整的定义了一个类。成员函数定义同常用一个c++源文件来保存。当然,也可以在类声明的同时,实现类的成员函数。
声明类的语法形式为:
class 类名称
{
public:
公有成员
protected:
保护型成员
private:
私有成员
};
其中,“成员”既可以是数据成员,也可以是成员函数的原型。关键字public、protected、private 说明类成员的访问控制属性。
类的成员包括数据成员和函数成员,分别描述问题的属性和操作,是不可分割的两个方面。数据成员的声明方式与一般变量相同;函数成员用于描述类的对象可以进行的操作,一般在类中声明原型,在类声明之后定义函数的具体实现。
public:类对外的接口,在类声明和类(函数)实现之后,类的对象可以访问‘
private:只允许本类的成员函数来访问。
protected:可访问性和 private 相似,其差别在于继承过程中对派生类的影响不同。
如果不写访问关键字,默认的是 private。
7.1.3 类的实现
函数的原型声明要在类的主体中,而函数的具体实现一般写再类声明之外。在类声明之后定义成员函数的语法形式为
返回值类型 类名::成员函数名(参数表)
{
函数体
}
其中,通过“ 类名 ”和作用域操作符“ :: ”来表示函数属于哪个类,其他部分和一般函数的定义相同。
类的成员函数还可以有多种形态:
1. 带默认参数值的成员函数
类的成员函数可以有,默认参数值,其调用规则与普通函数相同,即在参数表中指定值。注意:默认值要写在函数原型生命中,函数实现时不写默认值。
2.内联成员函数
两种方式:隐式声明和显式声明。
隐式声明:在类声明时定义的成员函数都是内联函数。函数定义时没有任何附加说明。
显式声明:在类声明之后定义内联函数需要在函数头部用关键字 inline 开始,格式如下:
inline 返回值类型 类名::成员函数名(参数表){函数体}
3.成员函数的重载
类名是成员函数名的一部分,所以一个类的成员和另一个类的成员函数即使同名,也不能认为是重载。
例如,可以在 Clock 类中再声明一个按另一种格式显示时间的ShowTime(int n)函数,和用来显示时间的函数构成重载:
class Clock
{
public:
void ShowTime();
void ShowTime(int n);
......// 其他成员声明
};
7.1.4 对象的定义和使用
定义一个对象和定义一个一般变量相同,语法形式为
类名称 对象名称;
类的成员时抽象的,对象的成员才是具体的。类声明中的数据成员一定不能有具体的属性值。
声明了类及其对象,在类的外部就可以访问对象的公有成员了。
数据成员:
对象名.公有数据成员
函数成员:
对象名.公有成员函数名(参数表)
在类的外部,只能通过对象访问类的公有成员;在类的成员函数内部,可以直接访问类的所有成员,这就实现了对访问范围的有效控制。
7.1.5 类的作用域与可见性
1. 类的作用于域
一个类的所有成员位于这个类的作用域内,一个类的任何成员函数都能访问同一类的任何其他成员。
类作用域是指类定义和相应的成员函数定义的范围,通俗地成为类的内部。在该范围内,一个类的成员函数对本类的其他成员具有无限制的访问权。在类的作用域外,对一个类的数据成员或函数成员的访问受到程序员的控制。
2. 类的可见性
类名实际上是个类型名,允许类与其他类型变量或其他函数同名。
在类的内部,与类或类的成员同名的全局变量名或函数名不可见。
在一个函数内,同名的类和变量可以同时使用,都是可见的。例如,若 Clock 类已定义,以下函数的定义是没有问题的。
void func()
{
class Clock a; //定义时要用到类名
int Clock = 10; //变量名和类型名相同
Clock++;
}
7.2 构造函数
1. 构造函数的定义
构造函数用来完成对象的初始化,给对象的数据成员赋初值。
定义构造函数的一般形式为
class 类名
{
public:
类名(形参表); //构造函数的原型
//类的其他成员
};
类名::类名(形参表) //构造函数的实现
{
//函数体
}
构造函数可以在类的内部实现,也可以在类的外部实现。
构造函数声明并且实现后,就可以在main()函数中,通过
Clock 类的构造函数来创建和初始化对象。
类名称 对象名称(参数表);
构造函数的特点是:构造函数的名称与类名相同,构造函数没有返回值,构造函数一定是公有函数。
作为类的成员函数,构造函数可以直接访问类的所有数据成员。
在类的内部定义的构造函数是内联函数。构造函数可以带默认形参值,也可以重载。
2. 构造函数的重载
构造函数可以像普通函数一样重载,调用时根据参数的不同,选择其中合适的一个。
3. 带默认参数值的构造函数
函数可以为其参数设置默认值,构造函数也可以。
4. 默认构造函数和无参构造函数
如果再定义类时没有定义构造函数,则系统在编译时自动生成一个默认形式的构造函数。默认构造函数具有以下形式:
类名::类名( ){ }
这是一个既没有形式参数,也没有任何语句的函数,这样的默认构造函数当然不能为对象初始化做任何事情。
注意:只有在类中没有定义任何构造函数的情况下,才能使用默认构造函数。
还有一种构造函数成为无参构造函数,它的一般形式是:
类名::类名( ){语句···}
另外,带有全部默认参数值的构造函数也是无参构造函数。
假如 Clock 类中只定义了构造函数 Clock(int H,int M,int S),并且没有默认值,以下程序就会有语法错误。
void main()
{
Clock c1(10,10,10);
Clock c2; //编译时会出错
}
原因是类中没有定义无参构造函数,因为类中定义了带参数的构造函数,编译系统也就不会再自动生成一个默认构造函数。
程序中,不能同时出现无参数构造函数和带有全部默认值形参值的构造函数,否则,就会出现编译错误。
注意,一旦定义了一个类的构造函数,系统就不再生成默认构造函数了。如果需要定义一个对象而不提供实际参数,需要定义一个无参构造函数,或者给所有参数都设置默认值。
5. 复制构造函数
复制构造函数用来复制一个对象。定义对象时,通过等号赋值进行对象的初始化,系统会自动调用复制构造函数。例如:
Clock c1(10,10,10); //先创建一个对象
Clock c2 = c1; //在定义c2时用c1初始化,自动调用复制构造函数
也可以在定义对象时,像调用构造函数一样,调用复制构造函数,只是实参时一个已经定义好的对象,例如:
Clock c2(c1); //利用复制构造函数将c1复制到c2
复制构造函数就是函数的形参是类的对象的引用的构造函数。
定义一个复制构造函数的一般形式为
class 类名
{
public:
类名(类名& 对象名); //复制构造函数原型
};
类名::类名(类名& 对象名) //复制构造函数的实现
{
//函数体
}
复制构造函数是一种特殊的构造函数,具有一般构造函数的所有特性,其形参是本类对象的引用,其作用是使用一个已经存在的对象(由复制构造函数的参数指定的对象)去初始化一个新的同类的对象。复制构造函数与原来的构造函数实现了函数的重载,如果程序在类定义时没有显示定义复制构造函数,系统也会自动生成一个默认的复制构造函数,将成员一一复制。
但是,某些情况下必须显示定义一个复制构造函数。例如:当类的数据成员包括指针变量时,类的构造函数用 new 运算符为这个指针动态申请空间,如果复制时只是简单地一一复制,就会出现两个对象指向相同的堆地址,则在退出运行时,程序会报错。这种情况必须定义复制构造函数,在复制构造函数中为新对象申请新的堆空间。
7.3 析构函数
对象所占用的空间要通过析构函数来释放。析构函数的原型是:
~类名( );
如果程序中不定义析构函数,系统也会提供一个默认的析构函数:
~类名( ){ };
这个析构函数只能用来释放对象的数据成员所占用的空间,但不包括堆内存空间。
特点:
•访问属性:公有成员函数。
•函数名:在类名前加“~”构成。
•不能重载无参数,无返回值。
•自动生成:析构函数是在对象生存期即将结束的时刻由系统自动调用的。如果没有定义析构函数,系统将自动生成和调用一个默认析构函数。
例7-4 定义学生类 student,数据成员包括学号、姓名、年龄、成绩;成员函数有构造函数、析构函数和输出显示函数。其中,“姓名”用字符指针(char*)来保存,在构造一个学生时,从堆中为“姓名”分配存储空间,那么需要定义析构函数,在对象生存期结束时,把堆空间释放,归还给系统。在这种情况下,也需要定义一个复制构造函数。
#include<iostream>
using namespace std;
class student
{
public:
student(int,char*,int,float);
student(student&); //复制构造函数
~student();
void printstu();
private:
int id;
char* name;
int age;
float score;
};
student::student(int i,char* c,int a,float s)
{
cout << "Constructing..." << endl;
id = i;
age = a;
score = s;
name = new char[strlen(c)+1];
if(name != 0)
strcpy(name,c);
}
student::student(student& s) //复制构造函数
{
cout << "Copy Constructing..." << endl;
id = s.id; //一般成员的简单复制
age = s.age;
score = s.score;
name = new char[strlen(s.name)+1]; //先申请堆空间
if(name != 0)
strcpy(name,s.name); //复制字符串
}
student::~student()
{
cout << "Destructing..." << endl;
delete []name;
name = 0;
}
void student::printstu()
{
cout << "学号:" << id << "姓名:" << name;
cout << " 年龄:" << age << "成绩:" << score << endl;
}
void main()
{
student stu(1,"wang",18,86);
stu.printstu();
}
类的析构函数不能重载,因为析构函数没有参数,因而无法重载,即构造对象的方式有许多种,但释放对象只有一种方式。