本文是对之前文章的一个补充。
首先我们知道:
- & 运算符后加变量名就变成了变量名的地址,& 可以称为取地址运算符
- 变量地址前加 * 运算符就能够访问到变量,* 可以成为取内容运算符
- 地址一般是用存储空间的第一个字节的地址表示变量地址,即低字节地址
- 地址也是有大小的
指针和指针变量
指针
最纯粹的指针可能就是 (void *),也就是用 malloc 函数在堆上申请内存空间的返回值,此时只表示申请内存空间的首地址。但其实指针存在很多类型。归总来说,指针就是有类型的地址,该类型表示一次性能够读取到的内存空间范围。
该部分内容在之前文章中也有提到,不熟悉的话可以再翻翻。
指针变量
指针变量的形式为:
datatype *var;
- datatype 表示一次性能够读取到的内存空间范围
- * 表示该变量是个指针
- var 表示该指针变量的变量名
变量大小
而不管是什么类型的指针变量,sizeof(var) 的值都是与计算机本身相关的常量值。
#include <stdio.h>
int main(void)
{
char *pc;
int *pi;
int (*pii)[3];
int arr[10];
printf("sizeof(pc) = %d\n",sizeof(pc));
printf("sizeof(pi) = %d\n",sizeof(pi));
printf("sizeof(pii) = %d\n",sizeof(pii));
printf("sizeof(arr) = %d\n",sizeof(arr));
return 0;
}
结果为:
sizeof(pc) = 4
sizeof(pi) = 4
sizeof(pii) = 4
sizeof(arr) = 40
注意最后的 arr 并不符合指针变量的形式,是数组名。而数组指针的形式也并不符合指针变量的形式,但却仍旧是指针。
变量类型
变量的值通过不同类型的指针变量进行解释,会得到不同的结果。还是之前那句话,指针变量的类型决定了该指针的寻址能力。
#include <stdio.h>
int main(void)
{
int a = 0x12345678;
char *ch = &a;
short *un = &a;
int *in = &a;
printf("a = %x\n",a);
printf("*ch = %x\n",*ch);
printf("*un = %x\n",*un);
printf("*in = %x\n",*in);
return 0;
}
结果为:
a = 12345678
*ch = 78
*un = 5678
*in = 12345678
指针运算
指针运算和数值运算可能有点不太一样,主要是与指针类型有关。
#include <stdio.h>
int main(void)
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
printf("%p\n",arr);
printf("%p\n",arr+5);
printf("%d\n",(arr+5)-arr);
printf("%d\n",(char *)(arr+5)-(char *)arr);
return 0;
}
结果为:
0060FE88
0060FE9C
5
20
从上边的结果可以看出:
- arr 和 arr+5 的地址间隔为 20
- 但是 (arr+5)-arr 却不是 20,而是 5,这是因为在指针运算中,会默认将其结果按照寻址能力进行换算
- 因此在将 (arr+5) 和 (arr) 转化为 (char *) 之后,计算的结果就是按照 (char *) 的寻址能力进行换算的
二级指针
其实我们之前使用过二级指针,在堆上返回一维数组的第二种形式中,我们没有使用返回值,而是使用二级指针作形参,改变了一级指针指向的内容。简单说来,二级指针是指向指针的指针。
定义和初始化
datatype **var = &p;
上次的 p 是一个一级指针。
#include <stdio.h>
int main(void)
{
char c = 'h';
char *pc = &c;
char **pp = &pc;
printf("&c = %p\n",&c);
printf("pc = %p\n",pc);
printf("*pp = %p\n",*pp);
printf("c = %c\n",c);
printf("*pc = %c\n",*pc);
printf("**pp = %c\n",**pp);
return 0;
}
结果是:
&c = 0060FEAB
pc = 0060FEAB
*pp = 0060FEAB
c = h
*pc = h
**pp = h
从上边的例子中可以看出,利用二级指针能够改变原始变量的值。
间接数据访问
改变二级指针的指向
可以改变二级指针的指向,而不改变原始变量的内容。
#include <stdio.h>
int main(void)
{
char c = 'h';
char d = 'h';
char *pc = &c;
char **pp = &pc;
printf("c = %c\n",c);
printf("*pc = %c\n",*pc);
printf("**pp = %c\n",**pp);
printf("&c = %p\n",&c);
printf("pc = %p\n",pc);
printf("*pp = %p\n",*pp);
*pp = &d;
printf("d = %c\n",d);
printf("*pc = %c\n",*pc);
printf("**pp = %c\n",**pp);
printf("&d = %p\n",&d);
printf("pc = %p\n",pc);
printf("*pp = %p\n",*pp);
return 0;
}
结果为:
c = h
*pc = h
**pp = h
&c = 0060FEAB
pc = 0060FEAB
*pp = 0060FEAB
d = e
*pc = e
**pp = e
&d = 0060FEAA
pc = 0060FEAA
*pp = 0060FEAA
改变二级指针指向的内容
还可以改变二级指针指向的内容,而不改变指针本身。
#include <stdio.h>
int main(void)
{
char c = 'h';
char *pc = &c;
char **pp = &pc;
printf("c = %c\n",c);
printf("*pc = %c\n",*pc);
printf("**pp = %c\n",**pp);
printf("&c = %p\n",&c);
printf("pc = %p\n",pc);
printf("*pp = %p\n",*pp);
c = 'e';
printf("c = %c\n",c);
printf("*pc = %c\n",*pc);
printf("**pp = %c\n",**pp);
printf("&c = %p\n",&c);
printf("pc = %p\n",pc);
printf("*pp = %p\n",*pp);
return 0;
}
结果是:
c = h
*pc = h
**pp = h
&c = 0060FEAB
pc = 0060FEAB
*pp = 0060FEAB
c = e
*pc = e
**pp = e
&c = 0060FEAB
pc = 0060FEAB
*pp = 0060FEAB
除此之外,还可以利用二级指针改变指向的一级指针的指向。同样利用 N 级指针改变指向的 N-1 级指针,N-2 级指针......的指向。
二级指针的步长
由于二级指针指向的是一级指针,而一级指针跟平台有关(此处为 4),因此二级指针的步长也是 4。
#include <stdio.h>
int main(void)
{
char c;
char *pc = &c;
char **ppc = &pc;
printf("ppc = %p\n",ppc);
printf("ppc+1 = %p\n",ppc+1);
int **ppi = NULL;
printf("ppi = %p\n",ppi);
printf("ppi+1 = %p\n",ppi+1);
double **ppd = NULL;
printf("ppd = %p\n",ppd);
printf("ppd+1 = %p\n",ppd+1);
return 0;
}
结果为:
ppc = 0060FE9C
ppc+1 = 0060FEA0
ppi = 00000000
ppi+1 = 00000004
ppd = 00000000
ppd+1 = 00000004
指针数组
定义
之前我们说到的是数组指针,而这里的是指针数组,准确地说应该是字符指针数组。
指针数组的本质是数组,数组中每一个成员是一个指针。定义形式为:
char *p[10];
而数组指针的形式为:
char (*p)[10];
可以看出两者的不同在于 * 和 p 的结合方式不同。指针数组中,是 char * 修饰的 p[10],而数组指针中是 char [10] 修饰的 (*p)。
也就是说,指针数组中的每个元素都是 char *。
初始化
既然是数组,就会有初始化:
#include <stdio.h>
int main(void)
{
char *p[10] = {"apple","banana","orange","strawberry"};
for(unsigned i=0; i<sizeof(p)/sizeof(*p); i++)
printf("%s\n",p[i]);
return 0;
}
结果为:
apple
banana
orange
strawberry
二级指针访问指针数组
指针数组名与二级指针的关系
- char **p 是二级指针
- char *arr[10] 是指针数组,arr[0] 本身是 char * 类型
- 因此就可将两者联系起来:char **p = arr;
#include <stdio.h>
int main(void)
{
char *p[10] = {"apple","banana","orange","strawberry"};
for(unsigned i=0; i<sizeof(p)/sizeof(*p); i++)
printf("%s\n",p[i]);
printf("*****************\n");
char **pp =p;
for(unsigned i=0; i<sizeof(p)/sizeof(*p); i++)
printf("%s\n",pp[i]);
return 0;
}
结果为:
apple
banana
orange
strawberry
*****************
apple
banana
orange
strawberry
最后的 NULL
在进程空间中我们提到过 argv 的参数个数会比 argc 的数值多 1,而多出来的那个参数是 NULL,为什么会这样子呢?
在上边的程序中,我们用到了下边的程序段:
char **pp =p;
for(unsigned i=0; i<sizeof(p)/sizeof(*p); i++)
printf("%s\n",pp[i]);
但是在日常的习惯中,如果我们使用 char **pp = p 之后,在 for 循环中就可以丢弃掉 p,但是实际上 pp 只是一个指针,因此在 for 中单单使用 pp 是不能遍历所有的数组元素的。因此,就需要在指针数组的最后边加上一个 NULL 来作为结束遍历的条件。
#include <stdio.h>
int main(void)
{
char *p[10] = {"apple","banana","orange","strawberry",NULL};
for(unsigned i=0; i<sizeof(p)/sizeof(*p); i++)
printf("%s\n",p[i]);
printf("*****************\n");
char **pp =p;
while(*pp)
printf("%s\n",*pp++);
return 0;
}
结果为:
apple
banana
orange
strawberry
*****************
apple
banana
orange
strawberry
上边的结果比之前的 for 循环的形式要简洁的多了。
堆上二维空间
之前我们说过堆上的一维空间,那么堆上的二维空间又是什么意思呢?
我们平常见到的二维数组是一种二维空间,而二维空间却不一定都是二维数组,但二维空间却具有数组的访问形式。比如我们可以将一维数组转化为数组指针的形式,从而得到类似二维空间的访问形式。
一级指针作返回值
#include <stdio.h>
#include <stdlib.h>
int *memapp(int base, int row, int col)
{
int *p = (int *)malloc(base*row*col);
return p;
}
int main(void)
{
int base = sizeof(int), row = 3, col = 3;
int (*p)[3] = memapp(base, row, col);
for(int i=0; i<row; i++)
for(int j=0; j<col; j++)
p[i][j] = i*col+j+1;
for(int i=0; i<row; i++)
for(int j=0; j<col; j++)
printf("%d\n",p[i][j]);
free(p);
return 0;
}
结果为:
1
2
3
4
5
6
7
8
9
上边的程序中,在堆上申请了连续一段空间,但在 main 函数中却将之解释成了二维空间。
二级指针作返回值
上边的程序中,函数 memapp 返回的是一段连续的空间,更像是一维空间。我们同样可以直接申请不连续的空间并利用二级指针返回。
#include <stdio.h>
#include <stdlib.h>
int **memapp(int base, int row, int col)
{
int **p = malloc(sizeof(void *)*row);
for(int i=0; i<row; i++)
{
p[i] = malloc(base*col);
}
return p;
}
int main(void)
{
int base = sizeof(int), row = 3, col = 4;
int **p = memapp(base, row, col);
for(int i=0; i<row; i++)
for(int j=0; j<col; j++)
p[i][j] = i*col+j+1;
for(int i=0; i<row; i++)
for(int j=0; j<col; j++)
printf("%d\n",p[i][j]);
for(int i=0; i<row; i++)
free(p[i]);
free(p);
return 0;
}
结果为:
1
2
3
4
5
6
7
8
9
10
11
12
比较需要注意的是最后堆上变量的释放形式,需要从里到外先释放一级指针的指向内容,再释放二级指针的指向内容。
const
因为多级指针的能够从次级指针对原始变量进行更改,因此有时候为了防止对数据或次级指针的更改,增强程序的健壮性,需要使用 const 关键字进行变量进行限制。
const 修饰变量
此时首先需要对变量进行初始化,并且经过 const 修饰符修饰过的变量不能再次发生更改。
#include <stdio.h>
int main(void)
{
const int a = 1;
const int b;
printf("%d\n",a);
printf("%d",b);
return 0;
}
结果为:
1
89
此时的 b 只能一直保持随机值。
const 修饰指针
此时表示指针的指向是恒定的,但是指向的内容却是可以更改的。
#include <stdio.h>
int main(void)
{
int a = 1;
int * const p = &a;
printf("%d\n",a);
printf("%d\n",*p);
a = 2;
printf("%d\n",a);
printf("%d\n",*p);
return 0;
}
结果为:
1
1
2
2
const 修饰指针指向内容
此时表示指针的指向的内容是恒定的,但是指向本身却是可以更改的。
#include <stdio.h>
int main(void)
{
int a = 1;
int b = 2;
const int *p = &a;
printf("%d\n",a);
printf("%d\n",*p);
p = &b;
printf("%d\n",a);
printf("%d\n",*p);
return 0;
}
结果为:
1
1
1
2
但是下边的修改却是可行的:
#include <stdio.h>
int main(void)
{
int a = 1;
const int *p = &a;
printf("%d\n",a);
printf("%d\n",*p);
a = 2;
printf("%d\n",a);
printf("%d\n",*p);
return 0;
}
结果为:
1
1
2
2
因为 a 并没有被限制,因此显然是可以修改的,所限制的只是 *p = 2 这种形式。
同时限制
此时不管是指针的指向还是指向的内容都不能修改,相当于该指针被锁定。
#include <stdio.h>
int main(void)
{
int a = 1;
int b = 2;
const int * const p = &a;
printf("%d\n",a);
printf("%d\n",*p);
return 0;
}
结果为:
1
1