12 C语言进阶指针详解

指针的进阶

  1. 字符指针
  2. 数组指针
  3. 指针数组
  4. 数组传参和指针传参
  5. 函数指针
  6. 函数指针数组
  7. 指向函数指针数组的指针
  8. 回调函数
  9. 指针和数组面试题解析

声明:该文是学习C语言进阶时的笔记。学习内容:B站鹏哥C语言,p34-p51部分。文中有任何不懂的地方可以观看视频。

指针的基本概念

  1. 指针就是一个变量用来存放地址,地址是内存空间的唯一标识。
  2. 指针的大小是固定的4或8字节(32位或64位平台上)。
  3. 指针是有类型一说的,指针类型决定了对地址访问的步长,也就是指针解引用操作时候的权限。
  4. 指针的运算。

字符指针

使用形式:

int main(){
    char ch = 'w';
    char* pc = &ch;
    *pc = 's';//需要解引用才可以访问到地址,然后对地址进行修改。
    return 0;
}

字符数组用来存字符串时:

int main(){
    char arr[]="abcdef";
    char* pc = arr;//此时指针使用的是数组的首元素地址。
    return 0;
}

其他情况:(注意观察字符串和字符输出格式和指针的调用方式)。

int main(){
    const char* p = "abcdef";//此时"abcdef"是常量字符串。
    printf("%c\n",*p);
    printf("%s",p);
    return 0;
}

image-20220108103649704

解析:如果将第一个printf中的*去掉会警告 Format specifies type ‘int’ but the argument has type ‘char *’。
如果将第二个printf中p改为*p会警告 Format specifies type ‘char *’ but the argument has type ‘char’。
p表示的是地址内容。*p表示的是地址内容指向的东西。

int main() {
    char arr1[] = "abcdef";
    char arr2[] = "abcdef";
    if (arr1 == arr2) {
        printf("hehe");
    } else {
        printf("haaa");
    }
    return 0;
}

解析:答案是haaa;因为arr1、arr2存放的是首元素的地址,而第一个abcdef和第二个abcdef是存放在不同位置的字符串。

int main() {
    char* p1 = "abcdef";
    char *p2 = "abcdef";
    if (p1 == p2) {
        printf("hehe");
    } else {
        printf("haaa");
    }
    return 0;
}

解析:这次的答案是hehe;因为这次的"abcdef"是常量,在内存中仅存储了一份。所以p1和p2指向的其实是同一个地方。biao

标准的写法是

const char* p1 = "abcdef";
const char *p2 = "abcdef";

指针数组

其实是数组

int* parr[] = {arr1,arr2,arr3};

基本使用

int main() {
    int arr1[] = {1, 2, 3, 4, 5};
    int arr2[] = {2, 3, 4, 5, 6};
    int arr3[] = {3, 4, 5, 6, 7};
    int* parr[] = {arr1,arr2,arr3};
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 5; ++j) {
            printf("%d ", *(parr[i])+j);//访问初始地址后几个的int单元的地址。
        }
        printf("\n");
    }
    return 0;
}

image-20220108125032475

int* arr1[10];//整型指针数组
char* arr2[4];//字符指针数组
char** arr3[8];//二级字符指针数组

数组指针

数组指针是什么?是指针。

整形指针,是能够指向整型的指针。浮点型指针,是能够指向浮点型的指针。

那么数组指针就是要能指向数组的指针。

定义方式

int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int (*p)[10] = &arr ;//数组的地址要存起来。

首先p和*结合,说明是一个指针,然后指向的是一个大小为10 的整型的数组,所以p是一个指针指向数组。就是数组指针。

image-20220109001151570

小练习

//补全代码
int main(){
	char* arr[5];
    pa = &arr;
}
int main(){
    char* arr[5];
    char* (*pa)[5] = &arr;
    return 0;
}

image-20220108181238163

数组指针要素:1. 是一个指针 2. 指针名 3. 指针指向数组的大小 4. 指针指向元素的类型。

数组名 和 &数组名

对于int arr[10];arr和&arr分别是什么。数组名表示数组首元素的地址。&数组表示整个数组的地址。

所以就可以将&数组名存入数组指针当中去。

数组指针使用理解代码(实际不这样使用)

int mian(){
    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
    }
    //遍历方式2
    for(int i = 0;i<10;i++){
        printf("%d ",(*pa)[i]);
    }
    return 0;
}

实际使用中一般在二维数组以上。

