C语言 指针

1.变量的内存实质

要理解 C 指针,我认为一定要理解 C 中“变量”的存储实质,所以我就从 “变量”这个东西开始讲起吧! 先来理解理解内存空间吧!请看下图:

如上图所示,内存只不过是一个存放数据的空间,就好像我的看电影时的电 影院中的座位一样。电影院中的每个座位都要编号,而我们的内存要存放各种各 样的数据,当然我们要知道我们的这些数据存放在什么位置吧!所以内存也要象 座位一样进行编号了,这就是我们所说的内存编址。座位可以是遵循“一个座位 对应一个号码”的原则,从“第 1 号”开始编号。而内存则是按一个字节接着一 个字节的次序进行编址,如上图所示。每个字节都有个编号,我们称之为内存地 址。好了,我说了这么多,现在你能理解内存空间这个概念吗?

我们继续看看以下的 C/C++语言变量声明:

int i;

char a;

每次我们要使用某变量时都要事先这样声明它,它其实是内存中申请了一个 名为 i 的整型变量宽度的空间(DOS 下的 16 位编程中其宽度为 2 个字节),和 一个名为 a 的字符型变量宽度的空间(占 1 个字节)。

我们又如何来理解变量是如何存在的呢。当我们如下声明变量时: int i; char a; 内存中的映象可能如下图:

图中可看出,i 在内存起始地址为 6 上申请了两个字节的空间(我这里假设 了 int 的宽度为 16 位,不同系统中 int 的宽度可能是不一样的),并命名为 i。 a 在内存地址为 8 上申请了一字节的空间,并命名为 a。这样我们就有两个不同 类型的变量了。

2.赋值给变量

再看下面赋值: i = 30; a = ’t’; 你当然知道个两个语句是将 30 存入 i 变量的内存空间中,将“t”字符存 入 a 变量的内存空间中。我们可以利用这样的形象来理解啦: 

3.变量在哪里?(即我想知道变量的地址)

好了,接下来我们来看看&i 是什么意思?

是取 i 变量所在的地址编号嘛!我们可以这样读它:返回 i 变量的地址编 号。你记住了吗?

我要在屏幕上显示变量的地址值的话,可以写如下代码:

printf("%x", &i);

以上图的内存映象为例,屏幕上显示的不是 i 值 30,而是显示 i 的内存地 址编号 6 了。当然,在你的实际操作中,i 变量的地址值不会是这个数了。

这就是我所认为的作为初学者应该能够想象到的变量存储的实质了。请这样 理解吧! 最后总结代码如下:

现在你可知道①、②两个 printf 分别在屏幕上输出的是 i 的什么东西啊? 好啦!下面我们就开始真正进入指针的学习了。 

 2.指针是什么?

指针,想说弄懂你不容易啊!我们许多初学指针的人都要这样感慨。我常常 在思索它,为什么呢?其实生活中处处都有指针,我们也处处在使用它。有了它 我们的生活才更加方便了。没有指针,那生活才不方便。不信?你看下面的例子。

这是一个生活中的例子:比如说你要我借给你一本书,我到了你宿舍,但是 你人不在宿舍,于是我把书放在你的 2 层 3 号的书架上,并写了一张纸条放在你 的桌上。纸条上写着:你要的书在第 2 层 3 号的书架上。当你回来时,看到这张 纸条,你就知道了我借与你的书放在哪了。你想想看,这张纸条的作用,纸条本 身不是书,它上面也没有放着书。那么你又如何知道书的位置呢?因为纸条上写 着书的位置嘛!其实这张纸条就是一个指针了。它上面的内容不是书本身,而是 书的地址,你通过纸条这个指针找到了我借给你的这本书。

那么我们 C/C++中的指针又是什么呢?请继续跟我来吧,下面看一条声明一 个指向整型变量的指针的语句:

int *pi;

pi 是一个指针,当然我们知道啦,但是这样说,你就以为 pi 一定是个多么 特别的东西了。其实,它也只过是一个变量而已。与上一篇中说的变量并没有实 质的区别。不信你看下面图:

(说明:这里我假设了指针只占 2 个字节宽度,实际上在 32 位系统中,指针的 宽度是 4 个字节宽的,即 32 位。)

