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. 字符数组和字符指针的区别
str1
和str2
是字符数组,它们的内容被存储在栈上。str3
和str4
是字符指针,它们的内容是指向常量字符串的指针,常量字符串通常存储在只读数据段。
1.2. 对比操作
-
对于
str1
和str2
,它们是两个独立的字符数组,它们的地址不同,因此str1 == str2
为假,输出"str1 and str2 are not same"。 -
对于
str3
和str4
,它们指向相同的常量字符串,常量字符串的地址通常是相同的,因此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;
}
在这个例子中,我们定义了三个函数add
、subtract
和multiply
,然后定义了一个包含这三个函数指针的数组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;
}
在这个例子中,我们定义了一个简单的状态机,包含三个状态函数stateA
、stateB
和stateC
,然后通过函数指针数组stateMachine
来表示状态机的状态。通过循环调用函数指针数组中的函数,我们可以模拟状态的切换。
6. 转移表
转移表是一种利用函数指针数组来实现分支逻辑的方法。它通常用于避免大量的switch
或if-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
数组包含了三个函数指针,分别指向add
、subtract
和multiply
函数。通过将运算符转换为数组索引的方式,我们可以直接调用相应的函数。
这样的实现使得代码更加清晰、易读,也更易于扩展。如果需要添加新的运算符,只需在函数指针数组中添加对应的函数指针,而不需要修改大量的分支逻辑代码。
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 冒泡排序的基本步骤
-
比较相邻元素: 从第一个元素开始,比较相邻的两个元素,如果顺序不符合排序规则,则交换它们的位置。
-
一轮遍历后的结果: 一轮遍历后,最大(或最小)的元素会移到序列的最后。此时,最后一个元素已经确定是最大(或最小)的,不再参与后续的比较。
-
重复遍历: 重复以上步骤,每一轮遍历确定一个当前未排序序列的最大(或最小)元素的位置。
-
直到整个序列有序: 经过多轮遍历,直到整个序列有序。
8.1.2 冒泡排序的示例
让我们通过一个简单的示例来演示冒泡排序的过程。考虑以下数组:
int arr[] = {64, 34, 25, 12, 22, 11, 90};
第一轮遍历:
- 比较
64
和34
,交换它们的位置。- 数组变为
{34, 64, 25, 12, 22, 11, 90}
。
- 数组变为
- 比较
64
和25
,交换它们的位置。- 数组变为
{34, 25, 64, 12, 22, 11, 90}
。
- 数组变为
- 比较
64
和12
,交换它们的位置。- 数组变为
{34, 25, 12, 64, 22, 11, 90}
。
- 数组变为
- 比较
64
和22
,交换它们的位置。- 数组变为
{34, 25, 12, 22, 64, 11, 90}
。
- 数组变为
- 比较
64
和11
,交换它们的位置。- 数组变为
{34, 25, 12, 22, 11, 64, 90}
。
- 数组变为
- 比较
64
和90
,不交换。
第一轮遍历后,最大的元素 90
已经移到了最后的位置。
第二轮遍历:
- 比较
34
和25
,不交换。 - 比较
34
和12
,不交换。 - 比较
34
和22
,不交换。 - 比较
34
和11
,不交换。 - 比较
34
和64
,交换。- 数组变为
{25, 12, 22, 11, 34, 64, 90}
。
- 数组变为
- 比较
64
和90
,不交换。
第二轮遍历后,第二大的元素 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;
}
解析:
&aa + 1
将指针移动到整个二维数组aa
的后面,然后转换为int*
类型,ptr1
现在指向了数组之外。*(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语言的其他特性,为读者呈现更加丰富的编程世界。