//参数是指针
void print(int (*p)[5],int x,int y){
    int i = 0;
    for (i = 0; i < x; ++i) {
        int j = 0;
        for (j = 0; j < y; ++j) {
            printf("%d ",*(*(p+i)+j));//地址表示法
            //下面的这种也是可以的。
            // printf("%d ",(*(p+i))[j]);//数组访问的方法
            // printf("%d ",p[i][j]);
            // printf("%d ",*(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 main() {
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int i = 0;
    int *p = arr;
    for (i = 0; i < 10; ++i) {
        printf("%d", p[i]);
        printf("%d", *(p + i));
        printf("%d", *(arr + i));
        printf("%d", arr[i]);
    }
    return 0;
}
//p[i]==*(p + i)==*(arr + i)==arr[i]

小练习

解释下面的代码的意思。

int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];

解析:第一行:arr是一个有5个元素的整型数组。
第二行:parr1是一个数组,该数组有10个元素,每个元素的类型时int*,综述parr1是一个指针数组。
第三行:parr2是一个指针,指针指向一个数组,数组有10个元素,每个元素的类型是int,综述,parr2是一个数组指针。
第四行:parr3是一个数组,该数组有10个元素,每个元素是一个数组指针,该数组指针指向的数组有5个元素,每个元素是int类型。(图解)

image-20220109002740510

数组参数、指针参数

在函数设计的时候难免会把数组或指针传给函数,那么函数应该怎么接收呢?

一维数组传参

怎么接收参数呢?

int main(){
    int arr[10];
    int* arr2 = {0};
    test(arr);
    test2(arr2);
    return 0;
}

test()接收参数

void test(int arr[]){}
void test(int arr[10]){}//有没有定义大小都可以。
void test(int *arr){}//直接接受传过来的地址。

test2()接受参数

void test2(int *arr[20]){}//有没有定义大小都可以。
void test2(int *arr[]){}
void test2(int **arr){}

二维数组传参

怎么接收参数呢?

int main(){
    int arr[3][5] = {0};
    test(arr);//二维数组传参
    return 0;
}

test()接受参数

void test(int arr[3][5]){}
void test(int arr[][5]){}
//注意,二维数组在传参的时候,行可以省略,但是列绝对不能省略。

因为在二维数组中,可以不知道有多少行,但是必须知道一行多少元素。

接受参数的为指针的形式

void test(int (*arr)[5]){}
//指针参数应该指向首元素的地址,二维数组的首元素是一个数组。
//又因为必须是指针,所以采用的是数组指针,指向数组的大小为5数组元素的类型为int。

不能直接使用一级指针,也不能直接使用二级指针。也不能直接用第一个数组的第一个元素的地址。
指针参数应该指向首元素的地址,二维数组的首元素是一个数组。

一级指针传参

接收参数

void test(int* p){}
void test2(char* p){}//什么类型的指针接受什么类型的参数

怎么传参呢?倒着往回推:函数接收指针可以向函数传地址或者存放指针的变量。同时还要注意指针的类型。

int main(){
    int a = 10;
    int* p1 = &a;
    test1(&a);
    test1(p1);
    char ch = 'h';
    char* pc = &ch;
    test2(&ch);
    test2(pc);
    return 0;
}

二级指针传参

自定义函数形式,同时理解怎么接收参数。

void test(int** p){}

怎么给函数传参呢?思考什么是二级指针,二级指针的表现形式。
解析:二级指针变量,一级指针的地址,一级指针数组的数组名。

int main(){
    int *p;//一级指针
    int **pp = &*p;//一级指针的地址
    int* arr[10];//指针数组。数组中存放的是指针。此时的数组名就相当于一个二级指针。
    test(&*p);
    test(pp);
    test(arr);
    
}

函数指针

数组指针是指向数组的指针,函数指针是指向函数的指针是存放函数的地址的一个指针

int Add(int x,int y){
    int z =0;
    z = x + y;
    return z;
}
int main(){
    int a = 10;
    int b = 20;
    printf("%p\n",&Add);
    printf("%p\n",Add);
    return 0;
}

image-20220110093215356

我们发现这就像数组名 和 &数组名是一样的道理。函数名和&函数名都是函数的地址。

对于数组指针我们有

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

那么函数指针是怎么回事呢?

int Add(int x,int y){}
int (*pa)(int,int) = Add;//*先和pa结合说明是个指针。
函数指针的使用
int Add(int x,int y){
    int z =0;
    z = x + y;
    return z;
}
int main(){
    int a = 10;
    int b = 20;
	int (*pa)(int,int) = Add;
    printf("%d\n",(*pa)(2,3));//(*pa)==Add
    return 0;
}

当然不同类型的函数指针定义的方式是不同的。

使用实例2

void Print(char* str){ printf("%s\n",str);}
int main(){
    void (*p)(char*) = Print;
    (*p)("hello world");
    return 0;
}

简单分析一下:pfun1和pfun2哪个可以存放函数的地址。

void (*pfun1)();
void* pfun2()

解析:首先第一步,要存放地址,需要找什么,需要找指针,哪个是指针,pfun1是指针。

pfun1存放的是指针。pfun1先和*结合。指针指向的是一个函数,这个函数是没有参数的,这个函数的返回值类型是void(相当于没有返回值。)。

阅读有趣的代码
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int, void(*)(int)))(int);

解析:代码1,首先void (*p)()这是一个函数指针,p是这个函数指针的变量名。void (*)()这是一个指针函数的类型。在0前面的括号中放一个类型,那么就是对0进行强制类型转化,转化成一个函数指针的地址。然后再用*解引用。解引用的就是void (*)()类型的函数。(*(类型)0)()。这样理解下来就是调用0地址处的函数。

代码2, void(*)(int)是一个函数指针类型,这个函数接收的参数是int类型。signal(int, void(*)(int))这是一个函数的声明,里面有两个参数,一个是int类型,一个是 void(*)(int)。(*signal(int, void(*)(int)))(int)所以这又是一个函数指针参数是int,返回类型是void。

