当前位置: 首页 > news >正文

C语言进阶(2) ---- 指针的进阶

前言:指针的主题,我们在初阶的《指针》章节已经接触过了,我们知道了指针的概念

1.指针就是个变量,用来存放地址,地址唯一标识一块内存空间。

2.指针的大小是固定的4/8个字节(32位平台/64位平台)。

3.指针是有类型的,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限。

4.指针的运算。

目录

1. 字符指针

2.指针数组

3. 数组指针

3.1 数组指针的定义

3.2 &数组名VS数组名

3.3 数组指针的使用

4. 数组参数、指针参数

4.1 一维数组传参

4.2 二维数组传参

4.3 一级指针传参

4.4 二级指针传参

5. 函数指针

6. 函数指针数组

7. 指向函数指针数组的指针

8. 回调函数

8.1.冒泡排序

8.2.qsort函数

8.3.用qsort来改正冒泡排序


1. 字符指针

在指针的类型中我们知道有一种指针类型为字符指针char*;

笔记:

        内存会划分字节为单位的空间,每个字节都有一个编号(地址/指针)

        指针/地址要被存储起来,需要一个空间,这个空间就是指针变量!

        指针(变量)

一般使用方式:

int main()
{char ch = 'w';char *pc = &ch;*pc = 'w';return 0;
}

还可以怎么使用呢,如下:

int main()
{const char*  pstr  = "hello bit."; //这里是把一个字符串放到pstr指针变量里了吗?printf("%s\n", pstr);return 0;}

代码 const char* pstr = "hello bit.";

特别容易让同学以为是把字符串hello bit放到字符指针pstr里了,但是-本质上是把字符串hello bit,首字母的地址放到了pstr中。

上面代码的意思是把一个常量字符串的首字符h的地址存放 指针变量pstr中。
就有可这样的面试题:
#include <stdio.h>
int main O
{char str1[] = "hell o bit.";char str2[] = "hello bit.";const char *str3 = "hell o bit.”;const char *str4 = "hello bit.”;if(str1 == str2)printf("strl and str2 are same\n");elseprintf("str1 and str2 are not same\n") ;if(str3 == str4)printf("str3 and str4 are same\n“);elseprintf("str3 and str4 are not same\n");return 0;
}

这里最终输出的是:

注: 这里str3和str4指向的是一个同一个常量字符串。 C/C++会把常量字符串存储到单独的一个内存区域, 当几个指针,指 向同一个字符串的时候, 实际会指向同一块内存,但是用相同的常量字符串去初始化不同的数组的 候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。

2.指针数组

在《指针》章节我们也学了指针数组,指针数组是一个存放指针的数组。
这里我们再复习一下,下面指针数组是什么意思?
1. int* arr1 [ 10 ]; // 整形指针的数组
2. char * arr2 [ 4 ]; // 一级字符指针的数组
3. char ** arr3 [ 5 ]; // 二级字符指针的数组

重要:       

        整形数组 - 存放整形的数组

        字符数组 - 存放字符的数组

        指针数组 - 存放指针的数组

        数组指针 - 指向数组的指针。

3. 数组指针

3.1 数组指针的定义

数组指针是指针?还是数组?

答案是:指针。指向数组的指针

整型指针: int* pint; 能够指向整形数据的指针。

浮点型指针:float* pf; 能够指向浮点型数据的指针。

那数组指针应该是能够指向数组的指针。

下面代码哪个是数组指针?
int * p1 [ 10 ];
>>p1 -> 是一个指针数组,存放的是指针数组,存放类型为int* .
int ( * p2 )[ 10 ];
>>p2 -> 是一种数组指针,存放的是数组指针,存放元素类型int。
//p1, p2 分别是什么?
p1存放的是第一个元素的地址,
p1+1 - 跳过一个int ,
*p 访问一个int数据4个字节。
p2存放的是arr数组的地址,
p2+1 - 跳过了整个数组,
*p2访问到的是arr数组,*p2等价于arr了,那么*p2就是数组名了,数组名有相当于首元素的地址,所以*p2本质上是arr数组第一个元素的地址。
解释:
int ( * p )[ 10 ];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证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 的类型是: int(*)[10] ,是一种数组指针类型
数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40.

//数组的地址存起来,就应该放在数组指针中!

int (*p)[10] = &arr;

3.3 数组指针的使用
那数组指针是怎么使用的呢?
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。
看代码:
#include <stdio.h>
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,0};int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p//但是我们一般很少这样写代码return 0;
}
一个数组指针的使用:
#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{int i = 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;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_arr1(arr, 3, 5);//数组名arr,表示首元素的地址//但是二维数组的首元素是二维数组的第一行//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址//可以数组指针来接收print_arr2(arr, 3, 5);return 0;
}

学习了指针数组和数组指针,解读下下面代码的意思:

