器→工具, 编程语言

C语言学习之指针

钱魏Way · · 10 次浏览

指针的基本概念

在C语言中,指针是一个非常重要的概念。指针本质上是一个变量,其存储的是值的地址,而不是值本身。这意味着,通过指针,我们可以直接访问或者操作内存中的数据。

理解指针的关键在于理解“指针是一个变量,其值为另一个变量的地址”。

  • 什么是地址:在计算机中,内存被组织成了一个个的存储单元,每个存储单元都有一个唯一的标识,这个标识就是地址。你可以把内存想象成一个巨大的仓库,每个格子都有唯一的编号,这个编号就是地址。
  • 什么是指针:指针就是一个特殊的变量,它存储的不是普通的数据,而是地址,这个地址指向内存中的另一个存储单元。

当你声明一个指针变量时,例如 int *p;,你创建了一个可以存储整型变量地址的指针 p。你可以用 & 运算符获取一个变量的地址,并用赋值语句将地址赋值给指针,例如 p = &a;。你也可以用 * 运算符获取指针所指向的变量的值,例如 int b = *p;。

指针在C语言中有很多应用,例如动态内存分配,函数参数传递,数据结构(如链表和树)等。掌握指针的使用,可以帮助你更好地理解和编写C语言程序。

使用指针的目的主要有以下几点:

  • 动态内存分配:在C语言中,指针常用于动态内存分配,例如使用malloc()、calloc()或者realloc()函数来在运行时动态创建或者调整数据结构的大小。
  • 操作大数据:当处理的数据量很大时,复制整个数据或者数据结构可能会消耗大量的时间和内存。使用指针,我们可以避免复制,只需传递数据的地址即可。
  • 函数返回多个值:在C语言中,一个函数只能返回一个值。但通过指针参数,你可以使函数间接地返回一个或多个额外的值。
  • 实现数据结构和算法:很多数据结构(如链表、树、图等)和算法(如快速排序)的实现都需要使用指针。
  • 函数指针和回调:指针可以用来指向函数,使得函数能够作为参数传递给其他函数,或者存储在数据结构中,这为实现更灵活的编程结构提供了可能,例如回调函数、函数表等。
  • 实现“引用传递”:在C语言中,所有函数参数默认是“值传递”,即函数接收的是实际参数的一个副本,对副本的修改不会影响原来的参数。通过使用指针作为函数参数,我们可以实现“引用传递”,即让函数直接操作原来的参数,而不是其副本。

指针是C语言中非常强大的一个工具,理解和熟练使用指针对于编写高效、灵活的C程序来说非常重要。

指针的声明和初始化

在C语言中,声明和初始化指针是两个重要的步骤。

声明指针

声明一个指针,需要指定指针的类型,即指针要指向的数据类型。语法如下:

type *pointer_name;

其中,type是指针指向的变量类型,pointer_name是指针变量的名称,星号(*)用来指定变量为指针类型。例如:

int *p;     // p是一个指向int类型的指针
char *ch;   // ch是一个指向char类型的指针
double *d;  // d是一个指向double类型的指针

在这些例子中,p、ch和d都是指针变量的名称,它们的类型分别是指向整数、字符和双精度浮点数的指针。

初始化指针

初始化指针,就是将一个内存地址赋值给它。通常,这个地址是一个已存在变量的内存地址。例如:

int var = 20;  // 定义一个整型变量
int *p;        // 定义一个整型指针

p = &var;      // 在指针p中存储变量var的地址

在这个例子中,变量var是一个整数类型变量,其值为20,p是一个指向整数类型的指针。我们用取地址运算符(&)获取变量var的地址,并将这个地址赋值给指针p。这样,指针p就被初始化,并指向变量var。

在声明指针的同时,也可以对其进行初始化:

int var = 20;  // 定义一个整型变量
int *p = &var; // 定义一个整型指针,并初始化为var的地址

注意,未被初始化的指针常被称为”野指针”,它们不指向任何有效的内存地址,使用这样的指针会导致程序行为不确定,因此应尽量避免。当我们不确定指针应该指向哪里的时候,可以先将其初始化为NULL:

int *p = NULL; // 定义一个指针并初始化为NULL

指针的大小

