类是图纸,对象才是实物(9.2.2 的老话)。声明对象最常用的一句是 类名 对象名;——写这一行,对象就真的存在了(占内存、有了初始状态、可以调方法)。对象也能像普通变量一样组成数组(一排对象)、用指针指着(p->)、用引用起外号(&r)。实例01 演示的就是"对象的引用":给一个已经存在的对象再起一个名字。
10.1.1~10.1.3 一直在造"图纸"(类),本节第一次把图纸变成实物。这里你会大量复用前面的老手艺:对象数组用 8.4 的结构体数组(几乎一模一样)、对象指针用 7.6.1 的 ->、对象引用用 7.8.1 的三条铁律(必须初始化、绑定后不能改绑、不能为空)。区别只在于:以前这些操作的是结构体或普通变量,现在操作的是对象——语法完全一样。
一、最常用的一句:类名 对象名;
class Point {
private:
int x, y;
public:
void set(int a, int b) { x = a; y = b; }
void show() { cout << "(" << x << ", " << y << ")" << endl; }
};
int main() {
Point p; // ← 声明对象:图纸 → 实物
p.set(3, 4); // 用点号调用成员函数
p.show(); // (3, 4)
return 0;
}
这一行做了什么:
| 动作 | 说明 |
|---|---|
| 分配内存 | 按类的大小(成员变量的总和 + 对齐,8.1.5)在栈上分配一块空间 |
| 成员变量初始值 | 如果没写构造函数,是不确定的(和未初始化的局部变量一样,2.3 的"垃圾值")——10.2 的构造函数就是来解决这个问题的 |
| 可以开始用 | 用 . 访问 public 成员 |
如果类里没有 public 成员函数,那么声明出来的对象几乎没法用——因为私有成员外面碰不到。这也是"类必须开至少一扇门"的原因。
二、对象的其他"姿势"
对象和普通变量一样,可以有数组、指针、引用,还能做函数参数(8.3 讲过的三种交法全适用)。
① 对象数组(对照 8.4 结构体数组)
Point pts[3]; // 一排 3 个 Point 对象
pts[0].set(1, 1);
pts[1].set(2, 2);
pts[2].set(3, 3);
for (int i = 0; i < 3; i++)
pts[i].show();
读法和结构体数组一样:数组名[i].成员——先选行,再选列(8.4.1)。
② 对象指针(对照 7.6.1)
Point p;
Point *ptr = &p; // 指针指向对象,要取地址
ptr->set(5, 6); // 指针用 ->
cout << (*ptr).x << endl; // 不推荐这么写
ptr->show(); // 推荐:箭头
口诀还是那句(8.3.2):变量点、指针箭头。
③ 对象引用(本节重点,对照 7.8.1)
Point p;
Point &r = p; // r 是 p 的另一个名字(外号)
r.set(7, 8); // 改 r 就是改 p
p.show(); // (7, 8) ← p 真的变了
三条铁律(7.8.1 原文):
| 铁律 | 含义 |
|---|---|
| 必须初始化 | Point &r; ❌ 编译不过——引用一出生就得绑定到谁身上 |
| 绑定后不能改绑 | r = other; 不是"改绑到 other",而是"把 other 的值赋给 r 所绑定的对象"(即 p = other) |
| 不能为空 | 引用必须有实体,没有"空引用"这种东西 |
三、实例01:对象的引用
场景:p1 已经存在,我们再给它起一个名字 alias,然后通过 alias 修改它——验证"改别名就是改本体"。
#include <iostream>
using namespace std;
class Point {
private:
int x, y;
public:
void set(int a, int b) { x = a; y = b; }
void show(const char *who) {
cout << who << " = (" << x << ", " << y << ")" << endl;
}
};
int main() {
Point p1;
p1.set(1, 2);
Point &alias = p1; // ★ 关键一句:alias 是 p1 的别名
cout << "&p1 = " << &p1 << endl;
cout << "&alias = " << &alias << endl; // 两个地址完全一样
p1.show("p1 原始");
alias.set(100, 200); // 通过别名修改
p1.show("p1 现在"); // p1 也变了
// 证明"改的不是绑定,是值"
Point p2;
p2.set(7, 8);
alias = p2; // ⚠️ 这不是改绑!是 "p1 = p2"
p1.show("p1 被赋值后"); // (7, 8) —— p1 的值变成了 p2 的
p2.show("p2 保持"); // (7, 8) —— p2 自己没变
cout << "&alias 还是 " << &alias << "(== &p1,没有改绑)" << endl;
return 0;
}
看到什么算成功(地址数字各机器不同,关键是两组地址相等):
&p1 = 0x61ff08
&alias = 0x61ff08
p1 原始 = (1, 2)
p1 现在 = (100, 200)
p1 被赋值后 = (7, 8)
p2 保持 = (7, 8)
&alias 还是 0x61ff08(== &p1,没有改绑)
读结果三句话:
&p1和&alias是同一个地址——它们是同一块内存的两个名字。alias.set(...)后p1变了——通过别名改,就是改本体。alias = p2;之后p1变成了 p2 的值,而alias的地址没变——这一句不是"改绑",是"赋值"。这是引用最容易误解的一处,7.8.1 专门强调过。
引用的实用场景:给长名字起短名字(比如
Point &p = group.members[0];之后直接写p.set(...))、函数参数免拷贝(7.8.2 的const T &)、返回值的别名。对象大的时候,用引用传参能省下一次整块复制。
四、实例01 的延伸:引用做函数参数(回收 7.8.2)
对象最好的用法之一:传引用进函数,函数里直接改原件,还不用复制整块。
void moveRight(Point &p, int dx) { // 引用参数
// p 就是外面那个对象本身
// 这里演示用 set 重新设置(假设你知道新坐标)
}
void reset(Point &p) { // 常见写法:把对象恢复成默认状态
p.set(0, 0);
}
int main() {
Point a;
a.set(3, 4);
reset(a); // 调用处和平常一样,不写 &
a.show("reset 后"); // reset 后 = (0, 0)
return 0;
}
三种交法(8.3 的表,这里是对象版):
| 写法 | 函数里能不能改原件 | 代价 |
|---|---|---|
void f(Point p) |
❌ 不能(复制了一份) | 复制整个对象 |
void f(Point *p) |
✅ 能(要写 p->) |
只传一个地址 |
void f(Point &p) |
✅ 能(写成普通对象的样子) | 只传一个地址 |
只读就用 const Point &p——不复制、也改不了,是传大对象的首选(8.3、7.8.2 的老结论)。
五、动手 Demo:三种姿势一次练完
#include <iostream>
using namespace std;
class Counter {
private:
int value;
public:
void init(int v) { value = v; }
void add(int n) { value += n; }
void show(const char *who) {
cout << who << " → " << value << endl;
}
};
void bump(Counter &c) { // 引用参数:能改原件
c.add(1);
}
int main() {
// ① 普通对象
Counter a;
a.init(10);
a.add(5);
a.show("a"); // a → 15
// ② 对象数组
Counter arr[2];
arr[0].init(1);
arr[1].init(100);
arr[0].add(1);
arr[0].show("arr[0]"); // arr[0] → 2
arr[1].show("arr[1]"); // arr[1] → 100
// ③ 对象指针
Counter *p = &arr[1];
p->add(50);
p->show("arr[1] via *p"); // arr[1] via *p → 150
// ④ 对象引用(并做函数参数)
Counter &r = arr[0];
bump(r);
bump(r);
arr[0].show("arr[0] after bump"); // arr[0] after bump → 4
return 0;
}
看到什么算成功:
a → 15
arr[0] → 2
arr[1] → 100
arr[1] via *p → 150
arr[0] after bump → 4
读结果:四种姿势操作的是同一个类,语法和操作结构体/普通变量一模一样——这就是"类只是一种自定义类型"的最好证明。对象数组里 arr[0] 和 arr[1] 各有一份自己的 value(2 和 150),互不干扰——每个对象独立存状态,这是对象的核心特征。
30 秒实验:把 void bump(Counter &c) 的参数改成传值 void bump(Counter c),重新运行。arr[0] after bump 会变回 2——因为函数改的是复制品,原件没动(6.3.1 的 swap 翻车,同一个道理)。这一改一看,传值和传引用的差别就再也忘不了。
六、常见误区
- "声明对象不用写
()"——本节的Point p;确实不用(因为还没学构造函数)。但 10.2 会看到Point p(3, 4);这种带括号的写法——那时括号里是构造函数的实参。看到括号不要慌:它是在"出生时"传参给构造函数。 - "对象引用可以用
&r = other改绑"——不行。r = other;是赋值(把other的值拷给r绑定的那个对象)。引用一旦绑定,一辈子只认那一个对象(7.8.1 的铁律)。 - "对象指针和对象引用差不多,随便选"——用途不同:引用更安全(必初始化、不能空、不用
->),能不用指针就用引用;指针能改指向(比如遍历数组时p++)、可以为空(表示"还没有对象")、能做数组。判据:"这个'谁'会不会变?" 会变用指针,不变用引用。 - "对象数组的元素必须一个个
set初始化"——不一定。如果类写了构造函数,可以在声明数组时统一初始化(10.2 之后就能写Point pts[3] = {Point(1,1), Point(2,2), Point(3,3)};)。现在只知道有这条路,到 10.2 再回来看。 - "
p.show()里的p就是函数里说的x"——不完全是。show()只是被p调用了;show()里的x到底是谁的x,取决于是哪个对象在调用它。这个"当前对象"的机制,10.4.4 用this指针一次说清。
自测三问(点开对答案)
1. `Point p;` 这一行到底做了什么?——**按类的大小在栈上分配内存**,让 `p` 成为按图纸造出的**实体**;成员变量此时**值不确定**(等构造函数来管,10.2),但已经可以用 `.` 调用公有方法了。 2. 对象引用和对象指针,各自适合什么场景?——**引用**:必初始化、不能为空、写法像普通变量,**适合"这个对象不会换"的场合**(尤其是函数传参);**指针**:可以为空、可以改指向、能参与数组和遍历、要用 `->`,**适合"指向会变 / 可能没有"的场合**。 3. `alias = p2;` 这一句在"改绑"吗?——**不是**。它把 `p2` 的**值**赋给了 `alias` 所绑定的对象(也就是 `p1`)。**引用绑定后不能改绑**,`&alias` 始终等于 `&p1`。- ☐ 10.2 构造函数(下一节)
相关笔记
- 类的实现 —— 上一节:把类拆成多文件
- 构造函数 —— 下一节:让对象"一出生就有好状态"
- 引用概述 —— 实例01 的基础:引用的三条铁律
- 结构体数组的声明与引用 —— 对象数组与它写法一致
- 使用结构体指针变量做函数参数 —— "变量点、指针箭头"的来源
- 🔷 C++基础 · 索引 —— 全课路线图