int arr [ 5 ];
arr 是一个整型数组,5个元素,每个元素类型为int。
int * parr1 [ 10 ];
parr1 是一个数组,有10个元素,每个元素的类型是 int* ,所以parr1是指针数组。
int ( * parr2 )[ 10 ];
parr2 和 * 结合,说明parr2是一个指针,该指针指向一个数组,数组是10个元素,每个元素是int类型的。所以 parr2是数组指针。
int ( * parr3 [ 10 ])[ 5 ];
parr3 和 [] 结合,说明parr3是一个数组,数组是10个元素,数组的每个元素是什么类型呢?
是一种数组指针,类型是int(*)[5] . 该类型的指针指向有5个int 类型的元素。
([ ] 优先级高于 * )

那怎么打印数组呢,根据之前学的内容:

int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//最简单的方法,*(p + i),直接首元素地址去打印,然后加一,往后跳去打印int*p = arr;int i = 0;for (i = 0; i < 10; i++){printf("%d ", *(p + i));}return 0;
}

当用数组指针来写的话:

    //用数组指针, 这样不如下面用首元素的地址往后跳,找到数组的地址,然后用下标int (* p)[10] = &arr;int i = 0;for (i = 0; i < 10; i++){printf("%d ", (*p)[i]);}

但是有点大材小用了,我们再来看二维数组中的使用,会不会比以前的方法变的简单呢

void print1(int arr[3][4], int row, int col)
{int i = 0;for (i = 0; i < row; i++){int j = 0;for (j = 0; j < col; j++){printf("%d ", arr[i][j]);//i行j列去打印,二维数组的普通打印用下标}printf("\n");}
}

我们用数组指针来写

void print2(int(*p)[4], int row, int col)
{int i = 0;for (i = 0; i < row; i++){int j = 0;for (j = 0; j < col; j++){			printf("%d ", (*(p + i))[j]);
//先拿到数组指针找到这个数组,p是指针哪一行的,加i就是找到所有的行,然后再加上一个下标}printf("\n");}
}

打印的时候还有一种写法

printf("%d ", p[i][j]);

//这样写也是可以的,直接找到整个数组第一行地址,然后往后下一行,每一行的下标j,找到所有元素

4. 数组参数、指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
4.1 一维数组传参

数组传参的时候,形参写成数组形式。--- ok

#include<stdio.h>void test1()//如何传参数?void test2()//如何传参数?int main()
{int arr1[10] = { 0 };int* arr2[20] = { 0 };test1(arr1);test2(arr2);
}

参数可以这样写:

#include <stdio.h>
void test(int arr[])//ok?     //ok 可以传参
{}//函数中arr传来,数组传参,参数的部分写为数组
void test(int arr[10])//ok? //ok
{}//函数中arr传来,数组传参,参数的部分写为数组,定义个大小也是可以的
void test(int *arr)//ok?     //ok
{}//函数中arr传来,是首元素地址,每个元素都整型,数组传参,也可以用指针接受,所有用整型指针来接收
void test2(int *arr[20])//ok? //ok
{}//函数中arr2传来,是首元素地址,arr2是数组里面放的指针,传来后,用指针数组来接受地址
void test2(int **arr)//ok?    //ok

{}//函数中arr2传来,是首元素地址,arr2是数组里面放的指针,形参里面的*arr,就是数组首元素地址解引用,就是数组,所有**arr就可以接收传来的指针

void test2(int* arr)
{}//这样是不行的,传来的数组名是首元素地址,都是指针,所有我们接收的时候也要二级指针

4.2 二维数组传参

void test(int arr[3][5])//ok? //ok
{} //形参传是二维数组,行是可以省略的,但是列不能省略。
void test(int arr[][])//ok?    //not ok - 不可传参
{}//列不可以省略
void test(int arr[][5])//ok?  //ok
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。

void test(int *arr)//ok?    //error 整型指针
{}
void test(int* arr[5])//ok? //error 数组
{}
void test(int (*arr)[5])//ok? //ok 指针数组
{}
void test(int **arr)//ok?//error 
{}
int main()
{
 int arr[3][5] = {0};
 test(arr);//指的是第一行数组传参
}

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]);//一级指针p,传给函数print(p, sz);return 0;
}
思考:
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?

比如:

void test1 ( int * p )
{}
//test1 函数能接收什么参数?
//test2函数能接收一个指向整形的指针作为参数;
//eg:具体数据类型的指针,指向结构体的指针,指向数组的指针,函数指针。
void test2 ( char* p )
{}
//test2 函数能接收什么参数?
//test2函数能接收一个指向字符的指针作为参数;
//eg:字符串字面量,字符数组,单个字符。
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;
}
思考:
当函数的参数为二级指针的时候,可以接收什么参数?
eg:
1.指向指针的指针;
2.指向数组的指针的指针;
3.指向结构体的指针的指针;
4.函数指针的指针;
void test(char **p)
{ }
int main()
{char c = 'b';char*pc = &c;char**ppc = &pc;char* arr[10];test(&pc);test(ppc);test(arr);//Ok?return 0;
}

5. 函数指针

