【C语言】指针进阶(超详细)

文章目录

  • 指针的进阶
    • 指针的了解
    • 字符指针
    • 指针数组
    • 数组指针
      • 数组指针的定义
      • &数组名VS数组名
      • 数组指针的使用
    • 数组参数,指针参数
      • 一维数组传参
      • 二维数组传参
      • 一级指针传参
      • 二级指针传参
    • 函数指针
    • 函数指针数组
    • 指向函数指针数组的指针
    • 回调函数

指针的进阶

本章重点

  1. 字符指针
  2. 数组指针
  3. 指针数组
  4. 数组传参和指针传参
  5. 函数指针
  6. 函数指针数组
  7. 指向函数指针数组的指针
  8. 回调函数

指针的了解

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

字符指针

一般使用:

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

还有一种使用方法:

#include 

int main() {
    const char* pstr = "hello world!";
    printf("%s\n", pstr);
    return 0;
}

这里特别容易认为是把字符串放到字符指针里,但是本质是把首字符的地址放进去了.
【C语言】指针进阶(超详细)_第1张图片
这里就有一道这样的面试题:

#include 

int main() {
    char str1[] = "hello world!";
    char str2[] = "hello world!";
    const char* str3 = "hello world!";
    const char* str4 = "hello world!";

    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");
    }
}

注意 判断字符串的相等用的是strcmp函数,==是判断地址是否相等

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

指针数组

指针数组是一个存放指针的数组

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

数组指针

数组指针的定义

数组指针是指针?还是数组?
当然是一个指针了,我们已经熟悉
整型指针:int* pint能够指向整型数据的指针
浮点型指针:float* pf能够指向浮点型数据的指针

那个是数组指针?

int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?

p2是一个数组指针,p2先和*结合,说明p2是一个指针变量,然后指向是一个大小为10个整型的数组,所以p2是一个指针,指向一个数组,叫做数组指针

&数组名VS数组名

数组名表示数组首元素的地址.
那么&数组名到底是什么?

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

【C语言】指针进阶(超详细)_第2张图片
实际上 &arr表示的是数组的地址而不是数组首元素的地址,数组的地址加1跳过的是整个数组的大小,所以&arr+1相对于arr+1的差值是40

数组名不是首元素地址的两个例外:
1. sizeof(arr)
2. &arr,这两个例子代表都是整个数组

数组指针的使用

#include 

void print_arr1(int arr[3][5], int row, int col) {
    for(int i = 0; i < row; i ++) {
        for(int j = 0; j < col; j ++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

void print_arr2(int (*arr)[5], int row, int col) {
    for(int i = 0; i < row; i ++) {
        for(int 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};
    print_arr1(arr, 3, 5);
    print_arr2(arr, 3, 5);
    //数组名arr表示首元素的地址
    //但是二维数组的首元素是二维数组的第一行
    //所以这里可以传递的arr,其实相当于第一行的地址,是一维数组的地址
    //可以数组指针来接收
    return 0;
}

我们来看看下面代码的意思

int arr[5]; // 存放整型的数组
int *parr1[10];// 指针数组
int (*parr2)[10];// 数组指针
int (*parr3[10])[5];//存放数组指针的一个数组

数组参数,指针参数

一维数组传参

#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);
}

这个地方自己看看就知道怎么回事了

二维数组传参

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

二维数组的数组名代表的是第一行元素的地址,所以要用数组指针来接收

一级指针传参

当一个函数的参数是一级指针的时候,函数能接收什么参数?

int a = 10;
test(&a)

int pta = 10;
int* p = &pta;
test(p) 

int arr[10];
test(arr); 

二级指针传参

当函数的参数是二级指针的时候,可以接收什么样的参数?

void test(char **p) {}
int main()
{
    char c = 'b';
    char*pc = &c;
    char**ppc = &pc;
    char* arr[10];
    test(&pc);
    test(ppc);
    test(arr);
    return 0; 
}

函数指针

先看一段代码:

#include 

void test() {
    printf("hello world!");
}

int main() {
    printf("%p\n", test);
    printf("%p\n", &test);
    return 0;
}

运行结果是:
【C语言】指针进阶(超详细)_第3张图片
输出的是两个地址,这两个地址是test函数的地址,那我们函数的地址要怎么保存起来?
void (*pfun1)();

pfun1可以存放,pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回类型是void

阅读两段有趣的代码

(   *(   void (*)()   )   0   )();
这段代码的意思是把0当成一个函数的地址,转换为一个无参,void返回值的函数指针.
void (*signal(int , void(*)(int)))(int);
这是一个函数声明
第一个参数是int类型的,第二个参数是函数指针类型的,
该函数的返回类型是函数指针类型
我们可以看到这个语句难以阅读,并且最不易阅读的是void(*)(int),所以我们只需要将它重定义

【C语言】指针进阶(超详细)_第4张图片
一般的重定义都是这个语法,但是这里不能这样,必须将重定义的放进里面变成这样
【C语言】指针进阶(超详细)_第5张图片

【C语言】指针进阶(超详细)_第6张图片
最后简化为这样,这里重定义的pf_t是一个返回类型.

函数指针数组

把函数的地址放到一个数组中,那么这个数组就叫函数指针数组那么是怎么定义的呢?

int (*pf[10])()

这个函数指针数组的主要用途是来存放函数的地址,来达到简化代码的效果.

使用方法的文章

指向函数指针数组的指针

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

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)[5])(const char*) = &pfunArr;
    return 0;
}

