自学内容网 自学内容网

C语言第12节:指针(2)

1. 数组名的理解

1.1 数组名的本质

在C语言中,数组名本质上是一个“常量指针”,它指向数组的第一个元素的地址。比如说,有一个整数数组:

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

这里的arr就是一个指向数组第一个元素的指针,arr的值实际上是&arr[0],即数组第一个元素的地址。

不过,要注意数组名和普通指针的区别:

  • 数组名是一个“常量指针”,即它的值是固定的,不能被修改。
  • 普通指针可以随意指向其他地址,但数组名不能改变,始终指向数组的第一个元素。

请看下面这个例子:

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

在这里插入图片描述

1.2 数组名和指针的关系

虽然数组名可以“退化”为指针,但它并不是一个真正的指针。具体区别包括:

  • 地址不可修改:数组名的地址是固定的,而普通指针可以修改所指向的地址。例如:

    int arr[5];
    int *p = arr;   // 合法,指针p指向数组第一个元素
    p = p + 1;      // 合法,p可以偏移到下一个元素
    arr = arr + 1;  // 错误,数组名不能修改
    
  • sizeof操作符的区别sizeof(arr)表示数组整个长度,单位为字节,而sizeof(p)表示指针本身的长度。例如:

    int arr[5];
    int *p = arr;
    printf("%lu\n", sizeof(arr)); // 输出整个数组的字节大小,比如5*sizeof(int)
    printf("%lu\n", sizeof(p));   // 输出指针大小,通常为4或8字节
    

    在这里插入图片描述

  • &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)

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

    在这里插入图片描述

&arr 是数组整体的地址。&arr + 1:跳过整个数组后的地址。而&arr[0]arr 都是首元素的地址,+1就是跳过一个元素。

2. 使用指针访问数组

假设有一个整数数组 arr

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

我们可以通过以下两种方式访问数组中的元素:

  • 使用数组下标表示法:arr[i]
  • 使用指针和解引用操作:*(arr + i)

例如:

printf("%d\n", arr[2]);       // 输出3,使用数组下标
printf("%d\n", *(arr + 2));   // 输出3,使用指针解引用

这里的 arr + 2 表示数组首地址加上 2 个元素的偏移量,通过解引用操作符 * 可以访问该地址处的元素值。

2.1 使用指针变量访问数组

我们也可以使用一个指针变量来访问数组元素。代码如下:

#include <stdio.h>
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 指针 p 指向数组的首元素

for (int i = 0; i < 5; i++)
{
printf("%d ", *(p + i)); // 输出每个元素的值
}
return 0;
}

在这里插入图片描述

这里 p 是指向数组首元素的指针,*(p + i) 就是访问数组第 i 个元素的值。

数组名arr是数组首元素的地址,可以赋值给p,其实数组名arrp在这里是等价的。我们可以使用arr[i]可以访问数组的元素,那p[i]也可以访问数组。

#include <stdio.h>
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 指针 p 指向数组的首元素

for (int i = 0; i < 5; i++)
{
printf("%d ", p[i]); // 输出每个元素的值
        //写成 printf("%d ", arr[i]); 也是一样的
}
return 0;
}

在这里插入图片描述

arr[i] 是等价于 *(arr+i),数组元素的访问在编译器处理的时候,也是转换成首元素的地址+偏移量求出元素的地址,然后解引用来访问的。(也就是说你写成i[p]或者i[arr]也是可以的,但不建议这么做)

2.2 指针的自增、自减操作

使用指针可以轻松地遍历整个数组,指针自增操作特别有用。例如:

#include <stdio.h>
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 指针 p 指向数组的首元素

for (int i = 0; i < 5; i++)
{
printf("%d ", *p); // 输出当前指针所指的元素
p++;   // 指针移动到下一个元素
}
return 0;
}

在这里插入图片描述

在循环中,每次 p++ 都会将指针移动到下一个元素的位置。这样做能够有效地访问数组中的每个元素,而不需要使用下标。

3. 一维数组传参的本质

3.1 数组传参的基础知识

在C语言中,数组不能作为整个对象传递给函数(即无法直接传递整个数组),而是通过“指向数组首元素的指针”传递。具体表现如下:

void func(int arr[]) {
    // 这里的 arr 实际上是一个指向 int 类型的指针
}

当一维数组arr作为参数传递给函数func时,arr会退化为指针,指向数组的第一个元素。因此,func函数接收到的并不是整个数组,而是一个指针,指向传入数组的第一个元素地址。

3.2 数组名的退化(Decaying)

数组传参过程中,数组名会退化为指针。这一现象称为“数组退化”或“数组衰退”。退化的具体含义如下:

  • 数组名(如arr)在表达式中,除了在sizeof&*等操作符下以及初始化定义时会特殊处理外,其他情况下都会退化为一个指针,指向数组的第一个元素。
  • 在函数参数列表中,一维数组的数组名会被自动视作一个指向数组第一个元素的指针。

例如:

#include <stdio.h>

void func(int arr[10]) {
    printf("Size of arr in func: %lu\n", sizeof(arr));  // 输出指针大小
}

int main()
{
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
func(arr);
return 0;
}

在这里插入图片描述

虽然函数定义中arr[10]似乎表明传入了一个大小为10的数组,但arr实际上是一个指针,所以sizeof(arr)只会得到指针的大小(例如4或8字节),而不是整个数组的大小。

3.3 数组参数和指针参数的等价性

因为数组会退化为指针,所以在函数参数列表中,int arr[]int *arr是等价的。它们都表示“一个指向int类型的指针”。例如,以下两种定义方式是等价的:

void func1(int arr[]);
void func2(int *arr);

在这两种情况下,arr都代表一个指针,指向数组首元素的地址。

3.4 通过指针访问数组元素

既然数组传参时退化为指针,那么在函数内部,可以通过指针的解引用来访问数组元素。以下代码示例说明了这一点:

#include <stdio.h>
void func(int *arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", *(arr + i));  // 通过指针解引用访问数组元素
    }
    printf("\n");
}

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    func(arr, 5);  // 将数组传递给函数
    return 0;
}

在这里插入图片描述

func中,arr是一个指针,通过*(arr + i)解引用可以访问数组的元素。

3.5 传递数组的长度

因为数组传参时只传递了数组的首地址,函数无法自动得知数组的长度。所以通常需要将数组的长度作为额外的参数传递给函数,以便在函数中正确地操作数组。上例中的size参数就是传递的数组长度。

3.6 sizeof的陷阱

在数组传参的场景中,sizeof操作符往往不能按预期返回数组的大小,而是返回指针的大小。这是因为传递的只是一个指针,而不是数组本身。

#include <stdio.h>
void func(int arr[]) {
    printf("Size of arr in func: %lu\n", sizeof(arr));  // 返回指针的大小
}

int main() {
    int arr[10];
    printf("Size of arr in main: %lu\n", sizeof(arr));  // 返回整个数组的大小
    func(arr);
    return 0;
}

在这里插入图片描述

main中,sizeof(arr)会返回整个数组的大小,比如10 * sizeof(int)。而在func中,sizeof(arr)返回的只是一个指针的大小(如4或8字节),因为arr在函数中已经退化为一个指针。

3.7 使用const限定符保护数组

如果数组在函数中只需要读取而不修改,使用const修饰指针是一个好的做法,这样可以防止函数内部意外修改数组的内容。例如:

void printArray(const int *arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

在这种情况下,如果在printArray函数中试图修改arr的元素,编译器会报错。

3.8 使用数组指针传递固定大小的数组

有时我们希望在函数中传递一个固定大小的数组,这时可以使用数组指针。例如:

void func(int (*arr)[10]) {
    // 这里的 arr 是一个指向包含10个整数的数组的指针
}

这里的int (*arr)[10]表示arr是一个指向包含10个整数的数组的指针,传递的数组必须是大小为10的数组。

总结

  • 数组传参本质:一维数组作为参数传递给函数时,数组名会退化为指向数组首元素的指针。函数接收到的是数组第一个元素的地址,而不是整个数组。
  • 等价写法:在函数参数中,int arr[]int *arr是等价的,均表示一个指向int类型的指针。
  • 数组长度:由于函数只接收到数组首地址,因此无法得知数组的长度,通常需要传递一个额外的参数表示数组长度。
  • sizeof的差异:在函数外sizeof数组会返回整个数组的大小,而在函数内则返回指针的大小。
  • const修饰:当函数不需要修改数组内容时,使用const修饰指针可以保护数组内容。

4. 冒泡排序

4.1 冒泡排序的基本原理

冒泡排序的核心思想是比较相邻的元素并交换它们的位置,使得每一轮遍历后,最大的元素“冒泡”到数组的末尾。这种过程会不断重复,直到整个数组有序。

冒泡排序的步骤可以分解为以下几点:

  1. 从数组的第一个元素开始,依次比较相邻的两个元素。
  2. 如果前一个元素比后一个元素大,则交换它们的位置。
  3. 完成一轮遍历后,最大的元素会被移到数组的末尾。
  4. 接下来,忽略已排序的末尾元素,重复上述步骤,直到所有元素有序。

4.2 冒泡排序的实现步骤

假设我们有一个数组arr,它包含n个待排序的元素。冒泡排序的主要实现步骤如下:

  • 外层循环控制要执行多少轮遍历,通常需要n-1轮。
  • 内层循环用于在当前轮次中依次比较相邻的元素。每次比较时,如果前面的元素比后面的元素大,就交换这两个元素。
  • 每轮遍历结束后,最大的未排序元素就会被移动到未排序区域的末尾。

4.3 冒泡排序的代码实现

以下是一个用C语言实现的标准冒泡排序代码:

#include <stdio.h>

void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {           // 外层循环,执行n-1轮
        for (int j = 0; j < n - 1 - i; j++) {   // 内层循环,逐个比较相邻元素
            if (arr[j] > arr[j + 1]) {          // 如果前一个元素比后一个大,交换
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

void printArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int arr[] = {5, 2, 9, 1, 5, 6};
    int n = sizeof(arr) / sizeof(arr[0]);
    bubbleSort(arr, n);
    printf("Sorted array: ");
    printArray(arr, n);
    return 0;
}

代码解释

  1. 外层循环for (int i = 0; i < n - 1; i++)
    • 控制排序的轮数。每轮排序可以将当前最大的元素放到未排序部分的末尾。
    • 因为最后一个元素不需要再比较,因此需要n-1轮。
  2. 内层循环for (int j = 0; j < n - 1 - i; j++)
    • 内层循环用于遍历未排序的部分,比较并交换相邻的元素。
    • 每轮排序后,最大值就会固定在数组末尾,所以内层循环的上限是n - 1 - i,逐渐减少已排序的部分。
  3. 交换元素
    • if (arr[j] > arr[j + 1]):如果当前元素arr[j]比下一个元素arr[j + 1]大,就交换它们,确保大的元素向数组的后端“冒泡”。
  4. 打印数组
    • printArray函数用于打印数组内容,便于观察排序结果。

4.4 运行结果

运行以上代码,输出结果如下:

Sorted array: 1 2 5 5 6 9

4.5 冒泡排序的优化

经典的冒泡排序可以进行优化,以减少不必要的比较和交换操作。以下是两种常用的优化方法:

优化一:检测是否已排序

如果在某一轮中没有发生交换,说明数组已经有序,可以提前终止排序。这种优化可以大大减少在接近有序数组上的比较次数。

修改代码如下:

void bubbleSortOptimized(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        int swapped = 0;  // 标记是否发生交换
        for (int j = 0; j < n - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                swapped = 1;  // 发生交换,更新标记
            }
        }
        if (swapped == 0) {  // 如果没有交换,提前终止
            break;
        }
    }
}

在这个优化版本中,如果在某轮遍历中没有发生交换,swapped保持为0,表示数组已经有序,立即退出循环。这样可以避免不必要的轮次。

优化二:双向冒泡排序(鸡尾酒排序)

双向冒泡排序(Cocktail Shaker Sort)是一种双向冒泡的算法。它先从左到右遍历一遍将最大元素移动到右端,再从右到左遍历一遍将最小元素移动到左端。这样可以在较少的轮次内使数组有序。

void cocktailShakerSort(int arr[], int n) {
    int swapped = 1;
    int start = 0;
    int end = n - 1;

    while (swapped) {
        swapped = 0;

        // 从左到右进行一轮冒泡
        for (int i = start; i < end; i++) {
            if (arr[i] > arr[i + 1]) {
                int temp = arr[i];
                arr[i] = arr[i + 1];
                arr[i + 1] = temp;
                swapped = 1;
            }
        }

        // 如果没有发生交换,数组已经有序
        if (!swapped)
            break;

        // 调整终止位置
        end--;

        swapped = 0;

        // 从右到左进行一轮冒泡
        for (int i = end - 1; i >= start; i--) {
            if (arr[i] > arr[i + 1]) {
                int temp = arr[i];
                arr[i] = arr[i + 1];
                arr[i + 1] = temp;
                swapped = 1;
            }
        }

        // 调整起始位置
        start++;
    }
}

4.6 冒泡排序的时间复杂度

  • 时间复杂度:
    • 最坏情况:当数组逆序时,每一轮都需要比较和交换,时间复杂度为 O(n^2)。
    • 最佳情况:当数组已经有序时(经过优化后的版本),只需一轮即可结束排序,时间复杂度为 O(n)。
  • 空间复杂度:冒泡排序是原地排序算法,只需一个额外的交换变量,空间复杂度为 O(1)。

4.7 冒泡排序的优缺点

优点

  • 算法简单,容易实现。
  • 对于小规模数据排序,效果还可以。
  • 稳定性好,因为相等的元素不会改变相对顺序。

缺点

  • 效率较低,时间复杂度较高,不适合大规模数据排序。
  • 即使数据基本有序,若未优化,每次都会执行完整的循环。

5. 二级指针

5.1 二级指针的基本概念

二级指针是指向一级指针的指针,即一个指针变量存储的是另一个指针变量的地址。对于一个变量 a,它的地址由一级指针 p 存储,而 p 的地址可以由二级指针 pp 存储。我们可以这样表示:

  • a:基本变量。
  • p = &ap 是一级指针,指向 a 的地址。
  • pp = &ppp 是二级指针,指向指针 p 的地址。

在C语言中,二级指针的定义方式如下:

int **pp;

这里 pp 是一个二级指针,类型为 int**,它可以存储一个 int* 类型的指针的地址。

5.2 二级指针的声明与初始化

要正确使用二级指针,首先需要理解如何声明和初始化它。

int a = 10;
int *p = &a;   // 定义一级指针,指向变量 a
int **pp = &p; // 定义二级指针,指向指针 p 的地址

在这里插入图片描述

在这个例子中:

  • a 是一个 int 类型变量,值为 10
  • p 是一个一级指针,指向 a,存储了 a 的地址。
  • pp 是一个二级指针,指向 p,存储了 p 的地址。

访问变量 a 的值可以通过以下三种方式:

  • 直接访问:a
  • 通过一级指针访问:*p
  • 通过二级指针访问:**pp

示例代码

#include <stdio.h>

int main() {
    int a = 10;
    int *p = &a;   // 一级指针,指向变量 a
    int **pp = &p; // 二级指针,指向指针 p 的地址

    printf("Value of a: %d\n", a);
    printf("Value of a using *p: %d\n", *p);
    printf("Value of a using **pp: %d\n", **pp);

    return 0;
}

输出结果

Value of a: 10
Value of a using *p: 10
Value of a using **pp: 10

在这个例子中,通过 **pp 访问到 a 的值,因为 pp 是一个指向指针的指针。

5.3 二级指针的内存布局

了解二级指针的内存布局,有助于理解其访问方式和行为。在内存中,一个二级指针指向一级指针的地址,而一级指针指向基本数据类型的地址。

假设有以下代码:

int a = 10;
int *p = &a;
int **pp = &p;

内存布局如下:

变量值(地址)指向
a10基本值
p地址1(指向a)&a
pp地址2(指向p)&p
  • a 存储值 10
  • p 存储 a 的地址。
  • pp 存储 p 的地址。

访问 **pp 会经过两次解引用操作,最终访问到 a 的值 10

6. 指针数组

在C语言中,指针数组是一个数组,其中每个元素都是一个指针。指针数组可以用来存储指向不同数据的地址,特别适合管理一组字符数组(字符串)或在函数中动态管理内存地址。理解指针数组是学习C语言指针的一个重要部分。

6.1 什么是指针数组

指针数组可以简单理解为:数组的每个元素都是一个指针。在C语言中,指针数组的声明形式如下:

int *arr[5];

这里的arr是一个数组,数组的元素类型是int*(指向int类型的指针)。arr数组包含5个元素,因此是一个含有5个指向整数的指针的数组。

需要注意的是:指针数组与数组指针不同。指针数组是“数组的每个元素是指针”;而“数组指针”是一个指针,它指向一个数组。

6.2 指针数组的声明与初始化

指针数组可以在声明的同时初始化。以下是几个示例:

示例1:整数指针数组

int a = 10, b = 20, c = 30;
int *arr[3] = { &a, &b, &c }; // 定义一个指针数组,包含3个指针,分别指向a, b, c

在这里插入图片描述

在这个例子中:

  • arr[0] 是指向 a 的指针,即 &a
  • arr[1] 是指向 b 的指针,即 &b
  • arr[2] 是指向 c 的指针,即 &c

可以通过指针数组访问和修改 abc 的值:

*arr[0] = 15; // 修改 a 的值为 15
*arr[1] = 25; // 修改 b 的值为 25
*arr[2] = 35; // 修改 c 的值为 35

示例2:字符指针数组(字符串数组)

指针数组常用于存储一组字符串:

const char *words[3] = { "Hello", "World", "C language" };

在这里,words 是一个指针数组,其中每个元素指向一个字符串的首地址。可以像访问二维字符数组一样访问每个字符串:

printf("%s\n", words[0]); // 输出 "Hello"
printf("%s\n", words[1]); // 输出 "World"
printf("%s\n", words[2]); // 输出 "C language"

6.3 指针数组的访问方式

指针数组中的每个元素都是一个指针,存储的是一个地址。可以通过数组下标访问指针数组中的指针,然后通过解引用操作符*访问指针指向的值。

例如:

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

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

访问方式可以总结为:

  • arr[i] 表示指针数组中第 i 个指针元素。
  • *arr[i] 表示第 i 个指针元素所指向的值。

6.4 指针数组在函数参数中的使用

在C语言中,指针数组可以作为函数的参数传递,这在需要传递一组字符串时非常有用。例如:

#include <stdio.h>

void printWords(const char *arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%s\n", arr[i]);
    }
}

