C语言 指针学习笔记

目录

一、指针初探

1、指针的基础概念

2、指针运算

3、指针与数组

4、二级指针

二、指针进阶

1、字符指针

2、指针数组和数组指针

3、数组参数和指针参数

4、函数指针和函数指针数组

5、回调函数

三、练习与巩固


指针一直很难理解的知识点,希望通过整理的方式做到梳理和融会贯通。附上本文学习的视频:

一次性搞定C语言指针-指针终结者(指针深入剖析C语言指针指针不难C语言指针C语言指针C语言指针C语言指针C语言指针C语言指针C语言指针C语言指针C语言指针指针)_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1ah411R7dp?spm_id_from=333.337.search-card.all.click

一、指针初探

1、指针的基础概念

        指针是什么?

        指针是编程语言中的一个用于存储地址的变量,利用地址,它的值直接指向存储在电脑存储器中另一个地方的值。

int main(){
    int a = 10;
    int* p = &a; // p是一个指针,p中存储的是a的地址
    return 0;
}

        指针有多大?

        指针要能够表示一个“地址的大小”。

        在一台32位的计算机上,地址的大小是32 bit = 4 B,故一个指针的大小为四个字节。

        在一台64位的计算机上,地址的大小是64 bit = 8 B,故一个指针的大小为八个字节。

int main(){
    printf("%d\n",sizeof(char*));
    printf("%d\n",sizeof(int*));
    printf("%d\n",sizeof(double*));
    //输出全为8(4)
}

        指针的类型?

        指针的大小既然相同,那为什么还要区分类型呢?

int main(){
    int a = 10;
    int* pa = &a;
    char* pc = &a;
    printf("%p\n",pa); //000000a37c9ff7cc
    printf("%p\n",pc); //000000a37c9ff7cc
    // 无论是用哪种类型的指针来接收a的地址,都是一样的
    // 那指针为什么要区分类型?
}

        1) 指针类型决定了指针进行解引用操作的时候,能够访问空间的大小。

        int* p; p能访问4个字节;

        char* p; p能访问1个字节;

        2) 指针类型决定了指针的步长(单位:字节)

int main(){
    int a = 10;
    int* pa = &a;
    char* pc = &a;

    printf("pa = %p\n",pa);
    printf("pa+1 = %p\n",pa+1);

    printf("pc = %p\n",pc);
    printf("pc+1 = %p\n",pc+1);
}

//pa = 000000eaacfff98c
//pa+1 = 000000eaacfff990
//pc = 000000eaacfff98c
//pc+1 = 000000eaacfff98d

        int* p ;p+1走4个bit;

        char* p; p+1走1个bit;

        如下这个例子可以更好的理解指针的移动步长。

int main(){
    int arr[10] = {0};
    int* p =arr;
    for(int i = 0;i<10;i++){
        *(p+i) = 1;
    }
    for(int i = 0;i<10;i++){
        printf("%d ",arr[i]);
    }
    // 输出1 1 1 1 1 1 1 1 1 1
    printf("\n");


    int arr2[10] = {0};
    char* p2 =arr2;
    for(int i = 0;i<10;i++){
        *(p2+i) = 1;
    }
    for(int i = 0;i<10;i++){
        printf("%d ",arr2[i]);
    }
    // 输出16843009 16843009 257 0 0 0 0 0 0 0
    // 00000001000000010000000100000001 第一个int 32bit 4B
    // 00000001000000010000000100000001 
    // 0000000100000001
}

野指针? 

野指针就是指针指向的位置是不可知的指针。

在这个例子中,p1不是野指针,故它存储的就是a的地址;p2是野指针,地址随机,可能指向一份受保护的地址空间,因此,使用p2去操作这块未知的地址是危险的。

int main(){
    int a = 20;
    int* p1 = &a;
    printf("%p\n",&a);
    printf("%p\n",p1);

    int* p2;// 野指针
    printf("%p\n",p2);
    // p2 = 20;

    return 0;
}

在这个例子中,test函数释放了局部变量a,于是p也成为了野指针。

int* test(){
    int a =10;
    return &a;
}

int main(){
    int* p = test();
    // *p = 20;
    return 0;
}

