彻底搞定C指针(上)

第一篇 变量的内存实质

1. 先来理解C语言中的变量的实质

要理解C指针,就必须理解C中“变量”的存储实质。

先理解内存空间,请看下图:

如上图所示,内存只是一个存放数据的空间,同时每个字节均有编号,称为内存地址

来看一下C/C++语言变量声明:

1
2
int i;
char a;

声明变量时,其实是在内存中申请了一个名为 i 的整型变量宽度的空间(现代的64位处理器中其宽度都是4个字节),和一个名为 a 的字符型变量宽度空间(占1个字节)。

内存中的映像可能如下图:

图中看出,i 在内存起始地址为6上申请了四个字节的空间(我这里默认为64位操作系统),并名为 i 。同理,a 在内存地址为10上申请了一字节的空间,并命名为 a 。

2. 赋值给变量

再看下面的赋值

1
2
i = 30;
a = 't';

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
2
3
4
5
char a, *pi;
a = 10;
pa = &a;
*pa = 20;
printf("%d", a);

如果能直接看出结果,那么这一篇的目的达到了。

第三篇 指针与数组名

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

看下面的代码:

1
2
3
4
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 数组的各元素值。

也可以这样访问元素,如下:

1
2
3
4
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. 通过指针访问数组元素

1
2
3
4
5
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 数组的各元素值。

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

1
2
3
4
5
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. 数组名与指针变量的区别

请看下面的代码:

1
2
3
4
5
6
7

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 在整个循环中的值是不断递增的,即指针值被修改了。而数组名是指针常量,是不可以被修改的,因此不能执行类似这样的自加操作。

在前面的 pa[i] , *(pa+i) 处,指针 pa 的值始终没有改变,所以变量指针 pa 与 数组名 a 可以互换。

4. 声明指针常量

再看下面的代码:

1
2
3
4
5
6
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 已经没有不同。同时更说明数组名就是指针常量,但是……

1
2
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}; /*可以,所以初始化数组时必定要这样。*/

第四篇 const int pi 与 int const pi 的区别

1. 从 const int i 说起

当我们需要声明一个以后不会被重新赋值的变量的时候,此时 const 就派上用场了。

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

有了 const 修饰的 ic 我们不称它为变量,而称符号常量,代表着 20 这个数。

const 的写法格式有两种:

1
2
3
const int ic = 20;
/*…………*/
int const ic = 20;

它们是完全相同的,const 与 int 谁前谁后都不影响。有了这个概念后,再看下面这两个:

1
2
3
const int* pi
/*…………*/
int const* pi

根据上面的逻辑,它们的语义无任何不同,你只需要记住:int 与 const 哪个放前哪个放后都是一样的

2. const int* pi 的语义

先来看看 const int pi 是什么作用(当然int const pi 也是一样的)。

1
2
3
4
5
6
7
/* 代码开始 */
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 呢?

3. 再看 int* const pi

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

1
2
3
4
5
6
7
/* 代码开始 */
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 来修改 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
2
3
4
5
6
7
8
/* begin */
const int i1 = 40;
int* pi;
pi = &i1; /* 这样可以吗?不行,VS 下是编译错。*/
/* const int 类型的 i1 的地址是不能赋值给指向 int 类型地址的指针 pi 的。否则 pi 岂不是能修改 i1 的值了吗!*/
pi = (int* ) &i1; /* 这样可以吗?强制类型转换可是 C 所支持的。*/
/* VS 下编译通过,但是仍不能通过 *pi = 80 来修改 i1 的值。去试试吧!看看具体的怎样。*/
/* end */

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

1
2
3
4
5
/* begin */ 
const int i1 = 40;
const int* pi;
pi = &i1; /* 两个类型相同,可以这样赋值。很显然,i1 的值无论是通过pi 还是 i1 都不能修改的。 */
/* end */

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

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