C语言——进阶指针理解

目录

1.字符指针

2.指针数组

3.数组指针

4.数组和指针的传参(一级指针传参和二级指针传参)

5.函数指针

6.函数指针数组

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

8.回调函数  —  函数调用约定(qsort的使用和指针中void*作用)

今天我们一起探讨一下进阶指针的一些知识,在上一个主题中,我们已经接触了初阶指针,并且简单了解了指针的概念:我们一起简单回顾一下:

1.指针就是变量,用来存放地址,地址唯一表示一块内存空间

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

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

4.指针的运算

本章的主题,我们来探讨一下指针的高级用法

1.字符指针

 字符指针,这个概念在C中很常见也是很重要的,我们知道指针本身是存储变量地址的变量,而字符指针应该就是指向字符类型的指针,整型指针用int* p表示,那么字符指针就应该用char* p来表示。

字符指针具体有什么用途呢?字符指针最常见的就是用来处理字符串,在C语言里,字符串通常是以字符数组的形式存在,而数组名本身就是一个指针,指向数组首元素地址,所以,字符指针就是用来指向字符串的首地址,这样就可以通过字符指针来访问和操作字符串了。·

下面演示一下字符指针的表示方法:

#include<stdio.h>
int main(){
    char ch = 'a';
    char* ph = &ch;
    *ph = 'e';
    printf("%d", ph)
    return 0;
}

 字符指针明白了再来看一下如何通过字符指针来访问字符串的首字符地址:

#include<stdio.h>
int main(){
    const char* pc = "abcdef";
    printf("%s", pc);
    return 0;
}

我们这里写了char* pc = "abcdef"表达式,那么我们是不是可以认为pc变量中放的就是abcdef字符串呢?这个想法是错误的,我们知道char*是指针,指针在内存中占4个字节,而一个字符占一个字节加上字符串后面的\0这里一共是占7个字节,但是指针只能存放4个字节,所以会导致放不下,出现溢出情况.
其实这里的指针存放的字符串的首字符地址,所以这里是把字符串首字符a的地址,赋给了pc,但是这种把字符串首字符地址赋给pc的写法是不安全的,因为abcdef是常量字符串,字符串本身是不能被修改的所以这里我们加上const修饰。

接下来我们分析一段代码:

#include<stdio.h>
int main() {
    const char* pc = "abcdef";
    const char* pa = "abcdef";
    char arr1[] = "abcdef";
    char arr2[] = "abedef";
    if (pc == pa) {
        printf("pa == pa\n");
    }
    else {
        printf("pa != pc\n");
    }
    if (arr1 == arr2) {
        printf("arr1 == arr2");
    }
    else {
        printf("arr1 != arr2");
    }
    return 0;
}

首先,对于第一个比较,pc和pa都是指向字符串常量的指针。在C语言中,编译器通常会把相同的字符串常量存放在同一个内存位置,以节省空间。所以当pc和pa都被赋值为相同的字符串"abcdef"时,它们可能指向同一个地址,这时候pc == pa就会成立,输出"pa == pa"。不过也可能会有例外,这取决于编译器的实现,如果编译器没有合并字符串常量,结果可能不同,但大部分现代编译器会优化。这里我们就采用大部分编译器的实现来讨论结果。

然后是arr1和arr2的比较。这里声明的是两个字符数组,初始化为不同的字符串。每个数组在内存中都是独立的数组,它们的地址应该是不同的。即便它们的字符串内容相同,但由于数组作为局部变量,会在栈上分配内存,各自的地址不同。所以在比较arr1 == arr2时,实际比较的是两个数组的起始地址是否相等,这显然是不成立的,所以应该输出"arr1 != arr2"。由此可见字符指针和字符数组还有一些区别的。

2.指针数组

上一个章节我们简单了解了一下指针和数组的关系,我们回顾一下:我们说数组名在很多情况下会被转换成指针,尤其是当数组名作为函数参数的时候,这时候我们传递的其实只是数组首元素的地址,我们知道数组在内存中是连续存放的,我们找到首元素地址,就可以找到整个元素。不过,他们虽然可以互换使用,但是他们的本质其实是不一样的,数组是一块连续的内存空间,用来存储相同类型的数据;而指针只是一个变量,存储的是另一个变量的地址。

