11、字符数组与字符串:字符数组与字符串的关系、字符串的初始化、字符串的输入输出、字符串处理函数(strlen/strcpy/strcat/strcmp)

说实话,字符数组和字符串这块,是很多初学者容易搞混的地方。我记得刚学C语言那会儿,总觉得它们就是同一个东西——毕竟打印出来都一样嘛。后来在项目里踩了坑才明白,这俩虽然长得像,但性格完全不同。

今天我就带你彻底理清楚它们的关系,顺便把字符串的初始化、输入输出,还有那几个最常用的处理函数(strlen、strcpy、strcat、strcmp)一次性讲透。

字符数组与字符串:到底有什么区别?

先给个结论:字符串是字符数组的一种特殊形式。说白了,所有字符串都是字符数组,但不是所有字符数组都是字符串。

区别在哪?字符串末尾必须有一个 '\0'(空字符),作为结束标志。而普通字符数组可以没有这个结束符。

核心要点:

  • 字符数组:存放字符的数组,末尾不一定有 '\0'
  • 字符串:字符数组 + 末尾 '\0' 结束符
  • C语言中所有字符串处理函数都依赖 '\0' 来识别字符串的结尾

我在项目中遇到过这样一个bug:定义了一个字符数组用来存数据,结果用 printf("%s") 打印时,后面多了一堆乱码。原因就是数组末尾没有 '\0',printf一直读到内存里遇到第一个0才停。

// 普通字符数组(不是字符串)
char arr1[] = {'H', 'e', 'l', 'l', 'o'};  // 没有'\0'

// 字符串(字符数组 + '\0')
char arr2[] = {'H', 'e', 'l', 'l', 'o', '\0'};  // 有'\0'
char arr3[] = "Hello";  // 编译器自动加'\0'

你想想看,"Hello" 这种双引号括起来的写法,编译器会在末尾偷偷加一个 '\0'。所以 arr3 实际占6个字节,不是5个。

字符串的初始化:几种常见方式

字符串初始化有好几种写法,我习惯根据场景选不同的方式。下面这几种都是对的,但细节上有区别:

初始化方式 示例 说明
字符数组 + 双引号 char s1[] = "hello"; 自动加'\0',大小由编译器计算
指定大小 + 双引号 char s2[10] = "hello"; 剩余位置自动补'\0'
字符逐个赋值 char s3[] = {'h','e','l','l','o','\0'}; 必须手动加'\0'
指针指向字符串常量 char *s4 = "hello"; 字符串常量,不可修改

注意: char *s4 = "hello"; 这种方式定义的字符串是存放在只读数据区的,不能通过 s4[0] = 'H'; 修改。我曾经在项目里这么写过,结果程序直接崩溃——嗯,血的教训。

字符串的输入输出

输入输出这块,常用的就几个函数:printfputsscanfgetsfgets。我一个个说。

输出:printf 和 puts

char str[] = "Hello C";
printf("%s\n", str);   // 输出:Hello C
puts(str);             // 输出:Hello C(自动换行)

putsprintf 简单,自动加换行。但 printf 更灵活,可以格式化输出多个变量。

输入:scanf、gets、fgets

char buf[100];

// scanf:遇到空格就停
scanf("%s", buf);   // 输入 "hello world",只读到 "hello"

// gets:读取整行(危险!不检查缓冲区大小)
gets(buf);          // 如果输入超过99个字符,会溢出

// fgets:安全读取整行
fgets(buf, sizeof(buf), stdin);  // 最多读99个字符,留一个给'\0'

千万不要用 gets! 这个函数已经被C11标准废弃了。它不检查缓冲区大小,输入过长时直接溢出,是安全漏洞的常见来源。我在早期项目里用过一次,后来被安全审查要求全部改成 fgets。

我个人习惯用 fgets,虽然它会保留换行符,但处理起来也就多一行代码的事:

fgets(buf, sizeof(buf), stdin);
// 去掉末尾的换行符
buf[strcspn(buf, "\n")] = '\0';

字符串处理函数:四大金刚

这四个函数是C语言字符串操作的核心,面试也经常考。我一个个讲清楚。

strlen:求字符串长度

char s[] = "hello";
int len = strlen(s);  // len = 5,不包含'\0'

strlen 从字符串开头一直数到 '\0' 为止,不包含 '\0' 本身。如果字符串没有 '\0',它会一直数下去,直到内存里碰到一个0——这就是我之前说的乱码问题的根源。

strcpy:字符串拷贝

char dest[20];
char src[] = "hello";
strcpy(dest, src);  // 把src拷贝到dest

这里要注意:dest 的空间必须足够大,能放下 src 的内容(包括 '\0')。否则就是缓冲区溢出。

安全替代: 很多编译器推荐用 strcpy_sstrncpy,它们可以指定最大拷贝长度,防止溢出。

strcat:字符串拼接

char dest[20] = "hello";
char src[] = " world";
strcat(dest, src);  // dest 变成 "hello world"

strcat 会找到 dest'\0' 位置,然后把 src 的内容从那里开始粘贴上去。同样,dest 的空间要足够大。

strcmp:字符串比较

char s1[] = "hello";
char s2[] = "hello";
char s3[] = "world";

int r1 = strcmp(s1, s2);  // r1 = 0,相等
int r2 = strcmp(s1, s3);  // r2 < 0,s1 < s3(按字典序)

返回值规则:相等返回0,s1小于s2返回负数,s1大于s2返回正数。比较的是字符的ASCII码值,不是长度。

我见过有人直接用 if (s1 == s2) 来比较字符串——这是错的!这样比较的是两个指针的地址,不是字符串内容。一定要用 strcmp

知识体系总览

下面这张图把本章的核心内容串起来了,你可以对照着复习:

字符数组与字符串 · 知识体系 字符串 字符数组 + '\0' = 字符串 初始化方式:数组/指针/逐个赋值 输入:scanf / gets(危险) / fgets(安全) 输出:printf / puts 四大处理函数 strlen(长度) | strcpy(拷贝) | strcat(拼接) | strcmp(比较)

避坑指南

最后总结几个我踩过的坑,你遇到了可以少走弯路:

  • 忘记留'\0'的位置:定义数组时,长度要至少比字符串长度多1。比如存"hello"需要6个字节。
  • 用 == 比较字符串:比较的是地址,不是内容。用 strcmp
  • gets 缓冲区溢出:永远用 fgets 替代。
  • strcpy/strcat 目标空间不足:确保目标数组足够大,或者用 strncpy/strncat
  • 修改字符串常量char *p = "hello"; p[0] = 'H'; 会导致未定义行为。

嗯,字符数组和字符串这块,说白了就是记住一个核心:'\0' 是字符串的灵魂。没有它,所有字符串处理函数都会乱套。你写代码的时候,脑子里时刻想着这个结束符,基本就不会出大问题。


专注资料整理