C++杂记003:this详解和函数调用原理

阅读量: 鲁文奎 2021-04-22 19:14:19
Categories: Tags:

[toc]

this 介绍

this 是C++中在类中可以使用的一个关键字, 表示一个指针, 指向当前类的实例对象。

class Person {
	private:
		string name;
		int age;
	public:
	Person(string n,int a)
	{
		name = n;
		age = a;
	}

	Person& add_age(int a)
	{
		age += a;
		return *this;
	}

	void printInfo() const {
		cout << "name: " << this->name << endl;
		cout << "age: " << this->age << endl;
	}

	~Person() {}
};

this 用法

两种常用使用场景

this 与 类的实例强绑定, 决定了this和static无缘。 所以, 在使用this的时候, 不要涉及static相关的内容。

  1. 在类的非静态成员函数中返回类对象本身的时候,可以使用return *this.
Person& add_age(int a)
{
	age += a;
	return *this;
}
  1. 当参数名与成员变量名相同时, 可以使用this来进行区分
void set_name(string name)
{
	this->name = name;
}

this指针的类型

在旧版本C++中, this的指向允许被修改, 但是这个特性最终被移除了,现在的this只能作为一个r-value。也正是因为此, this的类型完成了从TypeName * 到 TypeName * const的转换。
如果是在类中的常方法(被const修饰的方法)中, 因为不允许this修改所指向的内容, 因此this指针的类型被转换为const TypeName * const

	void printInfo() const {
		cout << "name: " << this->name << endl;
		cout << "age: " << this->age << endl;
	}

this 深究

好, 无聊的基础知识梳理的差不多了, 接下来就开始证道之旅!

this指针何时被赋值?

当实例对象调用成员函数的时候, 会把实例对象的地址隐式传递给this指针。结合代码, 我在关键的汇编代码处加了注释 ,如下:
创建Person类的实例对象的反汇编如下:

    44: 	Person p("Zhansan", 20);
011056FA  push        14h  
011056FC  sub         esp,1Ch  
011056FF  mov         ecx,esp  
01105701  mov         dword ptr [ebp-100h],esp  
01105707  push        offset string "Zhansan" (0110ED60h)  
0110570C  call        std::basic_string<char,std::char_traits<char>,std::allocator<char> >::basic_string<char,std::char_traits<char>,std::allocator<char> > (01101078h)  
01105711  lea         ecx,[p]  //关键步骤一: 这里, 将p的地址直接给了ecx寄存器
01105714  call        Person::Person (01101221h)  
01105719  mov         dword ptr [ebp-114h],eax  
0110571F  mov         dword ptr [ebp-4],0  
    45: 	p.add_age(10).printInfo();

函数 add_age 的反汇编代码如下:

    26: 	Person& add_age(int a)
    27: 	{
01105AF0  push        ebp  
01105AF1  mov         ebp,esp  
01105AF3  sub         esp,0CCh  
01105AF9  push        ebx  
01105AFA  push        esi  
01105AFB  push        edi  
01105AFC  push        ecx  // 这里, ecx保存着p的地址, 然后将ecx的值压栈
01105AFD  lea         edi,[ebp-0CCh]  
01105B03  mov         ecx,33h  // 这里仍然是操作ecx, 作为一个累计器, 不过此时ecx的值已经在栈中, 这里改变其值不重要
01105B08  mov         eax,0CCCCCCCCh  
01105B0D  rep stos    dword ptr es:[edi]  
01105B0F  pop         ecx  // 关键步骤二: 这里出栈, 将栈顶的值赋值给了ecx, 也就是p的地址
01105B10  mov         dword ptr [this],ecx  // 关键步骤三: 这里将ecx的地址赋值给了this
01105B13  mov         ecx,offset _CDD4FDA2_this@cpp (0111500Ah)  
01105B18  call        @__CheckForDebuggerJustMyCode@4 (011013CFh)  
    28: 		age += a;
01105B1D  mov         eax,dword ptr [this]  
01105B20  mov         ecx,dword ptr [eax+1Ch]  
01105B23  add         ecx,dword ptr [a]  
01105B26  mov         edx,dword ptr [this]  
01105B29  mov         dword ptr [edx+1Ch],ecx  
    29: 		return *this;
01105B2C  mov         eax,dword ptr [this]  
    30: 	}