由图示中可以看出,我们使用“int *pi”声明指针变量 —— 其实是在 内存的某处声明一个一定宽度的内存空间,并把它命名为 pi。你能在图中看出 pi 与前面的 i、a 变量有什么本质区别吗?没有,当然没有!pi 也只不过是一 个变量而已嘛!那么它又为什么会被称为“指针”?关键是我们要让这个变量所 存储的内容是什么。现在我要让 pi 成为具有真正“指针”意义的变量。请接着 看下面语句:

pi = &i;

你应该知道 &i 是什么意思吧!再次提醒你啦:这是返回 i 变量的地址编 号。整句的意思就是把 i 地址的编号赋值给 pi,也就是你在 pi 里面写上 i 的 地址编号。结果如下图所示:

你看,执行完 pi=&i 后,在图示中的内存中,pi 的值是 6。这个 6 就是 i 变量的地址编号,这样 pi 就指向了变量 i 了。你看,pi 与那张纸条有什么区别?pi 不就是那张纸条嘛!上面写着 i 的地址,而 i 就是那个本书。你现在看 懂了吗?因此,我们就把 pi 称为指针。所以你要记住,指针变量所存的内容就 是内存的地址编号!好了,现在我们就可以通过这个指针 pi 来访问到 i 这个变 量了,不是吗?看下面语句:

printf("%d", *pi);

那么*pi 什么意思呢?你只要这样读它:pi 的内容所指的地址的内容(嘻 嘻,看上去好像在绕口令了),就是 pi 这张“纸条”上所写的位置上的那本 “书” —— i 。你看,Pi 的内容是 6,也就是说 pi 指向内存编号为 6 的地址。*pi 嘛,就是它所指地址的内容,即地址编号 6 上的内容了,当然就是 30 这个“值” 了。所以这条语句会在屏幕上显示 30。也就是说 printf("%d", *pi)等价于 printf("%d", i) ,请结合上图好好体会吧!各位还有什么疑问?

到此为止,你掌握了类似&i、*pi 写法的含义和相关操作吗?总的一句话, 我们的纸条就是我们的指针,同样我们的 pi 也就是我们的纸条!剩下的就是我 们如何应用这张纸条了。最后我给你一道题:程序如下。

char a,*pa;

a = 10;

pa = &a;

*pa = 20; printf("%d", a);

你能直接看出输出的结果是什么吗?如果你能,我想本篇的目的就达到了。 好了,就说到这了。Happy Study! 在下篇中我将谈谈“指针的指针”即对

int **ppa;

中 ppa 的理解。

3.指针与数组名

1.通过数组名访问数组元素

看下面代码:

int i, a[] = {3,4,5,6,7,3,7,4,4,6};

for (i = 0; i <= 9; i++)

{

printf("%d\n", a[i]);

}

很显然,它是显示 a 数组的各元素值。

我们还可以这样访问元素,如下:

int i, a[] = {3,4,5,6,7,3,7,4,4,6};

for (i = 0; i <= 9; i++)

{

printf("%d\n", *(a+i));

}

它的结果和作用完全一样

2.通过指针访问数组元素

int i, *pa, a[] = {3,4,5,6,7,3,7,4,4,6};

pa = a; /*请注意数组名 a 直接赋值给指针 pa*/

for (i = 0; i <= 9; i++)

{

printf("%d\n", pa[i]);

}

很显然,它也是显示 a 数组的各元素值。

另外与数组名一样也可如下:

int i, *pa, a[] = {3,4,5,6,7,3,7,4,4,6};

pa = a;

for (i = 0; i <= 9; i++)

{

printf("%d\n", *(pa+i));

}

看 pa = a,即数组名赋值给指针,以及通过数组名、指针对元素的访问形 式看,它们并没有什么区别,从这里可以看出:数组名其实也就是指针。难道它 们没有任何区别?有,请继续。

3.数组名与指针变量的区别

请看下面的代码:

int i, *pa, a[] = {3,4,5,6,7,3,7,4,4,6};

pa = a;

for (i = 0; i <= 9; i++)

{

printf("%d\n", *pa);

pa++; /*注意这里,指针值被修改*/

}

可以看出,这段代码也是将数组各元素值输出。不过,你把循环体{}中的 pa改成 a 试试。你会发现程序编译出错,不能成功。看来指针和数组名还是不同的。 其实上面的指针是指针变量,而数组名只是一个指针常量。这个代码与上面的代 码不同的是,指针 pa 在整个循环中,其值是不断递增的,即指针值被修改了。 数组名是指针常量,其值是不能修改的,因此不能类似这样操作:a++。

