指针的主题,在指针初阶阶段,我们知道了指针的概念:
1.指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
2.指针的大小是固定4/8个字节(32为平台/64位平台)。
3.指针是有类型,指针的类型决定了指针的指针的进阶
- 一、字符指针
- 二、指针数组
- 三、数组指针
- 3.1数组指针的定义
- 3.2 &数组名 VS 数组名
- 3.3数组指针的使用
- 四、数组传参和指针传参
- 4.1一维数组传参
- 4.2二维数组传参
- 4.3一级指针传参
- 4.4二级指针传参
- 五、函数指针
- 六、函数指针数组
- 七、指向函数指针数组的指针
- 八、回调函数
- 九、指针和数组面试题的解析
一、字符指针
在指针的类型中我们知道有一种指针类型为字符指针char*
一般使用:
int main()
{
char ch='w';
char *pc=&ch;
*pc='w';
return 0;
}
还有一种使用方式:
int main()
{
const char* pstr = "hello bit.";
printf("%s\n", pstr);
return 0;
}
注意:这里特别容易误以为把hello bit.放到字符指针pstr里了,实际上/本质上是把字符串hello bit.首字符的地址放到了pstr中,也就是pstr指向首字符h的地址。
面试题:
#include <stdio.h>
int main()
{
char str1[] = "hello bit";
char str2[] = "hello bit";
const char* str3 = "hello bit";
const char* str4 = "hello bit";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
输出结果:
这里的str3和str4指向的是同一个常量字符串。c/c++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块,即str1的地址和str2的地址不相同,而str3和str4地址相同
二、指针数组
指针数组是一个存放指针的数组。指针只是一个修饰词,就像整形数组还是数组,存放的是整形类型的元素。
例如:
int arr1[4]={0};
int arr2[5]={0};
int arr3[3]={0};
int* arr[3]={arr1,arr2,arr3};
数组中的元素arr1,arr2,arr3表示一维数组中的首元素地址,都是int* 类型
同理char* arr2[3];
数组中的元素都是char*类型
char** arr[3][3];
数组中的元素都是char**类型
三、数组指针
3.1数组指针的定义
数组指针是指针。本质是指针,数组只是修饰词,就像:
整形指针:int* pint;能够指向整形数据的指针。
浮点型指针:float* pf;能够指向浮点型数据的指针。
那么数组指针:能够指向数组的指针(存放的是数组的地址的指针变量)
例如:
int (*p)[10];p先和 *结合,说明p是一个指针变量,然后指针指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
3.2 &数组名 VS 数组名
对于下面的数组:
int arr[10]; arr和&arr分别是啥?
我们知道arr是数组名,数组名表示数组首元素地址,那&arr数组名到底是啥?看代码:
#include <stdio.h>
int main()
{
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
运行结果:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
printf("arr=%p\n", arr);
printf("&arr=%p\n", &arr);
printf("arr+1=%p\n", arr + 1);
printf("&arr+1=%p\n", &arr + 1);
return 0;
}
运行结果:
根据上面的代码结果我们发现,其实&arr和arr,虽然值是一样的,但是意义是不一样的。
实际上:&arr表示的是整个数组的地址,而不是数组首元素的地址。arr表示的是数组首元素的地址,只是因为指针一开始都指向同一个位置(即数组首元素的地址),因此一开始他们的地址是相等的,但当他们发生变化时能够明显的看出变化,数组的地址+1,表示跳过整个数组的大小,所以&arr+1相对于&arr的差值是40个字节,40个字节大小是因为整形数组有十个元素,每个元素占4个字节的大小。同时在本例中&arr的类型就是:int (*)[10],是一种数组指针类型
3.3数组指针的使用
既然数组指针指向的是数组,那么数组指针中存放的应该是数组的地址。
看代码:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int (*p)[10]=&arr;
int i = 0;
int j = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(*p + i));
}
return 0;
}
运行结果:
我们发现通过数组指针的方法也可以运行代码,但是看起来有点复杂,多次一举,其实数组指针一般用于二维数组比较方便。
#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print_arr2(int (*arr)[5], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
printf("%d ", (*(arr+i))[j]);//*(arr+i)==arr[i]
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
print_arr1(arr, 3, 5);
//数组名arr表示二维数组的首元素地址,
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址,可以用数组指针来接收
print_arr2(arr, 3, 5);
return 0;
}
运行结果:
回顾:
int arr[5];//含有5个整型元素的数组
int *parr1[10];//指针数组,含有10个int *类型元素的数组
int (parr2)[10];//数组指针,含有10个int () [10]类型的元素
int (parr3[10])[5];//数组指针数组,parr3[10]说明parr3是一个数组,数组中的元素类型为int()[5]
四、数组传参和指针传参
4.1一维数组传参
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
#include <stdio.h>
void test(int arr[])//ok?(√)
{}
void test(int arr[10])//ok?(√)
{}
void test(int* arr)//ok?(√)
{}
void test2(int* arr2[20])//ok?//(√)
{}
void test2(int** arr2)//ok?(√)
{}
int main()
{
int arr[10] = { 0 };
int* arr2[20] = { 0 };
test(arr);
test2(arr2);
return 0;
}
4.2二维数组传参
#include <stdio.h>
void test(int *arr)//ok?(×)//传过来的是二维数组的首元素的地址即一维数组的地址,而数组的地址要用数组指针来接收(int(*arr)[5])
{}
void test(int* arr[5])//ok?(×)//这是一个数组指针
{}
void test(int (*arr)[5])//ok?(√)
{}
void test2(int** arr)//ok?(×)//这是一个二级指针,用来接收一级指针(int*)
{}
int main()
{
int arr[3][5] = {0};
test(arr);
return 0;
}
4.3一级指针传参
#include <stdio.h>
void print(int* p, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d\n", *(p + i));
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
//一级指针传给参数
print(p, sz);
return 0;
}
思考:
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
比如:
void test1(int* p)
{}//可以是 char* p=&n;char *p=数组名;test§;test(&n);test(arr);
4.4二级指针传参
#include <stdio.h>
void test(int** ptr)
{
printf("num=%d]n", **ptr);
}
int main()
{
int n = 10;
int* p = &n;
int** pp = &p;
test(pp);
test(&p);
return 0;
}
思考:当函数的参数为二级指针的时候,可以接收什么参数?
#include <stdio.h>
void test(int** ptr)
{
}
int main()
{
char c = 'b';
char* pc = &c;
char** ppc = &pc;
char* arr[10];
test(&pc);//(√)
test(ppc);//(√)
test(arr);//(√)
return 0;
}
五、函数指针
首先看一段代码:
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
输出结果:
输出的是两个地址,这两个地址是test函数的地址。那我们的函数的地址要想保存起来,怎么保存?看代码:
void test()
{
printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void* pfun2();
pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。所以void (*pfun1)();符和条件,void (*pfun1)()=test或者void (*pfun1)()=&test,*pfun1=test。第二个只能算是函数的声明
阅读两端有趣的代码:
//代码1
(*(void(*)())0)();
首先明白0是啥,他可以是地址(地址也是由数值组成的),也可以是数值,在这里0是作为一个地址,void(*)()是一个类型,这里是将0的类型强制转换为void( * )()类型的函数指针,然后再进行解引用,解引用之后就是函数名再加个括号就是在0地址处的函数调用
//代码2
void (*signal(int, void(*)(int)))(int);
signal是一函数声明,有2个参数int和void(*)(int),返回类型是void( * )(int)
signal函数指针指向的函数的参数是int类型,返回类型是void
注:推荐《C陷阱和缺陷》,这本书中提及这两个代码。
代码2太复杂,如何简化:
typedef void(*pfun_t)(int);//这句代码的意思是将void( * )(int)重命为pfun_t
pfun_t signal(int, pfun_t);
六、函数指针数组
把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (parr1[10])();?
int parr210;
int ()()parr3[10];
答案是:parr1先和[]结合,说明parr1是数组,数组的内容是什么呢?是int ()()类型的函数指针。
函数指针数组的用途:转移表
例子:(计算器)
#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;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
printf("*****************************\n");
printf(" 1:add 2:sub\n");
printf(" 3:mul 4:div\n");
printf("*****************************\n");
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("ret=%d\n", ret);
break;
case 2:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = sub(x, y);
printf("ret=%d\n", ret);
break;
case 3:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = mul(x, y);
printf("ret=%d\n", ret);
break;
case 4:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = div(x, y);
printf("ret=%d\n", ret);
break;
case 0:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
运行结果:
使用函数指针数组的实现:
#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;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int (*p[5])(int x, int y) = { 0, add, sub, mul, div };//转移表
while (input)
{
printf("*****************************\n");
printf(" 1:add 2:sub\n");
printf(" 3:mul 4:div\n");
printf("*****************************\n");
printf("请选择:");
scanf("%d", &input);
if (input <= 4 && input >= 1)
{
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
}
else
printf("输入有误\n");
printf("ret=%d\n", ret);
}
return 0;
}
运行结果:
七、指向函数指针数组的指针
指向函数指针数组的指针是一个指针,指针指向一个数组,数组的元素都是函数指针;
如何定义?
void test(const char* str)
{
printf("%s\n", str);
}
int mian()
{
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针的数组pfunarr
void (*pfunarr[5])(const char* str);
pfunarr[0] = test;
//指向函数指针数组pfunarr的指针ppfun
void (*(*ppfun)[5])(const char*) = &pfunarr;
}
八、回调函数
回到函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
首先演示一下库函数qsort函数的使用:
#include <stdio.h>
#include <stdlib.h>
int int_cmp(const void* p1, const void* p2)
{
return (*(int*)p1 - (*(int*)p2));
}
int main()
{
int arr[] = { 1,3,5,7,9,2,4,6,8,0 };
int i = 0;
qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
运行结果:
qsort函数的特点:
1.快速排序的方法 2.适合于任意类型数据的排序
通过cplusplus搜索qsort这个函数的用法,void* base表示指向了需要排序的数组的第一个元素,size_t num排序的元素个数,size_t size一个元素的大小,单位是字节。默认是升序。
对于void的指针是一无具体类型的指针,可以接收任意类型的地址,void类型指针不能直接进行解引用操作,也不能直接进行指针运算。现在用回调函数,来模拟实现qsort函数(采用冒泡的方式),这里也使用了void*的指针。
#include <stdio.h>
int int_cmp(const void* p1, const void* p2)
{
return (*(int*)p1 - *(int*)p2);
}
void swap(char* p1, char* p2, int size)
{
int i = 0;
for (i = 0; i < size; i++)
{
char tmp = *p1;
* p1 = *p2;
*p2 = tmp;
p1++;
p2++;
}
}
void bubble(void* base, int count, int size, int(*cmp)(const void*, const void*))
{
int i = 0;
int j = 0;
for (i = 0; i < count-1; i++)
{
for (j = 0; j < count - i - 1; j++)
{ //通过函数指针cmp调用int_cmp这个函数,这里就是一个回调函数,大于0为升序,以char类型可以控制比较字节的大小,如果是int类型,那么我要比较3个字节的大小就不好比较了
if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
{ //通过比较字节的大小
swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
}
}
}
}
int main()
{
int arr[] = { 1,3,5,7,9,2,4,6,8,0 };
int i = 0;
//将int_cmp的地址传给函数指针
bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
九、指针和数组面试题的解析
/1.sizeof(数组名),这里的数组名表示整个数组,计算整个数组的大小。
//2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
//3.除此之外所有的数组名都表示首元素的地址。
//4.sizeof计算的是元素类型的大小,若有多个元素,便计算多个元素类型的大小的之和
//5.strlen计算的字符串的个数,接收的是字符串的地址,遇到'\0'便不再统计,且'\0'不统计
#include <stdio.h>
#include <string.h>
int main()
{
//一维数组
int a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a));//a代表整个数组,求的是整个数组的大小,为16字节
printf("%d\n", sizeof(a+0));//代表首元素地址,本质为指针,大小为4/8个字节
printf("%d\n", sizeof(*a));//代表元素的大小,即类型的大小,为4个字节
printf("%d\n", sizeof(a+1));//代表第二个元素的地址,本质为指针,大小为4/8个字节
printf("%d\n", sizeof(a[1]));//代表第二个元素的大小,即类型的大小,为4个字节
printf("%d\n", sizeof(&a));//代表整个数组的地址,本质为指针,大小为4/8个字节
printf("%d\n", sizeof(*&a));//*&a==a,即整个数组的大小,为16字节
printf("%d\n", sizeof(&a+1));//数组的地址+1,跳过整个数组,虽超出数组的范围,并不影响求大小,本质为指针,大小为4/8个字节
printf("%d\n", sizeof(&a[0]));//代表首元素地址,本质为指针,大小为4/8个字节
printf("%d\n", sizeof(&a[0]+1));//首元素的地址+1,为第二个元素的地址,本质为指针,大小为4/8个字节
//字符数组
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr));//整个数组的大小,为6个字节
printf("%d\n", sizeof(arr + 0));//代表首元素地址,本质为指针,大小为4/8个字节
printf("%d\n", sizeof(*arr));//代表元素的大小,即元素类型的大小,为1个字节
printf("%d\n", sizeof(&arr + 1));//数组的地址+1,跳过整个数组,虽超出数组的范围,并不影响求大小,本质为指针,大小为4/8个字节
printf("%d\n", sizeof(arr[1]));//代表第二个元素所占的大小,即元素类型的大小,为1个字节
printf("%d\n", sizeof(&arr));//代表整个数组的地址,本质为指针,大小为4/8个字节
printf("%d\n", sizeof(&arr[0]+1));//首元素的地址+1,为第二个元素的地址,本质为指针,大小为4/8个字节
printf("%d\n", strlen(arr));//strlen从首元素地址开始搜寻,遇到'\0'停止,而此数组中没有'\0',为随机值
printf("%d\n", strlen(arr + 0));//从首元素地址开始搜寻,遇到'\0'停止,而此数组中没有'\0',为随机值
printf("%d\n", strlen(*arr));//strlen接收的是一个地址,从所指向的地址开始计算,而*arr代表首元素,故会报错
printf("%d\n", strlen(&arr + 1));//数组的地址+1,跳过整个数组,从数组+1处的地址开始搜寻,遇到'\0'停止,故为随机值
printf("%d\n", strlen(arr[1]));//代表第二个元素,报错
printf("%d\n", strlen(&arr));//代表数组的地址,从此地址开始搜寻,搜索到下个地址时跳过的是整个数组,接着从数组的地址+11处继续搜寻,遇到'\0'停止,故为随机值
printf("%d\n", strlen(&arr[0] + 1));//代表第二个元素的地址,从第二个元素的地址开始搜寻,遇到'\0'停止,故为随机值
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));//计算整个数组的大小,包括'\0',每个元素大小为1个字节,总大小为7个字节
printf("%d\n", sizeof(arr + 0));//代表首元素地址,本质为指针,为4/8个字节
printf("%d\n", sizeof(*arr));//代表首元素所占的大小,即元素类型的大小,为1个字节
printf("%d\n", sizeof(&arr + 1));//数组的地址+1,跳过整个数组,虽超出数组的范围,并不影响求大小,本质为指针,大小为4/8个字节
printf("%d\n", sizeof(arr[1]));//代表第二个元素的大小,即元素类型的大小,为1个字节
printf("%d\n", sizeof(&arr));//代表数组的地址,本质为指针,大小为4/8个字节
printf("%d\n", sizeof(&arr[0] + 1));//代表第二个元素的地址,本质为指针,大小为4/8个字节
printf("%d\n", strlen(arr));//strlen从首元素地址开始搜寻,遇到'\0'停止,但不计算'\0',大小为6个字节
printf("%d\n", strlen(arr + 0));//从首元素地址开始搜寻,遇到'\0'停止,大小为6个字节
printf("%d\n", strlen(*arr));//strlen接收的是一个地址,从所指向的地址开始计算,而*arr代表首元素,故会报错
printf("%d\n", strlen(&arr + 1));//数组的地址+1,跳过整个数组,从数组+1处的地址开始搜寻,遇到'\0'停止,故为随机值
printf("%d\n", strlen(arr[1]));//代表第二个元素,并不是一个地址,报错
printf("%d\n", strlen(&arr));//代表数组的地址,从此地址开始搜寻,搜索到下个地址时跳过的是整个数组,接着从数组的地址+11处继续搜寻,遇到'\0'停止,故为随机值
printf("%d\n", strlen(&arr[0] + 1));//代表第二个元素的地址,从第二个元素的地址开始搜寻,遇到'\0'停止,大小为5个字节
char* p = "abcdef";//字符串常量,实际给p的是该字符串的首地址,即p指向a的地址
printf("%d\n", sizeof(p));//p为指针,大小为4/8个字节
printf("%d\n", sizeof(p + 1));//代表b元素地址,本质为指针,为4/8个字节
printf("%d\n", sizeof(*p));//代表首元素所占的大小,即元素类型的大小,为1个字节
printf("%d\n", sizeof(&p + 1));//p的地址+1,还是地址,本质为指针,大小为4/8个字节
printf("%d\n", sizeof(p[0]));//p[0]==*(p+0),代表首元素的大小,即元素类型的大小,为1个字节
printf("%d\n", sizeof(&p));//代表p的地址,本质为指针,大小为4/8个字节
printf("%d\n", sizeof(&p[0] + 1));//代表第二个元素的地址,本质为指针,大小为4/8个字节
printf("%d\n", strlen(p));//strlen从首元素地址开始搜寻,遇到'\0'停止,但不计算'\0',大小为6个字节
printf("%d\n", strlen(p + 1));//从第二个元素地址开始搜寻,遇到'\0'停止,大小5个字节
printf("%d\n", strlen(*p));//strlen接收的是一个地址,从所指向的地址开始计算,而*p代表首元素,顾会报错
printf("%d\n", strlen(&p + 1));//p的地址+1,没有被束缚,为野指针,遇到'\0'停止,故为随机值
printf("%d\n", strlen(p[0]));//代表首元素,报错
printf("%d\n", strlen(&p));//代表p的地址,从此地址开始搜寻,搜索到下个地址时为野指针,遇到'\0'停止,故为随机值
printf("%d\n", strlen(&p[0] + 1));//代表第二个元素的地址,从第二个元素的地址开始搜寻,遇到'\0'停止,大小为5个字节
//二维数组
int pa[3][4] = { 0 };
printf("%d\n", sizeof(pa));//计算整个数组的大小,大小为48个字节
printf("%d\n", sizeof(pa[0][0]));//代表二维数组第一行第一个元素的大小,即元素类型的大小,大小为4个字节
printf("%d\n", sizeof(pa[0]));//代表二维数组第一行的数组大小,即一维数组的数组名,大小为16个字节
printf("%d\n", sizeof(pa[0] + 1));//代表二维数组第一行第二个元素的地址
printf("%d\n", sizeof(*(pa[0]+1)));//代表二维数组第一行第二个元素的大小,即元素类型的大小,为4个字节
printf("%d\n", sizeof(pa+1));//代表二维数组第一行数组的地址+1即第二行的数组地址,本质为指针,大小为4/8个字节
printf("%d\n", sizeof(*(pa + 1)));//代表二维数组第二行数组的地址进行解引用即第二行数组的数组名,大小为16个字节
printf("%d\n", sizeof(&pa[0] + 1));//代表二维数组第一行数组的地址+1,即第二行数组的地址,本质为指针,大小为4/8个字节
printf("%d\n", sizeof(*( &pa[0] + 1)));//对第二行数组的地址进行解引用,即第二行数组的数组名,大小为16个字节
printf("%d\n", sizeof(&pa));//代表二维数组的地址,本质为指针,大小为4/8个字节
printf("%d\n", sizeof(pa[3]));//超出数组的下标,但本质还是指针,大小为4/8个字节
return 0;
}