拓展:函数的调用过程

以虚函数举例:

 // Your original C++ source code
 class Base {
 public:
   virtual arbitrary_return_type virt0(...arbitrary params...);
   virtual arbitrary_return_type virt1(...arbitrary params...);
   virtual arbitrary_return_type virt2(...arbitrary params...);
   virtual arbitrary_return_type virt3(...arbitrary params...);
   virtual arbitrary_return_type virt4(...arbitrary params...);
   ...
 };
  1. 编译器会创建一个包含5个函数指针的static table , 保存在某块静态内存中。大多数编译器在编译.cpp文件中的Base类的第一个non_inline函数的时候就创建了此static table。我们暂且把这张表叫做v-table, 命名为Base::__vtable. 此static table 是所有类的实例对象共有的. static table结构如下:
// Pseudo-code (not C++, not C) for a static table defined within file Base.cpp

 // Pretend FunctionPtr is a generic pointer to a generic member function
 // (Remember: this is pseudo-code, not C++ code)
 FunctionPtr Base::__vtable[5] = {
   &Base::virt0, &Base::virt1, &Base::virt2, &Base::virt3, &Base::virt4
 };
  1. 编译器将对增加一个隐藏指针到每个实例对象中, 我们把这个隐藏指针叫做v-pointer。 v-pointer是一个隐藏的类成员, 结构如下:
 // Your original C++ source code
 class Base {
 public:
   ...
   FunctionPtr* __vptr;  ← supplied by the compiler, hidden from the programmer
   ...
 };
  1. 编译器在每个构造函数中对v-pointer进行了初始化, 这样每个实例的对象都会指向这个类的static v-table. 就像下面这样初始化v-pointer:
Base::Base(...arbitrary params...)
   : __vptr(&Base::__vtable[0])  ← supplied by the compiler, hidden from the programmer
   ...
 {
   ...
 }
  1. 如果说有一个派生类Der,继承自Base,编译器将重复步骤1和步骤3(没有步骤2). 在步骤1中, 编译器将创建一个和Base::__vtable一样的static table, 但是会用重载的函数指针替代Base::__vtable中的虚函数指针。假设派生类Der的代码如下:
 // Your original C++ source code
 class Der:Base {
 public:
   override arbitrary_return_type virt0(...arbitrary params...);
   override arbitrary_return_type virt1(...arbitrary params...);
   override arbitrary_return_type virt2(...arbitrary params...);
   ...
 };

派生类Der的v-table就会类似于下面这样:

// Pseudo-code (not C++, not C) for a static table defined within file Der.cpp

 // Pretend FunctionPtr is a generic pointer to a generic member function
 // (Remember: this is pseudo-code, not C++ code)
 FunctionPtr Der::__vtable[5] = {
   &Der::virt0, &Der::virt1, &Der::virt2, &Base::virt3, &Base::virt4
 }; 

在步骤3中, 编译器会在派生类Der的构造器中初始化v-pointer指针, 指向派生类Der的static v-table. Der中的v-pointer指针和Base中的v-pointer指针是同一个。切勿以为,在Der类中又创建了一个v-pointer指针.

  1. 调用。 我们的调用代码类似于这样:
 // Your original C++ code
 void mycode(Base* p)
 {
   p->virt3();
 }

编译器不知道你调用的是Base::virt3()还是Der::virt3(), 它只会去当前实例的vtable中去找。 如下:

// Pseudo-code that the compiler generates from your C++

 void mycode(Base* p)
 {
   p->__vptr[3](p);
 } 

好的, 关于C++的this指针的分享就到这里了。
谢谢大家的阅读。
有问题可以在下面留言。

参考资料