在C语言中,字符串实际上是使用空字符\0结尾的一维字符数组。因此,\0是用于标记字符串的结束。
空字符(Null character)又称结束符,缩写NULL,是一个数值为0的控制字符,\0是转义字符,意思是告诉编译器,这不是字符0,而是空字符。
下面的声明和初始化创建了一个RUNOOB字符串。由于在数组的末尾存储了空字符\0,所以字符数组的大小比单词 RUNOOB 的字符数多一个。
char site[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'};
依据数组初始化规则,您可以把上面的语句写成以下语句:
char site[] = "RUNOOB";
以下是 C/C++ 中定义的字符串的内存表示:
其实,您不需要把null字符放在字符串常量的末尾。C编译器会在初始化数组时,自动把\0放在字符串的末尾。
C语言中字符串的本质:指针指向头、固定尾部的地址相连的一段内存
C语言中字符串有3个核心要点:第一是用一个指针指向字符串头;第二是固定尾部(字符串总是以'\0'来结尾);第三是组成字符串的各字符彼此地址相连。
'\0'作为一个特殊的数字被字符串定义为(幸运的选为)结尾标志。产生的副作用就是:字符串中无法包含'\0'这个字符。这种思路就叫“魔数”(魔数就是选出来的一个特殊的数字,这个数字表示一个特殊的含义,你的正式内容中不能包含这个魔数作为内容)。
看这道题:
注意,如果想通过字符数组来创建字符串,必须手动在末尾添加'\0',如下所示:
如果没有添加末尾的'\0',可能会出现无法预料的结果:
运行结果:
对于直接字符串形式来说,虽然\0是自动加上的。但是在长度定义时,也要算上它,如果不算上,也可能会出错。
运行结果:
C语言没有原生字符串类型
很多高级语言像java、C#等就有字符串类型,有个String来表示字符串,用法和int这些很像,可以String s1 = "linux";来定义字符串类型的变量。
C语言没有String类型,C语言中的字符串是通过字符指针来间接实现的。C语言使用指针来管理字符串
C语言中定义字符串方法:char *p = "linux";此时p就叫做字符串,但是实际上p只是一个字符指针(本质上就是一个指针变量,只是p指向了一个字符串的起始地址而已)。注意:指向字符串的指针和字符串本身是分开的两个东西
char *p = "linux";在这段代码中,p本质上是一个字符指针,占4字节;"linux"分配在代码段,占6个字节;实际上总共耗费了10个字节,这10个字节中:4字节的指针p叫做字符串指针(用来指向字符串的,理解为字符串的引子,但是它本身不是字符串),5字节的用来存linux这5个字符的内存才是真正的字符串,最后一个用来存'\0'的内存是字符串结尾标志(本质上也不属于字符串)。
我们有多个连续字符(比如"linux")需要存储,实际上有两种方式:第一种就是字符串;第二种是字符数组。
字符数组a和字符串p,char a[];int *p;——a和p都是指向字符串的首元素。
这两种方式有何区别?
sizeof(数组名)得到的永远是数组的大小,和数组中有无初始化,初始化多、少等是没有关系的。
char *p = "linux"; sizeof(p)得到的永远是4,因为这时候sizeof测的是字符指针p本身的长度,和字符串的长度是无关的。字符数组与字符串的本质差异(内存分配角度)
字符数组char a[] = "linux";来说,定义了一个数组a,数组a占6字节,右值"linux"本身只存在于编译器中,编译器将它用来初始化字符数组a后丢弃掉(也就是说内存中是没有"linux"这个字符串的,它只有一个个字符);这句就相当于是:char a[] = {'l', 'i', 'n', 'u', 'x', '\0'};字符串char *p = "linux";定义了一个字符指针p,p占4字节,分配在栈上;同时还定义了一个字符串"linux",分配在代码段;然后把代码段中的字符串(一共占6字节)的首地址(也就是'l'的地址)赋值给p。
总结对比:字符数组和字符串有本质差别。字符数组本身是数组,数组自身自带内存空间,可以用来存东西(所以数组类似于容器);而字符串本身是指针,本身永远只占4字节,而且这4个字节还不能用来存有效数据,所以只能把有效数据存到别的地方,然后把地址存在p中。
也就是说字符数组自己存那些字符;字符串一定需要额外的内存来存那些字符,字符串本身只存真正的那些字符所在的内存空间的首地址。注意:
数组字符串的变量和字符串(字符形式)都在栈上,是可以修改的;
指针字符串的指针变量在栈上,但是字符串是以常量的形式存在代码段,是不能修改的。
验证如下:
字符数组形式:
#include
int main() { int i = 0; char a[] = "Hello World!"; a[5] = '-'; printf("Result is:"); for(i;i < 11;i++) { printf("%c",a[i]); } return 0; } //Result is:Hello-World 指针形式:
#include
int main() { char *a = "Hello World!"; *(a + 5) = '-'; printf("Result is:", *(a + 5)); return 0; } //3 Segmentation fault (core dumped) ./a.out 由上述对比可知,如果修改指针形式的字符串,则会触发段错误。
问题:如果定义了多个指针变量,都是同一个字符串,那这些指针都会指向同一个地址吗?也就是说,相同的字符串只有一份。
#include
int main() { char *a = "Hello World!"; char *b = "Hello World!"; char c[] = "Hello World!"; char d[] = "Hello World!"; char *e = "Hello World!"; printf("Result is: %x\n", a); printf("Result is: %x\n", b); printf("Result is: %x\n", c); printf("Result is: %x\n", d); printf("Result is: %x\n", e); return 0; } /* Result is: 402004 Result is: 402004 Result is: aadaa80b Result is: aadaa7fe Result is: 402004 */ 由上述结果可知,指针形式生成的字符串,在内存中只有一份。都是指向同一个地址。
字符数组形式的字符串,会分配不同的地址,一般是连续的。比如上面的两个字符数组字符串之间就是连续的。
另外,如果想要char a[] = "happy";形式中,元素不能被修改,则可以增加const进行修改,此时,和char *a = "happy";等效。
示例如下:
#include
int main() { const char a[] = "happy"; a[0] = 'H'; /* 我的第一个 C 程序 */ printf("Hello, World! %c\n", a[0]); return 0; } 运行结果:
我们知道,可以这样定义字符串,char *p = "stm32";
要注意,不是说这里的p表示的是个字符串指针,它仍然是一个字符指针,这个指针所指向的是字符串“stm32”的首字母的地址。如果直接解引用,*p是s
#include
int main() { char *p = "stm32"; /* 我的第一个 C 程序 */ printf("Hello, World! %c\n", *p); //Hello, World! s return 0; } 如果要访问字符串的各个字母,就要*(p + 1)/*(p + 2)/*(p + 3)……以此类推。
反过来,如果将某个字符串强制转换成char *,那么得到的也是这个字符串的首元素的地址,示例:
#include
int main() { char *p = (char *)"stm32"; /* 我的第一个 C 程序 */ printf("Hello, World! %c\n", *p); //Hello, World! s return 0; } 所以说,根本就没有真正的字符串类型,都是形式上的“障眼法”。
同理,把某个数强转成结构体指针,那么,所赋予的就是结构体首元素的首地址。
比如:
字符 '0' 和 '\0' 及整数 0 的区别
字符 '0':char c = '0'; 它的 ASCII 码实际上是 48,内存中存放表示:00110000。
字符 '\0': ASCII 码为 0,表示一个字符串结束的标志。这是转义字符(整体视为一个字符)。由于内存中存储字符,依然是存储的是对应字符集的字符编码,所以内存中的表现形式为 00000000。ASCLL值0表示空字符,空字符就是平时所说的 ‘\0’。
整数 0 : 内存中表示为 00000000 00000000 00000000 00000000,虽然都是 0,但是跟上面字符 '\0' 存储占用长度是不一样的。
补充:再次强调下数组形式字符串和指针形式字符串的区别
记录本小节内容的背景是这样的:
我有个结构体,里面有些参数是字符串,按照我一开始的想法,直接就是定义了char *str;
后来要给结构体重新赋值,可是出现了问题,定位了好久也没什么进展。
后来突然想起来,char *str定义的字符串放在代码区,是不能被改变的,所以,如果结构体里的字符串是希望被改变的,就不能定义成char *str的形式。
如果不能被定义成char *str的形式,那就肯定要定义成字符数组的形式。
于是我在结构体里定义了一个char str[],结果定义的地方报错,我试着调整下,加了个长度,变成了char str[3],恢复正常。可见,结构体里的每个变量,都要有固定的长度,要不然编译器就不知道到底要为它分配多少内存。char *s定义了一个char型的指针,它只知道所指向的内存单元,并不知道这个内存单元有多大。
注意,char *s类型初始化时是可以赋值的,但是之后就不能再被改变了。
既然解决了类型定义的问题,那么怎么来使用这个变量的。
既然是字符数组,那么赋值就需要注意。
数组如果一开始不初始化,那么之后就不能直接初始化,需要一个一个地赋值。
就算一开始直接初始化,后面要改变值的话,也要一个一个地赋值。
str[0] =
str[1] =
str[2] =
而且,这样的话,也不会自动在末尾添加'\0',也就是说,不会被当做字符串来看待,而是当做一个一个的字符来看待。
关于这一点,结构体也一样,如果一开始不直接初始化,那么后面就要一个一个地初始化。
如果是这样的话,那就比较麻烦了。
有一点需要注意,(结构体变量.arr)得到的就是常规arr效果的指针。此时,直接操作的就是一个指针。
有什么办法能够直接赋字符串呢?
可以使用memcpy或者strcpy函数来赋值。
字符串的话,推荐使用strcpy,因为str开头的函数会处理'\0',如果使用mem系列的话,就需要指定一个长度,而有时候长度是不固定的,就可能会因此多出一些空白符号。
比如:
strcpy(structVar.str,"100"),这个函数会将"100"连带末尾的结束符都赋值到目标指针处。
把字符串strcpy到这个指针开头的存储空间,并且有'\0'结尾。
注意:如果就是定义了字符类型的指针呢?char *p; *p = 'a',这样是可以的,因为此时它就是一个普通的字符指针,而不是一个字符串,编译器不会当做字符串来处理。
其实仔细看看字符串函数里的例程,就会发现,基本上都是定义成字符数组。
只有在确定不会改变时,才会使用char *来定义字符串,而且,这种也可以通过const修饰字符数组来实现。或者,在函数参数里,通常都是以char *的形式来传递,但是,其通常所需要的也就是一个数组的名称,也就是数组首元素的字符指针,这才是char *出现得比较多的地方,但本质上,传递的还是数组的起始点。有时,在接受一个字符指针时,也会自己定义一个char *,这和参数里的用法是一样的。
有个问题,直接写的字符串是哪种类型的?
就是直接在程序里写一个"string"
好像没有直接写的,都是赋值的形式,只不过传给函数时,看起来好像没有类型,其实在函数形参中就有。直接传个字符串过去,底层应该是char *形式的,不可更改的。
今天在传值时,我在函数里面直接将数组名传给了另外一个数组名,以为这样就能将数组里的内容传递到另一个数组了,其实,这传的就是一个指针,而且,改变了另一个数组所指向的位置,当函数退出时,变量出栈,其所指向的就是一个不定值的区域。
我所希望的复制操作,应该通过strcpy或者memcpy来实现,切记。
注意:结构体里的字符串一定要设置长度尽可能大,最低要求是刚好够用,要不然当字符串多的时候,就会造成元素之间的存储空间相互覆盖的问题。