字符串和内存函数
字符串函数
字符串求长strlen
函数声明
size_t strlen ( const char* string );故strlen函数计算\0之前的字符个数。字符串必须以\0结尾。
//1.
char arr1[] = "abcd";
printf("%d\n", strlen(arr1)); // 传过去的是以\0结尾的字符串,满足strlen的要求
//2.
char arr2[] = { 'a','b','c','d' };
printf("%d\n", strlen(arr2));// 字符数组而非字符串,不以\0结尾
//3.
int a = 10;
printf("%d\n", strlen(a));// 10被当作地址看待,访问0x0A地址处的非法内存,结果如下图
strlen函数返回类型为size_t,无符号数接收,以无符号的形式打印。
size_t ret = strlen("abcdef");
printf("%u\n", ret);无符号数可以直接比较但不要进行运算。
if (strlen("abc") - strlen("abcdef") < 0)
printf("hehe\n");
else
printf("haha\n");无符号数的运算结果被整形提升为无符号数,结果被视为无符号数是不会小于零的。
模拟实现
//1.计数器
size_t my_strlen(const char* str) {
assert(str);
size_t count = 0;
while (*str) {
count++;
str++;
}
return count;
}
//2.指针相减
size_t my_strlen(const char* str) {
assert(str);
char* begin = str;
while (*str) {
str++;
}
return str - begin;
}
//3.递归
size_t my_strlen(const char* str) {
assert(str);
if (*str) {
return 1 + my_strlen(str + 1);
}
else {
return 0;
}
}字符串拷贝strcpy
函数声明
char* strcpy ( char* strDestination, const char* strSource );strcpy将源字符串的内容(包括\0)依次拷贝到目标空间。目标空间可修改且足够大,确保能够且足以存放源字符串。

若源字符串不以\0结尾,则strcpy会向后一直访问到非法内存。
//1.
char arr1[] = "xxx";//目标空间不够大
char arr2[] = "abcdef";
//.2
const char arr1[] = "xxxxxxxxxx";//目标空间不可修改
char arr2[] = "abcdef";
strcpy(arr1, arr2);
strcpy在vs2019环境下使用会报出不安全的警告,该函数在拷贝前不会去检查是否会发生数组越界,警告也是正常的。
模拟实现
char* my_strcpy(char* dest, const char* src) {
assert(dest && src);
char* begin = dest;
while (*dest++ = *src++) {
;
}
return begin;
}字符串追加strcat
函数声明
char* strcat ( char* strDestination, const char* strSource );strcat将源字符串包括\0,追加到目标字符串的结尾并覆盖掉原本的\0。目标空间必须足够大且可修改,确保能够且足以追加字符串。

模拟实现
char* my_strcat (char* dest, const char* src) {
assert(dest && src);
char* begin = dest;
//移到末尾
while (*dest) {
dest++;
}
//追加
while (*dest++ = *src++) {
;
}
return begin;
}下面三种while循环的特点:
while (*dest++) { ; } //1.
while (*dest) { dest++; } //2.
while (*dest++ = *src++) { ; } //3.- 第一种:指针解引用后就
++,第二种指针解引用后进入循环才能++。 - 第一种:解引用的次数和
++的次数是相等的,当指针指向\0时也要++访问下一个元素。而第二种为真后++,指向\0时便停留在\0处。 - 第三种:将赋值运算
*dest=*src放入while循环内,当src指向\0时也要赋值给dest,在这之后判断整个表达式为假,退出循环。
字符串比较strcmp
函数声明
int strcmp ( const char* string1, const char* string2 );strcmp遍历比较两个字符串对应位置的字符的ASCII码值是否相等。
- 函数的返回值
- 字符串1小于字符串2时,返回小于0的数字
- 字符串1等于字符串2时,返回0
- 字符串1大于字符串2时,返回大于0的数字
模拟实现
int my_strcmp(const char* str1, const char* str2) {
assert(str1 && str2);
while (*str1 == *str2) {
if (*str1 == '\0') {
return 0;
}
str1++;
str2++;
}
return *str1 - *str2;
}都是
\0就返回0,不是就都++,直到不等时返回二者之差。
长度不受限制的字符串函数的通病是不安全,这些函数不做溢出检查直到程序出错才被迫停止。所以C语言还内置了相对安全的长度受限制的字符串函数,以及更加通用的内存函数。
字符串拷贝strncpy
函数声明
char* strncpy ( char* strDest, const char* strSource, size_t count );strcpy函数从源字符串拷贝count个字符到目标字符串,若count大于源字符串长度则以\0填充。
目标字符串可修改且足够大,若count小于源字符串个数则不追加\0,若大于源字符串个数则以\0填充到count个字符。
模拟实现
char* my_strncpy(char* dest, const char* src, size_t count) {
char* begin = dest;
assert(dest && src);
while (count && (*dest = *src)) {
dest++, src++;
count--;
}
while (count--) {
*dest++ ='\0';
}
return begin;
}
int main()
{
char arr1[10] = "abcdef";
char arr2[] = "xxx";
my_strncpy(arr1, arr2, 5);
printf("%s\n", arr1);
return 0;
}下列三种是进行字符串拷贝时的代码。当count小于源字符串个数时三者都没问题,但当count大于源字符串个数时,三者的情况却是不一样的。
//1.
while (count && *src) {
*dest++ = *src++;
count--;
}
//2.
while (count && (*dest++ = *src++)) {
count--;
}
//3.
while (count-- && (*dest++ = *src++)) {
;
}- 当指向源字符串的指针遇到
\0时,不进行赋值和++也不进行count--操作。
赋值赋了3次,count也减了3次。此时count为2,随后追加了2个\0。

