为什么通过空指针(NULL)能够正确调用类的部分成员函数
#include <iostream> using namespace std; class B {
public:
void foo() { cout << "B foo " << endl; }
void pp() { cout << "B pp" << endl; }
void FunctionB() { cout << "funB" << endl; }
}; int main()
{
B *somenull = NULL;
somenull->foo();
somenull->pp();
somenull->FunctionB(); return 0;
} 为什么 somenull 为空指针,还能执行通过呢?
能够阐明“静态绑定”和“动态绑定”的差别。
真正的原因是:由于对于非虚成员函数,C++这门语言是静态绑定的。
这也是C++语言和其他语言Java, Python的一个显著差别。
以此以下的语句为例:
somenull->foo();
这语句的意图是:调用对象somenull的foo成员函数。
假设这句话在Java或Python等动态绑定的语言之中,编译器生成的代码大概是:
找到somenull的foo成员函数。调用它。
(注意,这里的找到是程序执行的时候才找的,这也是所谓动态绑定的含义:执行时才绑定这个函数名与其相应的实际代码。
有些地方也称这样的机制为迟绑定。晚绑定。)
可是对于C++。为了保证程序的执行时效率,C++的设计者觉得凡是编译时能确定的事情,就不要拖到执行时再查找了。所以C++的编译器看到这句话会这么干:
1:查找somenull的类型,发现它有一个非虚的成员函数叫foo。(编译器干的)
2:找到了。在这里生成一个函数调用,直接调B::foo(somenull)。
所以到了执行时,因为foo()函数里面并没有不论什么须要解引用somenull指针的代码,所以真实情况下也不会引发segment fault。这里对成员函数的解析,和查找其相应的代码的工作都是在编译阶段完毕而非执行时完毕的,这就是所谓的静态绑定。也叫早绑定。
正确理解C++的静态绑定能够理解一些特殊情况下C++的行为。
this 指针是空指针 不去骚扰他 他就不搞死你
你敢动他试试
假设还没有看烦,能够參考以下的这些东西。
有以下的一个简单的类:
{
public:
static void Test1();
void Test2();
void Test3(int iTest);
void Test4();
private:
static int m_iStatic;
int m_iTest;
};
int CNullPointCall::m_iStatic = 0;
void CNullPointCall::Test1()
{
cout << m_iStatic << endl;
}
void CNullPointCall::Test2()
{
cout << "Very Cool!" << endl;
}
void CNullPointCall::Test3(int iTest)
{
cout << iTest << endl;
}
void CNullPointCall::Test4()
{
cout << m_iTest << endl;
}
那么以下的代码都正确吗?都会输出什么?
pNull->Test1(); // call 1
pNull->Test2(); // call 2
pNull->Test3(13); // call 3
pNull->Test4(); // call 4
你肯定会非常奇怪我为什么这么问。
一个值为NULL的指针怎么能够用来调用类的成员函数呢?。但是实事却非常让人惊讶:除了call 4那行代码以外,其余3个类成员函数的调用都是成功的。都能正确的输出结果。并且包括这3行代码的程序能非常好的执行。
经过细心的比較就能够发现,call 4那行代码跟其它3行代码的本质差别:类CNullPointCall的成员函数中用到了this指针。
对于类成员函数而言,并非一个对象相应一个单独的成员函数体,而是此类的全部对象共用这个成员函数体。 当程序被编译之后。此成员函数地址即已确定。而成员函数之所以能把属于此类的各个对象的数据差别开, 就是靠这个this指针。函数体内全部对类数据成员的訪问, 都会被转化为this->数据成员的方式。
而一个对象的this指针并非对象本身的一部分。不会影响sizeof(“对象”)的结果。this作用域是在类内部,当在类的非静态成员函数中訪问类的非静态成员的时候。编译器会自己主动将对象本身的地址作为一个隐含參数传递给函数。也就是说,即使你没有写上this指针。编译器在编译的时候也是加上this的。它作为非静态成员函数的隐含形參。对各成员的訪问均通过this进行。
对于上面的样例来说,this的值也就是pNull的值。也就是说this的值为NULL。
而Test1()是静态函数,编译器不会给它传递this指针,所以call 1那行代码能够正确调用(这里相当于CNullPointCall::Test1())。对于Test2()和Test3()两个成员函数,尽管编译器会给这两个函数传递this指针,可是它们并没有通过this指针来訪问类的成员变量,因此call 2和call 3两行代码能够正确调用;而对于成员函数Test4()要訪问类的成员变量,因此要使用this指针,这个时候发现this指针的值为NULL。就会造成程序的崩溃。
事实上,我们能够想象编译器把Test4()转换成例如以下的形式:
{
cout << this->m_iTest << endl;
}
而把call 4那行代码转换成了以下的形式:
所以会在通过this指针訪问m_iTest的时候造成程序的崩溃。
以下通过查看上面代码用VC 2005编译后的汇编代码来详解一下奇妙的this指针。
上面的C++代码编译生成的汇编代码是以下的形式:
0041171E mov dword ptr [pNull],0
pNull->Test1();
00411725 call CNullPointCall::Test1 (411069h)
pNull->Test2();
0041172A mov ecx,dword ptr [pNull]
0041172D call CNullPointCall::Test2 (4111E0h)
pNull->Test3(13);
00411732 push 0Dh
00411734 mov ecx,dword ptr [pNull]
00411737 call CNullPointCall::Test3 (41105Ah)
pNull->Test4();
0041173C mov ecx,dword ptr [pNull]
0041173F call CNullPointCall::Test4 (411032h)
通过比較静态函数Test1()和其它3个非静态函数调用所生成的的汇编代码能够看出:非静态函数调用之前都会把指向对象的指针pNull(也就是this指针)放到ecx寄存器中(mov ecx,dword ptr [pNull])。这就是this指针的特殊之处。看call 3那行C++代码的汇编代码就能够看到this指针跟一般的函数參数的差别:一般的函数參数是直接压入栈中(push 0Dh)。而this指针却被放到了ecx寄存器中。
在类的非成员函数中假设要用到类的成员变量,就能够通过訪问ecx寄存器来得到指向对象的this指针。然后再通过this指针加上成员变量的偏移量来找到对应的成员变量。
以下再通过另外一个样例来说明this指针是如何被传递到成员函数中和如何使用this来訪问成员变量的。
依旧是一个非常easy的类:
{
public:
void SetValue();
private:
int m_iValue1;
int m_iValue2;
};
void CTest::SetValue()
{
m_iValue1 = 13;
m_iValue2 = 13;
}
用例如以下的代码调用成员函数:
test.SetValue();
上面的C++代码的汇编代码为:
test.SetValue();
004117DC lea ecx,[test]
004117DF call CTest::SetValue (4111CCh)
相同的,首先把指向对象的指针放到ecx寄存器中;然后调用类CTest的成员函数SetValue()。
地址4111CCh那里存放的事实上就是一个转跳指令,转跳到成员函数SetValue()内部。
而411750h才是类CTest的成员函数SetValue()的地址。
{
00411750 push ebp
00411751 mov ebp,esp
00411753 sub esp,0CCh
00411759 push ebx
0041175A push esi
0041175B push edi
0041175C push ecx // 1
0041175D lea edi,[ebp-0CCh]
00411763 mov ecx,33h
00411768 mov eax,0CCCCCCCCh
0041176D rep stos dword ptr es:[edi]
0041176F pop ecx // 2
00411770 mov dword ptr [ebp-8],ecx // 3
m_iValue1 = 13;
00411773 mov eax,dword ptr [this] // 4
00411776 mov dword ptr [eax],0Dh // 5
m_iValue2 = 13;
0041177C mov eax,dword ptr [this] // 6
0041177F mov dword ptr [eax+4],0Dh // 7
}
00411786 pop edi
00411787 pop esi
00411788 pop ebx
00411789 mov esp,ebp
0041178B pop ebp
0041178C ret
以下对上面的汇编代码中的重点行进行分析:
1、将ecx寄存器中的值压栈,也就是把this指针压栈。
2、ecx寄存器出栈,也就是this指针出栈。
3、将ecx的值放到指定的地方,也就是this指针放到[ebp-8]内。
4、取this指针的值放入eax寄存器内。
此时,this指针指向test对象,test对象仅仅有两个int型的成员变量,在test对象内存中连续存放。也就是说this指针眼下指向m_iValue1。
5、给寄存器eax指向的地址赋值0Dh(十六进制的13)。事实上就是给成员变量m_iValue1赋值13。
6、同4。
7、给寄存器eax指向的地址加4的地址赋值。在4中已经说明,eax寄存器内存放的是this指针,而this指针指向连续存放的int型的成员变量m_iValue1。
this指针加4(sizeof(int))也就是成员变量m_iValue2的地址。
因此这一行就是给成员变量m_iValue2赋值。
通过上面的分析。我们能够从底层了解了C++中this指针的实现方法。
尽管不同的编译器会使用不同的处理方法。可是C++编译器必须遵守C++标准,因此对于this指针的实现应该都是几乎相同的。
为什么通过空指针(NULL)能够正确调用类的部分成员函数的更多相关文章
- 深入理解类成员函数的调用规则(理解成员函数的内存为什么不会反映在sizeof运算符上、类的静态绑定与动态绑定、虚函数表)
本文转载自:http://blog.51cto.com/9291927/2148695 总结: 一.成员函数的内存为什么不会反映在sizeof运算符上? 成员函数可以被看作是类 ...
- js里面如何才能让成员方法去调用类中其他成员
function fun(){ var _this = this; //如果函数是用var定义的私有函数,如下 var func1 = function(){ } //那么类中其他函数都可以直接通过f ...
- MFC 添加C++类,别的类不通过C++类的定义的对象就可以直接调用C++类里面的成员函数;
MFC 添加C++类,不用定义C++类的对象,别的类不通过C++类的定义的对象就可以直接调用C++类里面的成员函数: 1先在mfc程序中添加普通类CProdata,然后删除头文件Prodata.h里面 ...
- C++类的const成员函数、默认的构造函数、复制形参调用函数(转)
C++类的const成员函数 double Sales_item::avg_price() const { } const关键字表明这是一个const成员函数,它不可以修改Sales_item类的成员 ...
- CDocument类的UpdateAllViews()成员函数
(一)UpdateAllViews() 与 Invalidate()的区别 UpdateAllViews()是在DOC/VIEW结构中,当一个视图的数据改变后,通知所有视图作相应的改变,和重画毫无关系 ...
- C++(十六) — 类中引用成员函数、命名空间的使用
1.为什么类中引用成员函数? 类将属性和方法做了封装.类是一种数据类型,也就是:固定大小内存块的别名. 类的定义是一个抽象的概念,定义时不分配内存,当用类定义对象时,才分配一个固定大小的内存块. 此时 ...
- C++类的成员函数(在类外定义成员函数、inline成员函数)
类的成员函数(简称类函数)是函数的一种,它的用法和作用和前面介绍过的函数基本上是一样的,它也有返回值和函数类型,它与一般函数的区别只是:它是属于一个类的成员,出现在类体中.它可以被指定为private ...
- const当做标记的函数重载,但是仅仅是限于类里面的成员函数
(1)我们知道函数的重载时根据函数的参数类型以及函数参数个数来重载的,不能用函数返回值来重载函数.但是有时候函数参数个数和函数参数类型重载函数会和默认参数发生冲突: int fun(int i,cha ...
- 类1(this指针/const成员函数/类作用域/外部成员函数/返回this对象的函数)
假设我们要设计一个包含以下操作的 Sales_data 类: 1.一个 isbn 成员函数,用于返回对象的 book_no 成员变量 2.一个 combine 成员函数,用于将一个 Sales_dat ...
随机推荐
- js中由undefined说起
typeof()函数 返回的是字符串.有六种可能:"number"."string"."boolean"."object" ...
- iOS定制改动navigation的backbutton
iOS开发中.navigation的返回button是英文"back".想改动成中文"返回"或者自己定义的文字.这么简单的事情却折腾了小半个小时.原来是被lef ...
- easyUI Tab href,content差别
easyUI的Tab面板是继承了panel Tab中js的两种使用方法: 说明:jsp主页面加入一个id为tab的div,要引入easyUI的相关js.css <div data-option ...
- python-打开网页
最近一直想通过python来实现网页的操作.因为想把自己vimrc的配置直接通过脚本来实现.比较理想的情况下,就是通过一个脚本,把自己需要的一些资源直接从网上下载下来,然后再对下载的资源进行安装等操作 ...
- SQL查询表中的用那些索引
方法1. 使用系统表 -- 查询一个表中的索引及索引列 USE AdventureWorks2008 GO SELECT indexname = a.name , tablename = c. n ...
- css3新特性选择器(补充)
1.选择p标签中的第一个字符 p:first-letter{ color:red; font-size:25px; } 2.选择p标签中的第一行 p:first-line{ color:red; fo ...
- LuoguP2756 飞行员配对方案问题(最大流)
题目背景 第二次世界大战时期.. 题目描述 英国皇家空军从沦陷国征募了大量外籍飞行员.由皇家空军派出的每一架飞机都需要配备在航行技能和语言上能互相配合的2 名飞行员,其中1 名是英国飞行员,另1名是外 ...
- SpringMVC框架中的异常解析器-ExceptionHandler和HandlerExceptionResolver
SpringMVC框架中,处理异常还是挺方便的,提供了一个异常解析器. 处理局部异常 @Controller public class AccessController { /** * 处理这个Con ...
- js31---观察者模式
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/stri ...
- js全选反选按钮实现
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" ...