跳转至

类(class)是结构体的拓展,不仅能够拥有成员元素,还拥有成员函数。

在面向对象编程(OOP)中,对象就是类的实例,也就是变量。

C++ 中 struct 关键字定义的也是类,上文中的 结构体 的定义来自 C。因为某些历史原因,C++ 保留并拓展了 struct

定义类

类使用关键字 class 或者 struct 定义,下文以 class 举例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class ClassName {
  ...
};

// Example:
class Object {
 public:
  int weight;
  int value;
} e[array_length];

const Object a;
Object b, B[array_length];
Object *c;

与使用 struct 大同小异。该例定义了一个名为 Object 的类。该类拥有两个成员元素,分别为 weight,value;并在 } 后使用该类型定义了一个数组 e

定义类的指针形同 struct

访问说明符

不同于 struct 中的举例,本例中出现了 public,这属于访问说明符。

  • public:该访问说明符之后的各个成员都可以被公开访问,简单来说就是无论 类内 还是 类外 都可以访问。
  • protected:该访问说明符之后的各个成员可以被 类内、派生类或者友元的成员访问,但类外 不能访问
  • private:该访问说明符之后的各个成员 只能类内 成员或者友元的成员访问,不能 被从类外或者派生类中访问。

对于 struct,它的所有成员都是默认 public。对于 class,它的所有成员都是默认 private

关于 "友元" 和 "派生类",可以参考下方折叠框,或者查询网络资料进行详细了解。

对于算法竞赛来说,友元和派生类并不是必须要掌握的知识点。

关于友元以及派生类的基本概念

友元(friend):使用 friend 关键字修饰某个函数或者类。可以使得在 被修饰者 在不成为成员函数或者成员类的情况下,访问该类的私有(private)或者受保护(protected)成员。简单来说就是只要带有这个类的 friend 标记,就可以访问私有或受保护的成员元素。

派生类(derived class):C++ 允许使用一个类作为 基类,并通过基类 派生派生类。其中派生类(根据特定规则)继承基类中的成员变量和成员函数。可以提高代码的复用率。

派生类似 "is" 的关系。如猫(派生类)"is" 哺乳动物(基类)。

对于上面 privateprotected 的区别,可以看做派生类可以访问基类的 protected 的元素(public 同),但不能访问 private 元素。

访问与修改成员元素的值

方法形同 struct

  • 对于变量,使用 . 符号。
  • 对于指针,使用 -> 符号。

成员函数

成员函数,顾名思义。就是类中所包含的函数。

常见成员函数举例
1
2
3
vector.push_back();
set.insert();
queue.empty();

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Class_Name {
  ... type Function_Name(...) { ... }
};

// Example:
class Object {
 public:
  int weight;
  int value;

  void print() {
    cout << weight << endl;
    return;
  }

  void change_w(int);
};

void Object::change_w(int _weight) { weight = _weight; }

Object var;

该类有一个打印 Object 成员元素的函数,以及更改成员元素 weight 的函数。

和函数类似,对于成员函数,也可以先声明,在定义,如第十四行(声明处)以及十七行后(定义处)。

如果想要调用 varprint 成员函数,可以使用 var.print() 进行调用。

重载运算符

何为重载

C++ 允许编写者为名称相同的函数或者运算符指定不同的定义。这称为 重载(overload)。

如果同名函数的参数种类、数量中的一者或多者两两不相同,则这些同名函数被看做是不同的。

需要注意的是:如果两个同名函数的区别仅仅是返回值的类型不同则无法进行重载,此时编译器会拒绝编译!

如果在调用时不会出现混淆(指调用某些同名函数时,无法根据所填参数种类和数量唯一地判断出被调用函数。常发生在具有默认参数的函数中),则编译器会根据调用时所填参数判断应调用函数。

而上述过程被称作重载解析。

重载运算符,可以部分程度上代替函数,简化代码。

下面给出重载运算符的例子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Vector {
 public:
  int x, y;

  Vector() : x(0), y(0) {}

  Vector(int _x, int _y) : x(_x), y(_y) {}

  int operator*(const Vector& other) const { return x * other.x + y * other.y; }

  Vector operator+(const Vector&) const;
  Vector operator-(const Vector&) const;
};

