失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > c 字符串数组_C语言探索之旅 | 第二部分第四课:字符串

c 字符串数组_C语言探索之旅 | 第二部分第四课:字符串

时间:2022-06-14 11:07:04

相关推荐

c 字符串数组_C语言探索之旅 | 第二部分第四课:字符串

内容简介

前言

字符类型

显示字符

字符串其实就是字符的数组

字符串的创建和初始化

从 scanf 函数取得一个字符串

操纵字符串的一些常用函数

总结

第二部分第五课预告

1. 前言

上一课 C语言探索之旅 | 第二部分第三课:数组 ,我们结束了关于数组的旅程。

好了,这课我不说“废话”,直接进入主题(但又好像不是我的风格…)。这一课我们还是会涉及一些指针和数组的知识。

字符串,这是一个编程的术语,用来描述“一段文字”。

一个字符串,就是我们可以在内存中以变量的形式储存的“一段文字”。

比如,用户名是一个字符串,“程序员联盟”是一个字符串。

但是我们之前的课说过,呆萌的电脑兄只认得数字,“众里寻他千百度,电脑却只认得数。”

所以实际上,电脑是不认得字母的,但是“古灵精怪”的计算机先驱们是如何使电脑可以“识别”字母呢?

接下来我们会看到,他们还是很聪明的。

2. 字符类型

在这个小部分,我们把注意力先集中在字符类型上。

如果你还记得,之前的课程中我们说过:char(有符号字符类型)是用来储存范围从 -128 到 127 的数的;unsigned char(无符号字符类型)用来储存范围从 0 到 255 的数。

注意:虽然 char 类型可以用来储存数值,但是在 C语言中却鲜少用 char 来储存一个数。

通常,即使我们要表示的数比较小,我们也会用 int 类型来储存。

当然了,用 int 来储存比用 char 来储存在内存上更占空间。但是今天的电脑基本上是不缺那点内存的,“有内存任性嘛”。

char 类型一般用来储存一个字符,注意,是一个字符。

前面的课程也提到了,因为电脑只认得数字,所以计算机先驱们建立了一个表格(比较常见的有 ASCII 表, 更完整一些的有 Unicode 表),用来约定字符和数字之间的转换关系,例如大写字母 A 对应的数字是 65。

C语言可以很容易地转换字符和其对应的数值。为了获取到某个字符对应的数值(电脑底层其实都是数值),只需要把该字符用单引号括起来,像这样:

'A'

在编译的时候,'A' 会被替换成实际的数值 65。

我们来测试一下:

#include

intmain(intargc,char*argv[]){

charletter='A';

printf("%d\n",letter);

return0;

}

程序输出:

65

所以,我们可以确信大写字母 A 的对应数值是 65。类似地,大写字母 B 对应 66, C 对应 67, 以此类推。

如果我们测试小写字母,那你会看到 a 和 A 的数值是不一样的,小写字母 a 的数值是 97。

实际上,在大写字母和小写字母之间有一个很简单的转换公式,就是

小写字母的数值 = 大写字母的数值 + 32

所以电脑是区分大小写的,看似呆萌的电脑兄还是可以的么。

大部分所谓“基础”的字符都被编码成 0 到 127 之间的数值了。在 ASCII 表(发音 [aski])的官网 上,我们可以看到大部分常用的字符的对应数值。

当然这个表我们也可以在其他网站上找到,比如维基百科,百度百科,等等。

3. 显示字符

要显示一个字符,最常用的还是 printf 函数啦。这个函数真的很强大,我们会经常用到。

上面的例子中,我们用 %d 格式,所以显示的是字符对应的数值(%d 是整型)。如果要显示字符实际的样子,需要用到 %c 格式(c 是英语 character 的首字母,表示“字符”):

intmain(intargc,char*argv[]){

charletter='A';

printf("%c\n",letter);

return0;

}

程序输出:

A

当然我们也可以用常见的 scanf 函数来请求用户输入一个字符,而后用 printf 函数打印:

