C++ 类与对象

在 C++ 中,“类(class)” 是一种由我们自己定义的复合数据类型。前面使用结构体时,我们已经可以把姓名、年龄这些彼此相关的数据打包进同一个类型中,不用再定义成几个零散的变量。

不过在实际开发中,一个学生除了有姓名、年龄这些数据之外,往往还会有 “自我介绍”、“修改年龄” 等操作。使用类,我们就可以把相关的数据和操作这些数据的函数封装到同一个类型中,这也是面向对象编程(OOP)最基础的概念。

C++ 类与对象是什么?

初学面向对象编程时,最先接触的两个概念就是 “类” 和 “对象”。我们可以结合汽车制造的例子(如下图所示),从以下 2 个方面来理解它们之间的关系:

  • 类(class):相当于汽车的 “设计图纸”。图纸本身不是车,不能直接开上路,但它规定了这类汽车应该有哪些属性(如品牌、颜色)和哪些功能(如启动、刹车)。
  • 对象(object):相当于根据这张设计图纸真正生产出来的一辆辆 “具体的汽车”。同一张图纸可以造出多辆汽车,每辆车都是一个独立的对象。

面向对象中类与对象的汽车图纸类比

放到代码中也是一样的道理。比如我们要在程序里表示学生,可以先定义一个 Student 类,规定学生都有姓名 name、年龄 age 这些数据,并且能执行 introduce() 函数。等后面根据 Student 创建出 stu1、stu2 时,它们才是具体的学生对象。

这里小伙伴们一定要搞清楚:定义一个类,并不等于已经创建了对象。写完 class Student 只是定义了一种新的数据类型,还没有在内存中分配空间。根据类创建出具体对象的过程叫做 “实例化对象”,你可以把它简单理解为 “按图纸造出实物”。

C++ 类的定义与使用

在 C++ 中,想要使用类来组织数据,一般分为 2 步:① 使用 class 关键字定义类;② 根据类创建对象并访问里面的成员。下面我们就来看看这两步的具体写法。

1. 使用 class 定义类

在 C++ 中,我们可以使用 class 关键字来定义一个类。类的花括号内部既可以定义用来保存数据的变量,也可以定义用来操作这些数据的函数。

语法:

class 类名 {
public:
    数据类型 成员变量;
    返回值类型 成员函数(参数列表) {
        // 函数体
    }
};

说明:

其中,class 后面是我们自己定义的类名,通常采用首字母大写的 “大驼峰命名法”(如 Student、Car)。花括号内部的 public: 是访问修饰符,这里我们先简单理解成 “允许在类的外部直接访问下面这些成员” 即可,下一节会详细介绍。

定义在类内部的成员主要分为以下 2 类:

  • 成员变量:写在类内部、用来保存对象数据的变量,平时也常称为 “属性” 或 “数据成员”。定义时我们既可以只声明类型(如 int age;),也可以直接使用等号指定默认初始值(如 int age = 18;)。
  • 成员函数:写在类内部、用来操作对象数据的函数,平时也常称为 “方法”。成员函数可以直接读取或修改当前对象的成员变量,不需要再从外部把数据一个个作为参数传进来。

注意 普通函数定义的右花括号 } 后面不需要加分号。但和结构体一样,类定义结束之后,右花括号 } 后面的英文分号(;)一定不能省略,大家写代码时千万不要漏掉。

2. 创建对象并访问成员

类定义好之后,我们就可以像使用 int、double、std::string 这些常见类型一样,使用类名来创建对象,并通过点运算符(.)访问对象里面的成员。

语法:

类名 对象名;
对象名.成员变量 = 值;
对象名.成员函数(实参);

说明:

比如写下一行 Student stu;,就创建出了一个名为 stu 的学生对象。这和 int age; 的结构非常相似:Student 是类型名,stu 是具体的对象。创建好对象之后,我们就可以通过 对象名.成员变量 读写属性,通过 对象名.成员函数() 调用方法。

示例 1:定义类并创建对象

#include <iostream>
#include <string>

class Student {
public:
    std::string name;
    int age;

    void introduce() {
        std::cout << "姓名:" << name << '\n';
        std::cout << "年龄:" << age << '\n';
    }
};

int main() {
    Student stu;
    stu.name = "阿莫";    // 给成员变量赋值
    stu.age = 20;         // 给成员变量赋值
    stu.introduce();      // 调用成员函数
    return 0;
}

运行结果如下。

姓名:阿莫
年龄:20

分析:

在这个例子中,我们定义了一个 Student 类,包含 name、age 两个成员变量和 introduce() 成员函数。在 main() 函数中,通过 Student stu; 创建对象并给属性赋值后,调用 stu.introduce() 就能把学生信息打印出来。

可能有些小伙伴会觉得奇怪:“为什么 introduce() 没有接收任何参数,里面却能直接打印 name 和 age 呢?” 其实是因为它们都定义在同一个 class 内部。当我们通过 stu.introduce() 调用它时,函数自然能直接读取 stu 自己的成员变量。

