成员函数与 this 指针:函数属于数据

文章目录

  • 引言
  • [一、从 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));
}

movedistance 这两个函数住进了 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 指针的三个核心认知:

  1. 成员函数不是"凭空粘在对象上的魔法" ------编译器只是把调用者对象的地址作为隐藏的第一个参数传进去,和 C 的 struct * 模式在底层是一回事
  2. const 成员函数是接口契约------它告诉调用者"我不会修改你",让 const 对象也能安全地使用这个函数。这是编译器级别的不变量保证
  3. static 成员函数是"属于类的工具函数"------没有 this,不能碰对象状态,适合工厂方法和常量定义

下一篇文章,我们将进入面向对象的第三个核心概念------继承 。但我要先给你一个重要的提醒:继承不是"拿来用代码"的工具,而是一种严格的 is-a 关系。乱用继承比不用继承更危险。


📝 动手练习

  1. 把第3篇的 DynString 类改写为支持链式调用的 append 方法(返回 *this
  2. 写一个带 mutable 缓存的类,验证 const 成员函数确实可以修改 mutable 成员
  3. 用 GNU 的 nm -C 工具查看编译后的目标文件,观察成员函数的名称修饰(name mangling)结果
相关推荐
music score1 小时前
google 的C++自动化测试框架详解(Google Test)(2)
c++·qt·lucene
charlie1145141911 小时前
基于开源项目的现代C++实战——OnceCallback 实战(五):then 链式组合
开发语言·c++·开源
Shan12051 小时前
在C++中尝试封装为函数
开发语言·c++·算法
Shadow(⊙o⊙)1 小时前
Linux进程地址空间——钻入Linux内核架构性剖析 硬核手搓!
java·linux·运维·服务器·开发语言·c++
郝学胜-神的一滴2 小时前
干货版《算法导论》04:渐近复杂度与序列接口实战
java·开发语言·数据结构·c++·python·算法
Peter·Pan爱编程2 小时前
构造与析构:对象生命周期的“自动挡“
c++
redaijufeng2 小时前
C/C++程序从编译到链接的过程
c语言·开发语言·c++
点云学徒2 小时前
【PCL中Ptr释放问题 aligned_free 的2种解决方法】
c++·pcl·点云处理
草莓熊Lotso2 小时前
【CMake】 工程实战:可执行文件从编译、链接到安装全流程深度拆解
linux·运维·服务器·网络·c++·cmake