//代码二的另一种理解。
typedef void(*pfun_t)(int);//pfun_t将void(*)(int)这种指针类型重命名
pfun_f signal(int,pfun_f);

对代码2的综述:signal是一个函数声明;signal函数的参数有2个,第一个是int。第二个是函数指针,该函数指针指向的函数的参数是int,返回类型是void;signal函数的返回类型也是一个函数指针:该函数指针指向的函数的参数是int,返回值是void。

函数指针补充
int Add(int x,int y){
    int z =0;
    z = x + y;
    return z;
}
int main(){
    int a = 10;
    int b = 20;
	int (*pa)(int,int) = Add;
    printf("%d\n",(pa)(2,3));
    printf("%d\n",(*pa)(2,3));
    printf("%d\n",(**pa)(2,3));
    return 0;
}

三种调用形式都是同样的正确的输出结果。那么这里的*就没有起到解引用的作用。但是我们一般使用前两种,比较好理解:第一种,pa==Add,Add本身就相当于一级指针,可以直接使用。第二种*pa解引用变量名使用。这里的*其实就是个摆设而已。但是注意的是如果有*这个括号是必须加的。

函数指针数组

数组是一个存放在相同类型数据的存储空间,我们可不可以将函数指针存放在里面呢?

指针数组我们已经学过了,那么函数指针也存在数组中是不是就和指针函数一个道理了。

那么怎么实现呢?

int (*parr[10])();

parr先和[10]结合,说明parr是一个数组,int(*)()是一个函数指针。

函数指针的用途:转移表

//加法
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() {
    //函数指针的数组的定义parr[4]数组变量名
    int (*parr[4])(int, int) = {Add, Sub, Mul, Div};
    for (int i = 0; i < 4; ++i) {
        // 函数指针数组的调用
        printf("%d\n", parr[i](2, 3));
    }
    return 0;
}
小练习

char* my_strcpy(char* dest,const char* src){}

  1. 写一个函数指针pf,能够指向my_strcpy
  2. 写一个函数指针数组pfArr,能够存放4个my_strcpy函数的地址
//1. 
char* (*pf)(char* ,const char*);
//2.
char* (*pfArr[4])(char* ,const char*) = {my_strcpy};
微型计算器
// 1. 不使用指针计算器
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 res = 0;
    do {
        printf("*************************\n");
        printf(" 1:add     2:sub \n");
        printf(" 3:mul     4:div    0.exit\n");
        printf("*************************\n");
        printf("请选择:");
        scanf("%d", &input);
        switch (input) {
            case 1:
                printf("输入操作数:");
                scanf("%d %d", &x, &y);
                res = add(x, y);
                printf("res = %d\n", res);
                break;
            case 2:
                printf("输入操作数:");
                scanf("%d %d", &x, &y);
                res = sub(x, y);
                printf("res = %d\n", res);
                break;
            case 3:
                printf("输入操作数:");
                scanf("%d %d", &x, &y);
                res = mul(x, y);
                printf("res = %d\n", res);
                break;
            case 4:
                printf("输入操作数:");
                scanf("%d %d", &x, &y);
                res = div(x, y);
                printf("res = %d\n", res);
                break;
            case 0:
                printf("退出程序\n");
                break;
            default:
                printf("选择错误\n");
                break;
        }
    } while (input);

    return 0;
}
//使用函数指针数组
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 res = 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);
            res = (*p[input])(x, y);
            printf("res = %d\n", res);
        } else if(input == 0) {
            printf("退出程序");
        }
        else printf("输入有误\n");
    }
    return 0;
}

指向函数指针数组的指针

数组+指针 --> 数组指针
函数+指针 --> 函数指针
函数指针+数组 --> 函数指针数组
函数指针数组+指针 --> 指向函数指针数组的指针

指向函数指针数组的指针是一个指针。指针指向的是一个数组,数组的元素都是函数指针。

void test(char* str){ printf("%s\n",str); }
int main(){
    //函数指针
    void (*pf)(char*) = test;
    //函数指针数组pfArr
    void (*pfArr[4])(char*);
    pfArr[0] = test;//函数指针数组的使用
    //指向函数指针数组的指针ppfArr。
    void (*(*ppfArr)[4])(char*) = &pfArr;
    return 0;
}

void (*(*ppfArr)[4])(char*) = &pfArr;重新解析:(*ppfArr)是一个指针,(*ppfArr)[4]指向的是一个数组,(*(*ppfArr)[4])是一个函数指针数组,数组中每个函数的参数是(char*),返回值是void。

回调函数

回调函数就是一个通过指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个函数,当这个函数被用来调用其所指向的函数的时候,我们就是说这是回调函数。回调函数不是由该函数的实现方直接调用,而是特定的事件或条件发生时由另外一方调用的,用于对该事件或条件进行响应。

在微型计算器的第一版中,switch语句中其实比较冗余,冗余的部分就是除了调用函数的其他语句,那么我们可不可以把这些冗余的部分封装成一个函数呢?然后函数调用的部分来用指针访问。

