类由两部分组成:成员变量(数据,通常藏在 private 里)和成员函数(方法,通常开在 public 里)。前者是"这个对象有什么",后者是"这个对象能干什么"。衡量一个类写得好不好,就一条:外界能不能在不碰数据的情况下,把事情办完。
10.1 讲了"为什么要造类型",本节把类的内部构造拆开看清楚:哪些东西算成员、它们各扮演什么角色。这一节会反复用到 9.3.2 的 UML 类图三格框——类图里画的"属性 / 方法",就是这里的成员变量和成员函数,一一对应。另外,本节开始正式使用 private / public 这两个访问修饰符。
一、类的两个组成部分
| 部分 | 另一个名字 | 回答什么问题 | 通常放在 | UML 类图里画在 |
|---|---|---|---|---|
| 成员变量 | 属性 / 数据成员 / field | 这个对象有什么 | private |
第二格(属性格) |
| 成员函数 | 方法 / 成员方法 / method | 这个对象能干什么 | public |
第三格(方法格) |
用一句话记住它们的关系:
成员变量是"家底",成员函数是"窗口"。
所有对家底的读写,都应该从窗口进。窗口开得越少越好,但要开得够用。
二、访问修饰符:三档权限
类里面用三个标签来划分地盘:
| 标签 | 谁能访问 | UML 符号 | 用在哪 |
|---|---|---|---|
private(私有) |
只有自己(本类的成员函数) | - |
成员变量几乎总是放这里 |
public(公有) |
谁都能 | + |
成员函数(对外接口)放这里 |
protected(保护) |
自己 + 子类 | # |
留给继承用(本章几乎用不到,见到不慌) |
三个必须知道的细节:
- 标签可以写多次。
private:写一片成员、public:再写一片、回头又写private:——都合法。 - 标签管到"下一个标签"为止,不是管一行。
- 不写标签时看类关键字:
class默认private、struct默认public(10.1 讲过)。
class Demo {
int a; // 没写标签 → class 默认 private
private:
int b; // private
public:
int c; // public
protected:
int d; // protected
int e; // 延续 protected
};
三、成员函数里可以直接用成员变量(不用带对象名)
这是类和普通函数最直观的差别:
class Circle {
private:
double r; // 成员变量
public:
void setR(double v) {
if (v > 0) r = v; // 直接写 r,不用写 "哪个圆的 r"
}
double area() {
return 3.14159 * r * r; // 也是直接写 r
}
};
为什么可以不用带对象名? 因为成员函数一定是"某个对象"在调用它——调用时那个对象自动成了"当前对象"。10.4.4 会告诉你这个"当前对象"是怎么被记住的(this 指针),现在只需要知道:在成员函数里,成员变量就像自家抽屉,伸手就拿。
用起来:
Circle c1, c2; // 两个圆,各有一份自己的 r
c1.setR(2);
c2.setR(3);
cout << c1.area() << endl; // 12.56636 ← c1 的 r
cout << c2.area() << endl; // 28.27431 ← c2 的 r
每个对象都有一份自己独立的成员变量——这一点和结构体完全一致(8.1.2)。类只描述"有哪些成员",每个对象各自存一份值。
四、一个好类的三条标准
不是"能跑就行"。对照下面三条自查:
| 标准 | 好 | 坏 |
|---|---|---|
| 数据私有 | 成员变量都在 private |
成员变量裸露在 public |
| 接口有意义 | withdraw(100)、deposit(50) |
setBalance(x)、getBalance() 一把梭 |
| 职责单一 | Student 只关心学生的数据与行为 |
Student 又会打印、又会存文件、还会发网络请求 |
第三条尤其重要:一个类如果"什么都会",改任何一处都可能碰它——9.2.2 说的"改动被锁在对象内部"就废了。职责越单一,改动的影响面越小。
五、动手 Demo:写一个"会自己守规矩"的圆
#include <iostream>
using namespace std;
class Circle {
private:
double r; // 只有一个成员变量
public:
void setR(double v) { // 唯一的入口,带校验
if (v > 0) {
r = v;
} else {
cout << "半径必须大于 0,忽略 " << v << endl;
}
}
double area() { return 3.14159 * r * r; }
double perimeter() { return 2 * 3.14159 * r; }
void describe() {
cout << "半径 = " << r
<< ",面积 = " << area()
<< ",周长 = " << perimeter() << endl;
}
};
int main() {
Circle c;
c.setR(5); // ✅ 正常
c.describe();
c.setR(-3); // ❌ 被拦下来
c.describe(); // 半径还是 5,数据没被弄坏
// c.r = 100; // ← 去掉注释:编译错误(private)
return 0;
}
看到什么算成功:
半径 = 5,面积 = 78.5398,周长 = 31.4159
半径必须大于 0,忽略 -3
半径 = 5,面积 = 78.5398,周长 = 31.4159
读结果:非法值被拦,第二行打印的还是 5。这就是"类自己守规矩"——不需要调用方处处小心,规则写一次,谁都绕不过去。
30 秒实验:把 double r; 从 private 挪到 public,再在 main 里写 c.r = -3; c.describe();。你会发现程序照样跑,还高高兴兴地算出了负半径的"面积"——没人报错,但结果是错的。这就是 9.2.1 那个"不报错的错"。把 private 亲手去掉一次,你对封装的理解会牢一大截。
六、常见误区
- "成员变量必须全
private"——绝大多数情况是,但纯数据类(DTO/配置项)全公开也没问题。判据不是教条,而是:这些数据需不需要被保护、有没有规则要守。需要守规矩(分数范围、余额不能为负)就必须私有;纯粹是个数据袋子,公开反而省事。 - "
private成员子类也看不到吧?"——看不到。子类只能访问protected和public的(private连子类都挡)。这就是为什么有的成员要写成protected。 - "读取数据的函数必须有"——不一定。能不给就不给——每多一个
getX(),就多一扇门。先问:调用方真的需要这个值,还是只需要一个结果? 要结果就给他个算好的方法(area()),而不是把原料(r)递出去。 - "一个类里方法越多越强"——恰恰相反。方法越多,说明它管的闲事越多,职责越不单一。写类时的好习惯是:先想它"不该管什么",划清边界再动手。
- "类必须先定义完才能用"——同一文件里是的;跨文件靠"声明"(10.1.3 讲头文件的作用)。现在就记住这个词:
::是"这个函数属于谁"的标记。
自测三问(点开对答案)
1. 类的两个组成部分分别是什么?各回答什么问题?——**成员变量**(这个对象有什么,通常私有)和**成员函数**(这个对象能干什么,通常公有)。 2. 三个访问修饰符各管到哪儿?——`private` 只有本类能访问;`protected` 本类 + 子类;`public` 谁都能。标签**管到下一个标签为止**,可以写多次;不写时看类关键字(`class` → `private`,`struct` → `public`)。 3. 判断一个类"封装得好不好",看哪一条?——**看外界能不能在不直接碰数据的情况下把事情办完**。如果外面必须拿到原始数据才能干活,说明**接口没设计好**(门开错位置了)。- ☐ 10.1.2 类的声明与定义(下一节)
相关笔记
- C++类 —— 上一节:为什么需要类
- 类的声明与定义 —— 下一节:类的完整写法
- 结构体变量 —— "每个对象各有自己一份成员"的由来
- 统一建模语言的结构 —— 类图三格框与
+-#符号 - 🔷 C++基础 · 索引 —— 全课路线图