//函数指针
//函数其实也是有地址的,函数名(或者&函数名)就是函数的地址;
首先看一段代码:
#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 或者 pfun2 是指针,那哪个是指针?
答案是:
pfun1 可以存放。 pfun1 先和 * 结合,说明 pfun1 是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void
分析下两段有趣的代码:
#include<stdio.h>
int main()
{//代码1(*(void (*)())0)();//代码2void (*signal(int, void(*)(int)))(int);return 0;
}
// 代码 1
( * ( void ( * )()) 0 )();
//( void ( * )()) - 函数指针类型
//把0强制转换为一种函数指针类型,无参无返回类型;
这段代码的意思是是将0强制类型转化为void (*)()类型的函数指针,然后再去调用0地址处的这个函数;
// 代码 2
void ( * signal(int , void(*)(int )))( int );
//signal 是一个函数声明,声明的函数名字是signal,
signal 的参数有2个,第一个是int类型,第二个是函数指针:void(*)(int),
该指针指向的函数参数int, 返回类型是void。
//signal 函数的返回类型也是函数指针,该指针指向的函数参数int , 返回类型是void .

//void (*p)(int) ;  //p 是函数指针变量的名字

//typedef void(*pf_t)(int) ; //pf_t 是类型名

void (*signal(int, void(*)(int) )  )(int);
该代码是一次函数的声明,声明的函数名字叫signal
    signal函数的参数有2个,第一个是int类型,第二个是函数指针类型
    该函数指针能够指向的那个函数的参数是int,返回类型是void
 
    signal函数的返回类型是一个函数指针
    该函数指针能够指向的那个函数的参数是int, 返回类型是void
    相当于三个套娃函数指针
 
简化一下(typedef)类型简化,typedef unsigned int uint;(简化方法)
 
    typedef void(*pf_t)(int); //pfun_t就是函数指针的类型
    pf_t signal(int, pf_t);//这就是简化完,

6. 函数指针数组

数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组,
比如:
int * arr [ 10 ];
// 数组的每个元素是 int*
那要把函数的地址存到一个数组中,那这个数组就叫 函数指针数组 ,那函数指针的数组如何定义呢?
int ( * parr1 [ 10 ])();
int * parr2 [ 10 ]();
int ( * )() parr3 [ 10 ];
答案是: parr1
parr1 先和 [] 结合,说明 parr1 是数组,数组的内容是什么呢?
int (*)() 类型的函数指针。
函数指针数组的用途: 转移表
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;
}
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;
}

使用函数指针数组的实现

void menu()
{printf("*******************************\n");printf("****** 1. add   2. sub    *****\n");printf("****** 3. mul   4. div    *****\n");printf("****** 0. exit            *****\n");printf("*******************************\n");
}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;
}//函数指针数组存放上述函数的地址
//但是也限定死了,只能是两个整型的操作数才能
//需要注意的是,我设置的计算是1,2,3,4,进入计算,所以第一个函数指针设置为0,后面对应1,2,3,4
int main()
{int input = 0;int x = 0;int y = 0;int (*p[5])(int, int) = { NULL, Add, Sub, Mul, Div };int ret = 0;do{menu();printf("请选择:>");scanf("%d", &input);//也需要判断我们输入的数进行选择if (input == 0){printf("退出计算器\n");break;}else if (input >= 1 && input <= 4){//直接用函数指针数组,用函数指针,直接进入到函数里,用不用*都一样printf("请输入两个操作数:>");scanf("%d %d", &x, &y);ret = pf[input](x, y);printf("ret = %d\n", ret);}else{printf("选择错误\n");}} while (input);return 0;
}

7. 指向函数指针数组的指针

指向函数指针数组的指针是一个 指针,
指针指向一个 数组 ,数组的元素都是 函数指针 ;
如何定义?
函数指针数组——存放函数指针的数组(存放函数的地址)
int my_strlen(const char* str)
{return 0;
}int main()
{//指针数组char* ch[5];int arr[10] = {0};//pa是数组指针int (*pa)[10] = &arr;//pf是函数指针int (*pf)(const char*) = &my_strlen;//函数指针数组int (*pfA[5])(const char*) = { &my_strlen };return 0;
}

定义:int (*parr1[10])();      parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢?
是 int (*)() 类型的函数指针。

//首先,函数指针数组是数组
parr[];


//数组的每个元素是函数指针,以无参函数,且无返回值的函数为例
void (*) ();
 
//结合后
void (*) () parr[];
 
//规范的写法
void (*parr[])()

void test(const char* str)
{printf("%s\n", str);
}
int main()
{//函数指针pfunvoid (*pfun)(const char*) = test;//函数指针的数组pfunArrvoid (*pfunArr[5])(const char* str);pfunArr[0] = test;//指向函数指针数组pfunArr的指针ppfunArrvoid (*(*ppfunArr)[5])(const char*) = &pfunArr;return 0;
}

8. 回调函数

  回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
如果我们用回调函数来优化上面我们写的计算器
void menu()
{printf("*******************************\n");printf("****** 1. add   2. sub    *****\n");printf("****** 3. mul   4. div    *****\n");printf("****** 0. exit            *****\n");printf("*******************************\n");
}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;
}
//回调函数,写一个函数指针指向函数,传进来都是计算用的函数指针
//把我计算用的这些Add,Sub等等这些函数,传给calc这个函数,然后用clac函数参数的部分是函数指针
//这个clac函数通用了,可以加减乘除,就看传谁的地址,通过传来的函数地址,去找到这个函数
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("%d\n", ret);
}int main()
{int input = 0;do {menu();printf("请选择:>");scanf("%d", &input);switch (input){case 1:calc(Add);//把Add这个函数地址传给clac里面,就是pf这个函数指针,这个pf函数指针指向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;
}

思路:先写一个calc函数,参数用函数指针来接受,前面写的有功能函数,在主函数中,调用calc函数,把功能函数的地址传到calc函数中,calc函数正好用函数指针来接受,calc函数中指针pf通过传来的功能函数地址,找到这些函数,去进行计算,然后返回值再打印,相当于calc就是一个实现计算过程的模型。

8.1.冒泡排序

两两相邻函数进行比较:(从小到大排序)

缺点:只能排序整数

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[] = { 9,8,7,6,5,4,3,2,1,0 };int sz = sizeof(arr) / sizeof(arr[0]);bubble_sort(arr, sz);int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}
}
8.2.qsort函数
void qsort(void* base, //待排序的数组的起始地址size_t num,    //元素个数size_t width,  //一个元素的大小int (*cmp)(const void* e1, const void* e2)//两个元素的比较函数
);