在C和C++中,所有类型的指针(不论指向哪种类型)的大小都是相同的,都等于内存地址的大小。这是因为,无论是什么类型的指针,它们都只需要存储一个内存地址,而内存地址的大小是固定的。

在32位系统中,内存地址的大小通常是4字节(32位),所以指针的大小通常也是4字节。在64位系统中,内存地址的大小通常是8字节(64位),所以指针的大小通常也是8字节。当然,这并不是一成不变的,具体的大小取决于编译器和目标平台。

你可以使用sizeof操作符来获取指针的大小,如下所示:

#include <stdio.h>

int main() {
    int* ip;
    char* cp;
    double* dp;
    
    printf("Size of int pointer: %zu bytes\n", sizeof(ip));
    printf("Size of char pointer: %zu bytes\n", sizeof(cp));
    printf("Size of double pointer: %zu bytes\n", sizeof(dp));
    
    return 0;
}

在这个例子中,我们分别声明了一个int指针、一个char指针和一个double指针,然后使用sizeof操作符来获取它们的大小。无论这些指针的类型是什么,它们的大小都应该是相同的。

空指针与野指针

空指针(NULL Pointer)

空指针是一个特殊的指针值,它不指向任何的内存地址。在C语言中,通常用宏定义NULL来表示空指针。使用空指针的目的是为了在指针没有实际指向任何地址之前,提供一个明确的值。这有助于防止指针随机指向某处,引起不可预料的行为。

空指针的使用示例:

int *p = NULL; // p是一个空指针,不指向任何内存地址

在使用指针前,检查是否为空指针是一个好习惯,因为尝试访问空指针所指向的内存可能会导致程序崩溃。

if(p != NULL) {
    // 指针p不是空指针,可以安全使用
}

野指针(Dangling Pointer)

野指针是指未被初始化,或者已经释放内存后仍然被使用的指针。野指针指向的内存区域是不确定的,可能是任意的、无效的或者不可访问的地址。使用野指针的结果是不可预测的,可能导致程序崩溃、数据损坏或者安全漏洞。

野指针的产生有多种情况,以下是几个例子:

指针变量被声明但未初始化:

int *p; // p是一个野指针,因为它未被初始化

指针指向的内存被释放后,指针未置为NULL:

int *p = (int *)malloc(sizeof(int));
free(p); // 释放p指向的内存
// 此时p成为野指针,因为它指向的内存已被释放

函数返回局部变量的地址:

int* func() {
    int x = 10;
    return &x; // 返回局部变量的地址是危险的,因为x的生命周期在函数返回后就结束了
}

对比

  • 空指针是一个明确指向”无处”的指针,用NULL表示,它的用途是表示指针目前不指向任何有效的内存位置。
  • 野指针则是指向”不确定位置”的指针,它可能由于未初始化、内存释放或指向局部变量等原因而指向无效的内存地址。使用野指针是危险的,因为它可能导致程序崩溃或其他未定义行为。

避免野指针的最佳实践是,总是在声明指针时立即将其初始化为NULL,并在释放指向的内存后也将指针置为NULL。

const修饰指针

在C语言中,const关键字可以用来修饰指针,使得指针指向的内容或者指针本身的值不可修改。具体来说,const可以有两种用法:

指向const的指针(pointer to const):这种指针不能用来改变它所指向的值,但是可以改变指针自己的值(也就是说可以改变它所指向的地址)。例如:

const int *ptr;

在这个例子中,ptr是一个指向const int的指针,我们不能通过ptr来改变它所指向的整数的值,但是可以改变ptr所指向的地址。

const指针(const pointer):这种指针自己的值不可改变,也就是说它一旦指向了一个对象,就不能再指向其它对象,但是它可以用来改变它所指向的对象的值。例如:

int * const ptr;

在这个例子中,ptr是一个const指针,它所指向的地址不能改变,但是可以改变它所指向的整数的值。

还有一种情况是指向const的const指针(const pointer to const),这种指针既不能用来改变它所指向的对象的值,也不能改变它自己的值。例如:

const int * const ptr;

在这个例子中,ptr是一个指向const int的const指针,不能改变它所指向的整数的值,也不能改变它自己的值(也就是不能改变它所指向的地址)。

const关键字在修饰指针时,能够提供一种保证,使得程序员不会意外地改变一些不应该改变的值,从而提高了代码的稳定性和可读性。

