目录
1.前言
本篇原文为:C语言字符串操作详解:声明、复制、比较与函数使用。
更多C++进阶、rust、python、逆向等等教程,可点击此链接查看:酷程网
C 语言没有单独的字符串类型,字符串被当作字符数组,即char
类型的数组。比如,字符串“Hello”是当作数组{'H', 'e', 'l', 'l', 'o'}
处理的。
编译器会给数组分配一段连续内存,所有字符储存在相邻的内存单元之中。
在字符串结尾,C 语言会自动添加一个全是二进制0
的字节,写作\0
字符,表示字符串结束。
字符\0
不同于字符0
,前者的 ASCII 码是0(二进制形式00000000
),后者的 ASCII 码是48(二进制形式00110000
)。
所以,字符串“Hello”实际储存的数组是{'H', 'e', 'l', 'l', 'o', '\0'}
。
所有字符串的最后一个字符,都是\0
,这样做的好处是,C 语言不需要知道字符串的长度,就可以读取内存里面的字符串,只要发现有一个字符是\0
,那么就知道字符串结束了。
char localString[10];
上面示例声明了一个10个成员的字符数组,可以当作字符串。由于必须留一个位置给\0
,所以最多只能容纳9个字符的字符串。
字符串写成数组的形式,是非常麻烦的,C 语言提供了一种简写法,双引号之中的字符,会被自动视为字符数组。
{
'H', 'e', 'l', 'l', 'o', '\0'}
// 等价于
"Hello"
上面两种字符串的写法是等价的,内部存储方式都是一样的,双引号里面的字符串,不用自己添加结尾字符\0
,C 语言会自动添加。
注意,双引号里面是字符串,单引号里面是字符,两者不能互换。如果把Hello
放在单引号里面,编译器会报错:
// 报错
'Hello'
另一方面,即使双引号里面只有一个字符(比如"a"
),也依然被处理成字符串(存储为2个字节),而不是字符'a'
(存储为1个字节)。
如果字符串内部包含双引号,则该双引号需要使用反斜杠转义:
"She replied, \"It does.\""
反斜杠还可以表示其他特殊字符,比如换行符(\n
)、制表符(\t
)等:
"Hello, world!\n"
如果字符串过长,可以在需要折行的地方,使用反斜杠(\
)结尾,将一行拆成多行:
"hello \
world"
上面示例中,第一行尾部的反斜杠,将字符串拆成两行。
上面这种写法有一个缺点,就是第二行必须顶格书写,如果想包含缩进,那么缩进也会被计入字符串。
为了解决这个问题,C 语言允许合并多个字符串字面量,只要这些字符串之间没有间隔,或者只有空格,C 语言会将它们自动合并:
char greeting[50] = "Hello, ""how are you ""today!";
// 等同于
char greeting[50] = "Hello, how are you today!";
这种新写法支持多行字符串的合并:
char greeting[50] = "Hello, "
"how are you "
"today!";
printf()
使用占位符%s
输出字符串:
printf("%s\n", "hello world")
2.字符串声明
字符串变量可以声明成一个字符数组,也可以声明成一个指针,指向字符数组:
// 写法一
char s[14] = "Hello, world!";
// 写法二
char* s = "Hello, world!";
上面两种写法都声明了一个字符串变量s
。如果采用第一种写法,由于字符数组的长度可以让编译器自动计算,所以声明时可以省略字符数组的长度:
char s[] = "Hello, world!";
上面示例中,编译器会将数组s
的长度指定为14,正好容纳后面的字符串。
字符数组的长度,可以大于字符串的实际长度:
char s[50] = "hello";
上面示例中,字符数组s
的长度是50
,但是字符串“hello”的实际长度只有6(包含结尾符号\0
),所以后面空出来的44个位置,都会被初始化为\0
。
字符数组的长度,不能小于字符串的实际长度:
char s[5] = "hello";
上面示例中,字符串数组s
的长度是5
,小于字符串“hello”的实际长度6,这时编译器会报错。
因为如果只将前5个字符写入,而省略最后的结尾符号\0
,这很可能导致后面的字符串相关代码出错。
字符指针和字符数组,这两种声明字符串变量的写法基本是等价的,但是有两个差异。
第一个差异是,指针指向的字符串,在 C 语言内部被当作常量,不能修改字符串本身:
char* s = "Hello, world!";
s[0] = 'z'; // 错误
上面代码使用指针,声明了一个字符串变量,然后修改了字符串的第一个字符。这种写法是错的,会导致难以预测的后果,执行时很可能会报错。
如果使用数组声明字符串变量,就没有这个问题,可以修改数组的任意成员:
char s[] = "Hello, world!";
s[0] = 'z';
为什么字符串声明为指针时不能修改,声明为数组时就可以修改?
原因是系统会将字符串的字面量保存在内存的常量区,这个区是不允许用户修改的。
声明为指针时,指针变量存储的值是一个指向常量区的内存地址,因此用户不能通过这个地址去修改常量区。
但是,声明为数组时,编译器会给数组单独分配一段内存,字符串字面量会被编译器解释成字符数组,逐个字符写入这段新分配的内存之中,而这段新内存是允许修改的。
为了提醒用户,字符串声明为指针后不得修改,可以在声明时使用const
说明符,保证该字符串是只读的:
const char* s = "Hello, world!";
上面字符串声明为指针时,使用了const
说明符,就保证了该字符串无法修改。一旦修改,编译器肯定会报错。
第二个差异是,指针变量可以指向其它字符串:
char* s = "hello";
s = "world";
上面示例中,字符指针可以指向另一个字符串。
但是,字符数组变量不能指向另一个字符串:
char s[] = "hello";
s = "world"; // 报错
上面示例中,字符数组的数组名,总是指向初始化时的字符串地址,不能修改。
同样的原因,声明字符数组后,不能直接用字符串赋值:
char s[10];
s = "abc"