intmain(intargc,char*argv[]){

charletter=0;

scanf("%c",&letter);

printf("%c\n",letter);

return0;

}

如果我输入 C,那我将看到:

C

C

第一个字母 C 是我输入给 scanf 函数的,第二个 C 是 printf 函数打印的。

以上就是对于字符类型 char 我们大致需要知道的,请牢记以下几点:

signed char(有符号字符类型)用来储存范围从 -128 到 127 的数。

unsigned char(无符号字符类型)用来储存范围从 0 到 255 的数。

C语言中,如果你没写 signed 或 unsigned 关键字,默认情况下是 signed(有符号)。

计算机先驱们给电脑规定了一个表,电脑可以遵照里面的转换原则来转换字符和数值,一般这个表是 ASCII 表。

char 类型只能储存一个字符。

'A' 在编译时会被替换成实际的数值:65 。因此,我们使用单引号来获得一个字符的值。

4. 字符串其实就是字符的数组

这一部分的内容,就如这个小标题所言。

事实上:一个字符串就是一个“字符的数组”,仅此而已。

到这里,你是否对字符串有了更直观的理解呢?

如果我们创建一个字符数组:

charstring[5];

然后我们在数组的第一个成员上储存 'H',就是 string[0] = 'H',第二个成员上储存 'E'(string[1] = 'H'),第三个成员上储存 'L'(string[2] = 'L'),第四个成员储存 'L'(string[3] = 'L'),第五个成员储存 'O'(string[4] = 'O'),那么我们就构造了一个字符串。

下图对于字符串在内存中是怎么存储的,可以给出一个比较直观的印象(注意:实际的情况比这个图演示的要略微复杂一些,待会儿会解释):

上图中,我们可以看到一个数组,拥有 5 个成员,在内存上连续存放,构成一个字符串 "HELLO"(表示“喂,你好”)。

对于每一个储存在内存地址上的字符,我们用了单引号把它括起来,是为了突出实际上储存的是数值,而不是字符。在内存上,储存的就是此字符对应的数值。

实际上,一个字符串可不是就这样结束了,上面的图示其实不完整。

一个字符串必须在最后包含一个特殊的字符,称为“字符串结束符”,它是 '\0',对应的数值是 0。

“为什么要在字符串结尾加这么一个多余的字符呢?”

问得好!

那是为了让电脑知道一个字符串到哪里结束。

'\0' 用于告诉电脑:“停止,字符串到此结束了,不要再读取了,先退下吧”。

因此,为了在内存中存储字符串 "HELLO"(5 个字符),用 5 个成员的字符数组是不够的,需要 6 个!

因此每次创建字符串时,需要记得在字符数组的结尾留一个字符给 '\0'。

忘记字符串结束符是 C语言中一个常见的错误。

因此,下面才是正确展示我们的字符串 "HELLO" 在内存中实际存放情况的示意图:

如上图所见,这个字符串包含 6 个字符,而不是 5 个。

也多亏了这个字符串结束符 '\0',我们就无需记得字符串的长度了,因为它会告诉电脑字符串在哪里结束。

因此,我们就可以将我们的字符数组作为参数传递给函数,而不需要传递字符数组的大小了。

这个好处只针对字符数组,你可以在传递给函数时将其写为 char * 或者 char[] 类型。

对于其他类型的数组,我们总是要在某处记录下它的长度。

5. 字符串的创建和初始化

如果我们想要用 "Hello" 来初始化字符数组 string,我们可以用以下的方式来实现。当然,有点没效率:

charstring[6];//六个 char 构成的数组,为了储存:H-e-l-l-o +\0

string[0]='H';

string[1]='e';

string[2]='l';

string[3]='l';

string[4]='o';

string[5]='\0';

虽然是笨办法,但至少行得通。

我们用 printf 函数来测试一下。

要使 printf 函数能显示字符串,我们需要用到 %s 这个符号(s 就是英语 string 的首字母,表示“字符串”):

#include

