类与对象

之前章节中介绍的示例,所使用的数据和函数之间是没有任何直接联系的,他们之间的联系就是数据以参数的形式传入函数进行处理,这种方式也被称为 面向过程编程

面向对象编程 是在 面向过程编程 的基础上发展来的,它比面向过程编程具有更强的灵活性和扩展性。类(Class)对象(Object) 是面向对象编程的两个核心概念。面向对象编程是程序员发展的分水岭,很多初学者会因无法理解面向对象而放弃学习编程。

C++ 在 C 语言的基础上增加了面向对象编程。面向过程编程以过程(函数)为中心,关注的是解决问题的步骤,它将数据和操作数据的函数分离,而面向对象则是将变量与函数建立起联系。

面向对象编程从面向对象设计开始,而面向对象设计从 "搞抽象" 开始。

我们在学习结构体、枚举的时候,实际上也用到了抽象,只不过 C 语言的结构体只能抽象数据(变量),无法抽象函数。

什么是抽象?举几个例子:

  1. 奔驰、宝马、奥迪、小米、理想、问界等等都是汽车,可以抽象为 汽车类
  2. 医生、老师、学生、程序员等都是职业,或者说都是人,可以抽象为 职业类人类
  3. 猪、牛、羊、猫、狗、长颈鹿等都是动物,可以抽象为 动物类
  4. 哈士奇、萨摩耶、杜宾、比格等都是狗,可以抽象为 狗类

如果想要设计一只狗的类,你需要首先思考一下,狗具有什么特征?

  1. 从表面特征来看,比如狗的品种、毛发颜色、体重、名字、年龄等等;
  2. 从所具有的行为来描述,例如,狗会汪汪叫,会吃饭,会睡觉等等。

如果把狗的信息用代码来表示,那么,狗的表面特征可以用变量表示,行为特征可以用函数来表示,采用面向过程的方式,则代码如下:

#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() 方法的执行属于类的内部。

为了增加代码的可读性和维护性,私有成员的命名方式与普通变量函数的命名方式有所不同。为了与公共成员以及普通变量函数做区分:

  1. 一般会在私有属性的前面加一个 下划线_或者 m_ 前缀,也可以加 下划线_ 后缀;
  2. 保护成员函数:开头应当加上 1 个下划线 _
  3. 私有成员函数:开头应当加上 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++ 中,成员函数的声明和定义可以在同一地点进行,也可以分开进行。

头文件中如果包含了很多函数的定义,这会让代码看起来很冗长,也不容易找到想要的函数。而且,如果我们想要修改或添加某个函数的实现细节,那么我们就需要修改头文件。但是这样会影响到所有包含了这个头文件的源文件,也会增加代码的复杂度和出错的风险。

声明与定义分离的好处:

  1. 减少编译时间;
  2. 提高头文件代码的可读性和可维护性。

类的的分文件编写是一种让代码结构更加清晰的方法,通常可以分为以下几个步骤:

  • 创建后缀名为 .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。求:

  1. 矩形的面积和周长,
  2. 分别利用全局函数和成员函数判断,两个矩形是否相等。

代码如下:

#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;
}