首先演示一下qsort函数的使用:

#include <stdio.h>
//qosrt函数的使用者得实现一个比较函数
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;int sz = sizeof(arr) / sizeof(arr[0]);qsort(arr, sz , sizeof (int), int_cmp);for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++){printf( "%d ", arr[i]);}printf("\n");return 0;
}

qsort就是这样实现的,把你提供的函数 cmp_int 地址传到qsort,qsort里用函数指针去接收,然后去指向这个函数,也就是一个回调函数,e1,e2,是要比较的这两个元素的地址,e1<e2返回小于零的数

8.3.用qsort来改正冒泡排序

冒泡排序可以排任何类型的,进行改造,怎么改

第一层循环是不变的,内部两两比较也不变,变的是判断,整型大小比较才能用大小号,字符或者浮点型比较方法不一样,当我们想让这个冒泡排序任何地方使用,是不是就是把比较的部分抽出来,你想排序整型,你自己提供一个排序整型的方法,想排序啥样的,就提供什么样子的

函数1是用库函数qsort排序整型

函数2是用库函数qsort排序结构体

函数3是用冒泡排序模拟实现库函数qsort排序整型

函数4是用冒泡排序模拟实现库函数qsort排序结构体

int cmp_int(const void* e1, const void* e2)
{return *(int*)e1 - *(int*)e2;//参数是无类型的指针来接受,所以这里要强制类型转化为int,然后再解引用
}
//对于整型的排序
void test1()
{int arr[] = { 9,8,7,6,5,4,3,2,1,0 };int sz = sizeof(arr) / sizeof(arr[0]);qsort(arr, sz, sizeof(arr[0]), cmp_int);int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}
}
//先定义一个结构体
struct Stu
{char name[20];int age;
};//先对结构体的年龄比较
int cmp_stu_by_age(const void* e1, const void* e2)
{return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;//参数是无类型的指针来接受,所以这里要强制类型转化为struct Stu*,相当于拿到了结构体指针,然后再箭头指向年龄
}
//再对结构体的名字比较
int cmp_stu_by_name(const void* e1, const void* e2)
{return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);//参数是无类型的指针来接受,所以这里要强制类型转化为struct Stu*,相当于拿到了结构体指针,然后再箭头指向名字//名字比是字符串比较不能用减号了,用函数strcmp,比较对应位置上的字符,strcmp返回值跟qsort返回值一样的
}
//对于结构体的排序
void test2()
{//结构体的描述struct Stu s[3] = { {"zhangsan",20}, {"lisi", 50}, {"wangwu", 33} };int sz = sizeof(s) / sizeof(s[0]);qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}上面test1和test2是用库函数qsort来排序整型和结构体
上面test3和test4是用冒泡排序的思想模拟实现qsort来排序整型和结构体//改造冒泡排序函数,使得这个函数可以排序任意指定的数组
void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(const void* e1, const void* e2))
//为了能接收各种类型所以void* base,sz就是元素的个数, width宽度你只知道个数,宽度为了知道几个字节,最后用函数指针用来,在这个函数中指针接收要比较的地址
{size_t i = 0;for (i = 0; i < sz - 1; i++){size_t j = 0;for (j = 0; j < sz - 1 - i; j++){if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)//这里是找我们要比较的那俩地址,最开始是base,我们用width去找这一对,往后跳对应字节去找,用j可以找后面所有的要比较的数了//然后这俩地址传给e1,e2,就是通过指针找到比较函数{Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);//交换,交换函数,先把这俩地址给我,再把宽度给我}}}
}
//交换函数,用char接收,然后也接收宽度,交换的细节,是一个字节的交换,不是整个的交换,然后往后跳
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++;}
}//使用我们自己写的bubble_sort函数排序整型数组
void test3()
{int arr[] = { 9,8,7,6,5,4,3,2,1,0 };int sz = sizeof(arr) / sizeof(arr[0]);bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);//我们arr传给base,最开始的地址是base,第二个是加上一个,但是是无类型的,要给她类型//但是给他什么类型,也不能写死,所以我们用我们写的宽度,单位是字节,所以我们转化为char的指针//然后我找base后面的地址,就可以用字节(宽度)跳过int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}
}//使用我们自己写的bubble_sort函数排序结构体数组
//结构体的时候跟上面的是一样的
void test4()
{struct Stu s[3] = { {"zhangsan",20}, {"lisi", 50}, {"wangwu", 33} };int sz = sizeof(s) / sizeof(s[0]);bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age);bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}int main()
{//用库函数qsort实现排序test1();//整型test2();//结构体//自己用冒泡排序的思想模拟实现qsorttest3();//整型test4();//结构体return 0;
}

