写了 8 年C++,才知道this指针竟是这样工作的!从汇编看本质!

大家好,我是小康。今天我们来聊聊 C++ 的 this 指针。

相信我,看完这篇文章,你将彻底搞懂 C++ 中最神秘的 this 指针!不再被面试官问到 this 时一脸茫然!

前言:this指针,C++中的隐形杀手

嘿,朋友们!还记得第一次接触 C++ 的 this 指针时的懵逼感觉吗?

“为啥要用this?”

“它到底指向哪里?”

“为啥我不写 this 也能访问成员变量?”

“编译器是怎么处理这个神秘的指针的?”

如果你还在为这些问题挠头,那这篇文章就是为你准备的!今天咱们不搞那些抽象的概念解释,直接掀开 C++ 的盖子,从汇编代码的角度看看 this 指针到底是个啥玩意儿!我们不仅会了解它的基本概念,还会深入探索它在不同编译器、不同调用约定下的表现,以及它与 C++ 高级特性的关系。

微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆

一、this指针的真面目:一个隐藏的函数参数

先说个大实话:this指针其实就是编译器偷偷塞给你的一个函数参数,它指向当前对象的内存地址

是不是觉得有点懵?没关系,咱们用一个超简单的例子来说明:

class Dog {
public:
    int age;
    
    void bark() {
        cout << "汪汪,我今年" << age << "岁了!" << endl;
    }
};

int main() {
    Dog xiaohua;
    xiaohua.age = 3;
    xiaohua.bark();  // 输出:汪汪,我今年3岁了!
    
    return 0;
}

当我们调用xiaohua.bark()时,编译器实际上做了什么呢?它悄悄地把这个调用转换成了:

// 编译器内部转换后的代码(伪代码)
bark(&xiaohua);

也就是说,编译器偷偷把对象的地址作为第一个参数传给了成员函数!这个隐藏的参数,就是 this 指针!

二、揭秘:从汇编代码看this指针

不信?那我们直接看汇编代码!(别慌,我会用大白话解释)

假设我们有这样一个简单的类:

class Counter {
private:
    int count;
public:
    Counter() : count(0) {}
    
    void increment() {
        count++;
    }
    
    int getCount() {
        return count;
    }
};

int main() {
    Counter c;
    c.increment();
    cout << c.getCount() << endl;
    return 0;
}

我们可以用编译器将这段代码转成汇编语言。以下是在MSVC编译器(VS)编译后的简化汇编代码:

; Counter::increment() 方法的汇编代码(简化版)
?increment@Counter@@QAEXXZ:          ; Counter::increment
    mov eax, ecx             ; ECX寄存器中存储的是this指针
    mov edx, DWORD PTR [eax] ; 将this->count的值加载到EDX
    add edx, 1               ; count值加1
    mov DWORD PTR [eax], edx ; 将结果写回this->count
    ret                      ; 返回

看到了吗?在 Microsoft 的 x86 调用约定中,ECX寄存器被用来存储类成员函数的 this 指针。在这段汇编代码中,ecx就包含了我们对象c的内存地址!

如果我们切换到 G++ 编译器和 Linux 平台,汇编代码可能看起来略有不同:

; G++下的Counter::increment()方法(简化版)
_ZN7Counter9incrementEv:
    mov eax, DWORD PTR [edi]  ; 在 G++中,EDI寄存器存储this指针
    add eax, 1                ; count值加1
    mov DWORD PTR [edi], eax  ; 将结果写回this->count
    ret                       ; 返回

有趣的是,不同的编译器和平台对 this 指针的处理方式略有不同。这就是为什么理解底层机制如此重要——它让我们能够更好地理解跨平台编程时可能遇到的问题。

三、深入探索:this指针是怎么传递的?

说到这里,你可能会好奇:“既然 this 是个参数,编译器是怎么传给函数的呢?

这个问题涉及到所谓的"调用约定"。别被这个术语吓到,简单来说,调用约定就是"函数参数传递的规则",就像不同国家有不同的交通规则一样。

