红魔咖啡馆

头发越掉越多,头发越掉越少

0%

【C/C++】指针

指针

数据在内存中的存储

这是一段内存,他被分成了许多段,每段一个byte

当我们声明一个变量int a,计算机会给该变量分配一块空间,分配取决于数据类型与编译器

如:int 与float占4bytes,char占1bytes

当我们给变量a赋值a=5,计算机会去寻找该变量,去到他的地址,以二进制写入数据

指针的基础使用

指针是一种变量,它可以存储变量的地址

使用数据类型* 变量名创建一个指针:int *p char *c

使用取地址符&获取一个变量的地址,并可以将指针指向该变量地址:p=&a

print p,print &a,print &p分别对应打印p指向(a)的地址,a的地址,p的地址

将一个*放在指针变量前,可以对指针进行解引用,即获取p指向地址的值

print *p会返回a的值,*p = 8会更改a的值到8

实例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<stdio.h>
int main(){
    int a=5;
    int* p;  // 若该指针p没有被初始化,p会变成一个野指针,会因为指向任意位置而报错
    p = &a;  // 储存a的地址
    // 上两行等同于int* p = &a;
    printf("A:%d\n",p); // a的地址
    printf("V:%d\n",*p); // a的值
    printf("A:%d\n",&a); // a的地址

    *p = 12; // 更改p指向地址(a)的元素值
    printf("A:%d\n",a); // 更改后的a值

    int b = 20;
    *p = b; // p不会指向b,只会将b的值赋给a
    printf("A:%d\n",p); // 还是a的地址
    printf("V:%d\n",*p); // b的值

    return 0;
}

指针类型

不同的数据类型占据不同的内存:

int:4bytes

char:1byte

float:4bytes

void指针

  1. 可以存放任意类型的指针,且无需强制类型转换
  2. 需要进行显式转换后才能赋值给其他类型
  3. 可以与其他类型指针直接比较地址值
  4. 只有强制类型转换后才能操作(解引用、算术运算等)
  5. 可以和普通指针一样传入NULL或nullptr表示空指针
  6. 作为函数输入输出时,表示可以接受任意类型和输出任意类型的指针

指针类型间的转换

定义一个变量int a=1025:

他在内存中的布局为(从右到左分别为第0 1 2 3个字节,他们的地址也是连续的):

其中,最左边的一位为符号位,0为整数1为负数

若我们定义一个字符指针c指向a,由于字符只占一个字节,故c只会指向a的第一个字节:

对c进行算术运算(+1)会让他指向下一个字节 以至于得到4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
int main(){
  // 一个整形指针
  int a = 1025; // 在二进制中为四字节:00000000 00000000 00000100 00000001
  int* p;
  p = &a;
  printf("size of integer is %d\n", sizeof(int));
  printf("Address = %d, value = %d\n",p,*p);

  // 一个字符指针
  char *c;
  c = (char*)p; // 进行强制类型转换
  printf("size of integer is %d\n", sizeof(char));
  // 由于char指针只有一个字节,则机器只看从右边开始的一个字节 即00000001
  printf("Address = %d, value = %d\n",c,*c);
  // 增加一个字节,则指针指向从右边开始的第一个字节 即00000100
  printf("Address = %d, value = %d\n",c+1,*(c+1));

  // 一个void指针
  void *p0;
  p0 = p;  // 不需要显式的类型转换
  // 当p0没指向任何特定类型时,不能解引用
  printf("Address = %d, value = %d\n",p0,*p0);
  // 也不要进行算术运算
  printf("Address = %d %d\n",p0,p0+1);
}

指针算术运算

对一个指针进行加1操作,相当于将该指针增加一个该指针数据类型所占字节数的字节数

例如对int *p=&a; p++;得到p的值为a的地址加4

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main(){
  int a = 10;
  int *p;
  p = &a;
  // 指针加法 : +1代表增加一个数据类型的字节数
  printf("Address p is %d\n",p); // p的地址
  printf("Value at address p is %d\n",*p); // p指向的值
  printf("size of integer is %d bytes\n", sizeof(int)); // int类型所占的字节数
  printf("Address p+1 is %d\n",p+1); // p+1指向的地址
  printf("Value at address p+1 is %d\n",*(p+1)); // p+1指向的值(垃圾值)
}