回调函数

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

首先了解一下qsort函数的使用,

在这里插入图片描述
我们可以看到这个函数一共有四个参数,但是每个参数是什么意思呢?
【C语言】指针进阶(超详细)_第7张图片
首先看base,指针指向要排序数组的第一个元素,转换为void指针
num指的是数组中元素的个数,(是一个无符号整形类型)
size关于数组中每一个元素的字节数
compare指向比较两个元素的函数指针,这个函数是两个void*类型的指针.

接下来我们进行一个简单的使用,用来排序一个数组

#include 
#include 

int cmp(const void* e1, const void* e2) {
    return (*(int*) e1 - *(int*) e2);
}

void print(int arr[], int sz) {
    for(int i = 0; i < sz; i ++) {
        printf("%d ", arr[i]);
    }
}

int main() {
    int arr[] = { 1,3,4,3,0,8,8,3,6 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    qsort(arr, sz, sizeof(arr[0]), cmp);
    print(arr, sz);
}

接下来我们看看如何对结构体数组进行排序

#include 
#include 
#include 

struct S {
    char name[200];
    int age;
};

int cmp(const void* e1, const void* e2) {
    return strcmp(((struct S*)e1) -> name, ((struct S*)e2) -> name);
}

void print(struct S s[], int sz) {
    for(int i = 0; i < sz; i ++) {
        printf("%s %d\n", s[i].name, s[i].age);
    }
}

int main() {
    struct S s[] = {{"zhangsan", 20}, {"lisi 18"}, {"wangwu, 30"}};
    int sz = sizeof(s) / sizeof(s[0]);
    qsort(s, sz, sizeof(s[0]), cmp);
    print(s, sz);
}

最后我们使用回调函数,来模拟实现一个qsort函数.这是我们自己写代码来实现的qsort函数,可以交换任意类型的数组.

#include 
#include 
#include 

struct stu{
    char name[20];
    int age;
};

void Swap(char* buf1, char* buf2, int width) {
    for(int i = 0; i < width; i ++) {
        char tmp = *buf1;
        *buf1 = *buf2;
        *buf2 = tmp;
        buf1 ++;
        buf2 ++;
    }
}

void bubble_qsort(void* base, int sz, int width, int(*cmp)(const void* e1, const void* e2)) {
    for(int i = 0; i < sz - 1; i ++) {
        //一趟冒泡排序的过程
        for(int j = 0; j < sz - 1 - i; j ++) {
            if(cmp((char*)base+j*width, (char*)base+(j+1)*width)>0) {
                Swap((char*)base+j*width, (char*)base+(j+1)*width, width);
            }
        }
    }
}

int cmp_stu_by_name(const void* e1, const void* e2) {
    return strcmp(((struct stu*)e1) -> name, ((struct stu*)e2) -> name);
}

void print(struct stu s[], int sz) {
    for(int i = 0; i < sz; i ++) {
        printf("%s %d\n", s[i].name, s[i].age);
    }
}

void test() {
    struct stu s[] = { {"zhangsan", 29}, {"lisi", 30}, {"wangwu", 18} };
    int sz = sizeof(s) / sizeof(s[0]);
    bubble_qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
    print(s, sz);
}

int main() {
    test();
}

函数中很巧妙地地方

  1. 无论传入任何类型的数组,我们都把他强转为char类型的指针,然后我们通过通过乘以每个元素的长度来访问每个元素,这样就可以访问数组中的每个元素.
  2. 交换的时候,我们按照一个字节一个字节的交换,这样就可以实现交换.这里的cmp_stu_by_name就是一个回调函数.

总结
前面已经说过数组名的意义,这里再总结一遍

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

你可能感兴趣的:(C语言,c++,开发语言)