1、资料清单

资料 内容 入口
教材 《C++语言程序设计(第4版)》Chap4,原书页码 114-161 抽取 Markdown 下载
课件 Chap4 课件,位于本地郑莉 C++ 资料目录 本地资料夹
例题源代码 C++V5 源代码 下载
VS2019 工程 C++V5 VS2019 solution 下载

本章开始真正进入 C++ 的面向对象部分。主线是对象抽象 → 类的定义 → 访问控制 → 构造/析构 → 复制构造 → 类的组合

2、颜色标注

颜色 含义 使用位置
蓝色 核心术语、定义 类、对象、封装、构造函数
绿色 推荐写法、主线 私有数据、公有接口、初始化列表
黄色 易错点、边界条件 复制构造、赋值、构造顺序
红色 不建议做法 直接暴露数据、用析构函数做普通输出

3、学习目标

目标 要会到什么程度
类和对象 会从现实问题中抽象属性和行为
访问控制 分清 publicprivateprotected
封装 会把数据隐藏起来,只暴露必要接口
构造函数 会用构造函数和初始化列表创建有效对象
析构函数 理解对象销毁时的清理动作
复制构造函数 知道什么时候调用,能和赋值运算区分
类的组合 会在一个类中嵌入另一个类对象
结构体和联合体 知道它们和类的关系,以及现代写法选择

4、章节目录

小节 内容 本节关键词
4.1 面向对象程序设计基本特点 抽象、封装、继承、多态
4.2 类和对象 类定义、对象、成员变量、成员函数
4.3 构造函数和析构函数 默认构造、带参构造、复制构造、析构
4.4 类的组合 内嵌对象、初始化顺序
4.5 UML 类图 类名、属性、操作、访问权限
4.6 结构体和联合体 structunion、数据组织
4.7 个人银行账户管理程序 类设计的小型综合应用

5、从结构化程序到面向对象程序

Chap3 的函数让代码可以复用,Chap4 的类让数据和操作可以绑定在一起。

1
2
3
4
5
6
struct PointData {
double x;
double y;
};

double distanceFromOrigin(PointData p);

这仍然是“数据在一边,函数在一边”。类的做法是把对象的状态和行为放到同一个抽象中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Point {
public:
Point(double xValue, double yValue) : x(xValue), y(yValue) {}

double getX() const {
return x;
}

double getY() const {
return y;
}

private:
double x;
double y;
};
对比 结构化程序 面向对象程序
思考方式 函数如何处理数据 对象拥有什么状态、能做什么事
数据位置 常在函数外部传来传去 成为对象的内部状态
访问方式 外部直接操作数据较常见 通过对象接口访问
维护重点 函数流程 类的边界与职责

类不是为了把代码写复杂,而是为了让数据有边界,让状态修改有入口。也就是:对象管理自己的状态,外部只通过接口和它交互

6、面向对象的四个基本特点

6.1 抽象

抽象是从具体对象中提取共同特征。

例如描述一只狗,不需要把所有现实细节都放入程序。根据任务需要,可能只保留:

属性 行为
年龄 设置年龄
体重 获取体重
名字 输出信息

程序设计中的抽象不是“越多越好”,而是“刚好能解决问题”。

6.2 封装

封装把数据和操作数据的函数绑定在一起,并通过访问控制隐藏实现细节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Dog {
public:
void setAge(int value) {
if (value >= 0) {
age = value;
}
}

int getAge() const {
return age;
}

private:
int age = 0;
};

这里 age 是私有数据,外部不能随便写入负数,只能通过 setAge() 进入校验逻辑。

6.3 继承

继承用于表达“一类对象是另一类对象的特殊形式”。比如 Student 可以继承 Person。这一章先知道概念即可,后面 Chap7 会详细整理。

6.4 多态

多态表示同一消息作用在不同对象上,可以表现出不同的行为。比如不同图形都有 area(),圆和矩形的面积计算方式不同。后面 Chap8 会详细整理。

7、类和对象

7.1 类的基本写法

1
2
3
4
5
6
7
class 类名 {
public:
公有成员;

private:
私有成员;
};

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Rectangle {
public:
Rectangle(double lengthValue, double widthValue)
: length(lengthValue), width(widthValue) {}

double area() const {
return length * width;
}

private:
double length;
double width;
};

创建对象:

1
2
Rectangle rect(5.0, 4.0);
std::cout << rect.area() << std::endl;
概念 说明
对同一类对象的抽象描述,是类型
对象 类的具体实例,占有实际存储空间
数据成员 对象保存的状态
成员函数 对象能执行的操作

类定义最后的右花括号后面必须有分号:};。这是初学阶段非常常见的小错误。