指向指针的指针

假设定义了一个数据int x = 5;

我们定义一个指针指向x int *p = &x

此时我们可以再定义一个指针指向指针p int **q = &p

甚至还可以定义一个指针指向指针q int ***r = &q

(r是205)

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
int main(){
  int x =5;
  int* p = &x;
  *p = 6;
  int** q = &p;
  int*** r = &q;
  printf("%d\n",*p); // p指向x的值
  printf("%d\n",*q); // q指向p的值(x的地址)
  printf("%d\n",*(*q)); // q指向p的值指向x的值
  printf("%d\n",*(*r)); // r指向q的值,q指向p的值
  printf("%d\n",*(*(*r))); // r指向q的值,q指向p的值,p指向x的值
  
  // 更改x的值
  ***r = 10;
  print("x = %d\n", x);

  // **q与*p都指向x的值,则相当于x自加2
  **q = *p +2;
  print("x = %d\n", x);
}

指针用例—函数传引用 or 传值?

e.g. 局部变量与全局变量:

有以下代码

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
void f(int x){
    x=x+1;
}
int main(){
    int x=10;
	f(x);
    printf("%d\n",x);
    return 0;
}

该代码中的函数想让变量+1,但输出的结果显然还是10,这是为什么呢?

看以下代码:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
void f(int x){
    printf("Address of x in f is:%d\n",&x);
    x=x+1;
}
int main(){
    int x=10;
	  f(x);
    printf("Address of x in main is:%d\n",&x);
    return 0;
}

你会惊奇的发现,f函数中的x与main函数中的x的地址不一样,这也就说明了为什么+1不成立

当程序运行时,计算机会预留一部分内存给程序,他们被分为四个部分:栈、堆、数据区、代码区

  • 栈:非静态局部变量(函数参数等)。栈是向下生长的
  • 堆:用于动态内存分配。堆是向上生长的
  • 数据区:存储全局变量和静态变量
  • 代码区:可执行的代码与常量

当一个程序进行时,main函数被调用,关于这个函数的信息(如参数,局部变量,返回地址)会存在栈上,栈便为该函数开辟一块空间,称为栈帧(stack frame),每个函数都会有一个栈帧

让一个函数调用另一个函数时,两个函数分别称为主调函数与被调函数,在主调函数中调用其他函数用到的参数称为实际参数,被调函数中的函数称为形式参数,实参会被映射到形参。这个操作即传值

当main函数调用f函数时,一块它的栈帧会被创建,其中的参数会被分配到对应空间,执行+1操作后,这个函数的栈帧中的变量执行了+1,但不影响其他地方的变量。

当f函数执行完毕,程序回到main函数,此时f的栈帧会被清除,main函数会被继续执行,故局部变量的生命周期只是函数执行期间

接下来进行的函数是printf函数,这是一个库函数,在栈中创建它的栈帧并指型。这一个结构被称为(函数)调用栈,即:是将一个个函数的栈帧,按照调用的顺序依次压入栈中,等最上层的函数执行完了,就弹出相应的栈帧的过程

注意:栈是有大小的,如果因为无限递归等原因导致栈帧一直被创建而不清除,程序会因为栈溢出而终止

那传引用能否实现?

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
void f(int *p){
    *p = (*p)+1;
}
int main(){
    int a;
    a = 10;
    f(&a);
    printf("a = %d", a);
    return 0;
}

该函数传的是地址

当调用main函数,它的栈帧被创建,a=10进入栈。

调用f函数,它的栈帧被创建,则p接收到a的地址,入栈,此时p指向a。

在函数中解引用p,并执行操作,p指向的内存(a)的值就会增加,即a的值增加1

回到main函数,a的值就是11

这就是传引用,它可以节省很多内存空间,也可以处理一些复杂数据类型以节省内存

指针与数组

让我们声明一个数组 int a[5]

即我们创建了五个整型变量,在内存中连续存在(int 占四个字节),则整个数组占的大小为20bytes,作为一个连续的块