那么指针数组又是怎样的呢?比如,普通的整型数组是int arr[5],每个元素都是整数。那指针数组应该是每个元素都是指针的类型。比如int* parr[5],这样每个元素都是指向int类型的指针。这样的话,这个数组里的每个元素都可以存放一个地址

那指针数组有什么实际的应用场景呢?比如说,可能需要处理多个字符串的时候,可以用指针数组来存放这些字符串的首地址。比如char* parr = {"hello", "world", "example"},这样的话,每个元素都是指向字符的指针,也就是字符串,当然,字符串也可以放数组中存起来,整型的数据处理方法也是相同的。这样处理起来可能比较方便,因为每个字符串的长度不同,用指针数组可以灵活地管理这些字符串的地址,而不需要固定每个字符串的长度。

#include<stdio.h>
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[3] = { arr1, arr2, arr3 };
    int i, j = 0;
    for (i = 0; i < 3; i++) {
        for (j = 0; j < 5; j++) {
            printf("%d ", *(parr[i] + j));
            //printf("%d ", parr[i][j]);
        }
        printf("\n");
    }
    return 0;
}

不过,这样的话,指针数组和二维数组有什么不同呢?比如,二维数组char strArr[3][10]可以存三个字符串,每个最多9个字符(加上结尾的'\0')。而指针数组中的每个元素指向的字符串长度可以不同,这样可能更节省空间,特别是当字符串长度差异较大的时候。比如有的字符串很长,有的很短,用二维数组的话,每一行的长度都要按照最长的来定义,可能会浪费空间。而指针数组只需要每个指针指向不同长度的字符串,这样更灵活。

3.数组指针

我们上面说指针数组就是指向某个类型的指针的数组,其中每个元素都是某个类型(本质上指针数组就是一个数组)。理解了之后数组指针顾名思义数组指针就是指向数组的指针,我们想要彻底理解我们再来回顾一下数组名的用法

#include<stdio.h>
int main() {
    int arr[10] = { 0 };
    printf("%p\n", arr);
    printf("%p\n", &arr[0]);
    return 0;
}

当我们运行起来发现两次打印结果是相同的,那我们可以认为数组名就是数组的首元素地址吗?
其实这种说法没有错误,当我们打印sizeof(arr)的时候得到的结果却不是首元素的四个字节

总结:数组名通常表示的都是首元素的地址
但是有两个例外:1.sizeof(单独的数组名),这里的数组名表示的是整个数组,计算的是整个数组的大小,单位是字节
2. &数组名,这里的数组名表示的依然是整个数组,所以&数组名取出的是整个数组的地址

那么它们有什么区别吗?

我们把它们结果都+1运行起来发现arr和arr + 1相差的是四个字节;&arr[0]和&arr[0] + 1也是相差四个字节
为什么相差4个字节呢?因为数组这里数组每个元素都是int类型的,arr是数组名它的的类型是int*
int*是4个字节所以加一就是移动了4个字节
但是&arr和&arr + 1却没有相差四个字节,而是相差了40个字节,由此可见,&arr跳过的是整个数组

我们知道:整型指针是用来存放整型的地址,字符指针是用来存放字符的地址,那么数组指针应该放的就是数组的地址,它的表达方式为:

#include<stdio.h>
int main() {
    int arr[10] = { 0 };
    int* p = arr;
    int(*p2)[10] = &arr;
    //p2是指针发现有十个元素,每个元素都是int类型,p2是数组指针类型
    //这就是存放整个数组的指针,称为:数组指针
    return 0;
}

p2是数组指针方括号里是10个元素,每个元素都是int类型,p2是数组指针类型。我们把存放整个数组的指针称为数组指针。

我们如何使用数组指针呢?我们正常打印二维数组是这样的:

#include<stdio.h>

void print(int arr[3][5], int row, int col){
    int i, j = 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,2,3,4,5,6,3,4,5,6,7};
    print(arr, 3, 5);
    return 0;
}