前面 4、5 节中 pa[i],*(pa+i)处,指针 pa 的值是使终没有改变。所以 变量指针 pa 与数组名 a 可以互换。

4.声明指针常量

再请看下面的代码:

int i, a[] = {3,4,5,6,7,3,7,4,4,6};

int *const pa = a; /* 注意 const 的位置:不是 const int *pa */

for (i = 0; i <= 9; i++)

{

printf("%d\n", *pa);

pa++ ; /*注意这里,指针值被修改*/

}

这时候的代码能成功编译吗?不能。因为 pa 指针被定义为常量指针了。这 时与数组名 a 已经没有不同。这更说明了数组名就是常量指针。但是……

int *const a = {3,4,5,6,7,3,7,4,4,6}; /*不行*/

int a[]={3,4,5,6,7,3,7,4,4,6}; /*可以,所以初始化数组时必定 要这样。*/

以上都是在 VC6.0 上实验。

4.const int *pi与int *const pi 的区别

你知道我们声明一个变量时象这样 int i ;这个 i 是可能在它处重新变赋 值的。如下:

int i = 0;

/* . . . */

i = 20; /*这里重新赋值了*/

不过有一天我的程序可能需要这样一个变量(暂且称它变量),在声明时就 赋一个初始值。之后我的程序在其它任何处都不会再去重新对它赋值。那我又应 该怎么办呢?用 const 。

/* . . . */

const int ic =20;

/* . . . */

ic = 40; /*这样是不可以的,编译时是无法通过,因为我们不能对 const 修饰的 ic 重新赋值的。*/

/*这样我们的程序就会更早更容易发现问题了。*/

/* . . . */

有了 const 修饰的 ic 我们不称它为变量,而称符号常量,代表着 20 这 个数。这就是 const 的作用。ic 是不能在它处重新赋新值了。

认识了 const 作用之后,另外,我们还要知道格式的写法。有两种:

const int ic = 20;

int const ic = 20;

它们是完全相同的。这一点我们是要清楚。总之,你务必要记住 const 与 int 哪个写前都不影响语义。有了这个概念后,我们来看这两个家伙:

const int *pi

int const *pi

按你的逻辑看,它们的语义有不同吗?呵呵,你只要记住一点:int 与 const 哪个放前哪个放后都是一样的,就好比 const int ic;与 int const ic;一样。也就是说,它们是相同的。

好了,我们现在已经搞定一个“双包胎”的问题。那么

int *const pi;

与前两个语句又有什么不同呢?我下面就来具体分析它们的格式与语义吧!

2. const int *pi的语义

我先来说说 const int *pi 是什么作用 (当然 int const *pi 也是一 样的,前面我们说过,它们实际是一样的)。看下面的例子:

/* 代码开始 */

int i1 = 30;

int i2 = 40;

const int *pi = &i1;

pi = &i2; /* 注意这里,pi 可以在任意时候重新赋值一个新内存地 址*/

i2 = 80; /* 想想看:这里能用*pi = 80 来代替吗?当然不能!*/

printf("%d\n", *pi); /* 输出是 80 */

/* 代码结束 */

语义分析:

看出来了没有啊,pi 的值是可以被修改的。即它可以重新指向另一个地址 的,但是,不能通过*pi 来修改 i2 的值。这个规则符合我们前面所讲的逻辑吗? 当然符合了!

首先 const 修饰的是整个*pi(注意,我写的是*pi 而不是 pi)。所以 *pi 是常量,是不能被赋值的(虽然 pi 所指的 i2 是变量,不是常量)。

其次,pi 前并没有用 const 修饰,所以 pi 是指针变量,能被赋值重新指 向另一内存地址的。你可能会疑问:那我又如何用 const 来修饰 pi 呢?其实, 你注意到 int *const pi 中 const 的位置就大概可以明白了。请记住,通过 格式看语义。哈哈,你可能已经看出了规律吧?那下面的一节也就没必要看下去 了。不过我还得继续我的战斗!

3. 再看int *const pi

