指针概述
指针存放的都是首地址。
1、定义与初始化
形式:<数据类型>* <变量名> = <地址>;
int a = 10;
int *p = &a;
指针的类型不同,p++时的偏移地址量不同,偏移地址 = sizeof(类型)Byte
注意点:
- 指针的类型要与数据的类型保持一致,a为int,那么p就是int*," * " 称为指针运算符
- 指针应该赋值地址,&为取地址符,存放的是数据的首地址
- 指针变量不能赋值普通整数(0除外),不能赋值p=0x1234,而应该赋值p=(类型*)0x1234
2、目标与解引用
目标:指针指向的内存区域的数据。上述中,a就是目标。
解引用(间接访问):使用*p对目标进行访问," * "称为解引用。上述中*p就是解引用
注意:在使用(*p)++时,需要加括号。" * "的优先级低于 " ++ " " -- "
对于上述代码,指针的几种方式有:p、*p、&p,关系如下:
- p: 指针变量, 它的内容是地址量
- *p: 指针的目标,它的内容是数据
- &p: 指针变量占用的存储区域的地址,是个常量
3、指针大小与寻址空间
指针的大小与系统的位数有关。
- 32位的系统有32根地址线,因此指针大小为32位,4字节
- 64位的系统有64根地址线,因此指针大小为64位,8字节
注意:指针存放的是地址,地址的大小是固定的。不论是什么类型的指针,大小都为4或8字节。
寻址空间与指针的位数有关。
- 32位的指针大小可以寻址0~2^32-1范围
- 64位的指针大小可以寻址0~2^64-1范围
4、空指针
空指针并不是指没有赋值的指针,而是赋值为NULL的指针。NULL就是(void*)0
使用空指针的原因:
- 指针在定义时没有初始值,值是不确定的。给一个空指针可以防止野指针的出现。
- 空指针指向0,这个地址不允许访问,访问一定出现段错误,因此可以很快发现指针使用错误
良好的编程习惯:
- 在定义指针时,要么是赋值非空指针,要么是赋值NULL,防止野指针出现
- 在函数传入指针参数时,首先判断是否为空,防止后续产生段错误。
5、野指针
野指针指的是指向了一个不确定空间的指针。
有一些野指针在编译上不会产生任何问题,逻辑上却会产生很严重的莫名奇妙的bug
产生野指针的原因:
- 指针没有初始化,比如只定义了int* p;而没有赋值NULL或其他值
- 指针越界访问,比如char a[3]最大索引为2,然而去访问了a[3],产生了越界,就是野指针
- 指针指向空间被释放,比如malloc开辟的空间,free之后再去访问,这就是野指针
指针运算
1、指针±常数
指针±常数的符号有:+、- 、++、--
指针±常数后的值与指针的类型有关,参与运算的是指针保存的地址。
- 运算结果=当前位置 + 常数 * sizeof(类型),该公式适用全部指针,包括多级指针
对如下程序进行分析:
int a[5] = {1,2,3,4,5};
int* p = a;
p++;
这里的p为a的首地址,假设为0x00。p++之后,p的值 = 0x00+1*sizeof(int),这个值就是&a[1]
注意:
- 这里的p应该与a类型一致,int对应int,不能用char* p去访问int a[5]
- " 指针+常数 "代表指针向高地址移动," 指针-常数 "代表指针向低地址移动
2、指针-指针
指针-指针运算需要两个参与运算的指针的类型相同,这才是有效运算。运算结果代表这两个指针之间相差了多少个元素。比如上述代码,p++之后值为0x04,p的值为0x00,相减之后并不为4,而是为1,说明p++与p之间相差了一个int元素。代码验证如下:
3、自加自减运算注意点
" ++ "、" -- "的运算优先级比" * "的优先级要高,所以要注意运算的结合顺序。
下面以一个小代码为基础分析*p++、(*p)++、++p、++*p的含义
int a[5] = {1,2,3,4,5};
int* p = a;
3.1 *p++含义
p++先运算,但++为先用后加,所以运算的值是*p,结果为1。之后p指针+1,指向2
训练:分析*p++ = 3的值
p++先结合,先用再加,所以当前*p=1,之后将3进行赋值,所以a[0]变为3,最后指针+1,指向2
3.2 (*p)++含义
*p先运算,相当于把1取出来,之后再++。同样是先用再加,所以运算值为1,之后1+1变为2
3.3 *++p含义
++p先结合,先加后用。所以运算的值是*(p+1),结果为2,最终p指向2
3.4 ++*p含义
这时++与*已经不在有优先级的事情,因为没有运算的考虑点。在前面*p++时,我们不知道是先和*还是先和++结合,所以考虑优先级。对于++*p,p只能和*结合,所以含义如下:
*p结合之后再++,是先加再用,所以运算结果是1+1=2,最终a[0]=2,*p依旧指向a[0]
4、指针比较
指针可以进行比较,运算符有:>、<、==、!=
指针比较的含义:
- 与0比较,判断指针是否为空指针NULL
- 与正常指针比较,存放地址大的指针>存放地址小的指针,如p1=&a[0],p2=&a[1],则p1<p2
其他指针
1、多级指针
1.1 多级指针的含义
多级指针就是指向指针的指针,它存放的是指针变量的地址。因为存放的依旧是地址,所以多级指针的大小为4/8字节,与指针变量大小一样。
- 指向数据的指针是一级指针,用int* p = &a表示
- 指向指针的指针是二级指针,用int** pp = &p表示
1.2 多级指针偏移量
多级指针的偏移量同样适用公式" 运算结果=当前位置 + 常数 * sizeof(类型) "。
- 二级指针存放的是int*,所以偏移sizeof(int*)大小,int*是地址,所以大小为4/8字节
- 三级指针存放的是int**,所以偏移sizeof(int**)大小,int**是地址,所以大小为4/8字节
1.3 练习:使用多级指针访问指针数组。
分析如下代码的运行结果:
#include <stdio.h>
int main(){
char* a[] = {"work","at","alibaba"};
char** pa = a;
pa++;
printf("%s",*pa);
return 0;
}
分析:a为一个指针数组,pa是二级指针,指向指针数组头。pa++就是数组偏移,所以指向了a[1]这个位置,此时*pa就是将at的首地址取出,printf中的%s只需首地址即可打印字符串,因此输出结果是at。
2、void指针
2.1 void指针的含义
形式:void* <变量名> 如:void* p;
void指针又称为万能指针、泛指针、通用指针。当一个指针的类型为void时,所指向的对象仅仅是一个地址,而不代表任何类型,系统并不知道指针的偏移值大小,因此void指针不能进行算术运算,如"++"运算。
2.2 void指针使用规则
- 使用前必须进行初始化,赋值时,指针类型可为任意。
- 解引用必须进行强制转换,以告诉系统指针的偏移值大小。
2.3 解引用时发生了什么
当*p执行时,系统根据p的类型来判断拿出几个字节,来组成一个完整的数据。比如int*p就拿出4个字节,char*p就拿出1个字节。对于void类型,系统不知道要拿出多少字节,所以需要强制转换,让系统知道这件事。
3、const修饰的指针
3.1 const修饰的指针的种类
- 指针本身不能修改,即:p的值不能改。这称为" 常量化指针变量 "
- 指针指向的数据不能修改,即:*p的值不能改。这称为" 常量化指针目标 "
- 指针本身和指向的数据都不能修改,即:p和*p的值都不能改
3.2 const 指针各个种类的命名规则
就近原则:const修饰谁,谁不能改变。
- 常量化指针变量:int* const p,const修饰的是p,所以p不能改
- 常量化指针目标:const int* p,const修饰的是*p,所以*p不能改
- 两者结合:const int* const p,const修饰了*p和p,所以都不能改
4、main参数(argc,argv)
main的完整函数形式:int main(int argc,const char* argv[])
- argc:命令行参数的个数,就是argv的数组长度。
- argv:参数的指针数组,存放的是参数字符串的首地址。
注意:argv[0]是程序的全名,argv[1]是第一个参数的首地址,程序的全名也算一个参数个数。
编程规范:在main真正执行之前加上参数正确性判断。