我们知道在数组传参的过程中,传的其实是数组名也就是数组的首元素地址,而二维数组首元素的地址其实是二维数组的第一行
那么既然传的是地址,那么我们应该可以用指针来接收,这里传的元素应该是第一行五个元素都是int类型的数组也就是传的参数为一维数组,我们接收就可以使用数组指针来接收这个一维数组

#include<stdio.h>

void print(int (*p)[5], int row, int col){
    int i, j = 0;
    for(i = 0; i < row; i++){
        for(j = 0; j < col; j++){
            printf("%d ", *((*p + i) + j));
            //printf("%d ", p[i][j]);
        }
    }
}

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 (*p)[5]表示的就是去掉指针变量名就是指针类型也就是类型为int (*)[5]的数组指针类型,其中有五个元素,每个元素为int类型。

4.数组和指针的传参(一级指针和二级指针传参)

一级指针传参:用途:传递一级指针(如 int* p )用于访问或修改指针指向的数据,但不改变指针本身的内存地址。

#include<stdio.h>

void print(int *p, int a){
//void print(int **pa, int a)
    int i = 0;
    for(i = 0; i < a; i++){
        printf("%d ", arr[i]);
    }
}

int main(){
    int arr = {1,2,3,4,5,6};
    int* p = &arr;
    int sz = sizeof(arr) / sizeof(arr[0]);
    print(p, sz);
    return 0;
}

总结:如果函数的形式是一个一级指针,调用的时候我们可以传(数组名,整型变量的地址,一级指针)

二级指针传参:用途:修改指针本身的指向
使用场景:
重定向指针:让外部指针指向另一个变量或数组。
操作指针数组:指针数组的每个元素本身是指针,需二级指针修改。

#include<stdio.h>

void test(int** pa) {
    printf("%d ", **pa);
}

int main(){
    int n = 5;
    int* p = &n;
    int** pp = &p;
    test(pp);
    return 0;
}

总结:如果函数的形式是一个一级指针,调用的时候我们可以传(数组名,整型变量的地址,一级指针)

5.函数指针

上面已经提到了数组指针,什么是数组指针?指向数组的指针就是数组指针,那函数指针就是指向函数的指针。

#include<stdio.h>

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

int main() {
	int arr[5] = { 0 };
	//&数组名 - 取出的数组的地址
	int (*p)[5] = &arr;//数组指针

	//&函数名 - 取出的是函数的地址吗?
	printf("%p\n", &Add);
	return 0;
}

 运行起来之后发现系统确实给了一串十六进制的地址,由此可见,函数也是有地址的&函数名拿到的就是函数的地址,那么我们不加取地址符号会不会和数组名相同呢?

include<stdio.h>

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

int main() {
	int arr[5] = { 0 };
	//&数组名 - 取出的数组的地址
	int(*p)[5] = &arr;//数组指针

	//&函数名 - 取出的是函数的地址吗?
	printf("%p\n", &Add);
	printf("%p\n", Add);
	return 0;
}

我们运行起来发现结果是相同的,系统给了两个相同的十六进制的数,但是不能认为函数名和数组名是相同的,数组名是数组首元素的地址,而函数没有首元素这个说法,对于函数来说:&函数名和函数名都是函数的地址

那如何声明一个函数指针呢?数组指针的声明是int (*)[];这样,但是函数指针可能需要包括返回类型和参数类型。比如,假设有一个函数int p(int a, int b),那么对应的函数指针应该怎么声明呢?应该是这样的:int (*p)(int, int)。这里括号很重要,不然的话可能变成返回int指针的函数声明了。如果写成int p(int, int),那其实是声明了一个函数,返回类型是int,而参数是int和int。所以括号是必须的,用来区分函数指针和普通函数声明。

include<stdio.h>

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

int main() {
	int arr[5] = { 0 };
	//&数组名 - 取出的数组的地址
	int(*p)[5] = &arr;//数组指针
	int (*p)(int, int) = &Add;
	return 0;
}

因为存的是一个地址,所以我们用指针来接收地址(*p)发现函数的形参为两个int类型那么就是(int, int)

那么函数指针如何使用呢?和其它类型指针用法相同,比如:

#include<stdio.h>
int main() {
	int a = 5;
	int* p = &a;
	*p = 10;
	printf("%d ", *p);
	return 0;
}

