目录
1. 字符串操作函数
1.1. 求字符串长度
1.1.1. strlen
1.2. 长度不受限制的函数
1.2.1. strcpy
1.2.2. strcat
1.2.3. strcmp
1.3. 长度受到限制的字符串函数
1.3.1. strncpy
1.3.2. strncat
1.3.3. strncmp
1.4. 字符串查找函数
1.4.1. strstr
1.4.2. strtok
1.5. 错误信息报告函数
1.5.1. strerror
1.5.2. perror
1.6. 字符分类函数
2. 内存操作函数
2.1. memcpy
2.2. memmove
2.3. memcmp
2.4. memset
1. 字符串操作函数
1.1. 求字符串长度
1.1.1. strlen
//函数原型
size_t strlen( const char *string );
strlen这个函数就是求一个字符串的有效字符的个数。
1. strlen会以'\0'作为结束标志,其返回的是在字符串中'\0'之前出现的字符个数(且不包含'\0')
2. 参数指向的字符串必须要有'\0'作为结尾,如若没有,则结果不确定。
3. 其返回值类型为size_t
为了更好地理解strlen,接下来分别以三种方式模拟实现我们的strlen
// 通过一个计数器
size_t my_strlen1(const char* str)
{
size_t count = 0;
//如果当前字符不为'\0',就++计数器
while (*str++ && ++count);
return count;
}
// 通过递归思路
size_t my_strlen2(const char* str)
{
//如果当前字符不为'\0', 递归调用
if (*str)
return my_strlen2(str + 1) + 1;
// 如果当前字符为'\0',返回0
else
return 0;
}
// 通过指针 - 指针 等于 俩指针间的元素个数
size_t my_strlen3(const char* str)
{
// 起始地址
const char* start = str;
// 结束地址
const char* end = str;
// 这里之所以用前置++,是因为循环结束时,此时的end是指向'\0'的
while (*++end);
return end - start;
}
1.2. 长度不受限制的函数
1.2.1. strcpy
// 函数原型
char *strcpy( char *strDestination, const char *strSource );
strcpy函数就是将一个字符串的内容拷贝给另一个字符串。
但有几点需要注意:
1. 源字符串必须要有'\0';
2. 目标字符串必须有足够的空间
3. 拷贝时,会将源字符串的'\0'也拷贝给目标字符串
4. 其返回值是目标字符串的起始位置
注意:strcpy函数的返回类型的设置是为了实现链式访问。
为了更深入的理解,我们自然要去模拟实现一下我们的strcpy:
char* my_strcpy(char* str_destination, const char* str_source)
{
assert(str_destination && str_source);
// 保存目标字符串的起始位置
char* start = str_destination;
// 下面这种方式还可以简化:
/*while (*str_destination = *str_source)
{
++str_destination;
++str_source;
}*/
while (*str_destination++ = *str_source++);
//返回目标字符串的起始位置
return start;
}
1.2.2. strcat
// 函数原型
char *strcat( char *strDestination, const char *strSource );
strcat被称之为字符串追加函数,将源字符串的内容追加到目标字符串中。
同样要注意以下几点:
1. 源字符串必须要有'\0'2. 目标字符串需要有足够的空间3. 目标空间必须可修改4. 追加过程中,会覆盖掉目标字符串中首次出现的'\0',同时将源字符串中的'\0'追加到目标字符串中同样,为了更好地理解这个函数,我们需要自己模拟实现一下注意:我们下面实现的这个函数不支持自己给自己追加。
char* my_strcat(char* str_destination, const char* str_source)
{
assert(str_destination && str_source);
// 1. 保存目标字符串的起始地址
char* start = str_destination;
// 2. 先找目标字符串中的'\0'
while (*str_destination && str_destination++);
// 3. 从原目标字符串的'\0'开始,将源字符串的字符依次赋给目标字符串,包含'\0'
while (*str_destination++ = *str_source++);
// 4. 返回目标字符串的起始地址
return start;
}
1.2.3. strcmp
// 函数原型
int strcmp( const char *string1, const char *string2 );
strcmp函数的作用是比较其两个字符串,标准规定如下:
当string1 less than string2 时,其返回 < 0
当string1 greater than string2 时,其返回 > 0
当string1 identical to string2 时,其返回0
那么如何比较呢?
注意:strcmp函数不是比较两个字符串的长度,而是比较字符串中对应位置上的字符ASCII的大小,如果相同,就比较下一对儿,直到不同(不同就判断谁大谁小)或者都遇到'\0'(返回0)。
为了更好地理解这个函数,我们模拟实现一下:
int my_strcmp(const char* str1, const char* str2)
{
assert(str1 && str2);
// 如果当前这对字符相等,那么就继续
while (*str1 == *str2)
{
// 在继续之前,需要判断当前字符是否是相等
// 如果是'\0',说明两个字符串都结束了,返回0
if (*str1 == '\0')
{
return 0;
}
++str1;
++str2;
}
// 出了循环,则说明当前遇到不相等的字符了,那么此时就进行比较当前两个字符的ASCII大小
if (*str1 > *str2)
return 1;
else
return -1;
}
1.3. 长度受到限制的字符串函数
首先,之所以会有长度受限制的字符串函数,是因为上面的这些长度不受限制的函数可能在某些场景下会导致进程crash。例如:
void Test5(void)
{
char str1[5] = "haha";
const char* str2 = "cowsay hello";
strcpy(str1, str2); // 此时编译可以通过
}
但是我们发现,此时的进程就崩溃了。为什么呢?原因是因为strcpy这个函数再复制的时候会无脑去找'\0',不遇到'\0',strcpy不结束,但由于str1只能容纳5个有效字符,str2将自己的所有内容复制到str1中,就会导致非法访问,进而引发进程崩溃。
因此,人们为了解决这些问题,便有了长度受到限制的字符串函数。即你让我拷贝(追加、比较)几个字符,我就拷贝(追加、比较)几个字符。
1.3.1. strncpy
// 函数原型
char *strncpy( char *strDest, const char *strSource, size_t count );
strncpy函数就是将一个字符串的n个字符的内容拷贝给另一个字符串。
但有几点需要注意:
1. 源字符串必须要有'\0';
2. 目标字符串必须有足够的空间
3. 拷贝时,会将源字符串的n个字符拷贝给目标字符串
4. 其返回值是目标字符串的起始位置
5. 拷贝过程中,如果源字符串走到了'\0',并且此时n!=0,那么填充'\0',直到n等于0,循环结束
注意:strncpy函数的返回类型的设置是为了实现链式访问。
为了更深入的理解,我们自然要去模拟实现一下我们的strncpy:
char* my_strncpy(char* str_destination, const char* str_source,size_t count)
{
assert(str_destination && str_source);
// 保留目标字符串的起始地址
char* start = str_destination;
// 拷贝count个字符
while (count--)
{
// 如果源字符串走到了'\0',并且此时count != 0,那么填充'\0'
if (*str_source == '\0')
*str_destination++ = '\0';
else
*str_destination++ = *str_source++;
}
// 返回目标字符串的起始地址
return start;
}
1.3.2. strncat
// 函数原型:
char *strncat( char *strDest, const char *strSource, size_t count );
strncat被称之为字符串追加函数,将源字符串的n个字符内容追加到目标字符串中。
同样要注意以下几点:
1. 源字符串必须要有'\0'2. 目标字符串需要有足够的空间3. 目标空间必须可修改4. 追加过程中,会覆盖掉目标字符串中首次出现的'\0',同时追加源字符串的n个字符,并且最后会将'\0'也追加过去。如果此时的n大于源字符串的长度,那么strncat只会将源字符串的内容追加过去(包括'\0'),但不会继续填充,这点与strncpy稍有差异。同样,为了更好地理解这个函数,我们需要自己模拟实现一下注意:strncat可以支持自己给自己追加接下来,我们模拟实现一下我们的strncat
char* my_strncat(char* str_destination, const char* str_source,size_t count)
{
assert(str_destination && str_source);
// 保存目标字符串的起始地址
char* start = str_destination;
//先找目标字符串的'\0'
while (*str_destination && str_destination++);
// 进行追加
while (*str_source && count--)
{
*str_destination++ = *str_source++;
}
// 将目标字符串的末尾置为'\0'
*str_destination = '\0';
// 返回目标字符串的起始位置
return start;
}
void Test7(void)
{
char str1[25] = "fasgqw\0eeeeeee";
const char* str2 = "cow";
const char* str3 = "cowsay";
my_strncat(str1, str2, 5);
}
1.3.3. strncmp
strncmp函数的作用是比较其两个字符串,标准规定如下:
当string1 less than string2 时,其返回 < 0
当string1 greater than string2 时,其返回 > 0
当string1 identical to string2 时,其返回0
那么如何比较呢?
注意:与strcmp不同的是,strncmp会只比较n个字符,即只比较n次,其比较逻辑与strcmp差异不大。
接下来我们模拟实现一下:
int my_strncmp(const char* str1, const char* str2,size_t count)
{
assert(str1 && str2);
// 如果当前这对字符相等 且 count != 0,那么就继续
while (*str1 == *str2 && count--)
{
// 如果遇到了'\0'或者count == 0 那么返回0
if (str1 == '\0' || count == 0)
return 0;
++str1;
++str2;
}
// 出了循环,则说明当前遇到不相等的字符了且count != 0,那么此时就进行比较当前两个字符的ASCII大小
if (*str1 > *str2)
return 1;
else
return -1;
}
1.4. 字符串查找函数
1.4.1. strstr
//函数原型
char *strstr( const char *string, const char *strCharSet );
strstr函数是一个查找子串的C函数,其会在string这个字符串里查找是否有strCharSet这个子串。
返回值:如果查找到了合适子串,那么返回这个子串的起始地址。
如果查完string这个目标串,都没有找到,那么返回NULL;
接下来,我们分析一下如何实现这个strstr呢?
// 假设当前我要在str1中查找是否存在str2这个子串
const char* str1 = "abcccdef";
const char* str2 = "ccde";
思考,如果我们此时只借助一对指针变量作为辅助,可行吗?如下:
其比较逻辑比较简单,当前的一对字符不相等,str1++,相等同时++,如果当str2走到了'\0',那么说明找到了合适的子串,反之如果,当str1走到了'\0',那么说明没有这个子串
有了上面的分析,我们认为,单靠一对指针变量是无法完成任务的,我们可以让str1、str2保持原位置不懂,让它们始终指向首元素的位置,我们定义s1和s2、以及cur,s1和cur的初始位置是str1,s2的初始位置是str2,如果cur走到了'\0'那么说明没有合适的子串,如果找到了合适的子串,那么返回cur即可,处理逻辑如下:
char* my_strstr(const char* string, const char* str_ret)
{
assert(string && str_ret);
const char* s1 = string;
const char* s2 = str_ret;
char* cur = (char*)string;
while (*cur)
{
// s1从cur的位置开始
s1 = cur;
// 如果当前字符相等,就判断下一对字符
while (*s1 == *s2)
{
++s1;
++s2;
// 如果s2走到了'\0' 就说明当前cur所在的子串就是目标子串
if (*s2 == '\0')
return cur;
// 如果s1走到了'\0' 那么说明当前cur所在的位置没有目标子串,因此需要判断下一个位置的cur
if (*s1 == '\0')
break;
}
// 走到这里说明当前的cur是不匹配的,因此++cur
// 且str2要回到原点
++cur;
s2 = str_ret;
}
// 如果 cur走到了 '\0' 说明找不到这个子串,返回NULL
return NULL;
}
1.4.2. strtok
//函数原型:
char *strtok( char *str, const char *sep );
sep 参数是个字符串,定义了用作分隔符的字符集合第一个参数指定一个字符串,它包含了 0 个或者多个由 sep 字符串中一个或者多个分隔符分割的标记。strtok 函数找到 str 中的下一个标记,并将其用 ' \0' 结尾,返回值:返回被分割子串的起始位置。(注: strtok 函数会改变被操作的字符串,所以在使用strtok 函数切分的字符串一般都是临时拷贝的内容并且可修改。)strtok 函数的第一个参数不为 NULL ,函数将找到 str 中第一个标记, strtok 函数将保存它在字符串中的位置。strtok 函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。如果字符串中不存在更多的标记,则返回 NULL 指针。具体使用如下:
void Test10(void)
{
const char* str = "forever forward@never give up.";
char arr[40] = { 0 };
const char* sep = "@.#";
strcpy(arr, str);
char* ret = strtok(arr, sep);
/*
* forever forward@never give up. 被切割为: 将 @转化为 \0
* forever forward\0never give up.
* 此时的ret的值就是forever forward\0的起始地址
*/
printf("%s\n", ret); // forever forward
ret = strtok(NULL, sep);
/*
* 当第一个参数为NULL时,那么strtok函数,会继续切割上一个字符串
* 此时相当于会从上次的分隔符的下一个位置开始
* 具体为上面的 never give up. ,即从这个字符串的起始位置开始
* 当遇到 . 这个分隔符
* never give up. 会被分割: 将. 转化为 \0
* never give up\0
* 此时ret的值就是 never give up\0的起始地址
*/
printf("%s\n", ret); // never give up
}
有时候分隔符很多,被分割的字符串也很多,上面的写法就显得太粗糙了,因此,我们可以利用循环解决这个问题:
void Test11(void)
{
const char* str = "forever forward@never give up.";
char arr[40] = { 0 };
const char* sep = "@.#";
strcpy(arr, str);
for (char* ret = strtok(arr, sep); ret != NULL; ret = strtok(NULL, sep))
{
printf("%s\n", ret);
}
}
相信很多人在看到strtok的操作时,感觉很迷惑,为什么当传递NULL时,它会继续在原有基础之上切割一个字符串呢?其实我们可以用一个全局的静态变量,简单实现下我们的strtok:
// 判断当前字符是否是分隔符
bool is_exist_sep(char ch, const char* sep)
{
while (*sep)
{
if (ch == *sep)
return true;
++sep;
}
return false;
}
char* my_strtok(char* str, const char* sep)
{
// 用于保存分隔符的下一个位置
static char* _strtok = NULL;
// start起始位置
char* start = str;
// 如果传递过来的第一个参数是NULL
// 那么说明以前被分割过,因此取上次被置为'\0'的下一个位置
// 也就是我们定义的那个局部静态变量
if (start == NULL)
start = _strtok;
// 用于当前被分割子串的起始位置
char* ret = start;
while (!is_exist_sep(*start, sep))
{
++start;
if (*start == '\0')
return NULL;
}
// 走到这里说明于到了分隔符
// 因此将分隔符置为'\0',同时将下一个位置保存到这个局部静态变量中
*start = '\0';
_strtok = ++start;
// 返回被分割的子串的起始位置
return ret;
}
1.5. 错误信息报告函数
1.5.1. strerror
// 函数原型
// 头文件包含 #include <errno.h> && #include <string.h>
char *strerror( int errnum );
上面这个函数没啥说的,就是当进程出现错误的时候,这个函数会根据对应的错误码打印对应错误信息。
strerror的使用:
void Test13(void)
{
int *ptr = (int*)malloc(1024u * 1024u * 1024u * 2u);
if (!ptr)
printf("%s\n", strerror(errno)); // errno 是一个C标准定义的全局变量
else
printf("hehe");
}
1.5.2. perror
// 函数原型
// 头文件: #include <stdio.h>
void perror( const char *string );
perror也是一个打印进程错误信息的函数,只不过较strerror相比,它的使用更为简便。
void Test14(void)
{
int *ptr = (int*)malloc(1024u * 1024u * 1024u * 2u);
if (!ptr)
perror("malloc"); // 这里面的信息需要我们显示传递,但没有限制
else
printf("hehe");
}
1.6. 字符分类函数
函数(返回值类型都为bool) | 如果它的参数符合下列条件就为真 |
---|---|
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
| 任何图形字符 |
isprint | 任何可打印字符,包括图形字符和空白字符 |
2. 内存操作函数
2.1. memcpy
// 函数原型
void *memcpy( void *dest, const void *src, size_t count );
- 函数memcpy从src的起始位置开始向后拷贝count个字节的数据到dest的内存位置。
- 这个函数在遇到 '\0' 的时候并不会停下来,因为它是针对内存级别的拷贝
- 如果src和dest有任何的重叠,拷贝的结果都是未定义的
那么如何模拟实现我们的memcpy函数呢?
// 这里之所以用void*,是因为void*可以接受任意类型的指针变量
void* my_memcpy(void* dest, void* src, size_t count)
{
assert(src && dest);
// 保存目标位置的起始位置
void* start = dest;
// 按字节处理
while (count--)
{
// 由于dest和src的类型都是void*,不支持解引用,因此在这里要强转
// 并且要求按一个一个字节处理,因此强转为char*
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
// 返回目标位置的起始位置
return start;
}
注意:我们所实现的memcpy不支持src和dest的重叠拷贝,什么意思呢?如下:
为什么呢?因为当我们的memcpy的拷贝逻辑是从前往后拷贝的。当把1拷贝给4时,此时这个4就会被覆盖成1,当下次要用4的时候,此时已经是1了,这也就是为什么我们看到的结果是这样。于是为了解决重叠拷贝的问题,人们设计除了memmove,它是专门用来处理重叠拷贝的内存函数。
2.2. memmove
// 函数原型
void *memmove( void *dest, const void *src, size_t count );
- 和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
- 如果源空间和目标空间出现重叠,就得使用memmove函数处理。
那么如何设计我们的memmove函数呢?当发生重叠拷贝时,如何解决呢?有人说我们可以更改一下拷贝的顺序,但是如果是下面的场景呢?
void Test16(void)
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
my_memove(arr + 3, arr, 20);
my_memove(arr, arr + 3, 20);
}
你会发现,如果我们将拷贝顺序设置为绝对的,那么此时上面两种情况比有一种出错。因此为了控制合理顺序我们需要分析一下src和dest的位置关系:
如果dest和src的内容没有交叉情况,不管你是从前往后还是从后往前拷贝,都无所谓。
因此,我们总结如下:
当src < dest时,我们以从后往前的方式将src的内容拷贝给dest
当src > dest时,我们以从前往后的方式将src的内容拷贝给dest
有了这样的理解,我们的代码如下:
void* my_memmove(void* dest, void* src, size_t count)
{
assert(dest && src);
//1. 保存目标位置的起始位置
void* start = dest;
//2. 拷贝逻辑:
/*
* 如果src < dest,我们选择从后往前拷贝
* 假设以我们刚刚画的图为准
* [01 00 00 00 02 00 00 00 03 00 00 00 {04 00 00 00 05 00 00 00] 06 00 00 00 07 00 00 00 08 00 00 00} ...
* 即把5的最后一个字节赋值给8的最后一个字节,--count,循环继续
*/
if (src < dest)
{
while (count--)
{
//借助count,从后往前拷贝
*((char*)dest + count) = *((char*)src + count);
}
}
else
{
while (count--)
{
// 从前往后拷贝的逻辑,与memcpy的逻辑一致
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
//3. 返回目标地址的起始位置
return start;
}
2.3. memcmp
// 函数原型
int memcmp( const void *buf1, const void *buf2, size_t count );
比较从buf1 和buf2 指针开始的 count 个字节返回值如下:
如很模拟实现我们的memcmp呢?
int my_memcmp(const void* buf1, const void* buf2, size_t count)
{
assert(buf1 && buf2);
// 最多比较count个字节
// 如果当前的一对字节内容一致,继续比较下一对字节
while (count-- && *(char*)buf1 == *(char*)buf2)
{
buf1 = (char*)buf1 + 1;
buf2 = (char*)buf2 + 1;
// 如果count == 0,那么说明比较的这些字节全部对应相等,返回0
if (!count)
return 0;
}
// 走到这里说明,count !=0 ,且buf1和buf2所指的这个字节的内容不一样
if (*(char*)buf1 > *(char*)buf2)
return 1;
else
return -1;
}
2.4. memset
// 函数原型
void *memset( void *dest, int ch, size_t count );
对memset的简单理解:
dest:目标空间的起始位置
ch:你要对每个字节设置的值
count:你要设置多少个字节
对于memset我们也简单模拟实现一下:
void* my_memset(void* dest, int ch, size_t count)
{
assert(dest);
// 保存目标空间的起始位置
void* start = dest;
// 将count个字节的值设置为ch
while (count--)
{
*(char*)dest = ch;
dest = (char*)dest + 1;
}
// 返回目标空间的起始位置
return start;
}
void Test18(void)
{
int arr[3] = { 0x11223344, 0x22222222, 0x33333333 };
my_memset(arr, 0, 12);
}
memset是以字节为单位设置内存单元的。