int main() {
    const char *words[] = { "Apple", "Banana", "Cherry" };
    int size = sizeof(words) / sizeof(words[0]);
    printWords(words, size);
    return 0;
}

在这里插入图片描述

代码解释

  • printWords 函数接收一个指针数组 arr,每个元素是一个指向字符串的指针。
  • main 函数中创建了一个字符指针数组 words,每个元素指向一个字符串。
  • 通过printWords函数输出每个字符串。

7. 指针数组模拟二维数组

在C语言中,二维数组是一个常用的数据结构,通常用于存储表格形式的数据。一般而言,二维数组在内存中是连续分配的,元素按行优先顺序存储。然而,有时我们可能希望创建一种更加灵活的“二维数组”,例如每行的列数可以不同,或者希望避免在程序运行时动态分配内存。在这种情况下,指针数组是一种非常有效的替代方式。

7.1 二维数组的基本概念

一个二维数组通常定义为:

int arr[3][4];

这个数组有 3 行 4 列,每个元素占用sizeof(int)字节,内存布局上每行的元素是连续存储的。二维数组的访问方式为arr[i][j],表示访问第i行第j列的元素。

7.2 指针数组的基本概念

指针数组是一个数组,数组的每个元素是一个指针。可以定义指向不同数据类型的指针数组,例如:

int *arr[3];

上面的代码定义了一个包含 3 个元素的指针数组arr,每个元素都是一个指向int类型数据的指针。指针数组中的每个指针可以指向不同的内存地址,这就带来了极大的灵活性,使其能够模拟二维数组。

7.3 指针数组模拟二维数组的原理

指针数组模拟二维数组的核心思想是:将二维数组的每一行作为一个独立的一维数组,然后通过指针数组来存储每行数组的地址。这样就形成了一个二维结构:

  1. 每一行可以是一个单独的一维数组(可以大小不同)。
  2. 使用一个指针数组来存储每行数组的首地址。
  3. 通过双重下标 arr[i][j] 来访问每个元素。

7.4 指针数组模拟二维数组的实现步骤

假设我们要创建一个 3 行 5 列的二维数组,可以使用以下步骤:

  1. 定义若干个一维数组,每个数组代表二维数组的一行。
  2. 定义一个指针数组,存储每一行数组的首地址。
  3. 使用 arr[i][j] 的方式访问二维结构中的元素。

7.5 代码示例

以下代码示例展示了如何使用指针数组来模拟一个 3 行 5 列的二维数组:

#include <stdio.h>

int main() {
    // 定义三个一维数组,每个数组包含5个整数
    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};

    // 访问二维数组元素
    for (int i = 0; i < 3; i++) {          // 外层循环遍历每一行
        for (int j = 0; j < 5; j++) {      // 内层循环遍历每一列
            printf("%d ", parr[i][j]);     // 使用parr[i][j]访问元素
        }
        printf("\n");                      // 每行输出完后换行
    }

    return 0;
}

在这里插入图片描述

代码解析

  1. 定义一维数组

    int arr1[] = {1, 2, 3, 4, 5};
    int arr2[] = {2, 3, 4, 5, 6};
    int arr3[] = {3, 4, 5, 6, 7};
    

    这里定义了三个一维数组 arr1arr2arr3,每个数组包含 5 个整数元素。可以理解为二维数组的三行数据。

  2. 定义指针数组

    int *parr[3] = {arr1, arr2, arr3};
    
    • parr 是一个指针数组,包含 3 个元素,每个元素都是一个 int* 类型的指针。
    • 每个指针指向一维数组的首地址,因此 parr[0] 指向 arr1parr[1] 指向 arr2parr[2] 指向 arr3
    • 这样 parr 就相当于一个模拟的二维数组,有 3 行 5 列。
  3. 访问元素

    for (int i = 0; i < 3; i++) {          
        for (int j = 0; j < 5; j++) {      
            printf("%d ", parr[i][j]); 
        }
        printf("\n");
    }
    
    • parr[i][j] 访问第 i 行、第 j 列的元素。由于 parr[i] 是指向第 i 行的指针,所以 parr[i][j] 可以直接访问对应元素。
    • 这种访问方式与普通二维数组完全一致,代码更加简洁直观。

    运行结果

    1 2 3 4 5
    2 3 4 5 6
    3 4 5 6 7
    

7.6 指针数组模拟二维数组的优势

  1. 灵活性:指针数组中的每个指针可以指向不同大小的数组。因此,指针数组模拟的“二维数组”不要求每行的长度相同,可以轻松实现不规则的二维数据结构。
  2. 内存占用优化:不需要预先分配固定大小的内存,在不需要动态内存分配的情况下实现多维结构的灵活管理。
  3. 简洁的语法:可以使用类似二维数组的语法(如 arr[i][j])来访问元素,代码更清晰。

7.7 指针数组模拟不规则二维数组的示例

假设我们需要一个不规则的“二维数组”,其中每行的元素数量不同,例如:第一行有 3 个元素,第二行有 4 个元素,第三行有 5 个元素。可以按照以下方式实现:

#include <stdio.h>

int main() {
    // 定义不规则的一维数组
    int arr1[] = {1, 2, 3};
    int arr2[] = {4, 5, 6, 7};
    int arr3[] = {8, 9, 10, 11, 12};

    // 定义指针数组,指向不同大小的数组
    int *parr[3] = {arr1, arr2, arr3};

    // 定义每一行的列数
    int cols[] = {3, 4, 5};

    // 访问并打印不规则的二维数组
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < cols[i]; j++) {
            printf("%d ", parr[i][j]);
        }
        printf("\n");
    }

    return 0;
}
输出结果
1 2 3
4 5 6 7
8 9 10 11 12