Vector Vector::operator+(const Vector& other) const {
  return Vector(x + other.x, y + other.y);
}

Vector Vector::operator-(const Vector& other) const {
  return Vector(x - other.x, y - other.y);
}

// 关于4,5行表示为x,y赋值,具体实现参见后文。

该例定义了一个向量类,并重载了 * + - 运算符,并分别代表向量内积,向量加,向量减。

重载运算符的模板大致可分为下面几部分。

1
2
3
/*类定义内重载*/ 返回类型 operator符号(参数){...}

/*类定义内声明,在外部定义*/ 返回类型 类名称::operator符号(参数){...}

对于自定义的类,如果重载了某些运算符(一般来说只需要重载 < 这个比较运算符),便可以使用相应的 STL 容器或算法,如 sort

如要了解更多,可参见「参考资料」第四条。

可以被重载的运算符
1
2
3
4
5
6
+       -       *       /       %       ^       &
|       ~       !       =       <       >       +=
-=      *=      /=      %=      ^=      &=      |=
<<      >>      >>=     <<=     ==      !=      <=
>=      &&      ||      ++      --      ,       ->*
->      ()      []      new     new []  delete  delete []

在实例化变量时设定初始值

为完成这种操作,需要定义 默认构造函数(Default constructor)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class ClassName {
  ... ClassName(...)... { ... }
};

// Example:
class Object {
 public:
  int weight;
  int value;

  Object() {
    weight = 0;
    value = 0;
  }
};

该例定义了 Object 的默认构造函数,该函数能够在我们实例化 Object 类型变量时,将所有的成员元素初始化为 0

若无显式的构造函数,则编译器认为该类有隐式的默认构造函数。换言之,若无定义任何构造函数,则编译器会自动生成一个默认构造函数,并会根据成员元素的类型进行初始化(与定义 内置类型 变量相同)。

在这种情况下,成员元素都是未初始化的,访问未初始化的变量的结果是未定义的(也就是说并不知道会返回何值)。

如果需要自定义初始化的值,可以再定义(或重载)构造函数。

关于定义(或重载)构造函数

一般来说,默认构造函数是不带参数的,这区别于构造函数。构造函数和默认构造函数的定义大同小异,只是参数数量上的不同。

构造函数可以被重载(当然首次被叫做定义)。需要注意的是,如果已经定义了构造函数,那么编译器便不会再生成无参数的默认构造函数。这会可能会使试图以默认方法构造变量的行为编译失败(指不填入初始化参数)。

使用 C++11 或以上时,可以使用 {} 进行变量的初始化。

关于 {}

使用 {} 进行初始化,会用到 std::initializer_list 这一个轻量代理对象进行初始化。

初始化步骤大概如下

  1. 尝试寻找参数中有 std::initializer_list 的默认构造函数,如果有则调用(调用完后不再进行下面的查找,下同)。
  2. 尝试将 {} 中的元素填入其他构造参数,如果能将参数按照顺序填满(默认参数也算在内),则调用该默认构造函数。
  3. 若无 private 成员元素,则尝试在 类外 按照元素定义顺序或者下标顺序依次赋值。

上述过程只是完整过程的简化版本,详细内容参见 "参考资料九"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Object {
 public:
  int weight;
  int value;

  Object() {
    weight = 0;
    value = 0;
  }

  Object(int _weight = 0, int _value = 0) {
    weight = _weight;
    value = _value;
  }

  // the same as
  // Object(int _weight,int _value):weight(_weight),value(_value) {}
};

// the same as
// Object::Object(int _weight,int _value){
//   weight = _weight;
//   value = _value;
// }
//}

Object A;        // ok
Object B(1, 2);  // ok
Object C{1, 2};  // ok,(C++11)

关于隐式类型转换

有时候会写出如下的代码

1
2
3
4
5
6
7
8
class Node {
 public:
  int var;

  Node(int _var) : var(_var) {}
};

Node a = 1;

看上去十分不符合逻辑,一个 int 类型不可能转化为 node 类型。但是编译器不会进行 error 提示。

