之前章节中介绍的示例,所使用的数据和函数之间是没有任何直接联系的,他们之间的联系就是数据以参数的形式传入函数进行处理,这种方式也被称为 面向过程编程。
面向对象编程 是在 面向过程编程 的基础上发展来的,它比面向过程编程具有更强的灵活性和扩展性。类(Class)
和 对象(Object) 是面向对象编程的两个核心概念。面向对象编程是程序员发展的分水岭,很多初学者会因无法理解面向对象而放弃学习编程。
C++ 在 C 语言的基础上增加了面向对象编程。面向过程编程以过程(函数)为中心,关注的是解决问题的步骤,它将数据和操作数据的函数分离,而面向对象则是将变量与函数建立起联系。
面向对象编程从面向对象设计开始,而面向对象设计从 "搞抽象" 开始。
我们在学习结构体、枚举的时候,实际上也用到了抽象,只不过 C 语言的结构体只能抽象数据(变量),无法抽象函数。
什么是抽象?举几个例子:
- 奔驰、宝马、奥迪、小米、理想、问界等等都是汽车,可以抽象为
汽车类; - 医生、老师、学生、程序员等都是职业,或者说都是人,可以抽象为
职业类或人类; - 猪、牛、羊、猫、狗、长颈鹿等都是动物,可以抽象为
动物类; - 哈士奇、萨摩耶、杜宾、比格等都是狗,可以抽象为
狗类。
如果想要设计一只狗的类,你需要首先思考一下,狗具有什么特征?
- 从表面特征来看,比如狗的品种、毛发颜色、体重、名字、年龄等等;
- 从所具有的行为来描述,例如,狗会汪汪叫,会吃饭,会睡觉等等。
如果把狗的信息用代码来表示,那么,狗的表面特征可以用变量表示,行为特征可以用函数来表示,采用面向过程的方式,则代码如下:
#include <iostream>
#include <string>
using namespace std;
// 行为特征
void eat() {
cout << "吃饭" << endl;
}
void sleep() {
cout << "睡觉" << endl;
}
int main() {
// 表面特征
string name = "旺财";
int age = 10;
float weight = 9.9;
return 0;
}
而对象可以通过 类 把表面特征与行为特征建立起联系,因此,相比于只用变量和函数,类可以更好地模拟现实生活中的事物。
类的定义与使用
类(class) 可以说是 结构体(struct)的升级版。
与结构体一样,类只是一种复杂数据类型的声明,不占用内存空间。而对象是类这种数据类型的一个变量,或者说是通过类这种数据类型创建出来的一份实实在在的数据,所以占用内存空间。
注意
C++ 中的结构体和类,都是可以抽象函数的。
对于初学者而言,只需要将 C++ 中的结构体当作 C 语言的结构体来使用即可。
类定义是以关键字 class 开头,并在它后面依次包含 类名(类名首字母大写),一组放在 {}
内的成员属性和成员函数,以及结尾的 分号;。例如,我们使用关键字 class 定义 Dog 类,代码如下:
#include <iostream>
#include <string>
using namespace std;
class Dog {
// 访问权限控制符
public:
// 表面特征
string name;
int age;
// 行为特征
void eat() {
cout << "吃饭" << endl;
}
void sleep() {
cout << "睡觉" << endl;
}
};
类中的变量和函数都被称为类的 成员,类的成员变量称为类的 属性(Property),将类的成员函数称为类的 方法(Method)
。在面向对象的编程语言中,经常把函数(Function)称为 方法(Method)。
类是创建对象的模板,一个类可以创建多个对象,每个对象都是类的一个 实例(Instance)。创建对象的过程也叫 类的实例化
,被创建出来的对象拥有类的成员变量和成员函数,类在定义后必须先实例化才能使用。
举个例子,我的小狗就是狗这个大类的一个实例,因此我们需要先用 Dog 类来实例化一个实例来代表我的小狗:
#include <iostream>
#include <string>
using namespace std;
class Dog {
// 访问权限控制符
public:
// 表面特征
string name;
int age;
// 行为特征
void eat() {
cout << "吃饭" << endl;
}
void sleep() {
cout << "睡觉" << endl;
}
};
int main() {
// 创建对象
Dog my_dog;
// 对属性赋值
my_dog.name = "旺财";
my_dog.age = 3;
cout << my_dog.name << endl;
cout << my_dog.age << endl;
// 调用对象的方法
my_dog.eat();
my_dog.sleep();
return 0;
}
访问权限
在类的声明中,所有的属性和方法都是写在 public 中的。public 实际上是一个访问控制关键字,它规定了谁可以访问接下来声明的数据和函数,也称为 类访问修饰符,它们是面向对象编程中封装特性的重要实现手段。访问控制关键字有以下 3 种:
| 访问控制关键字 | 权限 | 类与对象内部 | 类与对象外部 | 子类 |
|---|---|---|---|---|
| public | 公共 | 可以访问 | 可以访问 | 可以访问 |
| private | 私有 | 可以访问 | 不可以访问 | 不可以访问 |
| protected | 保护 | 可以访问 | 不可以访问 | 可以访问 |
以上 3 种访问权限修饰符的使用方式是相同的,代码如下:
class Base {
public:
// 公有成员
protected:
// 受保护成员
private:
// 私有成员
};
公有成员的例子,可以查看上面的代码,这里着重讲解私有成员。私有属性和方法在类的外部是不可访问的,只有在类的内部可以正常访问。
例如,我们把 public 中的 age 属性和 sleep() 方法放在 private 中,代码如下:
#include <iostream>
#include <string>
using namespace std;
class Dog {
// 私有权限
private:
int age;
void sleep() {
cout << "睡觉" << endl;
}
public:
// 公共权限
string name;
void eat() {
cout << "吃饭" << endl;
}
};
int main() {
// 创建对象
Dog my_dog;
// 对属性赋值
my_dog.name = "旺财";
my_dog.age = 3;
cout << my_dog.name << endl;
cout << my_dog.age << endl;
// 调用对象的方法
my_dog.eat();
my_dog.sleep();
return 0;
}
运行会出现报错,提示 age、sleep() 为私有成员。