微型计算器
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 Calc(int (*pf)(int,int)){
    int x = 0;
    int y = 0;
    printf("输入操作数:");
    scanf("%d %d", &x, &y);
    printf("res = %d\n", pf(x,y));
}
void Calc(int (*pf)(int, int)) {
    int x = 0;
    int y = 0;
    printf("输入操作数:");
    scanf("%d %d", &x, &y);
    printf("res = %d\n", pf(x, y));
}

int main() {
    int x, y;
    int input = 1;
    int res = 0;
    do {
        printf("*************************\n");
        printf(" 1:add     2:sub \n");
        printf(" 3:mul     4:div    0.exit\n");
        printf("*************************\n");
        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;
}

Calc()函数接收传过来的指针参数,然后再通过这个参数调用传过来的那个函数的功能,那么传过来(被调用功能)的函数就被叫做回调函数。

image-20220110232952125

用qsort理解回调函数
  1. 冒泡函数回顾
//这里的冒泡排序是优化后的冒泡排序
void bubble_sort(int arr[],int sz){
    for(int i = 0; i<sz-1;i++){
        //定义标记
        int isSort = 1;
        //每一次的冒泡
        for(int j = 0;j<sz-i-1;j++){
            if(arr[j]>arr[j+1]){
                int temp = arr[j];// 交换
                arr[j]=arr[j+1];
                arr[j+1] = temp;
                isSort = 0;
            }
        }
        if (isSort == 1) break;		//最后的判断
    }
}
int main(){
	int arr[10] = {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]);
    }
}

在原来我们可能觉得使用起来是够用的,但是我们仔细想一想,如果说我们传入的是一个浮点数,或者我们传入的是结构体,我们还要重新写一次函数吗?我们肯定是不愿意的。那有什么办法呢?这时我们想到了,在做微型计算器的时候我们使用了回调函数的概念。这里可不可以使用呢?

在解决上面的问题之前,我们先来看看别人是怎么做的。

  1. qsort()库函数,使用的是快速排序。
void qsort (void* base, size_t num, size_t size,
            int (*compar)(const void*,const void*));
//  void* base		目标数组的起止位置
//  size_t num		数组的大小
//  size_t size		元素的大小
//  int (*compar)(const void*,const void*)		比较函数

void*类型的指针可以接收任意类型的地址。但是void类型的指针是不能进行解引用操作的。这种类型的指针也不能进行加减整数的运算。所以需要运算的时候可以进行强制类型转化。

qsort()函数的使用

#include <stdlib.h>
#include <string.h>
typedef struct Stu {
    char name[20];
    int age;
} Stu;

int compare_int(const void *elem1, const void *elem2) {
    return *(int *) elem1 - *(int *) elem2;
}

void test1() {
    int arr[10] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
    int sz = sizeof(arr) / sizeof(arr[0]);
    //compare_int()要求这个函数的返回值是a大返回>0,a小返回<0,一样大返回0
    qsort(arr, sz, sizeof(arr[0]), compare_int);
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

//qsort要求这个函数的返回值为int
int compare_float(const void *elem1, const void *elem2) {
    return (int) (*(float *) elem1 - *(float *) elem2);
}

void test2() {
    float arr[] = {9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0};
    int sz = sizeof(arr) / sizeof(arr[0]);
    qsort(arr, sz, sizeof(arr[0]), compare_float);
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%f ", arr[i]);
    }
    printf("\n");
}
//通过年龄排序
int compare_Stu_by_age(const void *elem1, const void *elem2){
    //(Stu*)elem1强制类型转化。
    return (int )(((Stu*)elem1)->age - ((Stu*)elem2)->age);
}
//通过名字排序
int compare_Stu_by_name(const void *elem1, const void *elem2){
    // 比较字符串需要使用strcmp
    return strcmp(((Stu*)elem1)->name, ((Stu*)elem2)->name);
}
void test3() {
    Stu arr[3] = {{"zhangsan", 20},
                {"lisi",     30},
                {"wangwu",   18}};
    int sz = sizeof(arr) / sizeof(arr[0]);
    // 通过年龄排序
    qsort(arr, sz, sizeof(arr[0]), compare_Stu_by_age);
    // 打印
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%s ", arr[i].name);
        printf("%d ", arr[i].age);
    }
    printf("\n");
    // 通过名字排序
    qsort(arr, sz, sizeof(arr[0]), compare_Stu_by_name);
    // 打印
    i = 0;
    for (i = 0; i < sz; i++) {
        printf("%s ", arr[i].name);
        printf("%d ", arr[i].age);
    }
    printf("\n");
}

int main() {
    test1();//比较int
    test2();//比较float
    test3();//比较结构体
    return 0;
}

qsort(参数1,参数2,参数3,参数4);
第一个参数:待排序数组的首元素地址
第二个参数:待排序数组的元素大小
第三个参数:待排序数组的每个元素的大小-单位是字节
第四个参数:是函数指针,比较两个元素的所用函数的地址-这个函数使用者自己实现。
函数指针的两个参数是:待比较的两个元素的地址。函数返回值为int类型。

我们发现库函数中的qsort函数可以实现除了整型之外的几乎所有类型的排序,我们的冒牌排序是不是也可以设计成这样呢?

