C语言高级特性:数组、指针与字符串编程实践
本文将通过一系列C语言编程实例,深入探讨数组、指针以及字符串在实际编程中的应用。我们将涵盖从基本的数组元素查找、指针作为函数参数和返回值的使用,到字符串的内存管理、操作与简单加密,以及命令行参数的处理等多个方面,旨在帮助读者巩固对C语言核心概念的理解。
1. 数组极值查找与多值返回
在处理数组数据时,查找最大值和最小值是常见的需求。C语言函数可以通过指针参数实现"传址调用",从而修改主调函数中的变量,达到返回多个值的目的。
以下示例展示了一个函数如何接收一个整数数组和两个指向整数的指针,然后将数组中的最大值和最小值分别存入这两个指针所指向的地址。
#include <stdio.h>
#define DATA_SIZE 5 // 定义数组大小
// 函数声明:查找数组中的最大值和最小值
void findExtremes(int dataArray[], int count, int *minValPtr, int *maxValPtr);
int main() {
int numbers[DATA_SIZE];
int minValue, maxValue;
printf("请输入%d个整数数据:\n", DATA_SIZE);
for (int i = 0; i < DATA_SIZE; ++i) {
scanf("%d", &numbers[i]);
}
printf("您输入的数据为: ");
for (int i = 0; i < DATA_SIZE; ++i) {
printf("%d ", numbers[i]);
}
printf("\n");
printf("正在处理数据...\n");
findExtremes(numbers, DATA_SIZE, &minValue, &maxValue); // 调用函数,传递变量地址
printf("处理结果:\n");
printf("最小值 = %d, 最大值 = %d\n", minValue, maxValue);
return 0;
}
// 函数定义:查找数组中的最大值和最小值
void findExtremes(int dataArray[], int count, int *minValPtr, int *maxValPtr) {
if (count <= 0) {
// 处理空数组或无效count的情况
if (minValPtr) *minValPtr = 0; // 或设置为错误码
if (maxValPtr) *maxValPtr = 0;
return;
}
// 初始化最大值和最小值
*minValPtr = dataArray[0];
*maxValPtr = dataArray[0];
// 遍历数组查找
for (int i = 1; i < count; ++i) { // 从第二个元素开始比较
if (dataArray[i] < *minValPtr) {
*minValPtr = dataArray[i];
} else if (dataArray[i] > *maxValPtr) {
*maxValPtr = dataArray[i];
}
}
}
在这个例子中,findExtremes 函数通过接收 int *minValPtr 和 int *maxValPtr 这两个指针,可以直接修改 main 函数中 minValue 和 maxValue 变量的值。在函数执行到 findExtremes 内部的初始化语句 *minValPtr = dataArray[0]; 时,minValPtr 和 maxValPtr 分别指向 main 函数栈帧中的 minValue 和 maxValue 变量的内存地址。
2. 返回指向数组元素的指针
除了通过指针参数返回多个值,函数也可以返回一个指针,指向数组中的特定元素。这种方法常用于查找某个满足条件的元素,并返回其在原数组中的位置。
以下示例演示了一个函数如何查找数组中的最大值,并返回指向该最大值元素的指针。
#include <stdio.h>
#define ARR_LEN 5 // 定义数组长度
// 函数声明:查找数组中最大元素的地址
int *locateMaxValue(int arr[], int length);
int main() {
int myNumbers[ARR_LEN];
int *ptrToMaxElement; // 用于存储返回的最大值指针
printf("请输入%d个整数数据:\n", ARR_LEN);
for (int i = 0; i < ARR_LEN; ++i) {
scanf("%d", &myNumbers[i]);
}
printf("当前数组数据: ");
for (int i = 0; i < ARR_LEN; ++i) {
printf("%d ", myNumbers[i]);
}
printf("\n");
printf("正在寻找最大元素位置...\n");
ptrToMaxElement = locateMaxValue(myNumbers, ARR_LEN); // 调用函数
printf("结果输出:\n");
if (ptrToMaxElement != NULL) {
printf("最大值 = %d\n", *ptrToMaxElement);
} else {
printf("数组为空或无效。\n");
}
return 0;
}
// 函数定义:查找数组中最大元素的地址
int *locateMaxValue(int arr[], int length) {
if (length <= 0) {
return NULL; // 无效长度,返回空指针
}
int largestIndex = 0; // 假设第一个元素是最大值
for (int i = 1; i < length; ++i) {
if (arr[i] > arr[largestIndex]) {
largestIndex = i; // 更新最大值的索引
}
}
return &arr[largestIndex]; // 返回最大元素在原数组中的地址
}
需要注意的是,当函数返回指针时,该指针必须指向在函数外部(如调用者栈帧或静态/全局区)分配的内存,或者动态分配的内存。切勿返回指向函数内部局部(自动)变量的指针,因为这些变量在函数返回后其内存空间会被回收,导致指针变为"悬空指针",访问它会引发未定义行为。
3. 字符串数组与字符串指针的对比
在C语言中,字符串可以用字符数组或字符指针来表示,但它们的语义和行为存在显著差异。理解这些差异对于正确操作字符串至关重要。
3.1 使用字符数组存储字符串
字符数组提供了一个可修改的内存区域来存储字符串。数组名本身是一个常量指针,表示数组首元素的地址,不能被重新赋值指向其他地址。
#include <stdio.h>
#include <string.h> // 包含strlen和strcpy函数定义
#define MAX_STR_LEN 80
int main() {
char message1[MAX_STR_LEN] = "Programming is an art";
char message2[MAX_STR_LEN] = "Learning makes one wise";
char temporaryBuffer[MAX_STR_LEN];
printf("关于字符数组message1:\n");
printf("sizeof(message1) = %zu 字节 (数组总大小)\n", sizeof(message1));
printf("strlen(message1) = %zu 字节 (实际字符串长度,不含null终止符)\n", strlen(message1));
printf("\n交换前:\n");
printf("message1: %s\n", message1);
printf("message2: %s\n", message2);
printf("\n正在交换字符串内容...\n");
// 使用strcpy函数交换字符串内容
strcpy(temporaryBuffer, message1);
strcpy(message1, message2);
strcpy(message2, temporaryBuffer);
printf("\n交换后:\n");
printf("message1: %s\n", message1);
printf("message2: %s\n", message2);
return 0;
}
在此示例中:
sizeof(message1)计算的是整个字符数组message1在内存中占用的总字节数,即MAX_STR_LEN(80) 字节。strlen(message1)统计的是字符串"Programming is an art"的实际字符个数,直到遇到第一个空字符\0为止,不包括\0。- 由于
message1是一个数组名,它是一个常量指针,不能通过message1 = "..."这样的赋值语句来改变其指向的地址。 - 通过
strcpy函数,我们可以将一个字符串的内容完全复制到另一个字符数组中,从而实现字符串内容的交换。
3.2 使用字符指针指向字符串字面量
字符指针可以用来指向字符串字面量(存储在只读内存区域),或者指向其他已分配的字符数据。指针本身是变量,可以被重新赋值指向不同的地址。
#include <stdio.h>
#include <string.h> // 包含strlen函数定义
int main() {
char *strPtr1 = "Hello, C Programming!";
char *strPtr2 = "Mastering Pointers is key.";
char *tempPtr; // 临时指针用于交换
printf("关于字符指针strPtr1:\n");
printf("sizeof(strPtr1) = %zu 字节 (指针变量本身的大小)\n", sizeof(strPtr1));
printf("strlen(strPtr1) = %zu 字节 (指针所指字符串的实际长度)\n", strlen(strPtr1));
printf("\n交换前:\n");
printf("strPtr1: %s\n", strPtr1);
printf("strPtr2: %s\n", strPtr2);
printf("\n正在交换指针指向...\n");
// 交换指针变量的值,使其指向不同的字符串字面量
tempPtr = strPtr1;
strPtr1 = strPtr2;
strPtr2 = tempPtr;
printf("\n交换后:\n");
printf("strPtr1: %s\n", strPtr1);
printf("strPtr2: %s\n", strPtr2);
return 0;
}
在此示例中:
strPtr1存放的是字符串字面量"Hello, C Programming!"的首字符地址。sizeof(strPtr1)计算的是指针变量strPtr1本身在内存中占用的字节数(通常为4或8字节,取决于系统架构),而不是它所指向的字符串的长度。strlen(strPtr1)统计的是strPtr1指向的字符串的实际字符个数。char *strPtr1 = "..."声明了一个可修改的指针变量,它可以被重新赋值指向其他字符串字面量。- 交换
strPtr1和strPtr2的操作实际上是交换了它们各自存储的地址值,让它们指向了不同的字符串字面量。字符串字面量本身("Hello, C Programming!" 和 "Mastering Pointers is key.")在内存中的位置和内容并没有发生改变。
4. 多维数组的指针访问
多维数组在C语言中是按行主序存储的,理解其内存布局对于通过指针进行高效访问至关重要。以下示例展示了两种不同类型的指针来访问二维数组的元素:指向单个元素的指针和指向一维数组的指针。
#include <stdio.h>
int main() {
int matrix[2][4] = {{10, 20, 30, 40}, {50, 60, 70, 80}};
int *elementPtr; // 指向int类型数据的指针
int(*rowPtr)[4]; // 指向包含4个int元素的一维数组的指针
printf("--- 1. 使用数组名和下标直接访问二维数组元素 ---\n");
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 4; ++j)
printf("%d ", matrix[i][j]);
printf("\n");
}
printf("\n--- 2. 使用指向单个元素的指针 (elementPtr) 间接访问 ---\n");
// elementPtr 从二维数组的第一个元素的地址开始,遍历所有元素
for (elementPtr = &matrix[0][0]; elementPtr < &matrix[0][0] + (2 * 4); ++elementPtr) {
printf("%d ", *elementPtr);
// 每4个元素换行,模拟二维数组的行结构
if (((elementPtr - &matrix[0][0]) + 1) % 4 == 0)
printf("\n");
}
printf("\n--- 3. 使用指向一维数组的指针 (rowPtr) 间接访问 ---\n");
// rowPtr 从二维数组的第一行的地址开始,每次递增一个一维数组的大小
for (rowPtr = matrix; rowPtr < matrix + 2; ++rowPtr) {
// 在内层循环中,通过 *(rowPtr + j) 或 (*rowPtr)[j] 访问行内元素
for (int j = 0; j < 4; ++j)
printf("%d ", *(*rowPtr + j)); // 等价于 (*rowPtr)[j]
printf("\n");
}
return 0;
}
在这个示例中:
elementPtr(类型为int *) 被初始化为指向二维数组的第一个元素的地址。它可以像访问一维数组那样连续遍历所有元素,因为二维数组在内存中是连续存储的。rowPtr(类型为int (*)[4]) 被初始化为指向二维数组的第一行的地址。每次递增++rowPtr时,它会跳过一整行(即4个int类型的空间),指向下一行的起始地址。在内层循环中,通过*(*rowPtr + j)这样的表达式来访问当前行中的第j个元素。
5. 字符串字符替换
对字符串进行字符替换是常见的文本处理操作。下面的函数演示了如何将字符串中所有指定字符替换为新的字符。
#include <stdio.h>
#include <string.h> // 包含strlen以备用
#define BUFFER_SIZE 80
// 函数声明:替换字符串中的特定字符
void replaceCharInString(char *targetString, char oldChar, char newChar);
int main() {
char sampleText[BUFFER_SIZE] = "The quick brown fox jumps over the lazy dog.";
printf("原始字符串: \n");
printf("%s\n", sampleText);
replaceCharInString(sampleText, 'o', '@'); // 调用函数替换字符 'o' 为 '@'
printf("替换后的字符串: \n");
printf("%s\n", sampleText);
return 0;
}
// 函数定义:替换字符串中的特定字符
void replaceCharInString(char *targetString, char oldChar, char newChar) {
// 遍历字符串直到遇到空字符 '\0'
while (*targetString != '\0') {
if (*targetString == oldChar) {
*targetString = newChar; // 如果当前字符匹配,则替换
}
targetString++; // 移动到下一个字符
}
}
在 replaceCharInString 函数中,循环条件 while (*targetString != '\0') 等价于 while (*targetString),因为空字符 '\0' 的ASCII值为0,在C语言中被视为假,非空字符被视为真。
6. 字符串按字符截断
有时我们需要从字符串的某个特定字符处截断它。以下函数实现了根据第一个出现的指定字符来截断字符串的功能。
#include <stdio.h>
#include <string.h> // 包含strlen以备用
#define MAX_INPUT 80
// 函数声明:在指定字符处截断字符串
char *truncateStringAtChar(char *inputStr, char separatorChar);
int main() {
char inputBuffer[MAX_INPUT];
char stopChar;
while (printf("请输入一个字符串: "), fgets(inputBuffer, MAX_INPUT, stdin) != NULL) {
// 移除fgets可能读取的换行符
inputBuffer[strcspn(inputBuffer, "\n")] = 0;
printf("请输入一个截断字符: ");
stopChar = getchar();
// 清除输入缓冲区中剩余的换行符,避免影响下一次gets或getchar
while (getchar() != '\n');
printf("正在执行截断处理...\n");
truncateStringAtChar(inputBuffer, stopChar); // 调用函数
printf("截断后的字符串: %s\n\n", inputBuffer);
}
return 0;
}
// 函数定义:在指定字符处截断字符串
// 找到inputStr中第一次出现separatorChar的位置,并将其替换为空字符'\0'
char *truncateStringAtChar(char *inputStr, char separatorChar) {
char *currentPtr = inputStr; // 使用临时指针遍历
// 遍历字符串直到找到分隔符或到达字符串末尾
while (*currentPtr != separatorChar && *currentPtr != '\0') {
currentPtr++;
}
*currentPtr = '\0'; // 将找到的分隔符或字符串末尾替换为空字符,完成截断
return inputStr; // 返回原始字符串指针
}
在 main 函数中,getchar() 的使用尤其需要注意。在 fgets() 读取一行输入后,通常会留下一个换行符在输入缓冲区中。如果没有在 getchar() 之后添加额外的 while (getchar() != '\n'); 来清除这个换行符,那么在后续的 getchar() 调用时,它可能会立即读取到这个残留在缓冲区中的换行符,导致程序行为异常,例如跳过用户输入。
7. 身份证号格式初步校验
字符串校验是数据处理中的常见任务。下面提供一个简单的函数,用于初步检查一个字符串是否符合中国18位身份证号码的基本形式(前17位数字,最后一位数字或'X')。
#include <stdio.h>
#include <string.h> // 包含strlen函数
#include <stdlib.h> // 包含system函数
#define ID_COUNT 5
// 函数声明:检查身份证号码的格式合法性
int checkIDFormat(const char *idString);
int main() {
char *idList[ID_COUNT] = {
"31010120000721656X",
"3301061996X0203301", // 格式不正确,第11位是X
"53010220051126571", // 长度不正确
"510104199211197977",
"53010220051126133Y" // 格式不正确,最后一位是Y
};
printf("身份证号码格式校验结果:\n");
for (int i = 0; i < ID_COUNT; ++i) {
if (checkIDFormat(idList[i])) {
printf("%s\t合法\n", idList[i]);
} else {
printf("%s\t非法\n", idList[i]);
}
}
// system("pause"); // 仅Windows环境使用,暂停程序
return 0;
}
// 函数定义:检查身份证号码的格式合法性
// 功能: 如果指针idString指向的身份证号码串形式上合法,返回1,否则返回0
// 形式合法定义:长度为18位,前17位是数字,第18位是数字或大写'X'
int checkIDFormat(const char *idString) {
int len = strlen(idString);
// 1. 检查长度是否为18位
if (len != 18) {
return 0;
}
// 2. 检查前17位是否都是数字
for (int i = 0; i < 17; ++i) {
if (idString[i] < '0' || idString[i] > '9') {
return 0; // 发现非数字字符
}
}
// 3. 检查第18位(最后一位)是否为数字或大写'X'
char lastChar = idString[17];
if ((lastChar >= '0' && lastChar <= '9') || lastChar == 'X') {
return 1; // 格式合法
} else {
return 0; // 最后一位不合法
}
}
这个 checkIDFormat 函数提供了一个基本的格式验证。实际的身份证号码校验还会涉及行政区划代码、出生日期和校验码的验证,这将更加复杂。
8. 简易凯撒密码编解码器
凯撒密码是一种简单的替换密码,通过将每个字母替换为字母表中固定间隔的字母来实现加密。以下C语言函数实现了对英文字符串的凯撒密码编解码功能。
#include <stdio.h>
#include <string.h> // 包含strlen以备用
#define TEXT_BUFFER_SIZE 80
// 函数声明:对字符串进行凯撒密码编码
void caesarEncode(char *text, int shift);
// 函数声明:对字符串进行凯撒密码解码
void caesarDecode(char *text, int shift);
int main() {
char secretWords[TEXT_BUFFER_SIZE];
int shiftValue;
printf("请输入英文文本: ");
fgets(secretWords, TEXT_BUFFER_SIZE, stdin);
// 移除fgets可能读取的换行符
secretWords[strcspn(secretWords, "\n")] = 0;
printf("请输入移位值n (整数): ");
scanf("%d", &shiftValue);
// 清除输入缓冲区中剩余的换行符
while (getchar() != '\n');
printf("编码后的英文文本: ");
caesarEncode(secretWords, shiftValue); // 调用编码函数
printf("%s\n", secretWords);
printf("对编码后的英文文本进行解码: ");
caesarDecode(secretWords, shiftValue); // 调用解码函数
printf("%s\n", secretWords);
return 0;
}
// 函数定义:对字符串进行凯撒密码编码
// 规则:字母a-z或A-Z根据移位值n进行替换,其他字符不变
void caesarEncode(char *text, int shift) {
// 确保移位值在0-25之间
shift = shift % 26;
if (shift < 0) shift += 26; // 处理负数移位,使其等效于正向移位
char *current = text;
while (*current != '\0') {
if (*current >= 'a' && *current <= 'z') {
// 小写字母处理
*current = 'a' + (*current - 'a' + shift) % 26;
} else if (*current >= 'A' && *current <= 'Z') {
// 大写字母处理
*current = 'A' + (*current - 'A' + shift) % 26;
}
current++;
}
}
// 函数定义:对字符串进行凯撒密码解码
// 解码是编码的逆操作,相当于将移位值反向应用
void caesarDecode(char *text, int shift) {
// 解码相当于进行负向移位
caesarEncode(text, -shift);
}
在凯撒密码的实现中,通过取模运算 % 26 确保了字母在循环移位后仍然保持在字母表范围内。解码函数 caesarDecode 可以通过调用 caesarEncode 并传入负的移位值来实现,这是利用了凯撒密码的对称性。
9. 命令行参数的排序
C语言的 main 函数可以接收命令行参数,通常通过 int argc 和 char *argv[] 来获取。argc 是参数的数量,argv 是一个指向字符串数组的指针数组,其中每个字符串是命令行中的一个参数。以下示例展示了如何对这些命令行参数进行排序。
#include <stdio.h>
#include <string.h> // 包含strcmp函数
// 函数声明:对命令行参数进行排序
void sortStringArray(int count, char *strings[]);
int main(int argc, char *argv[]) {
printf("原始命令行参数 (不包括程序名):\n");
for (int i = 1; i < argc; ++i) { // argv[0] 是程序名,从argv[1]开始是用户参数
printf("%s ", argv[i]);
}
printf("\n");
if (argc > 1) { // 只有当有用户参数时才进行排序
printf("正在对参数进行排序...\n");
sortStringArray(argc, argv); // 调用排序函数
printf("排序后的命令行参数:\n");
for (int i = 1; i < argc; ++i) {
printf("%s ", argv[i]);
}
printf("\n");
} else {
printf("没有提供额外的命令行参数进行排序。\n");
}
return 0;
}
// 函数定义:使用冒泡排序法对字符串数组(指针数组)进行排序
void sortStringArray(int count, char *strings[]) {
char *tempString;
// 冒泡排序外层循环
for (int j = 1; j < count - 1; ++j) { // 注意:我们只排序用户提供的参数,即从strings[1]到strings[count-1]
// 冒泡排序内层循环
for (int i = 1; i < count - j; ++i) { // strings[0] (程序名) 不参与排序
// 使用strcmp比较两个字符串
if (strcmp(strings[i], strings[i+1]) > 0) {
// 如果前一个字符串大于后一个,则交换指针
tempString = strings[i];
strings[i] = strings[i+1];
strings[i+1] = tempString;
}
}
}
}
在这个例子中,sortStringArray 函数接收 argc 和 argv。它使用冒泡排序算法,通过 strcmp 函数比较字符串,并交换 char* 指针来改变参数的顺序。注意,argv[0] 总是程序的名称,通常不参与排序。