7.2 publicprivateprotected

访问控制 类外是否可访问 派生类是否可访问 常见用途
public 可以 可以 类的外部接口
private 不可以 不可以直接访问 对象内部数据和实现细节
protected 不可以 可以 留给继承体系使用

本章重点先记住:数据成员优先设为 private,成员函数作为 public 接口

7.3 成员函数的类内定义与类外定义

类内定义:

1
2
3
4
5
6
7
8
9
class Counter {
public:
int getValue() const {
return value;
}

private:
int value = 0;
};

类外定义:

1
2
3
4
5
6
7
8
9
10
11
class Counter {
public:
int getValue() const;

private:
int value = 0;
};

int Counter::getValue() const {
return value;
}

Counter::getValue 中的 :: 是作用域限定符,表示这个函数属于 Counter 类。

8、构造函数和析构函数

8.1 构造函数

构造函数在对象创建时自动调用,用来把对象初始化到可用状态。

1
2
3
4
5
6
7
8
9
class Student {
public:
Student(int numberValue, std::string nameValue)
: number(numberValue), name(nameValue) {}

private:
int number;
std::string name;
};

构造函数特点:

特点 说明
名字与类名相同 Student(...)
没有返回类型 不能写 void
创建对象时自动调用 Student s(1, "Rui");
可以重载 不同参数列表对应不同初始化方式

8.2 初始化列表

推荐使用初始化列表初始化数据成员。

1
2
3
4
5
6
7
8
9
class Date {
public:
Date(int y, int m, int d) : year(y), month(m), day(d) {}

private:
int year;
int month;
int day;
};

和在构造函数体内赋值相比,初始化列表更直接,也能初始化 const 成员、引用成员和成员对象。

成员实际初始化顺序不是初始化列表里写的顺序,而是数据成员在类中声明的顺序

8.3 默认构造函数

默认构造函数是不需要实参就能调用的构造函数。

1
2
3
4
5
6
7
8
class Dog {
public:
Dog() : age(0), weight(5) {}

private:
int age;
int weight;
};

也可以通过默认参数实现:

1
2
3
4
5
6
7
8
9
class Dog {
public:
Dog(int initialAge = 0, int initialWeight = 5)
: age(initialAge), weight(initialWeight) {}

private:
int age;
int weight;
};

8.4 复制构造函数

复制构造函数用一个已有对象初始化一个新对象。

1
2
3
4
5
6
7
8
9
10
class Point {
public:
Point(int xValue, int yValue) : x(xValue), y(yValue) {}

Point(const Point& other) : x(other.x), y(other.y) {}

private:
int x;
int y;
};

常见触发场景:

场景 例子
用对象初始化同类新对象 Point b = a;
对象按值传参 void f(Point p);
对象按值返回 return p;

复制构造函数的参数通常写成 const 类名&。如果写成按值传参,会为了传参再次调用复制构造函数,形成循环调用。

8.5 析构函数

析构函数在对象生命周期结束时自动调用,负责释放对象持有的资源。

1
2
3
4
5
6
class FileWrapper {
public:
~FileWrapper() {
// 关闭文件、释放资源等
}
};

析构函数特点:

特点 说明
名字是 ~类名 ~Dog()
没有返回类型 也不能有参数
不能重载 一个类只能有一个析构函数
自动调用 对象离开作用域或被释放时调用

本章程序题里有些析构函数只是空函数,保留即可;如果没有资源需要释放,现代 C++ 中可以不显式写析构函数。

9、复制构造和赋值运算的区别

这两个概念非常容易混。

1
2
3
4
5
Rectangle a(5, 4);
Rectangle b = a; // 复制构造:创建新对象 b

Rectangle c(1, 1);
c = a; // 赋值:c 已经存在
对比 复制构造函数 赋值运算符
发生时机 新对象创建时 已有对象被重新赋值
是否创建对象
典型形式 T b = a; b = a;
函数形式 T(const T& other) T& operator=(const T& other)

本章只要求先能区分两者,运算符重载后面会继续整理。

10、类的组合

类的组合指一个类中包含另一个类的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Date {
public:
Date(int y, int m, int d) : year(y), month(m), day(d) {}

private:
int year;
int month;
int day;
};

class People {
public:
People(int numberValue, char sexValue, Date birthdayValue, std::string idValue)
: number(numberValue), sex(sexValue), birthday(birthdayValue), id(idValue) {}

private:
int number;
char sex;
Date birthday;
std::string id;
};

这里 People 由一个 Date 对象组合而成。

构造顺序:

顺序 内容
1 先构造成员对象
2 再执行当前类构造函数体
3 析构时顺序相反