7.8 指针数组模拟二维数组的注意事项

  1. 内存管理:在静态定义的数组场景中,指针数组模拟的二维数组不需要释放内存。如果使用动态分配的内存(如 malloc),则需要在使用结束时释放每行的内存。
  2. 边界检查:由于每行的长度可以不同,在访问 parr[i][j] 时需要确保 j 不超过当前行的列数,避免越界访问。
  3. 数组和指针的区别:指针数组和真正的二维数组在内存布局上不同。真正的二维数组内存是连续的,而指针数组每行的内存可以不连续,因此指针数组不适合在要求连续内存的情况下使用。

7.9 总结

通过指针数组模拟二维数组的方式具有较高的灵活性,适用于需要动态、灵活存储的数据结构。在C语言中,指针数组为管理不规则数据结构提供了一种高效、简便的解决方案。

核心要点

  1. 定义指针数组:指针数组可以存储每行一维数组的地址,形成二维数据结构。
  2. 灵活存储:指针数组可以实现不规则的二维数组结构,不要求每行长度一致。
  3. 访问方便:可以使用类似二维数组的方式(arr[i][j])访问指针数组中的数据。

指针数组模拟二维数组的方法非常适合在大小固定或不规则的场景下使用。在编写代码时,理解其内存布局和访问方式将帮助高效地利用指针数组来管理数据。

8. 字符指针变量

在C语言中,字符指针是一种用于指向字符(char)数据类型的指针变量。字符指针在处理字符串、文本数据以及动态字符数组方面具有广泛的应用。理解字符指针的定义、初始化、操作及其在字符串处理中的应用,有助于掌握C语言中关于字符串和指针的基本操作。

8.1 字符指针的定义与初始化

字符指针的定义和初始化与其他类型的指针类似。可以通过以下方式声明一个字符指针变量:

char *ptr;

这里的ptr是一个字符指针变量,用于存储字符型数据的地址。通过*ptr可以访问指针指向的字符数据。

字符指针可以指向一个字符变量,也可以指向一个字符串字面值。例如:

char c = 'A';
char *ptr = &c;    // 指向字符变量
char *str = "Hello, world!";  // 指向字符串字面量

在这段代码中:

  • ptr 是一个字符指针,存储了字符变量 c 的地址。
  • str 是一个字符指针,指向字符串 "Hello, world!" 的首地址。

8.2 字符指针和字符串

在C语言中,字符串是一组字符组成的数组,并以空字符 \0(null character)结尾。因此,字符串的存储实际上是一维字符数组。字符指针可以指向字符串的首地址,通过指针的解引用和指针运算访问字符串中的每一个字符。

8.2.1 字符串字面值与字符指针

字符串字面值(如 "Hello")在程序中以只读形式存储在内存中,可以用字符指针指向它。比如:

char *str = "Hello";

这里的str指向字符串字面量 "Hello" 的首地址。在这种情况下,不能修改字符串的内容,因为字符串字面值通常存储在只读内存区域(如代码段),对其进行修改会导致未定义行为。

8.2.2 字符数组与字符指针

字符数组是一块可读写的内存,字符指针也可以指向字符数组的首地址。与指向字符串字面值的指针不同,通过字符指针可以修改字符数组的内容。

char arr[] = "Hello";
char *ptr = arr;

这里 arr 是一个字符数组,ptr 指向该字符数组的首地址。此时可以通过 ptr 修改数组中的字符。

8.3 字符指针的常见用法

字符指针在C语言中的主要用途之一是用于字符串的处理。以下是一些常见的用法:

8.3.1 访问字符串中的字符

字符指针可以用于遍历字符串中的字符:

char *str = "Hello, world!";
while (*str != '\0') {  // 当指向的字符不是空字符时继续循环
    printf("%c ", *str);  // 输出当前字符
    str++;  // 移动指针到下一个字符
}

在这段代码中,str 指针指向字符串的首地址。每次通过 *str 访问当前字符,然后 str++ 将指针移动到下一个字符,直到遇到字符串的结束标志 '\0'

8.3.2 修改字符数组中的内容

如果字符指针指向一个字符数组(而非字符串字面值),则可以通过指针修改数组中的字符:

char arr[] = "Hello";
char *ptr = arr;

ptr[0] = 'h';  // 修改第一个字符为小写 'h'
printf("%s\n", arr);  // 输出 "hello"

这里 ptr 指向字符数组 arr 的首地址,通过 ptr[0] 可以修改字符数组的内容。

8.4 字符指针与指针运算

字符指针支持各种指针运算,这使得它在处理字符串时更加灵活:

8.4.1 移动指针访问字符

字符指针可以通过增加或减少来指向下一个或上一个字符:

char *str = "Hello";
printf("%c\n", *str);    // 输出 H
printf("%c\n", *(str + 1)); // 输出 e

通过 str + 1,我们可以将指针移动到字符串的下一个字符 e 并访问它。

8.4.2 计算字符串的长度

可以通过字符指针遍历字符串来计算字符串的长度:

char *str = "Hello, world!";
int length = 0;

while (*str != '\0') {  // 当指向的字符不是空字符时
    length++;
    str++;
}

printf("Length: %d\n", length);  // 输出字符串长度

在这个例子中,每次 str++ 将指针向后移动一位,直到到达字符串末尾的空字符 '\0'

8.5 字符指针的应用示例

5.1 字符串复制

可以使用字符指针复制字符串内容:

#include <stdio.h>

void stringCopy(char *dest, const char *src) {
    while (*src != '\0') {
        *dest = *src;  // 拷贝字符
        dest++;
        src++;
    }
    *dest = '\0';  // 添加字符串结尾的空字符
}

int main() {
    char src[] = "Hello";
    char dest[10];
    
    stringCopy(dest, src);
    printf("Copied string: %s\n", dest);  // 输出 "Hello"
    
    return 0;
}

在这个例子中,stringCopy 函数使用字符指针 srcdest 依次拷贝源字符串到目标字符串中。

5.2 字符串拼接

可以使用字符指针将两个字符串拼接到一起:

#include <stdio.h>

void stringConcat(char *dest, const char *src) {
    while (*dest != '\0') {
        dest++;  // 移动到dest字符串的末尾
    }
    while (*src != '\0') {
        *dest = *src;  // 拷贝src的字符到dest
        dest++;
        src++;
    }
    *dest = '\0';  // 添加字符串结束符
}

int main() {
    char dest[20] = "Hello";
    char src[] = " World";
    
    stringConcat(dest, src);
    printf("Concatenated string: %s\n", dest);  // 输出 "Hello World"
    
    return 0;
}

在这个代码中,stringConcat 函数将字符串 src 拼接到 dest 之后。

8.6 常见的字符指针错误

8.6.1 修改字符串字面量

字符串字面量通常存储在只读内存区中,使用字符指针指向字符串字面量时,不应修改它的内容,否则会引发未定义行为。

char *str = "Hello";
str[0] = 'h';  // 错误:尝试修改只读内存

8.6.2 指针未初始化

如果字符指针未初始化,直接使用会导致不可预知的行为。

char *str;
str[0] = 'H';  // 错误:未初始化的指针

8.7 小练习

#include <stdio.h>
int main()
{
    char str1[] = "hello bit.";
    char str2[] = "hello bit.";
    const char *str3 = "hello bit.";
    const char *str4 = "hello bit.";

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

    return 0;
}

8.7.1 运行结果

str1 and str2 are not same
str3 and str4 are same

8.7.2 代码分析