我们通过这种方式来改变了a的值,函数也是类似的:

#include<stdio.h>

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

int main() {
	int arr[5] = { 0 };
	//&数组名 - 取出的数组的地址
	int(*p)[5] = &arr;//数组指针
	int (*pa)(int, int) = &Add;
	int ret = (*pa)(2, 3);
	printf("%d ", ret);
	return 0;
}

我们通过和数组指针类似的写法,用函数指针的写法来访问函数内部的操作。

6.函数指针数组

函数指针数组是一种存储多个函数指针的数组,每个指针指向具有相同返回类型和参数列表的函数。我们上面说的函数指针和指针数组的结合就是函数指针数组

指针数组:int* arr[10] - 这是指针数组

函数指针:int (*Add)(int, int) - 这是函数指针,函数指针也是指针

函数指针数组:int (*arr[4])(int, int)把函数指针放在数组中其实就是函数指针数组

如何使用函数指针数组呢,我们先来实现一个简单的计算器:

#include<stdio.h>

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 (*arr[4])(int, int) = { Add, Sub, Mul, Div };
	int i = 0;
	for (i = 0; i < 4; i++) {
		int ret = arr[i](8, 4);
		printf("%d \n", ret);
	}
	return 0;
}

上面的代码使用函数指针数组实现了一个简单计算器功能,我们一起来分析一下:

首先在主函数中我们用了一个int (*)(int, int)函数指针类型,将我们四个功能函数放到一个数组里面这个时候我们就可以通过数组下标来访问这些函数的地址,从而间接使用这些函数的功能,再通过一个循环给函数中的x,y赋值,就可以实现一个简单计算器的功能

那函数指针数组和普通数组有什么不同呢?函数指针数组的元素都是函数指针,而普通数组的元素是数据。所以在操作上,比如初始化、赋值、调用等都需要按照函数指针的规则来处理。
还有,函数指针数组的初始化必须在定义时给出函数名,或者在后续赋值中给每个元素赋值。
最后,要注意函数指针的类型必须和数组元素类型匹配。比如,如果数组中有一个函数的参数类型或返回类型不一致,编译器会报错。例如,如果有一个函数是double Add(int, int),而数组的类型是int (*)(int, int),那么将Add加入数组会导致类型不匹配的错误。

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

想了解如何创建一个指针,这个指针可以指向整个函数指针数组,而不仅仅是数组中的某个元素。这种情况下,需要定义一个指向数组的指针,而不仅仅是数组元素的指针。如何正确声明这样的指针。函数指针数组的类型比较复杂,比如原来的数组声明是int (*arr[])(int, int),那么指向这个数组的指针应该怎么写呢?这里可能需要使用到数组指针的语法,即(ptr)N,其中N是数组的大小。但函数指针数组本身的类型还包括返回类型和参数,所以整个声明会更复杂。

简单来说:指向函数指针数组的指针其实就是一个指针,指针指向一个数组,数组的每个元素都是函数指针:我们通过拿函数指针数组的地址放到数组指针里面

#include<stdio.h>

int main() {
	int (*arr[4])(int, int) = { Add, Sub, Mul, Div };

    //指向函数指针数组的指针
    int (*(*arr[4]))(int, int) = &arr;
	return 0;
}

数组由于加了*号所以*arr[4]是一个指针数组,它的类型是函数指针,所以int(*(*arr[4]))(int, int)就是指向函数指针数组的指针,当然,如果继续深究:那么还会有指向函数指针数组的指针的数组,结构会越来越复杂。

8.回调函数 — 函数调用约定

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

我们接下来简单说明一下函数调用约定:函数调用约定定义了函数在调用过程中参数传递、堆栈管理和返回值处理的规则。它确保了调用方(Caller)和被调用方(Callee)对函数调用的底层细节达成一致,避免因规则不匹配导致的程序崩溃或数据损坏。

函数调用约定的核心概念:

1.参数传递顺序:

参数从左到右(C 风格)或从右到左(Pascal 风格)压入堆栈。

2.堆栈清理责任:

由调用方(Caller)或被调用方(Callee)负责清理堆栈中的参数。

