C语言中字符串详解

C语言中字符串详解

字符串时是C语言中很是重要的部分,咱们从字符串的性质和字符串的建立、程序中字符串的输入输出和字符串的操做来对字符串进行详细的解析。程序员

什么是字符串?

C语言自己没有内置的字符串类型,字符串本质上是一种特殊类型的数组,它的组成元素类型为char,除此以外不受制与数组长度的限制,以'\0'做为结束标志,做为字符串结束的标志。(\0做为一个特殊字符,它的ASCII值为0,可是它不是'0'字符,'0'字符的ASCII值为48。)express

定义字符串

1. 字符串字面量(字符串常量)

字符串字面量形如"string",也被称为字符串常量,编译器会将它末尾自动添加上字符串结尾标志\0。它做为一种静态存储类型, 在程序开始运行时被分配地址,一直存在到程序结束,引号括起来的部分将表示它储存的首地址,很相似于数组,数组名做为数组首元素储存的地址。数组

 #include <stdio.h>
 
 int main() {
 printf("%s %p   %c", "Hello", "Hello", *"Hello");
 return 0;
 }
 /**
  * Hello 00405044   H
  * **/

上面说明了字符串常量的储存形式,并且它自己只表明首元素的地址。安全

2. 字符串数组形式的初始化

字符串以一种特殊的字符串数组的形式存在,区别于通常数组,进行通常初始化时:函数

char a[] = {'h', 'e', 'l', 'l', 'o', '!', '\0'};测试

而不能是:ui

char a[] = {'h', 'e', 'l', 'l', 'o', '!'};spa

后者仍然是一个普通的字符串数组,不是字符串,这样的初始化显然是麻烦的,咱们能够这样:设计

char a[] = "hello!";指针

或者

char *a = "hello!";

怎么理解这两种行为呢,他们都使用a储存了字符串hello!的地址,可是它们也是有所不一样的,下面详细讨论下他们的区别所在。

3. 字符串数组和指针

  • 字符串数组形式:咱们知道字符串常量以静态形式储存在程序中,使用字符串数组来对它进行存储时须要将其拷贝到新的储存空间,而后将新的储存空间地址赋值到a上。

  • 指针形式:这时候就是一个常规意义上的赋值,咱们把在静态储存区的常量地址直接赋值到a上。

这样本质的区别有什么在应用上的区别呢?其一,使用字符串数组a做为常量指针来储存地址,使用指针形式是一种变量来储存地址;其二,由于字符串数组将是一种对原字符常量的一种拷贝,因此咱们支持和容许对这样字符串的修改,可是指针只是对原常量地址的一种储存,咱们不容许对常量进行修改,因此经过这个指针对原字符进行修改是未定义的恶劣行为,咱们看下面的程序:

 #include <stdio.h>
 
 int main() {
     char *a = "hello!";
     a[0] = 'w';
     printf("%s", "hello!");
 }

这样的程序看起来没问题,咱们但愿将hello!修改成wello!,而后咱们但愿打印hello!,可是这样的程序可能输出wello!,由于咱们修改了源地址上的数据,固然编译器也有可能崩溃。

因此通常状况下,咱们只但愿同过使用常量指针来储存字符串

const char *a = "hello!";,这样能够避免在程序中出现异常的修改常量的错误。

因此咱们能够总结,咱们但愿修改字符串时使用字符串数组,只但愿读取字符串时咱们使用指针,并且应该是常量指针。

还有一些关于它们值得讨论的部分,假如咱们想要使用咱们有一个字符串数组(本质上做为一个字符数组的数组),有下面两种形式:

char a[3][20] = {"I love you.", "Do you love me?", "Please."};

char *a[3] = {"I love you.", "Do you love me?", "Please."};

这样又有什么区别呢?第一个字符串数组占用3*20*1 = 60byte,第二个占用3个指针为3*4=12byte。在程序非静态部分无疑是后者更为俭省。并且前者由于固定格式的缘由,字符良莠不齐可是它们建立的空间却都必须知足容纳最长的字符串,形成必定空间的浪费。

