目录
🍛前言
本文来分享一波C语言中的字符串、常见的字符串函数以及内存函数的相关介绍和模拟实现,由于笔者水平实在有限,文章难免存在纰漏,读者各取所需即可。
🍛前置知识——字符串
C语言中对字符和字符串的处理很是频繁,但是C语言本身是没有字符串类型的,字符串通常放在常量字符串中或者字符数组中。
字符串常量适用于那些对它不做修改的字符串函数。
🍛1.字符串字面量
用双引号括起来的内容称为字符串字面量, 也叫作字符串常量 。 双引号中的字符和编译器自动加入末尾的\0字符, 都作为字符串储存在内存中, 所以"I am a symbolic stringconstant."、 "I am a string in an array."都是字符串字面量。
从ANSI C标准起, 如果字符串字面量之间没有间隔, 或者用空白字符分隔, C会将其视为串联起来的字符串字面量。
例如:
char greeting[50] = "Hello, and"" how are" " you"" today!";
与下面的代码等价:
char greeting[50] = "Hello, and how are you today!";
字符串常量属于静态存储类别 ,这说明如果在函数中使用字符串常量,该字符串只会被储存一次,在整个程序的生命期内存在, 即使函数被调用多次。(关于静态存储类别可以看看我写的存储类别的博客)
用双引号括起来的内容被视为指向该字符串储存位置的指针。 这类似于把数组名作为指向该数组位置的指针。
例如:
#include <stdio.h>
int main()
{
printf("%s, %p, %c\n", "We", "are", *"space farers");
return 0;
}
printf()根据%s 转换说明打印 We, 根据%p 转换说明打印一个地址。 因此, 如果"are"代表一个地址,printf()将打印该字符串首字符的地址。
字符串"space farers"被视为字符串地址,而字符串地址与首元素地址数值上相同,解引用就相当于解引用第一个字符,所以得到的就是s。
🍛2.字符串数组和初始化
定义字符串数组时, 必须让编译器知道需要多少空间。 一种方法是用足够空间的数组储存字符串。 在下面的声明中, 用指定的字符串初始化数组m1:
char m1[40] = "Limit yourself to one line's worth.";
还可以这样:
char m1[40] = { 'L','i', 'm', 'i', 't', ' ', 'y', 'o', 'u', 'r', 's', 'e', 'l',
'f', ' ', 't', 'o', ' ', 'o', 'n', 'e', ' ','l', 'i', 'n', 'e',
'\", 's', ' ', 'w', 'o', 'r','t', 'h', '.', '\0’
};
不过太麻烦了,一般直接用字符串字面量来初始化字符数组。
在指定数组大小时, 要确保数组的元素个数至少比字符串长度多1( 为了容纳空字符) 。 所有未被使用的元素都被自动初始化为0( 这里的0指的是char形式的空字符, 不是数字字符0)
通常, 让编译器确定数组的大小很方便。省略数组初始化声明中的大小, 编译器会自动计算
数组的大小。
例如:
char m2[ ] = "If you can't think of anything, fake it.";
字符数组名和其他数组名一样, 是该数组首元素的地址。
还可以使用指针表示法创建字符串。
const char * pt1 = "Something is pointing at me.";
该声明和下面的声明几乎相同:
const char ar1[] = "Something is pointing at me.";
但其实又有所区别,且听我慢慢道来。
🍛3.字符数组和指针
数组形式和指针形式有何不同?
const char ar1[] = "Something is pointing at me.";
char* pt1 = "Something is pointing at me.";
数组形式( ar1[ ]) 在计算机的内存中分配为一个内含29个元素的数组( 每个元素对应一个字符,还加上一个末尾的空字符'\0'),每个元素被初始化为字符串字面量对应的字符。
通常,字符串都作为可执行文件的一部分储存在数据段中。 当把程序载入内存时,也载入了程序中的字符串。 字符串储存在静态存储区中。
但是, 程序在开始运行时才会为该数组分配内存。 此时,才将字符串拷贝到数组中。 注意, 此时字符串实际上有两个副本。 一个是在静态内存中的字符串字面量, 另一个是储存在ar1数组中的字符串。
指针形式( *pt1)也使得编译器为字符串在静态存储区预留29个元素的空间。 另外, 一旦开始执行程序,它会为指针变量pt1留出一个储存位置, 并把字符串的地址储存在指针变量中。
字符串字面量被视为const数据,也就是不可被修改。 由于pt1指向这个const数据, 所以应该把pt1声明为指向const数据的指针,不然有可能会出问题。何出此言?
你想啊,指针既灵活又危险,当pt1指向该字符串字面量时,谁也说不准会不会出现*pt1这样的代码,一旦解引用试图修改该字符串就会出问题——C标准未定义该行为,也就是说不同编译器反应不同,有可能导致内存访问错误等等。原来字符串字面量就是被限制了不能被修改的,使用指针指向它就让使用者获得了修改的权限,尽管是不应该被修改的。为了限制指针的权限,使用const修饰指针让它不能修改字符串字面量就能避免一系列潜在的危险和错误。
总之, 初始化数组时实际上是把静态存储区的字符串拷贝到数组中, 而初始化指针只把字符串的地址拷贝给指针。
示例:
#define MSG "I'm special"
#include<stdio.h>
int main()
{
char ar[] = MSG;
const char *pt = MSG;
printf("address of \"I'm special\": %p \n", "I'm special");
printf(" address ar: %p\n", ar);
printf(" address pt: %p\n", pt);
printf(" address of MSG: %p\n", MSG);
printf("address of \"I'm special\": %p \n", "I'm special");
return 0;
}
说明什么?
第一,pt和MSG的地址相同,而ar的地址不同,这与我们前面讨论的内容一致。
第二,虽然字符串字面量"I'm special"在程序的两个 printf()函数中出现了两次,但是编译器只使用了一个存储位置,而且与MSG的地址相同。编译器可以把多次使用的相同字面量储存在一处或多处,只不过最初是存储在静态区的,后面有可能被别的字符数组拷贝。
第三,静态数据使用的内存与ar使用的栈区内存不同。
在C语言中,很多库函数只需要用之前查查手册或官网即可,而有一部分函数则需要熟练掌握用法和函数定义的思路 。
需要注意的一些事项:
1.传入函数的指针如果不期望用来修改指向对象的话要用const修饰一下。
2.默认指针在定义时已经初始化为NULL,当传入函数时用assert函数检测其有效性。
关于assert函数:
🍛1.长度不受限的字符串函数
所在头文件: <string.h>
顾名思义,这一类函数对字符串进行操作的时候总是以'\0'为基准进行的,长度往往不受限制,容易造成越界。
🍛1.1 strlen函数
🍛相关描述
函数原型:size_t strlen( const char *string );
函数功能:计算字符串长度,直到遇到空字符结束,但不包括空结束字符。
函数说明:
strlen函数返回的是在字符串中 '\0' 前面出现的字符个数(不包含 '\0' )。
参数指向的字符串必须要以 '\0' 结束。
注意函数的返回值为size_t,是无符号的(易错)。
返回类型:size_t (strlen特有的非负整数类型)
看看这个,是不是觉得打印出<=?要这么想你就掉进坑里了。
strlen返回值是无符号整型,3-6你以为会是-3,实际上并不是,这里可不是有符号整型的计算,它们的补码相减运算后得到的数被认为是无符号数,这里会得到一个很大的正数,必然是>0的。
🍛模拟实现
🍛1.1.1 迭代计数法
size_t MyStrlen(const char* dest)
{
assert(dest != NULL);
size_t cnt = 0;
while (*dest != '\0')
{
count++;
dest++;
}
return cnt;
}
🍛1.1.2 递归计数法
size_t MyStrlen(const char* dest)
{
assert(dest != NULL);
if (*dest)
return 1 + MyStrlen(dest + 1);
else
return 0;
}
🍛1.1.3 指针相减法
size_t MyStrlen(const char* dest)
{
assert(dest != NULL);
const char* tmp = dest;
while (*(++dest))//一直后移直到遇见空字符
{
;
}
return dest - tmp;
}
🍛1.2 strcpy函数
🍛相关描述
函数原型:char *strcpy( char *strDestination, const char *strSource );
函数功能:把一个字符串(后面的源字符串)拷贝到另一个字符串(前面的目标字符串)中
函数说明:源字符串必须以 '\0' 结束。
拷贝时会将源字符串中的 '\0' 拷贝到目标空间。
目标空间必须足够大,以确保能存放源字符串。 目标空间必须可变,也就是说不能是字符串字面量。
返回类型:char*
🍛模拟实现
char* MyStrcpy(char* dest, const char* src)
{
assert(dest != NULL);
assert(src != NULL);
char* ret = dest;
while (*dest++ = *src++)//包括了把空字符也拷过去
{
;
}
return ret;
}
🍛1.3 strcmp函数
🍛相关描述
函数原型:int strcmp( const char *string1, const char *string2 );
函数功能:比较两字符串,实际上是通过字符比较实现的
返回类型:int
字符比较(character comparison)是指按照字典次序对单个字符或字符串进行比较大小的操作,一般都是以ASCII码值的大小作为字符比较的标准。
字符串比较的时候,字符串的大小是从最左边第一个字符开始比较,大者为大,小者为小,若相等,则继续比较后面的字符;
比如ABC与ACDE比较,第一个字符相同,继续比较第二个字符,由于第二个字符是后面一个串大,所以不再继续比较,结果就是后面个串大。
再如ABC与ABC123比较,比较三个字符后第一个串结束,所以就是后面一个串大。
所以,长度不能直接决定大小,字符串的大小是由左边开始最前面的字符决定的。
注意:
当字符串有空格时,空格也参加比较。
由汉字组成的字符串可以参加比较。如”李红”<”王军”。它们的大小实际是由其拼音构成的字符串的大小来决定的,即:”LIHONG”<”WANGJUN”。
返回值对应不同的字符串关系
其中返回值为0说明两字符串内容完全相同。
🍛模拟实现
int MyStrcmp(const char* str1, const char* str2)
{
assert(str1 != NULL);
assert(str2 != NULL);
while(*str1 == *str2)
{
if(*str1 == '\0')
return 0;
str1++;
str2++;
}
return *str1 - *str2;
}
🍛1.4 strcat函数
🍛相关描述
函数原型:char *strcat( char *strDestination, const char *strSource );
函数功能:字符串追加,就是把一个字符串接到另一个字符串的尾巴上去。
strcat函数将字符串strSource附加到字符串strDestination后面,并使用空字符终止生成的字符串。strSource的初始字符覆盖strDestination的终止空字符。
函数说明:源字符串必须以 '\0' 结束。
目标空间必须有足够的大,能容纳下源字符串的内容。
目标空间必须可修改。
复制或追加字符串时不执行溢出检查。
字符串自己给自己追加,如何?
如果源字符串和目标字符串重叠,则strcat的行为未定义。
返回类型:char*
🍛模拟实现
char* MyStrcat(char* dest, const char* src)
{
assert(dest != NULL);
assert(src != NULL);
char* ret = dest;
while(*(++dest))
{
;
}
while(*dest++ = *src++)
{
;
}
return ret;
}
🍛1.5 strstr函数
🍛相关描述
函数原型:char *strstr( const char *string, const char *strCharSet );
函数功能:在字符串string中寻找第一次出现的子字符串strCharSet,如果找到返回对应地址,如果找不到返回空指针。
返回类型:char*
🍛模拟实现
🍛BF算法(暴力匹配)
前置思考:
两个指针分别指向总字符串和子字符串,将元素一一进行比对,不相同的话str后移,相同的话str和substr同时后移,直到substr遇到空字符,这时就说明找到了。
那有没有一种可能,就是说前几个字符都对上了,但是后面的对不上,也就是第一次匹配没匹配上,这时候该怎么处理呢?
那就不止设置一个循环。
实现思路:
用cur指针存放当前总字符串指向位置的地址,再用s1,s2两个指针分别对总字符串和子字符串中的字符进行比对,两指针未移动到字符串的末尾的前提下,若是字符相同则移动至下一个再比对,当s2遇到空字符时说明找到了子字符串,此时直接返回cur指针内容。
实现代码:
char* MyStrstr(const char* str, const char* substr)
{
//指针有效性检测
assert(str != NULL);
assert(substr != NULL);
const char* s1 = NULL;
const char* s2 = NULL;
const char* cur = str;
//子字符串为空时
if ('\0' == *substr)
{
return (char* )str;
}
while (*cur)
{
s1 = cur;
s2 = substr;
while (*s1 && *s2 && *s1 == *s2)
{
s1++;
s2++;
}
if ('\0' == *s2)
{
return (char*)cur;
}
cur++;
}
return NULL;
}
🍛2. 长度受限的字符串函数
还有一类长度受限的字符串函数,因其对操作的字符串长度由用户来规定,使用起来相对安全。本质上就是比长度不受限的函数要多了一个字符个数的参数num来限定操作的字符串长度,别的地方并无不同。
🍛2.1 strncpy函数
🍛相关描述
函数原型:char * strncpy ( char * destination, const char * source, size_t num );
函数功能:从字符串中复制字符
拷贝num个字符从源字符串到目标空间。 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。
如果源长于 num,则不会在目标末尾隐式追加空字符。因此,在这种情况下,不应将目标视为空终止的C字符串(读取它会使溢出)。
目标和源不得重叠。参数说明:
目标空间
指向要在其中复制内容的目标数组的指针。
源
要复制的 C 字符串。
数字
要从源复制的最大字符数。
size_t是无符号整数类型。返回类型:char*
🍛示例
当源字符串长度更大时:
当源字符串长度更小时:
🍛模拟实现
char* MyStrncpy(char* dest, const char* src, size_t num)
{
assert(dest && src);
char* ret = dest;
while (num && (*dest++ = *src++))
{
num--;
}
if (num > 0)
{
while (num--)
{
*dest++ = '\0';
}
}
return ret;
}
🍛2.2 strncat函数
🍛相关描述
函数原型:char * strncat ( char * destination, const char * source, size_t num );
函数功能:从字符串追加字符
将num个字符从源字符串追加到目标空间的末尾。
如果源字符串的长度小于num,则仅追加终止空字符之前的内容。无论源字符串长度大于还是小于num,追加完后会给末尾再加上一个'\0'。
参数说明:
目标空间
指向要在其中复制内容的目标数组的指针。
源
要追加的 C 字符串。
数字
要从源字符串追加的最大字符数。
size_t是无符号整数类型。返回类型:char*
🍛示例
源字符串较短时:
源字符串较长时:
🍛模拟实现
char* MyStrncat(char* dest, const char* src, size_t num)
{
assert(dest && src);
char* ret = dest;
while (*(++dest));
while (num && (*dest++ = *src++))
{
num--;
}
if (0 == num && '\0' != *src)
{
*dest = '\0';
}
return ret;
}
🍛2.3 strncmp函数
🍛相关描述
函数原型:int strncmp ( const char * str1, const char * str2, size_t num );
函数功能:比较到出现另个字符不一样或者一个字符串结束或者num个字符全部比较完。
比较的是指定数目的字符,也就是前num个字符才发生比较。
参数说明:
str1
str2
要相互比较的 C 字符串。
num
要从源字符串追加的最大字符数。
size_t是无符号整数类型。返回类型:int
🍛示例
🍛3. 其他字符串函数
🍛3.1 strtok函数
🍛相关描述
函数原型:char * strtok ( char * str, const char * sep );
参数说明:sep参数是个字符串,定义了用作分隔符的字符集合。
第一个参数str指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记。
函数功能:实际上是切割字符串,每次切一段。
strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注: strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)
strtok函数可以多次使用:
当strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
当strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。 如果字符串中不存在更多的标记,则返回 NULL 指针。
若有多个相邻切割符相当于一个。
返回类型:char*
🍛 示例
🍛3.2 strerror函数
🍛 相关描述
函数原型:char * strerror ( int errnum );
函数功能:C语言的库函数,在执行失败时,都会设置错误码。
C语言设置了一个全局范围的错误码存放变量——errno,所在头文件为<errno.h>
strerror函数返回错误码所对应的错误信息。
返回类型:char*
🍛示例
#include <stdio.h>
#include <string.h>
#include <errno.h>//必须包含的头文件
int main ()
{
FILE * pFile;
pFile = fopen ("unexist.ent","r");
if (pFile == NULL)
printf ("Error opening file unexist.ent: %s\n",strerror(errno));
//errno存放的是最新的错误码
return 0;
}
🍛3.3 atoi函数
所在头文件:<stdlib.>
函数原型:int atoi(const char *str);
函数功能:把字符串转换成整型数,比如说把"123"转换成123。
函数说明:
atoi()会扫描参数str字符串,跳过前面的空格字符串,直到遇上数字或正负号才开始做转换,而再遇到非数字或字符串‘\0’时才结束转换,并将结果返回,返回转换后的整型数。
如果字符串中的第一个非空格字符序列不是有效的整数(其他字符),或字符串全为空或只包含空格字符,则不会执行转换并返回零。
返回类型:int
🍛示例
#include<stdlib.h>
#include<stdio.h>
int main()
{
char str1[] = " 1234";
char str2[] = "abc123";
char str3[] = " ";
char str4[] = "\0";
char str5[] = " 123 abc";
int a = atoi(str1);
int b = atoi(str2);
int c = atoi(str3);
int d = atoi(str4);
int e = atoi(str5);
printf("%d %d %d %d %d\n", a, b, c, d, e);
return 0;
}
🍛模拟实现
注意事项:
需要判断:
1.空指针
2.空字符串
3.空格
4.+-
5.越界
6.非数字字符
7.返回值是否合法
#include<ctype.h>
#include<limits.h>
#include<assert.h>
enum status
{
INVALID,
VALID
};
enum status g_nSta = INVALID;//判断返回值是否合法
int MyAtoi(const char* str)
{
assert(str);//判断空指针
if (*str == '\0')//判断空串
return 0;
while (isspace(*str))//跳过空格
{
str++;
}
int flag = 1;
long long ret = 0;
if (*str == '+')//判断+-
{
flag = 1;
str++;
}
else if(*str == '-')
{
flag = -1;
str++;
}
while (*str)
{
if (isdigit(*str))//判断是不是数字字符
{
ret = ret * 10 + flag * (*str - '0');//数字字符转数字
if (ret > INT_MAX || ret < INT_MIN)//判断是否越界
{
return 0;
}
}
else
{
g_nSta = VALID;
return (int)ret;
}
str++;
}
g_nSta = VALID;
return (int)ret;
}
🍛4. 字符函数
所在头文件:<ctype.h>
🍛4.1 字符分类函数
判断目标字符是不是对应类型的字符,是的话返回真。
🍛4.2 字符转换函数
int tolower ( int c ); //把字符c变成大写字母字符
int toupper ( int c ); //把字符c变成小写字母字符
🍛4.3 示例
🍛5. 内存函数
所在头文件:<string.h>
🍛5.1 memcpy函数
🍛相关描述
函数原型:void * memcpy ( void * destination, const void * source, size_t num );
函数功能:内存拷贝(类同strcpy,而拷贝对象是内存)。
函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
这个函数在遇到 '\0' 的时候并不会停下来。 如果source和destination有任何的重叠,复制的结果都是未定义的。
void*泛型设计,使得各种数据类型通用。
注意:拷贝的应当是两块独立空间的内存,如果是在同一块内存空间上进行拷贝很有可能出错(交叉拷贝)。
参数说明:
目标空间
指向要在其中拷贝内容的目标空间的指针。
源
指向要拷贝的内存空间的指针。
数字
要从源复制的字节数。
size_t是无符号整数类型。返回类型:void*
🍛示例
#include <stdio.h> #include <string.h> struct { char name[40]; int age; } person, person_copy; int main () { char myname[] = "Pierre de Fermat"; memcpy ( person.name, myname, strlen(myname)+1 ); person.age = 46; memcpy ( &person_copy, &person, sizeof(person) ); printf ("person_copy: %s, %d \n", person_copy.name, person_copy.age ); return 0; }
🍛模拟实现
void* MyMemcpy(void* dest, const void* src, size_t num)
{
assert(dest && src);
void* ret = dest;
while(num--)
{
*(char*)dest = *(char*)src;
(*char)dest++;
(*char)src++;
}
return ret;
}
🍛5.2 memmove函数
🍛相关描述
函数原型:void * memmove ( void * destination, const void * source, size_t num );
函数功能:用来实现重叠内存之间的数据拷贝。
和memcpy类同,差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
如果源空间和目标空间出现重叠,就得使用memmove函数处理。
参数说明:
目标空间
指向要在其中复制内容的目标空间的指针
源
指向要复制的数据源的指针。
数字
要从源复制的字节数。
size_t是无符号整数类型。返回类型:void*
🍛示例
🍛模拟实现
在实现memcpy时,我们拷贝数据是从前向后拷贝的,这样的话无法适应所有情况。
根据情况的不同,有时候需要从前向后拷贝数据,有时候则需要从后向前拷贝数据。
当指针dest在指针src前面且两指针指向空间有所重叠时,需要从前向后拷贝数据:
当指针dest在指针src后面且两指针指向空间有所重叠时,需要从后向前拷贝数据:
当两个指针指向的空间没有重叠部分时,从前向后还是从后向前拷贝都没关系:
基于上述分析,为了利于实现,这里就认为:只要dest小于src就从前向后拷贝,大于就从后向前拷贝。
代码
void* MyMemmove(void* dest, const void* src, size_t num)
{
assert(dest && src);
void* ret = dest;
while(num--)
{
if(dest < src)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
else
{
*((char*)dest + num) = *((char*)src + num);
}
}
return ret;
}
🍛5.3 memcmp函数
🍛相关描述
函数原型:int memcmp ( const void * ptr1, const void * ptr2, size_t num );
函数功能:比较从ptr1和ptr2指针开始的num个字节的内容相不相等,类同strncmp。
返回值如下:
参数说明:
ptr1
指向内存块的指针。
ptr2
指向内存块的指针。
数字
要比较的字节数。
返回类型:int
🍛示例
🍛模拟实现
int MyMemcmp(const void* ptr1, const void* ptr2, size_t num) { assert(ptr1 && ptr2); if (0 == num) { return 0; } while (--num && (*((char*)ptr1)++ == *((char*)ptr2)++)); return *(char*)ptr1 - *(char*)ptr2; }
🍛5.4 memset函数
🍛相关描述
函数原型:void * memset ( void * ptr, int value, size_t num );
函数功能:填充内存块。
将ptr指针所指向的目标内存块的num个字节的内容全部用value填充。
返回值也是ptr的值。
参数说明:
ptr
指向要填充的内存块的指针。
value
要设置的值。该值作为 int 传递,但该函数使用此值的无符号 char 转换来填充内存块(因为一次填充的只有一个字节)。
num
要设置为该值的字节数。
size_t是无符号整数类型。
返回类型:void*
🍛注意
填充是以字节为单位的,也就是把value的值填充到每一个字节的内容去。
所以想要填充char数组以外的数组的话结果可能会出乎意料。
🍛常用用法示例
初始化数组为全0
char str[100]; memset(str,0,100);
清空结构体或结构体数组为全0
typedef struct Stu { char name[20]; int cno; }Stu; int main() { Stu stu1; memset(&stu1, 0, sizeof(stu1)); Stu stu2[10]; //结构体数组 memset(&stu2, 0, sizeof(stu2)); }
🍛敬请期待更好的作品吧~
感谢观看,你的支持就是对我最大的鼓励,阁下何不成人之美,点赞收藏关注走一波~