11、UML 类图速记

UML 类图常用三层结构:

1
2
3
4
5
6
7
8
9
+----------------------+
| 类名 |
+----------------------+
| - 私有属性 |
| + 公有属性 |
+----------------------+
| + 公有操作() |
| - 私有操作() |
+----------------------+
符号 含义
+ public
- private
# protected

例如 Dog 类可以表示为:

1
2
3
4
5
6
7
8
9
10
11
+----------------------+
| Dog |
+----------------------+
| - age: int |
| - weight: int |
+----------------------+
| + getAge(): int |
| + setAge(int): void |
| + getWeight(): int |
| + setWeight(int): void |
+----------------------+

12、结构体和联合体

12.1 struct

在 C++ 中,structclass 都可以有成员变量、成员函数和构造函数。主要默认访问权限不同:

类型 默认访问权限
class private
struct public

一般可以这样取舍:

场景 建议
只是简单数据聚合 struct
需要封装和不变式 class

12.2 union

联合体的多个成员共享同一段内存,同一时刻通常只保存其中一种值。

1
2
3
4
5
union Data {
char c;
int i;
float f;
};

传统 union 常需要额外记录当前保存的是哪一种类型。现代 C++ 中,更推荐用 std::variant 表达安全的多类型数据。

1
2
3
#include <variant>

std::variant<char, int, float> data;

13、本章代码复盘

下面这个小程序串起类、构造函数、访问控制、成员函数、复制构造和析构函数。

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
#include <iostream>
#include <string>

class Course {
public:
Course(std::string nameValue, int hourValue)
: name(nameValue), hours(hourValue) {
std::cout << "Construct: " << name << '\n';
}

Course(const Course& other)
: name(other.name), hours(other.hours) {
std::cout << "Copy construct: " << name << '\n';
}

~Course() {
std::cout << "Destruct: " << name << '\n';
}

void print() const {
std::cout << name << ": " << hours << " hours\n";
}

private:
std::string name;
int hours;
};

void showCourse(Course course) {
course.print();
}

int main() {
Course cpp("C++", 64);
cpp.print();

Course copy = cpp;
showCourse(copy);

return 0;
}

观察点:

观察点 说明
Course cpp(...) 调用普通构造函数
Course copy = cpp 调用复制构造函数
showCourse(copy) 按值传参,再次调用复制构造函数
函数结束、对象离开作用域 调用析构函数

如果类里自己管理动态内存、文件句柄、网络连接等资源,就必须认真处理复制、赋值和析构。这个问题后续会和“深拷贝/浅拷贝”“RAII”一起复盘。

14、课后习题答案

以下答案按现代 C++ 重新整理,程序题均使用 #include <iostream>int main()

4-1 解释 publicprivate 的作用,公有成员与私有成员有什么区别

public 用来声明公有成员,公有成员构成类的外部接口,类外可以通过对象访问。

private 用来声明私有成员,私有成员只允许本类的成员函数或友元访问,类外不能直接访问。

对比 public private
访问位置 类内和类外都可访问 通常只能类内访问
作用 提供外部接口 隐藏数据和实现细节
常见内容 构造函数、getter、setter、业务操作 数据成员、内部辅助函数

4-2 protected 关键字有何作用

protected 用来声明保护成员。保护成员在类外不可直接访问,但在派生类成员函数中可以访问。

本章先记住它介于 publicprivate 之间,继承部分到 Chap7 再展开。

4-3 构造函数和析构函数有什么作用

构造函数在对象创建时自动调用,用来初始化对象,让对象一出生就是有效状态。

析构函数在对象销毁前自动调用,用来完成清理工作,例如释放资源、关闭文件等。

函数 调用时机 主要作用
构造函数 对象创建时 初始化对象
析构函数 对象生命周期结束时 清理对象持有的资源

4-4 数据成员可以为公有的吗?成员函数可以为私有的吗

都可以,语法上合法。

但一般设计中,数据成员建议设为 private,通过公有成员函数访问和修改。成员函数可以是 private,常用于类内部的辅助逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Account {
public:
void deposit(double amount) {
if (isValidAmount(amount)) {
balance += amount;
}
}

private:
bool isValidAmount(double amount) const {
return amount > 0;
}

double balance = 0.0;
};

4-5 两个对象的数据成员值可以不同吗

可以。类只是类型模板,每个对象都有自己的一份普通数据成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A {
public:
explicit A(int value) : a(value) {}

int getA() const {
return a;
}

private:
int a;
};

int main() {
A a1(10);
A a2(20);
// a1.getA() 是 10,a2.getA() 是 20
}