我们定义一个指针int* p,将p指向a的第一个元素,则p解引用后打印的是a[0]的值

回忆之前说的指针算术,若我们将p+1,则p会往前移动四个字节,**此时*(p+1)即a[0]后四个字节的值,即a[1]**

与之前不同,一个值它的地址+1后会移动到一个未知内容的地址,而数组a中+1后p指向的值是已知的

若直接将数组名赋值给p,则p默认接收到的是数组a首元素的地址,称为数组的基地址

若想获得数组某个值的地址,可以使用&a[i]或者a+i

若想获得数组某个值,可以使用a[i]或者*(a+i)

注意:对数组名(常量 )自加是非法的,可以定义一个指针指向数组名让该指针自加

实例代码:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(){
  int a[]={2,4,5,8,1};
  for (int i = 0;i<5;i++){
    printf("Address = %d\n",&a[i]);
    printf("Address = %d\n",a+i);
    printf("Value = %d\n",a[i]);
    printf("Value = %d\n", *(a+i));
  }
}

指针用例—数组传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<stdio.h>
int suma(int a[],int size){
  int sum = 0;
  for (int i = 0;i<size;i++){
    sum+=a[i];
  }
  return sum;
}
int sumb(int a[]){
  int sum = 0;
  int size = sizeof(a)/sizeof(a[0]);
  printf("In sumb - size of a = %d, size of a[0] = %d\n",sizeof(a),sizeof(a[0]));
  for (int i = 0;i<size;i++){
    sum+=a[i];
  }
  return sum;
}
int main(){
  int a[]={1,2,3,4,5};
  // 计算数组大小
  printf("In main - size of a = %d, size of a[0] = %d\n",sizeof(a),sizeof(a[0]));
  int size = sizeof(a)/sizeof(a[0]);
  // 传入数组大小后进行加和
  int tot = suma(a,size);
  // 在函数中计算数组大小并加和
  int tot2 = sumb(a);
  printf("tot = %d\n", tot);
  printf("tot2 = %d\n", tot2);
}

以上代码会出现一些问题:

在调用main与sumb函数时,它们的栈帧会被创建

调用sumb函数时,编译器只会创建一个同名的指针在sumb的栈帧中,即它只指向main函数中数组a的首元素地址(int a[]等同于int *a

因此函数中的sizeof a是一个8字节的指针。

指针与字符数组

字符数组

C中的字符串存储在数组中,必须以’\0’结束

字符数组赋值:

可以指定每一位进行赋值,

或者使用字符串字面值(用双引号括起来的字符串)赋值,该方法会隐式地添加一个’\0’

或者使用大括号初始化每一位,以逗号间隔并在结尾加上’\0’

注意:只能在声明同时用字符串字面值赋值

使用指针

字符数组中数组名代表的是数组首元素的地址,可以用一个指针变量指向它,该变量也可以对字符数组进行操作

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
    char c1[6]="Hello";
    char *c2;
    c2 = c1;
    c2[0]='A'; // equal to c1[0]='A';
    
    c2++; // 指向下一个元素
    c1++; // 非法
}

有等价关系:

c2[i]等同于*(c2+i) c1[i]等同于*(c1+i)

函数传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
void print(char *c){
  int i = 0;
  // while (c[i]!='\0'){ // *(c+i)也可以
  //   printf("%c",c[i]);
  //   i++;
  // }
  // 由于c是指针,故可以通过自增与解引用来进行访问
  while(*c!='\0'){
    printf("%c",*c);
    c++;
  }
  printf("\n");
}
int main(){
  char c[20] = "Hello";
  print(c);
  return 0;
}

指针与二维数组

如果我们要创建一个二维数组b[2][3]

此时,b[0]b[1]表示三个整数的一维数组,在内存中占用3*4=12个字节,是按行存储的

int *p=b;指代的是返回一个指向一维数组的指针,此时不能指针运算或解引用,故不能这么用

故应使用int (*p)[3]来创建一个二维指针数组,其中3表示三个指向一维数组的指针

这时print b or &b[0]都指代第一个元素的地址

print *b or b[0] or &b[0][0]都指代第一个元素地址的值