在这个例子中,由于数组越界,指针对某块未被定义的空间进行了修改。

int main(){
    int arr[10] = {0};
    int* p = arr;
    for(int i = 0; i <= 12;i++){
        *p = i;
        p++;
        //*p++ = i;
    }
}

为了避免野指针的存在,请务必为每个指针变量初始化。

暂时不知道指向谁?

 int* p = NULL;

2、指针运算

指针加减整数

在看下一个例子之前,需要强调的是,对于一个数组a来说,数组名可以作为数组第一个元素的指针,即 a 等价于 &a[0],a + i 等价于&a[i]。

那么&a的意义是什么呢?&a数值上等于整个数组的起始位置地址,含义上代表整个数组所占内存的大小,于是,&a + 1 等价于数组结束之后的下一段的起始位置地址。

int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int sz = sizeof(arr)/sizeof(arr[0]);
    int* p = arr;
    for(int i=0;i<sz;i++){
        printf("%d ", *p);
        p = p + 1; // 指针与整数运算(等价于p++) 指向下一个元素
    }
}
// 1 2 3 4 5 6 7 8 9 10

以下这段代码于上述代码等价哦:

int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    for(int* p=&arr[0];p<&arr[10];){
        printf("%d ", *p++); 
    }
}

指针与指针运算

指针(大地址)—指针(小地址) = 中间元素个数

int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    printf("%d\n",&arr[9]-&arr[0]);// 9
    printf("%d\n",&arr[3]-&arr[4]);// -1
}

用指针实现strlen函数:

int mystrlen(char* str){
    char *p = str;
    int len = 0;
    while(*p++!='\0'){
        len++;
    }
    return len;
}

// 运用到了指针之间的减法运算 本质上与第一种方法一致
int mystrlen2(char* str){
    char *start = str;
    char *end = str;
    while(*end !='\0'){
        end++;
    }
    return end-start;
}

int main(){
    char s[] = "tangchujiang";
    printf("%d\n",mystrlen(s));
    printf("%d\n",mystrlen2(s));
}

利用指针的大小比较(即指针之间的运算),逆向输出数组:

int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    for(int* p=&arr[10];p>&arr[0];){
        printf("%d ", *--p);
    }
//    for(int* p=&arr[9];p>=&arr[0];p--){
//        printf("%d ", *p);
//    }
// 等价写法 关键是理解 a++ 和 ++a 的预算顺序
// 然而这种写法是不推荐的 原因是C语言的标准中规定:
// 允许拿数组空间向后一位的越界元素进行比较,不允许拿数组空间向前一位的越界元素进行比较
// 第二种写法在判断终止条件时,会用到“arr[-1]”这个越界元素

}
// 10 9 8 7 6 5 4 3 2 1

3、指针与数组

前文已经提及,数组名就是首元素地址,arr 等价于 &arr[0]。以下这个例子充分说明了二者之间的关系:

int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int *p = arr;
    for(int i = 0;i<10;i++){
        printf("%p=======%p\n",p+i,&arr[i]);
    }
    printf("\n");
    for(int i = 0;i<10;i++){
        printf("%d=======%d\n",*(p+i),arr[i]);
    }
}

但需要注意以下两种写法:

1、&arr[0]——数组名表示的表示整个数组

2、sizeof(arr)——数组名表示的是整个数组(必须只有arr本身 不能在表达式中)

int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    printf("%d",sizeof(arr)); // 40
}

对于数组名、取地址数组名、sizeof(数组名)等指代的理解,可以下划本文至练习部分的1、2题。 

4、二级指针

int main(){
    int a = 10;
    int* pa = &a;
    // pa指针也是一个变量 也有他的地址
    int** ppa = &pa;
    // ppa是个二级指针 即 指向指针的指针
    printf("%d\n",*pa);
    printf("%d\n",**ppa);
}

二、指针进阶

1、字符指针

字符指针有两种用法:

其一,它可以存储一个字符的地址,指向一个字符,如pc1;

其二,它可以存储一个字符数组的首地址,如pc2;