intmain(intargc,char*argv[]){

charstring[6];//六个 char 构成的数组,为了储存:H-e-l-l-o +\0

string[0]='H';

string[1]='e';

string[2]='l';

string[3]='l';

string[4]='o';

string[5]='\0';

//显示字符串内容

printf("%s\n",string);

return0;

}

程序输出:

Hello

如果我们的字符串内容多起来,上面的方法就更显拙劣了。其实啊,初始化字符串还有更简单的一种方式(读者:“你好‘奸诈’,不早讲,害我写代码这么辛苦…”):

intmain(intargc,char*argv[]){

charstring[]="Hello";//字符数组的长度会被自动计算

printf("%s\n",string);

return0;

}

以上程序的第一行,我们写了一个char [] 类型的变量,其实也可以写成 char * ,同样是可以运行的:

char*string="Hello";

这种方法就比之前一个字符一个字符初始化的方法高大上多了,因为只需要在双引号里输入你想要创建的字符串,C语言的编译器就很智能地为你计算好字符串的大小。

编译器计算你输入的字符的数目,然后再加上一个 '\0' 的长度(是 1),就把你的字符串里的字符一个接一个写到内存某个地方,在最后加上 '\0' 这个字符串结束符,就像我们刚才用第一种方式自己一步步做的。

但是简便也有缺陷。我们会发现,对于字符数组来说,这种方法只能用于初始化,你在之后的程序中就不能再用这种方式来给整个数组赋值了,比如你不能这样:

charstring[]="Hello";

string="nihao";//-->出错!

只能一个字符一个字符地改,例如:

string[0]='j';//-->可以!

但是问题又来了,对于用 char * 来声明的字符串,我们可以在之后整个重新赋值,但是不可以单独修改某个字符:

char*string="Hello";

string="nihao";//-->可以!

这样是可以的。但是如果修改其中的一个字符,就不可以:

string[1]='a';//-->出错!

很有意思吧。大家可以亲自动手试试。所以这里就引出了一个话题:

指针和数组根本就是两码事!

为什么会出现上述的情况呢?(请注意:下面的这块内容比较难,如果看不懂,也可以暂时跳过。不过建议测试一下给出的代码)。

那是因为:

1.

charstringArray[]="Hello";

这样声明的是一个字符数组,里面的字符串是储存在内存的变量区,是在栈上,所以可以修改每个字符的内容,但是不可以通过数组名整体修改:

stringArray="nihao";//-->出错!

只能一个个单独改:

stringArray[0]='a';//-->可以!

因为之前的课程里说过,stringArray 这个数组的名字表示的是数组首元素的首地址。

2.

char*stringPointer="Hello";

这样声明的是一个指针,stringPointer 是指针的名字。指针变量在 32 位系统下,永远占 4 个 byte(字节);在 64 位系统下,永远占 8 个 byte(字节)。其值为某一个内存的地址。

所以 stringPointer 里面只是存放了一个地址,这个地址上存放的字符串是常量字符串。这个常量字符串存放在内存的静态区,不可以更改。

和上面的字符数组情况不一样,上面的字符数组是本身存放了那一整个字符串。

stringPointer[0]='a';//-->出错!

但是可以改变 stringPointer 指针的指向:

stringPointer="nihao";//-->可以!(因为可以修改指针指向哪里)

大家可以自己测试一下:

char*n1="it";

char*n2="it";

printf("%p\n%p\n",n1,n2);//用%p查看地址

会发现二者的结果是一样的,指向同一个地址!

再进一步测试(生命在于折腾…):

char*n1="it";

char*n2="it";

printf("%p\n%p\n",n1,n2);

n1="haha";

printf("%p\n%p\n",n1,n2);

你会发现以上程序,指针 n2 所指向的地址一直没变,而 n1 在经过

n1="haha";

之后,它所指向的地址就改变了。

经过上面地分析,可能很多朋友还是有点晕,特别是可能不太清楚内存各个区域的区别。