8.7.2.1 定义部分
  • char str1[] = "hello bit.";char str2[] = "hello bit.";

    • str1str2 是两个字符数组,分别初始化为 "hello bit."
    • 当用字符数组初始化字符串时,每次定义一个新的数组,编译器会在内存中为每个数组分配独立的空间并复制字符串内容。也就是说,str1str2 各自占据了一块不同的内存。
  • const char *str3 = "hello bit.";const char *str4 = "hello bit.";

    • str3str4 是指向字符串字面量的指针。
    • 字符串字面量 "hello bit." 存储在常量区(只读区域),多个指针指向同一个字符串字面量时,编译器通常会优化,将它们指向同一个内存地址。因此,str3str4 很可能指向的是相同的内存地址(指向的是同一个字符串字面量)。
8.7.2.2 比较部分
  1. if (str1 == str2)
    • str1str2 是两个不同的字符数组,虽然它们的内容相同,但它们的地址不同。
    • str1 == str2 比较的是两个数组的首地址,结果是 false,所以输出 str1 and str2 are not same
  2. if (str3 == str4)
    • str3str4 是指向字符串字面量的指针。
    • 因为编译器会将相同的字符串字面量存储在相同的内存区域(编译器优化),str3str4 很可能指向相同的地址。
    • str3 == str4 结果是 true,所以输出 str3 and str4 are same

8.8 总结

字符指针在C语言中主要用于字符串操作,灵活而强大。理解字符指针的定义、初始化和各种操作,可以帮助我们更高效地处理字符数据。要点总结如下:

  • 字符指针的定义char *ptr 是一个字符指针,用于存储字符数据的地址。
  • 指向字符串:字符指针可以指向字符串字面量,也可以指向字符数组的首地址。
  • 操作字符串:字符指针支持各种字符串操作,如遍历、复制、拼接等。
  • 指针运算:字符指针可以通过增加或减少操作来指向下一个或上一个字符。
  • 注意事项:字符串字面量不可修改,字符指针需要初始化。

9. 数组指针变量

在C语言中,“数组指针”是一种指向数组的指针变量。数组指针变量的类型声明与普通指针变量略有不同,主要用于指向一个固定大小的数组。数组指针在多维数组的操作、函数参数传递以及复杂数据结构处理中非常有用。

9.1 数组指针变量是什么?

数组指针(Pointer to an Array)是一个指向数组的指针,即它存储的是数组的首地址,但它知道自己指向的不是单个元素,而是一个包含固定数量元素的数组。

  • 普通指针:普通指针(如 int *p)指向单个数据类型的变量,操作时每次跳过一个数据类型的大小。
  • 数组指针:数组指针(如 int (*p)[10])指向一个包含多个元素的数组,操作时每次跳过整个数组的大小。例如,int (*p)[10] 是一个指向包含 10 个 int 类型元素的数组的指针。

数组指针的主要特点是:

  • 数组大小固定:数组指针指向一个固定大小的数组,其类型包含了数组的长度信息。
  • 访问多个元素:通过数组指针可以直接访问数组中的多个元素。

数组指针通常用于二维数组或更高维数组的操作,也常用于函数传递固定大小的数组。

9.2 数组指针变量怎么初始化

定义一个数组指针时,需要在声明中指定数组的元素类型和长度,然后将其指向一个数组。数组指针的初始化方法如下:

示例1:定义和初始化一个数组指针

int arr[10];           // 定义一个包含 10 个元素的数组
int (*p)[10] = &arr;   // 定义一个数组指针 p,指向包含 10 个元素的数组 arr

在这里插入图片描述

在这个例子中:

  • int (*p)[10] 是一个数组指针,指向包含 10 个 int 类型元素的数组。
  • p 存储的是数组 arr 的地址,通过 p 可以访问 arr 中的元素。

示例2:数组指针用于二维数组

数组指针可以用于指向二维数组的行,从而方便地遍历或操作二维数组。例如:

int arr[3][5] = {       // 定义一个 3x5 的二维数组
    {1, 2, 3, 4, 5},
    {6, 7, 8, 9, 10},
    {11, 12, 13, 14, 15}
};
int (*p)[5] = arr;      // 定义一个数组指针 p,指向二维数组的每一行(5 个元素)

在这个例子中:

  • int (*p)[5] 是一个数组指针,指向包含 5 个 int 类型元素的数组(二维数组的一行)。
  • 通过 p[i][j] 可以访问二维数组 arr 中的元素。

示例3:使用数组指针遍历二维数组

#include <stdio.h>

int main() {
    int arr[3][5] = { 
        {1, 2, 3, 4, 5}, 
        {6, 7, 8, 9, 10}, 
        {11, 12, 13, 14, 15} 
    };
    int (*p)[5] = arr;   // 数组指针指向二维数组

    // 使用数组指针遍历二维数组
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 5; j++) {
            printf("%d ", p[i][j]);
        }
        printf("\n");
    }

    return 0;
}

在这个代码中:

  • int (*p)[5] 是一个指向包含 5 个元素数组的指针,可以通过 p[i][j] 访问二维数组 arr 中的元素。
  • p[i] 表示二维数组的第 i 行,p[i][j] 表示第 i 行的第 j 个元素。

9.3 总结

  • 数组指针是指向数组的指针,通常用于指向具有固定大小的数组。
  • 数组指针的声明中需要指定数组的大小,如 int (*p)[10] 表示指向包含 10 个 int 元素的数组的指针。
  • 数组指针常用于操作二维数组或更高维数组,尤其是在函数参数传递中,能够更有效地操作数组的多行数据。
  • 初始化时,可以将数组指针指向一个具有相同大小的数组。

10. 二维数组传参的本质

在C语言中,将二维数组作为参数传递给函数时,它的本质是将指向数组的指针传递给函数。二维数组传参涉及到指针的退化和数组内存布局的理解。

10.1 二维数组的内存布局

首先,了解二维数组的内存布局有助于理解它在传参时的表现。在C语言中,二维数组的元素在内存中是按行优先顺序(Row-Major Order)存储的。比如,对于一个二维数组 int arr[3][4],它的内存布局如下:

arr[0][0] arr[0][1] arr[0][2] arr[0][3]
arr[1][0] arr[1][1] arr[1][2] arr[1][3]
arr[2][0] arr[2][1] arr[2][2] arr[2][3]

arr 表示一个 3 行 4 列的二维数组。它在内存中的排列是连续的,每一行的元素依次排列,接着是下一行的元素,直到最后一个元素。

10.2 二维数组传参的本质:指针退化

在C语言中,数组传参时会发生“指针退化”(decaying to pointer),即数组名会退化为指向其首元素的指针。对于二维数组 arr[3][4] 来说,传递时会退化成指向数组 arr[0] 的指针,而 arr[0] 本身是一个包含 4 个 int 类型元素的一维数组。因此,传参时,arr 的类型实际上是 int (*)[4],即指向一个包含 4 个 int 元素的数组的指针

也就是说,int arr[3][4] 传递给函数后变成了 int (*arr)[4]

10.3 二维数组的函数参数定义

要在函数中接收一个二维数组,我们可以定义一个“数组指针”来接收它。例如,对于一个 int arr[3][4] 数组,可以定义如下函数来接收它:

