字符串的两种表现形式
字符数组:小心缺少的结束符
字符串有两种形式,第一种形式是字符数组:
c
#include <stdio.h>
int main() {
// 字符数组
char arr[] = { 'H','e','l','l','o','!' };
printf("%s", arr);
return 0;
}
运行后,你会发现打印结果是:Hello!烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫。
不要着急,这是因为字符串遇到 \0 空字符才会停止,而我们定义的字符数组没有这个结束符,程序自然会越界读取栈区未初始化的内存(0xCCCC),从而以字符串形式打印时,会显示为 "烫",当遇到 \0 才会停止。
我们加上 \0 即可,或是输出字符串长度的字符:
c
#include <stdio.h>
int main() {
// 字符数组
char arr[] = { 'H','e','l','l','o','!','\0' }; // 添加 \0 空字符
printf("%s\n", arr);
// 按需打印
char arr2[] = { 'H','e','l','l','o','!' };
int len = sizeof(arr2) / sizeof(arr2[0]);
for (int i = 0; i < len; i++)
{
printf("%c", arr2[i]);
}
printf("\n");
return 0;
}
字符数组还有另一种定义的方式,这种方式会自动在末尾添加 \0:
c
#include <stdio.h>
int main() {
char arr[] = "Hello!"; // 自动加上 \0 空字符
printf("%s\n", arr);
return 0;
}
字符指针:指向常量的字符串
第二种形式是字符指针,指向字符串常量的首地址。
c
#include <stdio.h>
int main() {
const char* str = "hello";
return 0;
}
核心差异剖析:读写权限与内存分配的区别
这两种形式有什么区别呢?
首先第一种形式,数据存在于 栈区,我们可以修改其内容。
即使用字符串常量去初始化数组(如
char arr[] = "Hello!";),只要它在函数内部,也是在栈区。因为程序运行时,会把常量区的"Hello!"拷贝一份到栈区的数组中。
c
#include <stdio.h>
int main() {
char arr[] = { 'H','e','l','l','o','!','\0' }; // 栈区
char arr2[] = "Hello!"; // 还是栈区
// 修改字符
arr[1] = 'a';
arr2[0] = 'h';
printf("arr: %s\n", arr); // arr: Hallo!
printf("arr2: %s\n", arr2); // arr2: hello!
return 0;
}
第二种形式,指针所指向的内容存在常量区 ,只可以读不能改。我们从指针的类型(const char*)也能看出,它是指向常量的字符指针。当然,指针自身的指向可以改变,因为它不是常指针(char* const)。
c
#include <stdio.h>
int main() {
const char* str = "hello world!";
str[5] = '-'; // 报错:表达式必须是可修改的左值
str = "maybe"; // 正常运行
return 0;
}
字符串基本操作实战
下面,我们来练习一下字符串的基本操作。
获取长度
先从获取字符串长度开始。
c
#include <stdio.h>
#include <string.h>
// 获取字符串长度,返回无符号整数
// 关键点:不断读取字符,遇到 \0 就停止
size_t my_strlen(const char* str) {
size_t length = 0;
const char* cur = str;
while (*cur != '\0') {
length++, cur++;
}
return length;
}
int main() {
const char* const str = "hello world!";
printf("str's length is %zu", my_strlen(str)); // 12
return 0;
}
在
<string.h>头文件中已经存在strlen,用于获取字符串的长度。
字符串比较
然后是字符串比较,我们可以使用 <string.h> 中的 strcmp。
str1等于str2,函数会返回 0。str1小于str2,函数会返回负数。str1大于str2,函数会返回正数。
c
#include <stdio.h>
#include <string.h>
int main() {
const char* str1 = "World";
const char* str2 = "Word";
int res = strcmp(str1, str2);
if (res == 0)
{
printf("equal");
}
else if (res < 0) {
printf("less than");
}
else {
printf("greater than");
}
return 0;
}
这个函数是区分大小写的。如果要忽略大小写,在 Windows 环境下可以使用 strcmpi 或 _stricmp;在 Linux 环境下,你需要使用 strcasecmp,它定义在 <strings.h> 头文件中。
子串查找
字符串查找使用的是 strstr:
c
#include <stdio.h>
#include <string.h>
int main() {
const char* str = "my name is leon,I'm happy!";
const char* sub_str = "leon";
// 如果找得到,返回的是子串在原串中第一次出现的位置
// 找不到,返回的是 NULL
const char* pos = strstr(str, sub_str);
if (pos != NULL)
{
printf("exist\n");
printf("%s\n", pos); // leon,I'm happy!
}
else {
printf("not exist\n");
}
// 转换为索引:直接相减即可,指针相减的结果就是相隔的元素个数
int index = pos - str;
printf("index is %d\n", index); // 11
return 0;
}
字符串拷贝
字符串拷贝使用的是 strcpy,它会将原字符串(第二个参数)拷贝到目标字符数组(第一个参数)中。
拷贝过程中遇到 \0 将会停止,并且拷贝结束后会自动添加上结束符 \0。
注:这里使用的
strcpy_s是 Windows 特有的安全函数版本,如果在 Linux 等其他环境下,请直接使用strcpy。
C
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "Jack";
char dest[5];
strcpy_s(dest, src);
printf("After strcpy_s: %s\n", dest);
return 0;
}
字符串拼接
字符串拼接使用的是 strcat。
c
#include <stdio.h>
#include <string.h>
int main() {
char str[100] = "My dream ";
char str2[50] = "will not end. ";
strcat_s(str, str2);
printf("%s\n", str); // My dream will not end.
strcat_s(str, "even if ");
strcat_s(str, "it is very small.");
printf("%s\n", str); // My dream will not end. even if it is very small.
return 0;
}
手写字符串替换函数
最后是字符串替换,这个稍微复杂点,特别是查找、替换的过程,需要处理好边界。
c
#include <stdio.h>
#include <stdlib.h> // 需要用到 calloc 和 free
#include <string.h> // 需要用到 strlen, strstr, memcpy
// 字符串替换
char* replace(const char* src, const char* old_sub, const char* new_sub) {
// 边界校验
if (!src || !old_sub || !new_sub) return NULL;
size_t old_sub_len = strlen(old_sub);
if (old_sub_len == 0) return NULL; // 禁止空匹配串
size_t src_len = strlen(src);
size_t new_sub_len = strlen(new_sub);
// 查找 old_sub 出现的次数,方便进行多次替换
size_t count = 0;
const char* p = src;
while ((p = strstr(p, old_sub)) != NULL) {
count++;
p += old_sub_len; // 跳过已匹配的部分
}
// 计算替换后的新字符串长度,开辟空间 (包含末尾的 \0)
size_t new_len = src_len + count * (new_sub_len - old_sub_len);
char* new_str = (char*)calloc(new_len + 1, sizeof(char));
if (new_str == NULL) {
// 内存分配失败
return NULL;
}
// 进行替换
char* dest = new_str;
const char* start = src; // 每次匹配时的起点
const char* cur_pos = start;
while ((cur_pos = strstr(cur_pos, old_sub)) != NULL) {
// 拷贝匹配点之前的字符
size_t pre_len = (size_t)(cur_pos - start);
memcpy(dest, start, pre_len); // 将 src 中长度为 pre_len 的字符复制到新字符串 dest 上
dest += pre_len;
// 拷贝替换的字符串
memcpy(dest, new_sub, new_sub_len);
dest += new_sub_len;
// 移动原字符串中的位置指针,跳过已匹配项
cur_pos += old_sub_len;
start = cur_pos;
}
// 拷贝剩余的字符串
size_t tail_len = strlen(start);
memcpy(dest, start, tail_len);
dest[tail_len] = '\0';
return new_str;
}
int main() {
char origin[] = "Hello World! World is big, World is beautiful";
char* res = replace(origin, "World", "C Language");
if (res != NULL)
{
printf("After replace: %s\n", res);
free(res);
res = NULL;
}
return 0;
}