数组与字符串
数组是存储相同类型元素的连续内存空间。字符串是字符数组的一种特殊形式,以空字符 \0 结尾。
一维数组
数组声明与初始化
数组是固定大小的同类型元素集合:
int numbers[5]; // 声明一个包含 5 个整数的数组
int arr1[5] = {1, 2, 3, 4, 5}; // 完全初始化
int arr2[5] = {1, 2}; // 部分初始化,其余为 0
int arr3[5] = {0}; // 全部初始化为 0
int arr4[] = {1, 2, 3, 4, 5}; // 自动推断大小为 5
C99 支持指定初始化器:
int arr[10] = {[0] = 1, [5] = 2, [9] = 3}; // 指定位置初始化
// arr: {1, 0, 0, 0, 0, 2, 0, 0, 0, 3}
数组访问
使用下标访问数组元素,下标从 0 开始:
int arr[5] = {10, 20, 30, 40, 50};
printf("%d\n", arr[0]); // 10(第一个元素)
printf("%d\n", arr[4]); // 50(最后一个元素)
arr[2] = 100; // 修改第三个元素
数组越界访问是未定义行为,不会自动检查:
int arr[5] = {1, 2, 3, 4, 5};
printf("%d\n", arr[5]); // 越界!未定义行为
printf("%d\n", arr[-1]); // 越界!未定义行为
数组大小
使用 sizeof 获取数组大小:
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]); // 5
printf("数组字节数: %zu\n", sizeof(arr)); // 20(5 * 4)
printf("元素大小: %zu\n", sizeof(arr[0])); // 4
printf("元素个数: %d\n", size); // 5
数组遍历
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < size; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
C99 支持范围 for 循环的类似写法(仅限指针):
for (int* p = arr; p < arr + size; p++) {
printf("%d ", *p);
}
数组与函数
数组作为参数传递时退化为指针:
void print_array(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main(void) {
int arr[] = {1, 2, 3, 4, 5};
print_array(arr, 5);
return 0;
}
在函数内无法用 sizeof 获取数组大小,必须额外传递。
多维数组
二维数组
二维数组是"数组的数组":
int matrix[3][4]; // 3 行 4 列的矩阵
int m1[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
int m2[2][3] = {1, 2, 3, 4, 5, 6}; // 按行顺序初始化
int m3[2][3] = {{1, 2}, {4}}; // 部分初始化
// m3: {{1, 2, 0}, {4, 0, 0}}
二维数组访问
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
printf("%d\n", matrix[0][0]); // 1(第一行第一列)
printf("%d\n", matrix[1][2]); // 6(第二行第三列)
matrix[0][1] = 20; // 修改元素
二维数组遍历
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
int rows = 2, cols = 3;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", matrix[i][j]);
}
printf("\n");
}
二维数组与函数
void print_matrix(int rows, int cols, int matrix[rows][cols]) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", matrix[i][j]);
}
printf("\n");
}
}
int main(void) {
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
print_matrix(2, 3, matrix);
return 0;
}
C99 支持变长数组(VLA)作为参数,第一维可以省略:
void print_matrix(int matrix[][3], int rows); // 固定列数
void print_matrix_vla(int rows, int cols, int matrix[rows][cols]); // 变长
内存布局
二维数组在内存中按行连续存储:
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
内存布局:
地址: [0] [1] [2] [3] [4] [5]
值: 1 2 3 4 5 6
└─第一行─┘ └─第二行─┘
字符数组与字符串
字符数组
字符数组存储单个字符:
char chars[5] = {'H', 'e', 'l', 'l', 'o'};
字符串
字符串是以空字符 \0 结尾的字符数组:
char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 显式结尾
char str2[6] = "Hello"; // 字符串字面量,自动添加 \0
char str3[] = "Hello"; // 自动推断大小为 6
char str4[] = "Hello"; // 包含 \0,大小为 6
字符串字面量自动在末尾添加 \0,声明数组时要预留空间。
字符串输入输出
char name[50];
printf("请输入姓名: ");
scanf("%s", name); // 遇到空白符停止
printf("你好, %s\n", name);
scanf 读取字符串的问题:
- 遇到空格、制表符、换行符停止
- 不检查缓冲区大小,可能溢出
安全的输入方式:
char name[50];
printf("请输入姓名: ");
fgets(name, sizeof(name), stdin); // 安全,读取一行
// 去除末尾的换行符
name[strcspn(name, "\n")] = '\0';
printf("你好, %s\n", name);
字符串长度
strlen 返回字符串的实际长度(不含 \0):
#include <string.h>
char str[] = "Hello";
printf("长度: %zu\n", strlen(str)); // 5
printf("数组大小: %zu\n", sizeof(str)); // 6(包含 \0)
字符串函数
<string.h> 提供了丰富的字符串操作函数:
#include <string.h>
char s1[20] = "Hello";
char s2[20] = "World";
char s3[20];
// 字符串长度
size_t len = strlen(s1); // 5
// 字符串复制
strcpy(s3, s1); // s3 = "Hello"
strncpy(s3, s1, sizeof(s3) - 1); // 安全版本
s3[sizeof(s3) - 1] = '\0'; // 确保以 \0 结尾
// 字符串连接
strcat(s1, " "); // s1 = "Hello "
strcat(s1, s2); // s1 = "Hello World"
strncat(s1, s2, sizeof(s1) - strlen(s1) - 1); // 安全版本
// 字符串比较
int result = strcmp("apple", "banana"); // 返回负数(apple < banana)
result = strcmp("apple", "apple"); // 返回 0(相等)
result = strcmp("banana", "apple"); // 返回正数(banana > apple)
// 字符串查找
char* p = strchr("Hello", 'l'); // 查找字符,返回 "llo"
char* q = strstr("Hello", "ll"); // 查找子串,返回 "llo"
字符串比较
strcmp 按字典序比较字符串:
int compare(const char* s1, const char* s2) {
int result = strcmp(s1, s2);
if (result < 0) {
printf("%s < %s\n", s1, s2);
} else if (result > 0) {
printf("%s > %s\n", s1, s2);
} else {
printf("%s == %s\n", s1, s2);
}
return result;
}
注意:不能直接用 == 比较字符串内容:
char s1[] = "Hello";
char s2[] = "Hello";
if (s1 == s2) { // 错误!比较的是地址
// ...
}
if (strcmp(s1, s2) == 0) { // 正确!比较内容
// ...
}
字符串转换
<stdlib.h> 提供字符串与数值的转换函数:
#include <stdlib.h>
// 字符串转整数
int num1 = atoi("123"); // 123
long num2 = atol("123456"); // 123456
// 字符串转浮点数
double d1 = atof("3.14"); // 3.14
// 更安全的转换函数
char* endptr;
long num3 = strtol("123abc", &endptr, 10); // 123, endptr 指向 "abc"
double d2 = strtod("3.14abc", &endptr); // 3.14
// 数值转字符串
char buffer[20];
sprintf(buffer, "%d", 123); // buffer = "123"
snprintf(buffer, sizeof(buffer), "%f", 3.14); // 安全版本
字符串数组
字符串数组
存储多个字符串:
char fruits[3][10] = {
"Apple",
"Banana",
"Orange"
};
for (int i = 0; i < 3; i++) {
printf("%s\n", fruits[i]);
}
指针数组
更灵活的方式是使用指针数组:
const char* fruits[] = {
"Apple",
"Banana",
"Orange"
};
for (int i = 0; i < 3; i++) {
printf("%s\n", fruits[i]);
}
区别:
char arr[3][10]:每个字符串占用固定 10 字节char* arr[3]:每个指针指向不同长度的字符串
常见操作示例
字符串反转
void reverse_string(char* str) {
int len = strlen(str);
for (int i = 0; i < len / 2; i++) {
char temp = str[i];
str[i] = str[len - 1 - i];
str[len - 1 - i] = temp;
}
}
int main(void) {
char str[] = "Hello";
reverse_string(str);
printf("%s\n", str); // olleH
return 0;
}
统计字符出现次数
int count_char(const char* str, char ch) {
int count = 0;
for (int i = 0; str[i] != '\0'; i++) {
if (str[i] == ch) {
count++;
}
}
return count;
}
int main(void) {
char str[] = "Hello, World!";
printf("'l' 出现 %d 次\n", count_char(str, 'l')); // 3
return 0;
}
字符串分割
#include <string.h>
int main(void) {
char str[] = "apple,banana,orange";
char* token = strtok(str, ",");
while (token != NULL) {
printf("%s\n", token);
token = strtok(NULL, ","); // 后续调用传 NULL
}
return 0;
}
输出:
apple
banana
orange
注意:strtok 会修改原字符串,将分隔符替换为 \0。
删除字符串中的字符
void remove_char(char* str, char ch) {
int j = 0;
for (int i = 0; str[i] != '\0'; i++) {
if (str[i] != ch) {
str[j++] = str[i];
}
}
str[j] = '\0';
}
int main(void) {
char str[] = "Hello, World!";
remove_char(str, 'l');
printf("%s\n", str); // Heo, Word!
return 0;
}
安全字符串处理
传统的 C 字符串函数(如 strcpy、strcat)存在缓冲区溢出的风险。C11 和 C23 引入了更安全的替代函数。
边界检查函数(C11 Annex K)
C11 的 Annex K 定义了一系列安全函数,这些函数会检查缓冲区边界:
#define __STDC_WANT_LIB_EXT1__ 1
#include <string.h>
int main(void) {
char dest[10];
const char* src = "Hello";
// 安全版本:指定目标缓冲区大小
errno_t err = strcpy_s(dest, sizeof(dest), src);
if (err != 0) {
printf("复制失败\n");
}
// 安全连接
err = strcat_s(dest, sizeof(dest), " World");
if (err != 0) {
printf("连接失败\n");
}
return 0;
}
常用安全函数:
| 传统函数 | 安全版本 | 说明 |
|---|---|---|
strcpy | strcpy_s | 安全复制 |
strcat | strcat_s | 安全连接 |
strncpy | strncpy_s | 安全指定长度复制 |
strncat | strncat_s | 安全指定长度连接 |
sprintf | sprintf_s | 安全格式化 |
vsprintf | vsprintf_s | 安全可变参数格式化 |
注意:Annex K 是可选特性,并非所有编译器都支持。Windows 的 MSVC 支持较好,而 GCC 和 Clang 的支持有限。
使用 snprintf 替代 sprintf
snprintf 是广泛支持的替代方案:
char buffer[20];
int written = snprintf(buffer, sizeof(buffer), "值: %d", 12345);
if (written < 0 || written >= (int)sizeof(buffer)) {
printf("输出被截断或出错\n");
}
自定义安全字符串函数
在不支持 Annex K 的环境中,可以实现自己的安全版本:
#include <string.h>
#include <stdbool.h>
// 安全复制字符串
bool safe_strcpy(char* dest, size_t dest_size, const char* src) {
if (dest == NULL || src == NULL || dest_size == 0) {
return false;
}
size_t src_len = strlen(src);
if (src_len >= dest_size) {
// 源字符串太长,截断复制
memcpy(dest, src, dest_size - 1);
dest[dest_size - 1] = '\0';
return false; // 表示截断
}
strcpy(dest, src);
return true;
}
// 安全连接字符串
bool safe_strcat(char* dest, size_t dest_size, const char* src) {
if (dest == NULL || src == NULL || dest_size == 0) {
return false;
}
size_t dest_len = strlen(dest);
size_t src_len = strlen(src);
if (dest_len + src_len >= dest_size) {
// 空间不足,截断连接
size_t available = dest_size - dest_len - 1;
if (available > 0) {
strncat(dest, src, available);
}
return false;
}
strcat(dest, src);
return true;
}
高级字符串操作
字符串构建器
频繁的字符串拼接效率低下,可以使用字符串构建器模式:
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
typedef struct {
char* buffer;
size_t length;
size_t capacity;
} StringBuilder;
StringBuilder* sb_create(size_t initial_capacity) {
StringBuilder* sb = malloc(sizeof(StringBuilder));
if (sb == NULL) return NULL;
sb->buffer = malloc(initial_capacity);
if (sb->buffer == NULL) {
free(sb);
return NULL;
}
sb->buffer[0] = '\0';
sb->length = 0;
sb->capacity = initial_capacity;
return sb;
}
void sb_free(StringBuilder* sb) {
if (sb != NULL) {
free(sb->buffer);
free(sb);
}
}
// 确保有足够空间
static int sb_ensure_capacity(StringBuilder* sb, size_t needed) {
if (sb->length + needed < sb->capacity) {
return 1;
}
size_t new_capacity = sb->capacity * 2;
while (new_capacity < sb->length + needed + 1) {
new_capacity *= 2;
}
char* new_buffer = realloc(sb->buffer, new_capacity);
if (new_buffer == NULL) return 0;
sb->buffer = new_buffer;
sb->capacity = new_capacity;
return 1;
}
void sb_append(StringBuilder* sb, const char* str) {
if (sb == NULL || str == NULL) return;
size_t len = strlen(str);
if (!sb_ensure_capacity(sb, len)) return;
memcpy(sb->buffer + sb->length, str, len + 1);
sb->length += len;
}
void sb_append_char(StringBuilder* sb, char ch) {
if (sb == NULL) return;
if (!sb_ensure_capacity(sb, 1)) return;
sb->buffer[sb->length++] = ch;
sb->buffer[sb->length] = '\0';
}
void sb_appendf(StringBuilder* sb, const char* fmt, ...) {
if (sb == NULL || fmt == NULL) return;
va_list args;
va_start(args, fmt);
// 先计算需要的空间
va_list args_copy;
va_copy(args_copy, args);
int needed = vsnprintf(NULL, 0, fmt, args_copy);
va_end(args_copy);
if (needed < 0 || !sb_ensure_capacity(sb, needed + 1)) {
va_end(args);
return;
}
vsnprintf(sb->buffer + sb->length, needed + 1, fmt, args);
sb->length += needed;
va_end(args);
}
const char* sb_to_string(StringBuilder* sb) {
return sb != NULL ? sb->buffer : "";
}
// 使用示例
int main(void) {
StringBuilder* sb = sb_create(64);
sb_append(sb, "Hello");
sb_append(sb, ", ");
sb_append(sb, "World");
sb_appendf(sb, "! 数字: %d", 42);
printf("%s\n", sb_to_string(sb)); // Hello, World! 数字: 42
sb_free(sb);
return 0;
}
字符串分割器
比 strtok 更安全的字符串分割实现:
#include <string.h>
#include <stdlib.h>
typedef struct {
const char* str;
const char* delimiters;
size_t pos;
} StringSplitter;
StringSplitter* split_create(const char* str, const char* delimiters) {
StringSplitter* ss = malloc(sizeof(StringSplitter));
if (ss == NULL) return NULL;
ss->str = str;
ss->delimiters = delimiters;
ss->pos = 0;
return ss;
}
void split_free(StringSplitter* ss) {
free(ss);
}
// 获取下一个分割片段,返回 NULL 表示结束
// 注意:返回的字符串需要调用者释放
char* split_next(StringSplitter* ss) {
if (ss == NULL || ss->str == NULL) return NULL;
// 跳过前导分隔符
while (ss->str[ss->pos] != '\0' &&
strchr(ss->delimiters, ss->str[ss->pos]) != NULL) {
ss->pos++;
}
if (ss->str[ss->pos] == '\0') {
return NULL; // 没有更多片段
}
// 找到片段结束位置
size_t start = ss->pos;
while (ss->str[ss->pos] != '\0' &&
strchr(ss->delimiters, ss->str[ss->pos]) == NULL) {
ss->pos++;
}
size_t len = ss->pos - start;
char* token = malloc(len + 1);
if (token != NULL) {
memcpy(token, ss->str + start, len);
token[len] = '\0';
}
return token;
}
// 使用示例
int main(void) {
const char* text = "apple, banana, orange";
StringSplitter* ss = split_create(text, ", ");
char* token;
while ((token = split_next(ss)) != NULL) {
printf("'%s'\n", token);
free(token);
}
split_free(ss);
return 0;
}
字符串修剪
#include <ctype.h>
#include <string.h>
// 去除字符串两端的空白字符
char* str_trim(char* str) {
if (str == NULL) return NULL;
// 去除前导空白
char* start = str;
while (isspace((unsigned char)*start)) {
start++;
}
// 全部是空白
if (*start == '\0') {
str[0] = '\0';
return str;
}
// 去除尾部空白
char* end = start + strlen(start) - 1;
while (end > start && isspace((unsigned char)*end)) {
end--;
}
*(end + 1) = '\0';
// 移动到开头
if (start != str) {
memmove(str, start, end - start + 2);
}
return str;
}
// 使用示例
char text[] = " Hello, World! ";
str_trim(text);
printf("'%s'\n", text); // 'Hello, World!'
字符串替换
#include <string.h>
#include <stdlib.h>
// 替换字符串中的所有匹配项
char* str_replace(const char* str, const char* old_sub, const char* new_sub) {
if (str == NULL || old_sub == NULL || new_sub == NULL) {
return NULL;
}
size_t old_len = strlen(old_sub);
size_t new_len = strlen(new_sub);
// 计算需要的新大小
size_t count = 0;
const char* pos = str;
while ((pos = strstr(pos, old_sub)) != NULL) {
count++;
pos += old_len;
}
size_t new_size = strlen(str) + count * (new_len - old_len) + 1;
char* result = malloc(new_size);
if (result == NULL) return NULL;
char* dest = result;
const char* src = str;
while ((pos = strstr(src, old_sub)) != NULL) {
size_t prefix_len = pos - src;
memcpy(dest, src, prefix_len);
dest += prefix_len;
memcpy(dest, new_sub, new_len);
dest += new_len;
src = pos + old_len;
}
strcpy(dest, src);
return result;
}
// 使用示例
char* result = str_replace("Hello World World", "World", "C");
printf("%s\n", result); // Hello C C
free(result);
C23 字符串新特性
strdup 和 strndup 标准化
C23 将 POSIX 中常用的 strdup 和 strndup 纳入标准库:
#include <string.h>
char* strdup(const char* s); // 复制字符串,返回新分配的内存
char* strndup(const char* s, size_t n); // 复制最多 n 个字符
// 使用示例
const char* original = "Hello, World!";
// strdup:完整复制
char* copy = strdup(original);
if (copy != NULL) {
printf("复制: %s\n", copy);
free(copy); // 记得释放
}
// strndup:复制前 n 个字符
char* prefix = strndup(original, 5);
if (prefix != NULL) {
printf("前缀: %s\n", prefix); // "Hello"
free(prefix);
}
memccpy
memccpy 复制直到遇到指定字符:
#include <string.h>
void* memccpy(void* dest, const void* src, int c, size_t n);
// 使用示例
char dest[100];
const char* src = "Hello,World!";
void* result = memccpy(dest, src, ',', 100);
// dest 现在包含 "Hello,"
// result 指向 dest 中的 ',' 之后的位置
if (result != NULL) {
printf("复制到 ',' 停止,剩余位置: %ld\n",
(char*)result - dest);
}
memset_explicit
安全的内存清除,保证不会被优化器消除:
#include <string.h>
void clear_password(char* password, size_t len) {
// 普通 memset 可能被编译器优化掉
// memset(password, 0, len);
// memset_explicit 保证内存被清除
memset_explicit(password, 0, len);
}
// 使用示例
char password[100] = "my_secret_password";
clear_password(password, sizeof(password));
// 密码内存被安全清除
UTF-8 支持
C23 增强了对 UTF-8 的支持:
#include <uchar.h>
// char8_t 类型
char8_t ch = u8'A';
const char8_t* utf8_str = u8"你好,世界";
// 多字节与 UTF-8 转换
size_t mbrtoc8(char8_t* pc8, const char* s, size_t n, mbstate_t* ps);
size_t c8rtomb(char* s, char8_t c8, mbstate_t* ps);
二进制格式化
printf 系列函数支持 %b 格式:
#include <stdio.h>
int main(void) {
int value = 42;
// C23 二进制输出
printf("二进制: %b\n", value); // 101010
printf("8位: %08b\n", value); // 00101010
printf("带前缀: %#b\n", value); // 0b101010
return 0;
}
字符串最佳实践
1. 始终检查缓冲区大小
// 不好:不检查大小
char buffer[10];
strcpy(buffer, "This is a very long string"); // 溢出!
// 好:使用安全的函数
char buffer[10];
snprintf(buffer, sizeof(buffer), "Long string"); // 安全截断
2. 正确处理字符串终止
// 创建字符串时确保终止
char buffer[10];
memcpy(buffer, "Hello", 5);
buffer[5] = '\0'; // 必须手动终止
// 或使用 strncpy 后手动终止
strncpy(buffer, source, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';
3. 使用 const 保护只读字符串
size_t count_char(const char* str, char ch) {
size_t count = 0;
for (; *str; str++) {
if (*str == ch) count++;
}
return count;
}
// 编译器会阻止意外修改
// str[0] = 'X'; // 编译错误
4. 检查 NULL 指针
size_t safe_strlen(const char* str) {
if (str == NULL) return 0;
return strlen(str);
}
5. 避免返回局部字符串
// 错误:返回局部变量
char* format_name(const char* name) {
char buffer[100];
snprintf(buffer, sizeof(buffer), "Name: %s", name);
return buffer; // 错误!返回局部变量地址
}
// 正确方案1:返回动态分配
char* format_name(const char* name) {
char* buffer = malloc(100);
if (buffer != NULL) {
snprintf(buffer, 100, "Name: %s", name);
}
return buffer; // 调用者负责释放
}
// 正确方案2:通过参数返回
void format_name(const char* name, char* buffer, size_t size) {
snprintf(buffer, size, "Name: %s", name);
}
小结
本章介绍了 C 语言的数组与字符串:
- 一维数组:声明、初始化、访问、遍历
- 多维数组:二维数组的声明、初始化、遍历
- 字符数组与字符串:字符串的本质、输入输出
- 字符串函数:strlen、strcpy、strcat、strcmp、strstr 等
- 字符串转换:atoi、atof、sprintf 等
- 常见字符串操作:反转、统计、分割、删除
- 安全字符串处理:边界检查函数、snprintf、自定义安全函数
- 高级字符串操作:字符串构建器、分割器、修剪、替换
- C23 新特性:strdup、strndup、memset_explicit、UTF-8 支持、二进制格式化
- 字符串最佳实践
下一章将学习 指针,这是 C 语言最核心也是最重要的概念。