目录
0.指针初阶回顾:
-
指针就是个变量,用来存放地址,指针唯一标识一块内存空间。
-
指针的大小固定为4/8个字节(32/64位平台)。
-
指针有类型的,指针的类型决定了指针+-整数的步长,指针解引用操作时的权限。
-
指针的运算
1.字符指针
字符指针 char*
#include <stdio.h>
int main()
{
char str1[] = "hello bit";
const char* str2 = "hello bit";
printf("%s\n", str1);
printf("%s\n", str2);
return 0;
}
字符指针除了表示字符的指针,还可以表示字符串。
const char* pstr = "hello bit.";
字符串可以用字符数组和字符指针来表示。
字符指针表示的字符串是常量字符串。
这里的const在有的编译器可以不加,有的编译器要求严格必须加上。
字符指针表示字符串不是把整个字符串放入字符指针中,本质是把字符串“hello bit“的首字符h的地址放入字符指针pstr中。
内存分区:
栈,堆,数据段(全局变量和静态变量),代码段(可执行代码,常量)
2.指针数组
指针数组顾名思义,就是存放指针的数组。
int* arr1[10];//整形指针数组
char *arr2[4];//一级字符指针数组
char **arr3[5];//二级字符指针数组
3.数组指针
(1)数组指针的定义:
数组指针就是指向数组的指针。
int *p1[10];
int (*p2)[10];
[ ]操作符的优先级是大于 * 操作符的。
p1先和[ ]结合,是一个数组,数组的每个元素都是int*的指针。
p2先和*结合,说明是一个指针,指针指向的类型是int [10],说明p2是指向数组的指针,指向的数组是一个大小为10的整型数组。
p1是指针数组,p2是数组指针。
(2)数组名和&数组名
arr和&arr的值是一样的,但是他们的意义是不一样的。
arr是数组首元素的地址,&arr是数组的地址,也就是数组指针。
&arr+1跳过的就是整个数组的大小。
数组指针解引用,其实就是数组首元素的地址。(这句话非常重要,以后都要用)
(3)数组指针的使用
数组指针一般很少在一维数组中使用
比如这样使用:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int(*p)[10] = &arr;
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("%d ", *(*p + i));
}
return 0;
}
我们会发现这样使用毫无意义,就是在脱kz放p!
其实数组指针一般在二维数组中应用的比较广泛:
void print_arr1(int arr[3][5], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print_arr2(int(*arr)[5], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);//*(*(arr+i)+j)
}
printf("\n");
}
}
int main()
{
int arr[3][5]={1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7};
print_arr1(arr, 3, 5);
print_arr2(arr, 3, 5);
return 0;
}
注意:
数组传参,传的其实都是指针,尽管形参的形式还是个数组,但其实是为了增强代码的可读性。
数组名是数组首元素的地址,二维数组的首元素是二维数组第一行的一维数组,所以二维数组的数组名是第一行一维数组的地址,也就是数组指针(这是重点哦)。
arr[i][j]在底层上就是* (*(arr+i)+j):
解析 arr[i][j],即* (*(arr+i)+j):
arr是二维数组第一行数组的地址。
arr+i是二维数组第i行数组的地址。
*(arr+i):前面说数组指针解引用就是数组首元素的地址,那么*(arr+i)就是第i行数组首元素的地址。
*(arr+i)+j:第i行数组第j个元素的地址。
*(*(arr+i)+j):第i行数组第j个元素
(4)数组指针的数组:
int (*parr1)[10];//parr1是数组指针,数组是大小为10的整形数组
int (*parr2[10])[5];数组指针数组:
parr2[10] : parr2是数组,该数组里面有10个元素
int (*)[5] : 每个元素都是数组指针,指针指向的数组是大小为5的整形数组
4.数组传参和指针传参
一维数组传参:
普通数组传参:
void test(int arr[ ]);
void test(int arr[10]);
void test(int* arr);
指针数组传参:
void test(int* arr[ ]);
void test(int* arr[10]);
void test(int** arr);
二维数组传参:
void test(int arr[3][5]);
void test(int arr[ ][5]);
void test(int arr[ ][ ]); 错误!!
void test(int (*arr)[5]); 二维数组的数组名是数组指针
一级指针传参:
一级指针
变量的地址
普通数组(不是指针数组)的数组名
二级指针传参:
二级指针
一级指针变量的地址
指针数组的数组名
5.函数指针
(1)函数的地址:
int Add(int a, int b)
{
return a + b;
}
int main()
{
printf("%p\n", Add);
printf("%p\n", &Add);
return 0;
}
Add 和 &Add都是函数的地址
(2)函数指针的存放:
int (*pfun)(int , int) = Add(或者&Add);
pfun是函数指针变量:pfun先和*结合,说明pfun是指针。
int (*)(int,int)是函数指针类型
注意:语法上是不允许这么写的:int (*)(int,int)pfun;
(3)使用存放函数指针的函数指针变量来调用函数:
一般调用:int a = Add(3,5);
int b = (*pfun)(4,5); - - - > &Add
int c = pfun(5,5); - - - > Add
两种方式都是可以的。
(4)两个有趣的代码:
代码一:
(*((void (*) ())0))();
void (*) () 是函数指针类型
( void (*)() )0 :将0强制类型转换成函数指针类型
*(( void (*)() )0 ) 对函数指针类型解引用,就是调用该函数。
调用函数:(*(( void (*)() )0 ) )()
意思就是调用0地址处的函数,函数无参,返回值void
代码二:
void (*signal(int,void(*)(int)))(int);
signal先和()结合,说明signal是函数名。
signal函数的第一个参数是int,第二个参数是函数指针void(*)(int),该函数指针指向一个参数为int,返回类型为void的函数。
signal函数的返回类型也是一个函数指针void(*)(int);去掉函数名和参数,剩下的就是返回类型:void(*)(int)
这是一个signal函数的声明
代码二的简化:使用typedef:
这种形式容易理解,但是语法上不允许,会报错。
简化:
注意:
对函数指针类型void(*)(int)重命名,在语法上也要这么写:typedef void(*pfun_t)(int),不能typedef void(*)(int)pfun_t
6.函数指针数组
(1)概念:
函数指针数组顾名思义就是一个指针数组。
前面学习了数组指针数组,也就是int (*parr2[10])[5];
那么函数指针数组就是这么定义的:
int (*pfArr[4]) (int, int);
注意操作符的优先级,[ ]的优先级比*高,pfArr先和[ ]结合,说明pfArr是一个数组。
补充:对于一个数组的声明,去掉数组名和[ ],剩下的就是数组元素的类型;对于一个函数声明,去掉函数名和参数,剩下的就是返回类型。
pfArr是数组名,去掉这个数组名和[ ],剩下的int (*)(int, int)是一个函数指针类型,说明pfArr是函数指针的数组。
对于int (*parr2[10])[5];parr先和[ ]结合,是一个数组;去掉数组名parr2和[ ]。剩下的是int ( * )[5],是一个数组指针,说明parr2是数组指针的数组。
(2)函数指针数组的用途:转移表,减少代码的冗余性。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
void menu()
{
printf("***************************\n");
printf("***** 1.add 2.sub ******\n");
printf("***** 3.mul 4.div ******\n");
printf("***** 0.exit ******\n");
printf("***************************\n");
}
int main()
{
int x, y;
int input = -1;
int ret = 0;
int (*pfArr[5])(int, int) = { NULL,add,sub,mul,div };//函数指针数组
while (input)
{
menu();
printf("请选择:>");
scanf("%d", &input);
if (input >= 1 && input <= 4)
{
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y);
}
else
{
printf("输入错误!\n");
}
printf("ret=%d\n", ret);
}
return 0;
}
我们想要实现一个计算器,如果不使用函数指针数组,免不了要使用switch语句来分开调用四个计算的函数,这样的话每个case语句就会有重复的代码出现,这样就造成了代码的冗余,而函数指针数组实现的转移表可以解决这个缺点。
7.指向函数指针数组的指针(了解)
很明显这是个指针,该指针指向的是函数指针数组。
如何定义?
int ( * (*p)[5])(int ,int) = &pfArr;
p先和*结合,说明p是指针, *p后面接[5],说明是数组指针,数组的元素类型是int ( * ) (int ,int),是函数指针类型。
8.回调函数
(1)概念
回调函数就是一个通过函数指针调用的函数。
比如上面的计算器示例除了函数指针数组,也可以通过回调函数实现:
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
void menu()
{
printf("***************************\n");
printf("***** 1.add 2.sub ******\n");
printf("***** 3.mul 4.div ******\n");
printf("***** 0.exit ******\n");
printf("***************************\n");
}
void Calc(int (*pf)(int, int))
{
int x = 0;
int y = 0;
int ret = 0;
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("ret = %d\n", ret);
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
Calc(Add);
break;
case 2:
Calc(Sub);
break;
case 3:
Calc(Mul);
break;
case 4:
Calc(Div);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
qsort函数
C语言库中也有一个回调函数,那就是qsort函数。
所谓qsort就是quick sort,他的底层是快速排序。并且qsort排序适用于任何数据类型的排序。
我们发现qsort函数的参数很有意思,首先void*无类型的指针决定了这个函数适用于任何类型数据的排序,其次第四个参数明显是一个函数指针,参数也都是void *,这是用来比较两个数据的,无类型的指针也说明可以用于任何数据类型的比较。
num是数据个数,size是数据的大小(单位字节)。
这个compar函数是需要用户自己去实现的,然后让qsort函数调用comp函数的指针。
对于void*指针,他可以接受任何类型的指针,但是这种指针是不能进行解引用操作的,也不能进行指针运算。
qsort函数的使用
下面展示qsort函数的使用:
void printArr(int arr[], int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
//int类型数据排序的比较函数
int comp_int(void* p1, void* p2)
{
return *(int*)p1 - *(int*)p2;
}
//int类型数据排序
void test1()
{
int arr[10] = { 1,3,5,7,9,2,4,6,8,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), comp_int);
printArr(arr, sz);
}
typedef struct Stu
{
char name[20];
int age;
}Stu;
//struct类型数据排序的比较函数
//按年龄排序
int comp_StuByAge(void* p1, void* p2)
{
return ((Stu*)p1)->age - ((Stu*)p2)->age;
}
//按姓名排序
int comp_StuByName(void* p1, void* p2)
{
return strcmp(((Stu*)p1)->name, ((Stu*)p1)->name);
}
//struct类型数据排序
void test2()
{
Stu arr[3] = { {"Jack",18},{"Tom",22},{"Lucy",15} };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), comp_StuByAge);
}
int main()
{
//test1();
test2();
return 0;
}
qsort函数的实现
下面我们通过冒泡排序模拟实现一下qsort排序:
//int类型数据排序的比较函数
int comp_int(void* p1, void* p2)
{
return *(int*)p1 - *(int*)p2;
}
typedef struct Stu
{
char name[20];
int age;
}Stu;
//struct类型数据排序的比较函数
//按年龄排序
int comp_StuByAge(void* p1, void* p2)
{
return ((Stu*)p1)->age - ((Stu*)p2)->age;
}
//按姓名排序
int comp_StuByName(void* p1, void* p2)
{
return strcmp(((Stu*)p1)->name, ((Stu*)p1)->name);
}
void Swap(char* buf1, char* buf2, int size)//交换arr[j],arr[j+1]这两个元素,一个一个字节的交换
{
int i = 0;
char tmp = 0;
for (i = 0; i < size; i++)
{
tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
void bubble_sort(void* base, //base存放的是待排序数组中第一个元素的地址,void*说明可以对任何数据类型进行排序
int size, //排序数据元素的个数
int width, //数据的大小
int (*cmp)(void* , void*))//比较函数的指针,比较函数用户自己实现。参数类型是void*,说明可以比较任何数据类型
{
int i = 0;
//趟数
for (i = 0; i < size - 1; i++)
{
int j = 0;
//一趟内部比较的对数
for (j = 0; j < size - 1 - i; j++)
{
//假设需要升序cmp返回>0,交换
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)//两个元素比较,需要将arr[j],arr[j+1]的地址要传给cmp
{
//交换
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
}
}
}
}
注意:
通过cmp函数指针变量调用比较函数时,参数为什么要用char*?
因为char正好一个字节,(char* )base + j * width和(char*)base + (j + 1) * width 正好可以定位到arr[j],arr[j+1]的地址。
swap函数的参数为什么是char*?
char* 正好一个字节,可以用char*来一字节一字节的交换。
9.指针和数组的面试题
(1)数组名的理解
数组名是数组首元素的地址
但是有2个例外:
sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。sizeof里面必须要只有一个数组名,其他的都不行。
&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
除此之外所有的数组名都表示数组首元素的地址。包括sizeof(arr + 0)和sizeof(arr)也不一样。sizeof(arr + 0)里的arr表示数组首元素地址。
(2)strlen函数的理解
strlen函数接收的是指针参数,参数类型是const char* ,表示一个字符的指针,通过寻找‘\0’来计算字符串长度。
(3)二维数组的数组名理解
两种思维理解
例如二维数组int arr[3][4]:
二维数组的数组名arr是第一行一维数组的指针,即数组指针,而数组指针解引用就是数组名,*arr是第一行一维数组的数组名,*(arr+i)就是第i行一维数组的数组名。
另一种思维理解:二维数组是一维数组的数组,对于arr[i][j],arr[i]可以看成第i个一维数组的数组名,而arr[i]即*(arr+i)。
(4)几个易错的题:
int a[]={1,2,3,4};
pritnf("%d\n",sizeof(*&a));
sizeof(*&a)就是sizeof(a),里面只有一个数组名,表示整个数组,大小是16;
int a[3][4]={0};
pritnf("%d\n",sizeof(a[0]));
两种思维理解(针对a[i]):
a是二维数组的数组名,就是第一行数组的指针,a[i]就是*(a + i), a + i就是第i行数组的指针, *(a + i)就是第i行数组的数组名(数组指针解引用就是数组名),sizeof里面只有一个数组名,就表示整个一维数组(值为16)。
二维数组是一维数组的数组,对于arri,arr[i]可以看成第i个一维数组的数组名,sizeof里面只有一个数组名,就表示整个一维数组(值为16)。
(5)sizeof的特点
sizeof只关注类型
int a[3][4]={0};
ritnf("%d\n",sizeof(a[3]));
这里其实并不会实际去访问第四行(第四行越界)。
又比如:
int a = 7;
short b = 4;
printf("%d\n",sizeof(s = a + 2));
这里输出的是2(short类型的大小)
10.指针的笔试题
笔试题1
int main()
{
int a[5] = { 1,2,3,4,5 };
int* ptr = (int*)(&a + 1);
printf("%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
&a:数组a的地址
答案:2和5
笔试题2
struct Test//20字节
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
//假设p的值为0x100000,求如下表达式:
int main()
{
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
注意:
指针+/-整数:根据指针类型决定加多少字节。
整数+整数:直接加整数个字节。
十六进制的0x1就是数字1;
内存中的每个内存单元都是1个字节,每个内存单元都有一个编号,就是地址,地址加1相当于跨一个字节。
答案:
1.0x100014(加20)
2.0x100001(加1)
3.0x100004(加4)
笔试题3
int main()
{
int a[4] = { 1,2,3,4 };
int *ptr1 = (int*)(&a + 1);
int *ptr2 = (int*)((int)a + 1);
printf("%x,%x", ptr1[-1], *ptr2);
return 0;
}
注意:
%x是以16进制打印。
ptr1是根据数组指针类型的来加1,加多少字节由数组大小决定。
ptr2转化成int再加1,直接加1个字节。
答案:4;02000000
笔试题4
int main()
{
int a[3][2] = { (0,1),(2,3),(4,5) };
int* p = a[0];
printf("%d", p[0]);
return 0;
}
注意:
这里的二维数组是不完全初始化,因为是括号()而不是花括号{ },()里面的表达式是逗号表达式。所以数组元素是1 3 5 0 0 0。
p是第一行数组的数组名,也就是1的地址。
答案:1
笔试题5
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
注意:
二维数组在内存中是连续存放的。
指针-指针,返回的是两个指针间的元素个数。
%p和%x的区别:两个都是打印十六进制数,%p一般打印地址。
数据在内存中存放的都是补码。%d是以十进制打印。
答案:FFFFFFFFFFFFFFFC,-4
笔试题6
int main()
{
int aa[2][5] = { 1,2,3,4,5,6,7,8,9,10 };
int* ptr1 = (int*)(&aa + 1);
int* ptr2 = (int*)(*(aa + 1));
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}
注意:
ptr2:aa是二维数组的数组名,表示第一行一维数组的地址,aa+1就是第二行一维数组的地址,*(aa+1)就是第二行一维数组的数组名,也就是6的地址。
答案:10,5
笔试题7
int main()
{
char* a[] = { "work","at","alibaba" };
char** pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
答案:at
笔试题8
int main()
{
char* c[] = { "ENTER","NEW","POINT","FIRST" };
char** cp[] = { c + 3,c + 2,c + 1,c };
char*** cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *--*++cpp + 3);
printf("%s\n", *cpp[-2] + 3);
printf("%s\n", cpp[-1][-1] + 1);
return 0;
}
注意:
运算符优先级:++/-- > *(解引用操作符) > +/-(加减)。
数组名是数组首元素的地址。
**++cpp:cpp是c+3的地址,++后就是c+2的地址,*++cpp就是c+2,c+2是第三个char *的地址,** ++cpp就是 *(c+2),就是第三的char *,指向字符串“POINT”的首字符P。
*--*++cpp + 3:cpp是c+2的地址,++后再 *就是c+1,再--再 * 就是第一个char *,指向字符串“ENTER”的首字符E,再+3就指向了E。
*cpp[-2] + 3: 转换成**(cpp-2)+3,cpp是c+1的地址,-2后再*就是c+3,再 *后就是第四个char *,指向字符串“FIRST”的首字符F,再+3就指向了S。
cpp[-1][-1] + 1:转换成*( *(cpp-1)-1)+1,cpp是c+1的地址,经过分析,*( *(cpp-1)-1)指向字符串“NEW”**的首字符N,再+1指向E。
答案:POINT ER ST EW