int main(){
    char ch = 'w';
    char* pc1  = &ch;
    printf("%c\n",*pc1);

    char arr[] = "abcdef";
    char* pc2 = arr; // 数组名就是字符数组首地址
    printf("%s\n",arr);
    printf("%s\n",pc2); // attention!!!

    return 0;
}

等一下,华生,你有没有发现盲点!

printf("%s\n",pc2);

解引用去哪里了?

其实我们无需感到奇怪,记得我们在使用scanf函数时,输入一个字符scanf("%c",&a)和一个字符串scanf("%s",b)时就已经在提示我们原因了,附上某乎上一位大佬的解释。 

总结:字符指针数组不需要解引用!

常量字符串

int main(){
    char *pc3 = "abcdef";//"abcdef"是一个常量字符串
    printf("%s\n",pc3);
}

这样定义的字符串叫做常量字符串,如果我们试图修改它:

*pc3 = 'w';

不会成功。它是如何存储的呢?

int main(){
    char arr1[]="abcdef";
    char arr2[]="abcdef";
    if(arr1==arr2){
        printf("equal1");
    }
    char* p1 = "abcdef";
    char* p2 = "abcdef";
    if(p1==p2){
        printf("equal2");
    }

    // 输出结果为 equal2
}

事实证明,arr1和arr2在内存中开辟了两片不同的地址空间,而既然常量字符串不能被修改,无法对他进行操作,那为什么要存两份?

于是,p1和p2其实指向了同一份地址空间,故p1 == p2。

常量字符串有且仅有一片存储空间。

最后,在定义常量字符串时用const修饰,是我们该形成的书写习惯。

const char* p1 = "abcdef";

2、指针数组和数组指针

指针数组

指针(修饰语)数组(主语)仍然是一个数组,区别于后面的知识点——数组指针。

字符数组——存放字符,整形数组——存放整形,则指针数组,顾名思义,存放指针的数组。

int main(){

    int arr1[] = {1,2,3,4,5};
    int arr2[] = {2,3,4,5,6};
    int arr3[] = {3,4,5,6,7};
    int *p2[3] = {arr1,arr2,arr3};

    for(int i = 0;i<3;i++){
        for(int j =0;j<5;j++){
            printf("%d",*(p2[i]+j));// p2[i]就是每个arr的首元素地址
        }
        printf("\n");
    }
    //12345
    //23456
    //34567
    return 0;
}

 数组指针

顾名思义,数组指针是指向数组的指针。

int main(){
//    int *pi = NULL; //可以存放整形地址
//    char *pc = NULL; //可以存放字符地址
//    数组指针—指向数组的指针—存放数组的地址
//    数组的地址??  &arr!!!

    int arr[10] = {1,2,3,4,5,6,7,8};
    // 优先级 []>* 故如果写为 int* p[10]——p是一个数组
    // 为了让p是一个指针 请用()
    // 理解:p是一个指针 指向一个数组 数组中的每个元素是整形
    int(*p)[10] = &arr;
    
}

请试图理解如下代码,它同时包括了数组指针和指针数组:

int main(){
    char* ch_arr[5];
    // p是一个指针 指向一个指针数组 数组中的每个元素是char*类型
    char* (*p)[5] = &ch_arr;
}

数组指针指向了一个数组,那他当然可以指向一个指针数组,这很合乎逻辑,不是吗?

数组指针——指向数组

当我们试图用数组指针指向一个一维数组:

int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int (*pa)[10] = &arr;
    printf("%p ",(*pa));// 对pa解引用 可以拿到整个数组
    printf("%p ",arr);
    printf("%p ",&arr[0]);
    // *pa == arr
}

我们可以拿到每一个元素: 

int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int (*pa)[10] = &arr;
    for(int i = 0;i<10;i++){
        printf("%d ",(*pa)[i]); // *pa = arr
        // 等价于 printf("%d ",arr[i]);
    }
    printf("\n");
    for(int i = 0;i<10;i++){
        printf("%d ",*(*pa+i));
        // 等价于 printf("%d ",*(arr+i));
    }
}

总感觉怪怪的,实现用指针输出数组中的每一个元素我们不早已经掌握了吗? 