void指针

在C中,void指针,也称为通用指针,是一种特殊类型的指针,它可以指向任何数据类型。void指针用void关键字表示,并且不能直接进行除赋值和比较以外的操作。也就是说,你不能直接对void指针进行解引用或者以它做任何算数操作。

以下是一个简单的例子,说明如何使用void指针:

#include <stdio.h>

int main() {
    int i = 5;
    float f = 3.14;
    
    // 创建void指针
    void* p;
    
    // void指针可以指向int
    p = &i;
    printf("value of i: %d\n", *(int*)p);
    
    // void指针也可以指向float
    p = &f;
    printf("value of f: %.2f\n", *(float*)p);
    
    return 0;
}

在这个例子中,我们首先创建了一个void类型的指针p。然后,我们让p指向一个int变量i,并通过强制类型转换和解引用,得到i的值。接下来,我们让p指向一个float变量f,并同样通过强制类型转换和解引用,得到f的值。

总的来说,void指针在C中的一个重要用途就是它可以用来传递和返回不确定类型的数据。例如,C标准库中的函数malloc和calloc就返回void指针,这样它们就可以用来分配任何类型的内存。同样地,C标准库中的qsort函数接收一个void指针参数,这样它就可以用来对任何类型的数组进行排序。

scanf函数与&

在C语言中的scanf函数用于从标准输入(通常是键盘)读取数据。为了将读取的数据存储到变量中,scanf需要知道变量的地址,因此你需要在变量名前加上&运算符来获取其地址。这是因为,scanf实际上修改的是变量的值,而不是变量的副本。

以下是一个简单的例子,使用scanf读取一个整数和一个浮点数:

#include <stdio.h>

int main() {
    int i;
    float f;
    printf("请输入一个整数和一个浮点数:");
    scanf("%d %f", &i, &f);
    printf("你输入的整数是%d,浮点数是%.2f\n", i, f);
    return 0;
}

这段代码首先声明了两个变量i和f,然后使用scanf来读取用户输入的整数和浮点数。注意到scanf的参数中使用了&运算符,这是因为我们需要传入每个变量的地址,而不是变量的值。

但是,对于数组和字符串(实际上也是一种数组),我们不需要在scanf中使用&,因为数组名本身就代表了数组第一个元素的地址。例如,下面的代码就读取了一个字符串:

#include <stdio.h>

int main() {
    char str[100];
    printf("请输入一个字符串:");
    scanf("%s", str);  // 注意这里没有使用&
    printf("你输入的字符串是:%s\n", str);
    return 0;
}

这段代码首先声明了一个字符数组str,然后使用scanf来读取用户输入的字符串。注意到scanf的参数中没有使用&运算符,这是因为str本身就代表了字符数组的地址。

指针的运算

在C语言中,指针的运算使得直接操作内存地址变得可能。指针运算主要包括几种类型:指针的算术运算、指针的比较以及指针与整数的加减运算。下面分别进行介绍。

指针的算术运算主要包括四种:递增(++), 递减(–), 加(+)以及减(-)。这些运算允许我们在连续的内存单元间移动指针。

  • 递增:如果p是指针,则p++会增加p的值,使其指向下一个元素的内存位置。这里的“下一个”取决于指针所指向的数据类型的大小。
  • 递减:类似地,p–会减少p的值,使其指向前一个元素的内存位置。
  • 加法:p + n(其中n是整数)将p的值增加n个元素的位置。
  • 减法:p – n将p的值减少n个元素的位置。

需要注意的是,指针运算中的加法和减法并不是简单地在地址值上加上或减去一个数字,而是根据指针指向的数据类型的大小,进行相应的缩放。这是因为不同类型的数据占用的内存大小不同。

指针与指针的减法

指针与指针之间也可以进行减法运算,结果是两个指针之间的元素个数。例如,如果p1和p2是两个指向同一数组的指针,且p1指向的元素在p2之前,则p2 – p1将给出两个指针之间的元素数量。

指针的比较

指针之间还可以进行比较运算(==, !=, <, >, <=, >=)。这些运算可以用来检查两个指针是否指向同一个地址,或者在使用指针遍历数组时检查是否达到了数组的边界。

示例:

int arr[5] = {10, 20, 30, 40, 50};
int *p1 = &arr[1], *p2 = &arr[3];