4-6 什么叫复制构造函数?何时被调用

复制构造函数是一种特殊构造函数,用已有对象初始化新的同类对象。

1
ClassName(const ClassName& other);

常见调用时机:

场景 例子
用一个对象初始化另一个对象 A a2 = a1;
函数形参是类对象且按值传参 void f(A a);
函数返回值是类对象 return a;

4-7 复制构造函数与赋值运算符有什么不同

复制构造函数用于创建新对象,赋值运算符用于已经存在的对象。

1
2
3
4
5
A a1;
A a2 = a1; // 复制构造

A a3;
a3 = a1; // 赋值

核心区别是:复制构造发生在对象创建时,赋值发生在对象已经存在之后

4-8 定义一个 Dog 类,包含 ageweight 等属性,以及操作方法

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
#include <iostream>

class Dog {
public:
Dog(int initialAge = 0, int initialWeight = 5)
: age(initialAge), weight(initialWeight) {}

int getAge() const {
return age;
}

void setAge(int value) {
if (value >= 0) {
age = value;
}
}

int getWeight() const {
return weight;
}

void setWeight(int value) {
if (value >= 0) {
weight = value;
}
}

private:
int age;
int weight;
};

int main() {
Dog jack(2, 10);

std::cout << "Jack is a dog who is "
<< jack.getAge() << " years old and "
<< jack.getWeight() << " pounds weight.\n";

jack.setAge(7);
jack.setWeight(20);

std::cout << "Now Jack is "
<< jack.getAge() << " years old and "
<< jack.getWeight() << " pounds weight.\n";

return 0;
}

输出示例:

1
2
Jack is a dog who is 2 years old and 10 pounds weight.
Now Jack is 7 years old and 20 pounds weight.

原始抽取答案里 SetWeight 写成了修改 itsAge,这里已经修正为修改 weight

4-9 设计并测试 Rectangle 类,用左下角与右上角坐标计算面积

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
#include <cmath>
#include <iostream>

class Rectangle {
public:
Rectangle(double leftValue, double bottomValue, double rightValue, double topValue)
: left(leftValue), bottom(bottomValue), right(rightValue), top(topValue) {}

double getWidth() const {
return std::abs(right - left);
}

double getHeight() const {
return std::abs(top - bottom);
}

double getArea() const {
return getWidth() * getHeight();
}

private:
double left;
double bottom;
double right;
double top;
};

int main() {
Rectangle rectangle(20, 50, 80, 100);

std::cout << "Width: " << rectangle.getWidth() << '\n';
std::cout << "Height: " << rectangle.getHeight() << '\n';
std::cout << "Area: " << rectangle.getArea() << '\n';

return 0;
}

输出示例:

1
2
3
Width: 60
Height: 50
Area: 3000

4-10 设计一个用于人事管理的 People

教材答案里标注此题作为实验选做题,没有直接给出答案。这里补一个完整版本,包含日期类组合、构造函数、析构函数、复制构造函数、内联成员函数和带默认值的成员函数。

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
#include <iostream>
#include <string>

class Date {
public:
Date(int yearValue = 2000, int monthValue = 1, int dayValue = 1)
: year(yearValue), month(monthValue), day(dayValue) {}

void print() const {
std::cout << year << '-' << month << '-' << day;
}

private:
int year;
int month;
int day;
};

class People {
public:
People(int numberValue = 0,
char sexValue = 'M',
Date birthdayValue = Date(),
std::string idValue = "")
: number(numberValue),
sex(sexValue),
birthday(birthdayValue),
id(idValue) {}

People(const People& other)
: number(other.number),
sex(other.sex),
birthday(other.birthday),
id(other.id) {}

~People() = default;

int getNumber() const {
return number;
}

void setNumber(int value) {
number = value;
}

void input() {
int year;
int month;
int day;

std::cout << "Number: ";
std::cin >> number;
std::cout << "Sex(M/F): ";
std::cin >> sex;
std::cout << "Birthday(year month day): ";
std::cin >> year >> month >> day;
birthday = Date(year, month, day);
std::cout << "ID: ";
std::cin >> id;
}

void display() const {
std::cout << "Number: " << number << '\n';
std::cout << "Sex: " << sex << '\n';
std::cout << "Birthday: ";
birthday.print();
std::cout << '\n';
std::cout << "ID: " << id << '\n';
}

private:
int number;
char sex;
Date birthday;
std::string id;
};

int main() {
People p1(1001, 'M', Date(2000, 9, 1), "123456200009010000");
p1.display();

People p2 = p1;
std::cout << "\nCopied people:\n";
p2.display();

return 0;
}

说明:

