从二进制的角度看类(Class)

    大三时选了C++的课程,老师强调:相对于C语言来说,C++最核心的改动在于引入了类的概念。现在为止,我只用C++写过一些简单的程序,包括使用MFC,并没有使用过诸如模板的高级C++特性。围绕类,一直有两个问题悬在心中:
    1. 为什么要引入类?
    2. 类的二进制本质究竟是什么?
最近,我探究了第二个问题,看到一些有意思的结论,简单总结在这儿。使用的操作系统是Ubuntu 14.04,编译器GCC/G++。
    
    首先,new/delete其实是两个特殊的操作符函数(operator),实现在C++运行库中。语句classA *p = new classA;或者delete p;意味着两步操作。第一,调用new或delete操作符函数,在堆中申请空间,存储与实例有关的数据。第二,编译器负责在new/delete操作符函数的调用之后添加对classA构造函数的调用。
    第二,真正实例化的是对象的数据,而非代码。一个类包含两部分:数据和代码。数据描述世界中某个对象的属性,譬如一个人的年龄,而代码则代表着该对象的动作(action),譬如走路,阅读。那么问题就来了,如果一个类classA实例化出两个对象:
    classA *p1 = new classA;
    classA *p2 = new classA;
那么p1和p2的代码部分是共享的还是独立存在的呢?答案是共享。事实上,通过简单的实验和反汇编就可以看出,上述代码的结果就是:p1和p2指向了堆中的不同位置,在那儿分别存储由两个实例的数据部分。对p1的某个成员函数的调用:
    p1 -> foo();
和对p2的同一个成员函数的调用:
    p2 -> foo();
都意味着对某个地址(函数入口)的调用:
    call xxxxxxxx (classA::foo()的地址)
这意味着,p1和p2所指向的实例是共享代码的——真正被实例化的只有数据。事实上,对实例成员函数的调用会带来两条机器指令:
    push xxxxxxxx
    call xxxxxxxx
而push指令的操作数就是该实例的地址。这意味着如下的C++语句到汇编语句的映射关系:
    p1 -> foo();    push p1
                    call xxxxxxxx (classA::foo()的地址)
    p2 -> foo();    push p2
                    call xxxxxxxx (classA::foo()的地址)
换句话说,任意一次对对象的成员函数的调用都含有一次隐式的参数传递。指向对象的指针被传递给了成员函数,用于告知成员函数:本对象的数据都在那儿了,你的任操作都是针对那些数据进行的!最后,值得补充的是,继承关系的二进制本质是代码共享。譬如,有一个类classB继承了类classA的函数foo(),那么classB::foo()和classA::foo()将映射到同一段二进制代码!
    第三,符号名修饰机制导致了extern "C"的存在,并且实现了多态。从高级程序语言来看,两个具有相同名称的函数,int foo(int)和char foo(float),在编译后将得到两个具有不同名称的符号(symbol)。符号名因编译器的不同实现而不同,但是符号名都和函数名、函数形参个数和类型、返回值类型、函数所属类以及函数所属命名空间有关。符号名修饰机制和许多C++特性密切相关,包括多态、重载、extern "C"等等。正是符号名修饰机制导致我们在使用C++标准库的代码中,总是见到using namespace std;。该语句将导致本编译单元中所有的变量、函数都被置于std的命名空间之中。根据ISO的C++规范,除操作符函数operator new和operator delete外,C++标准库中的所有函数和预定义的变量都被置于std命名空间内。如果我们不使用using...std而直接引用C++标准库中的函数,结果将是未声明错误。当然,我们也可以使用std::来指明命名空间,比如:
     std::cout<<"Hello World!"

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值