【原创】零值指针指向何处?

我啃 2006-10-22 12:34:03
仓促中赶出来的,还没有贴到BLOG上,拿出来给大家批斗,欢迎批评
------------------------------------------------------------------------------------
零值指针指向何处?
by Kenmark 2006-10-15 21:58
在阅读本文之前,希望读者对WINDOWS下程序的运行方式以及内存管理机制有基本的了解。
一、NULL指针和零值指针(null pointer and zero value pointer)
我们查看一下C++标准库定义的NULL指针
// Define NULL pointer value
#ifndef NULL
# ifdef __cplusplus
# define NULL 0
# else
# define NULL ((void *)0)
# endif
#endif // NULL
也就是说,NULL是一个宏,在C++里面被直接被定义成了整数立即数类型的0,而在没有__cplusplus定义的前提下,就被定义成一个值是0的void *类型指针常量,因此很多程序员就会认为零值指针就是NULL(空)指针。
这种认识是错误的!
零值指针,如其名,是值是0的指针,可以是任何一种指针类型,可以是通用变体类型void *也可以是char *,int *等等
空指针,其实空指针只是一种编程概念,就如一个容器可能有空和非空两种基本状态,而在非空时可能里面存储了一个数值是0,因此空指针是人为认为的指针不提供任何地址讯息,类似于container.empty()。
因此,零值指针和空指针完全是两种不同的概念,所以本文题目是“零值指针指向何处”而非“空指针指向何处”,如果出现“空指针指向何处”的问题,就像在问“空的盒子里面有什么”一样荒谬。
那么为为什么标准里面会这样定义空指针呢?
由于C++里面,任何一个概念都要以一种语言内存公认的形式表现出来,例如std::vector会提供一个empty()子函数来返回容器是否为空,然而对于一个基本数值类型(或者说只是一个类似整数类型的类型)我们不可能将其抽象成一个类(当然除了auto_ptr等只能指针)来提供其详细的状态说明,所以我们需要一个特殊值来最为这种状态的表现。
C++标准同一公认地规定,当一个指针类型的数值是0时,认为这个指针是空的。(我们在其他的标准下或许可以使用其他的特殊值来定义我们需要的NULL实现,可以是1,可以是2,是随实现要求而定的,但是在标准C++下面我们用0来实现NULL指针)
标准的原版是这么写的:
[6.3.2.3-3] An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.
[6.3.2.3-3] If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.所以在标准里面我们是用一个特殊值来代替这种概念而不是用值来定义这个概念。
所以我们还可以认为C++ STANDARD里面定义的NULL指针就是零指针,它可以是0,可以是0*17,甚至可以是'\0',C++标准没有一定要求其被转换成指针类型,只要其代表的数值是0,赋值后就被认为是NULL的。至于系统选取哪种形式作为空指针常量使用,则是实现相关的。一般的 C 系统选择 (void*)0 或者 0 的居多(也有个别的选择 0L);至于 C++ 系统,由于存在严格的类型转化的要求,void* 不能象 C 中那样自由转换为其它指针类型,所以通常选 0 作为空指针常量,而不选择 (void*)0。
相关的概念还可以看到:
[6.3.2.3-Footnote] The macro NULL is defined in <stddef.h> (and other headers) as a null pointer constant
二、对空指针实现的保护政策
既然我们选择了0作为空的概念,在非法访问空的时候我们需要保护以及报错。因此,编译器和系统提供了很好的政策。
我们程序中的指针其实是WINDOWS内存段偏移后的地址,而不是实际的物理地址,所以不同的我们程序的程序中的零值指针指向的同一个0的地址,其实在内存中都不是物理内存的开端的0,是分段的内存的开端,这里我们需要简单介绍一下WINDOWS下的内存分配和管理制度:
WINDOWS下,执行文件(PE文件)在被调用后,系统会分配给它一个额定大小的内存段用于映射这个程序的所有内容(就是磁盘上的内容)并且为这个段进行新的偏移计算,也就是说我们的程序中访问的所有NEAR指针都是在我们“自家”的段里面的,当我们要访问FAR指针的时候,我们其实是跳出了“自家的院子”到了他人的地方,我们需要一个段偏移地址来完成新的偏移(人家家里的偏移)所以我们的指针可能是OE02:0045就是告诉系统我们要访问0E02个内存段的0045好偏移,然后WINDOWS会自动给我们找到0E02段的开始偏移,然后为我们计算真实的物理地址。
所以程序A中的零值指针和程序B中的零值指针指向的地方可能是完全不同的。
保护政策:
我们的程序在使用的是系统给定的一个段,我们程序中的零值指针指向这个段的开端,为了保证NULL概念,系统为我们这个段的开头64K内存做了苛刻的规定,根据虚拟内存访问权限控制,我们程序中(低访问权限)访问要求高访问权限的这64K内存被视作是不容许的所以会必然引发 Access Volitation 错误,而这高权限的64K内存是一块保留内存(即不能被程序动态内存分配器分配,不能被访问,也不能被使用),就是简单的保留,不作任何使用。
三、关于NULL指针的其他
1)指针初始化
我们在直接定义一个指针后我们并不知道这个指针指向何处(而不是有些程序员认为的如同JAVA等语言会自动零值初始化)所以我们一旦非法地直接访问这些未知地内容时,极其有可能会触碰到我们程序所不能触碰地内存(这时类似64K限制地保护政策又会起效,就如同你不仅随意闯入了陌生人的家(野指针),而且拿着刀子要问他要钱(访问),警察(WINDOWS内存访问保护政策)当然请你去警察局(报错)谈谈),所以养成良好的指针初始化(赋值为NULL)以及使用FREE(或者时DELETE)之后立即再初始化为空是十分必要的!
2)malloc 函数在分配内存失败时返回 0 还是 NULL?
不同于C++里面的NEW再内存失败是会抛出一个BAD_ALLOC异常,malloc 函数是标准 C 规定的库函数。在标准中明确规定了在其内存分配失败时返回的是一个 “null pointer”(空指针):
[7.20.3-1] If the space cannot be allocated, a null pointer is returned.
对于空指针值,而非零值指针!
四、后记
看了本文后希望你:
1)了解了空指针和零值指针的区别
2)了解了我们C++标准里面实现空指针的方法
3)了解了初步的WINDOWS内存保护政策