// 指针指向首元素 而不是指向整个数组 会更好!!!
int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int *p = arr;
    for(int i = 0;i<10;i++){
        printf("%d ",*(p+i));
    }
}

 所以,我们在实际运用中,应该让数组指针指向二维数组。

遍历输出二维数组(非指针实现):

void print1(int arr[3][5],int x,int y){
    for(int i = 0; i<x;i++){
        for(int j = 0;j<y;j++){
            printf("%d ",arr[i][j]);
        }
        printf("\n");
    }
}
int main(){
    int arr[3][5] = {{1,2,3,4,5},{2,4,6,8,10},{1,3,5,7,9}};
    print1(arr,3,5); // attention!!!
    return 0;
}

在这段程序中,我们需要注意的是,当我们向print1函数传递参数的时候,我们传递的是二维数组的数组名,他应该代表着“二维数组的首地址”。需要注意,二维数组的首地址并不是元素arr[0][0]的地址,而是数组arr[0]的地址。我们要将这个二维数组看作一维数组考虑。

print1(arr[0],3,5);

因此,这里传参数arr和arr[0]是等价的。

遍历输出二维数组(指针实现):

void print2(int(*p)[5],int x,int y){
    for(int i = 0; i<x;i++){
        for(int j = 0;j<y;j++){
            // 以下四种写法完全等价
            // 写法一:p就是arr 就是数组名 所以p[i][j]便可以输出arr[i][j]
            printf("%d ",p[i][j]);
            // 写法二:p[i]是第i个一维数组 p[i]+j是这个数组内第j个元素的地址 解引用即可得到这个元素
            printf("%d ",*(p[i]+j));
            // 写法三:p+i是第i个一维数组的地址 解引用就是第i个一维数组 *(p+i)+j是这个数组内第j个元素的地址 解引用即可得到这个元素
            printf("%d ",*(*(p+i)+j));
            // 写法四:p+i是第i个一维数组的地址 解引用就是第i个一维数组 *(p+i))[j]等价于arr[i][j]
            printf("%d ",(*(p+i))[j]);
        }
        printf("\n");
    }
}

int main(){
    int arr[3][5] = {{1,2,3,4,5},{2,4,6,8,10},{1,3,5,7,9}};
    print2(arr,3,5);
    return 0;
}

时刻注意[]的优先级比*高,就不会出现加括号上的错误。

练习:这些分别是什么?

1、int arr[5];
2、int *parr1[10];
3、int (*parr2)[10];
4、int (*parr3[10])[5];

 解答:

1、整形数组 

2、parr1是一个有10个元素的数组,每个元素的类型是int*类型

3、parr2是一个指针,指向了一个有10个元素的数组,这个数组中的元素类型是int

4、parr3是一个有10个元素的数组,每个元素是一个数组指针,该数组指针指向的数组有5个元素,每个元素的类型是int。

3、数组参数和指针参数

写代码时,我们要传递数组的参数和指针的参数,这也是困扰了我很久的问题。

首先,我们从数组的视角来看:

1)讨论整形数组传参。

// 三种方式接受整形数组
void test1(int arr[]){} // 拿数组接收数组 数组大小可以省略
void test2(int arr[10]){} // 拿数组接受数组
void test3(int *arr){} // 传一个数组名 即首元素地址 故接收类型是int*

int main(){
    int arr[10] = {0};
    test1(arr);
    test2(arr);
    test3(arr);
}

2)讨论指针数组传参。

void test1(int* arr[]){} // 传递一个指针数组 就拿一个数组指针接收
void test2(int* arr[20]){} // 传递一个指针数组 就拿一个数组指针接收
void test3(int **arr){} // 传入一个一级指针地址 拿二级指针接收

int main(){
    int* arr2[10] = {0};
    test1(arr2);
    test2(arr2);
    test3(arr2);
//    p2可以接收一级指针的地址
//    int a = 20;
//    int *p = &a;
//    int **p2 = &p;
//    printf("%d",**p2);
}

3)讨论二维数组传参。