void printArray(int (*arr)[4], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

在这里,int (*arr)[4] 表示 arr 是一个指针,指向一个包含 4 个整数的数组,即二维数组的一行。函数 printArray 通过行指针的方式访问二维数组的每个元素。

为什么不是 int **arr

由于二维数组的内存是连续分配的,因此不能用 int **arr 来接收二维数组。int **arr 表示一个指向指针的指针,可以用来指向一个指针数组(即每一行在内存中可能是分开的),而二维数组的每一行是连续存储的。使用 int **arr 会导致编译器无法正确访问二维数组的内存布局。

10.4 传参示例代码

以下代码展示了如何将二维数组传递给函数,并在函数中访问数组的每个元素:

#include <stdio.h>

void printArray(int (*arr)[4], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

int main() {
    int arr[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    printArray(arr, 3);  // 将二维数组传递给函数
    return 0;
}
输出结果
1 2 3 4
5 6 7 8
9 10 11 12

在这个代码中:

  • arr 作为二维数组传递给 printArray 函数。
  • printArray 函数通过 int (*arr)[4] 参数来接收二维数组,并通过双重下标 arr[i][j] 访问二维数组中的元素。

10.5 常见的二维数组传参方式

除了使用数组指针 int (*arr)[4],在C语言中还有其他一些方式来将二维数组传递给函数:

方式一:明确指定列数

在函数参数中直接指定二维数组的列数。例如:

void printArray(int arr[][4], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

这里 int arr[][4] 表示一个二维数组,虽然未指定行数,但必须指定列数 4,这样编译器就能正确计算每行的步长。

方式二:使用宏定义列数

如果二维数组的列数在多个函数中用到,可以使用宏定义列数,增强代码的可维护性。例如:

#define COLS 4

void printArray(int arr[][COLS], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < COLS; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

在这种情况下,如果列数改变,只需修改 #define COLS 的值即可。

10.6 多维数组传参的通用性

二维数组传参中的列数必须指定,否则编译器无法知道每一行的步长,从而不能正确地访问每行数据。如果需要传递不同大小的二维数组,可以考虑以下两种方法:

  1. 使用可变长度数组(C99标准):C99标准引入了可变长度数组(Variable-Length Array, VLA),可以根据传入的列数动态确定数组步长:

    void printArray(int rows, int cols, int arr[rows][cols]) {
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                printf("%d ", arr[i][j]);
            }
            printf("\n");
        }
    }
    

    在调用时传入行数和列数:

    int arr[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} };
    printArray(3, 4, arr);
    
  2. 手动平铺成一维数组:将二维数组当作一维数组传递,并手动计算索引。例如:

    void printArray(int *arr, int rows, int cols) {
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                printf("%d ", arr[i * cols + j]);
            }
            printf("\n");
        }
    }
    

    调用时使用 printArray((int *)arr, 3, 4);,强制类型转换成 int *

10.7 总结

  • 二维数组传参的本质:二维数组传参本质上是指针传递,但在传参时数组名会退化为指向一维数组的指针。因此,int arr[3][4] 传递给函数时会退化为 int (*arr)[4]
  • 列数必须指定:在函数参数中使用二维数组时,必须指定列数,这样编译器才能正确地计算每行的步长。
  • 使用数组指针接收:二维数组可以通过数组指针来接收,在函数中以 int (*arr)[4] 的形式接收指向二维数组的指针。
  • 灵活性:可以使用C99标准的可变长度数组或手动处理一维数组来实现更灵活的二维数组传参。

11. 函数指针变量

在C语言中,函数指针是一种指向函数的指针,通常用于调用函数的地址。函数指针允许我们动态调用不同的函数,灵活地实现回调机制和函数的参数化。函数指针在处理复杂的结构、实现回调函数和动态函数调用方面非常有用。

11.1 函数指针变量的创建

函数指针的创建包括声明和初始化。函数指针的声明方式类似于普通指针,但它指向的是函数的地址,而不是数据的地址。

函数指针的定义语法

函数指针的定义语法如下:

return_type (*pointer_name)(parameter_list);
  • return_type 是函数的返回类型。
  • pointer_name 是函数指针的名称。
  • parameter_list 是函数的参数列表类型。

例如,声明一个指向返回类型为 int、参数为两个 int 的函数的指针:

int (*func_ptr)(int, int);

在这个例子中,func_ptr 是一个函数指针,它指向一个接受两个 int 类型参数并返回 int 类型的函数。

函数指针的初始化

函数指针的初始化是将函数的地址赋给函数指针。函数名本身就是函数的地址,所以可以直接赋值。例如:

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

int main() {
    int (*func_ptr)(int, int) = add;  // 将函数地址赋给函数指针 (用&add也没有问题)
    return 0;
}

在这段代码中,add 函数的地址被赋值给了 func_ptr,因此 func_ptr 就指向了 add 函数。此时可以通过 func_ptr 调用 add 函数。

11.2 函数指针变量的使用

函数指针的主要用途是动态调用函数,尤其是在函数作为参数传递或实现回调函数时非常有用。以下是函数指针的常见用法。

示例1:通过函数指针调用函数

一旦函数指针指向了特定的函数,就可以通过该指针调用函数。函数指针的调用方法与直接调用函数相似:

#include <stdio.h>

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

int main() {
    int (*func_ptr)(int, int) = add;  // 定义并初始化函数指针
    int result = func_ptr(10, 20);    // 通过函数指针调用 add 函数
    printf("Result: %d\n", result);   // 输出结果:30
    return 0;
}

在这个示例中:

  • func_ptr(10, 20) 调用了 add 函数,并传递了两个参数 1020
  • 通过函数指针 func_ptr 调用函数时,语法与普通函数调用基本相同。

示例2:将函数指针作为参数传递

函数指针可以作为参数传递给其他函数,从而实现动态函数调用。例如,下面的代码定义了一个高阶函数 operate,该函数接受一个函数指针作为参数,可以动态调用不同的函数:

#include <stdio.h>

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

int subtract(int a, int b) {
    return a - b;
}

// operate 函数接受一个函数指针 op 和两个整数参数
int operate(int (*op)(int, int), int x, int y) {
    return op(x, y);
}

int main() {
    int result1 = operate(add, 10, 5);        // 调用 add 函数
    int result2 = operate(subtract, 10, 5);   // 调用 subtract 函数

    printf("Add Result: %d\n", result1);      // 输出:15
    printf("Subtract Result: %d\n", result2); // 输出:5
    return 0;
}

在这个示例中:

  • operate 函数接受一个函数指针 op 和两个整数 xy
  • 可以将 addsubtract 函数的地址传递给 operate 函数,通过 op(x, y) 调用不同的函数,实现动态调用。

示例3:函数指针数组

函数指针还可以存储在数组中,通过数组下标来选择不同的函数调用。这在需要实现多个功能选择时非常方便。例如:

#include <stdio.h>

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

int subtract(int a, int b) {
    return a - b;
}

int multiply(int a, int b) {
    return a * b;
}

int main() {
    // 定义函数指针数组
    int (*operations[3])(int, int) = {add, subtract, multiply};

    int x = 10, y = 5;
    printf("Add: %d\n", operations[0](x, y));       // 调用 add 函数
    printf("Subtract: %d\n", operations[1](x, y));  // 调用 subtract 函数
    printf("Multiply: %d\n", operations[2](x, y));  // 调用 multiply 函数

    return 0;
}

在这个示例中:

  • operations 是一个函数指针数组,其中包含 addsubtractmultiply 三个函数的指针。
  • 可以通过 operations[0](x, y) 调用 add 函数,通过 operations[1](x, y) 调用 subtract 函数,通过 operations[2](x, y) 调用 multiply 函数。

11.3 函数指针的典型应用场景

  • 回调函数:函数指针常用于回调函数。例如,在排序函数中使用函数指针来比较元素,以实现不同的排序方式。
  • 事件处理:在嵌入式开发或系统编程中,函数指针经常用于事件处理、消息响应等。
  • 多态实现:函数指针可以实现类似多态的效果,使程序根据不同的输入动态调用不同的函数。
  • 函数表:函数指针数组可以用作函数表,实现类似的查表调用,提高代码的灵活性。

11.4 有趣的代码

11.4.1 代码1

(*(void (*)())0)();
11.4.1.1 分析

这段代码涉及到函数指针和类型强制转换。逐步分解如下:

  1. (void (*)())
    • void (*)() 是一种函数指针类型,它指向一个返回 void 且不接受任何参数的函数。
    • void (*)() 只是定义了一个函数指针的类型,还没有指定具体的地址。
  2. (void (*)())0
    • 这里的 (void (*)())0 将整数 0 强制转换成了一个 void (*)() 类型的函数指针。
    • 在C语言中,0 通常用作空指针,因此这里实际上定义了一个空的函数指针(即指向 0 地址的函数指针)。
    • 这种写法极其危险,因为它相当于指向了内存的起始位置 0(空指针),并没有真正指向任何可执行的函数。
  3. (*(void (*)())0)()
    • *(void (*)())0 通过解引用 0 地址来获得函数的内容,然后加上 () 表示调用该函数。
    • 因此,这段代码尝试调用地址为 0 的函数(本质上是一个空指针)。
