美文网首页
08-this的本质

08-this的本质

作者: ducktobey | 来源:发表于2019-11-09 16:24 被阅读0次

    this

    现在有以下一段代码程序

    struct Person {
        int m_age;
        void run() {
            cout << "Person :: run() " << m_age << endl;
        }
    };
    
    int main() {
        Person person;
        person.m_age = 10;
        person.run();
        getchar();
        return 0;
    }
    

    这段程序,我们都很清楚,首先我们创建了一个person对象,然后为对象的m_age成员变量赋了一个初始值,最后调用person对象的run函数。并且最终的打印结果是Person :: run() 10,相信各位读者是可以理解的。但是,这里有一个问题,假设我们将上面的程序进行一定的修改

    struct Person {
        int m_age;
        void run() {
            cout << "Person :: run() " << m_age << endl;
        }
    };
    
    int main() {
    
        Person person1;
        person1.m_age = 10;
        person1.run();
    
        Person person2;
        person2.m_age = 20;
        person2.run();
        getchar();
        return 0;
    }
    

    两个Person类创建的对象,分别有属于自己的m_age成员变量,也分别调用了对应的run函数。在前面说过,person1调用的run函数与person2调用的run函数,是同一个run函数,即person1.run()与person2.run()执行的代码是同一份,对应Person类对象调用run函数后,得到的结果却不一样,其中person1.run()的结果为Person :: run() 10,person2.run()的结果为Person :: run() 20。而且前面也说过,对象的成员变量是保存在对象创建的内存区域,函数并不保存在对象中,所以,成员变量和函数是独立开的,对象的内存在栈空间,而函数的内存在代码区,那最终打印m_age的时候,是怎么知道该打印10还是20呢,这样的结果到底是怎么导致的呢?

    可以这样来假设,假设当前的run函数是如下方式进行定义的,在调用函数时,可以传入一个参数,最终在函数内部,就可以获取到当前对象的成员变量了

    void run(Person *person) {
        cout << "Person :: run() " << person->m_age << endl;
    }
    

    后面调用的时候,就可以通过这种方式来进行调用

    struct Person {
        int m_age;
        void run(Person *person) {
            cout << "Person :: run() " << person->m_age << endl;
        }
    };
    
    int main() {
    
        Person person1;
        person1.m_age = 10;
        person1.run(&person1);
    
        Person person2;
        person2.m_age = 20;
        person2.run(&person2);
        getchar();
        return 0;
    }
    

    这样就可以办到,通过函数参数中的指针,间接的访问到该对象的存储空间,然后访问对象的成员变量了。所以,这是一种在函数里面可以访问到对象成员变量的一种方式。但是,如果每次声明函数时,都需要定义一个这样的参数,在调用函数时,都需要传入这个参数的话,就太麻烦了。好在编译器已经把这一切都做了,编译器在每个函数里面,都提供了一个this指针,并且该指针指向当前调用该函数的对象。所以可以这样理解,某个对象调用函数时,都会偷偷的将该对象赋值给this指针。即

    struct Person {
        int m_age;
        void run() {
            //this = &person1; 假想的
            cout << "Person :: run() " << m_age << endl;
        }
    };
    
    int main() {
    
        Person person1;
        person1.m_age = 10;
        person1.run();
        getchar();
        return 0;
    }
    

    可以理解为函数中默认有一个隐式参数this指针,并且该隐式参数this指针存储着函数调用者的地址,所以上面的代码也可以这样写

    struct Person {
        int m_age;
        void run() {
            cout << "Person :: run() " << this->m_age << endl;
        }
    };
    
    int main() {
    
        Person person1;
        person1.m_age = 10;
        person1.run();
        getchar();
        return 0;
    }
    

    现在就来看看具体是怎么实现的。首先假设有一个全局的函数func,然后再main函数中调用该func函数,对比与调用run函数的区别

    void func() {
    
    }
    
    int main() {
        func();
        Person person1;
        person1.m_age = 10;
        person1.run();
    
        Person person2;
        person2.m_age = 20;
        person2.run();
        getchar();
        return 0;
    }
    

    相信你一定能想象,现在func函数生成的汇编是什么样的。对的!没错call [函数地址],并且来看一下func函数与run函数的区别

    从run函数调用生成汇编代码中看到,在执行call指令之前,执行了lea操作。lea指令的作用是取出person1的内存地址,然后将内存地址,赋值给了ecx寄存器。将地址存到ecx寄存器以后,后面是怎么获取的呢?可以通过单步调试进入到call对应的函数中。

    在这一段代码中,我们看到有一句mov指令 mov dword ptr [this],ecx,这句指令是将ecx里面存储的数据,放到[this]指针的存储空间,这样,this指针就成功的指向了person1。这样就可以在函数里面,通过this指针访问person1里面的成员变量了。

    现在将整个流程梳理一遍:

    1. 在调用run函数之前,先将person1对象的地址,存放到了ecx寄存器
    2. 调用run函数
    3. 在函数内部,将ecx寄存器中存储的person1的地址值,存放到this指针对应的存储空间
    4. 通过this指针,访问person成员变量

    但是你可能会这样想,在平时编码的过程中,并没有直接通过this指针来访问函数的成员变量,而是直接访问的。这个问题其实是语法糖,在表面上看,没有用到this,但是实际上用到了this,只不过编译器处理以后,我们可以省略,所以下面的两种写法是等价的。

    void run() {
        cout << "Person :: run() " << this->m_age << endl;
    }
    void run() {
        cout << "Person :: run() " << m_age << endl;
    }
    

    究竟是不是等价的,看以下汇编就知道了。所以将这段代码转为汇编

    struct Person {
        int m_age;
        void run() {
            cout << "Person :: run() " << m_age << endl;
        }
    };
    
    void func() {
    
    }
    
    int main() {
        func();
        Person person1;
        person1.m_age = 10;
        person1.run();
    
        Person person2;
        person2.m_age = 20;
        person2.run();
        getchar();
        return 0;
    }
    

    生成汇编

    run函数生成的汇编

    最终发现,两段代码,生成的汇编代码是完全一样的。

    好的。现在已经了解到this指针的本质,那么问题来了,可以利用this.m_age来访问成员变量吗?

    答:不可以,因为this是指针,必须用this->m_age来访问。

    指针访问对象成员的本质

    🤔下面这段代码,最后打印出来的每个成员变量的值是多少?

    struct Person {
        int m_id;
        int m_age;
        int m_height;
        void display() {
            cout << "id =  " << m_id
                << ", age = " << m_age 
                << ", height = " << m_height << endl;
        }
    };
    
    int main() {
        Person person;
        person.m_id = 10;
        person.m_age = 20;
        person.m_height = 30;
        person.display();
        getchar();
        return 0;
    }
    

    话不多说,直接看以下生成的汇编代码。

    首先,为成员变量赋值的汇编非常简单[下图],直接将立即数存放到成员变量对应的存储空间。

    为了回答上面的问题,还需要再来看看指针变量是如何访问成员变量的

    struct Person {
        int m_id;
        int m_age;
        int m_height;
        void display() {
            cout << "id =  " << m_id
                << ", age = " << m_age 
                << ", height = " << m_height << endl;
        }
    };
    
    int main() {
        Person person;
        person.m_id = 10;
        person.m_age = 20;
        person.m_height = 30;
        person.display();
    
        Person* p = &person;
        p->m_id = 10;
        p->m_age = 20;
        p->m_height = 30;
        getchar();
        return 0;
    }
    

    对应的汇编代码

    补充:上图中eax中存储的即为对象person的地址值,然后根据对象的地址 + 成员变量的偏移,就能找到对应的成员变量。

    如何利用指针间接访问所指向对象的成员变量原理总结:

    1. 从指针中取出对象的地址
    2. 利用对象的地址 + 成员变量的偏移量,计算出成员变量的地址
    3. 根据成员变量的地址访问成员变量的存储空间

    说到这里,我想已经知道上面display函数的本质了吧?

    前面讲到,以下两种写法是等价的

    void display() {
        cout << "id =  " << m_id
            << ", age = " << m_age 
            << ", height = " << m_height << endl;
    }
    void display() {
        cout << "id =  " << this->m_id
            << ", age = " << this->m_age 
            << ", height = " << this->m_height << endl;
    }
    

    所以,答案我想已经有了吧!

    现在已经知道,指针访问成员变量是通过偏移来访问的,那下面的这个打印结果又是多少呢?

    struct Person {
        int m_id;
        int m_age;
        int m_height;
        void display() {
            cout << "id =  " << m_id
                << ", age = " << m_age 
                << ", height = " << m_height << endl;
        }
    };
    
    int main() {
        Person person;
        person.m_id = 10;
        person.m_age = 20;
        person.m_height = 30;
        Person* p = (Person*)&person.m_age;
        p->m_id = 40;
        p->m_age = 50;
        person.display();
        getchar();
        return 0;
    }
    

    打印的结果是10,40,50。

    如果将上面代码中的person.display()换为p->display()得到的结果,又是什么呢?

    demo下载地址

    文章完。

    相关文章

      网友评论

          本文标题:08-this的本质

          本文链接:https://www.haomeiwen.com/subject/gsjnbctx.html