void test1(int arr[3][2]){} // 拿二维数组接收二维数组
void test2(int arr[][2]){} // 只可以省略行 不能省略列
/* 为什么不能省略列?这里引用CSDN博主 谷歌玩家 的解释
 * 将二维数组看成一个个一维数组的组合,则列意味着一维数组的容量;
 * 只有确定每个一维数组存放多少个元素才能正确定义二维数组;
 * 如果省略列的定义,那么操作系统不知道每个一维数组要分配多少个元素,
 * 有可能是平均分配,有可能全都给第一个一维数组分配,这就造成了歧义。
*/
void test3(int (*arr)[2]){} // 用一个数组指针接收 指向一个数组 这个数组每个元素是int
// 恰好与arr[3][2]的第一行一致

int main(){
    int arr[3][2] = {0};
    test1(arr);
    test2(arr);
    test3(arr);// 二维数组的数组名表示首个一维数组(第一行)的地址
}

其次,我们从指针的视角来看:

1)讨论一级指针参数:

void test1(int* p){}

int main(){
    int a = 20;
    test1(&a); // 当然可以传递一个a的地址
    int *p = &a;
    test1(p); // 传递一个一级指针
}

2)讨论二级指针参数:

void test2(int** pp){}

int main(){
    int a = 20;
    int *p = &a;
    int** pp = &p;
    test2(&p);//可以传一级指针地址
    test2(pp);//可以传二级指针本身

    int* arr[10] = {0};
    test2(arr); //指针数组的数组名意味着数组的首元素地址 一个int*变量的地址可以用二级指针接收
}

4、函数指针和函数指针数组

函数也有地址吗?我们写一个简单的加法函数。

int add(int x,int y){
    return x+y;
}

int main(){
    printf("%p ",add);
    printf("%p ",&add);
    // 00007ff68b8f1731 00007ff68b8f1731
}

函数也有地址,且与数组不同,add和&add的含义是一样的。既然函数有地址,我们就可以定义指向函数的指针。

int (*pa)(int,int) = add;
// 函数的返回类型 + *指针名 + 函数参数类型
// 如果不加括号 则pa会和(int,int)结合 pa则成为一个函数而不是指针

*pa就是函数,(*pa)(2,3)等价于add(2,3)。 

int main(){
    int (*pa)(int,int) = add;
    (*pa)(2,3);
}

再看一个同样的例子,以加深理解:

#include <stdio.h>
void Print(char* str){
    printf("%s",str);
}
int main(){
    void (*p)(char *) = Print;
    (*p)("tangchujiang");
}

补充:调用函数指针时,*是可以省略的。

int main(){
    int (*pa)(int,int) = add;
    (*pa)(2,3);
    (pa)(2,3);
}

理解了函数指针的概念,我们来看函数指针数组。函数指针数组里存放着若干相同类型的函数的地址。

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;
}
int main(){
    int (*parr[4])(int,int) = {add,sub,mul,div};
    for(int i = 0;i<4;i++){
        printf("%d\n",parr[i](6,3));
    }
}

函数指针有什么作用呢?我们以下面这个计算器的小程序为例:

四则运算计算器(非函数指针):

#include <stdio.h>
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;
}

int main(){
    int input = 0;
    int x,y = 0;
    do {
        menu();
        printf(">>>>>>>>>>>\n");
        scanf("%d",&input);

        switch(input){
            case 1:
                printf("input x y>>>>>\n");
                scanf("%d %d",&x,&y);
                printf("result = %d\n",add(x,y));
                break;
            case 2:
                printf("input x y>>>>>\n");
                scanf("%d %d",&x,&y);
                printf("result = %d\n",sub(x,y));
                break;
            case 3:
                printf("input x y>>>>>\n");
                scanf("%d %d",&x,&y);
                printf("result = %d\n",mul(x,y));
                break;
            case 4:
                printf("input x y>>>>>\n");
                scanf("%d %d",&x,&y);
                printf("result = %d\n",div(x,y));
                break;
            case 0:
                printf("exit!\n");
                break;
            default:
                printf("wrong!\n");
        }
    }while(input);
}

他的问题是繁琐、冗余,有着大量重复性操作,我们可以利用函数指针进行简化:

int main(){
    int input = 0;
    int x,y = 0;
    int (*pfArr[5])(int,int) = {0,add,sub,mul,div};
    do {
        menu();
        printf(">>>>>>>>>>>\n");
        scanf("%d",&input);
        if(input>=1 && input <=4){
            printf("input x y>");
            scanf("%d %d",&x,&y);
            int ret = pfArr[input](x,y);
            printf("result = %d\n",ret);
        }
        else if(input == 0){
            printf("exit!");
        }
        else{
            printf("wrong!");
        }
    }while(input);
}

5、回调函数

理解回调函数的定义之前,我们先思考这个冒泡排序算法:它实现了对一个数组的排序。

void bubble_sort(int arr[],int sz){
    for(int i = 0;i<sz-1;i++){ //排序躺数
        for(int j = 0;j<sz-i-1;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,5,2,4,0};
    int sz = sizeof(arr)/sizeof(arr[0]);
    bubble_sort(arr,sz);
    for(int i = 0;i<sz;i++){
        printf("%d ",arr[i]);
    }
}

函数完美运行了,但是这个函数的问题是不沟通用,比如,当我试图排序一个浮点数数组:

float arr2[] = {1.3,3.3,7.8,0.9,1.4};

比如我想排序学生的年龄,而学生的结构是结构体类型:

struct Stu
{
    char name[20];
    int age;
};
typedef struct Stu student;

如何解决?我们分析C语言的库函数qsort是如何解决的。

void qsort(void *base,
           size_t nitems,
           size_t size,
           int (*compar)(const void *, const void*))
           {}

分析C语言库函数qsort

1)void* 是一个无类型指针,他可以接收任意类型的变量。但不能对他进行解引用操作,因为我们不知道void*指向的到底是int还是char,解引用该访问四个字节还是一个字节。

        int a = 10;
        char c = 'w';
        void* p = &a;
        void* q = &c;
        // *p = 20; 报错 不能进行解引用

2)size_t nitems接收要比较的元素的数量,size接收每个元素的大小;

size_t是标准C库中定义的,在64位系统中为long long unsigned int,非64位系统中为long unsigned int,即表示了表示C中任何对象所能达到的最大长度。

为什么要接收每个元素的大小?这是一种隐晦传类型的方式。因为C语言无法直接传数据的类型,所以通过传递每个元素的大小体现类型。

3)int (*compar)(const void *, const void*):compare是一个函数指针,它接受两个元素的地址。由于这两个元素的类型未知,所以我们是用void*接收的。

compare函数的返回值是int,那自然正数,0,负数就对应着三种不同的大小关系。

compare函数的具体实现正是我们要定义并传入的。

排序整形数组:

#include <stdio.h>
#include <stdlib.h> //qsort在此头文件中

int cmp_int(const void* e1,const void* e2){
    // 无法对void*类型解引用 就先进行强制类型转换
    return *(int *)e1 - *(int*)e2;
}

int main(){
    int arr[] = {9,5,2,4,0};
    int sz = sizeof(arr)/sizeof(arr[0]);
    qsort(arr,sz,sizeof(arr[0]),cmp_int);
    for(int i = 0;i<sz;i++){
        printf("%d ",arr[i]);
    }
}

排序浮点数数组:

int cmp_float(const void* e1,const void* e2){
    // 无法对void*类型解引用 就先进行强制类型转换
    return *(float *)e1 - *(float*)e2;
}