需要注意的是,如果类中的 int、double 等基本类型成员在创建对象后没有赋值,里面存放的是随机垃圾值。我们除了创建对象后逐个赋值之外,也可以在定义类时写成 int age = 18; 指定默认初始值,或者配合后面要学习的构造函数来完成初始化。

同一个类可以创建出多个不同的对象,那么修改其中一个对象的数据,会影响到另一个对象吗?下面我们再来看一个例子。

示例 2:创建多个独立对象

#include <iostream>
#include <string>

class Student {
public:
    std::string name;
    int age;

    void growUp(int years) {
        age = age + years;
    }

    void introduce() {
        std::cout << name << " 今年 " << age << " 岁\n";
    }
};

int main() {
    Student stu1;
    stu1.name = "Jack";
    stu1.age = 18;

    Student stu2;
    stu2.name = "Lucy";
    stu2.age = 19;

    stu1.growUp(2);    // 只让 stu1 增加 2 岁
    stu1.introduce();
    stu2.introduce();
    return 0;
}

运行结果如下。

Jack 今年 20 岁
Lucy 今年 19 岁

分析:

stu1 和 stu2 是根据同一个 Student 类创建出来的两个对象。虽然它们来自同一个类,但在内存中各自拥有独立的存储空间,用来保存自己的成员变量。

当我们执行 stu1.growUp(2); 时,修改的只是 stu1 自己的 age(从 18 变成 20),stu2 的 age 依然保持 19 不变。随后两次调用 introduce() 虽然执行的是同一份函数代码,但读取的都是各自对象的数据,彼此互不干扰。

成员函数的类内声明与类外定义

在前面的例子中,我们都是把成员函数的完整代码直接写在 class 的花括号内部。当函数只有短短两三行时,这种写法非常省事。

但如果一个类包含很多成员函数,或者某个函数的代码比较长,全都塞在类里面就会显得很臃肿。这时我们可以像前面学过的普通函数那样,把函数声明与定义拆开:在类的内部只写函数声明,而把具体的函数实现放到类的外部。

语法:

class 类名 {
public:
    返回值类型 函数名(参数列表);
};

返回值类型 类名::函数名(参数列表) {
    // 函数体
}

说明:

在类的内部声明成员函数时,我们只需要写出返回值类型、函数名和参数列表,并在末尾加上英文分号(;)。而在类的外部定义该函数时,必须在函数名前面加上 类名::。其中,:: 是作用域限定符,用来指明这个函数属于该类的成员函数,而不是普通的全局函数。

示例 3:在类外定义成员函数

#include <iostream>
#include <string>

class Student {
public:
    std::string name;
    int age;

    void introduce();    // 类内声明成员函数
};

// 类外定义成员函数
void Student::introduce() {
    std::cout << "姓名:" << name << ",年龄:" << age << '\n';
}

int main() {
    Student stu;
    stu.name = "Jack";
    stu.age = 20;
    stu.introduce();
    return 0;
}

运行结果如下。

姓名:Jack,年龄:20

分析:

void introduce(); 这句代码在 Student 类内部只做函数声明,末尾直接加分号结束。这样阅读类的定义时,我们一眼就能看清它对外提供了哪些功能。

到了类的外部,void Student::introduce() 通过 Student:: 指明了它是 Student 类的成员函数,因此在函数体内部依然可以直接访问成员变量 name 和 age。

这里小伙伴们要特别注意,在类的外部定义成员函数时,千万不能漏掉 Student:: 前缀。下面这种漏写前缀的写法就是错误的,因为编译器会把它当成普通的全局函数,此时在里面直接访问 name 就会编译报错。

void introduce() {
    std::cout << "姓名:" << name << '\n';
}

C++ class 与 struct 的区别

在上一章中,我们已经学过了结构体。既然 struct 也能用来定义复合数据类型,那它和 class 到底有什么区别呢?

在 C++ 中,struct 同样可以包含成员变量和成员函数。说白了,struct 和 class 在语法上最主要的区别只有一个,就是 “默认的访问权限” 不同:

  • struct:如果不写访问修饰符,里面的成员默认是 public(公有的),在外部可以直接通过对象访问。
  • class:如果不写访问修饰符,里面的成员默认是 private(私有的),在外部不能直接通过对象访问。

这也是为什么在前面的 class Student 示例中,我们都在开头写上了 public:。如果都不写访问修饰符,两者的默认访问权限对比如下:

struct StudentA {
    int age = 18;    // 默认是 public
};

class StudentB {
    int age = 18;    // 默认是 private
};

如上面代码所示,创建对象后在外部直接访问 StudentA 的 age 完全正常,但如果直接在外部访问 StudentB 的 age 就会编译报错。

在实际开发中,如果只是把几项简单的数据打包在一起,比如表示二维坐标的 x 和 y,直接使用 struct 会更省事。如果需要把数据、成员函数以及访问权限封装成一个完整的类型,我们一般都会使用 class。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号