因此想要使用一系列待显示的字符串时可使用指针数组,想要修改字符串在以后则使用通常形式的字符串的数组。

还有对字符串的拷贝,由于字符串变量所存在的形式都是字符串首元素的地址,因此咱们下意识对于字符串的拷贝每每是不起做用的:

 #include <stdio.h>
 
 int main() {
  char *a = "Hello!";
  char *pa = a;
  printf("a = %s   %s = pa\n", a, pa);
  printf("a -> %p\n", a);
  printf("pa -> %p\n", pa);
  printf("a = & %p\n", &a);
  printf("pa = & %p\n", &pa);
 }
 
 /**
  a = Hello!   Hello! = pa
  a -> 00405044
  pa -> 00405044
  a = & 0061FF1C
  pa = & 0061FF18
  * **/

在这里a和pa做为字符串打印时,内容时彻底相同的,可是仔细看咱们发现他们起始指向了相同的地址,也就是所咱们并无完成对字符串内容的拷贝,而只是对地址值的拷贝,并且a和pa做为指针储存在相邻的两个单元,相隔4个字节。这样的拷贝在某些意义上不大,咱们将在下面再讨论如何对于字符串进行拷贝。

字符串I/O

首先,在了解了字符串性质的状况下,咱们来了解字符串I/O,由于字符串须要在建立时得到一段连续的数组空间,因此尝试将输入的字符串加载进入程序时,咱们须要先 分配空间

这样作是必要的,由于对于未分配内存的字符指针,咱们并不知道它的初始状态,它可能指向任意位置,咱们在进行输入的时候颇有可能所以抹除了先前储存位置上的数据,一般这是不被编译器容许的,每每会形成程序崩溃。

 #include <stdio.h>
 
 int main() {
  char *a;
  scanf("%s", a);// 这个程序可能会崩溃
  puts(a);
  return 0;
 }
 

因此在处理字符串I/O以前,首先要考虑的就是为输入的字符串分配空间,并且保证输入的字符串不超过咱们申请的空间。

下面咱们来看一些I/O函数来深刻理解这样的理念。

1. gets()被废弃的选项

gets(),gets须要一个参数,一个字符串指针,它从I/O设备上读取一行信息(等到遇到一个换行符中止),而后在末尾添加空字符,最后的换行符也会被读取并丢弃。

看起来这是一个很不错的I\O函数,可是在C99标准中它被建议不要使用,在C11标准中被彻底废弃,这是由于它存在着严重的隐患,看下面这段程序:

 #include <stdio.h>
 
 int main() {
  char b[5] = "hhhh";
  char a[5];
  gets(a);
  puts(a);
  puts(b);
  return 0;
 }
 
 /**
  abcd
  abcd
  hhhh
  * **/
 
 /**
     abcdefghijklmn
     abcdefghijklmn
     fghijklmn
  **/

这段程序,咱们输入了两段内容进行测试,第一次abcd恰好长度为5,gets函数正常接受将它放到a分配的地址中,没有出现问题;可是在第二次咱们输入了超过了既定分配长度的字符,咱们发现原字符出现了异常的变化,超过了既定长度5,容纳下了全部的输入字符,可是随之咱们原有的字符串b也被彻底修改,原数据被彻底抹除。

这是由于它们的地址恰好相邻,gets函数并不会对字符长度进行检查,它只会将一整行的数据放入指针指向区域上,即便超过申请空间的边界,他也会继续写入,抹去相邻区域的数据也在情理之中了。

这给咱们程序带来了巨大的危险,若是溢出的部分占用了未使用的空间问题并不大,可是它轻易抹除以使用空间中内容极可能致使程序崩溃,因此咱们不要使用gets函数,应该尝试更多的根据建议使用fgets()或则gets_s来避免这样的问题。

2. fgets()和gets_s()

为了解决gets函数中存在的问题,有两个能够函数做为替代。

