函数介绍
C语言中对字符和字符串的处理是很频繁,但是C语言本身是没有字符串类型的,字符串通常仿造常量字符串或者字符数组中,字符串常量适用于那些对它不做修饰的字符串函数
重点介绍处理字符和字符串的库函数的使用和注意事项
- 求字符串长度
strlen
- 长度不受限制的字符串函数
strcpy
strcat
strcmp
- 长度首限制的字符串函数介绍
strncpy
strncat
strncmp
- 字符串查找
strstr
strok
- 错误信息报告
strerror
字符操作
- 内存操作函数
memcpy
memmove
memset
memcmp
1.1 strlen
size_t strlen (const char* str);
- 字符串以' \0 '作为结束标志,strlen函数返回的是再字符串中 ' \0 '前面出现的字符个数(不包含' \0 ')
- 参数指向的字符串必须以' \0 '结束
- 注意函数的返回值为size_t,是无符号的
- 学会strlen函数的模拟实现
求字符串的长度,统计的是字符串中\0之前出现的字符的个数
strlen函数的头文件是#include<string.h>
但是像下面这种就是不正确的
数组中没有\0,而strlen是计算\0之前的字符个数,所以打印的结果是随机值
这里并非我们所想的那样,打印出<,是因为我们忽略了strlen的返回类型是size_t类型,3-6=-3,无符号数减去无符号数得到的仍然是无符号数,-3被当作无符号数相当于无穷大的数,所以是>
strlen函数的模拟实现:
#include<assert.h>
int my_strlen(const char* str)
{
assert(str);
const char* start = str;//记录起始位置
const char* end = str;
while (*end != '\0')
{
end++;
}
return end - start;
}
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
1.2 strcpy
char* strcpy(char* destination,const char* source)
- Copies the C string pointed by source into the array pointed by destination,including the terminating null character(and stopping at that point.)
- 源字符串必须以' \0 '结束
- 会将源字符串中的' \0 '拷贝到目标保存空间
- 目标空间必须足够大,以确保能存放源字符串
- 目标空间必须可变
- 学会模拟实现
strcpy是将从源头空间中\0之前的字符拷到目标空间处,如果 源头空间没有\0将不会拷贝成功
看下面这个例子中将arr中的\0拷贝到目标空间中
但是下面就是刚才所说的,源头空间中没有\0的情况,如果没有\0将会拷贝失败
所以说strcpy是拷贝\0之前的字符
注意:不能将常量字符串修改,也就是说,不能将一个常量字符串的内容拷贝到另一个常量字符串,我们必须保证目标空间的内容必须是可修改的
strcpy函数的模拟实现:
char* my_strcpy(char* dest, const char* scr)
{
assert(dest && scr);
char* ret = dest;
while (*dest++ = *scr++)
{
;
}
return ret;
}
int main()
{
char arr[10] = { 0 };
const char* p = "abcdef";
my_strcpy(arr, p);
printf("%s\n", arr);
}
1.3 strcat
char* strcat (char* destination ,const char* source);
- Appends a copy of the source string to the destination string.The terminating null character in destination is overwritten by th first character of source,and a null-character is included at the end of the new string formed by the soncatenation of both destination.
- 源字符串必须以' \0 '结束
- 目标空间必须足够的大,能容纳源字符串的内容
- 目标空间必须可修改
strcat是从一个字符串的末尾开始追加,,所以是遇到第一个\0之后就开始追加,比如从下面的例子中就能很好的看出来
strcat函数的模拟实现:
char* my_strcat(char* dest, const char* src)
{
assert(dest && src);
//1.找到目标空间中\0
char* cur = dest;
while (*cur != '\0')
{
cur++;
}
//2.拷贝源头数据到\0之后的空间
while (*cur++ = *src++)
{
;
}
return dest;
}
int main()
{
char arr1[20] = "hello ";
char arr2[] = "world";
my_strcat(arr1, arr2);
printf("%s\n", arr1);
}
注意:这里有几个库函数是返回目标空间的起始地址,所以在最开始先保存一下目标空间的起始地址
这里while循环也可以写成while(*cur++=*src++);的形式,但是这样不太便于阅读,上面代码中while循环可读性更好一些
但是这个库函数不能自己给自己追加
如果自己给自己追加的话,就会将自己的\0覆盖掉,自己就没有\0了,而 strcat永远找不到\0就不能结束,也就成为死循环
1.4 strcmp
int strcmp(const char* str1,const char* str2);
- This function starts comparing the first character of each string.If they are equal to each other,it continues with the following pairs until the characters differ or until a terminating null-character is reached.
- 标准规定:
1.第一个字符串大于第二个字符串,则返回大于0的数字
2.第一个字符串等于第二个字符串,则返回0
3.第一个字符串小于第二个字符串,则返回小于0的数字
strcmp比较的是两个字符串中相同位置中的ASCII码值的比较,在从前比较的过程中,哪个对应位置上的ASCII码值比较大,哪个就大,注意这里比较的不是长度
strcmp模拟实现:
int my_strcmp(const char* str1, const char* str2)
{
assert(str1 && str2);
while (*str1 == *str2)
{
if (*str1 == '\0')
{
return 0;//str1和str2相等
}
str1++;
str2++;
}
return *str1 - *str2;
}
int main()
{
char arr1[] = "abcdef";
char arr2[] = "abp";
int ret = my_strcmp(arr1,arr2);
if (ret < 0)
printf("arr1<arr2\n");
else if (ret > 0)
printf("arr1>arr2\n");
else
printf("arr1==arr2\n");
return 0;
}
小结:
strcpy、strcat、strcmp都是与\0有关,所以它们称为长度不受限制的字符串函数
1.5 strncpy
拷贝num个字符从源字符串拷贝到目标空间
如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后面追加0,直到num个
1.6 strncat
xy后面的\0是strncat函数主动追加的
上面的strcat函数不能自己给自己追加,但是我们可以利用strncat给自己给自己追加
之前的strcat函数在追加的时候不会在后面添加\0,会导致死循环(前面已经写死循环的原因),但是strncat会再后面主动追加\0,这样就有机会停下来,构成一个完整的字符串
1.7strncmp
1.8strstr
在一个字符串中找另一个字符串是否存在(找str2在str1中是否存在),如果存在返回子串第一次出现的起始位置,如果不存在则返回空指针(NULL)
注意:这里返回的起始位置的地址,并没有改变字符串的内容
strstr函数的模拟实现
char* my_strstr(const char* str1, const char* str2)
{
const char* s1 = str1;
const char* s2 = str2;
const char* p = str1;
while (*p)
{
s1 = p;
s2 = str2;
while (*s1 != '\0' && *s2 != '\0' &&( *s1 == *s2))
{
s1++;
s2++;
}
if (*s2 == '\0')//此时遍历了要找的字符串,也就是这个字符串的每个字符都找到了
{
return(char*) p;
//p的类型是const char*,而这个函数的返回类型是char*,所以要强制类型转换
}
p++;
}
return NULL;
}
int main()
{
char arr1[] = "abcabcdefdef";
char arr2[] = "efd";
char* p = my_strstr(arr1, arr2);
printf("%s\n", p);
return 0;
}
1.9 strtok函数
char * strtok ( char * str , const char* sep );
- sep参数是个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记
- strtok函数找到str中的下一个标记,并将其用\0结尾,返回一个指向这个标记的指针(注:strtok函数会改变被操作的字符串,所以在使用strtok函数且分的字符串一般都是临时拷贝的内容并且可修改)-->就比如下面的例子中,@作为分隔符,会将其改为\0,并且返回首元素的地址( l 的地址),因为会改变原字符串的内容,所以将源字符串的内容拷贝到另外一个数组中,然后strtok函数调用这个数组,接着,在取找分隔符,将其改为\0然后返回q的地址
- strtok函数的第一个参数不为NULL,函数将找到str中第一个标记,strtok函数将保存它的字符串中的位置
- strtok函数的第一个参数为NULL,函数将在同一个字符串中被保存的位置(上一次被保存的位置)开始,查找下一个标记
- strtok函数在一个字符串中第一次调用不为空指针,再下一次调用的时候为空指针
- 如果字符串中不存在更多的标记,则返回NULL指针
但是我们一般不这么使用,像下面这种才是strtok函数的正确使用方式
1.10 strerror
char * strerror( int errnum) ;
返回错误码,所对应的错误信息(把错误码转换成错误信息)
这里我们只关注strerror函数的使用
strerror(errno)错误码记录到错误码的变量中,errno时C语言提供的全局的错误变量,并且在使用errno的同时要引头文件#include<errno.h>
由于我没有创建test.txt文件,所以会报错
此时我在此路径下创建了test.txt文件,所以这时不会报错
还有另外一个库函数和strerror库函数类似:perror函数
这个库函数perror=printf+strerror
字符分类函数:
使用要引用对应的头文件#include<ctype.h>
函数 | 如果它的参数符合下列条件就返回真(返回值为非0) |
iscntrl | 任何控制字符 |
isspace | 空白字符:空格' ' ,换页 '\f',换行 '\n',回车 '\r',制表符 '\t'或者垂直制表符 '\v' |
isdigit | 十进制数字0-9 |
isxdigit | 十六进制数字,包括所有十进制数字,小写字母a~f,大写字母A~F |
islower | 小写字母a~z |
isupper | 大写字母A~Z |
isalpha | 字母a~z或者A~Z |
isalnum | 字母或者数字,a~z,A~Z,0-9 |
ispunct | 标点符号,任何不属于数字或者字母的图形字符(可打印) |
isgraph | 任何图形字符 |
字符转换函数:(对应头文件也是#include<ctype.h>)
int tolower( int c );//转换成小写
int toupper( int c );//转换成大写
如果是小写,我们转换成大写,但是它本身就是大写,我们是不会转换的
1.11memcpy
void* memcpy ( void * destination , const void * sourcce , size_t num ) ;
- 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置
- 这个函数在遇到' \0 '的时候并不会停下来
- 如果source和destination有任何的重叠,复制的结果都是未定义的
一组内存函数:
- memcpy
- memmove
- memcmp
- memset
memcpy是什么类型的数据都能拷贝,而strncpy只能拷贝char类型的
参数为void*的时候,可以传进来任何类型的地址,所以使用这函数的时候是不挑类型的
模拟实现memcpy函数:
注意void*类型指针不能直接解引用,因为它是void*类型,解引用的时候不知道访问几个字节,同理也不能直接进行++和--操作
但是我们发现这里强制类型转换也是错误的,因为这里的强制类型转换是临时,这里++是对于当前的dest和src的,对当前左边的有用,但是对右边没用
有些人可能会这么写((char*)dest++); ((char*)src)++;但是这样的写法,在有些编译器能跑过去,在有一些编译是不能跑过去的,这里并不是一个好的方法,所以还需要换另一种方法,
#include<assert.h>
#include<stdio.h>
void* my_memcpy(void* dest,const void* src, size_t num)
{
assert(dest && src);
char* ret = dest;
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
//这里dest和src为void*类型能够接受任意类型的值
}
return (void*)dest;
}
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[10] = { 0 };
my_memcpy(arr2, arr1, 20);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr2[i]);
}
}
因为num的单位是字节,所以一个一个字节拷贝比较合适
重叠拷贝:
当我们想要将2 3 4拷贝到4 5 6时,4和5拷贝成功,但是4已经被拷贝成2,此时6的位置会被拷贝成2,此时我们就需要从后向前拷贝,但是并不是任何时候都要从后向前拷贝
此时是从后向前拷贝,但是这时3不能被拷贝成5,而是被拷贝成7,所以这次需要从前往后拷贝
因此重叠拷贝时,有时候需要从前向后拷贝,有时候需要从后前拷贝,但是我们memcpy函数并不能完成重叠时的拷贝,这时我们就引入了一个库函数:memmove函数
1.12 memmove
void * memove ( void* destination , const void* source , size_t )
- 和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的
- 如果源空间和目标空间出现重叠,就得使用memmove函数处理
当dest在src的前面的时候,从前往后拷贝;当dest在src的后面的时候,从后向前拷贝
注意:这里是将src的内容从前到后还是从后到前拷贝到dest中,不要混淆了
3区域是不重叠的区域,如果不重叠从后向前和从前向后都无所谓,但是2区域重叠时从后向前拷贝,此时,这个3区域从后向前拷贝更加的方便
#include<stdio.h>
#include<assert.h>
void* my_mommove(void* dest, void* src, size_t num)
{
void* ret = dest;
assert(dest && src);
if (dest < src)//从前向后拷贝
{
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
else//从后向前拷贝
{
while (num--)
{
*((char*)dest + num) = *((char*)src + num);
}
}
return ret;
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
my_mommove(arr + 2, arr, 20);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
}
memcpy只需要实现不重叠的拷贝就可以了
memmove是需要实现重叠内存拷贝的
也可以说memmove的功能是包括memcpy的,在VS编译器上memcpy和memmove的实现是一样的,但是在有些编译器上,它们的实现是不同的
1.13 memcmp
int memcpy ( const void* ptr , void* ptr2 , size_t num ) ;
比较从ptr1和ptr2指针开始的num个字节
这个函数也是一对一对字节进行比较的
1.14 memset-->内存设置
将ptr前num个字节改为value
将前8个字节改为0