原因是在进行赋值时,首先会将 1 作为参数调用 node::node(int),然后调用默认的复制函数进行赋值。

但大多数情况下,编写者会希望编译器进行报错。这时便可以在构造函数前追加 explicit 关键字。这会告诉编译器必须显式进行调用。

1
2
3
4
5
6
class Node {
 public:
  int var;

  explicit Node(int _var) : var(_var) {}
};

也就是说 node a=1 将会报错,但 node a=node(1) 不会。因为后者显式调用了构造函数。当然大多数人不会写出后者的代码,但此例足以说明 explicit 的作用。

不过在算法竞赛中,为了避免此类情况常用的是 "加强对代码的规范程度",从源头上避免

销毁

这是不可避免的问题。每一个变量都将在作用范围结束走向销毁。

但对于已经指向了动态申请的内存的指针来说,该指针在销毁时不会自动释放所指向的内存,需要手动释放动态内存。

如果结构体的成员元素包含指针,同样会遇到这种问题。需要用到析构函数来手动释放动态内存。

析构 函数(Destructor)将会在该变量被销毁时被调用。重载的方法形同构造函数,但需要在前加 ~

默认定义的析构函数通常对于算法竞赛已经足够使用,通常我们只有在成员元素包含指针时才会重载析构函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Object {
 public:
  int weight;
  int value;
  int* ned;

  Object() {
    weight = 0;
    value = 0;
  }

  ~Object() { delete ned; }
};

为类变量赋值

默认情况下,赋值时会按照对应成员元素赋值的规则进行。也可以使用 类名称()类名称{} 作为临时变量来进行赋值。

前者只是调用了复制构造函数(copy constructor),而后者在调用复制构造函数前会调用默认构造函数。

另外默认情况下,进行的赋值都是对应元素间进行 浅拷贝,如果成员元素中有指针,则在赋值完成后,两个变量的成员指针具有相同的地址。

1
2
3
4
// A,tmp1,tmp2,tmp3类型为Object
tmp1 = A;
tmp2 = Object(...);
tmp3 = {...};

如需解决指针问题或更多操作,需要重载相应的构造函数。

派生类

关于派生类,封装和多态
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
#include <bits/stdc++.h>
using namespace std;

// ========== 基类:Person(人员)==========
class Person {
private:
    string name;      // 封装:私有成员
    int age;
    string id;        // 身份证号

protected:
    double salary;         // 保护成员:派生类可访问,外部不可访问

public:
    // 构造函数
    Person(const string& n, int a, const string& i, double s = 0)
        : name(n), age(a), id(i), salary(s) {}

    // 虚析构函数(保证派生类正确析构)
    virtual ~Person() {}

    // 封装:通过公共接口访问私有成员
    string getName() const { return name; }
    int getAge() const { return age; }
    string getId() const { return id; }

    // 设置年龄
    void setAge(int newAge) {
        if (newAge > 0 && newAge < 150) {
            age = newAge;
        } else {
            cout << "年龄无效!" << endl;
        }
    }

    // 多态:虚函数,派生类可覆盖
    // ### 虚函数 具有传递性,只要是该类的派生类,那么就默认是 虚函数 ###
    // ### 基类带const 那么派生类 必须也得带const ###
    virtual void work() const {
        cout << name << " 正在工作..." << endl;
    }

    // 多态:显示基本信息
    virtual void displayInfo() const {
        cout << "姓名:" << name << ",年龄:" << age 
                  << ",身份证号:" << id << endl;
    }

    // 计算年收入(多态基础)
    virtual double getAnnualIncome() const {
        return salary * 12;  // 默认月薪制
    }
};

// ========== 派生类:Student(学生)==========
class Student : public Person {
private:
    string schoolName;
    string studentId;
    double scholarship;     // 奖学金

public:
    Student(const string& n, int a, const string& id, 
            const string& school, const string& stuId, double sc = 0)
        : Person(n, a, id, 0), schoolName(school), studentId(stuId), scholarship(sc) {}

    // 重写工作行为
    void work() const override {
        cout << getName() << " 正在学习..." << endl;
    }

    // 重写显示信息
    void displayInfo() const override {
        Person::displayInfo();  // 调用基类方法
        cout << "学校:" << schoolName << ",学号:" << studentId 
                  << ",奖学金:" << scholarship << "元" << endl;
    }