print b+1 or &b[1]会跳到下一个数组的首地址(+12)

使用print *(b+ 1)or b[i]or&b[1][0]指代下一个指向一维数组的并返回 值

解引用时,需要一步步解

*(*b+1)

*b 为b[0],一个一维数组的首元素地址,*b+1会让指针移动四个字节带到下一个整型变量,

相当于&b[0][1]

我们解引用后,*(*b+1)就相当于b[0][1]

指针运算:b[i][j]=*(b[i]+j)=*(*(b+i)+j)

指针与多维数组

原理

与二维数组类似

假如我们有一个int数组c[3][2][2],在内存中的存储如下

简化为三个二维数组线性存储,每个二维数组内两个一维数组线性存储

故我们可以声明一个指针int (*p)[2][2] = c;,指向2*2的二维数组

这时print cprint *cprint c[0]print &c[0][0]均输出第一个一维数组的地址

指针运算:c[i][j][k]=*(c[i][j]+k)=*(*(c[i]+j)+k)=*(*(*(c+i)+j)+k)

可以理解为解引用一次就脱一层[]

e.g.

print *(c[0][1]+1)指向的是c[0][1][1]

print *(c[1]+1)指向的是c[1][1],即c[1][1][0]

用于函数传参

如一维数组传参,我们可以通过传值或传引用两种方法传参

注意:传值时除了最高维度,其他维度必须要指定偏移量(即形参定义时必须和传入数组长度一样)

1
2
3
4
5
6
int two_dim(int a[][3]){

}
int three_dim(int a[][2][2]){

}

而根据数组名就是指向第一个元素的指针,我们可以直接传引用来实现降维度

注意:只能降到次一级维度,且其余的必须指定偏移量

1
2
3
4
5
6
int two_dim(int (*a)[3]){

}
int three_dim(int (*a)[2][2]){

}

指针与动态内存

内存的分配

在一个典型架构中,分配给应用程序的内存分为四个区段:栈、堆、数据区、代码区

  • 栈:非静态局部变量(函数参数等),函数调用信息。栈是向下生长的
  • 堆:用于动态内存分配。堆是向上生长的
  • 数据区:存储全局变量和静态变量
  • 代码区:可执行的代码与常量

当一个程序进行时,main函数被调用,关于这个函数的信息(如参数,局部变量,返回地址)会存在栈上,栈便为该函数开辟一块空间,称为栈帧(stack frame),每个函数都会有一个栈帧,大小在编译期间决定

所有函数从下往上开辟栈帧后,在执行时总是栈顶的函数在执行,其余函数暂停,等待上方函数返回值等。当上方函数返回后,它占用栈的内存也会被清除,下一个函数运行。任何时候正在执行的函数都是栈顶的那个函数

预留给栈的空间在运行期间并不会增长,也不能请求更多内存。如果运行时的栈增长超过了程序预留的栈内存大小,那么会造成栈溢出(stack overflow)

因此栈有两个限制:

  1. 在栈上的变量无法操作骑作用域
  2. 当声明一个很大的数据类型,可能会造成溢出;且只能在编译时分配他的大小,无法在程序运行时分配它的大小

堆(动态内存)

这时我们需要用到来分配或销毁或内存。我们可以任意使用堆上的内存,只要不超过系统内存限制。

堆又称为动态内存,使用堆内存称为动态内存分配 (注意这里的堆并不是数据结构)

C风格:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
int main(){
  int a; // 分配到栈上
  int *p; // 指向堆内存的指针
  p = (int*)malloc(sizeof(int)); // 在堆上开辟一块四字节的内存,让指针p指向其地址
  *p=10; // 解引用将值存入堆
  free(p); // 释放内存
  p = (int*)malloc(20*sizeof(int)); // 在堆上再开辟一块20个四字节的内存作为数组,让指针p指向其首元素地址
  // 可以用以下两种方式访问
  p[0] = 1; 
  *(p+1) = 2;
}

定义一个指针变量p,它被存储在栈中,指向堆中分配的内存地址

通过malloc在堆上分配一块四字节的内存,malloc会返回一个指向这块内存起始地址的指针,void类型。故我们需要进行一个强制类型转换,并赋给指针变量p