- 指针指向
\0时执行赋值操作后,赋值表达式为假,结束循环。
由于赋值操作放在循环的判断部分,故赋值执行了4次,而count减了3次。

即源字符串末尾\0也被拷贝了过去,但count仍为2,所以最后还要追加2个\0。但这样就改变了6个字符,显然是错误的。
- 将第二种代码稍作修改,将
count--的操作也放入判断部分,这样就避免了上述问题。

- 如果循环条件只有循环变量
count大小的判断,则进步条件放在判断部分还是循环体内没有影响。 - 但如果还有赋值或是其他语句,那么该语句相对于将其放在循环体内部的情况多被执行一次。
- 由于
count是无符号数当条件为假退出后,随即--就会变成很大的数字。后续再使用该循环变量的时候就会出错。
字符串追加strncat
函数声明
char* strncat ( char* strDest, const char* strSource, size_t count );在目标字符串末尾追加count个源字符串的字符,结尾默认添加\0。
追加到目标字符串末尾默认补'\0',count超出源字符串个数不在追加。

模拟实现
char* my_strncat(char* dest, const char* src, size_t count) {
assert(dest && src);
char* begin = dest;
//1. 来到目标字符串末尾
while (*dest) {
dest++;
}
//2. 追加
while (count && (*dest++ = *src++)) {
count--;
}
//3. count小于字符个数补0
if (count == 0) {
*dest = '\0';
}
return begin;
}字符串比较stnrcmp
函数声明
int strncmp ( const char* string1, const char* string2, size_t count );比较两个字符串的前count个字符,并返回相关的数值。
模拟实现
int my_strncmp(const char* str1, const char* str2, size_t count) {
assert(str1 && str2);
while (count-- && (*str1 == *str2)) {
str1++;
str2++;
}
return *str1 - *str2;
}字符串查找strstr
函数声明
char* strstr ( const char* string, const char* strCharSet );查找子字符串在目标字符串中首次出现的位置,有则返回之起始位置,无则返回空指针。
模拟实现
char* my_strstr(const char* str, const char* set) {
assert(str && set);
char* s1 = str;
char* s2 = set;
while (*s1) {
//归位
str = s1;
set = s2;
//防止s1=s1=\0
while ((*str && *set) && (*str == *set)) {
str++;
set++;
}
//判断
if (*set == '\0') {
return s1;
}
//进位
s1++;
}
return NULL;
}
s1和s2存放str和set每次归位时的位置,以便匹配错误时返回重新开始。
- 特殊情况
set="\0"时,\0是任意字符串的子字符串,即无任何字符的字符串是任意字符串的子字符串。所以返回母字符串 - 一次匹配错误时,str和set要归位初始位置,即
str=++s1;set=s2;要放在判断部分的后面。(set=s2下次循环再执行就相当于在后面)