如果对本文有什么意见或者是建议,非常欢迎您给我指点:
E-Mail: ken.mingyuan@gmail.com mingyuan@hotmail.com
QQ: 188916915
CSDN PASSPORT: Kenmark
...全文
566 17 打赏 收藏 转发到动态 举报
写回复
用AI写文章
17 条回复
切换为时间正序
请发表友善的回复…
发表回复
li01bin 2006-10-31
  • 打赏
  • 举报
回复
windows编程的bible windows高级编程,我估计真的没有几个人认认真真的看过这本书,不过确实是挺厚的 而且难度很大。
说到这里,我向大家推荐bible级的基本书,我是真的拜读过之后才向大家推荐的。
windows核心编程 超爽
探索c++对象模型 从编译器角度解释oop的实现
linux高级编程 只有这一本
OOPhaisky 2006-10-29
  • 打赏
  • 举报
回复
支持原创
但是其中有些观点与作者不同,待我消化消化
fflush 2006-10-29
  • 打赏
  • 举报
回复
2. 你在关于指针初始化的说明中,谈到为了避免访问到64k的限制而应该初始化,我认为之所以要初始化,更多的是因为我们不想因为没有初始化而返问到一些随机的地址,产生一些未知的结果。
fflush 2006-10-29
  • 打赏
  • 举报
回复
仔细看了一下,基本上就是说空指针更多的是一个概念,表示该指针哪儿也不指,而零值指针是一个指针,他的值是0。从实现上来讲,空指针并不需要一定是零指针,但是实现为零指针可能是最方便的一种方法,所以现在的os都是这么干的。基本上,同意lz的观点,虽然不清楚强调这样一个观点有多大的必要。关于文章本身,可能有这样一些问题,供参考

1. near和far这是很久以前的东西了,现在已经没有这种区别,反正我不是很清楚,所以也不知道你关于near和far的说法是否正确。我所知道的是,在windows(any version)下,一个进程是不可能访问到其他进程的地址空间的

2.
我啃 2006-10-29
  • 打赏
  • 举报
回复
OOPhaisky(异化$渴望成功~~)
有何异议?非常期待!
wang430903 2006-10-29
  • 打赏
  • 举报
回复
支持
xiaojun789 2006-10-29
  • 打赏
  • 举报
回复
ding
Tycool 2006-10-28
  • 打赏
  • 举报
回复
这个高了,呵呵
todototry 2006-10-28
  • 打赏
  • 举报
回复
mark
我啃 2006-10-28
  • 打赏
  • 举报
回复
随手乱写的,希望大家多给意见,否则怕以后遗臭万年,说又给世界上添了一篇垃圾,:)
whatsblog 2006-10-28
  • 打赏
  • 举报
回复
mark
laiwusheng 2006-10-28
  • 打赏
  • 举报
回复
mark
飞哥 2006-10-28
  • 打赏
  • 举报
回复
路过
支持~
FaKeChineseCompany 2006-10-28
  • 打赏
  • 举报