11.4.1.2 结论
  • 效果:该代码尝试调用一个空指针,通常会导致程序崩溃或产生未定义行为。在大多数现代系统中,访问 0 地址会触发内存访问错误(segmentation fault)。
  • 目的:这段代码展示了如何使用类型强制转换和函数指针调用,但它没有实际意义,因为调用空指针是不安全的。

11.4.2 代码2

void (*signal(int, void (*)(int)))(int);
11.4.2.1 分析

这段代码定义了一个函数 signal,并且 signal 的返回类型是一个函数指针。逐步分析如下:

  1. void (*signal(...))(int);

    • signal 是一个函数,它的返回类型是 void (*)(int),即指向返回类型为 void、参数类型为 int 的函数指针。
    • 这意味着 signal 返回一个指针,该指针指向的函数的签名为 void func(int)
  2. 参数部分

    int, void (*)(int)
    
    • signal 接受两个参数。
    • 第一个参数是 int 类型。
    • 第二个参数是一个函数指针 void (*)(int),它指向一个返回类型为 void、参数类型为 int 的函数。
  3. 整体意义

    • 结合返回类型和参数部分,这段声明表示 signal 是一个函数,它接受一个 int 和一个 void (*)(int) 类型的函数指针作为参数,返回一个 void (*)(int) 类型的函数指针。
    • 这段代码的典型用途是在操作系统或信号处理库中,通常用于定义信号处理函数。在POSIX标准中,signal 函数就是以类似的方式声明的。
11.4.2.2 结论
  • 作用:这段代码用于声明一个函数 signal,它接受一个信号编号和一个信号处理函数,并返回一个指向旧信号处理函数的指针。这种函数指针机制使得信号处理过程可以灵活地更换信号处理函数。
  • 典型应用:在信号处理库或操作系统中,使用这种函数指针声明可以方便地实现信号处理的动态设置。

11.5 总结

  • 函数指针变量的定义:通过 return_type (*pointer_name)(parameter_list); 定义一个函数指针。
  • 初始化:可以将函数名赋值给函数指针变量。
  • 调用函数:通过函数指针调用函数的语法与直接调用类似。
  • 传递函数指针:函数指针可以作为参数传递,实现回调机制。
  • 灵活性:函数指针使得C语言的函数调用更加灵活,适合实现动态函数调用、多态、事件处理等复杂场景。

12. typedef关键字

在C语言中,typedef 是一个用于定义类型别名的关键字。它允许我们为已有的数据类型创建新的名字,使代码更加简洁、易读。typedef 可以用于简单的数据类型、复杂的结构体、指针、函数指针等多种场合。

12.1 基本语法

typedef 的基本语法如下:

typedef 原类型 新类型名;
  • 原类型:可以是任何基本类型(如 intfloat)或复合类型(如指针、结构体等)。
  • 新类型名:为该类型指定的新的名称。

示例:

typedef int INTEGER;

这里,INTEGERint 类型的别名,定义后我们可以使用 INTEGER 代替 int

12.2 typedef 的基本用法

12.2.1 为基本数据类型定义别名

可以使用 typedef 为基本类型定义更直观的名字。例如:

typedef unsigned int UINT;
typedef float REAL;

这样,UINT 可以用来表示 unsigned int,而 REAL 可以用来表示 float,从而增加了代码的可读性。

示例
UINT x = 10;
REAL y = 5.5;

12.2.2 为指针类型定义别名

指针类型的声明可能会显得复杂,使用 typedef 可以简化指针的表示。例如:

typedef int* INT_PTR;

这样定义后,可以使用 INT_PTR 来声明指向 int 的指针:

INT_PTR ptr1, ptr2;

需要注意的是,INT_PTR ptr1, ptr2; 实际上定义了两个 int* 指针。如果直接使用 int* ptr1, ptr2;,则 ptr1 是指针,ptr2 只是一个 int 类型的变量,这会导致不必要的错误。

在这里插入图片描述

12.3 typedef 和结构体

在C语言中,struct 的使用往往需要写出完整的类型声明。使用 typedef 可以简化 struct 类型的声明,使代码更简洁。

示例1:使用 typedef 定义结构体别名
struct Point {
    int x;
    int y;
};

typedef struct Point Point;

这样就可以使用 Point 代替 struct Point 了:

Point p1;
p1.x = 10;
p1.y = 20;
示例2:直接在 typedef 中定义结构体

可以在 typedef 中直接定义结构体,并同时定义其别名:

typedef struct {
    int x;
    int y;
} Point;

这样定义后,就可以直接使用 Point 创建变量,而不需要使用 struct 关键字:

Point p1;
p1.x = 10;
p1.y = 20;

12.4 typedef 和枚举

typedef 也可以用于枚举类型。枚举类型声明时通常需要写出 enum 关键字,使用 typedef 可以简化此过程。

示例:使用 typedef 定义枚举类型
typedef enum {
    RED,
    GREEN,
    BLUE
} Color;

这样定义后,可以直接使用 Color 而不需要写 enum

Color c = RED;

12.5 typedef 和函数指针

函数指针的声明通常比较复杂,typedef 可以简化函数指针的使用,尤其是在回调函数或动态函数调用中非常有用。

示例:定义函数指针类型

假设有一个函数指针类型,指向返回值为 int、参数为 intint 的函数:

typedef int (*FUNC_PTR)(int, int);

这样定义后,就可以使用 FUNC_PTR 来声明函数指针变量了:

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

FUNC_PTR ptr = add;
int result = ptr(3, 4);  // 使用函数指针调用 add 函数

这里,FUNC_PTR 代替了复杂的函数指针类型 (int (*)(int, int)),使代码更清晰易读。

12.6 typedef 和数组

typedef 还可以为数组定义类型别名,特别是在处理多维数组时可以简化代码。

示例:为数组定义别名

假设我们有一个包含 10 个整数的数组:

typedef int IntArray[10];

现在可以使用 IntArray 来声明一个长度为 10 的 int 数组:

IntArray arr;  // 等价于 int arr[10];

这在处理多维数组时尤其方便。例如,定义一个 5x10 的二维数组:

typedef int Matrix[5][10];
Matrix m;

这样 m 就是一个 5x10 的二维数组。

12.7 typedef 的一些特殊用法

12.7.1 typedefconst

typedef 可以与 const 一起使用,但要注意 const 的位置。以下是两种不同的定义:

typedef char* STRING;
const STRING s1, s2;

在这里,const STRING s1, s2; 等价于 char * const s1, * const s2;,即 s1s2常量指针,指向的字符串可以改变,但指针本身不可变。

如果想要定义指向常量字符的指针,应该这样写:

typedef const char* CSTRING;
CSTRING s1, s2;

在这种情况下,s1s2 是指向常量字符的指针,指针本身可以改变,但指向的内容不可变。

12.7.2 typedefstruct 嵌套

typedefstruct 可以结合在一起实现嵌套类型。例如:

typedef struct Node {
    int data;
    struct Node* next;
} Node;

在这里,Node 是一个链表节点的类型定义,可以包含指向自身的指针 next,用于构成链表。

12.8 typedef 的优点

  • 提高代码的可读性:通过为复杂的类型定义简洁的别名,代码变得更清晰。
  • 增加代码的可维护性:如果底层类型发生变化,只需修改 typedef 定义即可,无需修改大量代码。
  • 减少代码的重复性:可以通过 typedef 统一定义复杂类型,避免在代码中多次重复相同的声明。
  • 支持复杂的数据结构:例如,链表、树、函数指针等复杂数据结构的定义,可以通过 typedef 简化。

12.9 总结

  • typedef 用于为已有类型定义新的类型别名。
  • typedef 可以简化复杂的类型声明,包括指针、结构体、数组和函数指针等。
  • typedef 能提高代码的可读性、可维护性和简洁性,是C语言中非常有用的工具。

理解并善用 typedef,可以让代码更加优雅、模块化,也方便后期的维护和扩展。

13. 函数指针数组

13.1 函数指针数组的定义

函数指针数组是一种数组,其中每个元素都是一个函数指针。其定义形式如下:

return_type (*array_name[])(parameter_list);
  • return_type:函数的返回类型。
  • array_name:函数指针数组的名字。
  • parameter_list:函数的参数列表。