如果有兴趣深入探究,既可以自己去看相关的 C语言书籍。也可以参考下表和一些解释,如果暂时不想把自己搞得更晕,可以跳过,以后讲到相关内容时自然更好理解。

一般情况下,一个可执行二进制程序(更确切的说,在 Linux 操作系统下为一个进程单元)在存储(没有调入到内存运行)时拥有 3 个部分,分别是代码段、数据段和 BSS 段。

这 3 个部分一起组成了该可执行程序的文件。

(1) 代码段(code segment / text segment):存放 CPU 执行的机器指令。通常代码段是可共享的,这使得需要频繁被执行的程序只需要在内存中拥有一份拷贝即可。代码段也通常是只读的,这样可以防止其他程序意外地修改其指令。另外,代码段还规划了局部数据所申请的内存空间信息。

代码段通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

(2) 数据段(data segment):或称全局初始化数据段/静态数据段(initialized data segment / data segment)。该段包含了在程序中明确被初始化的全局变量、静态变量(包括全局静态变量和局部静态变量)和常量数据。

(3) 未初始化数据段:也称 BSS(Block Started by Symbol)。该段存入的是全局未初始化变量、静态未初始化变量。

而当程序被加载到内存单元时,则需要另外两个域:栈和堆。

(4) 栈(stack):存放函数的参数值、局部变量的值,以及在进行任务切换时存放当前任务的上下文内容。

(5) 堆(heap):用于动态内存分配(之后的课程马上会讲到),就是使用 malloc / free 系列函数来管理的内存空间。

在将应用程序加载到内存空间执行时,操作系统负责代码段、数据段和 BSS 段的加载,并将在内存中为这些段分配空间。

栈也由操作系统分配和管理,而不需要程序员显式地管理;堆由程序员自己管理,即显式地申请和释放空间。

很多 C语言的初学者搞不懂指针和数组到底有什么样的关系。

现在就告诉大家:指针和数组之间没有任何关系!它们是“清白”的…

指针就是指针:指针变量在 32 位系统下,永远占 4 个 byte(字节);在 64 位系统下,永远占 8 个 byte(字节)。其值为某一个内存的地址。指针可以指向任何地方,但不是任何地方你都能通过这个指针变量访问到。

数组就是数组:其大小与元素的类型和个数有关。定义数组时必须指定其元素的类型和个数。数组可以存任何类型的数据,但不能存函数。

推荐大家去看《C语言深度解剖》(/s/1nvO0wNB)这本只有 100 多页的 PDF,是国人写的,里面对于指针和数组分析得很全面。

不禁感叹,C语言果然是博(xiang)大(dang)精(ke)深(pa)。

6. 从 scanf 函数取得一个字符串

我们可以用 scanf 函数获取用户输入的一个字符串,也要用到 %s 符号。

但是有一个问题:就是你不能知道用户究竟会输入多少字符。

假如我们的程序是问用户他的名字是什么。那么他可能回答 Tom,只有三个字符,或者 Bruce LI,就有 8 个字符了。

所以我们只能用一个足够大的数组来存储名字,例如 char[100]。你会说这样太浪费内存了,但是前面我们也说过了,目前的电脑一般不在乎这点内存。

所以我们的程序会是这样:

intmain(intargc,char*argv[]){

charname[100];

printf("请问您叫什么名字?");

scanf("%s",name);

printf("您好,%s,很高兴认识您!\n",name);

return0;

}

运行程序:

请问您叫什么名字?Oscar

您好,Oscar,很高兴认识您!

7. 操纵字符串的一些常用函数

字符串在 C语言里是很常用的。事实上,此刻你在电脑或手机屏幕上看到的这些单词、句子等,都是在电脑内存里的字符数组。

为了方便我们操纵字符串,C语言的设计者们在 string 这个标准库中已经写好了很多函数,可供我们使用。

当然在这以前,需要在你的 .c 源文件中引入这个头文件:

#include

下面我们就来介绍它们之中最常用的一些吧:

strlen:计算字符串的长度

