C语言字符串详解

字符串简介

在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是自动加上的。但是在长度定义时,也要算上它,如果不算上,也可能会出错。

运行结果:

字符串形式的字符串,比如“helloworld”,末尾本身就带了个’\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 <stdio.h>

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 <stdio.h>

int main()
{
	char *a = "Hello World!";
	
	*(a + 5) = '-';
	printf("Result is:", *(a + 5));
	
    return 0;
}

//3 Segmentation fault      (core dumped) ./a.out

由上述对比可知,如果修改指针形式的字符串,则会触发段错误。

问题:如果定义了多个指针变量,都是同一个字符串,那这些指针都会指向同一个地址吗?也就是说,相同的字符串只有一份。

#include <stdio.h>

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 <stdio.h>

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 <stdio.h>

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 <stdio.h>

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来实现,切记。

注意:结构体里的字符串一定要设置长度尽可能大,最低要求是刚好够用,要不然当字符串多的时候,就会造成元素之间的存储空间相互覆盖的问题。

  • 18
    点赞
  • 75
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值