确实,int *const pi 与前面的 int const *pi 会很容易给混淆的。 注意:前面一句的 const 是写在 pi 前和*号后的,而不是写在*pi 前的。很显 然,它是修饰限定 pi 的。我先让你看例子:

/* 代码开始 */

int i1 = 30;

int i2 = 40;

int *const pi = &i1;

/* pi = &i2; 注意这里,pi 不能再这样重新赋值了,即不能再指向另一个新地址。(第 4 行的注释)*/

/* 所以我已经注释了它。*/

i1 = 80; /* 想想看:这里能用 *pi = 80; 来代替吗?可以,这 里可以通过*pi 修改 i1 的值。(第 5 行的注释)*/

/* 请自行与前面一个例子比较。 */

printf("%d", *pi); /* 输出是 80 */

/* 代码结束 */

语义分析:

看了这段代码,你明白了什么?有没有发现 pi 值是不能重新赋值修改了。它只能永远指向初始化时的内存地址了。相反,这次你可以通过*pi 来修改 i1 的值了。与前一个例子对照一下吧!看以下的两点分析:

1)pi 因为有了 const 的修饰,所以只是一个指针常量:也就是说 pi 值 是不可修改的(即 pi 不可以重新指向 i2 这个变量了)(请看第 4 行的注释)。

2)整个*pi 的前面没有 const 的修饰。也就是说,*pi 是变量而不是常 量,所以我们可以通过*pi 来修改它所指内存 i1 的值(请看第 5 行的注释)。

总之一句话,这次的 pi 是一个指向 int 变量类型数据的指针常量。

我最后总结两句:

1) 如果 const 修饰在*pi 前,则不能改的是*pi(即不能类似这样: *pi=50;赋值)而不是指 pi。

2) 如果 const 是直接写在 pi 前,则 pi 不能改(即不能类似这样:pi=&i; 赋值)。

请你务必先记住这两点,相信你一定不会再被它们给搞糊了。现在再看这 两个声明语句 int const *pi 和 int *const pi 时,呵呵,你会头昏脑胀 还是很轻松惬意?它们各自声明的 pi 分别能修改什么,不能修改什么?再问问自己,把你的理解告诉我吧,可以发帖也可以发到我的邮箱(我的邮箱 yyf977@163.com)!我一定会答复的。

4.补充三种情况

这里,我再补充以下三种情况。其实只要上面的语义搞清楚了,这三种情况 也就已经被包含了。不过作为三种具体的形式,我还是简单提一下吧!

情况一:int *pi 指针指向 const int i 常量的情况

/* begin */

const int i1 = 40;

int *pi;

pi = &i1; /* 这样可以吗?不行,VC 下是编译错。*/

 

/* const int 类型的 i1 的地址是不能赋值给指向 int 类型地址的指 针 pi 的。否则 pi 岂不是能修改 i1 的值了吗!*/

pi = (int *) &i1; /* 这样可以吗?强制类型转换可是 C 所支持 的。*/

/* VC 下编译通过,但是仍不能通过 *pi = 80 来修改 i1 的值。去试试 吧!看看具体的怎样。*/

/* end */

情况二:const int *pi 指针指向 const int i1 的情况

/* begin */

const int i1=40;

const int * pi;

pi=&i1;/* 两个类型相同,可以这样赋值。很显然,i1 的值无论是通过 pi 还是 i1 都不能修改的。 */

/* end */

情况三:用 const int *const pi 声明的指针

/* begin */

int i;

const int * const pi=&i; /*你能想象 pi 能够作什么操作吗?pi 值不能改,也不能通过 pi 修改 i 的值。因为不管是*pi 还是 pi 都是 const 的。 */

/* end */

就先说这么多啦。下一篇我们再接着学剩下的。如有任何问题,欢迎与我联系。需要C语言入门大礼包、各种C语言C++学习资料的小伙伴可以加入小编的学习圈,里面聚集了一些正在学习C语言的小伙伴,在学习C语言的过程中遇到任何的问题,大家都可以一起交流,大家可以根据自己需要领取源码素材。希望大家都能够实现自己的梦想,加油!大家可以根据自己需要领取源码素材!

点击链接加入群聊【C语言/C++学习交流群】:icon-default.png?t=M276https://jq.qq.com/?_wv=1027&k=Mp09WOBC​​​​​​​


版权声明:本文为B85951005原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。