示例:定义函数指针数组

假设有三个函数 addsubtractmultiply,它们的返回类型为 int,参数为两个 int 类型。可以定义一个包含三个元素的函数指针数组:

int (*operations[3])(int, int);

这个 operations 数组可以存储 3 个函数指针,每个指针指向的函数应符合 int func(int, int) 的形式。

13.2 函数指针数组的初始化

定义了函数指针数组后,可以将函数的地址赋给数组中的元素。

示例:初始化函数指针数组

#include <stdio.h>

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

int subtract(int a, int b) {
    return a - b;
}

int multiply(int a, int b) {
    return a * b;
}

int main() {
    // 定义并初始化函数指针数组
    int (*operations[3])(int, int) = {add, subtract, multiply};

    // 使用函数指针数组调用函数
    int x = 10, y = 5;
    printf("Add: %d\n", operations[0](x, y));        // 调用 add 函数,输出 15
    printf("Subtract: %d\n", operations[1](x, y));   // 调用 subtract 函数,输出 5
    printf("Multiply: %d\n", operations[2](x, y));   // 调用 multiply 函数,输出 50

    return 0;
}

在这个例子中:

  • operations[0] 指向 add 函数。
  • operations[1] 指向 subtract 函数。
  • operations[2] 指向 multiply 函数。

通过 operations[i](x, y) 的方式可以调用对应的函数。

13.3 注意事项

  • 类型匹配:函数指针数组中的每个函数指针必须具有相同的参数和返回类型,才能存储在同一个数组中。
  • 下标范围:调用函数时要确保数组下标不超出定义范围,否则会导致未定义行为。
  • 错误检查:在类似菜单系统的应用中,应该检查用户输入的有效性,以避免调用无效的函数指针。

13.4 函数指针数组的应用场景

  • 菜单系统:根据用户输入选择不同的功能。
  • 回调机制:在事件驱动编程中,可以将函数指针数组作为回调函数列表。
  • 状态机:在实现状态机时,每个状态可以对应一个函数指针,通过函数指针数组管理不同的状态行为。
  • 函数表:在一些嵌入式系统或操作系统中,通过函数指针数组创建函数表,用于快速跳转到不同的函数。

13.5 总结

  • 函数指针数组:是一个数组,其中每个元素都是指向相同类型函数的指针。
  • 定义方式return_type (*array_name[])(parameter_list);
  • 用途:函数指针数组可以用于实现菜单系统、回调机制、状态机等场景,增强程序的灵活性。
  • 使用技巧:初始化函数指针数组后,可以通过 array_name[i](...) 调用相应的函数,实现动态函数调用。

14. 转移表

在C语言中,**转移表(Jump Table)**是一种通过数组或指针表实现的编程技巧,通常用于根据不同的输入执行不同的操作。转移表的核心思想是使用数组来存储函数指针,然后通过索引选择并调用相应的函数,这样可以避免使用大量的 if-elseswitch-case 语句,从而简化代码结构,提高效率。

下面我们通过实现一个简易的计算器程序来讲解如何使用转移表来管理不同的计算操作。

14.1 简易计算器需求

假设我们需要一个简易的计算器,支持以下四种基本运算:

  • 加法
  • 减法
  • 乘法
  • 除法

用户可以输入两个操作数和操作符,计算器程序根据操作符选择相应的运算函数并输出结果。

14.2 实现思路

通常,使用 switch-case 语句可以实现这样的功能,但是如果运算符很多,这种方式会让代码变得臃肿,且难以扩展。转移表通过函数指针数组的方式,简化了这种多分支的处理。

  • 定义四个运算函数:addsubtractmultiplydivide
  • 使用一个函数指针数组来存储这些运算函数。
  • 使用一个字符数组(或哈希表)将操作符映射到函数指针数组中的对应索引,以便根据操作符查找并调用相应的函数。

14.3 代码实现

以下是使用转移表实现简易计算器的代码:

#include <stdio.h>

// 定义四个基本运算函数
int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int multiply(int a, int b) {
    return a * b;
}

int divide(int a, int b) {
    if (b != 0) {
        return a / b;
    } else {
        printf("Error: Division by zero\n");
        return 0;
    }
}

// 使用函数指针数组实现转移表
int (*operations[])(int, int) = {add, subtract, multiply, divide};

// 用于将操作符映射到转移表中的索引
int get_operation_index(char op) {
    switch (op) {
        case '+': return 0;
        case '-': return 1;
        case '*': return 2;
        case '/': return 3;
        default: return -1; // 无效的操作符
    }
}

int main() {
    char operator;
    int x, y;
    
    printf("Enter an expression (e.g., 4 + 5): ");
    scanf("%d %c %d", &x, &operator, &y);
    
    // 获取操作符对应的函数指针索引
    int index = get_operation_index(operator);
    
    if (index != -1) {
        // 调用转移表中的相应函数
        int result = operations[index](x, y);
        printf("Result: %d\n", result);
    } else {
        printf("Error: Invalid operator\n");
    }

    return 0;
}

14.4 代码详解

14.4.1 定义运算函数

  • add:加法,返回 a + b
  • subtract:减法,返回 a - b
  • multiply:乘法,返回 a * b
  • divide:除法,返回 a / b,并检查是否为零

每个函数都有相同的签名:返回类型为 int,接受两个 int 类型的参数。

14.4.2 定义转移表(函数指针数组)

int (*operations[])(int, int) = {add, subtract, multiply, divide};

这里定义了一个函数指针数组 operations,其中每个元素都是一个函数指针,指向上述四个运算函数。这种方式将所有运算集中到一个数组中,方便管理和调用。

14.4.3 将操作符映射到函数指针数组的索引

int get_operation_index(char op) {
    switch (op) {
        case '+': return 0;
        case '-': return 1;
        case '*': return 2;
        case '/': return 3;
        default: return -1;
    }
}

该函数接受一个字符 op,并根据 op 的值返回函数指针数组中的索引:

  • + 对应索引 0
  • - 对应索引 1
  • * 对应索引 2
  • / 对应索引 3

如果输入的操作符无效(例如 %),则返回 -1,以便后续检查并输出错误提示。

14.4.4 主函数调用

main 函数中:

  1. 从用户输入中读取两个操作数和操作符。
  2. 使用 get_operation_index 函数获取操作符对应的索引。
  3. 检查索引是否有效(index != -1),如果有效,调用转移表中的相应函数并输出结果;否则,打印错误信息。
示例运行

假设用户输入 4 + 5,程序流程如下:

  1. 读取输入 4 + 5
  2. 使用 get_operation_index 查找 + 对应的索引,得到 0
  3. 调用 operations[0](4, 5),即调用 add(4, 5)
  4. 输出结果:Result: 9

14.5 优点分析

  • 代码简洁:使用函数指针数组减少了 if-elseswitch-case 结构,代码更加简洁和清晰。
  • 扩展性强:添加新操作只需在函数指针数组和 get_operation_index 函数中新增对应的条目,而不需要改动核心逻辑。
  • 效率高:通过索引直接调用函数,避免了条件分支的判断,提高了运行效率。

14.6 转移表应用场景

转移表在以下场景中非常有用:

  • 菜单系统:可以根据用户输入的选项选择相应的菜单项并调用对应的函数。
  • 状态机:在实现状态机时,不同状态的处理函数可以通过转移表来管理。
  • 命令解释器:对于不同的命令类型,可以使用转移表实现命令到函数的映射,从而执行相应的命令。
  • 事件驱动编程:可以根据不同的事件类型调用对应的事件处理函数。

14.7 总结

  • 转移表通过使用函数指针数组实现,能够简化多分支结构的代码,使得程序更清晰、可读性更高。
  • 在实现计算器时,转移表有效地将操作符映射到对应的运算函数,实现了动态调用。
  • 使用转移表不仅提升了代码的效率和扩展性,还为后续的维护提供了便利。

理解并灵活运用转移表,可以编写出简洁而高效的C代码,特别是在多功能选择或状态处理等复杂应用中。

—完—


原文地址:https://blog.csdn.net/weixin_44643253/article/details/143597880

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!