`
soboer
  • 浏览: 1303123 次
文章分类
社区版块
存档分类
最新评论

【C/C++语言入门篇】-- 深入指针

阅读更多

再上一篇,我们介绍了基本调试。之前也说了,之所以把调试放在前面讲是因为后面的文章基本都会用到调试。观察我们的程序到底发生了什么。让我们能够直接明了的看清楚问题的本质。本篇将深入一点介绍指针这个让无数初学者畏惧的东西。希望大家再看完本篇之后能对指针有新的认识,之后不再惧怕它。觉得它就那么回事。那下面我们就努力攻克这个令我们“惧怕”的东西。

我们可能进入大学读计算机相关专业,基本第一门编程语言就是C语言。可能老师们也喜欢跟学生总结整本书难点在什么地方。那么指针必然是老师提到的难点之一。我个人觉得这样的总结还不如不总结,原因很简单,因为这样会给学生心理负担,学到指针的时候那根弦都崩的很紧。从骨子里就认定了它有难度,初学者脆弱的心灵因此而感到惧怕。换个角度,为什么我们不能觉得指针也就那么回事?没有什么特别的嘛,哪里难了嘛!这样不是既有信心又有兴趣去搞定它?说了这么多,只想强调一点,什么东西都报怀疑态度未必是件坏事。指针不是老师说的那么恐怖。好了,下面我们就系统的从几个角度去理解指针。

概念上理解 所谓指针,没学过编程语言的可能会觉得是指南针或者鼠标的指针。呵呵,这种说法虽然差之千里,但是也不是毫无道理。为什么呢?比如指南针,以C语言指针的角度去思考,那么指南针之所以叫指南针因为它始终是指向南方的。对!南方,顿时恍然大悟。联系起来可以想象成:指南针就是指针变量,它指向南方。南方即是指南针这个变量的值。那么 指南针(指针) == 南方(这里的==可以理解成if( a == 100)里面的比较运算,下文同理)。此时我们又发现南方有座大山,大山在南方。哇,又恍然大悟。那这么说来大山就生在南方,假如我们想象南方就是内存的某个地址单元。大山就是这个地址单元的值。因此又有等式:*指南针 == 大山。

问题一:这里多了个星号是为什么?(看完后面我希望你能答出这个问题)

再来,我们就傻瓜的认为指针就是我们常用的鼠标在桌面熟悉的那个箭头。我们的箭头在我们的控制下,我们想点哪儿就点哪儿。哈哈,如此神奇。例如我们想点桌面的“记事本”图标。于是我们将箭头指向那个图标,然后双击。便打开了我们以前留下的一些记事。我们就能看到了。从这个简单的操作又可以让我们产生联想了。箭头就好比我们程序里面的指针,我们在想要打开记事本的时候,就箭头指向它。在这个时候,箭头指向了记事本。箭头(指针)== 记事本。在双击打开记事本之后,里面有内容,比如是:“我爱你!”。内容在记事本里面。那么这些内容就可以理解成是记事本里面存放的值,只不过这些值是以字符串的形式存储在里面的。因此有表达式:*箭头 == “我爱你”。

上面举了两个比较形象一点的例子,相信大家脑子里有一个基本的关系链了吧。就是 指针----->地方----->东西。也就是某个地方有个东西。这个地方被一张藏宝图记录下来放到一个隐秘的地方了。这张藏宝图就是所谓的指针,指明我们想要找的宝贝的那个地方。清楚了形象的意思,再来句专业的形象一点的概念:指针就是指向某个内存地址的一个变量,这个变量的值就是存放的这个内存地址。这个内存地址里面又存放了我们的数据。这样就构成了一个关系。我们可以方便的通过指针找到该地址并且向这个地址写入数据或者读取该地址的值。比较直接的读写方式就是赋值。

用法上理解 在前面了解了指针的概念,相信大家已经迫不及待想看到直接的代码了。很正常,程序员就喜欢最直接了当贴出代码。但是假如我们没有理解到他的意义,贴出的代码可能你也只是粗略的一看。认为自己已经清楚不已,这样往往会漏掉不少细节。也会少很多精彩。

指针也是变量(大家不可能不知道什么是变量吧,如果不知道自己拍自己砖头,然后去google)。不要因为它多了个星号就觉得它很特别,它就是个变量而已。因为是变量,那么指针也就有类型了,这个类型可以理解成就是指针的类型。它是某个类型就只能在语法上赋值为这种类型的值。也可以指针的类型理解成是他指向内存的地址里面存放的数据的类型,比如:

int*p; // 它就表示指向的内存地址里面存放的是int类型的值。

int a = 100;

p = &a;// 知道了吧,简单的赋值操作就让p指向了a所在的内存地址,在CC++语言里用&去某个变量的内存地址。简单的语句,咱们干了不少事,我们将变量a的内存地址取了出来,然后赋值给了指针p。是不是我们的对应关系:

指南针 南方 大山

鼠标箭头 记事本存放的记事

藏宝图 大山 宝贝

指针 内存地址 数据值

p &a a(100金币)

根据上面的对应关系大家更清楚了吧,既然指针有类型,那么我们就多看一些类型:

char* pName = "masefee"; // 这一句,是一个字符类型的指针。既然我们说的指针是存放的内存数据的地址,那么这里的就是后面"masefee"字符串的首地址,何为首地址?首先我们知道这个字符串肯定是存放在内存里面的,然后我们又知道内存的最小存储单元是字节,既然是字节,那么这个字符串总共占用的字节数就是8字节。7个字母加一个结束符'/0'一共8个。大家又会问了,为什么需要结束符?原因很简单,我们这个pName指针只指向了这个字符串的起始地址(首地址),它并不知道这个串有多长。假如没有结束符,我们取这个字符串的时候怎么知道取到哪里为止呢?因此'/0'就专门用来结束,这个也是个字符,在内存里面存放的值就是数字0.意思就是说'/0'字符的ASCII码就是0。扯远了,前面说了一共占8字节,内存中不可能在同一地址下存放8个字节哟。一个内存地址只能存放一个字节。所以这个"masefee"就占用了8个内存地址,首地址就是m字符的地址。大家可以将pName选中拖放到内存窗口的地址栏观察。形如:

0x0012fed4 m 首地址,pName的值就等于0x0012fed4这个地址值。

0x0012fed5 a

0x0012fed6s

0x0012fed7 e

0x0012fed8 f

0x0012fed9 e

0x0012feda e

0x0012fedb 0

上面就是数据在内存里面存放的位置关系,逐字节存储。这里注意,在内存里面通常我们看到的是16进制。这里只是为了更形象直接写成字符了。

short level = 2500;

short* pLevel = &level;

这两句相信大家也不难理解了,pLevel指向的就是level所在的内存地址。先看看在内存中的存放关系:

0x0012fed40xC4

0x0012fed5 0x09

这里只有2行是因为short只占2字节(16位)。那为什么奇怪的变成0xc4 0x09呢? 再观察0x0012fed4比0x0012fed5小,我们知道2500的十六进制数十0x09C4。这里为什么倒过来存放的呢? 高位存放在了后面,低位存放在了前面?原因是因为CPU,有的CPU是顺着存放有的是倒过来存放的。这里我们不追究,记住就可以了。这样一来,pLevel的值到底是哪个地址呢?答案很简单,小的那个。也可以理解成首地址。从小的地址往大的地址读取是人之常情撒。那为什么没有向字符串那样有结束符呢?原因也很简单,short我们是知道长度的,不需要结束符。这里记住一点,我们将一个大于1字节的基本数据类型变量(int short等)的地址赋值给指针后,该指针指向的地址我们可以将该变量理解成只在一个字节上(地址上)。该指针指的内存地址里面的值就是该变量值。当然你也可以就根据他的存储占用字节,理解该指针就是指向的这几个字节的首字节。另外,如果level的值不够占用2字节,另外一个字节就会被自动填充0。这里我们要取pLevel指向的地址里面的值可以用语句:

short lv = *pLevel; // *就表示间接访问该指针所指向的内存里面的值,这是CC++语法,就是这么规定的,后台处理就别管了。知道取指针所指向的内存地址里面的值的方式就是在前面搞一个星号,称之为间接访问。这样取出来后,lv的值就等于level的值:2500.

char className[5] = { 'M', 'a', 's', 'e', '/0'};

char* pClassName = className;

问题二: pClassName所指向的地址是数组里面哪一个字符的地址?这里为什么直接赋值没有加&符号?

int array[ 3] = { 1, 2, 3};

int* pArray = array;

这两句在问题二解决之后自然就会了,这里需要注意的是,int是4字节,占用的内存地址将有4个,假如该int变量值很小占用不到4个字节,剩余字节将填充为0.

本质上理解 谈到本质,指针可以说就是地址。为什么?因为他的值就是某个变量的所在内存地址。因为我们通常使用的电脑是32位机,那么我们每个字节的地址就占用4个字节,地址是16进制数,是整数。所以任何指针存放的值都是一个整数。所以没有特殊的情况(这种情况我们基本碰不到,这里就不说了)我们任何一种类型的指针都占用4字节,因为它存放的是32位整数。因此前面我们定义声明指针如:

int* p; char* p; 这里的int,char并不是代表指针本身的类型,而是指向的地址里面的值的类型。这里的p存放的就是一个32位的整数,你可以大胆的认为它就是一无符号整数。只不过这个无符号的整数是具有地址信息。

那么有的同学就会疑问了,既然是存放的一个整数,那么这个指针又是被存放在哪儿的呢?前面不是说了吗?我们指针也是变量,那么指针也就有它自己的内存地址,也就是用来存放这个指针的。比如:

int* p = &a;

int** pp = &p;

这里就不得不说说二级指针了,其实也没有什么特别的,二级指针名义上就是指针的指针。二级指针存放的是存放一级指针的那个内存地址。就好比:我的抽屉里---->藏宝图--->大山---->宝贝。这里的抽屉就是二级指针。藏宝图也还是要放到一个地方藏起来撒。不然都发财了。对吧!

谈到本质,指针既然是存放的整数,那么我们可以大胆的强制类型转换:

int a =100;

int* p = &a;

unsigned int addr = ( unsigned int )p; // 将变量p强制类型转换成无符号整数,此时它就是一个实实在在的整数了,这个整数就是变量a的内存地址了。是不是更加觉得指针也就那么回事了?呵呵,我们继续。

说到这里,我们不得不说说我们前面提到的void类型了,之前我们将void通常用来表示函数没有返回值。在这里,我们将了解到它的另一面。那就是:

void* p; // 无明确指向类型的指针,意思就是p并不知道他所指向的内存地址里面的数据是什么类型。看具体用法:

int* pInt;

p = ( void*)pInt;

这里的int型指针被强制转换成无类型的,既然是无类型,那么就不可能使用:

int var = *p;

这样加上星号,我们前面已经说了是间接访问p所指向的内存地址下面的值,这里p是存放了内存地址没错。但是这里是无类型的,我们的p就不知道到底该读取多少个字节到var变量里。可能有的朋友又会说那假如有结束符呢? 呵呵,也是不行的,既然是无类型,怎么能乱下结论一定是有结束符的数据类型呢?因此在C++语法上面是不允许间接访问void*指针的。如果想读取这个地址里面的值,那么此时就相当灵活了。你可以把这个void型指针强制类型转换成任何类型的指针。然后赋值过去。这样做是危险的,但是有时也是必须的。你在这样做的时候得保证这个void指针强制赋值过去后,你的数据是没有问题的。

假如:

int a = 0x7fffffff;

int* pA = &a;

void* p = ( void*)pA;

short b = *( short*)p; // 这里是先强制类型转换,然后再间接访问值,所以是*( short*).

这时b的值将会把a的值进行截断,因为short的范围比int的范围小。这样数据就会出问题。这里清楚了吧。

说到这里又不得不说说空指针和野指针了,空指针既然叫空指针,他的意思就是这个指针所存放的内存地址为0,不如:

int* a = 0; 指针a所存的地址就是0x00000000,这个地址专门用于指针初始化或者清零,内存的这个地址是被保护起来的,既不能往里面写数据也不能读里面的数据。如果试图如:

int*pNULL = 0;

int var = *pNULL;

这样将出错,出错信息是:

什么什么.exe的什么什么地方 未处理的异常: 0xC0000005: 读取位置 0x00000000 时发生访问冲突。

如果试着写:

*pNULL = 100;

将出错:

什么什么.exe的什么什么地方 未处理的异常: 0xC0000005: 写入位置 0x00000000 时发生访问冲突 。

所以,我们很多时候不能保证某个指针是否正常不为空指针时就得加以判断:

if ( pNULL)

*pNULL = 100;

这样程序就不会给空指针赋值了,读取一个道理。以后讲函数的时候会进一步讲解空指针的防范。

另外就是野指针,所谓野指针就跟野猪一个道理,到处乱跑。野指针就是指向了不该指向的内存地址,假如这个内存地址不可写或者不可读,我们的程序将会崩溃。假如这个内存没有被保护,读写都可以的话,这样的错误将很难找到。数据将会出错。会出现很多莫名其妙的现象;很多时候会因为越界刚好修改了某个指针的值,这样指向的内存地址就有可能不是我们想要的地址就造成了野指针。函数那一篇我们也将详细讲解野指针的避免。因为在函数里面讲野指针将更具体直观。还是举个野指针的例子吧:

int* p = &a; // 正常

p = (int*)0x12345678; // 这句不要奇怪,既然指针能够转换为整数,整数同样可以转换为指针,这里转换过后,p的值就等于0x12345678;这个内存地址并不是我们想要的,也很可能是非法的。这里的p就野了。也就是野指针。

这里再简单说说数据拼接,假如我有一个short类型的数组:

short a[ 2] = { 0xffff, 0xeeee};

short* pA = a;

这样一来,pA[ 0] 就是0xffff,pA[ 1 ]就是0xeeee。再有一个:

int* pInt = ( int*)pA; 将pA转换为int*(指针之间可以随便转换,只要确保不出错,前面已经说过),short*变成了int*。我们知道2个short刚好等于一个int所占的内存。然后我们操作这个pInt:

int var = *pInt; 这里也可以直接使用:int var = *( int*)pA; 那么此时var取出来的就是4个字节的内存数据,我们知道short数组a有2个元素刚好占用4个字节,而且数组是连续存放的。那么var将是这两个元素的组合值。

问题三:你们的电脑里组合出来的var的值是什么?

问题四:假如a数组有4个元素,然后转化成int*的pInt,那么var有几个?该怎么获取?

好了,不知不觉写了整整接近4小时了,这篇终于差不多了。累!这篇较之以前的文章要更深入一点,所以篇幅比较大一些。希望不懂的朋友多读几遍,理解其中的意思。相信你们的悟性!呵呵。打算睡觉了。Good morning!

四个问题一定得好好思考哟!

【C/C++入门篇系列】

【C/C++语言入门篇】-- 序言

【C/C++语言入门篇】-- HelloWorld思考

【C/C++语言入门篇】-- 基本数据类型

【C/C++语言入门篇】-- 调试基础

【C/C++语言入门篇】-- 深入指针

【C/C++语言入门篇】-- 数组与指针

【C/C++语言入门篇】-- 结构体

【C/C++语言入门篇】-- 深入函数

【C/C++语言入门篇】-- 位运算

【C/C++语言入门篇】-- 剖析浮点数

【C/C++语言入门篇】-- 文件操作

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics