寫了八年 C++,才知道 this 指針竟是這樣工作的!從匯編看本質(zhì)!
大家好,我是小康。今天我們來聊聊 C++ 的 this 指針。
相信我,看完這篇文章,你將徹底搞懂 C++ 中最神秘的 this 指針!不再被面試官問到 this 時一臉茫然!
如果你對 this 指針還不太熟悉,強烈推薦先看看這篇入門文章:C++ 中的 this 指針:你不知道的 5 個小秘密! 打好基礎后,再回來啃這篇硬核分析,絕對事半功倍!
一、前言:this指針,C++中的隱形殺手
嘿,朋友們!還記得第一次接觸 C++ 的 this 指針時的懵逼感覺嗎?
- "為啥要用this?"
- "它到底指向哪里?"
- "為啥我不寫 this 也能訪問成員變量?"
- "編譯器是怎么處理這個神秘的指針的?"
如果你還在為這些問題撓頭,那這篇文章就是為你準備的!今天咱們不搞那些抽象的概念解釋,直接掀開 C++ 的蓋子,從匯編代碼的角度看看 this 指針到底是個啥玩意兒!我們不僅會了解它的基本概念,還會深入探索它在不同編譯器、不同調(diào)用約定下的表現(xiàn),以及它與 C++ 高級特性的關系。
二、this指針的真面目:一個隱藏的函數(shù)參數(shù)
先說個大實話:this指針其實就是編譯器偷偷塞給你的一個函數(shù)參數(shù),它指向當前對象的內(nèi)存地址。
是不是覺得有點懵?沒關系,咱們用一個超簡單的例子來說明:
class Dog {
public:
int age;
void bark() {
cout << "汪汪,我今年" << age << "歲了!" << endl;
}
};
int main() {
Dog xiaohua;
xiaohua.age = 3;
xiaohua.bark(); // 輸出:汪汪,我今年3歲了!
return0;
}
當我們調(diào)用xiaohua.bark()時,編譯器實際上做了什么呢?它悄悄地把這個調(diào)用轉(zhuǎn)換成了:
// 編譯器內(nèi)部轉(zhuǎn)換后的代碼(偽代碼)
bark(&xiaohua);
也就是說,編譯器偷偷把對象的地址作為第一個參數(shù)傳給了成員函數(shù)!這個隱藏的參數(shù),就是 this 指針!
三、揭秘:從匯編代碼看this指針
不信?那我們直接看匯編代碼?。▌e慌,我會用大白話解釋)
假設我們有這樣一個簡單的類:
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;
return0;
}
我們可以用編譯器將這段代碼轉(zhuǎn)成匯編語言。以下是在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 調(diào)用約定中,ECX寄存器被用來存儲類成員函數(shù)的 this 指針。在這段匯編代碼中,ecx就包含了我們對象c的內(nèi)存地址!
如果我們切換到 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 是個參數(shù),編譯器是怎么傳給函數(shù)的呢?"
這個問題涉及到所謂的"調(diào)用約定"。別被這個術語嚇到,簡單來說,調(diào)用約定就是"函數(shù)參數(shù)傳遞的規(guī)則",就像不同國家有不同的交通規(guī)則一樣。
讓我用一個簡單的比喻:函數(shù)調(diào)用就像寄快遞,調(diào)用約定就是快遞公司的送貨規(guī)則:
- 參數(shù)應該放在哪里?(寄存器還是內(nèi)存棧)
- 參數(shù)應該以什么順序傳遞?(從左到右還是從右到左)
- 誰負責"打掃現(xiàn)場"?(誰來清理棧)
對于 C++ 中的 this 指針,這個"快遞"有著特殊的送貨方式,而且在不同平臺上規(guī)則還不一樣!
1. 看個實際例子
為了讓概念更具體,我們來看一個簡單的類和它的成員函數(shù)調(diào)用:
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);
return0;
}
當我們調(diào)用dog.bark()和dog.eat(100, true)時,編譯器在不同平臺上的處理方式有什么不同呢?
2. Windows平臺(32位)下this指針的傳遞
在Windows 32位系統(tǒng)下,MSVC編譯器會這樣處理:
this指針放在哪里? → ECX寄存器
其他參數(shù)怎么傳? → 從右到左壓入棧中
誰來清理棧? → 被調(diào)用函數(shù)負責清理棧(稱為callee-clean,通過ret N指令實現(xiàn))
當調(diào)用dog.eat(100, true)時,簡化的匯編代碼會是這樣:
; 從右到左壓棧,先壓入isHungry參數(shù)
push 1 ; true
; 再壓入foodAmount參數(shù)
push 100 ; 100克狗糧
; this指針(dog對象的地址)放入ECX寄存器
lea ecx, [dog] ; 加載dog的地址到ECX
; 調(diào)用函數(shù)
call Dog::eat ; 調(diào)用eat方法
; 函數(shù)內(nèi)部會在返回前清理棧
3. Linux平臺(32位)下this指針的傳遞
在Linux 32位系統(tǒng)下,G++編譯器的處理方式有所不同:
this指針放在哪里? → 作為第一個參數(shù),最后壓入棧
其他參數(shù)怎么傳? → 從右到左壓入棧中
誰來清理棧? → 對于普通成員函數(shù),使用的是 cdecl 約定,由調(diào)用者清理棧
當調(diào)用dog.eat(100, true)時,簡化的匯編代碼會是:
; 從右到左壓棧,先壓入isHungry參數(shù)
push 1 ; true
; 再壓入foodAmount參數(shù)
push 100 ; 100克狗糧
; 最后壓入this指針
push [dog的地址] ; this指針
; 調(diào)用函數(shù)
call _ZN3Dog3eatEib ; 調(diào)用eat方法,這是G++的名稱修飾(name mangling)
; 函數(shù)返回后,調(diào)用者清理棧
add esp, 12 ; 清理3個參數(shù)(each 4字節(jié))
4. 64位系統(tǒng)下 this 指針的傳遞
在 64 位系統(tǒng)中,參數(shù)傳遞方式變得更加統(tǒng)一,主要通過寄存器完成,但 Windows 和 Linux 平臺使用的寄存器和規(guī)則有所不同:
(1) Windows 64位(MSVC編譯器):
- this 指針放在 RCX 寄存器(第一個參數(shù)位置)
- 后續(xù)參數(shù)分別放在 RDX, R8, R9 寄存器
- 多余參數(shù)(超過4個)才會壓棧
- 誰來清理棧?→ 調(diào)用者負責清理棧(通過 add rsp, N 指令來實現(xiàn))
(2) Linux 64位(G++編譯器):
- this 指針放在 RDI 寄存器(第一個參數(shù)位置)
- 后續(xù)參數(shù)分別放在 RSI, RDX, RCX, R8, R9 寄存器
- 多余參數(shù)(超過6個)才會壓棧
- 誰來清理棧?→ 調(diào)用者負責清理棧(通過 add rsp, N 指令來實現(xiàn))
以Windows 64位為例,調(diào)用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
; 調(diào)用函數(shù)
call Dog::eat
; 函數(shù)返回后,如果有參數(shù)通過棧傳遞,調(diào)用者需要清理棧
; 在這個例子中,所有參數(shù)都通過寄存器傳遞,不需要棧清理
這里有個有趣的變化:在 32 位系統(tǒng)中,Windows 和 Linux 對 this 指針的處理方式差異很大(寄存器vs棧),而在64位系統(tǒng)中,兩者都使用寄存器傳遞 this 指針,只是使用的具體寄存器不同。
另外,64 位系統(tǒng)無論 Windows 還是 Linux,都使用統(tǒng)一的調(diào)用約定,不再像 32 位平臺那樣對成員函數(shù)和普通函數(shù)使用不同的約定。這使得 64位 平臺下的函數(shù)調(diào)用機制更加一致和簡潔。
五、this指針到底有啥用?實用案例詳解
你可能會問:"那我為啥要關心 this 指針?。坑植皇俏易约簩懙?。"
好問題!this 指針雖然是編譯器偷偷加的,但了解它有這些超實用的好處:
1. 區(qū)分同名變量
當成員變量和函數(shù)參數(shù)同名時,this可以明確指向成員變量:
class Person {
private:
string name;
int age;
public:
void setInfo(string name, int age) {
this->name = name; // 區(qū)分成員變量和參數(shù)
this->age = age; // 沒有this就會造成歧義
}
};
2. 實現(xiàn)鏈式編程
返回 this 指針可以實現(xiàn)方法的連續(xù)調(diào)用,這是很多現(xiàn)代 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"
這種編程風格在很多現(xiàn)代框架中非常常見,比如jQuery、C#的LINQ、Java的Stream API等。
3. 在構造函數(shù)初始化列表中使用
this指針在構造函數(shù)初始化列表中也很有用:
class Rectangle {
private:
int width;
int height;
int area;
public:
Rectangle(int width, int height) :
width(width), // 參數(shù)width賦值給成員變量width
height(height), // 參數(shù)height賦值給成員變量height
area(this->width * this->height) // 使用已初始化的成員計算area
{
// 構造函數(shù)體
}
};
注意在初始化列表中,成員變量是按照 聲明順序 初始化的,而不是按照初始化列表中的順序。上面的例子中,如果 area 在 width 和 height 之前聲明,那么計算 area 時使用的 width 和 height 將是未初始化的值!
4. 實現(xiàn)單例模式
this指針在實現(xiàn)單例模式時也非常有用:
class Singleton {
private:
static Singleton* instance;
// 私有構造函數(shù)
Singleton() {}
public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
// 返回this的方法可以鏈式調(diào)用
Singleton* doSomething() {
cout << "Doing something..." << endl;
return this;
}
Singleton* doSomethingElse() {
cout << "Doing something else..." << endl;
return this;
}
};
// 初始化靜態(tài)成員
Singleton* Singleton::instance = nullptr;
// 使用方式
Singleton::getInstance()->doSomething()->doSomethingElse();
六、匯編角度看不同對象調(diào)用同一方法
讓我們更進一步,看看不同對象調(diào)用同一個方法時,this指針有什么不同:
int main() {
Dog dog1, dog2;
dog1.age = 3;
dog2.age = 5;
dog1.bark(); // 汪汪,我今年3歲了!
dog2.bark(); // 汪汪,我今年5歲了!
}
從匯編角度來看,這兩次調(diào)用使用的是完全相同的指令,唯一的區(qū)別是傳入的 this 指針不同:
; dog1.bark()調(diào)用
lea ecx, [dog1] ; 將dog1的地址加載到ECX(this指針)
call Dog::bark ; 調(diào)用bark方法
; dog2.bark()調(diào)用
lea ecx, [dog2] ; 將dog2的地址加載到ECX(this指針)
call Dog::bark ; 調(diào)用相同的bark方法
這就解釋了為什么C++可以用同一份成員函數(shù)代碼處理不同的對象——因為函數(shù)通過 this 指針就能知道它正在操作哪個對象!
七、this指針與C++的高級特性
1. this指針與虛函數(shù)
虛函數(shù)是 C++ 多態(tài)的基礎,而this指針在虛函數(shù)調(diào)用中扮演著關鍵角色
看個簡單的多態(tài)例子:
class Animal {
public:
virtual void makeSound() {
cout << "動物發(fā)出聲音..." << endl;
}
};
class Dog :public Animal {
public:
void makeSound() override {
cout << "汪汪汪!" << endl;
}
};
void letAnimalSpeak(Animal* animal) {
animal->makeSound(); // 調(diào)用虛函數(shù)
}
int main() {
Dog dog;
letAnimalSpeak(&dog); // 輸出:汪汪汪!
}
這里 this 指針有什么作用呢?在虛函數(shù)調(diào)用中,this指針主要完成兩件事:
- 找到正確的函數(shù)地址:當調(diào)用animal->makeSound()時,編譯器通過 this 指針找到對象的虛函數(shù)表,再從表中找到正確版本的函數(shù)
- 傳遞給實際執(zhí)行的函數(shù):找到函數(shù)后,this指針會作為參數(shù)傳給它,這樣函數(shù)才知道它在操作哪個對象
從匯編角度看,虛函數(shù)調(diào)用大致是這樣的:
; animal->makeSound()的匯編實現(xiàn)(簡化版)
mov ecx, [animal] ; 獲取this指針
mov eax, [ecx] ; 從this指針(ecx)加載vptr(虛表指針)
call [eax + 偏移量] ; 調(diào)用虛表中對應的函數(shù)
# 這里的偏移量是虛函數(shù)在虛表中的位置。
這就是為什么letAnimalSpeak(&dog)能正確調(diào)用Dog::makeSound()——因為 this 指針指向的是 Dog 對象,所以系統(tǒng)能找到 Dog 的虛函數(shù)表,進而調(diào)用 Dog 的 makeSound()方法。
this指針讓多態(tài)成為可能,它確保了同樣的代碼能根據(jù)對象的實際類型執(zhí)行不同的操作。
2. this指針與const成員函數(shù)
在 const 成員函數(shù)中,this指針的類型會發(fā)生變化:
class Data {
private:
int value;
public:
int getValue() const {
// 在const成員函數(shù)中,this的類型是 const Data* const
// this = new Data(); // 錯誤!不能修改this指針
// this->value = 10; // 錯誤!不能通過this修改成員
return value;
}
void setValue(int v) {
// 在非const成員函數(shù)中,this的類型是 Data* const
// this = new Data(); // 錯誤!不能修改this指針
this->value = v; // 正確,可以修改成員
}
};
從編譯器角度看,const成員函數(shù)相當于:
// 編譯器內(nèi)部轉(zhuǎn)換
int getValue(const Data* const this); // const成員函數(shù)
void setValue(Data* const this, int v); // 非const成員函數(shù)
注意: this 本身總是一個常量指針(const指針),但在 const 成員函數(shù)中,它還指向常量對象。
3. this指針與移動語義
在 C++11 引入的移動語義中,this指針同樣發(fā)揮著重要作用:
class Resource {
private:
int* data;
size_t size;
public:
// 移動構造函數(shù)
Resource(Resource&& other) noexcept {
// 竊取other的資源
this->data = other.data;
this->size = other.size;
// 使other處于有效但未定義狀態(tài)
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處于有效但未定義狀態(tài)
other.data = nullptr;
other.size = 0;
}
return *this; // 返回自身引用,支持鏈式賦值
}
};
在移動語義中,this指針用于:
- 防止自賦值(if (this != &other))
- 訪問和修改當前對象的成員
- 返回自身引用(return *this)
八、實戰(zhàn)例子:手動模擬 this 指針的工作方式
為了徹底理解 this 指針,讓我們寫個例子,手動模擬編譯器的工作:
// 常規(guī)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;
}
};
// 模擬編譯器轉(zhuǎn)換后的代碼
struct Cat_Raw {
int age;
string name;
};
// 注意第一個參數(shù)是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() {
// 常規(guī)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歲了~
return0;
}
看到了嗎?兩種方式的輸出完全一樣!這就是 C++ 編譯器在背后做的事情——它把對象方法調(diào)用悄悄轉(zhuǎn)換成了普通函數(shù)調(diào)用,而this指針就是這個轉(zhuǎn)換的關鍵。
九、this指針在不同編程語言中的對比
為了幫助大家更好地理解 this 指針,我們來看看它在不同編程語言中的表現(xiàn):
1. C++中的this
- 是指向當前對象的常量指針
- 隱式傳遞給非靜態(tài)成員函數(shù)
- 在成員函數(shù)內(nèi)部可以顯式使用,也可以省略
- 類型為ClassName* const或const ClassName* const(const成員函數(shù))
2. Java中的this
- 引用當前對象
- 不能被修改
- 可以在構造函數(shù)中調(diào)用其他構造函數(shù):this(args)
- 也可以用于區(qū)分局部變量和成員變量
3. JavaScript中的this
- 行為更加復雜,由調(diào)用方式?jīng)Q定
- 在全局上下文中,this指向全局對象(瀏覽器中是window)
- 在函數(shù)內(nèi)部,this取決于函數(shù)如何被調(diào)用
- 箭頭函數(shù)中的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指針的注意事項與陷阱
1. 在靜態(tài)成員函數(shù)中無法使用
靜態(tài)成員函數(shù)屬于類而不是對象,所以沒有 this 指針:
class Counter {
private:
static int totalCount;
int instanceCount;
public:
static void incrementTotal() {
totalCount++;
// instanceCount++; // 錯誤!靜態(tài)方法沒有this指針
// this->instanceCount++; // 錯誤!靜態(tài)方法沒有this指針
}
};
2. 在構造函數(shù)和析構函數(shù)中使用this的注意事項
在構造函數(shù)和析構函數(shù)中使用 this 時需要格外小心,因為對象可能還未完全構造或已經(jīng)銷毀:
class Dangerous {
private:
int* data;
public:
Dangerous() {
data = newint[100];
registerCallback(this); // 危險!對象還未完全構造
}
~Dangerous() {
delete[] data;
unregisterCallback(this); // 危險!對象已經(jīng)銷毀
}
void callback() {
// 如果在構造過程中調(diào)用,可能訪問未初始化的成員
// 如果在析構過程中調(diào)用,可能訪問已銷毀的成員
}
};
3. 返回*this的臨時對象問題
使用返回*this的鏈式調(diào)用時,需要注意臨時對象的生命周期問題:
class ChainedOps {
public:
ChainedOps& doSomething() {
cout << "做點什么..." << endl;
return *this;
}
ChainedOps& doSomethingElse() {
cout << "再做點別的..." << endl;
return *this;
}
};
// 安全用法
ChainedOps obj;
obj.doSomething().doSomethingElse(); // 沒問題,obj是持久對象
// 需要注意的用法
ChainedOps().doSomething().doSomethingElse(); // 這行代碼本身沒問題
上面第二種用法,初學者可能會擔心有問題。實際上在這個簡單例子中,根據(jù)C++標準,臨時對象的生命周期會延長到整個表達式結束,所以這段代碼能正常工作。
但如果函數(shù)返回的是對臨時對象內(nèi)部數(shù)據(jù)的引用,就可能有問題:
class Container {
private:
vector<int> data{1, 2, 3};
public:
vector<int>& getData() {
return data; // 返回內(nèi)部數(shù)據(jù)的引用
}
};
// 危險用法
auto& vec = Container().getData(); // 危險!vec引用了臨時Container對象中的data
// 此時臨時Container對象已被銷毀,vec成為懸掛引用
vec.push_back(4); // 未定義行為!
這里的問題是,臨時對象Container()在表達式結束后被銷毀,但我們保存了它內(nèi)部數(shù)據(jù)的引用,這個引用就成了懸掛引用。
所以,關于*this的返回,記住這條簡單規(guī)則:
- 返回*this本身一般是安全的
- 但如果保存了臨時對象的成員引用,可能導致懸掛引用問題
十一、總結:揭開this指針的神秘面紗
通過今天的深入探索,我們知道了:
- this指針就是編譯器偷偷塞給成員函數(shù)的第一個參數(shù)
- this指針指向調(diào)用該成員函數(shù)的對象
- 不同編譯器和平臺對this指針的傳遞方式有所不同
- this指針讓不同對象能夠共用同一份成員函數(shù)代碼
- this指針在C++的高級特性(如多態(tài)、const成員函數(shù)、移動語義)中扮演著重要角色
- 從匯編角度看,this實際上就存儲在特定的寄存器中(如x86的ECX)
- 使用this指針時需要注意一些陷阱,尤其是在構造/析構函數(shù)中以及返回對象引用時
理解 this 指針的工作原理,不僅能讓你寫出更清晰、更強大的 C++ 代碼,還能幫助你更好地理解面向?qū)ο缶幊痰谋举|(zhì)!