在C语言中,字符串和指针是两个紧密关联的核心概念。字符串本质上是以空字符'\0'结尾的字符数组,而指针则是存储内存地址的变量。理解它们的交互方式是掌握C语言的关键。
C语言没有专门的字符串类型,而是通过字符数组来表示。例如:
c复制char str[] = "Hello";
实际上在内存中存储为:'H','e','l','l','o','\0',共6个字节。这个空字符'\0'是字符串结束的标志,所有标准字符串处理函数都依赖它来确定字符串长度。
注意:忘记在手动构建字符串时添加'\0'是常见错误,会导致后续操作出现不可预测的行为。
指针可以指向字符串的首字符,通过指针的算术运算可以遍历整个字符串:
c复制char *ptr = "World"; // 指针指向字符串字面量
while(*ptr != '\0') {
printf("%c", *ptr);
ptr++;
}
字符串字面量(如"World")实际上存储在程序的只读数据段,尝试修改它们会导致未定义行为。
最安全的字符串声明方式是使用字符数组:
c复制char str1[10] = "Hello"; // 显式指定大小
char str2[] = "World"; // 编译器自动计算大小
这种方式分配的数组在栈上,可以安全修改内容。
使用指针声明字符串时需特别注意:
c复制char *ptr1 = "Constant"; // 指向只读字符串字面量
char ptr2[] = "Mutable"; // 可修改的字符数组
ptr1指向的字符串不能修改,而ptr2可以修改。这是新手常混淆的点。
对于运行时确定长度的字符串,可以使用动态内存分配:
c复制char *dyn_str = malloc(50 * sizeof(char));
strcpy(dyn_str, "Dynamic string");
// 使用完毕后必须释放
free(dyn_str);
标准库的strlen()函数用于获取字符串长度,其基本原理是:
c复制size_t my_strlen(const char *str) {
size_t len = 0;
while(*str++ != '\0') len++;
return len;
}
这个实现展示了如何通过指针遍历字符串直到遇到'\0'。
标准strcpy()不检查目标缓冲区大小,可能导致缓冲区溢出。更安全的做法是:
c复制char *safe_strcpy(char *dest, const char *src, size_t dest_size) {
size_t i;
for(i = 0; i < dest_size-1 && src[i] != '\0'; i++) {
dest[i] = src[i];
}
dest[i] = '\0';
return dest;
}
strcmp()比较两个字符串的字典序:
实现示例:
c复制int my_strcmp(const char *s1, const char *s2) {
while(*s1 && (*s1 == *s2)) {
s1++;
s2++;
}
return *(const unsigned char*)s1 - *(const unsigned char*)s2;
}
当需要处理多个字符串时,可以使用指针数组:
c复制char *fruits[] = {"Apple", "Banana", "Cherry"};
for(int i = 0; i < 3; i++) {
printf("%s\n", fruits[i]);
}
这种方式比二维字符数组更节省内存,特别是字符串长度差异较大时。
从函数返回字符串需要特别注意生命周期问题:
c复制// 错误示范:返回局部数组的指针
char *bad_func() {
char str[] = "Hello";
return str; // 数组生命周期结束
}
// 正确做法1:返回静态变量
char *safe_func1() {
static char str[] = "Hello";
return str;
}
// 正确做法2:动态分配
char *safe_func2() {
char *str = malloc(6);
strcpy(str, "Hello");
return str;
}
实现类似strtok()的字符串分割功能:
c复制char *my_strtok(char *str, const char *delim) {
static char *last = NULL;
if(str) last = str;
if(!last || !*last) return NULL;
char *start = last;
while(*last && !strchr(delim, *last)) last++;
if(*last) {
*last = '\0';
last++;
} else {
last = NULL;
}
return start;
}
字符串操作中最常见的错误是段错误,主要原因包括:
调试技巧:
预防缓冲区溢出的最佳实践:
字符串操作可能成为性能瓶颈,优化建议:
我们将实现以下功能:
c复制// safe_str.h
#ifndef SAFE_STR_H
#define SAFE_STR_H
#include <stddef.h>
size_t safe_strlen(const char *str);
int safe_strcpy(char *dest, const char *src, size_t dest_size);
int strcasecmp(const char *s1, const char *s2);
char *strreverse(char *str);
const char *strfind(const char *haystack, const char *needle);
#endif
c复制// safe_str.c
#include "safe_str.h"
#include <ctype.h>
size_t safe_strlen(const char *str) {
if(!str) return 0;
size_t len = 0;
while(*str++) len++;
return len;
}
int safe_strcpy(char *dest, const char *src, size_t dest_size) {
if(!dest || !src || dest_size == 0) return -1;
size_t i;
for(i = 0; i < dest_size-1 && src[i] != '\0'; i++) {
dest[i] = src[i];
}
dest[i] = '\0';
return 0;
}
int strcasecmp(const char *s1, const char *s2) {
while(*s1 && *s2 && tolower(*s1) == tolower(*s2)) {
s1++;
s2++;
}
return tolower(*s1) - tolower(*s2);
}
char *strreverse(char *str) {
if(!str) return NULL;
char *start = str;
char *end = str + safe_strlen(str) - 1;
while(start < end) {
char temp = *start;
*start++ = *end;
*end-- = temp;
}
return str;
}
const char *strfind(const char *haystack, const char *needle) {
if(!haystack || !needle) return NULL;
size_t needle_len = safe_strlen(needle);
if(needle_len == 0) return haystack;
for(; *haystack; haystack++) {
size_t i;
for(i = 0; i < needle_len; i++) {
if(haystack[i] != needle[i]) break;
}
if(i == needle_len) return haystack;
}
return NULL;
}
c复制// test_safe_str.c
#include "safe_str.h"
#include <assert.h>
#include <stdio.h>
void test_safe_strlen() {
assert(safe_strlen("hello") == 5);
assert(safe_strlen("") == 0);
assert(safe_strlen(NULL) == 0);
}
void test_safe_strcpy() {
char buf[10];
assert(safe_strcpy(buf, "hello", 10) == 0);
assert(strcmp(buf, "hello") == 0);
assert(safe_strcpy(buf, "too long string", 10) == 0);
assert(strcmp(buf, "too long ") == 0);
}
int main() {
test_safe_strlen();
test_safe_strcpy();
printf("All tests passed!\n");
return 0;
}
理解字符串在内存中的存储位置很重要:
指针加减运算的实际含义:
c复制char *p = "Hello";
p++; // 移动sizeof(char)字节
对于char指针,加减1移动1字节;对于int指针,加减1移动sizeof(int)字节。
字符串常量有两个重要特性:
例如:
c复制char *p1 = "hello";
char *p2 = "hello";
// p1和p2可能指向同一地址
C11标准引入了更安全的字符串函数,如:
这些函数要求显式指定目标缓冲区大小,并在溢出时调用约束处理函数。
对于只读字符串操作,可以使用结构体实现字符串视图:
c复制typedef struct {
const char *data;
size_t length;
} string_view;
string_view sv_create(const char *str, size_t len) {
return (string_view){str, len};
}
这种方式避免了不必要的拷贝,提高了性能。
对于复杂项目,可以考虑使用第三方字符串库,如:
这些库提供了更丰富的功能和更好的安全性。