3.返回值存储位置:

通常通过寄存器(如  EAX )返回,复杂类型(如结构体)可能通过堆栈或指针传递。

4.寄存器使用规则:

哪些寄存器由调用方保存,哪些由被调用方保存(例如  EBX , ESI , EDI  需由被调用方保存)。

常见的函数调用约定:

1.  cdecl (C Declaration)

特点:

参数从右到左压入堆栈。调用方负责清理堆栈(通过  add esp, n  指令)。支持可变参数函数(如  printf )。

2.  stdcall (Standard Call)

特点:

参数从右到左压入堆栈。被调用方负责清理堆栈(通过  ret n  指令)。不支持可变参数函数。

3.  fastcall 

特点:

前两个参数通过寄存器传递(如  ECX ,  EDX ),其余参数通过堆栈。被调用方清理堆栈。性能更高(减少堆栈操作)。

4.  vectorcall (SIMD 优化)

特点:

通过寄存器和堆栈混合传递参数(如浮点参数用  XMM0 - XMM3 )。用于加速 SIMD 运算(如向量计算)。

5.  thiscall (C++ 成员函数)

特点:

C++ 成员函数默认约定。this  指针通过寄存器(如  ECX )传递,其他参数从右到左压入堆栈。被调用方清理堆栈。

我们本章主要使用_cdecl - 函数调用约定来操作回调函数。

那回调函数如何使用呢?如何工作的呢?

比如,在一个库函数里,当某个事件发生时,就会调用用户自己定义的函数。这样用户可以根据需要自定义处理方式,而库函数不需要知道具体细节。比如排序函数,可能需要用户提供比较两个元素的函数,这时候用回调函数就比较方便。

我们先举一个简单的例子:

#include<stdio.h>
int Add(int x, int y) {
	return x + y;
}

void calc(int (*pa)(int, int)) {
	int a = 5;
	int b = 3;
	int ret = pa(a, b);
	printf("%d", ret);
}

int main() {
	calc(Add);
	return 0;
}

像这样我们通过函数传参给另一个函数,不是由实现方直接调用,这里把Add的地址传给了calc函数,由calc函数来实现最终的结果。

我们来举一个常见的冒泡排序,我们一般的冒泡排序是这样的:

#include<stdio.h>

void bubble_sort(int arr[], int sz) {
    int i = 0;
    int j = 0;
    int flag = 1;//假设这串数字是排好序的
    for (i = 0; i < sz - 1; i++) {
        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;
                flag = 0;
            }
        }
        if (flag == 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]);
    }
    return 0;
}

我们如何使用回调函数以及函数指针的方法来实现冒泡排序呢?这时候C语言库中引入一个qsort库函数,它的功能是使用快速排序的思想实现的一个排序函数,是C语言提供的一个库函数(可以排序任意类型的数据)

使用方法:C库中这样定义的:

void qsort(void* base, size_t num, size_t width,int(* cmpare)(const void* elem1, const void* elem2))

逐步分析一下:

void qsort:qsort库函数,返回类型是void

void* base  —  要排序数据的起始位置

size_t num —  待排序的元素个数

size_t width —  待排序的数据元素的大小(单位是字节)

int(* cmpare)(const void* elem1, const void* elem2))  —  函数指针 — 比较函数

CSDN中规定:如果elem1 less than elem2   我们return < 0;
                         如果elem1 greater than elem2 我们return > 0
                         如果elem1 equivalent to elem2 我们return 0;
 

比较两个整型元素:elem1指向一个整数,e2指向一个整数

但是void*是不可以直接接引用的。void*是无具体类型的指针,这种指针可以接收任意类型的地址,但是又因为void* 是无具体类型的指针,所以不能解引用操作,也不能+-整数

#include<stdio.h>
#include<stdlib.h>

int cmp_int(const void* e1, const void* e2) {
	if (*(int*)e1 > *(int*)e2) {
		return 1;
	}
	else if (*(int*)e1 == *(int*)e2) {
		return 0;
	}
	else {
		return -1;
	}
}

int main() {
	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]);
	}
	return 0;
}

