彻底搞懂C/C++指针
彻底搞定C指针(上)
第一篇 变量的内存实质
1. 先来理解C语言中的变量的实质
要理解C指针,就必须理解C中“变量”的存储实质。
先理解内存空间,请看下图:
如上图所示,内存只是一个存放数据的空间,同时每个字节均有编号,称为内存地址。
来看一下C/C++语言变量声明:
1 | int i; |
声明变量时,其实是在内存中申请了一个名为 i 的整型变量宽度的空间(现代的64位处理器中其宽度都是4个字节),和一个名为 a 的字符型变量宽度空间(占1个字节)。
内存中的映像可能如下图:
图中看出,i 在内存起始地址为6上申请了四个字节的空间(我这里默认为64位操作系统),并名为 i 。同理,a 在内存地址为10上申请了一字节的空间,并命名为 a 。
2. 赋值给变量
再看下面的赋值
1 | i = 30; |
3. 查看变量地址
代码如下:
1 | printf("%x", &i); |
以上图映像为例,则显示 i 的内存地址编号6。
第二篇 指针是什么
1. 指针是什么
先看一条声明一个指向整型变量的指针的语句:
1 | int* pi; |
pi 是一个指针,但其实只不过是个变量而已,与上篇所说的变量实质上均一样。
图示可以看出,使用” int pi “ 声明*指针变量——其实是在内存的某处声明一个一定宽度的内存空间,并把它命名为 pi 。
执行下面的语句:
1 | pi = &i; |
即在内存中 pi 的值是6,为 i 变量的地址编号,这样 pi 就指向变量 i 了。因此,将 pi 称为指针。记住指针变量所存的内容就是内存的地址编号!
现可以通过这个指针 pi 来访问到 i 这个变量,看下面的语句:
1 | printf("%d", *pi); |
pi 可以读成:pi 的内容所指的地址的内容。也就是说 printf(“%d”, pi) 等价于 printf(“%d”, i) 。
到底为止,你已经掌握了类似 &i、*pi 写法的含义和操作了。最后再给一道题:程序如下。
1 | char a, *pi; |
如果能直接看出结果,那么这一篇的目的达到了。
第三篇 指针与数组名
1. 通过数组名访问各数组元素。
看下面的代码:
1 | int i, a[] = {3,4,5,6,7,3,7,4,4,6}; |
很显然是显示 a 数组的各元素值。
也可以这样访问元素,如下:
1 | int i, a[] = {3,4,5,6,7,3,7,4,4,6}; |
它的结果和作用完全一样。
2. 通过指针访问数组元素
1 | int i, *pa, a[] = {3,4,5,6,7,3,7,4,4,6}; |
同理,也是显示 a 数组的各元素值。
另外与数组名一样也可如下:
1 | int i, *pa, a[] = {3,4,5,6,7,3,7,4,4,6}; |
看 pa = a ,即数组名赋值给指针,以及通过数组名、指针对元素的访问形式来看,好像并没有什么区别,数组名就是指针。但它们仍存在其他的区别。
3. 数组名与指针变量的区别
请看下面的代码:
1 |
|
可以看出,这段代码是将属数组各元素值输出。不过,如果将循环体中的 pa 改成 a,你会发现程序编译出错。这说明指针和数组名还是存在区别的,其实上面的指针是指针变量,而数组名只是一个指针常量。这个代码与上面的代码不同之处是,指针 pa 在整个循环中的值是不断递增的,即指针值被修改了。而数组名是指针常量,是不可以被修改的,因此不能执行类似这样的自加操作。
在前面的 pa[i] , *(pa+i) 处,指针 pa 的值始终没有改变,所以变量指针 pa 与 数组名 a 可以互换。
4. 声明指针常量
再看下面的代码:
1 | int i, a[] = {3,4,5,6,7,3,7,4,4,6}; |
这个时候的代码不能编译成功,因为 pa 指针被定义为常量指针了,这时与数组名 a 已经没有不同。同时更说明数组名就是指针常量,但是……
1 | int* const a = {3,4,5,6,7,3,7,4,4,6}; /*不行*/ |
第四篇 const int pi 与 int const pi 的区别
1. 从 const int i 说起
当我们需要声明一个以后不会被重新赋值的变量的时候,此时 const 就派上用场了。
1 | const int ic =20; |
有了 const 修饰的 ic 我们不称它为变量,而称符号常量,代表着 20 这个数。
const 的写法格式有两种:
1 | const int ic = 20; |
它们是完全相同的,const 与 int 谁前谁后都不影响。有了这个概念后,再看下面这两个:
1 | const int* pi |
根据上面的逻辑,它们的语义无任何不同,你只需要记住:int 与 const 哪个放前哪个放后都是一样的。
2. const int* pi 的语义
先来看看 const int pi 是什么作用(当然int const pi 也是一样的)。
1 | /* 代码开始 */ |
语义分析:
可以看出,pi 的值是可以被修改的。即它可以重新指向另一个地址,但是不能通过 *pi 来修改 i2 的值。乍一看好像不符合前面的逻辑,其实依然符合!
首先,const 修饰的是整个 pi (注意,是 pi 而不是 pi)。所以 *pi 是常量,是不能被赋值的(虽然 pi 所指的是 i2 是变量,不是常量)。
其次,pi 前并没有用 const 修饰,所以 pi 是指针变量,能被赋值重新指向另一个内存地址。看到这里,你可能会想,那我该怎么用 const 修饰 pi 呢?
3. 再看 int* const pi
确实,int const pi 与前面的 int const pi 会很容易混淆。注意:前面一句的 const 是写在 pi 前和 号后的,而不是写在 pi 前的。很显然,它是修饰限定 pi 的。
1 | /* 代码开始 */ |
语义分析:
你发现 pi 不能重新赋值修改了,它只能永远指向初始化时的内存地址了。相反,这次你可以通过 *pi 来修改 il 的值了。与前一个例子对比一下:
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 ,是不是清楚了;如果还是不懂,请再把前面的细看一遍。
4. 补充三种情况
其实这三种情况只要上面的语义搞清楚了,就都包含了。
情况一: int* pi 指针指向 const int i 常量的情况
1 | /* begin */ |
情况二:const int *pi 指针指向 const int i 的情况
1 | /* begin */ |
情况三:用 const int* const pi 声明的指针
1 | /* begin */ |