函数1,函数2,就是我们上面用库函数来进行排序,很方便,只要我们创建一个类型函数,是要去排序整型还是结构体还是其他的,我们去创建一个,然后用函数指针去接收;

函数3,函数4,就是我们用冒牌排序的思想模拟实现这个库函数,在分析一下。

bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);

传要排序的数组,个数,字节,还有我们提供的函数地址

bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age);

bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_name);

结构体也是一样的,要传过去什么,最重要的是我们提供的函数

首先就是用什么去接收 

void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(const void* e1, const void* e2))
为了能接收各种类型所以void* base,sz就是元素的个数, width宽度你只知道个数,宽度为了知道几个字节,最后用函数指针用来,在这个函数中指针接收要比较的地址

然后就是去判断两个比较数

 if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
这里是找我们要比较的那俩地址,最开始是base,我们用width去找这一对,往后跳对应字节去找,用j可以找后面所有的要比较的数了,然后这俩地址传给e1,e2,就是通过指针找到比较函数

判断完去交换位置

Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);

交换,交换函数,先把这俩地址给我,再把宽度给我

思路:

当我们整体看的时候,我们先进入到test3函数中,到我们的冒泡排序bubble_sort中传入数据

传到cmp_int时候,是把cmp_int函数的地址传到bubble_sort函数中的参数int(*cmp)(const void* e1, const void* e2)函数指针中,也就是cmp

所以我bubble_sort函数中的cmp指针指向的就是cmp_int函数,然后我们开始进入循环然后到判断,利用base和wide宽度找到要比较的两个数的地址

由cmp指针指向到cmp_int函数(把这俩比较数的地址传来,给e1,e2)然后算出返回值,然后看是否进行交换,进入到swap交换函数中去交换。

