文章目录
11.0 导入例子
void swap(int x, int y){
int temp;
temp = x;
x = y;
y = temp;
}
int main(){
int x = 10, y = 20;
swap(x, y);
printf("x = %d, y = %d\n", x, y);
getchar();
return 0;
}
函数章节中已经指出上述代码无法交换main函数中x和y的值,因为swap函数中的x和y与main函数中的x和y不一对不同的变量,swap函数中的x和y交换了值,但是对main函数中的x和y没有影响。怎样定义一个可以交换main函数中x和y的值的函数呢?这就需要用到指针变量,让它们指向x和y,这样这些指针变量就可以直接访问x和y了。
void swap(int *p, int *q){
// p指向x,q指向y,*p就是x的别名,*q就是y的别名
int temp;
temp = *p;
*p = *q;
*q = temp;
}
int main(){
int x = 10, y = 20;
swap(&x, &y); // 将x和y的地址传给函数
printf("x = %d, y = %d\n", x, y);
getchar();
return 0;
}
11.1 指针基本概念
指针变量用来存储变量的地址,用指针变量可以对变量进行间接访问,相当于给变量起了别名。
11.1.1 变量的地址
每个变量都有一个内存位置,各变量的内存可能有多个字节组成,首字节(地址值最小的字节)的地址称为变量的地址。比如,下图中变量i占的地址为从2000到2003共4个字节,所以变量i的地址是2000。
&称为取地址运算符,可以得到变量的地址:
&变量名
printf输出地址值需要使用转换说明符%p:
#include <stdio.h>
int main ()
{
int var1;
float var2;
printf("var1 变量的地址: %p\n", &var1 );
printf("var2 变量的地址: %p\n", &var2 );
getchar();
getchar();
return 0;
}
注意,每次运行程序变量的地址可能会变化。
注意,不能对表达式取地址,比如
int x = 0;
printf("%p", &(x + 1)); // 编译报错
11.1.2 指针变量
指针变量是专门存储变量地址值的一类变量。需要根据预计存储的地址值是属于哪种类型的变量,而声明该类型的指针变量。比如,声明存储整型变量地址的int指针变量,
int *p;
声明存储双精度浮点类型变量地址的double指针变量,
double *q;
指针变量可以和其它变量一起出现在声明中:
int i, j, a[10], *p;
int类型的指针变量可以也只能保存某个int变量的地址:
int i, *p;
...
p = &i;
p存储了i的地址,也可以说p指向了i。输出指针变量的值同样使用转换说明%p。
...
printf("%p\n", p);
也可以在声明指针变量的同时对它初始化
int i;
int *p = &i;
甚至可以把i的声明和p的声明合并,但是需要先声明i:
int i, *p = &i;
一旦p指向了某个变量,比如i,那么就可以使用间接寻址(引用)运算符*来访问那个变量(存储在变量中的值)。
int i, *p = &i;
i = 10;
printf("%d\n", *p); // 显示10
实际上,只要p指向i,那么*p就是i的别名(小名),*p和i是等价的“变量名”。
int i, *p = &i; // p指向了i,*p是i的别名
i = 10;
printf("%d\n", *p); // 显示10
i = 20;
printf("%d\n", *p); // 显示20
*p = 30;
printf("%d\n", i); // 显示30
注意,C语言要求每个指针变量只能指向一种特定类型的变量:
int *p; // 只能指向int类型变量
double *q; // 只能指向double类型变量
char *r; // 只能指向char类型变量
可能有人会说不都是变量的地址吗,这些地址值都是整数,为什么不能用int整型变量来保存各种类型变量的地址呢?首先地址范围和整型范围可能是不同的,其次,更重要的是指针变量最重要的作用是通过地址访问它指向的变量,因为不同类型的变量有不同的处理机制,所以指针变量要知道它指向的变量的类型,故指针变量本身是有类型的。
下面代码是在声明指针变量p的同时给它赋初值
int i;
int *p = &i;
相当于
int i, *p;
p = &i;
在p已经被声明过后,并且p已经指向了某个变量后,*p就是那个变量的别名。再出现类似的赋值就是一种含义上的错误。
int i, j;
int *p = &i;
*p = &j; // 含义上的错误
*p = 10; // 等价于将10赋值给i
注意,下面声明
int *p, q;
声明了指向整型变量的指针变量p和普通整型变量q。
int* p, q;
如果把*和int放在一起,声明的两个变量p和q,仍然p是指针变量,q是整型变量。因为本质上*是用来修饰p的,指针变量的声明
int *p;
不论*是紧邻nt还是p,都应该被解读为,*说明p是指针变量,而前面的int说明p是用来指向int变量的。这与数组的声明有点类似,
int a[10];
如果要声明两个指针变量,需要在每个指针变量前放置*
int *p, *q;
11.1.3 指针变量的赋值
如果两个指针变量类型相同,可以相互赋值。假设声明如下,
int i, j, *p = &i, *q;
赋值语句
q = p;
将p的值(i的地址)赋值给q,效果是q和p同指向i
由于p和q都指向i,可以通过p和q来访问i
*p = 1;
*q = 2;
不要把
q = p;
和
*q = *p;
混淆了。第一句话是指针赋值,第二句话不是,它是指针引用的变量之间的赋值。比如,
p = &i;
q = &j;
i = 1;
*q = *p;
上述赋值相当于
j = i;
11.1.4 用NULL地址来安全的使用指针变量
就像普通变量没有赋初值的时候不能访问(读取它的值),以及访问数组元素时下标不能越界(访问了未申请的地方),指针变量也是这样,
int *p;
printf("%d\n", *p);
因为p还没有初始化,它的值可能是乱七八糟的值,或者说它指向了某个未知的地方,对这个地方的访问会导致未定义的行为。
如果未初始化的p的值恰好是有效的内存地址,下面的赋值会试图修改存储在该地址的数据
int *p;
*p = 1; // 错误
对内存中未知位置的修改是危险的,可能会导致未定义的行为或者系统崩溃。只有让指针指向某个声明了的变量后,对p的间接引用才是安全的,因为此时实际上是对那个变量的引用。
为了安全的访问指针变量,在没有明确p应该指向哪个变量之前(对p进行实际的初始化),可以用地址值NULL来初始化指针变量
int *p = NULL;
NULL 指针是定义在头文件stdio.h中的值为零的常量。
#define NULL 0 // 在stdio.h头文件中
在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。如果指针变量的值为0则明确的表明该指针不指向一个有效的(可访问的)内存位置,或者说它还没有指向某个变量,不能对它进行访问(使用间接寻址运算符*),如果对它访问,则系统一定检测出异常。
#include <stdio.h>
int main ()
{
int *ptr = NULL;
printf("ptr 的地址是 %p\n", ptr );
*ptr = 1; // 导致程序检测出异常
getchar();
return 0;
}
这比只声明指针变量p,但是没有对它初始化,即让它的值是完全未知的好,因为万一未初始化的p的值恰好指向有效的内存地址,上面的赋值语句会试图修改存储在该地址的数据。而此时系统可能检查不出来这样的问题。这是程序很大的隐患。
为了进一步提高程序的安全性,还可以在使用指针变量前,判断它的值是否为空:
int *p = NULL;
... // 在使用p之前,应该让p指向某个整型变量
if (p != NULL){
// p已经指向了某个变量,可以使用*p了
}else{
// p没有指向某个变量,不能使用*p
}
11.1.4 常见问题
问题1. 只能给变量或者说“左值”取地址,表达式不可以。
int i = 0, *p;
p = &i; // 正确,i是变量,即左值,有明确的地址
p = &(i + 10); // 错误,i+10是表达式,不属于“左值”,不能对它取地址
问题2. 指针变量还未初始化就访问
int *p;
*p = 10;