大家好,我是小康。今天我们来聊聊 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::incrementmov eax, ecx ; ECX寄存器中存储的是this指针mov edx, DWORD PTR [eax] ; 将this->count的值加载到EDXadd edx, 1 ; count值加1mov DWORD PTR [eax], edx ; 将结果写回this->countret ; 返回
看到了吗?在 Microsoft 的 x86 调用约定中,ECX寄存器被用来存储类成员函数的 this 指针。在这段汇编代码中,ecx
就包含了我们对象c
的内存地址!
如果我们切换到 G++ 编译器和 Linux 平台,汇编代码可能看起来略有不同:
; G++下的Counter::increment()方法(简化版)
_ZN7Counter9incrementEv:mov eax, DWORD PTR [edi] ; 在 G++中,EDI寄存器存储this指针add eax, 1 ; count值加1mov DWORD PTR [edi], eax ; 将结果写回this->countret ; 返回
有趣的是,不同的编译器和平台对 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赋值给成员变量widthheight(height), // 参数height赋值给成员变量heightarea(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指针主要完成两件事:
- 找到正确的函数地址:当调用
animal->makeSound()
时,编译器通过 this 指针找到对象的虚函数表,再从表中找到正确版本的函数 - 传递给实际执行的函数:找到函数后,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指针用于:
- 防止自赋值(
if (this != &other)
) - 访问和修改当前对象的成员
- 返回自身引用(
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* const
或const 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 指针的理解和疑问!
怎么关注我的公众号?
扫下方公众号二维码即可关注。
另外,小康还建了一个技术交流群,专门聊技术、答疑解惑。如果你在读文章时碰到不懂的地方,随时欢迎来群里提问!我会尽力帮大家解答,群里还有不少技术大佬在线支援,咱们一起学习进步,互相成长!