9. 指针和数组笔试题解析
// 一维数组
int a [] = { 1 , 2 , 3 , 4 };
printf ( "%d\n" , sizeof ( a ));
//数组名a单独放在sizeof内部,计算的是整个数组的大小,单位是字节,4*4=16;
printf ( "%d\n" , sizeof ( a + 0 ));
//a表示数组首元素地址,a+0还是数组首元素的地址,是地址大小4/8;
printf ( "%d\n" , sizeof ( * a ));
//a表示的首元素的地址,*a就是对首元素的地址的解引用,就是首元素,大小为4字节;
printf ( "%d\n" , sizeof ( a + 1 ));
//a表示首元素的地址,a+1是第二个元素的地址,是地址,大小就是4/8个字节;
printf ( "%d\n" , sizeof ( a [ 1 ]));
//a[1]是第二个元素,大小是4个字节;
printf ( "%d\n" , sizeof ( & a ));
//&a表示的是数组的地址,但也是地址,地址大小就是4/8个字节;
printf ( "%d\n" , sizeof ( *& a ));
printf ( "%d\n" , sizeof ( & a + 1 ));
printf ( "%d\n" , sizeof ( & a [ 0 ]));
printf ( "%d\n" , sizeof ( & a [ 0 ] + 1 ));
// 字符数组
char arr [] = { 'a' , 'b' , 'c' , 'd' , 'e' , 'f' };
printf ( "%d\n" , sizeof ( arr ));
printf ( "%d\n" , sizeof ( arr + 0 ));
printf ( "%d\n" , sizeof ( * arr ));
printf ( "%d\n" , sizeof ( arr [ 1 ]));
printf ( "%d\n" , sizeof ( & arr ));
printf ( "%d\n" , sizeof ( & arr + 1 ));
printf ( "%d\n" , sizeof ( & arr [ 0 ] + 1 ));
printf ( "%d\n" , strlen ( arr ));
printf ( "%d\n" , strlen ( arr + 0 ));
printf ( "%d\n" , strlen ( * arr ));
printf ( "%d\n" , strlen ( arr [ 1 ]));
printf ( "%d\n" , strlen ( & arr ));
printf ( "%d\n" , strlen ( & arr + 1 ));
printf ( "%d\n" , strlen ( & arr [ 0 ] + 1 ));
char arr [] = "abcdef" ;
printf ( "%d\n" , sizeof ( arr ));
printf ( "%d\n" , sizeof ( arr + 0 ));
printf ( "%d\n" , sizeof ( * arr ));
printf ( "%d\n" , sizeof ( arr [ 1 ]));
printf ( "%d\n" , sizeof ( & arr ));
printf ( "%d\n" , sizeof ( & arr + 1 ));
printf ( "%d\n" , sizeof ( & arr [ 0 ] + 1 ));
printf ( "%d\n" , strlen ( arr ));
printf ( "%d\n" , strlen ( arr + 0 ));
printf ( "%d\n" , strlen ( * arr ));
printf ( "%d\n" , strlen ( arr [ 1 ]));
printf ( "%d\n" , strlen ( & arr ));
printf ( "%d\n" , strlen ( & arr + 1 ));
printf ( "%d\n" , strlen ( & arr [ 0 ] + 1 ));
char * p = "abcdef" ;
printf ( "%d\n" , sizeof ( p ));
printf ( "%d\n" , sizeof ( p + 1 ));
printf ( "%d\n" , sizeof ( * p ));
printf ( "%d\n" , sizeof ( p [ 0 ]));
printf ( "%d\n" , sizeof ( & p ));
printf ( "%d\n" , sizeof ( & p + 1 ));
printf ( "%d\n" , sizeof ( & p [ 0 ] + 1 ));
printf ( "%d\n" , strlen ( p ));
printf ( "%d\n" , strlen ( p + 1 ));
printf ( "%d\n" , strlen ( * p ));
printf ( "%d\n" , strlen ( p [ 0 ]));
printf ( "%d\n" , strlen ( & p ));
printf ( "%d\n" , strlen ( & p + 1 ));
printf ( "%d\n" , strlen ( & p [ 0 ] + 1 ));
// 二维数组
int a [ 3 ][ 4 ] = { 0 };
printf ( "%d\n" , sizeof ( a ));
printf ( "%d\n" , sizeof ( a [ 0 ][ 0 ]));
printf ( "%d\n" , sizeof ( a [ 0 ]));
printf ( "%d\n" , sizeof ( a [ 0 ] + 1 ));
printf ( "%d\n" , sizeof ( * ( a [ 0 ] + 1 )));
printf ( "%d\n" , sizeof ( a + 1 ));
printf ( "%d\n" , sizeof ( * ( a + 1 )));
printf ( "%d\n" , sizeof ( & a [ 0 ] + 1 ));
printf ( "%d\n" , sizeof ( * ( & a [ 0 ] + 1 )));
printf ( "%d\n" , sizeof ( * a ));
printf ( "%d\n" , sizeof ( a [ 3 ]));
总结:
数组名的意义:
1. sizeof( 数组名 ) ,这里的数组名表示整个数组,计算的是整个数组的大小。
2. & 数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
3. 除此之外所有的数组名都表示首元素的地址。

结束语

指针有很多需要学习的地方,本文有借鉴前人经验,非常感谢。。。

相关文章:

C语言进阶(2) ---- 指针的进阶

前言&#xff1a;指针的主题&#xff0c;我们在初阶的《指针》章节已经接触过了&#xff0c;我们知道了指针的概念&#xff1a; 1.指针就是个变量&#xff0c;用来存放地址&#xff0c;地址唯一标识一块内存空间。 2.指针的大小是固定的4/8个字节(32位平台/64位平台)。 3.指针是…...

使用Python筛选图片

需求&#xff1a;图片识别&#xff0c;一堆人脸照片中&#xff0c;其中有不是人脸的&#xff0c;把模糊的&#xff0c;侧脸的&#xff0c;重复的去掉 使用说明&#xff1a; 安装好所需要的包&#xff1a; 第一步&#xff1a;输入图片路径 第二步&#xff1a;创建图片输出路径…...

GESP CCF python五级编程等级考试认证真题 2024年12月

1 单选题&#xff08;每题 2 分&#xff0c;共 30 分&#xff09; 第 1 题 下面的程序中&#xff0c;x,y都是正整数&#xff0c;完成的算法是&#xff08; &#xff09; def chenadai(x, y): while y: x, y y, x % y return x A. 最小公倍数 …...

URL的概念与格式

URL概念及组成 1、URL简介2、URL组成 1、URL简介 URL&#xff08;Uniform Resource Locator&#xff09;即统一资源定位符&#xff0c;是一种用于标识和定位互联网上资源的字符序列 URL主要用于在Web中标识和定位Web页面、图像、文件、服务和其他各种类型的资源 URL提供了一种…...

【Elasticsearch】高亮搜索:从原理到Web呈现

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;精通Java编…...

samout llm解码 幻觉更低更稳定

这段代码定义了一个简单的对话生成系统&#xff0c;包括模型加载、词汇表加载、以及基于给定提示生成文本的功能。下面是对代码的解析&#xff1a; load_model_and_voc(device"cpu"): 该函数用于加载预训练的模型和词汇表&#xff08;vocabulary&#xff09;。它首先…...