printf("%d\n", *p1);  // 输出: 20
p1++;
printf("%d\n", *p1);  // 输出: 30

int diff = p2 - p1;
printf("%d\n", diff);  // 输出: 2,因为p2和p1之间有两个元素

if (p1 < p2) {
    printf("p1 is before p2\n");
}

指针运算是C语言提供的强大特性之一,它允许直接操作和访问内存。不过,需要谨慎使用以避免出现野指针或越界访问等问题。

指针的解引用

在C语言中,解引用指针就是通过指针获取存储在指针所指向的内存地址的值。这是通过使用解引用运算符*来实现的。

例如,假设你有一个整型指针p,它指向一个整型变量var:

int var = 10;
int *p = &var;

在这个例子中,p是一个指向整型的指针,它存储的是变量var的内存地址。如果你想获取存储在var中的值,可以通过解引用指针p来实现:

int value = *p;

这将把p指向的内存地址中的值(即var的值)赋值给value。在这个例子中,value的值将会是10。

解引用也可以用于改变指针所指向的值。例如:

*p = 20;

这会将p指向的内存地址中的值改为20。因为p指向var,所以这实际上会改变var的值。

注意,只有当指针指向一个有效的内存地址时,才可以解引用它。如果指针是NULL或者指向已释放的内存,尝试解引用它将导致未定义的行为,通常会导致程序崩溃。这就是在处理指针时应尽量避免空指针和野指针的原因。

动态内存分配

在C语言中,动态内存分配是程序在运行期间根据需要分配内存的方法。这是通过C语言的几个内建函数来实现的,包括malloc(), calloc(), realloc()和free()。

malloc()

malloc()函数在堆区分配一块指定大小的内存,返回该内存的起始地址。如果分配成功则返回指向被分配内存的指针,否则返回NULL。

例如:

// 分配一块可以容纳10个int类型的内存
int* ptr = (int*) malloc(10 * sizeof(int));

这行代码将在内存中分配足够存放10个int类型的空间,然后将这块内存的地址赋值给ptr指针。

calloc()

calloc()函数的工作方式类似于malloc(),但有两个主要的不同点。第一,calloc()需要两个参数:第一个是元素的数量,第二个是每个元素的大小。第二,calloc()将分配的内存初始化为0。

例如:

// 分配一块可以容纳10个int类型的内存,并初始化为0
int* ptr = (int*) calloc(10, sizeof(int));

realloc()

realloc()函数用于改变已分配内存的大小。它需要两个参数:第一个参数是需要被重新分配的内存的指针,第二个参数是新的大小。

例如:

// 改变ptr指向的内存大小,新的大小可以容纳20个int类型
ptr = realloc(ptr, 20 * sizeof(int));

free()

free()函数用于释放之前分配的内存。如果不释放已分配的内存,可能会导致内存泄露。

例如:

free(ptr);

这行代码将释放ptr指向的内存块,之后ptr就成为指向已释放内存的野指针。为了避免这种情况,通常在free()之后,会将指针设置为NULL:

free(ptr);
ptr = NULL;

动态内存分配是一种强大的工具,它可以让你的程序在运行时动态地使用内存。然而,它也需要谨慎使用,因为错误的使用可能会导致内存泄露或者是其他的问题。

指针类型

字符指针

字符指针在C语言中非常常见,特别是在处理字符串时。字符指针是一个指向字符的指针。我们可以使用字符指针来创建、读取和操作字符串。

例如,我们可以声明和初始化一个字符指针来指向一个字符串字面量:

char *str = "Hello, World!";

在这个例子中,str是一个字符指针,它指向的是字符串”Hello, World!”在内存中的第一个字符(即’H’)的地址。

你可以使用字符指针来遍历字符串:

char *str = "Hello, World!";
for (char *p = str; *p != '
char *str = "Hello, World!";
for (char *p = str; *p != '\0'; p++) {
putchar(*p);   // 打印当前字符
}
'; p++) { putchar(*p); // 打印当前字符 }

这个例子使用一个名为p的字符指针来遍历字符串。注意字符串在C语言中是以空字符(’\0’)结束的。所以,当p指向的字符是空字符时,我们就知道已经到达字符串的末尾。

你还可以使用字符指针来改变字符串中的字符,但是要注意,只有当字符串存储在可修改的内存区域(如字符数组)时才可以这样做。字符串字面量通常存储在只读内存区域,尝试修改它们的值通常会导致未定义的行为。

char arr[] = "Hello, World!";
char *p = arr;
p[7] = 'A';  // 将字符串中的'W'改为'A'
printf("%s", arr);  // 输出: "Hello, Aorld!"

在这个例子中,arr是一个字符数组,存储了一个字符串。p是一个字符指针,指向arr的第一个元素。我们通过p[7] = ‘A’;这行代码将字符串中的’W’改为’A’。

学习字符指针是理解C语言字符串操作的关键部分,因为在C中,字符串常常通过字符指针来处理。

数组指针

数组指针在C语言中是一个相当重要的概念。它是一个指针,指向一个数组。

例如,假设有一个整数数组:

int arr[3] = {10, 20, 30};

你可以创建一个指针,指向这个数组:

int (*p)[3] = &arr;

在这个例子中,p是一个指向大小为3的整数数组的指针。注意,指针和它所指向的数组的大小必须相匹配。也就是说,如果数组的大小发生变化,指针的类型也需要相应地改变。

你也可以利用数组指针来访问数组元素:

int arr[3] = {10, 20, 30};
int (*p)[3] = &arr;
printf("%d", (*p)[1]);  // 输出: 20

在这个例子中,我们使用(*p)[1]来访问数组的第二个元素。这是因为*p解引用了指针,得到了它所指向的数组,然后[1]取得了数组的第二个元素。

数组指针和指针数组是两个不同的概念。一个数组指针是一个指向数组的指针,而一个指针数组是一个包含指针的数组。例如,int *arr[10];声明的是一个大小为10的指针数组,其中的每个元素都是一个指向整型的指针。

理解数组指针对于理解C语言中复杂的数据结构和内存管理很重要。

冒泡排序

冒泡排序算法也可以用于排序指针数组,例如字符串数组。这需要一个专门的比较函数,可以比较两个指针所指向的值。

以下是一个C语言实现的例子,使用冒泡排序算法对一个字符串数组进行排序:

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

void bubbleSort(char *arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - 1 - i; j++) {
            if (strcmp(arr[j], arr[j + 1]) > 0) {
                // Swap arr[j] and arr[j + 1]
                char *temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main() {
    char *arr[] = {"apple", "banana", "kiwi", "peach", "grape"};
    int n = sizeof(arr) / sizeof(arr[0]);
    bubbleSort(arr, n);
    printf("Sorted array: \n");
    for(int i = 0; i < n; i++)
        printf("%s ", arr[i]);
    return 0;
}

在这个实现中,我们使用了strcmp函数来比较两个字符串。如果strcmp函数的返回值大于0,那么我们就交换这两个字符串的位置。

需要注意的是,我们实际上交换的是字符串的指针,而不是字符串本身。这比交换整个字符串更高效,特别是对于非常长的字符串

指针数组

指针数组在C语言中是一个存储指针的数组。它是一个数组,数组中的每个元素都是一个指针。元素的类型是指针,数组的大小是固定的。

例如,你可以创建一个包含3个整数指针的指针数组:

int *p[3];

在这个例子中,p是一个包含3个指针的数组,每个指针都是一个指向整数的指针。

你可以将每个指针指向一个整数:

int a = 10, b = 20, c = 30;
int *p[3] = {&a, &b, &c};

也可以通过每个指针访问其指向的值:

printf("%d\n", *p[0]);  // 输出: 10
printf("%d\n", *p[1]);  // 输出: 20
printf("%d\n", *p[2]);  // 输出: 30

这种指针数组在多种应用中都非常有用,例如,它可以用来创建一个字符串的数组:

char *strs[3] = {"One", "Two", "Three"};

在这个例子中,strs是一个包含3个元素的指针数组,每个元素都是一个指向字符的指针(也就是一个字符串)。

注意,指针数组和数组指针是两个不同的概念。一个指针数组是一个包含指针的数组,而一个数组指针是一个指向数组的指针。

函数指针

函数指针在C语言中是一种指针类型,它可以指向一个函数,而不是一个数据类型。这意味着,函数指针可以用来调用它所指向的函数,也可以作为一个参数传递给其它函数。这对于实现回调函数或者函数表非常有用。

函数指针的声明语法是:

return_type (*pointer_variable_name)(parameter_types);

例如,我们声明一个指向以下函数的指针:

int add(int a, int b){
    return a+b;
}

函数指针的声明如下:

int (*addPtr)(int, int);

接下来,我们可以让这个指针指向add函数:

addPtr = add;

一旦指针指向了一个函数,我们就可以通过指针来调用这个函数:

int sum = (*addPtr)(3, 4);  // sum is now 7

注意,虽然(*addPtr)(3, 4)和addPtr(3, 4)在C语言中是等价的,但是使用(*addPtr)(3, 4)形式更能突出addPtr是一个函数指针。

函数指针的一个重要应用是作为其他函数的参数。例如,你可以写一个函数,它的一个参数是一个函数指针,这样你就可以在这个函数内部调用传递过来的函数。这种技术在写库函数或者API时非常有用,因为它允许用户自定义一些行为。

转移表

转移表,也被称为跳转表或者查找表,是一种常用于编程中的数据结构,它可以将程序控制流的决策从运行时计算转化为查表操作,从而提高程序运行的效率。

转移表通常是一个由函数指针、对象方法引用或者某种类似的可执行代码引用组成的数组或者其他数据结构。在程序运行时,根据某些条件选择转移表中的某个项,并执行对应的代码。

以下是一个简单的C语言示例,展示了如何使用转移表来实现一个简单的计算器:

#include <stdio.h>

// 定义四个基本运算函数
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 division(int a, int b) { return b != 0 ? a / b : 0; }

// 定义函数指针类型
typedef int (*operation)(int, int);

// 创建转移表
operation operations[] = {add, sub, mul, division};

int main() {
    printf("3 + 2 = %d\n", operations[0](3, 2));     // add
    printf("3 - 2 = %d\n", operations[1](3, 2));     // sub
    printf("3 * 2 = %d\n", operations[2](3, 2));     // mul
    printf("3 / 2 = %d\n", operations[3](3, 2));     // division
    return 0;
}

在这个例子中,我们首先定义了四个简单的数学运算函数:add、sub、mul和division。然后,我们定义了一个函数指针类型operation,并创建了一个operation类型的数组operations,将四个函数的地址存入数组,构成了一个转移表。在main函数中,我们通过索引转移表来选择并执行不同的运算。这样,我们就可以根据用户的输入或者其他条件,动态地选择和执行运算,而无需使用冗长的if或switch语句。

二级指针

在C语言中,二级指针或者被称为指针的指针,是一个指向另一个指针的指针。你可以通过解引用一次来访问第一级指针,解引用两次来访问原始数据。

二级指针的声明如下:

type **pointer_to_pointer;

例如,假设我们有一个整数和一个指向整数的指针:

int val = 10;
int *ptr = &val;

我们可以创建一个指向ptr的二级指针:

int **pptr = &ptr;

在这个例子中,pptr是一个指向ptr的二级指针。我们可以通过pptr来访问ptr,也可以通过ptr来访问val:

printf("%d\n", **pptr);  // 输出: 10

这个表达式**pptr首先解引用pptr得到ptr,然后解引用ptr得到val,所以它的值是val的值。

二级指针在一些复杂的场景中很有用,例如动态内存分配的二维数组,或者是当你需要一个函数以引用方式来修改一个指针时。例如,你可能有一个函数需要修改它的一个参数指向的地址,你就需要将这个参数的地址(也就是这个参数的指针)传递给这个函数,那么这个函数的参数就是一个二级指针。

指针参数

数组参数

在C语言中,数组作为参数传递给函数时,实际上是传递的数组的首地址,也就是说,函数接收的是一个指向数组第一个元素的指针。因此,在函数内部,无法直接得知数组的大小,通常需要传递一个额外的参数来指定数组的大小。

例如,下面是一个计算整数数组所有元素之和的函数:

int sum(int *arr, int n) {
    int total = 0;
    for (int i = 0; i < n; i++) {
        total += arr[i];
    }
    return total;
}

在这个例子中,arr是一个指向整数的指针,实际上就是传入的数组的首地址。n是数组的大小。函数通过指针arr和数组大小n来计算所有元素的和。

调用这个函数的方式如下:

int nums[] = {1, 2, 3, 4, 5};
int total = sum(nums, 5);

在这个例子中,nums是一个包含5个元素的数组。当我们将nums作为参数传递给sum函数时,实际上传递的是nums数组的首地址。

需要注意的是,由于数组作为指针传递,函数内部对数组元素的修改会影响到原数组。这是因为,函数内部操作的是原数组的实际存储,而不是数组的副本。

指针参数

在C语言中,指针作为参数传递给函数,可以让函数访问和操作函数外部的变量。因为指针存储的是变量地址的值,所以通过指针,函数可以直接修改它所指向的值。

例如,下面是一个用来交换两个整数的函数:

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

在这个例子中,a和b都是指向整数的指针。函数通过解引用指针*a和*b来交换a和b所指向的值。

调用这个函数的方式如下:

int x = 10;
int y = 20;
swap(&x, &y);

在这个例子中,&x和&y是x和y的地址。将它们作为参数传递给swap函数后,函数将交换x和y的值。

使用指针作为参数的一个主要原因是,它允许函数修改多个参数。在C语言中,函数参数默认是通过值传递(pass-by-value),这意味着函数接收的是参数的副本,而不是原始参数本身。因此,函数无法直接修改原始参数的值。但是,如果参数是一个指针,那么函数就可以通过这个指针来修改原始参数的值。

此外,使用指针作为参数还可以提高效率,特别是当参数是大型结构或者数组时。因为复制一个大型结构或者数组需要花费大量的时间和内存,而复制一个指针只需要很少的时间和内存。

qsort函数

qsort函数是C语言标准库中提供的一个快速排序函数。快速排序是一种非常高效的排序算法,具有平均时间复杂度为O(n log n)。

qsort函数的原型如下:

void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
  • base是指向待排序数组的首元素的指针。
  • nmemb是待排序数组的元素个数。
  • size是每个元素的大小(以字节为单位),可以通过sizeof运算符获取。
  • compar是一个函数指针,指向一个比较函数。比较函数需要接受两个void指针作为参数,并返回一个int。如果比较函数返回的值小于0,那么第一个元素会被置于第二个元素之前;如果返回0,那么两个元素的顺序不会改变;如果返回的值大于0,那么第二个元素会被置于第一个元素之前。

以下是一个使用qsort函数对整数数组进行排序的例子:

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

// 比较函数
int compare(const void *a, const void *b) {
    return (*(int*)a - *(int*)b);
}

int main() {
    int arr[] = {10, 5, 15, 12, 90, 80};
    int n = sizeof(arr)/sizeof(arr[0]);
    
    qsort(arr, n, sizeof(int), compare);
    
    printf("Sorted array: \n");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    
    return 0;
}

在这个例子中,我们定义了一个compare函数,该函数通过指针类型转换和减法运算来比较两个整数的值。然后,我们调用qsort函数,将arr、n、sizeof(int)以及compare作为参数传入,从而对数组arr进行排序。

回调函数

回调指针

在C语言中,函数指针是指向函数的指针。通过函数指针,我们可以在程序中像操作其他类型的指针一样操作函数。这一特性使得我们可以使用函数指针作为函数的参数,从而实现函数的动态调用,这就是所谓的“回调函数”。

回调函数是一种策略,让程序的某一部分(通常是一个组件或一个库)调用另一部分提供的特定的代码。这常常用在需要抽象和分离功能的情况下,例如事件处理、排序算法、线程或进程间的通信等。

例如,下面是一个使用回调函数的例子:

#include <stdio.h>

// 这是回调函数
void print_number(int n) {
    printf("%d\n", n);
}

// 这是一个接受回调函数作为参数的函数
void do_something(int n, void (*callback)(int)) {
    // 做一些处理...
    // 然后调用回调函数
    callback(n);
}

int main() {
    // 调用do_something,传入一个数和一个回调函数
    do_something(5, print_number);
    return 0;
}

在这个例子中,do_something函数接受一个回调函数作为参数。在do_something函数内部,它调用了传入的回调函数。在main函数中,我们调用了do_something函数,并传入了一个函数print_number作为回调函数。当do_something函数运行时,它会调用我们提供的print_number函数,从而打印出传入的数。

注意回调函数的类型必须匹配。在这个例子中,回调函数必须是一个接受一个int参数并返回void的函数。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注