C语言中字符串详解

字符串时是C语言中非常重要的部分,我们从字符串的性质和字符串的创建、程序中字符串的输入输出和字符串的操作来对字符串进行详细的解析。

什么是字符串?

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

定义字符串

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'};

而不能是:

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

后者仍然是一个普通的字符串数组,不是字符串,这样的初始化显然是麻烦的,我们可以这样:

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.
  * **/

总结

总的来说字符串使用时,无论在何时务必用注意分配空间的使用,不要访问到未分配的空间,这样会给程序带来无法预料的结果。

 

C语言中字符串详解的更多相关文章

  1. MS SQL Server 数据库连接字符串详解

    MS SQL Server 数据库连接字符串详解 原地址:http://blog.csdn.net/jhhja/article/details/6096565 问题 : 超时时间已到.在从池中获取连接 ...

  2. 【转】angular中$parse详解教程

    原文: https://yq.aliyun.com/ziliao/40516 ------------------------------------------------------------- ...

  3. C语言memset函数详解

    C语言memset函数详解 memset() 的作用:在一段内存块中填充某个给定的值,通常用于数组初始化与数组清零. 它是直接操作内存空间,mem即“内存”(memory)的意思.该函数的原型为: # ...

  4. Python变量和字符串详解

    Python变量和字符串详解 几个月前,我开始学习个人形象管理,从发型.妆容.服饰到仪表仪态,都开始做全新改造,在塑造个人风格时,最基础的是先了解自己属于哪种风格,然后找到参考对象去模仿,可以是自己欣 ...

  5. C语言之预处理详解

    C语言之预处理详解 纲要: 预定义符号 #define #define定义标识符 #define定义宏 #define的替换规则 #与## 几点注意#undef 带副作用的宏参数 宏和函数的对比 命名 ...

  6. C语言内存对齐详解(2)

    接上一篇:C语言内存对齐详解(1) VC对结构的存储的特殊处理确实提高CPU存储变量的速度,但是有时候也带来了一些麻烦,我们也屏蔽掉变量默认的对齐方式,自己可以设定变量的对齐方式.VC 中提供了#pr ...

  7. C语言内存对齐详解(3)

    接上一篇:C语言内存对齐详解(2) 在minix的stdarg.h文件中,定义了如下一个宏: /* Amount of space required in an argument list for a ...

  8. 【转】declare-styleable的使用(自定义控件) 以及declare-styleable中format详解

    原文网址:http://www.cnblogs.com/622698abc/p/3348692.html declare-styleable是给自定义控件添加自定义属性用的 1.首先,先写attrs. ...

  9. SQL Server日期时间格式转换字符串详解

    本文我们主要介绍了SQL Server日期时间格式转换字符串的相关知识,并给出了大量实例对其各个参数进行对比说明,希望能够对您有所帮助. 在SQL Server数据库中,SQL Server日期时间格 ...

随机推荐

  1. 【2020杭电多校】Total Eclipse 并查集+思维

    题目链接:Total Eclipse 题意: t组输入,给你一个由n个点,m条边构成的图,每一个点的权值是ai.你每一次可以选择一批联通的点,然后让他们的权值都减去1.问最后把所有点的权值都变成0需要 ...

  2. P1541 乌龟棋(DP)

    题目背景 小明过生日的时候,爸爸送给他一副乌龟棋当作礼物. 题目描述 乌龟棋的棋盘是一行NNN个格子,每个格子上一个分数(非负整数).棋盘第1格是唯一的起点,第NNN格是终点,游戏要求玩家控制一个乌龟 ...

  3. poj1787 Charlie's Change

    Description Charlie is a driver of Advanced Cargo Movement, Ltd. Charlie drives a lot and so he ofte ...

  4. Codeforces Gym-102219 2019 ICPC Malaysia National E. Optimal Slots(01背包+输出路径)

    题意:给你一个体积为\(T\)的背包,有\(n\)个物品,每个物品的价值和体积都是是\(a_{i}\),求放哪几个物品使得总价值最大,输出它们,并且输出价值的最大值. 题解:其实就是一个01背包输出路 ...

  5. CodeForces - 1201B Zero Array

    You are given an array a1,a2,-,ana1,a2,-,an. In one operation you can choose two elements aiai and a ...

  6. Codeforces Round #666 (Div. 2) C. Multiples of Length (构造,贪心)

    题意:有一个长度为\(n\)的序列,可以操作\(3\)次,每次选取一段区间,然后区间的元素加减区间长度的倍数,\(3\)次操作后使得序列所有元素为\(0\),问具体操作情况. 题解:假如我们能选择一整 ...

  7. java中的装箱及拆箱

    java中存在8中基本的数据类型,每一种数据类型都有包装类型. 包装类型:每一个基本的数据类型都会------对应一个包装类型. boolean------------------>Boolea ...

  8. Leetcode(15)-三数之和

    给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组. 注意:答案中不可以包含重复的三元组. ...

  9. redis持久化-AOF

    1.aof文件写入与同步 2.aof重写 重写的目的是为了减小aof文件的体积,redis服务器可以创建一个新的aof文件来代替现有的aof文件,新文件不会有冗余的命令. BGREWRITEAOF:遍 ...

  10. 洛谷p2216 多次单调队列,扫描矩阵中的最大值减去最小值最的固定大小子矩阵

    #include <iostream> #include <cstdio> #include <cstring> using namespace std; int ...