跳到主要内容

数组与字符串

数组是存储相同类型元素的连续内存空间。字符串是字符数组的一种特殊形式,以空字符 \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 字符串函数(如 strcpystrcat)存在缓冲区溢出的风险。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;
}

常用安全函数:

传统函数安全版本说明
strcpystrcpy_s安全复制
strcatstrcat_s安全连接
strncpystrncpy_s安全指定长度复制
strncatstrncat_s安全指定长度连接
sprintfsprintf_s安全格式化
vsprintfvsprintf_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 中常用的 strdupstrndup 纳入标准库:

#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 语言最核心也是最重要的概念。