让我用一个简单的比喻:函数调用就像寄快递,调用约定就是快递公司的送货规则:

  • 参数应该放在哪里?(寄存器还是内存栈)
  • 参数应该以什么顺序传递?(从左到右还是从右到左)
  • 谁负责"打扫现场"?(谁来清理栈)

对于 C++ 中的 this 指针,这个"快递"有着特殊的送货方式,而且在不同平台上规则还不一样!

3.1 看个实际例子

为了让概念更具体,我们来看一个简单的类和它的成员函数调用:

class Dog {
public:
    int age;
    
    void bark() {
        cout << "汪汪,我今年" << age << "岁了!" << endl;
    }
    
    void eat(int foodAmount, bool isHungry) {
        if (isHungry) {
            cout << "真香!我吃了" << foodAmount << "克狗粮!" << endl;
        } else {
            cout << "我不饿,只吃了" << foodAmount/2 << "克。" << endl;
        }
    }
};

int main() {
    Dog dog;
    dog.age = 3;
    dog.bark();
    dog.eat(100, true);
    return 0;
}

当我们调用dog.bark()dog.eat(100, true)时,编译器在不同平台上的处理方式有什么不同呢?

3.2 Windows平台(32位)下this指针的传递

在Windows 32位系统下,MSVC编译器会这样处理:

this指针放在哪里? → ECX寄存器
其他参数怎么传? → 从右到左压入栈中
谁来清理栈? → 被调用函数负责清理栈(称为callee-clean,通过ret N指令实现)

当调用dog.eat(100, true)时,简化的汇编代码会是这样:

; 从右到左压栈,先压入isHungry参数
push 1               ; true
; 再压入foodAmount参数
push 100             ; 100克狗粮
; this指针(dog对象的地址)放入ECX寄存器
lea ecx, [dog]       ; 加载dog的地址到ECX
; 调用函数
call Dog::eat        ; 调用eat方法
; 函数内部会在返回前清理栈

3.3 Linux平台(32位)下this指针的传递

在Linux 32位系统下,G++编译器的处理方式有所不同:

this指针放在哪里? → 作为第一个参数,最后压入栈
其他参数怎么传? → 从右到左压入栈中
谁来清理栈? → 对于普通成员函数,使用的是 cdecl 约定,由调用者清理栈

当调用dog.eat(100, true)时,简化的汇编代码会是:

; 从右到左压栈,先压入isHungry参数 
push 1               ; true
; 再压入foodAmount参数
push 100             ; 100克狗粮
; 最后压入this指针
push [dog的地址]      ; this指针
; 调用函数
call _ZN3Dog3eatEib  ; 调用eat方法,这是G++的名称修饰(name mangling)
; 函数返回后,调用者清理栈
add esp, 12          ; 清理3个参数(each 4字节)

3.4 64位系统下 this 指针的传递

在 64 位系统中,参数传递方式变得更加统一,主要通过寄存器完成,但 Windows 和 Linux 平台使用的寄存器和规则有所不同:

1、Windows 64位(MSVC编译器)

  • this 指针放在 RCX 寄存器(第一个参数位置)
  • 后续参数分别放在 RDX, R8, R9 寄存器
  • 多余参数(超过4个)才会压栈
  • 谁来清理栈?→ 调用者负责清理栈(通过 add rsp, N 指令来实现)

2、Linux 64位(G++编译器)

  • this 指针放在 RDI 寄存器(第一个参数位置)
  • 后续参数分别放在 RSI, RDX, RCX, R8, R9 寄存器
  • 多余参数(超过6个)才会压栈
  • 谁来清理栈?→ 调用者负责清理栈(通过 add rsp, N 指令来实现)

以Windows 64位为例,调用dog.eat(100, true)时的简化汇编:

