通过指针,可以简化一些C编程任务的执行, 还有一些任务,如动态内存分配,没有指针时无法执行的.
每一个变量都有一个内存位置,每一个内存位置都定义了可使用连字号(&)运算符访问的地址,它表示了在内存中的一个地址:
#include <stdio.h>
int main()
{
int var1;
char var2[10];
printf("var1变量的地址: %p\n", &var1 );
printf("var2变量的地址: %p\n", &var2 );
return 0;
}
一. 什么是指针
指针是一个变量,其值为另一变量的地址,即,内存位置的直接地址.就像其他变量或者常量一样,必须在使用指针存储其他变量地址之前,对其进行声明,指针变量声明的一般形式为:
type *var-name;
int *ip; //一个整型的指针
double *dp; //一个double类型的指针
float *fp;
char *ch; //一个字符型的指针
所有实际数据类型,不管是整型,浮点型,字符型,还是其他的数据类型,对应指针的值的类型都是一样的,都是一个代表内存地址的十六进制数.
不同数据类型的指针之间唯一的不同是:指针所指向的变量或常量的数据类型是不同的.
二. 怎么使用指针
- 定义一个指针变量
- 把变量地址赋值给指针
- 访问指针变量中可用地址的值
# include <stdio.h>
int main()
{
int var=20;
int *p;
ip=&var; //在指针变量中存储var的地址
printf("address of var variable: %p\n", &var);
//在指针变量中存储的地址
printf("address stored in ip variable: %p\n", ip);
//使用指针访问值
printf("value of *ip variable: %d\n", *ip);
return 0;
}
代码被编译和执行后产生的结果:
Address of var variable: bffd8b3c
Address stored in ip variable: bffd8b3c
Value of *ip variable: 20
三. NULL指针
在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个NULL值是一个良好的编程习惯.赋为NULL值的指针被称为空指针.
NULL指针时一个定义在标准库中的值为零的常量
实例:
#include <stdio.h>
int main()
{
int *ptr =NULL;
printf("ptr 的地址是 %p\n", ptr );
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
ptr 的地址是 0x0
在大多数的操作系统上,程序不允许访问地址为0 的内存,因为该内存是操作系统保留的,然而,内存地址0有特别重要的意义,它表明该指针不指向一个可访问的内存位置.按照惯例,如果指针包含空值(零值),则假定它不指向任何东西.如需检查一个空指针,可以使用if语句
if(ptr) //如果p非空,则完成
if(!ptr) //如果p为空,则完成
四. 指针详解
-
指针的算术运算
可以对指针进行四种算术运算: ++, --, +, -
指针是一个用数值表示的地址,假设
ptr
是一个指向地址1000的整型指针,是一个32位的整数,让我们对该指针进行下面的算术运算.(存地址就是指向地址!)(一个指针变量当然只存一个地址,)ptr++;
在执行完上述的运算之后,
ptr
将指向位置1004,因为ptr
每增加一次,它都将指向下一个整数位置,即当前位置往后移动4字节.这个运算会在不影响内存位置中实际值的情况下,移动指针到下一个内存位置.如果ptr
指向一个地址为1000的字符,上面的运算会导致指针指向位置1001,因为下一个字符位置在1001- 指针的每一次递增,它会指向下一个元素的存储单元
- 指针的每一次递减,它都会指向前一个元素的存储单元
- 指针在递增和递减跳跃的字节数取决于指针所指向变量数据类型长度,比如int就是4个字节
递增一个指针
在程序中常常用指针代替数组,因为变量指针可以递增,而数组不能递增,数组可以看成一个指针常量.下面的程序递增变量指针,一遍顺序访问数组中的每一个元素:
实例:
#include <stdio.h> const int MAX = 3; int main() { int var[]={10, 100, 200}; int i, *ptr; //指针中的数组地址 *ptr =var; for(i=0; i<MAX; i++){ printf("存储地址:var[%d]=%x\n", i, ptr); printf("存储值: var[%d]=%d\n", i, *ptr); //移动到下一个位置 ptr++; } return 0; }
当上面的代码被编译和执行时,会产生下列结果:
存储地址:var[0] = bf882b30 存储值:var[0] = 10 存储地址:of var[1] = bf882b34 存储值: var[1] = 100 存储地址:of var[2] = bf882b38 存储值:var[2] = 200
递减一个指针
同样的,对指针进行递减运算,即把值减去其数据类型的字节数.
#include <stdio.h> const int MAX = 3; int main() { int var[] = {10, 100, 200}; int i; *ptr; //指针中最后一个元素的地址 ptr = &var[MAX-1]; for(i=MAX; i>0; i--){ printf("存储地址:var[%d]=%x\n", i-1, ptr); printf("存储值:var[%d]=%d\n", i-1, *ptr); //移动到下一个位置 ptr--; } return 0; }
当上面的代码被编译和执行时,会产生下列结果:
存储地址:var[2] = 518a0ae4 存储值:var[2] = 200 存储地址:var[1] = 518a0ae0 存储值:var[1] = 100 存储地址:var[0] = 518a0adc 存储值:var[0] = 10
指针的比较
指针可以用关系运算符进行比较,如==,<和>. 如果
p1
和p2
指向两个相关的变量,比如同一个数组中的不同元素,则可以对p1
和p2
进行大小比价 -
指针数组
可以定义用来存储指针的数组
我们可以让数组存储指针.指向整数的指针数组的声明:
int *ptr[MAX];
在这里,我们把
ptr
声明为一个数组,由MAX个整数指针组成,因此,ptr
中的每个元素都是一个指向int值得指针.下面实例用到了三个整数,把它们的地址存储在一个指针数组中#include <stdio.h> const int MAX = 3; int main() { int var[]={10, 100, 200}; int i, *ptr[MAX]; for(i=0;i<MAX;i++){ ptr[i]=&var[i]; //赋值为整数的地址 } for(i=0; i<MAX; i++){ printf("Value of var[%d]=%d\n", i, *ptr[i]); } return 0; }
可以用一个指向字符的指针数组来存储一个字符串列表
#include <stdio.h> const int MAX = 4; int main() { const char *names[]={ "Zara Ali", "Hina Ali", "Nuha Ali", "Sara Ali", }; int i = 0; for(i=0; i<MAX; i++) { printf("Value of names[%d] = %s\n", i, names[i] ); } return 0; }
当上面的代码被编译和执行时,它会产生下列结果:
Value of names[0] = Zara Ali Value of names[1] = Hina Ali Value of names[2] = Nuha Ali Value of names[3] = Sara Ali
-
指向指针的指针
-
传递指针给函数
通过引用或地址传递参数,使得传递的参数在调用函数中被改变
-
从函数返回指针
允许函数返回指针到局部变量,静态变量和动态内存分配
int *p[3]; -- 首先从 p 处开始, 先与 [] 结合, 因为其优先级比 * 高,所以 p 是一个数组, 然后再与 * 结合, 说明数组里的元素是指针类型, 然后再与 int 结合, 说明指针所指向的内容的类型是整型的, 所以 p 是一个由返回整型数据的指针所组成的数组。本质上是数组,数组里面是指针,指针指向int类型数据.
int (*p)[3]; -- 首先从 p 处开始, 先与 * 结合,说明 p 是一个指针然后再与 [] 结合(与"()"这步可以忽略,只是为了改变优先级), 说明指针所指向的内容是一个数组, 然后再与int 结合, 说明数组里的元素是整型的。所以 p 是一个指向由整型数据组成的数组的指针。本质上是指针,指向由3个int类型数据元素组成的数组.
int **p; -- 首先从 p 开始, 先与 * 结合, 说是 p 是一个指针, 然后再与 * 结合, 说明指针所指向的元素是指针, 然后再与 int 结合, 说明该指针所指向的元素是整型数据。由于二级指针以及更高级的指针极少用在复杂的类型中, 所以后面更复杂的类型我们就不考虑多级指针了, 最多只考虑一级指针。
int p(int); -- 从 p 处起,先与 () 结合, 说明 p 是一个函数, 然后进入 () 里分析, 说明该函数有一个整型变量的参数, 然后再与外面的 int 结合, 说明函数的返回值是一个整型数据。
int (*p)(int); -- 从 p 处开始, 先与指针结合, 说明 p 是一个指针, 然后与()结合, 说明指针指向的是一个函数, 然后再与()里的 int 结合, 说明函数有一个int 型的参数, 再与最外层的 int 结合, 说明函数的返回类型是整型, 所以 p 是一个指向有一个整型参数且返回类型为整型的函数的指针。
int *(*p(int))[3]; -- 可以先跳过, 不看这个类型, 过于复杂从 p 开始,先与 () 结合, 说明 p 是一个函数, 然后进入 () 里面, 与 int 结合, 说明函数有一个整型变量参数, 然后再与外面的 * 结合, 说明函数返回的是一个指针, 然后到最外面一层, 先与[]结合, 说明返回的指针指向的是一个数组, 然后再与 * 结合, 说明数组里的元素是指针, 然后再与 int 结合, 说明指针指向的内容是整型数据。所以 p 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数。
‘指针变量’存的是’内存地址’, 任何与指针相关的概念,都可以联系内存地址加以理解.
内存是线性的,内存以地址空间的形式呈现给我们看,所以说所谓的地址空间也是线性的,指针存放的是内存地址,所以可以对地址做++, --这样的运算.
两种情况不赋NULL,是坏习惯:
- 初始化指针不赋NULL: 因为这样的指针会指向一片未知的区域,这样的指针不是空指针,但指向一片访问受限制的内存区域,这就叫做"野指针". 这会导致一些报错比如"segmentation fault"
- free()后指针不赋NULL,为指针分配内存之后,指针便可以指向一片合法可使用的内存,但使用free()释放那片内存之后,指针依然存着那片内存,但这片内存已经释放,不可访问,若不小心使用了这个指针,就会报错.这个叫做"悬空指针"