要求 对应代码
构造函数 People(...)
析构函数 ~People() = default;
复制构造函数 People(const People& other)
内联成员函数 类内定义的 getNumber()setNumber()
带默认形参 People(int numberValue = 0, ...)
聚集/组合 People 中包含 Date birthday

4-11 定义矩形类,有长、宽两个属性,计算面积

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
#include <iostream>

class Rectangle {
public:
Rectangle(double lengthValue, double widthValue)
: length(lengthValue), width(widthValue) {}

double getArea() const {
return length * width;
}

double getLength() const {
return length;
}

double getWidth() const {
return width;
}

private:
double length;
double width;
};

int main() {
double length;
double width;

std::cout << "请输入矩形的长度:";
std::cin >> length;
std::cout << "请输入矩形的宽度:";
std::cin >> width;

Rectangle rectangle(length, width);

std::cout << "长为 " << rectangle.getLength()
<< ",宽为 " << rectangle.getWidth()
<< " 的矩形面积为:"
<< rectangle.getArea() << '\n';

return 0;
}

示例:

1
2
3
请输入矩形的长度:5
请输入矩形的宽度:4
长为 5,宽为 4 的矩形面积为:20

4-12 定义一个 DataType 类,能处理字符型、整型、浮点型三种数据

传统写法可以用 enum 标记当前类型,再用 union 保存数据。

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
#include <iostream>

class DataType {
public:
explicit DataType(char value) : type(Character) {
data.c = value;
}

explicit DataType(int value) : type(Integer) {
data.i = value;
}

explicit DataType(float value) : type(FloatingPoint) {
data.f = value;
}

void print() const {
switch (type) {
case Character:
std::cout << "字符型: " << data.c << '\n';
break;
case Integer:
std::cout << "整型: " << data.i << '\n';
break;
case FloatingPoint:
std::cout << "浮点型: " << data.f << '\n';
break;
}
}

private:
enum ValueType {
Character,
Integer,
FloatingPoint
};

union Value {
char c;
int i;
float f;
};

ValueType type;
Value data;
};

int main() {
DataType a('c');
DataType b(12);
DataType c(1.44f);

a.print();
b.print();
c.print();

return 0;
}

输出:

1
2
3
字符型: c
整型: 12
浮点型: 1.44

现代 C++ 也可以用 std::variant 简化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <variant>

class DataType {
public:
explicit DataType(char value) : data(value) {}
explicit DataType(int value) : data(value) {}
explicit DataType(float value) : data(value) {}

void print() const {
std::visit([](const auto& value) {
std::cout << value << '\n';
}, data);
}

private:
std::variant<char, int, float> data;
};

教材阶段先掌握 enum + union 的写法即可。

4-13 定义 Circle 类,计算圆面积

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
#include <iostream>

class Circle {
public:
explicit Circle(double radiusValue) : radius(radiusValue) {}

double getArea() const {
constexpr double pi = 3.14159265358979323846;
return pi * radius * radius;
}

private:
double radius;
};

int main() {
double radius;

std::cout << "请输入圆的半径:";
std::cin >> radius;

Circle circle(radius);

std::cout << "半径为 " << radius
<< " 的圆面积为:"
<< circle.getArea() << '\n';

return 0;
}

示例:

1
2
请输入圆的半径:5
半径为 5 的圆面积为:78.5398

4-14 定义 Tree 类,grow(int years) 增加树龄,age() 显示树龄

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
#include <iostream>

class Tree {
public:
explicit Tree(int initialAge = 0) : ages(initialAge) {}

void grow(int years) {
if (years > 0) {
ages += years;
}
}

void age() const {
std::cout << "这棵树的年龄为 " << ages << '\n';
}

private:
int ages;
};

int main() {
Tree tree(12);

tree.age();
tree.grow(4);
tree.age();

return 0;
}

输出:

1
2
这棵树的年龄为 12
这棵树的年龄为 16

原答案把第二次输出放在析构函数里,程序结束时虽然能看到结果,但不适合作为正常业务输出。这里改成主动调用 age(),逻辑更清楚。

15、本章复盘

复盘问题 自查答案
类和对象有什么区别 类是类型,对象是实例
为什么数据成员通常设为私有 避免外部绕过校验直接修改状态
构造函数什么时候调用 对象创建时自动调用
析构函数什么时候调用 对象生命周期结束时自动调用
复制构造和赋值有什么区别 一个创建新对象,一个修改已有对象
类的组合是什么 一个类中包含另一个类的对象
structclass 默认访问权限有什么不同 struct 默认 public,class 默认 private

下一章 Chap5 会继续整理作用域、可见性、生存期、静态成员、友元和共享数据保护。