如果,类中的成员没有写在 public、private、protected 3 个访问修饰符中,那么,该成员则默认为私有的。例如,我们可以把上面代码中 private 关键字删除,成员属性 age 和方法 sleep()
依然无法使用,代码如下:
#include <iostream>
#include <string>
using namespace std;
class Dog {
// 未写在访问权限中,默认为私有成员
int age;
void sleep() {
cout << "睡觉" << endl;
}
public:
// 公共权限
string name;
void eat() {
cout << "吃饭" << endl;
}
};
int main() {
// 创建对象
Dog my_dog;
// 对属性赋值
my_dog.name = "旺财";
my_dog.age = 3;
cout << my_dog.name << endl;
cout << my_dog.age << endl;
// 调用对象的方法
my_dog.eat();
my_dog.sleep();
return 0;
}
运行结果与之前是相同的,也是报错提示 age、sleep() 为私有成员。但是,如果我们把 sleep() 方法,在公共成员 eat() 的函数体中调用,并在类外调用 eat() 方法,则 sleep() 可以正常访问,因为 eat() 方法的执行属于类的内部。
为了增加代码的可读性和维护性,私有成员的命名方式与普通变量函数的命名方式有所不同。为了与公共成员以及普通变量函数做区分:
- 一般会在私有属性的前面加一个
下划线_或者m_前缀,也可以加下划线_后缀; - 保护成员函数:开头应当加上 1 个下划线
_; - 私有成员函数:开头应当加上 2 个下划线
__。
示例如下:
class Dog {
private:
int _age; // m_age、age_
float _weight; // m_weight、weight_
void __sleep() {
cout << "睡觉" << endl;
}
protected:
void _bark() {
cout << "汪汪" << endl;
}
};
那么,我们该如何查看类中私有属性呢?实际操作中,我们一般会在私有区域定义属性,在公有区域定义与之相关的函数,以便在类的外部也可以调用这些函数
一般情况下,我们会使用 get_私有属性名 作为查看私有属性的函数名,set_私有属性名 作为修改或设置私有属性的函数名,如下所示:
#include <iostream>
#include <string>
using namespace std;
class Dog {
private:
int _age;
float _weight;
void __sleep() {
cout << "睡觉" << endl;
}
public:
string name;
void eat() {
__sleep();
cout << "吃饭" << endl;
}
// 获取私有属性 age 方法
int get_age() {
return _age;
}
// 设置私有属性 age 方法
int set_age(int age) { // bool、char、short
// 判断参数值是否合理
if (age > 30 || age < 0) {
cout << "年龄不合理" << endl;
return 0;
}
_age = age;
return 1;
}
// 获取私有属性 weight 方法
float get_weight() {
return _weight;
}
// 设置私有属性 weight 方法
void set_weight(float weight) {
_weight = weight;
}
};
int main() {
// 创建对象
Dog my_dog;
// 调用公共成员
my_dog.name = "旺财";
cout << my_dog.name << endl;
my_dog.eat();
// 通过调用公共接口,操作私有属性
if (!my_dog.set_age(16)) {
cout << "赋值失败" << endl;
}
my_dog.set_weight(9.9);
cout << "我的狗今年:" << my_dog.get_age() << " 岁,体重:" << my_dog.get_weight() << "kg" << endl;
return 0;
}
使用 get/set 方法作为私有属性的接口,最大的好处是,我们还可以对用户输入的数据进行校验,例如,狗的寿命最多也就 20 年,那么年龄如果是小于 0 或大于 20,都是不合理的,那么,我们的 set 方法就可以这么写:
int set_age(int age) {
if (age > 20 || age <= 0) {
return 0;
}
_age = age;
return 1;
}
这样,我们的 set 函数也就有了返回值,如果结果合理,那么返回值就是 1,不合理则返回值为 0。
在类的声明中,访问控制关键字可以重复出现,比如,我们的成员属性和方法有非常多,我们就可以把他们分开来写,示例如下:
#include <iostream>
#include <string>
using namespace std;
class Dog {
private:
int age;
int num_1;
int num_2;
int num_3;
int num_4;
int num_5;
int num_6;
private:
void sleep() {
cout << "睡觉" << endl;
}
void func_1() {}
void func_2() {}
void func_3() {}
void func_4() {}
void func_5() {}
void func_6() {}
public:
string name;
public:
void eat() {
sleep();
cout << "吃饭" << endl;
}
// 获取 age 的值
int get_age() {
return age;
}
// 修改 age 的值
void set_age(int a) {
age = a;
}
};
声明与定义分离
在 C++ 中,成员函数的声明和定义可以在同一地点进行,也可以分开进行。
头文件中如果包含了很多函数的定义,这会让代码看起来很冗长,也不容易找到想要的函数。而且,如果我们想要修改或添加某个函数的实现细节,那么我们就需要修改头文件。但是这样会影响到所有包含了这个头文件的源文件,也会增加代码的复杂度和出错的风险。
声明与定义分离的好处:
- 减少编译时间;
- 提高头文件代码的可读性和可维护性。
类的的分文件编写是一种让代码结构更加清晰的方法,通常可以分为以下几个步骤:
- 创建后缀名为
.h的头文件,在头文件中写类的声明,以及类函数的声明; - 创建后缀名为
.cpp的源文件,在源文件中写类函数的定义,同时引入自定义头文件,将头文件与源文件绑定 - 在需要使用函数的地方,引入自定义头文件,然后直接调用函数,无需再写函数的实现。
当定义与声明分开时,在类函数定义部分需要使用 类名:: 前缀来指定所属的类,如果不使用 ::,编译器则不清楚调用的是哪一个作用域内的函数。这有助于提高代码的组织结构,尤其是对于大型项目。
比如,我们可以把上面的 Dog 类的声明与定义分离,代码如下:
class Dog {
private:
int _age; // 私有属性加下划线前缀
float _weight;
public:
string name;
void eat();
int get_age(); // 获取 age 的值
void set_age(int age); // 修改 age 的值
float get_weight(); // 获取 weight 的值
void set_weight(float weight); // 修改 weight 的值
};
void Dog::eat() {
cout << "吃饭" << endl;
}
int Dog::get_age() {
return _age;
}
void Dog::set_age(int age) {
_age = age;
}
float Dog::get_weight() {
return _weight;
}
void Dog::set_weight(float weight) {
_weight = weight;
}
练习题 - 矩形类
定义矩形类 Rect,包括 2 个 private 类型数据成员 length、width,分别代表其长和宽,单位为 cm。求:
- 矩形的面积和周长,
- 分别利用全局函数和成员函数判断,两个矩形是否相等。
代码如下:
#include <iostream>
using namespace std;
class Rect {
private:
int _length;
int _width;
public:
int get_length();
int get_width();
void set_length(int length);
void set_width(int width);
int calc_area(); // 面积
int calc_peri(); // 周长
bool is_same(Rect& box); // 比较是否相同
};
int Rect::get_length() {
return _length;
}
int Rect::get_width() {
return _width;
}
void Rect::set_length(int length) {
_length = length;
}
void Rect::set_width(int width) {
_width = width;
}
int Rect::calc_area() {
return _length * _width;
}
int Rect::calc_peri() {
return 2*(_length + _width);
}
bool Rect::is_same(Rect &box) {
return _width == box.get_width() && _length == box.get_length()
||
_width == box.get_length() && _length == box.get_width();
}
// 全局函数,判断矩形是否相同
bool is_same_box(Rect& r_1, Rect& r_2) {
return r_1.get_length() == r_2.get_length() && r_1.get_width() == r_2.get_width()
||
r_1.get_length() == r_2.get_width() && r_1.get_width() == r_2.get_length();
}
int main() {
// 创建 2 个矩形对象
Rect box_1, box_2;
// box_1 赋值
box_1.set_length(10);
box_1.set_width(9);
// box_2 赋值
box_2.set_length(9);
box_2.set_width(8);
// 打印矩形的面积与周长
cout << "box_1 的面积为:" << box_1.calc_area() << ",周长为:" << box_1.calc_peri() << endl;
cout << "box_2 的面积为:" << box_2.calc_area() << ",周长为:" << box_2.calc_peri() << endl;
// 通过全局函数判断两个矩形是否相同
cout << "========通过全局函数判断========" << endl;
if (is_same_box(box_1, box_2)) {
cout << "相同" << endl;
} else {
cout << "不同" << endl;
}
// 通过成员函数判断
cout << "========通过成员函数判断========" << endl;
if (box_1.is_same(box_2)) {
cout << "相同" << endl;
} else {
cout << "不同" << endl;
}
if (box_2.is_same(box_1)) {
cout << "相同" << endl;
} else {
cout << "不同" << endl;
}
return 0;
}