目录
前言
指针是C语言的灵魂,本文是对指针的一个大概总结。
一、指针简介
1.什么是指针
一般来说,指针被认为是一个概念,是计算机内存地址的代名词之一,而指针变量本身就是变量,存放内存的地址。在大多数情况下,不强调它们的区别,并且把指针变量简称为指针,但是实际上它们的意义不同。本文如果没有说明,把指针和指针变量同等对待。
2.指针变量的定义
(1)定义指针变量的一般形式为:类型名* 指针变量名
①指针声明符*表明声明的变量时指针
②类型声明符表明指针所指向对象的类型
例如:int* pi; //定义了一个指针变量pi,指向整型变量。char* cp; //定义了一个指针变量cp,指向字符型变量
注意:①°无论何种类型的指针变量,它们都是用来存放地址的,因此
指针变量自身所占内存的大小
和
它所指向的变量数据类型
无关
,尽管不同类型的变量所占内存空间不同,但是
不同类型指针变量所占内存空间大小相同
。
②°指针声明符*不是指针的组成部分,如:int* p;说明p是指针变量,而*p不是。
③°指针的类型和它所指向变量的类型必须相同。
3.指针变量的初始化
指针变量需要先赋值再使用,看下面赋值语句:
int i,*p;
p = &i;
p = 0;
p = NULL;
p = (int*)1732;
①对于赋值的第一条语句:&把i的地址取出,赋给指针变量p,这是很常用的一种赋值方法
②对于赋值的第二、三条语句:NULL在stdio.h的文件中有定义,其值为0,这两条语句把0赋给指针,代表该指针为空指针,不指向任何单元
③对于赋值的第四条语句:使用强制类型转换(int*)来避免编译错误,表示p指向的地址为1732的int型变量。但是我们不建议把绝对地址赋值给指针,NULL除外。
注意:
①在指针变量定义或者初始化时变量名前面的*,只表示该变量是一个指针变量,它不是间接访问符
②不能用数值作为指针变量的值(0除外),int* p = 100;//error int* p = 0;//ok
③可以用初始化了的指针变量给另一个指针变量作初始值
④把一个变量的地址作为初始化值赋给指针变量时,该变量必须在此之前已经定义。因为变量只有在定义后才被分配单元,它的地址才能赋给指针变量。
4.指针类型的意义
(1)
指针的类型决定了指针向前或者向后走一步有多大(距离)。
地址是按字节编址的,在C中,指针加1指的是增加一个存储单元。如:
#include <stdio.h>
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc + 1);
printf("%p\n", pi);
printf("%p\n", pi + 1);
return 0;
}
编译运行该代码,输出如下:
可以看到char类型是1个字节,所以当指针pc+1时,指针pc只向前走了1个字节;而int类型是4个字节,当指针pi+1时,指针pi向前走了4个字节
(2)
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
#include <stdio.h>
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
int *pi = &n;
*pc = 0;
*pi = 0;
return 0;
}
char*
的指针解引用就只能访问一个字节,而
int*
的指针的解引用就能访问四个字节。
5.指针的大小
(1)在
32位的机器
上,
假设有
32
根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者
0);
则地址是32
个
0
或者
1
组成二进制序列,那地址就得用
4
个字节的空间来存储,所以一个指针变量的大小就应该是
4个字节
。
(2)那如果在
64
位机器
上,如果有
64
个地址线,那
一个指针变量的大小是8个字节
,才能存放一个地址。
6.指针的运算
(1)指针+-整数
:
在指针类型的意义那里我们已经见过啦
(2)指针-指针
结果的绝对值表示它们之间相隔的数组元素的数目,前提:两个指针指向同一块区域
TIP:由此我们可以想到,指针-指针可以应用到我们自己模拟实现strlen()的功能。
(3)指针的关系运算
(两个相同类型的指针)
注意:C语言标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。(第二种方法
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行
)
7.野指针
(1)
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
(2)野指针的成因:
①指针未初始化
#include <stdio.h>
int main()
{
int *p; //局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
②指针越界访问
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = arr;
int i = 0;
for(i=0; i<=11; i++)
{
*(p++) = i; //当指针指向的范围超出数组arr的范围时,p就是野指针
}
return 0;
}
③
指针指向的空间释放(后续笔记再总结)
(3)规避野指针的做法:
①指针初始化
②小心指针越界
③指针指向空间释放即使置NULL
④避免返回局部变量的地址
⑤指针使用之前检查有效性
如:看以下代码:
#include <stdio.h>
int main()
{
int *p = NULL;
//....
int a = 10;
p = &a;
if(p != NULL)
{
*p = 20;
}
return 0;
}
8.二级指针
(1)二级指针:指向指针的指针(
指针变量也是变量,是变量就有地址,指针变量的地址存放在指针里面
)
(2)一般定义为:类型名** 变量名
如:int** pa;//int* *pa,pa是一个指针,pa指向的变量是一个int*的变量
(3)二级指针的初始化:
int a = 10;
int* pa = &a;//a的地址存在pa中,pa是一级指针
int** ppa = &pa;//pa的地址存在ppa中,ppa是二级指针
(4)二级指针的操作:
int a = 10, b = 20, t;
int* pa = &a,* pb = &b, pt;
int** ppa = &pa, ** pb = &pb, ppt;
ppt = ppb; ppb = ppa; ppa = ppt;//①
pt = pb; pb = pa; pa = pt;//②
t = b; b = a; a = t;//③
部分变量的值变化如表:
**ppa | **ppb | *pa | *pb | a | b | |
原 | 10 | 20 | 10 | 20 | 10 | 20 |
① | 20 | 10 | 10 | 20 | 10 | 20 |
② | 10 | 20 | 20 | 10 | 10 | 20 |
③ | 20 | 10 | 10 | 20 | 20 |
10 |
解析如下:
9.指针表示法和数组表示法
(1)一维数组
①arr[i] 等价于 *(arr+i),可以认为*(arr+i)的意思是“到内存的arr位置,然后移动i个单位,检索存储在那里的值”。
②arr + i 等价于 &arr[i],前面我们说过数组名是数组首元素的地址,所以arr是该数组首元素的地址,首元素的地址+i得到的是第i个元素的地址,即&arr[i]
注意:不要混淆*(arr+2)和*arr+2;前者的意思是arr第2个元素的值,后者的意思是arr第0个元素的值+2
(2)二维数组
假设有定义:int a[3][2];
①a:
数组名是数组首元素的地址
。我们可以
把二维数组a看成是由a[0],a[1],a[2]组成的一维数组
,而a[0],a[1],a[2]各自又是一个一维数组。因此数组名a是a[0](一个内含两个int值的数组)的首元素地址,
a[0]是该数组首元素(a[0][0])的地址
。所以我们可以知道两个等价关系:
a 等价于 &a[0]; a[0] 等价于 &a[0][0]
②a[0]是该数组首元素(a[0][0])的地址,所以,
*(a[0])等价于a[0][0]
的值;与此类似,*a代表该数组首元素(a[0])的值,而a[0]本身又是一个int类型的地址,所以该值的地址为&a[0][0],换言之,
*a就是&a[0][0]
。
**a等价于*&(a[0][0])
,因为*a就是首行首元素的地址,所以再对其进行解引用才能找到首元素的值。
我们来捋一下:a即&a[0], a[0]即&a[0][0], 因此,我们可以得出&a[0] 等价于 &&a[0][0]。
其实二维数组名相当于一个二级指针,而a[0]相当于一级指针。
注意:二级指针和二维数组名是两码事
③由于a[i] 等价于 *(a+i),我们也可以得出a[i][j] 等价于 *( *(a+i) + j)或者*(a[i] + j)。
我们来剖析一下 *( *(a+i) + j)这个式子:a是该二维数组首元素的地址(第0行的地址),a+i就是第i行的地址
*(a+i)就是第i行首元素的地址
*(a+i)+j就是第i行,第j列的地址
*(*(a+i)+j)就是第i行,第j个元素
如:*(*(a+2)+1)就是第2行第1列(下标从0开始计数)的元素值,即a[2][1]。
说了这么多,我们上个表格来看看它们之间的等价关系:
二级指针 | 一级指针 | 数组元素 | ||||||
a | &a[0] | &&a[0][0] | *a | a[0] | &a[0][0] | **a | a[0][0] | *(a[0]+0) |
二、指针数组和数组指针
1.指针数组
其实C语言中数组可以是任何类型的,如果数组的各个元素都是指针类型,用于存放内存地址,那么这个数组就是指针数组。
(1)一维指针数组定义的一般格式为:类型名* 数组名[数组长度]
如:int* arr[5];//
arr
是一个数组,有五个元素,每个元素是一个整形指针。该数组的类型是int* []。(去掉变量名就是类型)
(2)指针数组的初始化:
指针数组的各个元素是指针,用于存放地址,因此,我们可以用指针(地址)作为初始化内容,如:
//(1)
int arr[] = {1,2,3};
int* parr[]={arr};
//(2)
char* color[5] = {"red", "blue", "yellow", "green", "black"};
//字符串常量实质上是一个指向该字符串首字符的指针常量
(3)指针数组和二级指针
char* color[5] = {“red”, “blue”, “yellow”, “green”, “black”};
①数组名是数组首元素(一个指向“red”的字符指针)的地址,所以color其实相当于一个二级指针
②color[1]等价于*(color+1):color+1是数组第1个元素的地址,*(color+1)访问的是数组的第一个元素,它是一个字符指针,指向“blue”的首字符”b”
③*(*(color+2)+4):同理:*(color+2)访问的是“yellow”的首字符“y”的地址,加4,向后偏移4,找到“o”的地址,再解引用,得到“o”
④(*color+1)[1]:*color实际上访问的是”red”的首字符“r”的地址,加1,向后偏移1,找到”e”的地址,按照[]的访问,向后再偏移1个字符的地址,访问到“d”.这条表达式,它等价于*(*color+1+1)
⑤*color[3]+2:color[3]找到的是第3个元素的地址,*color[3]找到的是第三个元素的首字符“g”,字符”g”+2,实际上是”g”的ASCII码值+2,得到105,105对应的是字符“i”。
2.数组指针
(1)数组指针:可以指向数组的指针
如:int (*p)[10];//
p先和*结合,说明p是一个指针变量,然后指针指向的是一个大小为10的数组,该数组的每个元素是一个整型。所以p是一个指针,指向一个数组,叫数组指针,该
数组指针的类型为int(*)[10]。
我们可能会疑问:既然数组指针的类型是int (*)[10],数组指针的变量名为p,为什么不写成int (*)[10] p;呢?其实是为了好看方便,所以写成了int (*p)[10];
数组指针的一般形式
为:类型名 (*指针变量名)[数组长度]。类型名指的是指针指向的数组的元素的类型。
(2)&数组名和数组名
①第一组的+4的解释:arr是数组首元素的地址,因为arr的类型是int*,所以arr+1跳过4个字节
②第二组的+4的解释:&arr[0]是数组首元素的地址,同样的,&arr[0]的类型也是int*,所以&arr[0]+1也是跳过4个字节
③第三组的+40的解释:&arr是数组的地址,它的类型是int(*)[10],一个指向大小为10的整型数组的指针,所以它+1,应该跳过一个数组的大小10*4=40。
(3)数组指针的使用
数组指针里面存放的是数组的地址。我们来看2个代码:
①
第一个for循环打印出来的是随机值的原因如图,解决方案为第二个for循环,我们可以把这个一位数组看成是二维数组的第一行,这样*(p+0)访问的是一维数组首元素的地址,然后(*(p+0))[i],就可以访问后面的元素了,(*(p+0))[i]等价于p[0][i]。其实,这样传参大可不必,太容易出错了,我们应该这样传参:print_arr(arr);然后用一级指针接收。
②
#include <stdio.h>
void print_arr(int (*arr)[5], int row, int col)
{
int i;
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
print_arr(arr, 3, 5);
return 0;
}
数组名arr,表示首元素的地址,但是二维数组的首元素是二维数组的第一行,所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址,可以数组指针来接收。
(4)我们再来看一句代码的意思:
int (*parr[10])[5];首先parr先和[]结合,说明它是一个数组,该数组有10个元素,每个元素是一个数组指针,该指针指向5个元素,每个元素的类型是int
三、函数指针
1.函数指针定义的一般形式为:(类型名)(*变量名)(参数类型表)
类型名:指定函数的返回类型;变量名:指向函数的指针变量的名称
如:int(*funp)(int,int);//定义了一个函数指针funp,它可以指向有两个整型参数且返回值类型为int的函数,该
函数指针的类型为int(*)(int,int)
,由于和数组指针相同的道理,我们不写成int(*)(int,int) funp;
2.通过函数指针调用函数
(1)在使用函数指针之前,要先对它赋值。赋值时,将一个
函数名
赋给函数指针,但是该函数必须已经定义或声明,且函数返回值的类型要和函数指针的类型一样。(
函数名和&函数名意义相同,都代表函数的地址
)(函数指针用来存放函数的地址)
(2)函数调用的一般格式:
(*函数指针名)(参数表)
如:假设fun(x, y)已经有定义,现在要调用fun函数
int(*funp)(int,int) = fun;
fun = (3,5);
(*funp)(3,5);
//两者完全等价
3.函数指针作为函数参数:
C语言的调用中,函数名或已赋值的函数指针也能作为实参,此时,形参就是函数指针,它指向实参所代表函数的入口地址,如:
int Add(int x,int y)
{
return x + y;
}
int main()
{
int (*pf)(int, int) = Add;
int ret;
ret = (*pf)(4, 5);
printf("%d\n", ret);
return 0;
}
编译运行该代码,输出如下:
9
其实ret = (*pf)(4,5);也可以写成ret = pf(4,5);int (*pf)(int, int) = Add;Add可以赋值给pf并且不报任何警告,所以Add和pf可以说是等价,既然Add(4,5);那么我的pf也可以写成这样pf(4,5)。但是前者的写法更好理解,pf指向Add的地址,解引用后找到函数的内容。
4.一些有趣的代码:
(1)
(
*
(
void (*)()
)0
)()
;//我们一层层的往外剥。把0强制转换成“指向返回值为void的函数指针类型”,解引用0地址,就是去0地址处的这个函数,被调用的函数是无参,返回类型为void。该代码为一次函数调用。其实,我们也可以简化一下代码,我们设fp是一个指向返回值为void类型的函数的指针,那么fp的声明如下:void(*fp)();函数调用如下:(*fp)();最后我们用
(
void (*)()
)0
来替换fp,即我们的(*(void (*)())0)();。
(2)void (*
signal(int ,
void(*)(int)
)
)(int);//这个其实是一个函数声明,声明的函数名是signal,signal函数有2个参数,第一个是int类型,第二个是void(*)(int)的函数指针类型,signal函数的返回类型是void(*)(int)。其实,这个代码我们也可以简化,看如下代码:
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);
解释:把void(*)(int)重新命名为pfun_t,signal函数的返回类型是void(*)(int)即pfun_t。注意:不要写成typedef void(*)(int) pfun_t;这是错的。
四、函数指针数组
1.函数指针数组顾名思义就是存放函数指针的数组。
2.函数指针数组的定义:一下哪个代码才是?
int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];
parr1
先和
[]
结合,说明
parr1
是数组,数组的内容是
int (*)()
类型的函数指针。该函数指针数组的类型是int(*[])()。
3.函数指针数组的初始化:既然
函数指针数组存放的是函数指针,那么我们可以用函数指针(函数的地址)给函数指针数组初始化。见下面的函数指针数组的用途。
4.函数指针数组的用途:转移表
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
...
int main()
{
//int (*pf)(int, int) = Add;
//int (*pf)(int, int) = Sub;
int (*pfArr[2])(int, int) = { Add,Sub };//初始化
...
return 0;
}
五、指向函数指针数组的指针
指向函数指针数组的指针:首先它是一个
指针,指针指向一个数组
,数组的元素都是
函数指针。它放的是函数指针数组的地址。
如:
int (*pfArr[5])(int, int); //–pfArr是一个函数指针的数组
int (*(*ppfArr)[5])(int, int) = &pfArr;//-pfArr是一个指向函数指针数组的指针,它的类型是int (*(*)[5])(int, int)
六、回调函数
1.回调函数:
一个通过函数指针调用的函数
。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
2.常见的回调函数调用:qsort()–快速排序的库函数,什么类型的数据都可以排序
(1)void*–无具体类型的指针,能够接收任意类型的地址,缺点:不能进行运算。(因为无类型,你不知道它加减整数和解引用时跳几个字节)
int a = 10; void* p = &a; p++;//error
(2)
qsort()这个库函数需要引用头文件<stdlib.h>或<search.h>
其函数声明为:
void
qsort(
void
*
base
,
size_t
num
,
size_t
width
,
int
(
*
cmp
)(const
void
*
elem1
,
const
void
*
elem2
)
);
①base:要排列的数据的开始
②num:要排序的元素个数
③width:一个元素的大小,单位是字节
④int (*cmp )(const void *elem1, const void *elem2 ):cmp指向的是:排序时:用来比较2个元素的函数
compare
(
(void
*)
elem1
,
(void
*)
elem2
);
qsort在排序过程中调用该函数一次或多次,该函数比较两个元素并返回指定其关系的值,每次调用函数都会有返回值,如图:
当elem1>elem2时,qsort将调换两元素之间的位置,这是一个升序排序,如果想降序,可以转换大于和小于的含义,即elem1<elem2时,qsort才调换两元素之间的位置。
(3)举例:比较整型数据的大小、结构体内容,看如下代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Stu
{
char name[20];
int age;
};
//比较整型数据的大小
int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;//e1,e2为void*,无法解引用,所以强制类型转换int*
//此处为升序,如果想降序,可以return *(int*)e2 - *(int*)e1;
}
//比较结构体内容
int cmp_by_name(const void* e1, const void* e2)
{
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
int main()
{
int arr[] = { 1,5,8,9,0,2,3,7,4,6 };
struct Stu s[3] = { {"张三",15},{"李四",30},{"王五",10} };
int sz = sizeof(arr) / sizeof(arr[0]);
int sz = sizeof(s) / sizeof(s[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_int);//将数组中元素排序
qsort(s, sz, sizeof(s[0]), cmp_by_name);//按照名字排序
return 0;
}
调试结果如下:
注意:cmp_int,cmp_by_name这两个函数不是我们去调用的,而是我们把它的地址传给qsort,由qsort调用的