一、声明指针
- 例子1
int i = 1;
int *p = &i;
上面语句的意思是声明i为一个整型变量,声明p为一个指向整型的指针,并且取i的地址对该指针进行初始化。
int i;
*&i = 2;
这里的意思是把值2赋值给变量i与i=2;
作用相同。我们来分析下,首先&
操作符产生i的地址,它是一个指针常量(我们并不需要知道这个指针常量的实际值)。接着,*
操作符对其操作数执行间接访问操作,这个操作数就是i的地址。所以值2就存储于i中。
注意:这里需要说明的是*
号根据所处的上下文环境不同,它表示的含义也不一样。在声明中int *p = &i;
,其*
号作用是指明p的类型以便告诉编译器p是一个指向int类型的变量指针,而在语句中(比如*&i = 2;
),*
号会执行间接访问操作。
- 例子2
int* b, c, d;
人们会很自然的认为这条语句把所有三个变量声明为指向整型的指针,但其实*
号只对b有用。b是一个整型指针,其余两个只是普通的整型。要声明三个指针正确的应该是这样:
int *b, *c, *d;
- 例子3
char *message = "Hello world";
这条语句把message声明为一个指向字符的指针,并用字符串常量中第1个字符的地址对该指针进行初始化。
注意:千万不要误认为是对*message
赋值,事实上它是赋值给message
本身。等价于下面的声明
char *message;
message = "Hello world";
- 例子4
char *ptr_to_char;
这条语句意思是把变量ptr_to_char声明为一个指向字符的指针。但添加typedef
声明变为:
typedef char *ptr_to_char;
这个声明把ptr_to_char作为指向字符的指针类型的新名字,可以像其它预定义类型的名字一样用它来声明变量。例如:
ptr_to_char a = "Hello world";
注意:没有typedef
时ptr_to_char是一个变量,有typedef
时ptr_to_char是一种新的数据类型名字,像int
,char
一样可以用来声明变量。ptr_to_char
声明的变量是指向字符的指针。
- 例子5
void f(const int *p);
int main(void)
{
int i = 1;
f(&i);
return 0;
}
void f(const int *p)
{
int j;
*p = 2; /* 错误的 */
p = &j; /*合法的 */
}
因为函数f
有const
进行了限定(只读不能修改),不能改变指针p所指向的对象,但是可以改变p本身。
- 例子6
void f(int * const p);
int main(void)
{
int i = 1;
f(&i);
return 0;
}
void f(int * const p)
{
int j;
*p = 2; /* 合法的 */
p = &j; /* 错误的 */
}
如果const
放到了参数名的前面,那么不能改变p本身,但可以改变p所指向的对象。这个特性不经常用到。
二、NULL指针
NULL是一个特殊的指针变量,表示不指向任何东西。NULL
指针的概念是非常有用的,它可以赋值给一个任何类型的指针,用于表示那个指针目前并未指向任何东西。
对指针进行间接访问操作(*
)可以获得它所指向的值。但从定义上看,NULL指针并未指向任何东西。因此对一个NULL
指针进行间接访问操作是非法的。在对指针进行间接访问操作之前,必须确保它并非NULL指针。
良好的编程习惯就是,如果你已经知道指针将被初始化为什么地址,就把它初始化为该地址,否则就把它初始化为NULL
。
三、指针常量
指针常量与非指针常量在本质上是不同的,因为编译器负责把变量赋值给计算机内存中的位置,程序员事先无法知道某个特定的变量将存储到内存中的哪个位置。因此,可以通过取地址操作符&
获取一个变量的地址,而不是直接把它地址写成字面值常量的形式。
在极少见的情况下,我们偶尔需要使用指针常量,这时我们可以通过把一个整型值强制转换为指针类型来创建它。
比如:假设我们知道了整型变量a存储在内存的地址为100,那么可以这样赋值
*(int *)100 = 25;
把100从“整型”转换为“指向整型的指针”,再对它进行间接访问就是合法的。
但是很少有机会需要使用这种技巧。
再看一个示例:
int a[10];
这里的数组名a
是个指针常量而不是指针变量,它保存了数组第1个元素的地址(a指向了数组第一个元素)。你不能修改常量的值。
只有在两种场合下,数组名并不用指针常量来表示(数组名并不保存数组第一个元素的地址):
1)数组名作为sizeof
操作符时sizeof
返回整个数组的长度,而不是指针常量的长度
#include <stdio.h>
int main(void)
{
int a[10];
printf("size = %lu\n", (unsigned long) sizeof(a));
return 0;
}
2)数组名作为&
操作符时对一个数组名使用取地址操作符产生的是一个指向数组的指针
#include <stdio.h>
int main(void)
{
int calendar[12][31];
int (*monthp)[31];
monthp = calendar; // 等价于 monthp = &calendar[0];
}
monthp是一个指向拥有31个整型元素的数组的指针。如果把calendar当作一个一维数组元素组成的一维数组,那么可以把monthp当作是指向calendar第一个元素的指针。而*monthp
就是一个一维数组,它可以像普通的一维数组名那样用下标取元素,(*monthp)[0]
等价于*((*monthp) + 0)
,还等价于calendar[0][0]
,如果把calendar看成是一个二维数组的话,这三种方式都是取calendar第0行第0列的元素。
四、指向指针的指什
- 例子1
#include <stdio.h>
int main(void)
{
int i;
int *pi;
int **ppi;
int ***pppi;
pi = &i;
ppi = π
pppi = &ppi;
i = 1;
*pi = 1;
**ppi = 1;
***pppi = 1;
printf("i = %d\n", i);
return 0;
}
上面的代码14-17行都具有相同的效果,都是对变量i进行赋值为1。
i是一个整型。
pi是一个指向整型的指针。
ppi是一个指向整型的指针的指针。
pppi是一个指向整型的指针的指针的指针。
上面代码10-12行还可以换成下面这样:
pppi = &ppi;
*pppi = π
*ppi = &i; // **pppi = &i;
五、void *
通用型指针
看一个示例:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
static char *concat(const char *s, const char *t);
int main(int argc, char **argv)
{
if (argc != 3) {
fprintf(stderr, "Usage:concat <s1> <s2>\n");
exit(EXIT_FAILURE);
}
printf("r = %s\n", concat(*(argv + 1), *(argv + 2)));
exit(0);
}
static char *concat(const char *s, const char *t)
{
char *r = NULL;
if ((r = malloc(strlen(s) + strlen(t) + 1)) == NULL) {
perror("malloc failed in concat");
exit(EXIT_FAILURE);
}
strcpy(r, s);
strcat(r, t);
return r;
}
运行程序:
[zq@localhost Temp]$ ./concat abc def
r = abcdef
上面的示例中malloc
函数返回值类型是void *
,这种返回值类型我们称为通用类型指针,当调用内存分配函数时,
函数无法知道计划存储在内存块中的数据类型是什么类型的,所以它不能返回int
类型指针,也不能返回char
类型指针等普通类型的指针。取而代之的,函数就返回void *
类型指针。
通常情况下,不需要强制转换,可以把void *类型值赋值给任何指针类型的变量
上面的代码中printf("r = %s\n", concat(*(argv + 1), *(argv + 2)));
也可以写成printf("r = %s\n", concat(argv[1], argv[2]));
六、总结
- 指针是C语言的灵魂,它很强大但也很容易出错,必须掌握指针编程的知识和技巧
- 指针就是地址,指针变量就是保存内存地址的变量,指针变量的值就是一个数字
- 一般我们说p指向了i,意思是指针p保存了i的地址
- 使用
&
获取变量的地址 - 间接访问操作
*
只能作用于指针类型的表达式且非NULL
- 数组取下标与指针间接访问的互相转换可以记住一个公式:
array[s] = *(array + (s))
array是代表数组名,s是表达式
- 声明一个指针变量并不会自动分配任何内存。在对指针执行间接访问前,指针必须进行初始化或者使它指向现有的内存或者给它分配动态内存。对未初始化的指针变量执行间接访问操作是非法的,而且这种错误常常难于验证检测。其结果常常是一个不相关的值被修改。这种错误是很难被调试发现的。