使用堆上内存的唯一方式是通过引用,自己维护一个指针指向这块内存。我们通过对指针变量p解引用并赋值来使用。

若我们再以同样方式分配一块四字节内存,让p指向它,并赋值为20,p此时指向的便是堆中的另一块内存。而之前那块内存仍在堆上,并不会被自动回收,此时称为内存泄漏,因此我们需要在用完一块内存后,及时调用free()释放内存。

如果要分配一个数组内存,我们只需要传入数组大小字节数即可,返回的是内存的初始地址

若malloc无法在堆上成功分配内存,会返回NULL

C++风格:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>

int main(){
  int a;
  int *p; // 指向堆内存的指针
  p = new int; // 在堆上开辟一块int大小的内存(四字节)
  *p = 10; // 解引用并赋值
  delete p; // 释放内存
  p = new int[20]; // 在堆上开辟一个20大小的int数组
  delete[] p; // 释放数组内存
}

c++中使用关键字new与delete开辟与释放内存,不需要类型转换

相关函数

malloc

定义:void* malloc(size_t size)(size_t相当于无符号整数)

用法:int *p = (int*)malloc(分配空间大小)

  • 分配空间大小一般习惯通过sizeof计算,如想要开辟一个存储三个int变量的数组,我们可以用3*sizeof(int)来计算大小,一般不直接写。
  • 由于malloc返回的是void指针,指向其初始地址,而void指针无法解引用,因此我们一般在前面进行强制类型转换来转化成int指针以方便操作
  • malloc不会将分配的空间初始化,建议使用memset(p,0x00,sizeof(p))初始化

calloc

定义:void* calloc(size_t num, size_t size)

用法:int *p = (int*)calloc(分配元素个数, 每个元素空间大小)

  • calloc可以指定分配的元素个数
  • calloc在分配空间后会自动将其初始化为0

realloc

定义:void* realloc(void* ptr, size_t size)

用法:realloc(已指向某处内存的指针, 修改空间大小)

  • realloc用于修改分配的内存大小
  • 若需要的新内存块比原来大,程序会创建一块新内存并将内容复制过去
  • 若之前的内存的相邻部分还有可用内存,程序会直接拓展原空间
  • 若需要的新内存块比原来小,则多余部分的内存会被释放掉

注意以下用法:

1
2
int *a = (int*)realloc(a,0); // 相当于free(a)
int *b = (int*)realloc(NULL,n*sizeof(int)); // 相当于malloc

内存泄漏

当我们动态申请了内存后,忘记去释放,此时程序占用了一些未使用的内存,称为内存泄露

对于栈:由于栈帧在使用完后会被自动销毁,故不会发生内存泄漏

对于堆:在开辟空间后,堆上的内存必须要被显式地释放掉,否则会一直存在

注意:任何未使用和未引用的堆上内存都是垃圾,程序员要确保不要浪费内存

函数返回指针

我们可以在函数类型处加上*来声明函数返回值是一个指针。

注意以下情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
// 现在该函数返回的是一个int类型的指针,即c的地址
void print(){
  printf("hello world");
}
int* add(int* a,int* b){
  int c = (*a)+(*b);
  return &c;
}
int main(){
  int x = 2,y=4;
  int* p =add(&x,&y);
  print();
  printf("sum=%d",*p);

}

该程序对print的输出正常,而输出sum时出现了异常,从底层原理分析:

执行main函数,开辟栈帧,执行add函数,在main函数上再开辟栈帧,main函数会等待add返回

此时add中a与b存储了main函数中x与y的地址,c存储了*a*b的和,返回的是c的地址,故main函数中的p指针存储的是c的地址

add执行完毕,占用空间被清除。但注意,此时p指向的内存仍未变化,即此时它指向了被释放掉的内存空间,值是随机的

现在执行print函数,开辟栈帧,原空间被覆盖,因此p存储的地址对应的值已经不是c的值了,因此输出异常。

还有一种情况:若不执行print函数,输出的值可能会正确。因为此时程序还没重写或清除那个空间上的数据(虽然已经释放空间)

