C语言——指针(二)

0.前言

在上一篇博客中,我们探讨了C语言中指针的基础知识,包括指针的定义、内存和地址、指针运算等。在本篇博客中,我们将深入研究指针的高级应用,包括字符指针、数组指针、函数指针等内容。这些概念对于理解和编写更为复杂的C程序至关重要。

1. 字符指针变量

现在,我们来深入讨论字符指针的定义和使用。

#include <stdio.h>

int main() 
{
    char str[] = "hello, world!";
    const char *ptr = str; // 字符指针指向字符数组

    while (*ptr != '\0') 
    {
        printf("%c", *ptr);
        ptr++;
    }

    return 0;
}

在这个例子中,我们定义了一个字符数组str,然后用字符指针ptr指向这个字符数组的首地址。通过循环遍历字符指针,我们可以逐个访问字符串中的字符,并输出结果:"hello, world!"。

 下面我们来看一道有意思的题目,原题来自《剑指offer》:

#include <stdio.h>

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

    return 0;
}

这段代码定义了四个字符串,分别使用字符数组和字符指针进行初始化,并通过比较操作符==判断字符串是否相同。我们来逐步分析代码的执行过程。

1.1. 字符数组和字符指针的区别

  • str1str2是字符数组,它们的内容被存储在栈上。
  • str3str4是字符指针,它们的内容是指向常量字符串的指针,常量字符串通常存储在只读数据段。

1.2. 对比操作

  • 对于str1str2,它们是两个独立的字符数组,它们的地址不同,因此str1 == str2为假,输出"str1 and str2 are not same"。

  • 对于str3str4,它们指向相同的常量字符串,常量字符串的地址通常是相同的,因此str3 == str4为真,输出"str3 and str4 are same"。

2. 数组指针变量

2.1数组指针变量是什么

C语言——指针(一)中我们介绍了指针数组。指针数组是一种数组,数组中存放的是地址(指针)。那么问题来了:数组指针变量是什么呢?数组?还是指针?

顾名思义,我们不难想到:“数组”修饰“指针”,故数组指针当然是一种指针。

在C语言中,数组指针变量是一种特殊的指针类型,它指向数组的首地址。数组指针的概念可以帮助我们更灵活地处理数组,尤其是多维数组。

下面到考验眼力的时候了:

int *ptr1[10];
int(*ptr2)[10];

ptr1与ptr2分别是什么呢?

回顾上篇博客的内容,我们不难发现:ptr1是指针数组,即可以存放10个int*类型数据的数组。

那么ptr2就是我们本节介绍的数组指针。

解释:ptr2先和*结合,说明ptr2是⼀个指针变量变量,然后指着指向的是一个大小为10个整型的数组。所以ptr2是一个指针,指向一个数组,叫 数组指针。
这里要注意:[ ]的优先级要高于*号的,所以必须加上()来保证ptr2先和*结合。

2.2数组指针变量的初始化

数组指针变量的初始化需要注意的是,指针的类型要与数组的元素类型相匹配。下面我们通过例子详细介绍数组指针变量的初始化。

#include <stdio.h>

int main() 
{
    int arr[] = {1, 2, 3, 4, 5};
    int(*ptr)[5]; // 定义一个指向包含5个整数的数组的指针

    ptr = &arr; // 将数组的首地址赋给数组指针

    // 使用数组指针访问数组元素
    for (int i = 0; i < 5; i++) 
    {
        printf("%d ", (*ptr)[i]);
    }
    return 0;
}

在这个例子中,我们定义了一个包含5个整数的一维数组arr,然后定义了一个指向包含5个整数的数组的指针ptr。通过ptr = &arr;,我们将数组arr的首地址赋给了数组指针ptr

在使用数组指针访问数组元素时,需要使用(*ptr)来表示整个数组,然后通过(*ptr)[i]的方式访问数组的每个元素。

这种数组指针的用法在处理多维数组时尤为有用,它可以简化对数组的操作,提高代码的可读性和灵活性。

3. 二维数组传参的本质

