C指针总结

指针是 C 语言中的一大难点,本文会对指针的类型解析,指针的算术运算,指针的赋值取值,指针的应用场景等方面一步步探索指针的奥妙,揭开指针神秘的面纱。

识别各种各样的指针

单指针

  • 单指针很好理解,就是一个指向内存空间地址的变量;
  • 指针变量的大小取决于所用机子的寻址方式,与所指内容无关,一般 32 位系统为 4 个字节,64 位系统为 8 个字节。

单指针定义与使用的方式如下

1
2
3
int a = 0;      
int *p = &a; // 定义并初始化指针p
int b = *p; // 取指针p的值

二级指针

  • 二级指针(又称二重指针,emmm,随便怎么叫吧)就是指针的指针;
  • 与普通的单指针相比仅仅是所指向内容上的不同。

二级指针定义与使用的方式如下

1
2
3
4
5
int a = 0;
int *p = &a; // 定义并初始化指针p
int *pp = &p; // 定义并初始化二重指针p
int *q = *pp; // 对pp取值得到的是一个整形指针
int b = **pp; // 对pp两次取值得到的是一个整形数据

既然有二级指针,当然有三级、四级、五级……就不一一分析了,而且实际应用中也不会涉及太多级的指针。

数组指针

数组指针又称行指针,即指向数组的指针。其定义与使用方式如下

1
2
3
int arr[][2] = {1, 2, 3, 4};
int (*p)[2] = arr;
int a = p[0][1];

指针数组与数组指针写法类似,其定义与使用方法如下

1
2
int *arr[3] = {0};
int *p = arr[1];
  • 数组指针本质是指针,其指向一个数组内存空间的首地址
  • 指针数组本质是数组,数组中的元素是指针

可以通过向右优先结合的方式来判断复杂指针的类型,如

1
2
3
4
// 从p开始向右,先与[]结合,可知是一个数组。
// 然后向左结合,可知数组中存的是指针
// 继续向左,指针指向的数据类型是int
int *p[3] = {0};
1
2
3
4
// 从p开始向右,遇到右括号,先结合括号内的*,可知是一个指针
// 向右结合,可知指针指向的是一个数组
// 再向左结合,可知指针指向一个int型的数组
int (*p)[2] = arr;

函数指针

指针也可指向函数,其定义与使用方法如下

1
2
3
4
5
6
7
8
9
10
11
int fun()
{
// ......
return 0;
}
int (*p)() = fun;
int main()
{
p();
return 0;
}

函数指针容易和返回值为指针的函数混淆

1
int *fun();

同样可以用上述的向右优先结合的方式来一步步分析

总结

类型名 定义格式
单指针 int *p;
二级指针 int **p;
数组指针 int (*p)[2];
指针数组 int *arr[2];
函数指针 int (*p)();
指针函数 int *func();

指针的算术运算

  • 指针可以加上或减去一个整数;
  • 此加减以存储单元为单位,取决于指针的基类型大小;

下面的程序可以看出指针进行算术运算时的变化

1
2
3
4
5
6
7
8
int main()
{
int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int *p = a;
printf("%d, %x\n", *p, p);
printf("%d, %x\n", *(p+3), p+3);
return 0;
}

程序执行的结果为:

图2_1

p 和 p+3 之间的内存地址相差 0xc 即 12个 = 3个int的大小

运算符 & 和 *

  • 这里 & 是取地址运算符, * 是间接运算符。
  • &a 的运算结果是一个指针,指针的类型是 a 的类型加个*,指针所
    指向的类型是 a 的类型,指针所指向的地址就是 a 的地址。
  • *p 的结果是 p 所指向的东西,它的类型是 p 指向的类型,它所占用的地址是 p 所指向的地址。
1
2
3
4
5
6
7
int a = 0;
int *p;
int **pp;
p = &a; // 这里 &a 就是 a 的地址
*p = 1; // 这里修改 *p 修改的是 a 的值
pp = &p; // 这里 &p 就是 p 的地址
**p = 2; // 这里修改 **p 修改的是 a 的值

指针与数组

数组的数组名其实可以看作一个指针。看下例:

1
2
3
4
int arr[] = {1, 2, 3, 4};
printf("%d\n", *arr);
printf("%d\n", *(arr+1));
printf("%d\n", arr[2]);
  • 数组的元素可以用 数组名+偏移量 的方式找到;
  • 同样,一个指向数组首地址的指针也可以用 [] 进行访问。

指针的类型转换

指针的类型被强制转换后,其指向的地址不受影响,但其进行算术运算时的大小会发生改变。

1
2
3
4
5
6
7
8
9
10
11
int main()
{
int a = 0;
char *p = (char *)&a;
printf("%x\n", &a);
printf("%x\n", p);
printf("%x\n", p+1);
printf("%x\n", p+2);
printf("%x\n", p+3);
return 0;
}

程序运行的结果是

图5_1

指针的应用场景

给函数传不定量的参数

典型的示例就是 main 函数的 argc 和 argv

下面是一个找最长字符串的命令的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main(int argc, char *argv[])
{
int pos = -1;
int maxLen = 0;
for(int i = 1; i < argc; ++i)
{
int len = strlen(argv[i]);
if(maxLen < len)
{
pos = i;
maxLen = len;
}
}
if(pos != -1)
{
printf("pos = %d\n", pos);
printf("len = %d\n", maxLen);
printf("str = %s\n", argv[pos]);
}
return 0;
}

程序运行结果如下

图6_1

给排序函数传入比较规则

以一个选择排序为例,客户程序员可以不需要修改 Sort 函数的内容,只需要改变传入的参数,就可以实现按不同的方式排列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 插入排序函数
void Sort(int *a, size_t n, bool(*cmp)(int, int))
{
if( NULL == a )
return ;
int i, j;
for( i = 1; i < n; ++i )
{
if( cmp( a[i], a[i-1] ) )
{
T tmp = a[i];
for( j = i-1; j >= 0 && !cmp( a[j], tmp ); --j )
a[j+1] = a[j];
a[j+1] = tmp;
}
}
}

// 比较函数
bool compare(int a, int b)
{
return a < b;
}

int main()
{
int a[] = {7, 2, 3, 4, 5, 2, 4};
// 函数名作为参数传入
sort(a, sizeof(a)/sizeof(a[0], compare));
return 0;
}

指针的安全使用

指针的使用要注意安全性

  • 要清楚指针到底指向了哪里,不能超出程序所能使用的内存范围,常见的错误如数组越界,强制转换成更大空间的指针(int -> double)等。
  • 指针使用完毕后要及时至空,以免造成野指针的问题。
  • 动态分配的内存空间要记得释放,避免发生内存泄露。
  • 不要给指针直接赋非0数值,除非你在搞贴近硬件的开发,并且清楚对应的地址代表着什么。