首先我们来考虑函数的参数。1.需要知道首元素的地址(从哪里开始的),2.整个数组的大小(来判断需要几轮,每轮运算多少。)3.运算的是什么类型的,但是类型是不能传参的,所以我们可以传这个类型的大小(宽度)。4.两个值进行比较的函数地址。

主要功能实现

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++;
    }
}
//这里的冒泡排序是优化后的冒泡排序
// (*compare)(void* elem1,void* elem2)函数指针。用来比较两个数的参数。
void bubble_sort(void *base, int sz, int width, int (*compare)(const void *elem1, const void *elem2)) {
    //大 轮
    for (int i = 0; i < sz - 1; i++) {
        //定义标记
        int isSort = 1;
        //每一次的冒泡
        for (int j = 0; j < sz - i - 1; j++) {
            // 两个元素的比较
            //如果是结构体我们没有通用的方法。那么怎么办????
            //我们可以学qsort让用户自己来写比较的方法。
            if (compare((char *) base + j * width, (char *) base + (j + 1) * width) > 0) {
                // 交换
                Swap((char *) base + j * width, (char *) base + (j + 1) * width, width);
                isSort = 0;
            }
        }
        if (isSort == 1) break;        //最后的判断
    }
}

解析:整体使用的是冒泡排序的思想。难点:本人认为共有两个难点:1.在冒泡排序进行两个元素判断的时候,因为传进来的是void类型的指针是不能直接使用的。但是我们有被比较变量的宽度,通过char指针控制步长可以精确匹配到首地址然后进行量元素的比较。(在这里本来有个疑问,访问到变量的首地址,此时还是char怎么可以得到变量的全部部分呢?答案是这里是个回调函数,回调到用户编写的函数后又进行了一次强制类型转化,这时我们就访问到了变量的全部。)比较完之后就要进行两值的交换。2.两值的交换就是第二个问题了:步长就相当于两个变量的间隔,只要通过这个间隔从左开始,向右逐个交换。就可以将两个变量所在的地址中的内容全部交换。(下面是我的图解)。

image-20220111182358747

完整代码展示

#include <string.h>

void bubble_sort(void *base, int sz, int width, int (*compare)(const void *elem1, const void *elem2));

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

int compare_int(const void *elem1, const void *elem2) {
    return *(int *) elem1 - *(int *) elem2;
}

void test1() {
    int arr[10] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
    int sz = sizeof(arr) / sizeof(arr[0]);
    //compare_int()要求这个函数的返回值是a大返回>0,a小返回<0,一样大返回0
    bubble_sort(arr, sz, sizeof(arr[0]), compare_int);
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

//qsort要求这个函数的返回值为int
int compare_float(const void *elem1, const void *elem2) {
    return (int) (*(float *) elem1 - *(float *) elem2);
}

void test2() {
    float arr[] = {9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0};
    int sz = sizeof(arr) / sizeof(arr[0]);
    bubble_sort(arr, sz, sizeof(arr[0]), compare_float);
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%f ", arr[i]);
    }
    printf("\n");
}

//通过年龄排序
int compare_Stu_by_age(const void *elem1, const void *elem2) {
    //(Stu*)elem1强制类型转化。
    return (int) (((Stu *) elem1)->age - ((Stu *) elem2)->age);
}

//通过名字排序
int compare_Stu_by_name(const void *elem1, const void *elem2) {
    // 比较字符串需要使用strcmp
    return strcmp(((Stu *) elem1)->name, ((Stu *) elem2)->name);
}

void test3() {
    Stu arr[3] = {{"zhangsan", 20},
                  {"lisi",     30},
                  {"wangwu",   18}};
    int sz = sizeof(arr) / sizeof(arr[0]);
    // 通过年龄排序
    bubble_sort(arr, sz, sizeof(arr[0]), compare_Stu_by_age);
    // 打印
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%s ", arr[i].name);
        printf("%d ", arr[i].age);
    }
    printf("\n");
    // 通过名字排序
    bubble_sort(arr, sz, sizeof(arr[0]), compare_Stu_by_name);
    // 打印
    i = 0;
    for (i = 0; i < sz; i++) {
        printf("%s ", arr[i].name);
        printf("%d ", arr[i].age);
    }
    printf("\n");

}


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++;
    }
}

//这里的冒泡排序是优化后的冒泡排序
// (*compare)(void* elem1,void* elem2)函数指针。用来比较两个数的参数。
void bubble_sort(void *base, int sz, int width, int (*compare)(const void *elem1, const void *elem2)) {
    //大 轮
    for (int i = 0; i < sz - 1; i++) {
        //定义标记
        int isSort = 1;
        //每一次的冒泡
        for (int j = 0; j < sz - i - 1; j++) {
            // 两个元素的比较
            //如果是结构体我们没有通用的方法。那么怎么办????
            //我们可以学qsort让用户自己来写比较的方法。
            if (compare((char *) base + j * width, (char *) base + (j + 1) * width) > 0) {
                // 交换
                Swap((char *) base + j * width, (char *) base + (j + 1) * width, width);
                isSort = 0;
            }
        }
        if (isSort == 1) break;        //最后的判断
    }
}

int main() {
    test1();//比较int
    test2();//比较float
    test3();//比较结构体
    return 0;
}