; this指针放入RCX
lea rcx, [dog]       ; 加载dog对象地址到RCX
; foodAmount放入RDX
mov rdx, 100         ; 100放入RDX
; isHungry放入R8
mov r8, 1            ; true放入R8
; 调用函数
call Dog::eat
; 函数返回后,如果有参数通过栈传递,调用者需要清理栈
; 在这个例子中,所有参数都通过寄存器传递,不需要栈清理

这里有个有趣的变化:在 32 位系统中,Windows 和 Linux 对 this 指针的处理方式差异很大(寄存器vs栈),而在64位系统中,两者都使用寄存器传递 this 指针,只是使用的具体寄存器不同。

另外,64 位系统无论 Windows 还是 Linux,都使用统一的调用约定,不再像 32 位平台那样对成员函数和普通函数使用不同的约定。这使得 64位 平台下的函数调用机制更加一致和简洁。

四、this指针到底有啥用?实用案例详解

你可能会问:“那我为啥要关心 this 指针啊?又不是我自己写的。”

好问题!this 指针虽然是编译器偷偷加的,但了解它有这些超实用的好处:

4.1 区分同名变量

当成员变量和函数参数同名时,this可以明确指向成员变量:

class Person {
private:
    string name;
    int age;
public:
    void setInfo(string name, int age) {
        this->name = name;  // 区分成员变量和参数
        this->age = age;    // 没有this就会造成歧义
    }
};

4.2 实现链式编程

返回 this 指针可以实现方法的连续调用,这是很多现代 C++ 库的常用技巧:

class StringBuilder {
private:
    string data;
public:
    StringBuilder& append(const string& str) {
        data += str;
        return *this;  // 返回对象本身
    }
    
    StringBuilder& appendLine(const string& str) {
        data += str + "\n";
        return *this;
    }
    
    string toString() const {
        return data;
    }
};

// 使用方式
StringBuilder builder;
string result = builder.append("Hello").append(" ").append("World").appendLine("!").toString();
// 结果: "Hello World!\n"

这种编程风格在很多现代框架中非常常见,比如jQuery、C#的LINQ、Java的Stream API等。

4.3 在构造函数初始化列表中使用

this指针在构造函数初始化列表中也很有用:

class Rectangle {
private:
    int width;
    int height;
    int area;
public:
    Rectangle(int width, int height) : 
        width(width),       // 参数width赋值给成员变量width
        height(height),     // 参数height赋值给成员变量height
        area(this->width * this->height)  // 使用已初始化的成员计算area
    {
        // 构造函数体
    }
};

注意在初始化列表中,成员变量是按照 声明顺序 初始化的,而不是按照初始化列表中的顺序。上面的例子中,如果 area 在 width 和 height 之前声明,那么计算 area 时使用的 width 和 height 将是未初始化的值!

4.4 实现单例模式

this指针在实现单例模式时也非常有用:

class Singleton {
private:
    static Singleton* instance;
    
    // 私有构造函数
    Singleton() {}
    
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
    
    // 返回this的方法可以链式调用
    Singleton* doSomething() {
        cout << "Doing something..." << endl;
        return this;
    }
    
    Singleton* doSomethingElse() {
        cout << "Doing something else..." << endl;
        return this;
    }
};

// 初始化静态成员
Singleton* Singleton::instance = nullptr;

// 使用方式
Singleton::getInstance()->doSomething()->doSomethingElse();

微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆

五、汇编角度看不同对象调用同一方法

让我们更进一步,看看不同对象调用同一个方法时,this指针有什么不同:

int main() {
    Dog dog1, dog2;
    dog1.age = 3;
    dog2.age = 5;
    
    dog1.bark();  // 汪汪,我今年3岁了!
    dog2.bark();  // 汪汪,我今年5岁了!
}

从汇编角度来看,这两次调用使用的是完全相同的指令,唯一的区别是传入的 this 指针不同:

; dog1.bark()调用
lea ecx, [dog1]   ; 将dog1的地址加载到ECX(this指针)
call Dog::bark    ; 调用bark方法