首先是基于gets的升级版gets_s他须要另外的一个参数指定最大读取长度,并根据这个长度来作出相对应的操做:

  • 正常状况下,gets_s从标准输入流中读取信息,相似于gets在未达到最大长度并且读取到换行符时,它将从缓冲区读取该换行符并将其丢弃并在末尾补充上空字符。

  • 在读取出现问题时,gets_s读取到最大读取长度数目的字符可是仍然未读取到换行符时,gets_s将会将对应指针(数组首字符)指向数据设定为空字符,而后继续读取知道读取到文件末尾或者换行符,而后返回空指针,以后调用依赖实现的函数的处理函数,可能停止或者退出程序。

在这里咱们给出一段处理函数使用的实例:

 #include <stdio.h>
 #include <stdlib.h>
 #include <crtdbg.h>  // For _CrtSetReportMode
 
 void myInvalidParameterHandler(const wchar_t* expression,
                                const wchar_t* function,
                                const wchar_t* file,
                                unsigned int line,
                                uintptr_t pReserved)
 {
     wprintf(L"Invalid parameter detected in function %s."
             L" File: %s Line: %d\n", function, file, line);
     wprintf(L"Expression: %s\n", expression);
     printf("Error!");
 }
 
 int main() {
     char a[5];
 
     _invalid_parameter_handler oldHandler, newHandler;
     newHandler = myInvalidParameterHandler;
     oldHandler = _set_invalid_parameter_handler(newHandler);
     _CrtSetReportMode(_CRT_ASSERT, 0);
     gets_s(a, 5);
     puts(a);
     return 0;
 }

在这里及时咱们输入超过5位字符,程序也不会呈现崩溃退出。

详细信息参照

咱们发现gets_s函数中使用并不特别方便,还有一个函数能够做为替代fgets函数,它相较于前二者,还须要另一个参数,读入文件名称,若是从键盘中读取,那么即为标准输入流stdinfgets函数的通常行为:

  • 正常读取到换行符或则文件末尾时,读取中止,将换行符读入字符串中而后在字符串末尾上填入空字符。这时候函数会返回指向读取函数储存位置的指针,若是到达文件末尾将返回空指针,当读取发生某些其余错误时也会返回空指针,在C语言中它被定义为宏NULL

  • 在读取超过字符串最大长度的字符时,将要达到最大长度时中止读取而后在末尾补充上空字符。在读取到文件末尾时函数会返回空指针。

 #include <stdio.h>
 
 int main() {
     char a[5];
     char *status = fgets(a, 5, stdin);
     puts(a);
     printf("a = &%p\tstatus = &%p\n", a, status);
     return 0;
 }
 
 
 /**
     123456
     123
     a = &0135FA10   status = &0135FA10
  * **/
 
 /**
     123
     123
 
     a = &0061FF17   status = &0061FF17
  **/

对于最大长度参数n,代表函数最多读取n-1个数据(包括换行符),因此输入123会将换行符正常读取而后puts函数又输出了一个换行符,因此输出了两个换行符;可是输入更多时,函数读取到四个数据后中止读取补充上空字符。

不一样于gets_s函数,读取不到换行符时,函数也不会对缓冲区中其余数据作出任何操做,对于前者会清空缓冲区中全部下一个换行符前的全部内容,可是fgets并不会,咱们能够自由的选择对这些缓冲区的内容进行处理。

因为fgets()函数的安全性和可扩展性更佳,因此咱们推荐更多的去使用fgets()函数。它每每是最佳选择。

3. scanf()不甚理想的选择

scanf做为泛用性很强的函数,也有它读取字符串的模式:

scanf("%s", a);

可是使用它来读取字符串并非最理想的选择,由于scanf函数读取字符时开始与一个非空字符,终止于第一个空字符。这样下来他可能只能够读取到一个简单的单词,而不是咱们指望的包含空格等完整内容的字符串,因此通常状况下咱们不使用scanf读取整句字符串,而将它用于单词和具备特定格式的字符的读取。

咱们能够经过转换说明修饰符来读取规则的字符串:

scanf("%5s", a);这样就能够读取长度为5的单词(中间读取到换行符依旧会中止读取,其中不包括空字符),功能能够比拟fgets(a, 6, stdin);,可是后者可能包括特殊的换行符之类,因此它们也算是各有用武之地。

4. 输出函数

系统的说明了几个C语言输入函数,咱们如今来相似的梳理输出函数,它们与输入函数是相对应的,也是各有特点的。

  • puts——gets,输出字符串直到空字符,而且会在最后输出一个换行符,这样的存在也可能访问到未被分配的内存,这样的行为是未定义的,可是这样很不靠谱。

  • fputs——fgets,输出字符直到碰到空字符,可是与fgets匹配,它不会在输出最后输出换行符,并且须要额外的参数指示输出位置,若是是屏幕则为stdout

  • printf——scanfscanf相较于前二者较为多才多艺,不会输出换行符,能够根据本身对格式的要求进行自由控制,并且在同时输出多个字符时用起来十分方便。

字符串处理函数

讨论完字符串性质和I\O后咱们来继续讨论和字符串息息相关的一些C语言自带的字符串处理函数(其中大部分都是咱们能够实现的),熟悉他们方便咱们更好的处理字符串。通常状况下他们定义在头文件string.h中。

  1. strcatstrncat

这两个函数被用来字符串合并。

对于strcat接受两个字符串指针做为参数,将第二个字符串接到第一个字符串上,而后返回第一个字符串的指针,可是它也存在相似gets的缺陷,当第一个指针所指向被分配的空间并不足够大时,额外从第二个字符读取的字符将会可能覆写掉其余已经分配空间上的数据。可是基于C语言制定时相信程序员的准则它仍然能够继续使用,不一样于getsgets产生的错误可能由用户制造,可是strcat制造的问题却能够由程序员来避免,因此它仍然可使用。

strncat须要额外的一个指定拷贝后的字符的最大长度(包含空字符),以此来保证拷贝后的数组不会超过以分配的储存空间,其余内容同strcat一致。

  1. strcmpstrncmp

这两个函数用于字符串比较。

对于strcmp,接受两个字符串指针比较它们指向的字符串(而不是它们所指向的地址)若是相同则返回0,不然返回非零的数字,具体状况根据编译器的实现有所不一样。

strcmp也能够经过指定从指定的起始位置开始比较字符串,只须要在传递指针时进行加减运算:

strcmp(a+5, b+4);这样使得字符串的比较更加灵活。

strncmp使得字符串的比较更加灵活,经过第三个参数n来指定比较的长度,咱们能够进行前缀匹配。

  1. strcpystrncpy

这两个函数用于字符串的拷贝。

strcpy拷贝第二个字符串指针的字符到第一个字符串指针所指向的空间中去,可是咱们也须要注意第一个参数所指向的空间也必须足够大容纳第二个字符串。咱们也大可没必要从字符开始部分开始拷贝,咱们能够吧参数指针移动到任何咱们想要它拷贝到的位置:

strcpy(a+4, "hello!");

strncpy弥补了strcpy的缺点,能够在第三个参数中指定拷贝的最大长度(这个大小不包含空字符,由于函数设计就预想到可能碰不到空字符就要中止,因此拷贝完这个最大长度后,函数会向原字符后自动添加上空字符),可是n的大小最大为第一个字符数组空间大小减去1。

  1. sprintf

sprintf声明在stdio.h中,相似于printf它能够将字符串进行格式化并输出到一个字符串中,使用时一样须要考虑字符串分配空间的问题,这个问题在全部涉及字符串的使用时都要考虑!下面看一段用例:

 #include <stdio.h>
 
 int main() {
     char *s = "Today is ";
     int year = 2021, month = 2, day = 2;
     char data[30];
     sprintf(data, "%s%d/%d/%d.", s, year, month, day);
     puts(data);
     return 0;
 }
 
 
 /**
    Today is 2021/2/2.
  * **/

总结

总的来讲字符串使用时,不管在什么时候务必用注意分配空间的使用,不要访问到未分配的空间,这样会给程序带来没法预料的结果。

相关文章
相关标签/搜索