我们这里使用强制类型转换来实现elem1和elem2进行比较。我们在主函数中我们调用qsort库函数实参为arr数组名,数组元素个数,首元素地址大小以及实现函数的地址我们通过这种方法就可以实现冒泡排序的功能,并且qsort不仅仅可以实现整型数组的排序,我们再来实现一个结构体排序方法:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int cmp_int(const void* e1, const void* e2) {
	if (*(int*)e1 > *(int*)e2) {
		return 1;
	}
	else if (*(int*)e1 == *(int*)e2) {
		return 0;
	}
	else {
		return -1;
	}
}

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_name(const void* e1, const void* e2) {
	//strcmp --> > 0 == 0 < 0
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);

}

void test2() {
	struct Stu s[] = { {"zhangsan", 15}, {"lisi", 30}, {"wangwu", 25} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}

int main() {
	test1();
	test2();
	return 0;
}

我们来分析一下这段代码

首先,代码里有两个测试函数test1和test2,分别对应不同的排序例子。test1是对整数数组排序,test2是对结构体数组按名字排序。主函数main调用了这两个测试函数。test1函数我们上面解释过了。test2函数,这里定义了一个结构体Stu,包含姓名和年龄。使用qsort按姓名排序,比较函数cmp_stu_by_name使用了strcmp,因为strcmp的返回值正好符合qsort的需要。结构体数组初始化后,调用qsort按名字排序,这里就能够正确按字母顺序排列名字。

现在我们对qsort库函数有基本了解,那我们现在再加一段函数,来优化一开始的冒泡排序

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int cmp_int(const void* e1, const void* e2) {
	if (*(int*)e1 > *(int*)e2) {
		return 1;
	}
	else if (*(int*)e1 == *(int*)e2) {
		return 0;
	}
	else {
		return -1;
	}
}

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_name(const void* e1, const void* e2) {
	//strcmp --> > 0 == 0 < 0
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);

}



void test2() {
	struct Stu s[] = { {"zhangsan", 15}, {"lisi", 30}, {"wangwu", 25} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}

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* base, int sz, int width, int (*cmp)(const void* e1, const void* e2)) {
	int i, j = 0;
	for (i = 0; i < sz - 1; i++) {
		int flag = 1;//假设数组中的元素是有序的
		for (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);
				flag = 0;
			}
		}
		if (flag == 1) {
			break;
		}
	}
}

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);
	int i = 0;
	for (i = 0; i < sz; i++) {
		printf("%d ", arr[i]);
	}
}

int main() {
	test1();
	test2();
	test3();
	return 0;
}

 这里的test3就是使用了qsort的基本用法来实现冒泡排序的功能

以上就是进阶指针的内容,指针内容在C语言中是非常重要的,在C语言中,指针是最核心、最基础的概念之一,它直接反映了计算机内存的底层机制,是C语言高效性、灵活性和底层控制能力的核心体现。

我们总结一下指针的核心作用:

1.直接操作内存
指针存储的是内存地址,允许程序绕过变量名直接访问或修改内存中的数据。这种能力使得C语言能够实现:
 
I.动态内存管理:通过  malloc 、 calloc  和  free  等函数手动分配/释放堆内存。
 
II.硬件级编程:直接读写特定内存地址(如嵌入式系统中操作寄存器)。
 

2.高效处理数据
 
I.数组与字符串的底层实现:数组名本质是首元素地址,指针遍历比下标访问更高效。
 
II.避免数据拷贝:传递大型结构体或数组时,传递指针而非副本,节省内存和时间。
 

3.函数参数传递的灵活性
 
I.按引用传递:通过指针参数修改外部变量(例如  void swap(int *a, int *b) )。
 
II.返回多个值:通过指针参数间接返回结果。
 

4.复杂数据结构的构建
 
I.动态数据结构:链表、树、图等依赖指针连接节点。
 
II.
函数指针:实现回调机制、多态行为(如  qsort  的排序函数参数)。
 

5.与硬件/系统的交互
 
I.直接访问外设:通过指针映射到硬件寄存器地址(如  volatile uint32_t *reg = 0x40000000; )。
 
II.
操作系统接口:系统调用(如文件操作)依赖指针传递缓冲区地址。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值