单片机:实现多任务处理(附带源码)

单片机实现多任务处理 多任务处理是现代操作系统的重要特性&#xff0c;通常通过多线程、多进程的方式来并行执行多个任务。在嵌入式系统中&#xff0c;由于资源有限&#xff0c;通常通过时间片轮转或中断机制来模拟多任务处理。本项目将展示如何在8051单片机上实现简单的多任…...

负载均衡oj项目:介绍

目录 项目介绍 项目演示 项目介绍 负载均衡oj是一个基于bs模式的项目。 用户使用浏览器向oj模块提交代码&#xff0c;oj模块会在所有在线的后端主机中选择一个负载情况最低的主机&#xff0c;将用户的代码提交给该主机&#xff0c;该主机进行编译运行&#xff0c;将结果返回…...

剑指Offer 03比特位计数

只是记录 题目链接 题目链接 自己想出来的 第一种解法 思路简述 遍历[0,n]之间的数字&#xff0c;对于每一个数字按照二进制的方式展开&#xff0c;判断最低位置是否为1&#xff0c;若为1则1&#xff0c;反之不加&#xff0c;直到该数字等于0就停止。 public static int[] …...

多音轨视频使用FFmpeg删除不要音轨方法

近期给孩子找宫崎骏动画&#xff0c;但是有很多是多音轨视频但是默认的都是日语&#xff0c;电视上看没办法所以只能下载后删除音轨文件只保留中文。 方法分两步&#xff0c;先安装FFmpeg在转文件即可。 第一步FFmpeg安装 FFmpeg是一个开源项目&#xff0c;包含了处理视频的…...

elasticsearch 使用enrich processor填充数据

文章目录 使用 POST 请求手动插入用户数据1. 创建 Enrich Policy步骤 1.1: 创建 Enrich Policy步骤 1.2: 执行 Enrich Policy 2. 创建 Ingest Pipeline步骤 2.1: 创建 Ingest Pipeline步骤 2.2: 配置 Enrich Processor 参数 3. 使用 Ingest Pipeline步骤 3.1: 使用 Pipeline 进…...

VMProtect:软件保护与安全的全面解决方案

在当今数字化时代&#xff0c;软件的安全性和保密性愈发重要。VMProtect 作为一款备受瞩目的软件保护工具&#xff0c;因其强大的功能和广泛的应用而成为开发者保护软件的首选方案。 VMProtect 是一款新一代的软件保护实用程序&#xff0c;支持多个编译器平台&#xff0c;包括…...

Web 毕设篇-适合小白、初级入门练手的 Spring Boot Web 毕业设计项目:教室信息管理系统(前后端源码 + 数据库 sql 脚本)

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 1.0 项目介绍 开发工具&#xff1a;IDEA、VScode 服务器&#xff1a;Tomcat&#xff0c; JDK 17 项目构建&#xff1a;maven 数据库&#xff1a;mysql 8.0 系统用户前台和管理…...

第十二篇:linux下socket本地套接字通讯

使用套接字除了可以实现网络间不同主机间的通信外&#xff0c;还可以实现同一主机的不同进程间的通信&#xff0c;且建立的通信是双向的通信。socket进程通信与网络通信使用的是统一套接口&#xff0c;只是地址结构与某些参数不同。 用途 进程间通信&#xff1a;本地套…...

Spring Boot 2.1.7 数据源自动加载过程详解

在 Spring Boot 中&#xff0c;数据源的自动配置是框架中一个关键功能&#xff0c;本文将以 Spring Boot 2.1.7 版本为例&#xff0c;详细讲解在单数据源情况下数据源是如何自动加载的。我们通过源码分析&#xff0c;追踪整个加载流程。 1. 自动配置类的发现 Spring Boot 使用…...

【Vue.js 3.0】provide 、inject 函数详解

在 Vue 3 中&#xff0c;provide 和 inject 是用于跨组件层次结构进行依赖注入的一对 API。这些 API 主要用于祖先组件和后代组件之间的数据传递&#xff0c;尤其是当这些组件之间没有直接的父子关系时。 1. 示例 1.1 provide provide 函数用于在祖先组件中定义一个值&#…...

JVM(Java虚拟机)的虚拟机栈

JVM&#xff08;Java虚拟机&#xff09;的虚拟机栈是Java程序运行时的重要组件&#xff0c;以下是对其的详细解析&#xff1a; 一、概念与功能 概念&#xff1a;虚拟机栈也称为Java栈&#xff0c;是JVM为每个线程分配的一个私有的内存区域。每个线程在创建时都会创建一个虚拟…...

Elasticsearch02-安装7.x

零、文章目录 Elasticsearch02-安装7.x 1、Windows安装Elasticsearch &#xff08;1&#xff09;JDK安装 Elasticsearch是基于java开发的&#xff0c;所以需要安装JDK。我们安装的Elasticsearch版本是7.15&#xff0c;对应JDK至少1.8版本以上。也可以不安装jdk&#xff0c;…...

iPhone恢复技巧:如何从 iPhone 恢复丢失的照片