; dog2.bark()调用
lea ecx, [dog2]   ; 将dog2的地址加载到ECX(this指针)
call Dog::bark    ; 调用相同的bark方法

这就解释了为什么C++可以用同一份成员函数代码处理不同的对象——因为函数通过 this 指针就能知道它正在操作哪个对象!

六、this指针与C++的高级特性

6.1 this指针与虚函数

虚函数是 C++ 多态的基础,而this指针在虚函数调用中扮演着关键角色

看个简单的多态例子:

class Animal {
public:
    virtual void makeSound() {
        cout << "动物发出声音..." << endl;
    }
};

class Dog : public Animal {
public:
    void makeSound() override {
        cout << "汪汪汪!" << endl;
    }
};

void letAnimalSpeak(Animal* animal) {
    animal->makeSound();  // 调用虚函数
}

int main() {
    Dog dog;
    letAnimalSpeak(&dog);  // 输出:汪汪汪!
}

这里 this 指针有什么作用呢?在虚函数调用中,this指针主要完成两件事:

  1. 找到正确的函数地址:当调用animal->makeSound()时,编译器通过 this 指针找到对象的虚函数表,再从表中找到正确版本的函数
  2. 传递给实际执行的函数:找到函数后,this指针会作为参数传给它,这样函数才知道它在操作哪个对象

从汇编角度看,虚函数调用大致是这样的:

; animal->makeSound()的汇编实现(简化版)
mov ecx, [animal]    ; 获取this指针
mov eax, [ecx]       ; 从this指针(ecx)加载vptr(虚表指针)
call [eax + 偏移量]   ; 调用虚表中对应的函数

# 这里的偏移量是虚函数在虚表中的位置。

这就是为什么letAnimalSpeak(&dog)能正确调用Dog::makeSound()——因为 this 指针指向的是 Dog 对象,所以系统能找到 Dog 的虚函数表,进而调用 Dog 的 makeSound()方法。

this指针让多态成为可能,它确保了同样的代码能根据对象的实际类型执行不同的操作。

6.2 this指针与const成员函数

在 const 成员函数中,this指针的类型会发生变化:

class Data {
private:
    int value;
public:
    int getValue() const {
        // 在const成员函数中,this的类型是 const Data* const
        // this = new Data(); // 错误!不能修改this指针
        // this->value = 10;  // 错误!不能通过this修改成员
        return value;
    }
    
    void setValue(int v) {
        // 在非const成员函数中,this的类型是 Data* const
        // this = new Data(); // 错误!不能修改this指针
        this->value = v;     // 正确,可以修改成员
    }
};

从编译器角度看,const成员函数相当于:

// 编译器内部转换
int getValue(const Data* const this);  // const成员函数
void setValue(Data* const this, int v);  // 非const成员函数

注意: this 本身总是一个常量指针(const指针),但在 const 成员函数中,它还指向常量对象。

6.3 this指针与移动语义

在 C++11 引入的移动语义中,this指针同样发挥着重要作用:

class Resource {
private:
    int* data;
    size_t size;
    
public:
    // 移动构造函数
    Resource(Resource&& other) noexcept {
        // 窃取other的资源
        this->data = other.data;
        this->size = other.size;
        
        // 使other处于有效但未定义状态
        other.data = nullptr;
        other.size = 0;
    }
    
    // 移动赋值运算符
    Resource& operator=(Resource&& other) noexcept {
        if (this != &other) {  // 自赋值检查
            delete[] data;     // 释放自身资源
            
            // 窃取other的资源
            this->data = other.data;
            this->size = other.size;
            
            // 使other处于有效但未定义状态
            other.data = nullptr;
            other.size = 0;
        }
        return *this;  // 返回自身引用,支持链式赋值
    }
};