    // 计算年收入(学生主要靠奖学金)
    double getAnnualIncome() const override {
        return scholarship;  // 奖学金按年发放
    }
};

// ========== 派生类:Teacher(教师)==========
class Teacher : public Person {
private:
    string department;
    int teachingYears;      // 教龄
    double bonus;           // 年终奖

public:
    Teacher(const string& n, int a, const string& id,
            const string& dept, int years, double b = 0)
        : Person(n, a, id, 5000), department(dept), teachingYears(years), bonus(b) {}

    // 重写工作行为
    void work() const override {
        cout << getName() << " 正在教书育人..." << endl;
    }

    // 重写显示信息
    void displayInfo() const override {
        Person::displayInfo();
        cout << "部门:" << department << ",教龄:" << teachingYears << "年" << endl;
    }

    // 计算年收入(月薪×12 + 年终奖)
    double getAnnualIncome() const override {
        return salary * 12 + bonus;
    }
};

// ========== 派生类:Manager(经理)==========
class Manager : public Person {
private:
    string department;
    double yearEndBonus;    // 年终分红

public:
    Manager(const string& n, int a, const string& id,
            const string& dept, double bonus)
        : Person(n, a, id, 15000), department(dept), yearEndBonus(bonus) {}

    void work() const override {
        cout << getName() << " 正在管理团队..." << endl;
    }

    void displayInfo() const override {
        Person::displayInfo();
        cout << "管理部门:" << department << endl;
    }

    double getAnnualIncome() const override {
        return salary * 12 + yearEndBonus;
    }
};

// ========== 多态演示函数 ==========
void showPersonWork(const Person& p) {
    p.work();  // 运行时多态
}

void showIncome(const Person& p) {
    cout << p.getName() << " 的年收入:" << p.getAnnualIncome() << "元" << endl;
}

// ========== 主函数 ==========
int main() {
    // 创建各种人员对象
    Student stu("张三", 20, "110101200001011234", 
                "清华大学", "20240001", 8000);

    Teacher tea("李四", 35, "110101198912121234", 
                "计算机学院", 8, 30000);

    Manager mgr("王五", 42, "110101198205051234", 
                "技术研发部", 200000);

    // 多态:通过基类指针/引用访问
    // 多态的本质是"运行时才知道调用哪个函数"
    // 只有通过指针/引用访问对象时,编译器才会生成动态绑定的代码
    // 值传递会导致对象切片,永远无法实现多态
    // 容器(如 vector)如果要存储多态对象,必须存储指针(或智能指针)
    vector<Person*> persons;
    persons.push_back(&stu);
    persons.push_back(&tea);
    persons.push_back(&mgr);

    cout << "========== 人员信息 ==========" << endl;
    for (const auto& p : persons) {
        p->displayInfo();
        cout << "------------------------" << endl;
    }

    cout << "\n========== 工作行为(多态) ==========" << endl;
    for (const auto& p : persons) {
        showPersonWork(*p);
    }

    cout << "\n========== 收入统计(多态) ==========" << endl;
    for (const auto& p : persons) {
        showIncome(*p);
    }

    // 演示封装:不能直接访问私有成员
    // cout << stu.name;      // 错误:name 是 private
    // cout << stu.salary;    // 错误:salary 是 protected,外部不可访问

    cout << "\n========== 封装演示 ==========" << endl;
    cout << "学生姓名(通过接口):" << stu.getName() << endl;

    // 通过公共接口修改年龄(带验证)
    stu.setAge(21);
    cout << "修改后年龄:" << stu.getAge() << endl;

    stu.setAge(200);  // 无效年龄
    cout << "尝试设置无效年龄后:" << stu.getAge() << endl;

    return 0;
}

更多 构造函数(constructor)内容,参见「参考资料」第六条。

参考资料

  1. cppreference class
  2. cppreference access
  3. cppreference default_constructor
  4. cppreference operator
  5. cplusplus Data structures
  6. cplusplus Special members
  7. C++11 FAQ
  8. cppreference Friendship and inheritance
  9. cppreference value initialization