目录
前言
指针进阶
字符指针
指向常量字符串的指针
指针数组
指针数组打印数组内容
数组指针
对数组指针的理解
&数组名和数组名
数组指针的使用
数组参数、指针参数
一维数组传参
二维数组传参
一级指针传参
二级指针传参
函数指针
阅读两段有趣的代码
代码1
代码2
函数指针数组
函数指针数组的用途:转移表(通过下标调用函数)
指向函数指针数组的指针
回调函数
qsort函数来排序整型数组
qsort函数排序结构体
模拟qsort函数
我们在初级指针章节已经接触过了指针,我们知道了指针的概念:
1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
3. 指针是有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限。
4. 指针的运算。
在这里我们将继续探索指针的奥秘,掌握高级指针初识以及运用计算,本篇内容将详细介绍各种指针,内容较多,干货满满,相信大家会收获很多!!话不多说,进入正题:
在指针的类型中我们知道有一种指针类型为字符指针 char* ,
一般使用:
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
还有另外一种使用方式:
int main()
{
char* pstr = "hello bit.";//常量字符串
char arr[]="hello bit";
printf("%s",arr);//数组名表示字符串首地址,即“h”的地址,打印为hello bit;
printf("%s\n", pstr);//pstr存放首字符“h”的地址。打印为hello bit;
return 0;
}
代码 char* pstr = "hello bit."; 大家特别容易以为是把字符串 hello bit 放到字符指针 pstr 里 了,但是本质上是把字符串 hello bit. 首字符的地址放到了pstr中。就像这样:
上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中。
了解了这个之后我们来看看一道题:
#include
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
char* str3 = "hello bit.";//常量字符串,不能改,那么两个字符肯定存在一个地址中,不过是用不同的指针指向首字符
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)//字符串指针存放的是首字符的首地址,都指向'h'的地址
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[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组
#include
int main()
{
int a[5] = { 1,2,3,4,5 };
int b[5] = { 2,3,4,5,6 };
int c[5] = { 3,4,5,6,7};
int* arr[3] = { a,b,c };//数组名表示首地址
int i = 0;
for (i = 0; i < 3; i++)//指的是三个一维数组
{
int j = 0;
for (j = 0; j < 5; j++)//每个一维数组的元素
{
printf("%d ", *(arr[i] + j));//相当于arr[i][j];
}
printf("\n");
}
}
好了,对于指针数组就是这样,比较简单,下面介绍一个比较重要的知识:
数组指针是指针,我们已经熟悉:
整形指针: int * pint; 能够指向整形数据的指针。
浮点型指针: float * pf; 能够 指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。
其基本的结构就是:int (*p)[10];
下面我对于数组指针做出了一个解释:
p先和*结合,说明p是一个指针变量。
然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合
如果我们要用一个指针来存放一个数组的地址,那么这个指针应该是数组指针指向整个数组的地址:
int arr[10]={1,2,3,4,5};
int (*parr)=&arr;//取出的是整个数组的地址,int是元素类型,(*parr)是数组指针
对于下面的数组:
int arr[10];
arr和&arr分别是什么:
我们知道arr是数组名,数组名表示首元素的地址,那么&数组名是什么,我们可以通过代码来一步步深入研究:
#include
int main()
{
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
虽然两个地址一样,但是意义决然不同,我们还可以看这么一段代码:
#include
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;
}
或者通过指针来访问:
#include
int main()
{
int arr[10] = { 0 };
int* p1 = arr;//首元素地址
int(*p2)[10] = &arr;//整个数组的地址
printf("%p\n", p1);
printf("%p\n", p1+1);//跳过一个整形,四个字节
printf("%p\n", p2);
printf("%p\n", p2+1);//跳过一个整形数组,40个字节
}
通过以上代码和运行结果可以知道:
其实&arr和arr,虽然值是一样的,但是意义应该不一样的。
&arr 表示的是数组的地址,而不是数组首元素的地址。 数组的地址+1,跳过整个数组的大小。
但是我们要记住有两个例外:
1.sizeof(数组名),数组名表示整个数组,计算的是整个数组的大小。
2.&数组名,数组名表示整个数组,取出的是整个数组的地址。
⚠️:这两个例外对于关于指针的解题很重要,所以我们一定要熟记。
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。
数组指针的使用对于二维数组是比较多的,对于一维数组则比较少,我们先来看看他对于一维数组的使用:
#include
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
return 0;
}
#include
int main()
{
int arr[10]={1,2,3,4,5,6,7,8,9,10};
int (*pa)[10]=&arr;
int i=0;
for(i=0;i<10;i++)
{
printf("%d\n",*(*pa)+i);
}
return 0;
}
但是我们一般不会这样写,这样写看着别扭,可读性不高,而是这样用于二维数组:
#include
void print(int(*p)[5], int r, int c)
{
int i = 0;
int j = 0;
for (i = 0; i < r; i++)
{
for (j = 0; j < c; j++)
{
printf("%d ", *(*(p + i) + j));//p指向第一个一维数组的地址,p+i依次指向第二个第三个数组
//*(p+i)表示一维数组名,数组名表示首元素地址,所以*(p+i)表示每一行的首元素地址
//*(*(p+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(arr, 3, 5);//二维数组名表示第一行一维数组的地址;
return 0;
}
对数组指针有了了解之后,我们可以来判断一下下面代码的意思:
int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
▶️解析:
int arr[5]:整型数组
int *parr1[10]:整型指针数组
int (*parr2)[10]:数组指针
int (*parr3[10])[5]:parr3是一个存储指针的数组,该数组能够存放10个数组指针,每个数组指针能够指向一个数组,数组有5个元素,每个元素是int型;
在写代码的时候难免要把数组或者指针传给函数,那函数的参数该如何设计呢?
#include
void test(int arr[])//数组接收,ok
{}
void test(int arr[10])//数组接收,ok
{}
void test(int *arr)//指针接收,ok
{}
void test2(int *arr[20])//指针的地址用指针数组接收,ok
{}
void test2(int **arr)//一级指针地址用二级指针接收,ok
{}
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};//指针数组
test(arr);//数组名表示首元素地址
test2(arr2);//数组名表示第一个指针的地址
}
void test(int arr[3][5])//二维数组,ok
{}
void test(int arr[][])//省略了列
{}
void test(int arr[][5])//ok
{}
void test(int *arr)//数组名表示首元素地址,二维数组数组名表示一维数组的地址
{
void test(int* arr[5])//指针数组
{
void test(int (*arr)[5])//数组指针,存放数组的地址,ok
void test(int **arr)//二级指针,表示指针的地址
{}
int main()
{
int arr[3][5] = {0};
test(arr);//传的是一维数组的地址
}
✔️总结:
二维数组传参,函数形参的设计只能省略第一个[]的数字。 //因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素,这样才方便运算。
示例1️⃣:
#include
void print(int *p, int sz)
{
int i = 0;
for(i=0; i
示例2️⃣:
void test(char* p)
{}
int main()
{
char ch='w';
char* str=&ch;
test(&ch);//将ch的地址传参给一级指针
test(str);//存放ch地址的指针
}
示例1️⃣:
#include
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;
}
示例2️⃣:
void test(char **p)
{
}
int main()
{
char c = 'b';
char*pc = &c;
char**ppc = &pc;
char* arr[10];//存放一级指针的数组
test(&pc);//传一级指针地址,ok
test(ppc);//传二级指针,ok
test(arr);//传存放一级指针的数组,数组名表示首元素地址,即第一个指针地址,所以用二级指针接收,ok
}
总结:
如果一个函数的参数是一级指针,那么他可以接收 普通数组,变量地址,和一级指针;
如果一个函数的参数是二级指针,那么他可以接收 二级指针,一级指针地址,和 一级指针数组 ;
我们已经学过存放普通变量地址的指针,也学过存放数组地址的指针,那么肯定有存放函数地址的指针。
指向整型变量的指针:
int a=10;
int* pa=&a;
指向字符变量的指针:
char ch='w';
char* pc=&ch;
指向数组的指针:
int arr[10]={0};
int(*parr)=&arr;
那么指向函数的指针应该是:
void test(char* str)
{}
void (*pt)(char*)=&test;
对于函数指针,我们要能够正确判断:
函数指针的判断方法
就拿void (*pt)(char*)=&test来说,void指的是函数的返回类型,(*pt)表示指针,注意要有括号才能保证pt和*号结合,表示他是指针,而(char*)则是函数的参数类型。
了解了函数指针的结构我们是否可以用函数指针来代替函数名,也就是说通过函数指针来调用函数呢?
首先我们来看这样一段代码:
#include
int Add(int x, int y)
{
return x + y;
}
int main()
{
system("color f1");
printf("%p\n", &Add);
printf("%p\n", Add);
}
再看运行结果发现:
输出的是竟然是两个一样的地址,这两个地址是 test 函数的地址。那函数的地址要想保存起来,怎么保存?
当然是用存放函数地址的函数指针保存,那么下面的哪个有能力存放函数的地址:
1.int(*pt1)(int,int)=&Add;
2.int* pt2(int,int)=&Add;
首先,能给存储地址,就要求pt1和pt2是指针,显然在表达式1中*号先和pt1结合,表示pt1是指针,后面括号中的两个int表示两个int类型的参数。*pt1前面的int表示函数的返回类型是int类型。
而pt2则不能达到这样的效果。
下面我们来看看一段这样的代码,将会发现很多不知道的秘密:
我们发现这三个打印出来的是一样的,那么为什么呢?
首先既然&Add和Add是一样的,那么int(*pf)(int,int)=&Add就可以写成int(*pf)(int,int)=Add;
对指针pf解引用的到Add,所以Add(3,5)和(*pf)(3,5)是等价的;
pf能存放Add的地址,所以Add(3,5)可以写成pf(3,5);
(*(void (*)())0)();
解读:
- void (*)():函数指针类型;
- (void(*)())0:对0进行强制转换,转换为指向函数的指针,解释为函数地址;
- *(void(*)())0:对0处的地址进行解引用得到函数;
- (*(void(*)())0)():用指针调用函数的模型,所以这是调用0地址处的函数;
void (*signal(int , void(*)(int)))(int);
解读:
- signal后面有(),signal没有和*号结合,而是和()结合,说明signal是函数;
- signal函数的第一个参数为int,第二个参数是函数指针类型,该函数指针指向一个参数为int,返回类型是void的函数;
- signal函数的返回类型也是一个函数指针类型,该函数指针指向一个参数为int,返回类型是void的函数;
整体来看这是一次函数声明,但是这样太复杂了,可以简化:
void(*)(int)signal(int,void(*)(int));
这样就很容易理解,void(*)(int)是函数返回类型,signal是函数名,(int,void(*)(int)是函数参数;
但是这样会报错,在vs编译器中会出现语法错误,不符合规定,所以会把函数的返回类型拆开到函数参数的后面,所以我们可以采用“typedef”对类型进行重定义后可以简化;
因为题中有两个void(*) (int),所以可以用typedef进行重定义:
typedef void(*pfun_t)(int);//本来是typedef void(*)(int) pfun_t这样写的,但是不符合语法规定;
那么总结起来这个代码可以写成:
typedef void(*pfun_t)(int);
pfun_t signal(int,pfun_t);//pfun_t指的是void(*)(int);
数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组, 比如:
int *arr[10];
//数组的每个元素是int*
那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[10])();
parr1 先和 [] 结合,说明parr1是数组,数组的内容是 是 int (*)() 类型的函数指针。
那么函数指针数组有什么作用呢?
见代码:
#include
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int main()
{
int(*pf1)(int, int) = Add;
int(*pf2)(int, int) = Sub;
int(*pfArr[2])(int, int) = { Add,Sub };//int(*)(int,int)就是一个函数指针类型,pfArr[2]就是一个函数指针数组;
}
我们可以用函数指针数组来实现一个简单的计算器,首先来看看没有我们之前写的普通版的计算器代码:
#include
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");
breark;
default:
printf( "选择错误\n" );
break;
}
} while (input);
return 0;
}
这样的话我们直接从case语句看不能一眼的看出是什么函数,而且代码量比较大,所以我们可以通过函数指针数组进行优化:
#include
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;
}
这个函数指针数组代替了switch case语句,让代码变得那个更加简单。
我们已经知道对于整型数组有指向整型数组的指针:
int arr[5];
int(*p1)[5]=&arr;
对于整型指针数组有指向整型指针数组的指针:
int *arr[5];
int*(*p2)[5]=&arr;
那么指向函数指针数组的指针应该怎么表示呢?
指向函数指针数组的指针是一个指针,指针指向一个数组 ,数组的元素都是函数指针。
int (*p)(int,int);//函数指针;
int (*p2[4](int,int);//函数指针数组;
int (*(*p3)[4])(int,int)=&p2;//p3就是指向函数指针数组的指针,存放函数指针数组的地址;
那么指向函数数组指针的定义应该是这样的:
void test(const char* str)
{
printf("%s\n", str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[10])(const char*) = &pfunArr;
return 0;
}
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一 个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该 函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或 条件进行响应。
而qsort函数就是一个回调函数,这是一个快速排序函数,默认排序为升序,定义在头文件stdlib.h中,能排序各种类型的数据,而我们之前使用的冒泡排序函数则比较单一,在整个程序中只能排序同一种函数。
要想知道qsort函数的优点我们先来回顾一下冒泡排序函数的应用:
#include
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
void bubble_sort(int arr[],int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
print_arr(arr, sz);
bubble_sort(arr, sz);
print_arr(arr, sz);
return 0;
}
这里采用qsort函数的话会更加简单,那么我们来了解一下qsort函数:
这里的void都是不确定的类型,使用者可以根据实际情况来设置类型;
#include
#include
//qosrt函数的使用者得实现一个比较函数
int cmp_int(const void* p1, const void* p2)//由于要比较元素的类型不确定,所以要用不可修改的void的地址;
{
return (*(int*)p1 - *(int*)p2);//强制转换为int*型,前者大于后者则返回大于0的数,前者小于后者返回小于0的数;
}
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), cmp_int);
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
#include
#include
#include
struct stu
{
char name[20];
int age;
};
int sort_by_age(const void* e1, const void* e2)
{
return ((struct stu*)e1)->age - ((struct stu*)e2)->age;//强制转换为需要比较的两个结构体类型元素
}
int sort_by_name(const void* e1, const void* e2)
{
return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);//strcmp函数比较字符串,返回的是字符的ASCII码值;
}
void test1()
{
struct stu s[] = { {"zhangsan",30},{"lisi",34},{"wangwu",28} };
int sz = sizeof(s) / sizeof(s[0]);
//按年龄排序
qsort(s, sz, sizeof(s[0]), sort_by_age);
//按姓名排序
qsort(s, sz, sizeof(s[0]), sort_by_name);
}
int main()
{
test1();
}
当没有进行排序时:
当按年龄排序时:
当按姓名排序时:
这就是关于qsort函数对不同类型的排序,需要注意的是qsort函数默认排的序为升序,当需要降序排序时只需要交换比较函数语句里面两个成员的位置即可;
掌握了qsort函数的使用之后,我们需要模拟qsort函数的运用;
/* qsort函数,头文件,
* 语法结构:void qort(void* base,size_t num,size_t size,int (*compare)(const void*,const void*))
* void* base存放的是待排序数据的第一个对象的地址
* size_t num指的是待排序对象的元素个数,传参时可以用sizeof求得
* size_t size指的是每个元素个数的字节大小
* int (*compare)(const void*,const void*))指的是比较两个元素的函数指针,存放比较函数的地址
* 这里的void*是指不确定的指针类型,便于程序员自己设计指针类型
*/
#include
void print_arr(int arr[],int sz)//打印函数
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int cmp_int(const void* e1, const void* e2)//比较函数
{
return *(int*)e1 - *(int*)e2;//比较函数,由于不知道要比较的是什么类型,所以可以强制转换为自己需要转换的类型
//qsort规定,当结果大于0时返回大于0的数,小于0时返回小于0的数
//至于函数内部怎么根据这个来排序,怎么把比较元素的地址传给e1,e2的是库函数规定的
}
void Swap(char* buf1,char* buf2,int width)//当满足条件时交换函数
{
int i = 0;
for (i = 0; i < width; i++)
{
char tmp =* buf1;
*buf1 =* buf2;
*buf2 = tmp;
buf1++;
buf2--;
}
}
//模拟的qsort函数
void my_qsort(void* base,int sz,int width,int(*cmp_int)(const void* e1,const void* e2))
{
int i = 0;
for (i = 0; i < sz; i++)
{
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
//base接收首元素地址的指针
// j*width表示用下标或者说元素个数乘以一个元素的字节宽度就等于指针要跳过的大小
//(char*)base + j * width和(char*)base + (j + 1) * width代表要比较的两个元素的地址
//由于需要比较的元素类型多变,所以用char型指针+字节宽度就很通用
if (cmp_int((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
{
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
//不知道交换什么类型就可以一个一个字节交换,所以要把字节宽度传参
}
}
}
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
print_arr(arr, sz);
my_qsort(arr,sz,sizeof(arr[0]),cmp_int);//arr代表首元素地址
print_arr(arr, sz);
}
好了,本篇内容就到这里,相信大家看完这篇文章,一定对指针有了更全面的了解。在这里我建议大家可以去像我一样去总结各种指针以及他的用法,这样我们一定会更上一层楼,一起加油啊,大家看完了,别忘了一键三连哦