int main(){
    float arr[] = {1.3,3.3,7.8,0.9,1.4};
    int sz = sizeof(arr)/sizeof(arr[0]);
    qsort(arr,sz,sizeof(arr[0]),cmp_float);
    for(int i = 0;i<sz;i++){
        printf("%.2f ",arr[i]);
    }

 排序结构体:

struct Stu
{
    char name[20];
    int age;
};
typedef struct Stu student;

int cmp_struct_by_age(const void* e1,const void* e2){
    return ((student*)e1)->age-((student*)e2)->age;
}

int main(){
    student stu[] = {{"zhangsan",20},{"lisi",5},{"tangchujiang",30}};
    int sz = sizeof(stu)/sizeof(stu[0]);
    qsort(stu,sz,sizeof(stu[0]),cmp_struct_by_age);
    for(int i = 0;i<sz;i++){
        printf("%s %d\n",stu[i].name,stu[i].age);
    }
}

了解了C语言自带的qsort函数后,我们将自己的冒泡排序函数完成:

#include <stdio.h>
void swap(char* buf1,char* buf2,int width);

void bubble_sort(void* base,int sz,int width,int (*cmp)(void* e1,void* e2)){
    for(int i = 0;i<sz-1;i++){
        for(int j = 0;j<sz-i-1;j++){
            if(cmp((char*)base+j*width,(char*)base+(j+1)*width)>0){
                // 这就是传入宽度的意义——模拟类型
                swap((char*)base+j*width,(char*)base+(j+1)*width,width);
            }
        }
    }
}

void swap(char* buf1,char* buf2,int width){
    for(int i = 0;i<width;i++){
        // 交换方式:一个一个字符进行交换
        char temp = *buf1;
        *buf1 = *buf2;
        *buf2 = temp;
        buf1++;
        buf2++;
    }
}

int cmp_int(const void* e1,const void* e2){
    // 无法对void*类型解引用 就先进行强制类型转换
    return *(int *)e1 - *(int*)e2;
}

int main(){
    int arr[10] = {1,3,5,7,9,2,4,6,8,10};
    int sz = sizeof(arr)/sizeof(arr[0]);
    bubble_sort(arr,sz,sizeof(arr[0]),cmp_int);
    for(int i = 0;i<sz;i++){
        printf("%d ",arr[i]);
    }
}

函数回调不是什么高大上的概念:把一段可执行的代码像参数传递那样传给其他代码,而这段代码会在某个时刻被调用执行,这就叫做回调。

如果一个函数的名字被当做参数使用,那么这个函数就可以叫做回调函数。

三、练习与巩固

后续更新!不断更新!

1、一维数组与指针

int main(){
    int a[4] = {1,2,3,4};
    // sizeof(数组名)表示整个数组
    // &(数组名)表示整个数组
    // 除此之外 所有的数组名都代表首地址!!
    printf("%d\n",sizeof(a)); // 16
    printf("%d\n",sizeof(a+0)); // 8(32位计算机为4)a+0 仍未首元素地址
    printf("%d\n",sizeof(*a)); // 4 a是首元素地址 *a找到首元素 首元素有4个字节

    printf("%d\n",sizeof(a+1)); // 8
    printf("%d\n",sizeof(a[1])); // 4

    printf("%d\n",sizeof(&a)); // 8  &a是数组的地址 不是首元素的地址 但他仍为一个地址
    printf("%d\n",sizeof(*&a)); // 16 这样才能找到数组

    printf("%d\n",sizeof(&a + 1)); // 8
    //无论是谁的地址 只要是地址 就是8位(4位)
    printf("%d\n",sizeof(&a[0])); // 8
    printf("%d\n",sizeof(&a[0] + 1)); // 8
    
}

2、二维数组与指针

int main(){
    int a[3][4] = {0};
    printf("%d\n",sizeof(a)); // 48 共有12个int类型
    printf("%d\n",sizeof(a[0])); // 16 共4个int
    // 相当于把数组名单独放在sizeof()中 计算的是第一行的大小
    printf("%d\n",sizeof(a[0][0])); // 4 第一个

    printf("%d\n",sizeof(a[0]+1));// 8
    // a[0]出现在表达式里面表示第一个元素的地址 a[0]+1表示了第一行第二个元素的地址
    printf("%d\n",sizeof(*(a[0]+1)));// 4

    printf("%d\n",sizeof(a+1));// 8
    // a是二维数组的数组名 代表了“首元素”地址 但二维数组的首元素抽象为第一个一维数组(第一行)
    // 故(a+1)代表了第二个一维数组的地址
    printf("%d\n",sizeof(*(a+1)));// 16

    printf("%d\n",sizeof(&a[0]+1));// 8
    // a[0]是第一行的数组名 &a[0]是第一行的地址 &a[0]+1是第二行的地址
    printf("%d\n",sizeof(*(&a[0]+1)));// 16

    printf("%d\n",sizeof(*a));// 16
    // a是第一行地址
    printf("%d\n",sizeof(a[3]));// 16
    
}

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值