strlen 函数返回一个字符串的长度(不包括 '\0')。

为什么名字是 strlen?其实很好记:

str 是 string(表示“字符串”)的前三个字母。

len 是 length(表示“长度”)的前三个字母。

因此,strlen 就是“字符串长度”。

函数原型是这样:

size_tstrlen(constchar*string);

注意:size_t 是一个特殊的类型,它意味着函数返回一个对应大小的数目。

不是像 int,char,long,double 之类的基本类型,而是一个被“创造”出来的类型。

在接下来的课程中我们就会学到如何创建自己的变量类型。

暂时说来,我们先满足于将 strlen 函数的返回值存到一个 int 变量里(电脑会把 size_t 自动转换成 int)。当然,严格来说应该用 size_t 类型,但是我们这里暂时不深究了。

函数的参数是 const char * 类型,之前的课程中我们学过,const(只读的变量)表明此类型的变量是不能被改变的,所以函数 strlen 并不会改变它的参数的值。

写个程序测试一下 strlen 函数:

#include

#include

intmain(intargc,char*argv[]){

charstring[]="Hello";

intstringLength=0;

//将字符串的长度储存到stringLength中

stringLength=strlen(string);

printf("字符串%s中有%d个字符\n",string,stringLength);

return0;

}

程序运行,显示:

字符串Hello中有5个字符

当然了,这个 strlen 函数,其实我们自己也可以很容易地实现。只需要用一个循环,从开始一直读入字符串中的字符,计算数目,一直读到 '\0' 字符结束循环。

我们就来实现我们自己的 strlen 函数好了:

#include

#include

intstringLength(constchar*string);

intmain(intargc,char*argv[]){

charstring[]="Hello";

intlength=0;

length=stringLength(string);

printf("字符串%s中有%d个字符\n",string,length);

return0;

}

intstringLength(constchar*string){

intcharNumber=0;

charcurrentChar=0;

do

{

currentChar=string[charNumber];

charNumber++;

}while(currentChar!='\0');//我们做循环,直到遇到'\0',跳出循环

charNumber--;//我们将charNumber减一,使其不包含'\0'的长度

returncharNumber;

}

程序输出:

字符串Hello中有5个字符

strcpy:把一个字符串的内容复制到另一个字符串里

为什么名字是 strcpy?其实很好记:

str 是 string 的前三个字母。

cpy 是 copy(表示“拷贝,复制”)的缩写。

因此,strcpy 就是“字符串拷贝”。

函数原型:

char*strcpy(char*targetString,constchar*stringToCopy);

这个函数有两个参数:

targetString:这是一个指向字符数组的指针,我们要复制字符串到这个字符数组里。

stringToCopy:这是一个指向要被复制的字符串的指针。

函数返回一个指向 targetString 的指针,通常我们不需要获取这个返回值。

用以下程序测试此函数:

#include

#include

intmain(intargc,char*argv[]){

/*我们创建了一个字符数组 string,里面包含了几个字符。

我们又创建了另一个字符数组copy,包含100个字符,为了足够容纳拷贝过来的字符*/

charstring[]="Hello",copy[100]={0};

strcpy(copy,string);//我们把string复制到copy中

//如果一切顺利,copy的值应该和string是一样的

printf("string是%s\n",string);

printf("copy是%s\n",copy);

return0;

}

程序输出:

string是Hello

copy是Hello

如果我们的 copy 数组的长度小于 6,那么程序会出错,因为 string 的总长度是 6(最后有一个 '\0' 字符串结束符)。

strcpy 的原理图解如下:

strcat:连接两个字符串

为什么名字是 strcat?其实很好记:

str 是 string 的前三个字母。

cat 是 concatenate (表示“连结,使连锁”)的缩写。

因此,strcat 就是“字符串连结”。

strcat 函数的作用是连接两个字符串,就是把一个字符串接到另一个的结尾。

函数原型:

char*strcat(char*string1,constchar*string2);