在移动语义中,this指针用于:

  1. 防止自赋值(if (this != &other)
  2. 访问和修改当前对象的成员
  3. 返回自身引用(return *this

七、实战例子:手动模拟 this 指针的工作方式

为了彻底理解 this 指针,让我们写个例子,手动模拟编译器的工作:

// 常规C++类
class Cat {
private:
    int age;
    string name;
public:
    Cat(int a, string n) : age(a), name(n) {}
    
    void meow() const {
        cout << name << "喵喵,我" << age << "岁了~" << endl;
    }
    
    void setAge(int a) {
        age = a;
    }
};

// 模拟编译器转换后的代码
struct Cat_Raw {
    int age;
    string name;
};

// 注意第一个参数是Cat_Raw*,相当于this指针
void meow_raw(const Cat_Raw* this_ptr) {
    cout << this_ptr->name << "喵喵,我" << this_ptr->age << "岁了~" << endl;
}

void setAge_raw(Cat_Raw* this_ptr, int a) {
    this_ptr->age = a;
}

int main() {
    // 常规C++方式
    Cat cat(3, "小花");
    cat.meow();  // 输出:小花喵喵,我3岁了~
    cat.setAge(4);
    cat.meow();  // 输出:小花喵喵,我4岁了~
    
    // 手动模拟编译器的方式
    Cat_Raw cat_raw{3, "小花"};
    meow_raw(&cat_raw);  // 输出:小花喵喵,我3岁了~
    setAge_raw(&cat_raw, 4);
    meow_raw(&cat_raw);  // 输出:小花喵喵,我4岁了~
    
    return 0;
}

看到了吗?两种方式的输出完全一样!这就是 C++ 编译器在背后做的事情——它把对象方法调用悄悄转换成了普通函数调用,而this指针就是这个转换的关键。

八、this指针在不同编程语言中的对比

为了帮助大家更好地理解 this 指针,我们来看看它在不同编程语言中的表现:

8.1 C++中的this

  • 是指向当前对象的常量指针
  • 隐式传递给非静态成员函数
  • 在成员函数内部可以显式使用,也可以省略
  • 类型为ClassName* constconst ClassName* const(const成员函数)

8.2 Java中的this

  • 引用当前对象
  • 不能被修改
  • 可以在构造函数中调用其他构造函数:this(args)
  • 也可以用于区分局部变量和成员变量

8.3 JavaScript中的this

  • 行为更加复杂,由调用方式决定
  • 在全局上下文中,this指向全局对象(浏览器中是window)
  • 在函数内部,this取决于函数如何被调用
  • 箭头函数中的this是词法作用域的this(继承自外部上下文)
function test() {
    console.log(this);  // 在浏览器中,这里的this是window
}

const obj = {
    name: "对象",
    sayHello: function() {
        console.log(this.name);  // 这里的this是obj
    }
};

obj.sayHello();  // 输出:对象

const fn = obj.sayHello;
fn();  // 输出:undefined(因为this变成了全局对象)

这种对比让我们更加理解 C++ 中 this 指针的特殊性和重要性。

九、this指针的注意事项与陷阱

9.1 在静态成员函数中无法使用

静态成员函数属于类而不是对象,所以没有 this 指针:

class Counter {
private:
    static int totalCount;
    int instanceCount;
    
public:
    static void incrementTotal() {
        totalCount++;
        // instanceCount++;  // 错误!静态方法没有this指针
        // this->instanceCount++;  // 错误!静态方法没有this指针
    }
};

9.2 在构造函数和析构函数中使用this的注意事项

在构造函数和析构函数中使用 this 时需要格外小心,因为对象可能还未完全构造或已经销毁:

class Dangerous {
private:
    int* data;
    
public:
    Dangerous() {
        data = new int[100];
        registerCallback(this);  // 危险!对象还未完全构造
    }
    
    ~Dangerous() {
        delete[] data;
        unregisterCallback(this);  // 危险!对象已经销毁
    }
    
    void callback() {
        // 如果在构造过程中调用,可能访问未初始化的成员
        // 如果在析构过程中调用,可能访问已销毁的成员
    }
};

9.3 返回*this的临时对象问题

使用返回*this的链式调用时,需要注意临时对象的生命周期问题:

class ChainedOps {
public:
    ChainedOps& doSomething() {
        cout << "做点什么..." << endl;
        return *this;
    }
    
    ChainedOps& doSomethingElse() {
        cout << "再做点别的..." << endl;
        return *this;
    }
};

// 安全用法
ChainedOps obj;
obj.doSomething().doSomethingElse();  // 没问题,obj是持久对象

// 需要注意的用法
ChainedOps().doSomething().doSomethingElse();  // 这行代码本身没问题

上面第二种用法,初学者可能会担心有问题。实际上在这个简单例子中,根据C++标准,临时对象的生命周期会延长到整个表达式结束,所以这段代码能正常工作。

但如果函数返回的是对临时对象内部数据的引用,就可能有问题:

class Container {
private:
    vector<int> data{1, 2, 3};
    
public:
    vector<int>& getData() {
        return data;  // 返回内部数据的引用
    }
};

// 危险用法
auto& vec = Container().getData();  // 危险!vec引用了临时Container对象中的data
// 此时临时Container对象已被销毁,vec成为悬挂引用
vec.push_back(4);  // 未定义行为!

这里的问题是,临时对象Container()在表达式结束后被销毁,但我们保存了它内部数据的引用,这个引用就成了悬挂引用。

所以,关于*this的返回,记住这条简单规则:

  • 返回*this本身一般是安全的
  • 但如果保存了临时对象的成员引用,可能导致悬挂引用问题

总结:揭开this指针的神秘面纱

通过今天的深入探索,我们知道了:

1、this指针就是编译器偷偷塞给成员函数的第一个参数

2、this指针指向调用该成员函数的对象

3、不同编译器和平台对this指针的传递方式有所不同

4、 this指针让不同对象能够共用同一份成员函数代码

5、this指针在C++的高级特性(如多态、const成员函数、移动语义)中扮演着重要角色

6、从汇编角度看,this实际上就存储在特定的寄存器中(如x86的ECX)

7、使用this指针时需要注意一些陷阱,尤其是在构造/析构函数中以及返回对象引用时

理解 this 指针的工作原理,不仅能让你写出更清晰、更强大的 C++ 代码,还能帮助你更好地理解面向对象编程的本质!

最后提个问题给大家思考:如果 this 指针不存在,C++的面向对象还能实现吗? 欢迎在评论区留言讨论!


哈喽,各位铁子们!看到这儿还没溜的,肯定是真爱 C++ 的硬核选手!

说实话,我特别佩服能把 this 指针这种底层玩意儿看完的朋友,咱们真是"臭味相投"啊!(程序员的自嘲😂)

我是小康,就是那个整天研究着 CPU 吐了什么汇编、内存里藏了什么秘密的"代码考古学家"。平时没事就爱扒拉底层代码,分享 Linux 常用后端技术,然后用大白话讲给大家听。

C++ 这鬼东西,表面上高级,底下全是套路啊!但只要掌握了这些套路,那写代码时就能像开了挂一样畅快!

想不想跟我一起玩转更多底层技术的花活儿?关注我的公众号【跟着小康学编程】,我保证:

  • 没有那些网上随便就能搜到的水文
  • 没有云里雾里看不懂的高深理论
  • 只有来自实战的硬核干货和接地气的底层分析

坐等你来公众号里抬杠、讨论、提问!咱程序员就是要互相伤害才能共同进步嘛~

如果这篇文章对你有帮助,别忘了点赞、收藏、关注 哦~ 也欢迎在评论区分享你对 this 指针的理解和疑问!

怎么关注我的公众号?

微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆

另外,小康还建了一个技术交流群,专门聊技术、答疑解惑。如果你在读文章时碰到不懂的地方,随时欢迎来群里提问!我会尽力帮大家解答,群里还有不少技术大佬在线支援,咱们一起学习进步,互相成长!

想找我?加我微信即可,微信号:jkfwdkf ,备注 「加群

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值