1. C语言字符串的本质与内存表示
在C语言中,字符串本质上是以空字符'\0'结尾的一维字符数组。这个看似简单的定义背后隐藏着几个关键特性:
- 字符串必须以'\0'(ASCII值为0)作为结束标志
- 字符串在内存中是连续存储的字符序列
- 字符串长度不固定,由'\0'的位置决定
1.1 字符串的内存布局
让我们通过一个具体例子来看字符串的内存表示:
c复制char greeting[] = "Hello";
这段代码会在内存中分配6个字节的空间(5个字符+'\0'),布局如下:
| 索引 | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| 值 | H | e | l | l | o | \0 |
注意:初学者常犯的错误是忘记为'\0'预留空间。例如
char str[5] = "Hello"会导致未定义行为,因为数组没有足够空间存放结束符。
1.2 字符串的初始化方式
C语言提供了多种初始化字符串的方法:
- 逐个字符初始化:
c复制char s1[] = {'H','e','l','l','o','\0'};
- 使用字符串字面量:
c复制char s2[] = "Hello";
- 指定数组大小(必须包含'\0'):
c复制char s3[6] = "Hello";
- 指针方式(字符串常量):
c复制const char *s4 = "Hello";
每种方式有不同特点:
- 前三种创建的是可修改的字符数组
- 指针方式指向的是只读内存区的字符串常量
- 省略大小时,数组大小会自动计算为字符串长度+1
2. 字符串与字符的区别
2.1 基本概念对比
字符和字符串在C语言中是两个密切相关但本质不同的概念:
| 特性 | 字符(char) | 字符串 |
|---|---|---|
| 表示方式 | 单引号,如 'A' | 双引号,如 "ABC" |
| 内存占用 | 1字节 | 长度+1字节(含\0) |
| 本质 | 整型值(ASCII码) | 字符数组 |
| 常用操作 | 比较、赋值 | 复制、连接、比较等 |
2.2 常见混淆场景
- 单引号与双引号混用:
c复制char c = "A"; // 错误!"A"是字符串,不是字符
char *s = 'A'; // 错误!'A'是字符,不是字符串
- 忘记字符串结束符:
c复制char name[3] = {'J','o','e'}; // 不是合法字符串,缺少\0
printf("%s", name); // 可能导致内存越界访问
- 修改字符串常量:
c复制char *p = "constant";
p[0] = 'C'; // 未定义行为!字符串常量不可修改
3. 标准字符串处理函数
C标准库<string.h>提供了一系列字符串处理函数,这些函数都依赖于'\0'来确定字符串的结束位置。
3.1 核心字符串函数详解
- strlen - 计算字符串长度
c复制size_t strlen(const char *str);
特点:
- 不计算'\0'
- 时间复杂度O(n),需要遍历整个字符串
- 与sizeof的区别:sizeof返回数组总大小,strlen返回实际字符数
- strcpy - 字符串复制
c复制char *strcpy(char *dest, const char *src);
注意事项:
- 必须确保dest有足够空间
- 不会自动检查缓冲区溢出
- 安全版本:strcpy_s(C11)
- strcat - 字符串连接
c复制char *strcat(char *dest, const char *src);
典型用法:
c复制char path[100] = "/home/";
strcat(path, username); // 注意path要有足够剩余空间
- strcmp - 字符串比较
c复制int strcmp(const char *s1, const char *s2);
返回值:
- 0:字符串相等
- <0:s1小于s2
-
0:s1大于s2
3.2 安全字符串函数(C11)
为避免缓冲区溢出,C11引入了安全版本:
c复制errno_t strcpy_s(char *dest, rsize_t destsz, const char *src);
errno_t strcat_s(char *dest, rsize_t destsz, const char *src);
这些函数需要指定目标缓冲区大小,在溢出时返回错误而非导致未定义行为。
4. 字符串操作实战技巧
4.1 手动实现字符串函数
理解字符串函数的最好方式是手动实现它们:
- strlen实现:
c复制size_t my_strlen(const char *s) {
size_t n = 0;
while (*s++) n++;
return n;
}
- strcpy实现:
c复制char *my_strcpy(char *dest, const char *src) {
char *ret = dest;
while ((*dest++ = *src++));
return ret;
}
4.2 常见字符串处理模式
- 字符串遍历:
c复制for (int i = 0; str[i] != '\0'; i++) {
// 处理str[i]
}
- 字符串反转:
c复制void reverse(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;
}
}
- 字符串分割(模拟strtok):
c复制char *split(char *str, const char *delim) {
static char *last = NULL;
if (str) last = str;
if (!last || !*last) return NULL;
char *token = last;
while (*last && !strchr(delim, *last)) last++;
if (*last) *last++ = '\0';
return token;
}
4.3 性能优化技巧
- 避免重复计算字符串长度:
c复制// 不好
for (int i = 0; i < strlen(s); i++) {...}
// 好
size_t len = strlen(s);
for (int i = 0; i < len; i++) {...}
- 使用memcpy处理已知长度的字符串:
c复制char buf[100];
memcpy(buf, src, strlen(src)+1); // 比strcpy更快
- 批量操作时考虑缓存友好性:
c复制// 按顺序处理字符串数组,不要随机跳转
5. 字符串与指针的复杂关系
5.1 指针与字符数组
字符数组和字符指针常被混淆,但它们有本质区别:
c复制char arr[] = "hello"; // 可修改的数组
char *ptr = "world"; // 指向只读常量的指针
关键差异:
- arr是数组名,ptr是指针变量
- sizeof(arr)返回数组大小,sizeof(ptr)返回指针大小
- arr的地址不可变,ptr可以指向其他字符串
5.2 指针运算与字符串
指针运算让字符串处理更高效:
c复制char *find_char(char *str, char c) {
while (*str && *str != c) str++;
return *str ? str : NULL;
}
5.3 多级指针与字符串数组
字符串数组的两种表示方式:
- 二维字符数组:
c复制char names[][10] = {"Alice", "Bob", "Charlie"};
- 指针数组:
c复制char *names[] = {"Alice", "Bob", "Charlie"};
内存布局差异:
- 二维数组是连续内存块,每个元素大小相同
- 指针数组元素大小不同,指向不同内存区域
6. 字符串处理中的常见陷阱
6.1 缓冲区溢出
这是最危险也最常见的错误:
c复制char buf[10];
strcpy(buf, "This string is too long"); // 缓冲区溢出!
防御措施:
- 使用strncpy替代strcpy
- 使用snprintf格式化输出
- 启用编译器栈保护选项
6.2 未初始化的指针
c复制char *str;
strcpy(str, "hello"); // 灾难!str未初始化
正确做法:
c复制char str[100]; // 或
char *str = malloc(100);
6.3 字符串常量修改
c复制char *p = "constant";
p[0] = 'C'; // 未定义行为
解决方案:
c复制const char *p = "constant"; // 明确声明不可修改
6.4 忘记分配终止符
c复制char s[3] = "abc"; // 没有空间放'\0'
正确做法:
c复制char s[4] = "abc"; // 或
char s[] = "abc"; // 让编译器计算大小
7. 现代C语言中的字符串处理
7.1 安全函数族(C11)
C11标准引入的安全函数:
c复制strcpy_s(dest, dest_size, src);
strcat_s(dest, dest_size, src);
strlen_s(src, max_size);
这些函数需要:
- 明确指定目标缓冲区大小
- 在错误时调用约束处理函数
- 提供更安全的替代方案
7.2 宽字符与多字节字符串
处理国际化字符串:
c复制#include <wchar.h>
wchar_t wstr[] = L"宽字符字符串";
相关函数:
- mbstowcs:多字节转宽字符
- wcstombs:宽字符转多字节
- wcslen:宽字符字符串长度
7.3 第三方字符串库
对于复杂项目,可以考虑:
- bstring:更安全的字符串库
- ICU:Unicode处理库
- GLib:GNOME基础库中的字符串工具
8. 字符串应用实例
8.1 实现一个简单的字符串类
虽然C没有内置字符串类,但我们可以模拟:
c复制typedef struct {
char *data;
size_t length;
size_t capacity;
} String;
String string_create(const char *init) {
String s;
s.length = strlen(init);
s.capacity = s.length + 1;
s.data = malloc(s.capacity);
strcpy(s.data, init);
return s;
}
void string_append(String *s, const char *str) {
size_t new_len = s->length + strlen(str);
if (new_len + 1 > s->capacity) {
s->capacity = new_len * 2 + 1;
s->data = realloc(s->data, s->capacity);
}
strcat(s->data, str);
s->length = new_len;
}
8.2 解析CSV文件
c复制void parse_csv(const char *filename) {
FILE *fp = fopen(filename, "r");
if (!fp) return;
char line[1024];
while (fgets(line, sizeof(line), fp)) {
char *token = strtok(line, ",");
while (token) {
printf("Field: %s\n", token);
token = strtok(NULL, ",");
}
}
fclose(fp);
}
8.3 实现基础加密算法
简单的字符串加密示例:
c复制void xor_encrypt(char *str, char key) {
for (; *str; str++) {
*str ^= key;
}
}
// 使用:xor_encrypt(password, 0xAA);
在实际项目中,应该使用标准加密库如OpenSSL,而非自己实现加密算法。