指针和数组面试题解析

  1. 一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));
//sizeof(数组名),此时的数组名代表的是整个数组。计算的是数组总大小  应该输出4*4 = 16字节(之后省略)
printf("%d\n",sizeof(a+0));
//a+0代表的是首元素地址+0  计算的是首元素地址的大小。32位平台输出4 64位平台输出8
printf("%d\n",sizeof(*a));
//首先解引用,a代表的是首元素地址,*a就是首元素。 计算的是首元素的大小  int型大小为4。
printf("%d\n",sizeof(a+1));
//同sizeof(a+0)   32位平台输出4 64位平台输出8
printf("%d\n",sizeof(a[1]));
//a[1]代表的是第二个元素,计算的是第二个元素的大小。  int型为4。
printf("%d\n",sizeof(&a));
//&a是取出数组的地址。地址的大小还是32位平台输出4 64位平台输出8。
printf("%d\n",sizeof(*&a));
//*&a对数组地址进行解引用,就是整个数组。计算的就是整个数组的大小。应该输出4*4 = 16
//理解二:*&a中*&两个操作符作用相互抵消,最后就相当于sizeof(a)。
printf("%d\n",sizeof(&a+1));
//&a数组的地址(&a+1)是整个数组地址的下一个地址,只要是地址,大小就是32位平台4, 64位平台8
printf("%d\n",sizeof(&a[0]));
//&a[0]取第一个元素的地址。只要是地址,大小就是32位平台4, 64位平台8
printf("%d\n",sizeof(&a[0]+1));
//&a[0]第一个元素的地址,+1就是第二个元素的地址。只要是地址,大小就是32位平台4, 64位平台8
  1. 字符数组

(1)直接在数组中初始化存放

char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));
//sizeof(数组名),此时的数组名代表的是整个数组。计算的是数组总大小  应该输出1*6 = 6
printf("%d\n", sizeof(arr+0));
//arr+0代表的是首元素地址+0  计算的是首元素地址的大小。32位平台输出4 64位平台输出8
printf("%d\n", sizeof(*arr));
//首先解引用,arr代表的是首元素地址,*arr就是首元素。 计算的是首元素的大小  char型大小为1
printf("%d\n", sizeof(arr[1]));
//第二个元素的大小。char型大小1.
printf("%d\n", sizeof(&arr));
//&arr数组存放的地址,地址,32位平台4,64位平台8。
printf("%d\n", sizeof(&arr+1));
//存放整个arr数组地址之后的下一个地址。地址,32位平台4,64位平台8。
printf("%d\n", sizeof(&arr[0]+1));
//第二个元素的地址。地址,32位平台4,64位平台8。


printf("%d\n", strlen(arr));
//遇到'\0'结束计算。但是使用数组初始化存放的时候,应该是随机值。
printf("%d\n", strlen(arr+0));
//strlen(arr)和strlen(arr+0)其实是完全一样的。从首元素地址开始往后找'\0'。
printf("%d\n", strlen(*arr));
//strlen()参数应该是地址。*arr是首元素,是元素。元素a转化为ASCII码97,然后去访问97这个地址。
//程序可能直接崩溃。
printf("%d\n", strlen(arr[1]));
//同上
printf("%d\n", strlen(&arr));
//整个元素的地址,就是首元素的地址,向后寻找'\0',是个随机值。
printf("%d\n", strlen(&arr+1));
//从整个数组结束后的下一个地址开始找'\0'。随机值
printf("%d\n", strlen(&arr[0]+1));
//从第二个元素开始向后寻找'\0'。随机值。
//但是上面的随机值之间是有联系的。 
//strlen(arr)=strlen(arr+0)=strlen(&arr)= strlen(&arr+1)+6 = strlen(&arr[0]+1)+1

(2)使用字符串进行初始化

char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
//实际存储的时候在最后还有'\0'所以结果:7*1 = 7
printf("%d\n", sizeof(arr+0));
//首元素地址+0还是首元素地址,地址大小4/8。
printf("%d\n", sizeof(*arr));
//首元素地址解引用是首元素,大小为1
printf("%d\n", sizeof(arr[1]));
//第二个元素大小为1
printf("%d\n", sizeof(&arr));
//虽然是数组的地址但是也是地址。4/8
printf("%d\n", sizeof(&arr+1));
//一看是地址就是4/8
printf("%d\n", sizeof(&arr[0]+1));
//仍然是地址


printf("%d\n", strlen(arr));
//6
printf("%d\n", strlen(arr+0));
//6
printf("%d\n", strlen(*arr));
//首元素访问失败,访问第一个值的ASCII码对应的地址。非法访问内存,程序可能直接错误。
printf("%d\n", strlen(arr[1]));
//同上理
printf("%d\n", strlen(&arr));
//数组的地址,仍旧是6。这里会有一个警告,数组的地址 char(*p)[7] = &arr,&arr的类型是char(*)[7]
//但是因为传过来的是一个地址所以程序可以运行。
printf("%d\n", strlen(&arr+1));
//这个数组结束之后的下一个数组的开始地址。会有警告原因同上。结果是随机值。
printf("%d\n", strlen(&arr[0]+1));
//该数组的第二个元素到'\0'的长度。6-1 = 5
  1. 常量字符串地址传递给指针。
