文章目录
- 引言
- [一、从 C 到 C++:函数入住结构体](#一、从 C 到 C++:函数入住结构体)
-
- [1.1 C 的世界观:函数和数据是平行线](#1.1 C 的世界观:函数和数据是平行线)
- [1.2 C++ 的世界观:函数住在结构体里](#1.2 C++ 的世界观:函数住在结构体里)
- [二、this 指针:编译器偷偷帮你的忙](#二、this 指针:编译器偷偷帮你的忙)
-
- [2.1 底层真相](#2.1 底层真相)
- [2.2 显式使用 this](#2.2 显式使用 this)
- [2.3 返回 *this:实现链式调用](#2.3 返回 *this:实现链式调用)
- [三、const 成员函数:this 变身 const](#三、const 成员函数:this 变身 const)
-
- [3.1 const 对象的困境](#3.1 const 对象的困境)
- [3.2 const 成员函数的作用](#3.2 const 成员函数的作用)
- [3.3 mutable:const 函数中的"例外"](#3.3 mutable:const 函数中的"例外")
- [四、static 成员函数:不拿 this 的函数](#四、static 成员函数:不拿 this 的函数)
- 五、编译器视角:成员函数到底长什么样
- 六、完整对照与总结
- 总结
本系列为《C++深度修炼:基础、STL源码与多线程实战》第4篇
前置条件:理解 C 语言函数指针和 struct,读过第2、3篇了解 class 与构造
引言
在 C 语言的世界观里,数据和函数是两个独立的东西。你定义结构体来存放数据,再定义函数来操作这些数据------它们之间唯一的联系,是函数名里的前缀和第一个参数的类型:
c
point_move(&p, x, y);
rect_area(&r);
bank_account_deposit(&acc, 500);
C++ 翻转了这个关系:函数成为数据的一部分。
cpp
p.move(x, y);
r.area();
acc.deposit(500);
这不是换了一种写法。这条 . 的背后,藏着一个 C++ 最核心的机制------this 指针。理解它在底层如何工作,你才能真正理解 const 成员函数、运算符重载、继承与多态。
一、从 C 到 C++:函数入住结构体
1.1 C 的世界观:函数和数据是平行线
cpp
// point_c.c
#include <stdio.h>
#include <math.h>
struct Point { double x, y; };
void point_init(struct Point *p, double x, double y) {
p->x = x;
p->y = y;
}
void point_move(struct Point *p, double dx, double dy) {
p->x += dx;
p->y += dy;
}
double point_distance(const struct Point *a, const struct Point *b) {
double dx = a->x - b->x;
double dy = a->y - b->y;
return sqrt(dx * dx + dy * dy);
}
int main() {
struct Point p1, p2;
point_init(&p1, 0, 0);
point_init(&p2, 3, 4);
point_move(&p1, 1, 1); // 调用者必须知道 p1 传给哪个函数
printf("distance = %.2f\n", point_distance(&p1, &p2));
}
规则很清楚:每个函数接收一个指向 struct Point 的指针作为第一个参数,函数名以 point_ 为前缀来表明归属。
1.2 C++ 的世界观:函数住在结构体里
cpp
// point_v1.cpp
#include <cstdio>
#include <cmath>
class Point {
public:
Point(double x, double y) : x_(x), y_(y) {}
void move(double dx, double dy) {
x_ += dx;
y_ += dy;
}
double distance(const Point &other) const {
double dx = x_ - other.x_;
double dy = y_ - other.y_;
return sqrt(dx * dx + dy * dy);
}
private:
double x_, y_;
};
int main() {
Point p1(0, 0), p2(3, 4);
p1.move(1, 1); // p1 是被操作的对象
printf("distance = %.2f\n", p1.distance(p2));
}
move 和 distance 这两个函数住进了 Point 里 。调用时不再写 point_move(&p1, 1, 1),而是写 p1.move(1, 1)------ "对象.操作"。
二、this 指针:编译器偷偷帮你的忙
2.1 底层真相
当你写 p1.move(1, 1) 时,编译器实际上把它翻译成类似 C 的调用:
cpp
// 你写的:
p1.move(1, 1);
// 编译器内心OS:
Point::move(&p1, 1, 1);
// ^^^^ 偷偷多传了一个指向 p1 的指针
这个隐藏的参数就是 this ------一个指向调用者对象的指针。在成员函数内部,你可以直接使用它。
2.2 显式使用 this
cpp
class Point {
public:
Point(double x, double y) {
this->x_ = x; // 等价于 x_ = x;
this->y_ = y; // 等价于 y_ = y;
}
void move(double dx, double dy) {
this->x_ += dx; // 等价于 x_ += dx;
this->y_ += dy; // 等价于 y_ += dy;
}
// 参数名和成员名冲突时,this 区分歧义
void set_x(double x_) { // 参数叫 x_
this->x_ = x_; // this->x_ 是成员,x_ 是参数
}
private:
double x_, y_;
};
💡 大多数时候
this->可以省略,因为编译器会自动在成员变量前加this->。但有两种情况必须显式使用:
- 参数名遮蔽了成员名(如上面的
set_x)- 需要返回对象自身时(如
return *this;)
2.3 返回 *this:实现链式调用
cpp
class Accumulator {
public:
Accumulator() : total_(0) {}
Accumulator &add(int v) {
total_ += v;
return *this; // 返回自身引用
}
Accumulator &multiply(int v) {
total_ *= v;
return *this;
}
int value() const { return total_; }
private:
int total_;
};
int main() {
Accumulator acc;
acc.add(5).multiply(3).add(2); // 链式调用
// ((5 + 0) * 3) + 2 = 17
printf("%d\n", acc.value()); // 17
}
return *this 返回了对象自身的引用,让下一个 . 可以继续链在同一个对象上。std::cout 的 << 操作符就是靠这个模式工作的。
三、const 成员函数:this 变身 const
3.1 const 对象的困境
cpp
void print_point(const Point &p) {
// p 是 const 引用------承诺不修改 p
// 但编译器怎么知道 p.distance() 不会偷偷改 p 的成员?
double d = p.distance(other); // 能编译通过吗?
}
答案取决于 distance 是否被标记为 const 成员函数。
3.2 const 成员函数的作用
cpp
class Point {
public:
// const 成员函数:承诺不修改成员变量
double distance(const Point &other) const {
// this 的类型是 const Point*
// 你不能在这里写 x_ += 1; ------ 编译器报错
double dx = x_ - other.x_;
double dy = y_ - other.y_;
return sqrt(dx * dx + dy * dy);
}
// 非 const 成员函数:可能修改成员
void move(double dx, double dy) {
x_ += dx; // 合法:可以修改
}
private:
double x_, y_;
};
void print_distance(const Point &a, const Point &b) {
// a.distance(b) 可以,因为 distance 是 const
// a.move(1, 1) 不行!------ move 不是 const,可能修改对象
}
规则很简单:
| 对象类型 | 能调用非 const 函数 | 能调用 const 函数 |
|---|---|---|
Point & |
✅ | ✅ |
const Point & |
❌ | ✅ |
const 是"承诺不修改"的签名------它让编译器和读者都能信任这个函数。
3.3 mutable:const 函数中的"例外"
有些时候,const 成员函数需要修改某些**不影响"逻辑状态"**的成员------比如缓存:
cpp
class ExpensiveCalc {
public:
int compute() const {
if (!cached_) {
result_ = heavy_computation(); // const 方法中修改 result_?
cached_ = true; // 也修改 cached_?
}
return result_;
}
private:
int heavy_computation() const { /* 耗时计算 */ return 42; }
mutable bool cached_ = false; // mutable:const 函数也能改
mutable int result_ = 0; // 同上
};
mutable 告诉编译器:"这个成员不影响对象的逻辑常量性,即使在 const 函数中也可以修改。"
四、static 成员函数:不拿 this 的函数
有些函数逻辑上属于这个类,但不需要操作具体对象:
cpp
#include <cmath>
class MathConstants {
public:
static double pi() { return 3.141592653589793; }
static double e() { return 2.718281828459045; }
};
int main() {
// 用 类名::函数名() 调用,不需要对象
double radius = 5.0;
double circumference = 2 * MathConstants::pi() * radius;
}
static 成员函数的核心特征:
| 特征 | 普通成员函数 | static 成员函数 |
|---|---|---|
| 有 this 指针 | ✅ | ❌ |
| 能访问非 static 成员 | ✅ | ❌(没有 this,不知道是哪个对象的成员) |
| 能访问 static 成员 | ✅ | ✅ |
| 调用方式 | obj.func() |
Class::func() 或 obj.func() |
| 可以是 const | ✅ | ❌(没有 this,const 无从谈起) |
典型用途:工厂方法、工具函数、命名空间式的函数分组。
五、编译器视角:成员函数到底长什么样
让我们回到最底层。C++ 的成员函数在编译后,本质上就是普通的 C 函数加了一个隐藏参数。
cpp
class Point {
public:
void move(double dx, double dy) { x_ += dx; y_ += dy; }
private:
double x_, y_;
};
编译器会对 move 做名称修饰(name mangling),把它变成类似这样的东西:
// 编译器生成的等价C代码(示意,实际mangling规则更复杂)
void _ZN5Point4moveEdd(Point *this, double dx, double dy) {
this->x_ += dx;
this->y_ += dy;
}
而你写的 p.move(1.0, 2.0) 被翻译成 _ZN5Point4moveEdd(&p, 1.0, 2.0)。
如果是 const 成员函数,this 前面加 const:
cpp
double distance(const Point &other) const;
// → double _ZNK5Point8distanceERKS_(const Point *this, const Point &other);
// ↑ 注意 const
🧠 核心理解 :
p.move(x, y)和point_move(&p, x, y)在底层根本就是同一回事。C++ 没有增加任何运行时开销------它只是在语法层面重新组织了函数和数据的关系。
六、完整对照与总结
| 维度 | C 风格 | C++ 风格 |
|---|---|---|
| 函数定义 | void point_move(Point*, double, double) |
void move(double, double) 在 class 内 |
| 调用语法 | point_move(&p, 1, 1) |
p.move(1, 1) |
| 操作对象 | 第一个参数惯例 | this 指针隐式传递 |
| const 保证 | const Point* 是"建议" |
const 成员函数强制保证 |
| 静态函数 | 普通 C 函数,靠前缀区分 | static 成员函数,作用域在类内 |
| 链式调用 | 返回指针 point_move(&p)->... |
返回引用 return *this |
总结
C++ 成员函数和 this 指针的三个核心认知:
- 成员函数不是"凭空粘在对象上的魔法" ------编译器只是把调用者对象的地址作为隐藏的第一个参数传进去,和 C 的
struct *模式在底层是一回事 - const 成员函数是接口契约------它告诉调用者"我不会修改你",让 const 对象也能安全地使用这个函数。这是编译器级别的不变量保证
- static 成员函数是"属于类的工具函数"------没有 this,不能碰对象状态,适合工厂方法和常量定义
下一篇文章,我们将进入面向对象的第三个核心概念------继承 。但我要先给你一个重要的提醒:继承不是"拿来用代码"的工具,而是一种严格的 is-a 关系。乱用继承比不用继承更危险。
📝 动手练习:
- 把第3篇的
DynString类改写为支持链式调用的append方法(返回*this)- 写一个带
mutable缓存的类,验证 const 成员函数确实可以修改mutable成员- 用 GNU 的
nm -C工具查看编译后的目标文件,观察成员函数的名称修饰(name mangling)结果