因为 string2 是 const 类型,所以我们就想到了,这个函数肯定是将 string2 的内容接到 string1 的结尾,改变了 string1 所指向的字符指针,然后返回指向 string1 所指字符数组的指针。

略微有点拗口,但不难理解吧。

写个程序测试一下:

#include

#include

intmain(intargc,char*argv[]){

/*我们创建了两个字符串,字符数组string1需要足够长,因为我们要将string2的内容接到其后*/

charstring1[100]="Hello",string2[]="Oscar!";

strcat(string1,string2);//将string2接到string1后面

//如果一切顺利,那么string1的值应该会变为"HelloOscar!"

printf("string1是%s\n",string1);

//string2没有变

printf("string2始终是%s\n",string2);

return0;

}

程序输出:

string1是HelloOscar!

string2始终是Oscar!

strcat 的原理如下:

当 strcat 函数将 string2 连接到 string1 的尾部时,它需要先删去 string1 字符串最后的 '\0'。

strcmp:比较两个字符串

为什么名字是 strcmp?其实很好记:

str 是string 的前三个字母。

cmp 是 compare(英语“比较”)的缩写。

因此,strcmp 就是“字符串比较”。

函数原型:

intstrcmp(constchar*string1,constchar*string2);

可以看到,strcmp 函数不能改变参数 string1 和 string2,因为它们都是 const 类型。

这次,函数的返回值有用了。strcmp 返回:

0:当两个字符串相等时。

非零的整数(负数或正数):当两个字符串不等时。

用以下程序测试 strcmp 函数:

#include

#include

intmain(intargc,char*argv[]){

charstring1[]="Textoftest",string2[]="Textoftest";

if(strcmp(string1,string2)==0)//如果两个字符串相等

{

printf("两个字符串相等\n");

}

else

{

printf("两个字符串不相等\n");

}

return0;

}

程序输出:

两个字符串相等

sprintf:向一个字符串写入

当然,这个函数其实不是在 string.h 这个头文件里,而是在 stdio.h 头文件里。但是它也与字符串的操作有关,所以我们也介绍一下,而且这个函数是很常用的。

看到 sprintf 函数的名字,大家是否想到了printf 函数呢?

printf 函数是向标准输出(一般是屏幕)写入东西,而 sprintf 是向一个字符串写入东西。最前面的 s 就是英语 string 的首字母。

写个程序测试一下此函数:

#include

intmain(intargc,char*argv[]){

charstring[100];

intage=18;

//我们向string里写入"你18岁了"

sprintf(string,"你%d岁了",age);

printf("%s\n",string);

return0;

}

程序输出:

你18岁了

其他常用的还有一些函数,如 strstr(在字符串中查找一个子串),strchr(在字符串里查找一个字符),等等,我们就不一一介绍了。

8. 总结

电脑不认识字符,它只认识数字(0 和 1)。为了解决这个问题,计算机先驱们用一个表格规定了字符与数值的对应关系,最常用的是 ASCII 表和 Unicode 表。

字符类型 char 用来存储一个字符,且只能存储一个字符。实际上存储的是一个数值,但是电脑会在显示时将其转换成对应的字符。

为了创建一个词或一句话,我们需要构建一个字符串,我们用字符数组来实现。

所有的字符串都是以 '\0' 结尾,这个特殊的字符 '\0' 标志着字符串的结束。

在 string 这个 C语言标准库中,有很多操纵字符串的函数,只需要引入头文件 string.h 即可。

9. 第二部分第五课预告

今天的课就到这里,一起加油咯。

下一课:C语言探索之旅 | 第二部分第五课:预处理

作者:谢恩铭

出处:公众号「程序员联盟」

原文链接:/p/2be7006765ec

转载请注明出处,谢谢合作!转载授权请加我微信 frogoscar

喜欢本文的朋友,欢迎关注公众号程序员联盟,收看更多精彩内容

点个[在看],是对我最大的支持!

如果觉得《c 字符串数组_C语言探索之旅 | 第二部分第四课:字符串》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。