前言
本篇旨在帮助不了解字符串或者逻辑梳理不够透彻的伙伴们理出一条脉络。选择能看懂的部分即可,建议收藏,后期学习完C语言方便回顾。
适用范围:0基础C语言(刚学字符串)- 学过函数 - 学过指针 ---大致了解了数据内存(栈、堆、静态区、常量区) --- C语言语法大成者。
只是梳理逻辑,代码相对简单,对字符串的算法题型(洛谷,力扣)不涉及。
这里不在赘述字符,字符数组的东西了。因为最开始学数据类型和数组时已经
我们知道C语言中字符串是一种重要的数据类型,它的一些相关特点我们将在本篇重新介绍梳理,C库提供了大量的字符串函数供我们操作,比如strlen就是计算字符串长度的函数。后面还有大量的字符串函数,比较字符串,拷贝字符串,查找字符串的有关函数。还有通过指针模拟实现一些函数,以此熟悉。
字符串概念
几乎每C相关的书籍都会都会单独为字符串开设一篇专题,可见字符串的重要性可见一般。前面有提过字符(char )和字符数组(char []);常量字符串和变量字符串的概念是否了解呢?
这里整合字符串一系列的知识,处理字符串的便捷方法。
引入(两个疑问)
0.空字符是以下哪个?
'\0' '0' ' '
这里的空指的是null ,即对应ASCII十进制为0的字符,是'\0'
后面'0'是字符0,' '是空格字符。
#include<stdio.h>
int main(void)
{
printf("%d %d %d", '\0', '0', ' ');
return 0;
}
1.字符串和字符的关系?
字符是形如a,b,c,!,@,.,/的一系列符号。在C中用单引号括起来,如char s1 = 'a' ;
字符串顾名思义就是一串字符,即字符的序列。只不过术语称其为“字符串”。
2.字符串和字符数组的关系?
字符串在C中特指以双引号引起来的字符序列。如"abcd",需要注意的是这种写法会默认最后补上一个null(空字符)。
从C来说,它会将字符串当作字符数组处理,即如果一个字符串长度为n就为其开辟n+1的内存空间,(多的1放'\0')。结论是字符串是字符数组,而通过前面的学习知道一般的字符数组不一定是字符串。
既然字符串被当作数组存储,那么对于一个字符串"abcd",编译器会将其视为char* 的指针。
char* p = "abcd";
"abcd"是字符数组,那么上面的语句就是将首元素的地址赋值给字符指针p;
至于"abcd"具体存储内存那个位置,下面再谈。
一、字符串表示(概念)
字符串是'\0'结尾的char类型的数组。'\0'称为空字符,这个我们初学C语言了解字符串已经知道了。
如下图,如果以这样的方式初始化一个字符数组,末尾有'\0'就是字符串, 没有末尾的空字符,这个数组单纯就是一般的字符数组。
#define N 20
const char arrary[N] = {'h','e','l','l','o',' ','w', 'o','r','l','d','\0'};
char arrary2[] = "hello world";
char* arr = "hello world";
双引号括起来的内容也是字符串,叫常量字符串(字符串常量)。双引号内的字符和编译器结尾默认添加空字符,都将字符串存储在内存中。
由于双引号引起来的后面默认添加'\0',如果存储在字符数组中,请确保实际数组元素个数比引起来的字符个数至少多一。如果字符数组空间足够大,则双引号为填充完的部分默认都为'\0'。
首先我们要了解常量字符串是在静态储存区(常量区),这意味着使用在常量字符串会在内存中存储一次。常量字符串的生命周期是变量被存储到程序结束。
Q:怎么证明常量字符串只被存储一次呢?
声明两个字符指针变量同时指向它,如果它被存储一次,说明两个指针指向同一个内存块(存储的地址相同),那么可以进行指针减法,且结果为零。
#include<stdio.h>
int main()
{
char* arr1 = "abcd";
char* arr2 = "abcd";
printf("%d", arr2 - arr1);
return 0;
}
需要注意的是,两个指针在main函数声明创建,是在栈区。它们指向(存储)的地址是常量区的常量字符串(实际存储的是常量字符串首字符的地址) 。
char arrary2[] = "hello world";
char* arr = "hello world";
如果将这个用第一种方式,理解为创建一个arrary2字符数组向内存申请空间,将常量字符串的内容拷贝一份,这么写数组大小交给编译器确定,否则请确保数组元素个数大于字符串中已经填入的字符个数。(编译器后面会添加'\0',至少数组元素个数要比字符数多一)
事实上,常量字符串(双引号括起来的部分被视为指向该字符串的指针),所以第二种方式就可以理解了。这里意思是让arr指向字符串中的第一个字符。
第一种和第二种它们的位置是一样的吗?
显然不是,一个在栈,另一个在静态区。
如果不了解栈堆静态区,可以了解C语言动态内存分配,C/C++数据在内存中的分布。
字符串与数组
一种对于字符串当作数组的理解
#include<stdio.h>
int main(void)
{
for (int i = 0; i < 4; i++)
{
printf("%c ", "abcd"[i]);
}
return 0;
}
字符串被视为字符数组,同样可以用处理数组的方式处理它。"abcd"当作这个字符数组的数组名,也就是数组首元素的地址。可以用下标引用操作符和指针+整数来访问字符串中的每一个字符。
#include<stdio.h>
int main(void)
{
for (int i = 0; i < 4; i++)
{
printf("%c ", *("abcd"+i));
}
return 0;
}
把字符数组当作字符串处理的难处
有些编程语言为字符串单独开了一个数据类型string,很遗憾,根据现在我们了解。C语言并没有字符串类型,而是把它当作数组类型。
用字符数组来处理字符串,可能有以下问题。
1.字符数组最后一个元素必须为空字符。
2.如果字符串长度确定,确保字符数组空间至少大一,确保每个字符数组以空字符结尾。
由于C中关于字符串函数都有'\0'的标志结尾,如果不注意空字符,很容易在调用这些函数上出问题。记住字符串函数是用来处理字符串的,不能用来处理一般的字符数组。
字符数组和字符指针
指针篇曾回答了下列问题,先看一下下面的图片,稍后补充一些东西。
这里提供了我们操作字符串的另一种方式,即通过字符指针的形式。
比如上面的p1就指向"hello world",就可以访问字符串了,只是不能修改字符串常量中的字符。
这里建议用const强调一下。 const char* p1 = "hello world";
第二种就是字符指针指向用字符数组处理的字符串,即p2。这里就可以将p2作为字符串任意使用了。这里p2指向的是定长数组arr,还可以让p2指向动态内存分配的字符串。
字符串与函数
一、字符串“最好用”的两个IO函数gets , puts(适合C语言萌新)
据说每一个C语言新手都要掌握的6个(I/O)函数:
printf scanf
gets puts
getchar putchar
1.1 输入函数(Input)
1.1.1 "单词函数"scanf函数
转换说明%s,可以将字符串写入字符数组,或者动态分配的内存空间。
通过下面说明scanf输入字符串的问题。
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
char* p = (char*)malloc(sizeof(short) * 10);
scanf("%s", p);
puts(p);
return 0;
}
尝试在屏幕上输入 hello world
观察结果
按理说puts应该打印hello world。结果却不如所愿,这说明scanf读到空格就不往后读了 。整行输入只读到前面hello这个单词。scanf函数处理不了类似英语中的句子短语,只能处理小小的单词。
所以称其为"单词输入"函数。
scanf函数在输入时会跳过最开始的空格,开始往后读字符直到遇到空字符,换行符,制表符。会在读入最后的一个字符后面补上'\0',确保其是字符串。
按回车键会立刻结束输入。
scanf的缺点:通常不能读入整行输入,遇到空格符就不读了。
这个时候就需要gets函数来解决这个问题了。
1.1.2 "抛开缺点不谈巨好用"的gets函数
标准输入是什么?
简单理解就是从键盘上获取数据。
参数是字符指针,逐字符从键盘上读取(可以读取空字符),将其放入数组内或者动态分配的内存。按回车键停止输入。
gets不会像scanf跳过最开始的空白字符,一直读取字符直到遇到换行符。
读到换行符停止读取,并将换行符丢弃换成空字符。确保是字符串。
gets和puts天生一对,基本上配对使用。后面会说明puts函数。
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
char* p = (char*)malloc(sizeof(short) * 10);
gets(p);
puts(p);
return 0;
}
gets函数的问题
你可能关注到上面的文字,已经逐步取消了gets函数的使用。这里说明原因:
gets传入参数的字符指针合法使用的空间不知道,极有可能越界访问。比如上面开辟了20字节的空间,可以合法输入19个字符,最后一个放空字符。但大多数情况我们不知道空间是多少,稍不注意就越界了。如果出现字符串过长,就会有缓冲区溢出的问题。如果占有的未分配的空间,问题还不算大;如果占有另外已经使用的空间,可能会引起严重程序事故。
你可能要问scanf不会出现字符串过长的问题吗?
只用%s可能出现问题,但可以用%.ns指定scanf读取的最大字符数,避免出现越界访问。
可惜gets天生不安全。虽然C标准取消了gets,但多数编译器照顾以前的代码还是能使用gets函数。
gets只要注意不越界就是即看即用,参数简单,输入字符串非常方便的函数。
总结:gets函数很方便,会使用,注意避免出现问题即可。
1.1.3 "文件操作"的fgets函数(优化了gets,但变复杂了)
由于gets不能指定输入最大字符数,所以fgets加入参数限制了读入字符个数。
第一个参数:同gets
第二个参数:读入字符的最大数量,实际读入num-1个字符,最后一个放空字符
第三个参数:文件指针,本质是结构体指针,这里你认为默认stdin(标准输入流) 即可。
标准输入流认为从输入设备(键盘)读取数据。
关于fgets函数更常见使用在C文件操作那一块。
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
char* p = (char*)malloc(sizeof(short) * 10);
fgets(p,6,stdin);
puts(p);
return 0;
}
1.2输出函数(Ouput)
1.2.1 老朋友printf函数
printf提供转换说明%s,格式化打印字符串。我们以现在的视角重新观察以下代码。
#include<stdio.h>
int main(void)
{
char arr[20]="What can I say";
printf("%s\n",arr);
}
printf的第二个参数传的是地址,打印结束条件是'\0'。printf格式化打印字符串的功能是从当前起始位置逐个写入字符到格式化字符串并往下继续写直至遇到'\0',最后写入完毕打印格式字符串到屏幕上。
#include<stdio.h>
int main(void)
{
char arr[20]="What can I say";
char* p=arr+5;
printf("%s\n",p);
}
1.2.2 “神中神”puts函数
printf并不是唯一一个能输出字符串的函数,C库提供了一个更完美的函数puts。
这里功能已经说明很清楚了,需要传入字符串的地址。可以用字符指针或者字符数组的形式。
1.stdout标准输出,如果不了解就简单认为是打印到显示屏上。
2.打印完毕会自行换行,不用像printf那样额外加入'\n'
示例
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
char* s1 = "hello world";
puts(s1);
printf("haha");
return 0;
}
1.2.3 文件操作 fputs函数
这个放文件操作说明。这里大致说一下fputs函数怎么用。
FILE* 是文件指针,stream是流。不清楚没关系,意思是fputs函数可以指定输出设备,就不止是输出到屏幕上,还可以是文件等。第一个参数和返回值和puts一样。
如果我们还想要打印到屏幕上,那么第二个参数就是stdout标准输出流。
还有一个点fputs在输出完毕后不会加上换行符,这也是和puts的一个区别。
#include<stdio.h>
int main(void)
{
char* s1 = "hello world";
fputs(s1,stdout);
printf("hehe");
return 0;
}
二、(处理)字符串的常用函数
字符串长度:字符串的首个字符到'\0'之前的那个字符的长度,strlen计算的结果就是字符串的长度。
下面说明处理字符串的10个常见函数。以下仅针对字符串,必须'\0'结尾的字符数组。
2.1strlen函数
size_t strlen (const char* str)
1.strlen求字符串的长度,统计'\0'前面的字符个数。
2.返回类型是size_t ,size_t是无符号整型。
3.包含头文件:string.h
忽视返回类型size_t的错点
看完下面代码,尝试回答一下。
#include <stdio.h>
int i;
int main()
{
i--;
if (i > sizeof(i))
{
printf(">\n");
}
else
{
printf("<\n");
}
return 0;
}
结果是>,为什么呢?
首先全局变量我们不初始化它,默认初始化为0,i--之后,i变为-1。我们知道sizeof(变量名),计算的是变量名的类型,单位是字节,但是sizeof返回类型是size_t。它是一个无符号整型。
不同数据类型进行运算要先转换成同一类型。显式整型转换我们了解(不了解的话可以查一下显隐式类型转换和整型提升的内容,还有二进制),这里涉及到另一种,隐式类型转换。i为int类型(有符号整型),size_t无符号整形,在表达式中比较,int会转换成unsigned int。这里i中-1在无符号整型会转化成很大的整数。
模拟strlen实现方法
1.计数法
size_t my_strlen(char* p)
{
size_t count = 0;
while (*p != '\0')
{
p++;
count++;
}
return count;
}
2.指针-指针
size_t my_strlen(const char* p)
{
char* src = p;//记录初始位置
char* dst = p;//遍历到'\0'的位置
while (*dst)
{
dst++;
}
return dst - src;//终点减起点即结果。
}
3.函数递归
#include<assert.h>
#include<stdio.h>
size_t my_strlen(char* p)
{
assert(p != NULL);
if (*p != '\0')
return 1 + my_strlen(p + 1);
return 0;
}
int main()
{
char arr[] = "abcdef";
size_t len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
铺垫
接下来说明三个处理字符串的函数以及与之对应另外三个函数
strcpy,strcat,strcmp
strncpy,strncat,strncmp
如何记住这些函数的功能呢?
下面请看每个函数的详解
2.2strcpy函数
char* strcpy(char* destination,const char * source );
功能将源字符串拷贝到目标字符数组上。
1.soure必须为字符串,确保有'\0'。
2.destination必须有足够大的空间够拷贝。
3.该函数返回目标字符数组首元素的地址。
4.目标字符指针不得指向常量字符串,即它能被修改。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void)
{
char arr[20]="hello world";
char* p=(char*)malloc(20);
puts(strcpy(p,arr));
}
模拟strcpy实现
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
char* my_strcpy(char* dst, const char* src)
{
assrty(dst && src);
char* ret = dst;//记录当前目标空间的起始地址
while (*src != '\0')
{
*dst = *src;
dst++;
src++;
}
*dst = '\0';//结尾放'\0'
return ret;//返回目标字符串的起始地址
}
int main(void)
{
char arr[20] = "hello world";
char* p = (char*)malloc(20);
puts(my_strcpy(p, arr));
}
2.3strcat函数
char * strcat ( char * destination, const char * source );形式和strcpy一致。
该函数实现的功能:将源字符串添加到目标字符串的末尾
1.返回值:返回目标字符串的地址
2.destination要有足够大的空间追加源字符串。
3.目标空间必须可修改。
4.不能自己和自己追加,目标空间和源空间不能有重叠。
#include<stdio.h>
#include<string.h>
int main()
{
char arr[80];
strcpy(arr,"Good");
strcat(arr," Morning!");
strcat(arr,"\nhello ");
strcat(arr,"world\n");
printf("%s",arr);
return 0;
}
尝试自己与自己追加,strcat函数内部死循环了后非法访问内存,程序崩溃了。
模拟实现strcat
char* my_strcat(char* dst, const char* src)
{
assert(dst && src);
char* ret = dst;
while (*dst != '\0')//先找到目标字符串的'\0'
{
dst++;
}
while (*src != '\0')//从目标字符串的'\0'处开始追加字符。
{
*dst++ = *src++;
}
*dst = '\0';//末尾补上'\0'确保是字符串。
return ret;//返回目标字符串的起始地址
}
2.4strcmp函数
strcmp函数的比较原理:
比较两个参数字符串的对应位置的字符大小(比较对应的ASCII码值)
"abc"和"eq",先比较a和e,由于字符e的ASCII码值大于a所以返回负值。
"abc"和"abc",依次比较,a和a,b和b,c和c,最后'\0'和'\0',空字符了,比较结束,返回0。
示例
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[20] = "abcdef";
char arr2[20] = "abq";
printf("%d", strcmp(arr1, arr2));
return 0;
}
模拟实现strcmp
int my_strcmp(const char* dst, const char* src)
{
assert(dst && src);
while (*dst++ == *src++)
{
if (*dst == '\0')
return 0;
}
return *dst - *src;
}
2.5strncpy函数
char * strncpy ( char * destination, const char * source, size_t num );功能和strcpy一样,只不过有长度限制,多了一个参数num来限制拷贝源字符串的字符个数。如果源字符串的字符个数小于num,则拷贝完源字符串往后继续补'\0'直到个数满足num。
2.6strncat函数
char * strncat ( char * destination, const char * source, size_t num );功能和strcat一样,也限制了长度,也是多一个参数num来限制追加字符的个数如果源字符串的字符个数小于num,则追加完源字符串往后继续补'\0'直到个数满足num。
#include <stdio.h>
#include <string.h>
int main()
{
char str1[20];
char str2[20];
strcpy(str1, "To be ");
strcpy(str2, "or not to be");
strncat(str1, str2, 6);
printf("%s\n", str1);
return 0;
2.7strncmp函数
int strncmp ( const char * str1, const char * str2, size_t num );比较指定前num个字符。和strcmp功能相似,可以指定比较多少位。strcmp一一比较对应的字符,直到的发现不同。完全相同返回0,有任一一位不同返回非0。(正负取决参数的位置,和相应位的ASCII码值)。如果num均大于等于两个字符串各自总个数,功能就和strcmp完全一样了。
2.8strstr函数
char * strstr ( const char * str1, const char * str2);
功能在str1中查找str2,在一个字符串中查找另一字符串。
返回值:返回字符串str2在字符串str1第一次出现的位置。若没找着则返回空指针。
#include<stdio.h>
#include<string.h>
int main()
{
char* p1="Hello";
char* p2="el";
char* ret = strstr(p1,p2);
printf("%s",ret);
return 0;
}
模拟实现有点复杂,可以看一下KMP算法。
char* my_strstr(const char* str1,const char* str2)
{
assert(str1 && str2);
const char* s1=NULL;
const char* s2=NULL;
const char* cur = str1;
if(*str2=='\0')
return (char*)cur;
while(*cur)
{
s1 = cur;
s2 = str2;
while(*s1!='\0'&&*s2!='\0'&&*s1==*s2)
{
s1++;
s2++;
}
if(*s2=='\0')
return (char*)cur;
cur++;
}
return NULL;
}
2.9strtok函数
下面一篇写得足够详细,我们只需要知道如何使用strtok函数即可,不需要掌握模拟实现。
字符分割函数strtok-****博客
3.0streeror函数
char * strerror(int errnum);
头文件:string.h
这个函数可以把参数部分对应的错误码信息的字符串地址返回来。
在不同系统和C语言标准库的实现中都规定了一些错误码,一般是放在errno.h这个头文件中说明的C语言程序启动的时候会用一个全局变量errno来记录程序的错误码,程序启动时,errno的值为0,表示没有错误,如果我们使用标准库函数的时候发生了一些错误,就会将相应的错误码存放到errno中,而一个错误码都有对应的错误信息,strerror函数就可以将错误信息的字符串返回。
errno.h文件中的错误码信息,但我们并不知道这些错误码到底对应了什么,这个时候借助strerror
#include<errno.h>
#include<stdio.h>
#include<string.h>
int main()
{
int i = 0;
for (i = 0; i <= 10; i++)
{
printf("%s\n", strerror(i));
}
}
打印 0~10的错误码信息,结果如下图,strerror会把错误信息的字符串地址返回,用printf打印到屏幕上。通过错误信息,我们就可以知道犯了哪些错误。
试着打印一遍所有错误码对应的错误信息吧。
下面只是提供一个例子,不需要掌握文件知识,只是了解一下文件打开失败,错误码的对应的错误信息
#define _CRT_SECURE_NO_WARNINGS 1
#include<errno.h>
#include<stdio.h>
#include<string.h>
int main()
{
//以只读方式打开"test.txt",如果文件不存在则打开失败。
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//读文件
//关闭文件
fclose(pf);
return 0;
}
perror函数(推荐用这个函数)
什么意思呢?
perror只要调用就可以自动打印errno错误码的信息,并且该函数的参数为字符指针,我们可以自己输入字符串(自定义消息),还是如下例子。
#define _CRT_SECURE_NO_WARNINGS 1
#include<errno.h>
#include<stdio.h>
#include<string.h>
int main()
{
//以只读方式打开"test.txt",如果文件不存在则打开失败。
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen fail");//改为perror函数
return 1;
}
//读文件
//关闭文件
fclose(pf);
return 0;
}
由此可见perror可以直接打印错误信息还可以自定义信息,而streeror还需要和printf配合。
功能上可以理解为 perror==printf+streeror;
perror先打印自定义信息(即perror参数部分的常量字符串),后面跟着冒号在打印错误码对应的错误信息。不想打印自定义信息可以输入空字符串。