char *p = "abcdef";
printf("%d\n", sizeof(p));
//指针的大小,地址的大小 4/8
printf("%d\n", sizeof(p+1));
//得到的是字符b的地址,仍旧是地址 4/8
printf("%d\n", sizeof(*p));
//解引用地址,访问到的是字符char的大小为1
printf("%d\n", sizeof(p[0]));
//int arr[10],arr[0]==*(arr+0),p[0]==*(p+0)。
//p[0]访问到的就是a,大小为1
printf("%d\n", sizeof(&p));
//地址4/8
printf("%d\n", sizeof(&p+1));
//地址4/8
printf("%d\n", sizeof(&p[0]+1));
//取到a的地址然后加一 得到的其实就是b的地址。地址4/8


printf("%d\n", strlen(p));
//传进来的是地址所以可以运算,从第一个元素到'\0'的长度6
printf("%d\n", strlen(p+1));
//p是a的地址,p+1是b的地址。到'\0'的长度5
printf("%d\n", strlen(*p));
//*p访问第一个值的ASCII码对应的地址。非法访问内存,程序可能直接错误。
printf("%d\n", strlen(p[0]));
//同上
printf("%d\n", strlen(&p));
//p是一个指针,内容是地址,&p找到的是这个内容的地址。不是这个内容指向的地址。
//但是因为求的是长度,得看在内容中有没有一组结果刚好是0,可能会结束。总体而言还是随机值。
//这里注意的是它会读取内存内容。
printf("%d\n", strlen(&p+1));
//p的地址的下一个地址,原理同上。随机值
printf("%d\n", strlen(&p[0]+1));
//先p[0]就是a,+1就是b长度为6-1=5
  1. 二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));
//整个数组的大小3*4*4=48
printf("%d\n",sizeof(a[0][0]));
//第一行第一个元素大小int 4
printf("%d\n",sizeof(a[0]));
//第一行的数组的大小4*4=16
//a[0]相当于第一行作为一维数组的数组名,sizeof(arr[0])把数组名单独放在sizeof()内,计算的是第一行的大小
printf("%d\n",sizeof(a[0]+1));
//a[0]相当于第一行的数组名,是第一行首元素的地址,a[0]+1是第一行第二个元素的地址。地址:4/8
printf("%d\n",sizeof(*(a[0]+1)));
//根据上个解析可知,a[0]+1是第一行第二个元素的地址,解引用就是这个位置的元素,大小int为 4
printf("%d\n",sizeof(a+1));
//a是二维数组的数组名,没有sizeof(a),也没有&(a),所以a是首元素地址。而把二维数组看成一维数组时,二维数组的首元素是他的第一行,a就是第一行(首元素)的地址。a+1就是第二行的地址。大小为4/8
printf("%d\n",sizeof(*(a+1)));
//sizeof(a[1])计算第二行的大小,单位是字节  4*4 = 16
printf("%d\n",sizeof(&a[0]+1));
//第二行的地址,因为&a[0]取到的是第一行的地址。地址大小4
printf("%d\n",sizeof(*(&a[0]+1)));
//计算第二行的大小。第二行的地址解引用就是第二行。
printf("%d\n",sizeof(*a));
//a是首元素地址-第一行地址 *a就是第一行,这里就是计算第一行的大小。16
printf("%d\n",sizeof(a[3]));
//由于sizeof内部的是不参与计算的,只是看他的形式,就假设有第四行,大小为16

小结

1.sizeof(数组名),这里的数组名代表的是整个数组,计算的是数组的大小。
2.&数组名,这个数组名代表的是整个数组,取出的是整个数组的地址
3.除此之外所有的数组名都是首元素的地址。

指针笔试题

笔试题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;
}

解析:答案是2,5。&a + 1是跳过整个数组的下一个地址。数组指针加一还是数组指针类型,不能存放进整型指针中,所以进行了强制类型转化。在输出的时候,输出的第一个值是数组首元素地址加一也就是第二个元素的地址,然后解引用得到的就是第二个元素,所以第一个输出的就是2;ptr是跳过整个数组的下一个地址,且ptr是int类型的指针,那么指针减一就是他的上一个地址,所以指向的应该是数组a的最后一个元素,所以第二个输出的是5

笔试题2

struct Test
{
 int Num;
 char *pcName;
 short sDate;
 char cha[2];
 short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
int main()
{
 printf("%p\n", p + 0x1);
 printf("%p\n", (unsigned long)p + 0x1);
 printf("%p\n", (unsigned int*)p + 0x1);
 return 0; 
}

解析:0x100014 0x100001 0x100004。首先p是什么,p是结构体指针;0x1就是十进制的1。第一个打印中要跳过这个结构体的下一个地址是什么?0x100000(16)+20(10)=0x100014(16)。第二个打印,先将p转换成无符号长整型然后加一。按照地址打印输出:0x100000(16)+1=0x100001(16)。第三个打印,先将p转换成无符号的int型指针,加一就是跳过一个指针的大小就是0x100000(16)+4(10)=0x100004(16)

笔试题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;
}