在计算机时代&#xff0c;我们依靠手机来捕捉和存储珍贵的回忆。但是&#xff0c;如果您不小心删除或丢失了手机上的照片怎么办&#xff1f;这真的很令人沮丧和烦恼&#xff0c;不是吗&#xff1f;好吧&#xff0c;如果您在 iPhone 上丢失了照片&#xff0c;您不必担心&#xf…...

vba批量化调整word的图和图表标题

vba代码 将图片进行居中操作 Sub ChangePictureFormate()Dim oPara As ParagraphDim oRange As RangeDim i As LongDim beforeIsPicture As BooleanbeforesIsPicture False 确保文档中至少有图片If ActiveDocument.InlineShapes.Count 0 ThenMsgBox "没有找到图片。&qu…...

【Flutter_Web】Flutter编译Web第二篇(webview篇):flutter_inappwebview如何改造方法,变成web之后数据如何交互

前言 欢迎来到第二篇文章&#xff0c;这也是第二个难题&#xff0c;就是原有的移动端本身一些页面H5的形式去呈现&#xff08;webview&#xff09;&#xff0c;例如某些需要动态更换内容的页面&#xff0c;某些活动页面、支付页面&#xff0c;不仅仅做页面呈现&#xff0c;还包…...

【C语言的奥秘11】指针知识点总结(续)

目录 一、指针的运算 1、指针与整数相加减 2、指针-指针&#xff08;地址-地址&#xff09; 3、指针的关系运算 六、指针和数组 七、二级指针 八、指针数组 一、指针的运算 1、指针与整数相加减 看一下下面的代码&#xff1a; #include<stdio.h> int my_strlen(c…...

excel 列名是数据表 的字段名 ,单元格的值 是数据表对应字段的值,生成sql插入语句

在 Excel 中&#xff0c;按 Alt F11 打开 VBA 编辑器。在菜单栏选择 插入 -> 模块&#xff0c;在新模块中粘贴以下代码。 VBA 代码 Sub GenerateSQLInsertStatementsToFile()Dim ws As WorksheetDim lastRow As Long, lastCol As Long, i As Long, j As LongDim sql As S…...

AI Agent与MEME:技术与文化融合驱动Web3创新

AI Agent如何引领Web3新时代&#xff1f; 随着Web3与区块链技术的迅速发展&#xff0c;AI Agent作为人工智能与区块链的交汇点&#xff0c;正在逐步成为推动去中心化生态的重要力量。同时&#xff0c;MEME文化凭借其强大的社区驱动力和文化渗透力&#xff0c;在链上生态中扮演着…...

IO的入门

目录 1.IO概述1.1流的分类 2.字符流2.1 案例 1.IO概述 IO&#xff08;Input/Output&#xff09;:输入和输出&#xff0c;指的是某个设备或环境进行数据的输入或者输出。例如&#xff1a;键盘的输入&#xff0c;再比如显示器就是输出设备&#xff0c;输出图像。 对于java来说输…...

构建一个rust生产应用读书笔记四(实战1)

我们需要从访客那里收集哪些信息&#xff0c;以便将其登记为电子邮件通讯的订阅者&#xff1f; 电子邮件地址&#xff1a;这是最基本的要求&#xff0c;因为我们需要通过电子邮件地址向订阅者发送内容。姓名&#xff1a;虽然这不是强制性的&#xff0c;但我们希望收集一个名字…...

SpringCloudAlibaba | Sentinel从基础到进阶

一、Sentinel简介 Sentinel是SpringCloudAlibaba的一个组件&#xff0c;主要用于解决微服务架构中的高可用性和稳定性问题&#xff08;雪崩问题&#xff09;。 常见的使用场景有&#xff1a; 流量控制舱壁模式&#xff08;线程隔离&#xff09;超时处理熔断降级 二、流量控…...

算法刷题Day18: BM41 输出二叉树的右视图

题目链接 描述 思路&#xff1a; 递归构造二叉树在Day15有讲到。复习一下&#xff0c;就是使用递归构建左右子树。将中序和前序一分为二。 接下来是找出每一层的最右边的节点&#xff0c;可以利用队列层次遍历。 利用队列长度记录当前层有多少个节点&#xff0c;每次从队列里…...

【信息系统项目管理师-论文真题】2015下半年论文详解(包括解题思路和写作要点)

更多内容请见: 备考信息系统项目管理师-专栏介绍和目录 文章目录 论题一:大项目或多项目的成本管理解题思路写作要点论题二:项目的采购管理解题思路写作要点论题一:大项目或多项目的成本管理 随着移动互联网、物联网、云计算、大数据等新一代信息技术的广泛应用,我国目前…...

Windows如何安装go环境,离线安装beego

一、安装go 1、下载go All releases - The Go Programming Language 通过网盘分享的文件&#xff1a;分享的文件 链接: https://pan.baidu.com/s/1MCbo3k3otSoVdmIR4mpPiQ 提取码: hxgf 下载amd64.zip文件&#xff0c;然后解压到指定的路径 2、配置环境变量 需要新建两个环境…...