回复
不给分就不告诉你……
jixingzhong 2006-10-28
  • 打赏
  • 举报
回复
^_^
chenhu_doc 2006-10-22
  • 打赏
  • 举报
回复
仔细看看。
Jokar 2006-10-22
  • 打赏
  • 举报
回复
纯支持:)

另外:关于我们程序里访问near,far指针的那个例子可能不是很好~
还有:明确说明 进程虚拟地址空间的概念 可能表达和理解都更容易些~
定义类 •定义类的语法格式: •[修饰符] class 类名 {….类体…..} •类体可以由多个属性、方法、构造器组成。 •注意:类的修饰符可以是public、final 或省略这两个。类名只要是合法的标识符. •一个类里可以包含三种最常见的成员:构造器、属性、方法。 定义属性的格式语法 •[修饰符] 属性类型 属性名 [= 默认值]; 定义方法的格式语法 •[修饰符] 方法返回值类型 方法名(形参列表) {….方法体….} – 方法中的修饰符可以是public ,protected,private,static,final,abstract,其中访问控制符只能出现一 个,abstract ,final 只能出现其一. –返回值类型可以是基本类型和引用类型,如果无返回值,要用void 来声明 –形参列表,可以由零到多组组成,参数之间用逗号(“,”)隔开. –static 是一个特殊的关键字,译为”静态”,所有有static 修饰的叫类属性,类方法,类成员. 定义构造器的语法格式 •[修饰符] 构造器名(形参列表) {……} –修饰符可以是public protected private 构造器必须和类名相同,形参和方法的形参一样. 对象的产生和使用 •创建对象的根本途径就是构造器,所以创建对象通过关键字new 加上对应的构造器即可. •如果访问权限允许,类里定义的属性和方法可以通过类或实例来调用, •有static 修饰的访求和属性,既可通过类来调用,也可以通过实例来调用. 对象.引用和指针 •Person p = new Person();,这行代码创建了一个Person 实例,也被称为对象,这个对象被赋给了p变量. •也就是说引用型变量里存放的仅仅是一个引用,它指向实际的对象. 对象的this 引用 •this 关键字总是指向调用该方法的对象.   –1.构造器中引用该构造器执行初始化的对象   –2.在方法中引用调用该方法的对象 •在方法里的this 指谁调用就指谁 •注: this 不能用在有static 修饰的方法中. 方法的详解 •方法的所属性   –一旦将一个方法定义在一个类里,如果用static 修饰了,这个方法属于这个类,否则属于这个类的对象.   –方法不能独立执行,必须要有调用者.(如:类.方法、对象.方法)   –方法不能独立定义,只能定义在类里.   –方法要么属于一个类,要么属于一个对象 方法的参数传递机制 传递方式只有一种:值传递. 形参长度可变的方法 •如果在定义方法时,在最后一个参数的类型后增加三点…,则表明该形参接受多个参数值,多个参数值被当成数组传   入. •长度可变的形参只能位于最后一个参数,并一个方法里只能有一个可变长度的参数. 递归 •递归就是在方法中再次调用自己。 •递归一定要向已知方向递归. 方法的重载 •Java 允许在一个类里定义多个同名方法,只要形参列表不同即可. •所以方法的重载只要满足两个条件(两同一不同):1.同一个类中,方法名相同;2.形参不同。和返回值类型无关.   所以在调用这些方法时要传入不同的参数值. 成员变量和局部变量 •成员变量指的是在类范围里定义的变量;局部变量指的是在一个方法内定义的变量。 •不管是成员变量还是局部变量都遵守相同的命名规则。 •成员变量分为类属性和实例属性.对于类属性无论通过类还是对象来访问都是访问同一个对象,只要一个对象改变 了类属性,那么其他对象对应的类属性也改变了.成员变量不用显式初始化,只要定义了一个类属性或实例属性,系统 默认进行初始化。 局部变量 •局部变量可分为三种:   –形参   –方法局部变量   –代码块局部变量. •与成员变量不同的是除了形参外,其他局部变量都必须显式地初始化, •Java 里允许局部变量和成员变量重名。这样局部变量会覆盖成员变量,这时通过this 来调用实例的属性. 成员变量的初始化 •当类被加载时,类成员就在内存中分配了一块空间。 •当对象被创建时,实例成员就在内存中分配了内存空间。 •实例变量与实例共存亡;类变量与类本身共存亡。 局部变量的运行机制 •局部变量仅在方法内有效。 •当方法执行完成时,局部变量便会自动销毁。 封装 •理解封装:封装是面向对象的三大特征之一。 • 封装包含两方面含义:   –合理隐藏。   –合理暴露。 本文原创作者:pipi-changing 本文原创出处:http://www.cnblogs.com/pipi-changing/ 使用访问控制符 •private 私有的。在同一个类里能被访问。 •default 默认的。包访问权限 •protected 受保护的。子类中也能访问 •public 公共的。在任何地方都可以访问 package 和 import •package 打包格式:package 包名;放在程序开始的顶端。 •包机制的两个方面的保证。1.源文件里要使用package 语句指定包。2.class 文件必须放在对应的路径下。 •import 引入包格式。分为两种:   –非静态导入,导入的是包下所有的类。如:import package.subpackage.*;   –静态导入,导入的是类的静态属性。如:import static package.className.*; Java 的常用包 •java.lang.*, •java.util.*, •java.net.* , •java.io.*, •java.text.*, •java.sql.*, •java.awt.*, •java.swing.*. 使用构造器执行初始化 •构造器最大的用处就是在创建对象时执行初始化,系统会默认的进行初始化。 •如果程序员没有Java 类提供任何构造器,则系统会为这个类提供一个无参的构造器。 •一旦程序员提供了自定义的构造器,遇系统不再提供默认的构造器。 构造器的重载 •构造器的重载和方法的重载一样,都是方法名相同,形参列表不相同。 •在构造器中可通过this来调用另外一个重载的构造器。 继承的特点 •Java通过关键字extends来实现,实现继承的类称为子类,被继承的类称为基类、超类、父类。父类是大类,子   类是小类。 •Java的继承是单继承,每个子类最多只有一个直接父类。 本文原创作者:pipi-changing 本文原创出处:http://www.cnblogs.com/pipi-changing/ 继承 •子类继承父类的语法格式如下: •修饰符 class subclass extends superclass {。。。。。。} •子类扩展了父类,将可以获得父类的全部属性和方法,但不能获得父类构造器 •Java里的继承都是单继承,也就是只能有一个直接的父类,可以有n个间接父类。 重写父类的方法 •方法的重写要遵循“两同两小一大” 指的是:方法名相同,形参列表相同。返回值类型更小或相同,抛出的异常更   小或相同,访问控制权限要更大。 父类实例的super限定 •通过关键字super 来调用父类的方法或属性。 super调用父类的构造器 •子类构造器总会调用父类构造器。 •如果子类构造器没有显式使用super调用父类构造器;子类构造器默认会调用父类无参数的构造器。 •创建一个子类实例时,总会先调用最顶层父类的构造器。 多态性 •Java 引用变量有两个类型:一个是编译时的类型,一个是运行时的类型,编译时的类型由声明该变量时使用的类   型决定,运行时的类型由实际赋给该变量的对象决定。 •如果编译时类型和支行时的类型不一致,这就有可能出现所谓的多态。 •两个相同类型的引用变量,由于它们实际引用的对象的类型不同,当它们调用同名方式时,可能呈现出多种行为   特征,这就是多态。 引用变量的类型转换 •将一个子类对像赋给父类,这就是向上转型。向上转型是自动的。 •强制类型转换: 类型转换运算符是小括号,语法如下(type)variable; instanceof运算符 •前一个操作通常是一个引用类型的变量,后一个操作通常是一个类(也可以是接   口)。如果是 返回true, 否返回false。 继承和组合 •使用继承的注意点 •利用组合实现复用:总之,继承要表达的是一种“是(is-a)”的关系,而组合表达的是”有(has-a)“的关系。 本文原创作者:pipi-changing 本文原创出处:http://www.cnblogs.com/pipi-changing/ 初始化块 •使用初始化块:初始化块由大括号括起{…},只能由static修饰。 •初始化块和构造器:初始化块总在构造器之前执行,初始化块不能接受形参。 静态初始化块 •用static修饰的初始化块为静态初始化块,由于是静态的,所以是属于类,当类加载时,就执行静态初始化块 ,   但执行一个子类时,最先执行其最顶层父类的静态初始化, •初始化块是属于实例 的。只要创建一次对象,初始化块就执行一次。 下面贴出代码: Dog DogTest Person PersonTest ReturnThis StaticAccessNonStatic ThisInConstructor Overload OverloadVarargs PrimitiveTransferTest Recursive ReferenceTransferTest Varargs 复制代码 public class BlockTest { public static void main(String[] args) { { // 定义一个代码块局部变量a int a; // 下面代码将出现错误,因为a变量还未初始化 // System.out.println("代码块局部变量a的值:" + a); // 为a变量赋初始值,也就是进行初始化 a = 5; System.out.println("代码块局部变量a的值:" + a); } // 下面试图访问的a变量并不存在 // System.out.println(a); } // 代码块局部变量a的值:5 } 复制代码 。。。。。。。。。。。。。。。。。。。

64,662

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