在C语言中,二维数组传参的本质是传递了数组的地址。当我们传递一个二维数组给函数时,实际上是将数组的首地址传递给了函数,而这个首地址就是第一行这个一维数组的地址。这样的传递方式使得我们可以通过指针形式更灵活地处理二维数组。

让我们通过一个简单的例子来说明二维数组传参的本质:

传递二维数组给函数

#include <stdio.h>

// 函数接受二维数组作为参数
void printArray(int arr[][3], int rows, int cols) 
{
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

int main() 
{
    int array[2][3] = {{1, 2, 3}, {4, 5, 6}};

    // 调用函数并传递二维数组
    printArray(array, 2, 3);

    return 0;
}

 在上述例子中,printArray 函数接受一个二维数组和两个整数参数。虽然在函数参数中我们写的是int arr[][3],但实际上这是一种语法糖(注:语法糖:计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性),本质上等同于int (*arr)[3]。在函数内部,我们可以通过指针形式访问二维数组的元素。

形参也可以写成指针形式

#include <stdio.h>

// 函数接受二维数组的地址作为参数
void printArray(int (*arr)[3], int rows, int cols)
 {
    for (int i = 0; i < rows; ++i)
    {
        for (int j = 0; j < cols; ++j) 
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

int main() 
{
    int array[2][3] = {{1, 2, 3}, {4, 5, 6}};

    // 调用函数并传递二维数组的地址
    printArray(array, 2, 3);

    return 0;
}

在这个例子中,printArray 函数的形参写成了指针形式int (*arr)[3],这和前面的写法是等价的。通过这种写法,我们强调了传递的是一个指向包含3个整数的数组的指针。

本质上是传递地址

无论使用哪种形式,关键的本质是我们传递的是数组的地址,具体来说是第一行这个一维数组的地址。这种传递方式使得函数能够更灵活地处理不同大小的二维数组,为多维数组的传参提供了便利。

4. 函数指针变量

在C语言中,函数指针是指向函数的指针变量,允许我们通过指针调用函数。函数指针的使用在一些特定场景下非常有用,比如回调函数、动态函数调用等。下面将详细介绍函数指针的创建、使用和通过一个例子剖析函数指针的应用。

4.1函数指针的创建

创建函数指针的语法如下:

return_type (*pointer_name)(parameter_type1, parameter_type2, ...);

其中:

  • return_type 是函数返回值的类型。
  • pointer_name 是函数指针的名字。
  • parameter_type1, parameter_type2, ... 是函数的参数类型。

4.2函数指针的使用

函数指针的使用包括指针的赋值和调用。首先,我们需要将函数的地址赋给函数指针,然后可以通过函数指针调用相应的函数。

#include <stdio.h>

// 函数定义
int add(int a, int b) 
{
    return a + b;
}
int subtract(int a, int b) 
{
    return a - b;
}
int main() 
{
    // 函数指针的声明和赋值
    int (*operation)(int, int);
    operation = add;  // 将add函数的地址赋给函数指针

    // 使用函数指针调用函数
    int result = operation(5, 3);
    printf("Result: %d\n", result);

    // 将subtract函数的地址赋给函数指针
    operation = subtract;
    result = operation(5, 3);
    printf("Result: %d\n", result);
    return 0;
}

上述例子中,我们声明了一个函数指针int (*operation)(int, int);,然后通过operation = add;add函数的地址赋给了函数指针。接着,我们通过operation(5, 3);调用了add函数。后续通过将subtract函数的地址赋给函数指针,再次调用得到了subtract函数的结果。

4.3代码剖析

我们不妨先看两段“有趣的”代码:(来自《C陷阱和缺陷》)

// 示例1
(*(void (*)())0)();
// 示例2
void (*signal(int , void(*)(int)))(int);

 你是不是看的一头雾水?我们马上来仔细介绍。

示例1涉及一个函数指针的定义和调用。

  • void (*)():定义了一个函数指针,该指针指向一个没有参数且返回类型为void的函数。
  • (*)():表示一个函数指针的语法。
  • (void (*)())0:将地址0强制类型转换为一个函数指针,这是一个危险的操作,因为0通常是空指针,而调用空指针可能导致未定义的行为。

整体来说,这个例子的目的是尝试调用地址为0的函数指针,这通常是一个糟糕的实践,因为它可能导致程序崩溃或产生不可预测的行为。这种用法一般不推荐在实际的编程中使用,仅用于说明函数指针的语法。

示例2涉及一个函数signal,该函数接受两个参数:一个整数和一个函数指针,然后返回一个函数指针。让我们逐步解析:

  • void(*)(int):表示一个函数指针,该指针指向一个接受一个整数参数且返回类型为void的函数。
  • signal(int , void(*)(int)):函数signal接受两个参数,一个整数和一个函数指针,并返回一个函数指针。

整体来说,这个例子是一个典型的用法,signal函数通常用于注册信号处理函数。例如,signal(SIGINT, handler)表示在接收到SIGINT信号时调用名为handler的处理函数。

4.4typedef关键字

typedef关键字可以用来简化函数指针的声明,提高代码可读性。例如:

#include <stdio.h>

// 使用typedef简化函数指针声明
typedef int (*Operation)(int, int);

// 函数定义
int add(int a, int b) 
{
    return a + b;
}
int subtract(int a, int b) 
{
    return a - b;
}
int main() 
{
    // 使用typedef声明函数指针
    Operation operation;//左边是类型,右边是变量名
    
    // 赋值并调用
    operation = add;
    printf("Result: %d\n", operation(5, 3));

    operation = subtract;
    printf("Result: %d\n", operation(5, 3));

    return 0;
}

在这个例子中,Operation被定义为一个函数指针类型,可以更清晰地表达我们声明的是一个函数指针。使用typedef关键字可以简化复杂的函数指针声明,使代码更易读。

5. 函数指针数组

在C语言中,函数指针数组是一种数组,其元素是函数指针。这种数据结构在一些特定的场景中非常有用,比如实现多态性(Polymorphism)或者构建简单的状态机。让我们详细介绍函数指针数组的定义、初始化和使用。

5.1 函数指针数组的定义

函数指针数组的定义形式如下:

return_type (*array_name[size])(parameter_type1, parameter_type2, ...);

其中:

  • return_type 是函数返回值的类型。
  • array_name 是函数指针数组的名字。
  • size 是数组的大小。
  • parameter_type1, parameter_type2, ... 是函数的参数类型。

5.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 (*operation[3])(int, int) = {add, subtract, multiply};

int main() 
{
    // 使用函数指针数组调用函数
    for (int i = 0; i < 3; ++i) 
    {
        printf("Result: %d\n", operation[i](5, 3));
    }
    return 0;
}

在这个例子中,我们定义了三个函数addsubtractmultiply,然后定义了一个包含这三个函数指针的数组operation。通过循环遍历函数指针数组,我们可以调用数组中的函数。

5.3 函数指针数组的应用

函数指针数组通常用于实现多态性,通过在运行时动态选择调用哪个函数,达到不同行为的效果。这在设计状态机等场景中非常有用。

#include <stdio.h>

// 状态机函数类型
typedef void (*StateFunc)();

// 状态机的具体状态函数
void stateA() 
{
    printf("State A\n");
}

void stateB() 
{
    printf("State B\n");
}

void stateC() 
{
    printf("State C\n");
}

int main() 
{
    // 状态机函数指针数组的定义和初始化
    StateFunc stateMachine[] = {stateA, stateB, stateC};

    // 模拟状态切换
    for (int i = 0; i < 3; ++i) 
    {
        stateMachine[i]();
    }
    return 0;
}

在这个例子中,我们定义了一个简单的状态机,包含三个状态函数stateAstateBstateC,然后通过函数指针数组stateMachine来表示状态机的状态。通过循环调用函数指针数组中的函数,我们可以模拟状态的切换。

6. 转移表

转移表是一种利用函数指针数组来实现分支逻辑的方法。它通常用于避免大量的switchif-else语句,提高代码的可维护性和可读性。在这里,我们将详细介绍转移表的概念,并通过一个计算器的实现例子来演示它的应用。

6.1 转移表的定义

转移表本质上就是一个函数指针数组,每个元素都指向一个具体的函数。通过数组的索引或者其他方式来选择调用特定的函数,从而达到分支逻辑的效果。

6.2 计算器的一般实现

让我们以一个简单的计算器为例,通过switch语句和转移表两种方式来实现。

使用 switch 语句的计算器实现

#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() 
{
    char operator;
    int operand1, operand2, result;

    printf("Enter an operator (+, -, *): ");
    scanf("%c", &operator);

    printf("Enter two operands: ");
    scanf("%d %d", &operand1, &operand2);

    // 使用 switch 语句选择调用的函数
    switch (operator) 
    {
        case '+':
            result = add(operand1, operand2);
            break;
        case '-':
            result = subtract(operand1, operand2);
            break;
        case '*':
            result = multiply(operand1, operand2);
            break;
        default:
            printf("Invalid operator\n");
            return 1;
    }

    printf("Result: %d\n", result);

    return 0;
}

使用转移表的计算器实现

#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;
}
// 函数指针类型
typedef int (*Operation)(int, int);

int main() 
{
    char operator;
    int operand1, operand2, result;

    // 函数指针数组的定义和初始化
    Operation operations[] = {add, subtract, multiply};

    printf("Enter an operator (+, -, *): ");
    scanf(" %c", &operator);  // 注意这里加一个空格,避免换行符的影响

    printf("Enter two operands: ");
    scanf("%d %d", &operand1, &operand2);

    // 使用转移表选择调用的函数
    if (operator >= '+' && operator <= '*') 
    {
        result = operations[operator - '+'](operand1, operand2);
    }    
    else     
    {
        printf("Invalid operator\n");
        return 1;
    }

    printf("Result: %d\n", result);

    return 0;
}

 6.3 代码剖析

在第一个例子中,我们使用了switch语句,根据输入的运算符选择调用相应的函数。而在第二个例子中,我们使用了函数指针数组,通过转移表的方式实现相同的逻辑。

在第二个例子中,Operation被定义为一个函数指针类型,operations数组包含了三个函数指针,分别指向addsubtractmultiply函数。通过将运算符转换为数组索引的方式,我们可以直接调用相应的函数。

这样的实现使得代码更加清晰、易读,也更易于扩展。如果需要添加新的运算符,只需在函数指针数组中添加对应的函数指针,而不需要修改大量的分支逻辑代码。

7.回调函数

在C语言中,回调函数是一种函数指针的应用,它允许我们将一个函数作为参数传递给另一个函数,从而实现一种灵活的扩展方式。回调函数通常用于实现在某些事件发生时执行特定操作的机制。让我们详细介绍回调函数的概念和应用。

7.1 回调函数的定义

回调函数就是一个通过函数指针传递给另一个函数的函数。在函数内部,通过调用这个函数指针,实现对外部传入函数的调用。

7.2 回调函数的使用场景

回调函数常常用于以下场景:

  • 事件处理:当某个事件发生时,执行特定的函数。
  • 排序函数:在排序算法中,允许用户自定义比较函数。
  • 用户交互:在用户接口开发中,根据用户的行为执行相应的操作。

7.3 回调函数的示例

让我们通过一个简单的示例来演示回调函数的用法。假设我们有一个执行某个操作的函数,而这个操作是由外部传入的回调函数定义的。

#include <stdio.h>

// 回调函数类型
typedef void (*Callback)(int);

// 执行操作的函数,接受一个回调函数作为参数
void performOperation(int data, Callback callback) 
{
    // 执行某个操作
    printf("Performing operation with data %d\n", data);

    // 调用回调函数
    callback(data);
}

// 回调函数的具体实现
void callbackFunction(int data) 
{
    printf("Callback function called with data %d\n", data);
}

int main() 
{
    // 在主函数中调用 performOperation,并传入回调函数
    performOperation(42, callbackFunction);

    return 0;
}

7.4 回调函数的优势

使用回调函数的主要优势在于它能够提供一种灵活的扩展机制,使得代码更具通用性和可维护性。通过在不同场景下传入不同的回调函数,我们可以实现相同的操作逻辑,但具体的行为可以根据回调函数的实现而变化。

在实际的开发中,回调函数常用于事件处理、异步编程、模块化设计等场景,它是一种强大的编程模式,使得代码更加灵活和可扩展。

8.qsort使用及模拟实现

8.1从冒泡排序说起

在介绍qsort之前,让我们先回顾一下冒泡排序,这是一种基本的排序算法。冒泡排序的基本思想是通过相邻元素的比较和交换,将较大的元素逐步向数组的尾部移动,较小的元素逐步向数组的头部移动,最终实现整个数组的有序排列。

冒泡排序是一种基础的排序算法,它的核心思想是通过多次遍历待排序的序列,依次比较相邻的元素,并交换顺序不符合要求的相邻元素。经过一轮的遍历,最大(或最小)的元素就像气泡一样"冒"到了最终的位置。这个过程会一直重复,直到整个序列有序。

8.1.1 冒泡排序的基本步骤
  1. 比较相邻元素: 从第一个元素开始,比较相邻的两个元素,如果顺序不符合排序规则,则交换它们的位置。

  2. 一轮遍历后的结果: 一轮遍历后,最大(或最小)的元素会移到序列的最后。此时,最后一个元素已经确定是最大(或最小)的,不再参与后续的比较。

  3. 重复遍历: 重复以上步骤,每一轮遍历确定一个当前未排序序列的最大(或最小)元素的位置。

  4. 直到整个序列有序: 经过多轮遍历,直到整个序列有序。

8.1.2 冒泡排序的示例

让我们通过一个简单的示例来演示冒泡排序的过程。考虑以下数组:

int arr[] = {64, 34, 25, 12, 22, 11, 90};

第一轮遍历:

  1. 比较 6434,交换它们的位置。
    • 数组变为 {34, 64, 25, 12, 22, 11, 90}
  2. 比较 6425,交换它们的位置。
    • 数组变为 {34, 25, 64, 12, 22, 11, 90}
  3. 比较 6412,交换它们的位置。
    • 数组变为 {34, 25, 12, 64, 22, 11, 90}
  4. 比较 6422,交换它们的位置。
    • 数组变为 {34, 25, 12, 22, 64, 11, 90}
  5. 比较 6411,交换它们的位置。
    • 数组变为 {34, 25, 12, 22, 11, 64, 90}
  6. 比较 6490,不交换。

第一轮遍历后,最大的元素 90 已经移到了最后的位置。

第二轮遍历:

  1. 比较 3425,不交换。
  2. 比较 3412,不交换。
  3. 比较 3422,不交换。
  4. 比较 3411,不交换。
  5. 比较 3464,交换。
    • 数组变为 {25, 12, 22, 11, 34, 64, 90}
  6. 比较 6490,不交换。

第二轮遍历后,第二大的元素 64 已经移到了倒数第二的位置。

继续进行类似的遍历,最终完成整个冒泡排序的过程

 8.1.3代码实现
#include <stdio.h>

// 冒泡排序的基本实现
void bubbleSort(int arr[], int n) 
{
    for (int i = 0; i < n - 1; i++) 
    {
        for (int j = 0; j < n - i - 1; j++) 
        {
            if (arr[j] > arr[j + 1]) 
            {
                // 交换相邻元素
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main() 
{
    int arr[] = {64, 34, 25, 12, 22, 11, 90};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("Original array: ");
    for (int i = 0; i < n; i++) 
    {
        printf("%d ", arr[i]);
    }

    // 使用冒泡排序
    bubbleSort(arr, n);

    printf("\nSorted array: ");
    for (int i = 0; i < n; i++) 
    {
        printf("%d ", arr[i]);
    }

    return 0;
}

 8.2qsort基本介绍

qsort是C语言标准库中提供的快速排序函数,它可以用于对任意类型的数组进行排序。qsort函数的声明如下:

void qsort(void *base, size_t num, size_t size, int (*compar)(const void *, const void *));

其中:

  • base:指向要排序的数组的指针。
  • num:数组中元素的个数。
  • size:每个元素的大小。
  • compar:指向比较函数的指针。

其中compar函数指针的返回值如下:(来自legacy.cplusplus.com)

由此可见,使用qsort的关键点是自己动手写一个函数指针。我们下面看一些实例:

 

8.3使用qsort排序

8.3.1排序整型数据

让我们首先看一个使用qsort排序整型数组的例子。

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

// 比较函数,用于qsort
int compareIntegers(const void *a, const void *b) 
{
    return (*(int *)a - *(int *)b);
}

int main() 
{
    int arr[] = {64, 34, 25, 12, 22, 11, 90};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("Original array: ");
    for (int i = 0; i < n; i++) 
    {
        printf("%d ", arr[i]);
    }

    // 使用qsort排序整型数组
    qsort(arr, n, sizeof(int), compareIntegers);

    printf("\nSorted array: ");
    for (int i = 0; i < n; i++) 
    {
        printf("%d ", arr[i]);
    }

    return 0;
}
 
8.3.2排序结构体类型数据

现在,让我们看一个使用qsort排序包含结构体的数组的例子。

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

// 结构体定义
struct Person 
{
    char name[50];
    int age;
};

// 比较函数,用于qsort
int comparePeople(const void *a, const void *b) 
{
    return strcmp(((struct Person *)a)->name, ((struct Person *)b)->name);
}

int main() 
{
    struct Person people[] = {
        {"John", 25},
        {"Alice", 22},
        {"Bob", 30},
        {"Charlie", 28}
    };

    int n = sizeof(people) / sizeof(people[0]);

    printf("Original array:\n");
    for (int i = 0; i < n; i++) 
    {
        printf("%s (%d years old)\n", people[i].name, people[i].age);
    }

    // 使用qsort排序结构体数组
    qsort(people, n, sizeof(struct Person), comparePeople);

    printf("\nSorted array:\n");
    for (int i = 0; i < n; i++) 
    {
        printf("%s (%d years old)\n", people[i].name, people[i].age);
    }

    return 0;
}
 

对对对这段代码展示了如何使用qsort对包含结构体的数组进行排序,同时通过自定义的比较函数实现结构体元素的比较。在实际应用中,通过修改比较函数,可以实现不同的排序方式,如按年龄、按姓名长度等。

8.3.3排序浮点数

下面是一个使用qsort排序浮点型数组的例子。

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

// 比较函数,用于qsort
int compareFloats(const void *a, const void *b) 
{
    return (*(float *)a > *(float *)b) - (*(float *)a < *(float *)b);
}
//(表达式1 - 表达式2):这是一个巧妙的构造,它通过相减操作得到一个结果。
//如果 表达式1 成立且 表达式2 不成立,结果为 1;如果 表达式1 不成立且 表达式2 成立,结果为 -1;如果两者都成立或者都不成立,结果为 0。
//这样,整个表达式最终返回一个整数值,表示两个浮点数的大小关系。

int main() 
{
    float arr[] = {3.14, 1.1, 2.0, 0.5, 5.7};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("Original array: ");
    for (int i = 0; i < n; i++) 
    {
        printf("%f ", arr[i]);
    }

    // 使用qsort排序浮点数数组
    qsort(arr, n, sizeof(float), compareFloats);

    printf("\nSorted array: ");
    for (int i = 0; i < n; i++) 
    {
        printf("%f ", arr[i]);
    }

    return 0;
}
 

8.4模拟实现qsort(以改造冒泡排序为例)

现在让我们以冒泡排序为基础,实现一个简化版本的qsort函数(qsort库函数使用快速排序,有兴趣的小伙伴可以自行了解)。

#include<stdio.h>
int int_cmp(const void* p1, const void* p2)
{
	return (*(int*)p1 - *(int*)p2);
}
void _swap(void* p1, void* p2, int size)
{
	int i = 0;
	for (i = 0; i < size; i++)
	{
		char tmp = *((char*)p1 + i);
		*((char*)p1 + i) = *((char*)p2 + i);
		*((char*)p2 + i) = tmp;
	}
}
void bubble(void* base, int count, int size, int(*cmp)(void*, void*))
{
	int i = 0;
	int j = 0;
	for (i = 0; i < count - 1; i++)
	{
		for (j = 0; j < count - i - 1; j++)
		{
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
			{
				_swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
	}
}
int main()
{
	int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
	int i = 0;
	bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
	for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

这段代码实现了一个通用的冒泡排序函数 bubble,可用于对不同类型的数组进行排序。通过提供比较函数和通用交换函数,代码展示了如何通过字节级别的操作实现通用性。主函数使用该排序函数对整型数组进行排序,通过整型比较函数实现元素比较,并输出排序结果。

 

9.sizeof和strlen对比

  • 返回值类型:

    • sizeof 返回的是数据类型或变量在内存中的字节数,是一个编译时确定的常量。
    • strlen 返回的是字符串中的字符个数,是一个运行时计算的值。
  • 适用对象:

    • sizeof 适用于任何数据类型,包括基本数据类型、数组、结构体等。
    • strlen 主要用于计算字符串的长度。
  • 计算方式:

    • sizeof 在编译时计算,不需要遍历数据内容。
    • strlen 在运行时通过遍历字符串的内容来计算长度,直到遇到字符串末尾的空字符。
  • 注意事项:

    • sizeof 可以用于计算数组的大小,包括数组元素的个数。
    • strlen 只计算字符串的长度,不包括字符串末尾的空字符在内。

举例如下:

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

int main() 
{
    char str[] = "Hello, World!";
    char array[20];

    printf("Size of str (including '\\0'): %lu\n", sizeof(str));
    printf("Length of str (excluding '\\0'): %lu\n", strlen(str));

    printf("Size of array: %lu\n", sizeof(array));
    
    return 0;
}

在这个示例中,sizeof(str) 计算包含空字符的字符串大小,而 strlen(str) 只计算字符串的长度,不包括空字符。另外,sizeof(array) 计算整个数组的大小,包括数组中的所有元素。

10.习题剖析

//题目1
#include <stdio.h>
int main()
{
	int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int* ptr1 = (int*)(&aa + 1);
	int* ptr2 = (int*)(*(aa + 1));
	printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
	return 0;
}

解析:

  1. &aa + 1 将指针移动到整个二维数组 aa 的后面,然后转换为 int* 类型,ptr1 现在指向了数组之外。
  2. *(aa + 1) 获取数组 aa 中第二行的起始地址,然后转换为 int* 类型,ptr2 现在指向了数组第二行的起始位置。

所以,*(ptr1 - 1) 输出的是数组 aa 后面位置的前一个元素,即 10。而 *(ptr2 - 1) 输出的是数组 aa 第二行起始位置的前一个元素,即 5

答案:

//题目2
#include <stdio.h>
int main()
{
 char *c[] = {"ENTER","NEW","POINT","FIRST"};
 char**cp[] = {c+3,c+2,c+1,c};
 char***cpp = cp;
 printf("%s\n", **++cpp);
 printf("%s\n", *--*++cpp+3);
 printf("%s\n", *cpp[-2]+3);
 printf("%s\n", cpp[-1][-1]+1);
 return 0;
}

 这道题难度较大,我们仅仅对一下答案,详细解析可以参考其他博主的讲解(直接搜索这段代码即可)

11.结语

 

通过本篇博客,我们深入探讨了C语言中指针的高级应用。从字符指针到函数指针,再到回调函数和排序算法的实现,我们希望读者通过这些知识点的学习,能够更加熟练地运用指针进行复杂程序的设计和实现。在下一篇博客中,我们将继续深入C语言的其他特性,为读者呈现更加丰富的编程世界。

 

  • 9
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值