(鄙人总结,希望和大家交流,切莫转载,谢谢!)
先看一段这个代码:
#include
class Moo {
public:
void Printf(int a) { ::printf("%p, %d\n", this, a); }
};
int main() {
Moo *p = nullptr;
p->Printf(1);
p = new Moo;
p->Printf(1);
delete p;
}
执行结果是:
0000000000000000, 1
00000226001ABCC0, 1
可以看到,当指针p为空时,调用成员函数打印"this"
指针为0,当p指向一个实际对象时,"this"
为一个实际的内存地址。
问题来了,明明"this"
没有在参数列表中出现,为什么在函数体内可以使用呢?是不是非静态成员函数自带了this
这个关键字来引用对象本身。
定义一个全局函数,其中传入Moo的指针类型
#include
class Moo {
public:
void Printf(int a) { ::printf("%p, %d\n", this, a); }
};
// 增加全局函数
void Print(Moo *_this, int a) { ::printf("%p, %d\n", _this, a); }
int main() {
Moo *p = nullptr;
p->Printf(1);
p = new Moo;
p->Printf(1);
Print(p, 1);
delete p;
}
可以从执行结果中看到,Moo::Print
和Print
的执行结果是相同的
(nil), 1
0x555874ccc2c0, 1
0x555874ccc2c0, 1
查看Intel asm汇编,在未优化的汇编代码中可以看到这两个函数的汇编指令是相同的。可以知道Moo::Print
和Print
同样都是传入了Moo的指针
.LC0:
.string "%p, %d\n"
Moo::Printf(int):
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi # Moo类型指针,8位
mov DWORD PTR [rbp-12], esi # int,4位
mov edx, DWORD PTR [rbp-12]
mov rax, QWORD PTR [rbp-8]
mov rsi, rax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
nop
leave
ret
Print(Moo*, int):
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi # Moo类型指针,8位
mov DWORD PTR [rbp-12], esi # int,4位
mov edx, DWORD PTR [rbp-12]
mov rax, QWORD PTR [rbp-8]
mov rsi, rax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
nop
leave
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], 0
mov rax, QWORD PTR [rbp-8]
mov esi, 1
mov rdi, rax
call Moo::Printf(int)
mov edi, 1
call operator new(unsigned long)
mov QWORD PTR [rbp-8], rax
mov rax, QWORD PTR [rbp-8]
mov esi, 1
mov rdi, rax
call Moo::Printf(int)
mov rax, QWORD PTR [rbp-8]
mov esi, 1
mov rdi, rax
call Print(Moo*, int)
mov rax, QWORD PTR [rbp-8]
test rax, rax
je .L4
mov esi, 1
mov rdi, rax
call operator delete(void*, unsigned long)
.L4:
mov eax, 0
leave
ret
非静态成员函数实际上的形参个数会多隐含一个"this"
,它用于引用对象本身。在成员函数执行的过程中,通过"this"
找到对象的地址,于是可以按照内存布局,找到对象的所有非静态成员变量的地址。
const修饰的对象只能调用const方法,究其原因也是因为这个"this"
。
看下面这段代码:
class Moo {
public:
void Printf(int a) { ::printf("%p, %d\n", this, a); }
void Printf(int a) const { ::printf("%p, %d\n", this, a); }
};
这两个函数完成了重载,没有报错的原因在于this
类型的不同:
可以看到在参数列表外面的右边使用const来修饰后,this
被const修饰为底层const指针,即这个指针引用的对象的内容是不允许修改的
#include
class Moo {
public:
void Printf(int a) { ::printf("non-const %p, %d\n", this, a); }
void Printf(int a) const { ::printf("const %p, %d\n", this, a); }
void HelloConst() const { ::printf("hello\n"); }
void Hello() { ::printf("hello\n"); }
};
int main() {
Moo *p1 = new Moo; // Moo *
Moo const *p2 = new Moo; // const Moo *
p1->Printf(233);
p2->Printf(666);
p1->HelloConst();
// p2->Hello();
delete p1;
delete p2;
}
void Printf();
HelloConst
是ok的,因为Moo *a; const Moo *b = a;
是成立的。void Printf() const;
Hello
是错误的,因为const Moo *a; Moo *b = a;
是不成立的。从汇编代码层面分析,被const修饰和未被const修饰的两个函数体相同的同名函数,虽然生成的汇编指令一模一样,但在.text端的地址是不同的,因此在最终寻址上会区别开。
.LC0:
.string "non-const %p, %d\n"
Moo::Printf(int):
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov DWORD PTR [rbp-12], esi
mov edx, DWORD PTR [rbp-12]
mov rax, QWORD PTR [rbp-8]
mov rsi, rax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
nop
leave
ret
.LC1:
.string "const %p, %d\n"
Moo::Printf(int) const:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov DWORD PTR [rbp-12], esi
mov edx, DWORD PTR [rbp-12]
mov rax, QWORD PTR [rbp-8]
mov rsi, rax
mov edi, OFFSET FLAT:.LC1
mov eax, 0
call printf
nop
leave
ret
是故得出一个额外的结论:const 修饰成员函数时,根本上是修饰了 "this"
。
之前有提到静态成员函数。静态成员函数不属于任何类的实例化对象,而是属于类。既然不属于某个特定的对象,所以就不需要"this"
,于是无法访问类的其他成员变量,只能访问静态变量了。
// 此处不含"this", 编译器会报错
static void static_print() { this; cout << "const static_print" << endl; };
因此C++11以前,使用静态成员函数作为回调函数注册给调用者。普通成员函数不能做到这一点,因为在注册时由于隐含的"this"
使得函数参数个数不匹配,导致回调函数注册失败。
静态成员函数因为没有"this"
,所以参数是匹配的。
C++11 引入的类模板std::function
跟可变参函数模板std::bind(_Fx&& _Func, _Types&&... _Args)
搭配可以做到普通成员函数作为回调函数设置,原理也就是将this
绑定到生成的调用对象上,生成的调用对象编译成汇编指令后,不会去拷贝this
到寄存器中。
最开始的示例中提到一个nullptr
去调用成员函数,却没造成段错误的原因是:
当Moo*
为nullptr
去调用成员函数时,并没有比对着Moo的内存模型去修改对应地址的值,也就是说并没有去修改一个权限为只读的段空间,也就是说CPU数据总线,没有把数据写回用户空间的只读区域,所以不会造成段错误。
比如下面代码依旧不会出错:
#include
class Moo {
private:
int m = 10;
public:
void Printf() {
this;
this->m;
}
};
int main() {
Moo *p1 = nullptr;
p1->Printf();
}
它对应的汇编:
Moo::Printf():
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
nop
pop rbp
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], 0
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call Moo::Printf()
mov eax, 0
leave
ret
从汇编代码来看,根本没有去获取地址中的m
的内容。
如果修改成下面,当空指针访问时,就会出现段错误:
class Moo {
public:
int m = 10;
void Printf() {
this->m++;;
}
};
因为对应的汇编变成了下面的情况:
Moo::Printf():
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov eax, DWORD PTR [rax]
lea edx, [rax+1]
mov rax, QWORD PTR [rbp-8]
mov DWORD PTR [rax], edx
nop
pop rbp
ret