KMP算法专门针对字符串匹配和查找这种功能。
字符串分割strtok
函数声明
char* strtok ( char* strToken, const char* strDelimit );strtok函数通过以\0替换分隔符的方式修改需分割的字符串,并返回该标记的地址。 如字符串"192.168.11.1"或者"yyx@ms.com"二者字符串都有相似之处,即整个字符串由分隔符.,@分割。如果需要得到分隔符所分割出的每个小字符串,则可以用strtok函数切分。
- 参数
strDelimit是所有分隔符所组成字符串,参数strToken是由一个或多个分隔符和标记组成的字符串。
类似于.,@称为分隔符,而分隔符所分割出的子字符串如192,com被称作标记。
strstr将标记以\0结尾且返回指向该标记的指针。故被操作字符串会发生修改,故一般使用临时拷贝的内容。- 首个参数传入字符串地址时,找到其首个标记。随后保存分隔符的位置,此时仅需传入
NULL则可访问该字符串的下一个标记。
第一次调用传入需分割字符串的地址,函数保存首个标记后的分隔符被\0替换的位置。之后的调用就无需在传入地址。当访问到最后一个标记时,返回NULL。
char arr[] = "www.yourfriendyo.top";
printf("%s\n", strtok(arr2, "."));
printf("%s\n", strtok(NULL, "."));
printf("%s\n", strtok(NULL, "."));int main()
{
char arr[] = "yourfriendyo@ms.com";
char* sep = "@.";
for (char* i = strtok(arr, sep); i != NULL; i = strtok(NULL, sep)) {
printf("%s\n", i);
}
return 0;
}字符串报错strerror
函数声明
char *strerror ( int errnum );strerror返回内置错误码所对应的错误信息的字符串地址。
printf("%s\n", strerror(40));//Function not implemented
printf("%s\n", strerror(30));//Read - only file system
printf("%s\n", strerror(20));//Not a directory一个错误码对应一个错误信息,手动传参并打印函数的返回值。当然没有人会这样使用报错函数,这也不是报错函数设计的初衷。
- 当程序发生错误时,程序会自动将错误码存入内置全局变量
errno中,此时我们调用strerror函数即可获得此次错误的报错信息。 - 更为直接的报错函数
perror,优点简单方便加入自定义信息,缺点必须打印错误信息。
int main()
{
FILE* pFile = fopen("D:test.txt", "r");
if (pFile == NULL) {
//text.txt: No such file or directory
printf("text.txt: %s\n", strerror(errno));
perror("text.txt");
}
return 0;
}内存函数
字符串操作函数适用于字符串内容,而内存操作函数适用于任意类型的数据如整形数据或是结构体,更具普适性。
内存拷贝memcpy
函数声明
void* memcpy ( void* dest, const void* src, size_t count );memcpy将源内存的前count个字节的内容拷贝到目标内存中。若源空间和目标空间有重叠,则拷贝时会覆盖源字符串内容。

C标准并未要求memcpy完成发生内存重叠的内容拷贝,但编译器也可能对其进行优化。对内存重叠的内容进行拷贝时,可以使用memmove。
模拟实现
以字节为单位拷贝内存中的内容,以适用所有类型的数据。
void* my_memcpy(void* dest, const void* src, size_t count) {
void* begin = dest;
while (count--) {
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return begin;
}内存移动memmove
函数声明
void* memmove ( void* dest, const void* src, size_t count );memmove将源空间的前count个字节的内容拷贝到目标空间中,并支持完成内存重叠的拷贝。
模拟实现
当发生内存重叠时,在源空间该字节未拷贝之前要保护其不被修改。
当目标空间在源空间的后边时,从后向前拷贝,当目标空间在源空间的前面时,从前向后拷贝。

实现方案

void* my_memmove(void* dest, const void* src, size_t count) {
assert(dest && src);
char* begin = dest;
if (dest > src) {
//后->前
while (count--) {
*((char*)dest + count) = *((char*)src + count);
}
}
else {
//前—>后
while (count--) {
*(char*)dest = *(char*)src;
(char*)dest += 1;
(char*)src += 1;
}
}
return begin;
}
int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//1234->3456
my_memmove(arr + 2, arr, 4 * sizeof(int));
//3456->1234
my_memmove(arr, arr + 2, 4 * sizeof(int));
for (int i = 0; i < 10; i++) {
printf("%d ", arr[i]);
}
return 0;
}C语言没有字符串类型,但凡遇到对字符串的操作都要细化到操作每个字符。
内存比较memcmp
函数声明
int memcmp ( const void* buf1, const void* buf2, size_t count );比较两块内存空间的前count个对应字节内容,并返回相关的数值。
模拟实现
int my_memcmp(const void* buf1, const void* buf2, size_t count) {
assert(buf1 && buf2);
while (count-- && (*(char*)buf1 == *(char*)buf2)) {
(char*)buf1 += 1;
(char*)buf2 += 1;
}
return *(char*)buf1 - *(char*)buf2;
}
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 1,2,3,4,6 };
int ret = my_memcmp(arr1, arr2, 1 * sizeof(int));
printf("%d\n", ret);
return 0;
}内存初始化memset
函数声明
void* memset ( void* dest, int c, size_t count );将目标空间前count个字节初始化为整形数据c。