而对于main与add函数,由于被调函数的栈空间总是在主调函数之上,因此被调函数执行时主调函数仍在栈内存中,因此add可以访问main函数中的变量。但若我们想要返回被调函数的一个局部变量给主调函数,当被调函数结束后,内存已被释放,因此会出问题。

因此可以从栈底向上传局部变量或局部变量的地址,但不能从栈顶向下传局部变量或局部变量的地址

因此我们可以修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
// 现在该函数返回的是一个int类型的指针,即c的地址
void print(){
  printf("hello world");
}
int* add(int* a,int* b){
  int *c = (int*)malloc(sizeof(int)); // 在堆上开辟内存
  *c = (*a)+(*b);
  return c; // 返回的是堆上的指针
}
int main(){
  int x = 2,y=4;
  int* p =add(&x,&y);
  print();
  printf("sum=%d",*p);
}

我们将c开辟在堆上,此时就不会被清除了,返回c是安全的

函数指针

我们可以用指针指向函数地址,即指向函数的指针。我们可以用这种指针解引用和调用函数。

函数的地址

在内存中,一个函数就是一块连续的内存。

一般程序执行指令会按照地址依次执行,而函数调用可以让程序跳到某一个地址开始执行其中的指令。

此时对应的函数地址可以称为函数的入口点,即函数第一条指令的地址

函数指针的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
void print(char *name){
  printf("hello");
}
int add(int a, int b){
  return a+b;
}
int main(){
  int c;
  int (*p)(int,int); // 函数指针
  p = &add; // 指针指向add函数的地址(不用&也可)
  c = (*p)(2,3); // 解引用,并传入参数执行函数(不用解引用也行)
  printf("%d\n",c);
  void (*ptr)(char*);
  ptr = print; // 不用&的情况
  ptr("mixbp"); // 不用解引用的情况
}

由于单纯的函数名代表函数入口点,故不用&与解引用也可以

注意:为了指向一个函数,函数指针的类型必须是正确的

回调函数

将函数指针作为函数参数传入,并在函数内部通过该函数指针调用函数,被调用的函数即为回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>

void print(){
  printf("hello");
}
void b(void (*ptr)()){
  ptr(); // 用ptr回调传进来的函数
}

int main(){
  void (*p)() = print; // 定义一个指针指向print
  b(p); // 传入该函数指针
  b(print); // 这样也可以传
}

代码中,函数b可以通过函数指针来回调函数print

可以定义一个指向print函数的指针传入,也可以直接传入print,此时指代的是print函数的首地址

应用:排序

设计一个排序函数,并可以按照不同逻辑进行排序

我们可以在普通排序函数中添加一个函数指针参数(比较函数),通过设计蚂蚁比较逻辑并传入排序函数,可以灵活实现不同情景的比较,而不用每次都根据不同逻辑重新写一遍排序函数

如实现正序、逆序、按绝对值排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <math.h>
int compare(int a,int b){
  if (a>b) return 1;
  else return -1;
}
int r_compare(int a, int b){
  if (a>b) return -1;
  else return 1;
}
int abs_compare(int a, int b){
  if (abs(a)>abs(b)) return 1;
  else return -1;

}
void sort(int *a, int n, int (*cmp)(int,int)){
  int tep;
  for (int i = 0;i<n;i++){
    for (int j = 0;j<n-1;j++){
      if (cmp(a[j],a[j+1])>0){
        tep = a[j];
        a[j] = a[j+1];
        a[j+1]=tep;
      }
    }
  }
}
int main(){
  int a[]={3,2,1,5,6,4};
  int b[]={-2,-3,5,4,1,-6};
  sort(a,6,compare); // 正序排序
  for (int i = 0;i<6;i++) printf("%d ",a[i]);
  printf("\n");
  sort(a,6,r_compare); // 逆序排序
  for (int i = 0;i<6;i++) printf("%d ",a[i]);
  printf("\n");
  sort(b,6,abs_compare); // 绝对值排序
  for (int i = 0;i<6;i++) printf("%d ",b[i]);
  printf("\n");

}

同样的逻辑,在c的stdlib.h库中有一个qsort函数,只要给予它排序逻辑并传入就可以对任意数组排序