总目录 > 1 语言基础 > 1.5 C++ 进阶 > 1.5.1 类与对象
前言
这篇文章首先主要介绍 C++ 中最重要的特性——类 class,以及和它密切相关的对象。C++ 刚刚诞生时就被人们称作 “C With Class”,足以看出 class 对 C++ 意义之大。其实之前学了很久的 C++ 却基本没用过 class,以至于在看一些博客的代码时就很懵,后来才知道 class 和 struct 其实是比较类似的,但是功能更强大,只是说竞赛里一般不太需要用到。
更新日志
20200916 - 完善内容,增加 抽象与封装,对象应用,友元,接口与实现的分离 的介绍。
子目录列表
1、抽象与封装
2、类与结构体
3、数据成员与成员函数
4、对象
5、构造函数
6、析构函数
7、重载运算符
8、静态成员
9、this 指针
10、对象的应用
11、友元
12、接口与实现的分离
1.5.1 类与对象
1、抽象与封装
在 1.3 C++ 与面向对象程序设计 中,我们介绍了面向对象语言的四大特征,其中以抽象与封装最为重要。
① 抽象
抽象是人们认识客观事物的一种常用方法,即在描述事物时,有意去掉次要部分与具体细节,而抽取出与所关注问题相关的重要特征进行考察,并以这些特征代表该事物。计算机软件开发中采用的抽象方法主要有过程抽象和数据抽象两种。
> 过程抽象
过程抽象是面向过程程序设计采用的“以功能为中心”的抽象方法,将整个系统的功能划分成若干部分,每部分由若干过程(函数)完成,强调过程的功能设计,而忽略实现的详细细节。
过程抽象的结果给出了函数名称、接受参数和能提供的功能,函数使用者只需知道这些尽可能进行函数调用。
> 数据抽象
数据抽象是面向对象程序设计放阀, 采用”以数据为中心“的抽象方法,忽略事物与当前问题域无关的、不重要的部分与细节,抽取同类事物与当前所研究问题相关联的、共有的基本特征与行为,形成关于该事物的抽象数据类型,即 ADT。
举个生动的例子:2020新生报到,现在学校要对所有新生的一些信息进行统计,方便信息化管理。每个人都是独一无二的,有人高有人矮,有人单眼皮有人双眼皮,有人A型血有人B型血,想通过几句话来描述一个人的所有特征或者将其与其他人区别开来,不是个简单的事情。但对于学校而言,单双眼皮并不是学校需要关注了解的特征,完全可以忽略掉;高矮并不是信息化管理很重要的特征,也可以忽略掉……而最后抽取出来的关键特征,比如新生们的性别、祖籍、家庭住址,所在学院、班级、宿舍号等等,才是学校所需求的。
> 数据抽象
1、类与结构体
类 (class) 是结构体(请参见 1.2 C 语言数据类型 中的 结构体 部分)的扩展,可以拥有成员元素和成员函数。其实在 C++ 中对 struct 的定义同样是类,但是是在 C 中就存在 struct,并定义为结构体,仅仅只是包含成员元素;而 C++ 保留只是为了向下兼容,同时扩充了功能,增加了对成员函数的支持。
2、访问说明符
class Object { int value4; private: int value3; public: int value1; double value2; } a[MAXN]; const Object b; Object c, d[MAXN]; Object *e;
访问说明符是 class 中的特性,分为:
① public:可以被公开访问:类内,类外,友元,派生类;
② private:只能被类内或者友元的成员访问;
③ protected:只能被类内,友元或者派生类访问。
对于 struct,所有成员默认为 public;
对于 class,在不标访问说明符的情况下,默认为 private,如例子中的 value4。
友元(friend):用于修饰某个函数或者某个类,使其能访问该类的 private / protected 成员。
派生类(derived class):略。
为什么要对各个成员通过访问说明符分类?
对于 private, protected 类型,其作用一方面是保护类的数据不被随意修改,更加安全;一方面起到黑盒的效果,对外部接口而言它只需要访问 public 类型的数据即可,而其他的不需了解也能正常使用。
注意,数据成员不能被直接赋初值。
3、数据成员和成员函数
class Object { public: int value; void print() { cout << value << endl; return; } void update(int); } a[MAXN]; void Object :: update(int _value) { value = _value; }
上述代码中,int value 为其中的一个数据成员,这一点和 struct 结构体是一致的,不过多介绍。
成员函数是 class 独有的特性,表示专属于这个类的函数。代码中的两个函数分别为成员函数两种定义方式:直接定义,或者先声明再定义,再定义时写在类外,记得加上 “<类名> :: ” 的标识表示其所属的命名空间。
如果不希望成员函数修改数据成员的值,可以将其设置为常量成员函数,在函数名后加上一个 const 即可,如:
class Object { public: int value; void print() const { cout << value << endl; return; } void update(int) const; } a[MAXN];
4、对象
C++ 是面向对象语言,通常会见到这么个说法:对象是类的实例,类是对象的模板。看起来太抽象,那么可以这样举例:大学生是一个类,而我作为大学生,是这个类的其中一个对象;计算机语言是一个类,而 C++ 是其中一个对象。类本身是抽象的,而对象则是实际存在的。比如我们定义一个叫 Student 的类,那么主函数里的定义即表示 jk 是一个 Student,hzq 也是一个 Student,它们都是 Student 类的对象:
class Student { public: int Id, Grade; int getGrade() { return Grade; } }; int main() { Student jk; Student hzq; return 0; }
访问对象的数据成员和成员函数有两种方式,例如上述代码,如果需要获取 jk 的学号,可以直接:
int g1 = jk.Grade; int g2 = jk.getGrade();
也可以定义对象指针:
1 Student *p = &jk; 2 int g1 = p -> grade; 3 int g2 = (*p).grade; 4 int g3 = p -> getGrade(); 5 int g4 = (*p).getGrade();
其中,g1 和 g2 是等价的,g3 和 g4 是等价的。
当然,在类外访问的前提是它是 public 属性的。
相同类的对象之间可以直接赋值。
5、构造函数与析构函数
① 构造函数
构造函数是类的一种特殊成员函数,会在每次创建类的新对象时自动调用执行。其名称与类名称保持一致,且不存在返回值,一般用于预处理或者为成员变量设置初始值。
1 class Vector { 2 public: 3 int x, y; 4 Vector(int _x, int _y) { 5 x = _x, y = _y; 6 } 7 } a[MAXN]; 8 9 int main() { 10 Vector x = Vector(1, 2); 11 return 0; 12 }
设置初始值还可以使用初始化列表,比如:
Vector(int _x, int _y) : x(_x), y(_y) {}
和上面的 L4~L6 是等价的。
这是带参数形式的,一般用于传入变量。同样也可以不带参数。
② 默认构造函数
1 class Vector { 2 public: 3 int x, y; 4 Vector() { 5 cout << "this is a Vector" << endl; 6 } 7 } a[MAXN]; 8 9 int main() { 10 Vector x; 11 return 0; 12 }
程序运行时,会输出一个 “this is a Vector”。
这样不带参数的构造函数称之为默认构造函数。因为 class 中的所有变量是不会进行初始化的,所以直接新创建一个对象并且其初始值是未知的,则默认的 x, y 并非为 0,这对后面程序的编写可能有影响,所以一般会在默认构造函数里对所有变量进行一次 0 赋值。
class Vector { public: int x, y; Vector() { x = 0, y = 0; } } a[MAXN];
同一个类可以构造多个拥有不同个数或不同类型的参数的函数,如下面的代码:
class Tdate { public: Tdate(); Tdate(int d); Tdate(int m, int d); Tdate(int m, int d, int y); protected: int month, day, year; };
③ 析构函数
与构造函数相反,当对象结束其声明时,系统会自动执行析构函数。其作用往往是清理善后,比如在建立对象时使用了 new 动态内存空间,析构函数就会自动执行 delete 来销毁内存空间。
形式为在构造函数的基础上加上一个“~”符号。系统默认自带析构函数,所以一般情况下不需要定义。如下代码:
class Vector { public: int x, y; Vector(int _x, int _y) : x(_x), y(_y) {} Vector() : x(0), y(0) {} ~Vector() {} } a[MAXN];
但如果需要对每一个类声明结束后进行声明操作,也可以自行定义,比如:
class Vector { public: int x, y; ~Vector() { cout << "Vector end." << endl; } };
④ 复制构造函数
复制构造函数是一个特殊的构造函数,用于根据已存在的对象初始化一个新建对象。先看这段代码:
1 class Person { 2 private: 3 char *name; 4 int age; 5 public: 6 Person(char *Name, int Age); 7 ~Person(); 8 void setAge(int x) { 9 age = x; 10 } 11 void print(); 12 }; 13 14 Person :: Person(char *Name, int Age) { 15 name = new char[strlen(Name) + 1]; 16 strcpy(name, Name); 17 age = Age; 18 cout << "constructor ...." << endl; 19 } 20 21 Person :: ~Person() { 22 cout << "destructor..." << age << endl; 23 delete name; 24 } 25 26 int main() { 27 Person p1("张三", 21); 28 Person p2 = p1; 29 return 0; 30 }
代码中包含了一个构造函数,一个析构函数。主函数中首先定义了一个 21 岁的张三,然后又定义了一个 p2 指针指向 p1,这就存在一个问题:p1 声明完后,析构函数将其名字“张三”已经 delete 了,然后在 p2 声明完后,根据析构函数的特性又需要 delete 这个“张三”,但其实其指向的 p1 已经把“张三”删除了。部分编译器并不会报错,但这显然是有问题的。这里,就需要使用重新定义复制构造函数了。
复制构造函数和析构函数一样,也是系统默认自带的,在不自行定义的情况下,默认将所有成员函数复制过去,如上述代码,其隐式复制构造函数为:
Person :: Person(const Person &o) { name = o.name; age = o.age; cout << "constructor ...." << endl; }
其本质为将 p2 的成员 name, age 全部指向 p1 中对应的成员,即成员按位复制(bit-by-bit)。为了解决这个问题,可以将复制构造函数改成如下形式:
Person :: Person(const Person &o) { name = new char[strlen(o.name) + 1]; strcpy(name, o.name); age = o.age; cout << "constructor ...." << endl; }
即在复制时分配新的内存,这样在析构函数删除指针时,两者相对独立。
这样的区别,类似于把 D 盘里的文件创建一个桌面快捷方式和复制一个新的文件放在桌面的区别。
上述所有构造函数和前面的普通成员函数一样,也可以先声明后定义,不再赘述。
⑤ 对象定义
介绍完构造函数,则可以把对象定义的所有方法列举一下,如下形式都是合法的:
1 Vector x; 2 Vector x = Vector(1, 2); 3 Vector x(1, 2); 4 Vector x = {1}; 5 Vector y(x); 6 Vector y = x;
L1 调用的是默认构造函数;
L2, 3, 4 调用的是普通构造函数,其中 L4 只适用于单个参数或者只有一个参数没有设定默认值的情况。
L5, 6 调用的是复制构造函数。
6、重载运算符
1 class Vector { 2 public: 3 int x, y; 4 Vector(int _x, int _y) : x(_x), y(_y) {} 5 Vector() : x(0), y(0) {} 6 int operator * (const Vector &o) { 7 return x * o.y + o.x * y; 8 } 9 Vector operator + (const Vector &); 10 } a[MAXN]; 11 12 Vector Vector :: operator + (const Vector &o) { 13 return Vector(x + o.x, y + o.y); 14 }
作为类,其中的变量并不能直接相加减或其他运算,因为编译器并不清楚需要怎样进行运算,这时候就需要我们给出运算方法,比如上面代码就对 Vector 类定义了 + 和 *,分别表示向量的相加和向量的内积运算,分别返回一个向量和一个整型数。
同样也可以先声明再定义。
而重载运算符还可以与 STL 容器或算法进行联动,常用的比如 sort:
1 class Vector { 2 public: 3 int x, y; 4 Vector(int _x, int _y) : x(_x), y(_y) {} 5 Vector() : x(0), y(0) {} 6 friend bool operator < (const Vector &a, const Vector &b) { 7 return a.x < b.x; 8 } 9 } a[MAXN]; 10 11 int main() { 12 a[1] = Vector(3, 2), a[2] = Vector(1, 4), a[3] = Vector(2, 5); 13 sort(a + 1, a + 4); 14 cout << a[1].x << a[2].x << a[3].x; 15 return 0; 16 }
重载了 < 符后,可以直接对 Vector 类进行排序,这里定义的是以 x 坐标从小到大排序。
注意,如果按照上述写法,声明在类内时,一定要使用 friend;或者直接在类外声明,而不需要 friend。同时,还可以这样写:
bool operator < (const Vector &a) const { return x < a.x; }
7、静态成员
前面我们介绍了类与对象的概念,发现类中定义的所有数据成员和成员函数是对于其所有对象均存在且独立的,即 Student 类定义了一个 Id 表示学号和一个 Grade 表示成绩,则无论是 jk 还是 hzq 还是其他任何 Student,都会有一个独立的学号和成绩。
而静态成员则不同。在类中定义一个静态数据成员,如下面这个 int 类型的 total:
class Student { public: int Id, Grade; int getGrade() { return Grade; } static int total; };
它不属于任意一个对象,只属于这个类,即使类没有对象,这个成员依然存在,相当于类中的全局变量。但是在定义前它不会被分配空间,所以仍需额外定义,并且往往在类外进行定义。它可以用于统计类中对象的个数,如下代码:
class Student { public: int Id, Grade; Student() { total++; } static int total; }; int Student :: total; int main() { Student jk; Student hzq; cout << Student :: total; return 0; }
最后的输出结果为 2。注意定义时是可以赋初值的,不赋的话默认为 0。
成员函数同理,也有静态的。
8、this 指针
this 指针是类中一种特殊的指针,表示指向调用该函数的对象自身,即成员函数所属的类对象的首地址,编译时 this 指针是隐含的,如下列原代码及其隐含代码:
Vector(int a, int b) { x = a; y = b; } inline Vector(Vector *this, int a, int b) { this -> x = a; this -> y = b; }
this 指针一般用于区分二义性,比如我们在写带参数的构造函数时,如果其参数与类的数据成员命名相同,那么程序也无法理解你的意思,这个时候可以在类的数据成员前显式使用 this 指针,如下代码:
Vector(int x, int y) { this -> x = x; this -> y = y; }
当然这种情况一般也是可以避免的。
9、对象成员
类的数据成员不仅可以是基本数据类型,也可以是其他类的对象,这种成员称之为对象成员。比如线面先声明了一个 Salary 类表示工资,再声明了一个 Worker 类表示工人,工人中有一个 Salary 类的对象作为数据成员,表示工人的工资。
class Salary { public: double Wage, Subsidy; }; class Worker { private: char *name; int age; Salary salary; };