解析:4,2000000。首先解释ptr1[-1],同笔试题1中的内容ptr1是跳过数组后的第一个地址,ptr1[-1]就是这个地址的上一个int。那就是a[3]。对于第二个我们直接图解(黄色是第一步,蓝色第二步,红色第三步)。因为我们的电脑是小端存储模式,所以存储数组具体元素的时候,按照如下的存储。(int *)((int)a + 1)。首先是将a的地址强制转化为int,然后对其加1,再转化为int型的指针,最后就是就是表示红色框框柱的部分。读取这部分,根据小端存储模式:0x02000000。输出时去掉高位的0。

image-20220112165219090

笔试题4:

int main()
{
    int a[3][2] = { (0, 1), (2, 3), (4, 5) };
    int *p;
    p = a[0];
    printf( "%d", p[0]);
	return 0;
}

解析:答案是1。(0, 1), (2, 3), (4, 5)先要想到这是逗号表达式,所以int a[3][2] = { (0, 1), (2, 3), (4, 5) };这句话相当于是int a[3][2]={1,3,5}。在内存中存储的时候,第一行第一个是1,第一行第二个是3,第二行第一个是5,其余的全都是0。a[0]是第一行的数组,输出的时候没有sizeof,没有&所以代表的是首元素地址。p[0]相当于*(p+0)就是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;
}

解析:答案是FFFFFFFFFFFFFFFC,-4。int(*p)[4],是指针数组。将a[5][5]强制赋值到p中,结果如图下所示(数组分布)。图中量蓝点的地址进行作差,因为体重是低地址减去高地址,答案是-4。但是用%p的形式输出的时候应该是输出-4对应补码,所对应的16进制数FFFFFFFFFFFFFFFC(32位是FFFFFFFC) , -4。

image-20220112174851821

笔试题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;
}

解析:答案是10,5。&arr是整个数组,经过计算后的ptr是整个数组后面的第一个地址。在输出的时候int指针-1然后取地址,得到的应该是aa数组的最后一个元素,10。aa + 1是第二行的地址,然后解引用得到的就是第二行,再强制类型转化为指针。输出的时候,ptr2 - 1,地址减一,减去的是int*,此时取到的就是第二行的前一行的最后一个的地址,再解引用,得到的就是该位置的元素为5。

补充:

int arr[] = {1,2,3,4};
int* p = arr;

在这种情况下,*(p+2) <=>p[2]<=>*(arr+2)<=>arr[2]

笔试题7

int main()
{
	char* a[] = {"work","at","alibaba"};
	char**pa = a;
	pa++;
	printf("%s\n", *pa);
	return 0;
}

解析:答案是at。a是指针数组。a代表的是首元素的地址,就是第一个字符串"work"的地址。因为a本身就是一个指针,然后将这个指针放在二级指针pa中存储。pa++就相当于与a+1;就是第二个字符串的地址,解引用后就是第二个字符串也就是"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;
}

解析:答案是:POINT,ER,ST,。

注意:1.自增之后值就已经改变了。2.取的是字符串的地址还是字符的地址。虽然是一个地址,但是运算不同。

首先char *c[] = {“ENTER”,“NEW”,“POINT”,“FIRST”};是一个指针数组(数组中存放的指针。)。char**cp[] = {c+3,c+2,c+1,c};中c是首元素的地址。这样的话,cp[]就相当于是将数组c的逆序。cp是首元素地址存放到cpp二级指针变量中。

image-20220112201941100

第一个printf中先++cpp,这个指针要加一,那就是cp中的第二个元素的地址,第一次解引用得到是c中第三个元素的地址。第二次解引用就该字符串的第一个字符的地址,输出"%s"得到"POINT"。

image-20220112202453225

第二个printf中根据优先级,先++再解引用再–再解引用再加三。但是,这里在第一次printf的时候我们经cpp指到了cp中的第二个元素的地址。所以1.先++指向的是cp中的第三个元素的地址。再解引用得到的是c中的第二个元素的地址(不懂把第一个printf再看一遍),再–得到的是c中第一个元素的地址,再解引用就是c中第一个字符串的第一个字符的地址。最后加3得到的就是从从第四个字符开始的字符串"ER"。

image-20220112202949537

第三个printf中*cpp[-2]+3等价于*(*(cpp-2))+3,然后+3。首先计算之前强调cpp已经指到了cp的第三个元素,(cpp-2)指到了cp的第一个元素,解引用得到的是c的第四个元素字符串,再解引用,取到的是c中第四个字符串中的第一个字符的地址。输出"%s"得到"ST"。

image-20220112203841919

第四个printf中cpp[-1][-1]+1等价于*(*(cpp-1)-1)+1。因为我们知道cp在第二次printf的时候更新了位置(更新到了cp的第三个元素的位置),但是在第三个printf中并没有作出修改,所以(cpp-1)代表的是cp中第二个元素的位置,解引用访问到的是c中的第三个元素-字符串的地址。在这个基础上先-1取到的就是c中第二个字符串的地址,然后解引用操作,就是c中第二个字符串的第一个字符的地址,再+1,就是c中第二个字符串的第二个字符。输出"%s"得到"EW"。

image-20220112204246660

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黎丶辰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值