如:腾讯面试题(下面会学习)
Memcpy和memmove的区别;
头文件:
size_t strlen(const char* str);//求字符串长度函数
说明:计算字符串 str的长度,直到空结束字符('\0'
),但不包括空结束字符('\0'
)。
参数str:要计算长度的字符串。
返回值:返回字符串的长度,类型为无符号(unsigned)
。
实例演示:
int main()
{
char arr1[] = "abcd";
char arr2[] = { 'a', 'b', 'c','d' };
printf("%d\n", strlen(arr1));
printf("%d\n", strlen(arr2));
return 0;
}
a b c d \0
;arr2数组在内存中存储的是 a b c d
。两者区别在于有无 \0
strlen()
函数在计算字符串长度时,直到 \0
(不计 \0
)❗❗易错点
代码1:
int main()
{
if (strlen("abc") - strlen("abcde") > 0)
printf(">\n");
else
printf("<\n");
printf("%d\n", 3 - 5);
printf("%u\n", 3 - 5);
return 0;
}
小朋友你是否有很多问号:这这这不是3 - 5 = -2嘛!?这不是 < 0吗?
分析:
strlen()
函数返回无符号整型数,3 - 5 底层下以补码来计算:
3-5等价于3+(-5)
00000000 00000000 00000000 00000011 - 3的补码
11111111 11111111 11111111 11111011 - -5的补码
11111111 11111111 11111111 11111110 - 3-5的补码
11111111 11111111 11111111 11111110 - -2的补码
0x ff ff ff fe -2的16进制
0xfffffffe
用带符号整数的意义来解释是-2;用无符号数的意义来解释是一个很大的数字4294967294(int)strlen("abc") - (int)strlen("abcde")
代码2:
int main()
{
int a = 10;
printf("%d", strlen(a));
return 0;
}
分析:
strlen()
实参和形参类型不兼容,在此会把整型变量a看作为地址,会造成非法访问。正确的是实参为字符串常量。
总结:
strlen()
,返回值是无符号整型,返回的是字符串 \0
前的字符个数(不含 \0
)strlen()
的字符串,字符串应以 \0
结尾,否则会返回随机值(因为 \0
位置未知);实参应为字符串,实质传递的是字符串首字符的地址⚡ 模拟实现strlen()函数:
有三种方法实现:
首先,我们应该了解strlen()函数,才能模仿它写出来,照葫芦画瓢:
my_strlen()
返回值是size_tmy_strlen()
仅是求字符串长度,不可对字符串进行修改,因此形参用const
来修饰assert(str);
对str进行断言,防止访问空指针造成安全问题(程序崩溃)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 != '\0')
{
str++;
}
return str - begin;
}
3️⃣递归版本:
size_t my_strlen(const char* str)
{
assert(str);
if (*str != '\0')
return 1 + my_strlen(str + 1);
else
return 0;
}
头文件:
char* strcpy(char* dest, const char* src);//字符串拷贝函数
说明:把 src 所指向的字符串(含 '\0'
)复制到 dest ,第一个 '\0'
是拷贝的结束标志。
❗注意:如果目标数组dest不够大,而源字符串src的长度又太长,可能会造成缓冲溢出的情况。
参数dest:指向用于存储复制内容的目标数组;
参数src:要复制的字符串。
返回值 :返回一个指向最终的目标字符串dest的字符型指针。
代码演示:
int main()
{
char arr[20] = { 0 };
strcpy(arr, "hello csdn!\n");
printf("%s", arr);
return 0;
}
运行结果:
将源字符串拷贝到目标数组
注意事项:
①拷贝是以第一个\0为结束标志
②目标空间要足够大,以装得下源字符串
③目标空间可变
1️⃣拷贝是以第一个\0为结束标志
int main()
{
char arr1[15] = "##########";
char arr2[] = "ab\0cd\0ef\0";
printf("%s", strcpy(arr1, arr2));//ab
return 0;
}
int main()
{
char arr1[] = "#####";
char arr2[] = "hhhhhhha";
printf("%s", strcpy(arr1, arr2));
return 0;
}
int main()
{
char* p = "########";//"########"在常量区,不可修改
//or const char p[] = "########";//const修饰,不可改变
char arr2[] = "csdn";
printf("%s", strcpy(p, arr2));
return 0;
}
⚡模拟实现strcpy()函数:
所谓的拷贝,不就是将字符串里的字符一个个复制粘贴吗?别忘了这可是我们最拿手的操作
我们知道:
strcpy()
返回char* ;源字符串src不可修改;拷贝是以第一个\0为结束标志 。
char* my_strcpy(char* dest, const char* src)
{
assert(dest && src);//断言,防止访问空指针
char* begin = dest;//begin记录首字符的地址
while (*dest++ = *src++)//后置++
{
;
}
return begin;
}
int main()
{
char arr1[10] = { 0 };
char arr2[] = "csdn!";
printf("%s", my_strcpy(arr1, arr2));//csdn!
return 0;
}
头文件:
char* strcat(char* dest, const char* src);//字符串追加函数
说明:把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。
参数:
返回值:返回一个指向最终的目标字符串 dest 的指针。
代码演示:
int main()
{
char arr1[10] = "CSDN!";
char arr2[] = "YES";
strcat(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
运行结果:
从监视中,可以看到:strcat将源字符串(包括\0),追加至目标字符串的结尾并覆盖掉目标空间的\0
注意事项:
①源字符串必须以\0结尾
②源字符串不能追加给自己
③目标空间必须足够大,以确保能够存放源字符串 src
④目标空间必须可变,即目标空间 src 不可以被 const 修饰
分析:源字符串不以\0结尾,导致非法访问
2️⃣源字符串不能追加给自己
监视:
可以看出strcat在疯狂地呐喊这"CSDN!",直到断气!
emmm正经点,就是strcat会一直往arr1追加"CSDN!",直到越界非法访问。
老规矩,我们用图来解释:
arr1给自己追加字符串时,会将字符串中的\0覆盖掉(第一个!后的\0被覆盖),导致没有\0追加上去,造成死循环直到越界非法访问。
⚡模拟实现strcat()函数:
char* my_strcat(char* dest, const char* src)
{
assert(src && dest);
char* begin = dest;//记录目标字符串的起始地址
//找到dest中的\0
while (*dest)
{
dest++;
}
//到此,dest指向\0,开始追加
while (*dest++ = *src++)
{
;
}
return begin;
}
int main()
{
char arr1[20] = "CSDN!";
char arr2[] = "YES";
my_strcat(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
头文件:
int strcmp(const char* str1, const char* str2);//字符串比较函数
说明:两个字符串自左向右逐个字符相比(按 ASCII 值大小相比较),从第一对字符比起,若相等则比较下一对,直到遇到不同的字符或 \0 才停止
参数:
返回值:
代码演示:
int main()
{
char* p1 = "abcd";
char* p2 = "abxy";
printf("%d\n", strcmp(p1, p2));
return 0;
}
运行结果:
分析: 比较c和x,c(ASCII) < x(ASCII),返回负数-1
注意事项:
①str1和str2都要以 \0 作为字符串的结尾,否则会造成越界非法访问。
②根据编译器的不同,返回的结果也不同。在 VS2019 中,大于返回 1,等于返回 0,小于返回 -1。但在 Linux-gcc 中,大于返回正数,等于返回0,小于返回负数。
因此有下面建议:
// 不推荐 ❌
if(strcmp(p1, p2) == 1)
printf("p1 > p2");
else if(strcmp(p1, p2 == 0))
printf("p1 == p2");
else if(strcmp(p1, p2) == -1)
printf("p1 < p2");
// 推荐 ✅
if(strcmp(p1, p2) > 0)
printf("p1 > p2");
else if(strcmp(p1, p2 == 0))
printf("p1 == p2");
else if(strcmp(p1, p2) < -1)
printf("p1 < p2");
⚡ 模拟实现strcmp()函数:
int my_strcmp(const char* str1, const char* str2)//仅比较不修改,都用const
{
assert(str1 && str2);//防止访问空指针
while (*str1 == *str2)
{
if (*str1 == '\0')//遇 \0 则停止
return 0;
str1++;
str2++;
}
return *str1 - *str2;
}
头文件:
char* strncpy(char* dest, const char* src, size_t n);//指定长度的字符串拷贝函数
说明:strncpy函数把 src 所指向的字符串复制到 dest,最多复制 n 个字符(遇到中间的\0停止,会小于n)。
注意事项:
①当 src 的长度小于 n 时,dest 的剩余部分将用 \0 填充。
②目标空间必须足够大,以确保能够存放源字符串 src。
③目标空间必须可变,即目标空间 src 不可以被 const 修饰。
1️⃣当 src 的长度小于 n 时,dest 的剩余部分将用 \0 填充。
int main()
{
char arr1[10] = "abcde";
char arr2[] = "xyz";
strncpy(arr1, arr2, 5);//3 < 5
printf("%s\n", arr1);
return 0;
}
⚡模拟实现strncpy()函数:
char* my_strncpy(char* dest, const char* src, size_t n)
{
assert(dest && src);
char* begin = dest;
while (n && (*dest = *src))
{
dest++;
src++;
n--;
}
while (n--)
{
*dest = '\0';
}
return begin;
}
int main()
{
char arr1[10] = "abcde";
char arr2[] = "xyz";
my_strncpy(arr1, arr2, 5);//3 < 5
printf("%s\n", arr1);
return 0;
}
头文件:
char* strncat(char* dest, const char* src, size_t n);//指定长度的字符串追加函数
说明:源字符串最多追加n个字符到目标空间。
代码演示:
int main()
{
char arr1[] = "CSDN!\0xxxx";
char arr2[] = "YES";
strncat(arr1, arr2, 5);
printf("%s\n", arr1);
return 0;
}
②n小于源字符串,追加完主动补 \0。
int main()
{
char arr1[] = "CSDN!\0xxxx";
char arr2[] = "YES";
strncat(arr1, arr2, 1);
printf("%s\n", arr1);
return 0;
}
注意事项: 与strcat不同的是:字符串可通过strncat给自己追加,因为追加完会补 \0
⚡模拟实现strncat()函数:
char* my_strncat(char* dest, const char* src, size_t n)
{
assert(dest && src);
char* begin = dest;
//找出dest的 \0
while (*dest)
dest++;
//追加
while (n && (*dest++ = *src++))
n--;
//n小于源字符串补0
if (n == 0)
*dest = '\0';
return begin;
}
头文件:
int strncmp(const char* str1, const char* str2, size_t n);//指定长度的字符串比较函数
说明:把 str1 和 str2 进行比较(按 ASCII 值大小),最多比较前 n 个字符。
代码演示:
int main()
{
char* p1 = "abcde";
char* p2 = "abxyz";
printf("%d\n", strncmp(p1, p2, 3));
return 0;
}
⚡ 模拟实现strncmp()函数:
int my_strncmp(const char* str1, const char* str2, size_t n)
{
assert(str1 && str2);
while (n-- && (*str1 == *str2))
{
str1++;
str2++;
}
return *str1 - *str2;
}
头文件:
char* strstr(const char* string, const char* substring);//字符串查找子串函数
说明:字符串 string 中查找第一次出现字符串 substring 的位置,不包含终止符 ‘\0’。该函数返回在 string 中第一次出现 substring 字符串的位置,若未找到则返回 NULL。
⚡ 模拟实现strstr()函数:
char* my_strstr(const char* str1, const char* str2)
{
assert(str1 && str2);
if (*str2 == '\0')
{
return NULL;//子串为空,返回NULL
}
char* s1;
char* s2;
char* cp = str1;
while (*cp != '\0')
{
s1 = cp;
s2 = str2;
while (*s1 == *s2 && *s1 != '\0' && *s2 != '\0')
{
s1++;
s2++;
}
if (*s2 == '\0')
{
return cp;
}
cp++;
}
return NULL;
}
拓展:
KMP算法 - 在字符串中查找子字符串的一种算法
头文件:
char* strtok(char* str, const char* delim);//分割字符串函数
说明:将字符串 str 分解为一组字符串,delim 为分隔符集合。
原理:找到分隔符,并用 ‘\0’ 代替,即字符串的结束符。
参数:
解释分隔符和标记:
. | - @
等等称为分隔符;
对于www.
csdn.
net,由分隔符.
所分割出来的子字符串被称为标记,如www,csdn,net
返回值:从 str 开头开始的一个个被分割的串的指针,当没有被分割的串时则返回NULL。
注意事项:
①这是调用一次strtok的结果:
②想要完整分割字符串,则要配合循环使用。因为stryok函数会自己保存指针,所以在下一次调用时,只需要给它传一个NULL即可。
③strtok函数会修改源字符串。我们一般会对源字符串拷贝一份,并让拷贝出来的去被切割(让替罪羊去送死)。
正确使用:
int main()
{
char web[] = "www.csdn.com";
char* delim = ".@";//分隔符集合
for (char* p = strtok(web, delim); p != NULL; p = strtok(NULL, delim))
{
printf("%s\n", p);
}
//证明源字符串被修改
for (int i = 0; i < sizeof(web)/sizeof(web[0]); i++)
{
printf("%c", web[i]);
}
return 0;
}
头文件:
char* strerror(int errnum);//字符串报错函数
说明: 从内部数组中搜索错误号 errnum,返回一个指向错误消息字符串的指针。strerror 生成的错误字符串取决于开发平台和编译器。
参数:errnum – 错误号,通常是errno
使用:errno是一个全局的错误码变量,当C语言的库函数在执行过程中发生错误,就会把对于的错误码赋值到errno中,通过调用strerror函数就可以把错误信息打印出来。
注意事项: 使用errno需要呼叫头文件
#include
#include
#include
int main()
{
printf("%s\n", strerror(errno));
printf("%s\n", strerror(1));
printf("%s\n", strerror(2));
printf("%s\n", strerror(3));
return 0;
}
常使用的场景在文件操作(后面会学习,关注我不迷路~)
#include
#include
#include
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
printf("%s\n", strerror(errno));
else
printf("open file successfully");
fclose(pf);//关闭文件
pf = NULL;
return 0;
}
头文件:
void perror(const char* str);//输出报错信息函数
说明:perror()用来将上一个函数发生错误的原因输出到标准设备 (stderr) 。首先输出字符串 str,后跟一个冒号,然后是一个空格。
联系:此错误原因依照全局变量 error 的值来决定要输出的字符串,perror函数只是将你输入的一些信息和现在的error所对应的错误一起输出。
代码演示:
#include
#include
//不需要#include
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
perror("fopen");
else
printf("open file successfully");
fclose(pf);//关闭文件
pf = NULL;
return 0;
}
总结: 在C语言编程中,通常使用perror()来输出错误信息,用strerror(errno)来获取错误信息。error是错误码,不同的错误码对应这不同的错误信息。
函数 | 如果它的参数符合下列条件就返回真 |
---|---|
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 | 任何可打印字符,包括图像字符和空白字符 |
isupper使用:
#include
#include
int main()
{
char ch1 = 'A';
int ret = isupper(ch1); // 判断ch1是否为大写
printf("%d\n", ret);//1
return 0;
}
字符转换函数
函数 | 功能 |
---|---|
tolower | 大写转小写 |
toupper | 小写转大写 |
tolower函数使用:
#include
#include
int main()
{
char ch = tolower('H'); // 大写转小写
putchar(ch);//h
return 0;
}
头文件:
void* memcpy(void* str1, const void* str2, size_t n);//内存拷贝函数
说明:从源存储区 str2 复制 n 个字节到目标存储区 str1,并返回一个指向目标存储区 str1 的指针。
代码演示:
// 将字符串复制到数组 dest 中
int main()
{
const char src[50] = "www.casdn.net";
char dest[50];
memcpy(dest, src, strlen(src) + 1);
printf("dest = %s\n", dest);
return(0);
}
注意事项:
①memcpy函数的参数1,参数2都是void* 型,void*又称通用类型。
②与strcpy函数不同,memcpy函数遇到 ‘\0’ 不会停止。
③若源空间和目标空间有重叠,则拷贝时会覆盖源字符串内容。
1️⃣memcpy函数的参数1,参数2都是void* 型,void*又称通用类型。
用结构体类型来测试
struct Stu
{
char name[20];
int age;
};
int main()
{
struct Stu arr1[5] = { 0 };
struct Stu arr2[] = { {"zhangsan", 69}, {"lisi", 18} };
memcpy(arr1, arr2, sizeof(arr2));
printf("name:%s\nage:%d\n", arr1[0].name, arr1[0].age);
return 0;
}
运行结果:
2️⃣与strcpy函数不同,memcpy函数遇到 ‘\0’ 不会停止。
int main()
{
char arr1[10] = { 0 };
char arr2[] = "abc\0def";
memcpy(arr1, arr2, 6);
return 0;
}
3️⃣若源空间和目标空间有重叠,则拷贝时会覆盖源字符串内容。
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
memcpy(arr + 2, arr, 5 * sizeof(int));
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
运行结果:
我们画图分析:
我们看到的结果是编译器对memcpy函数优化后的结果。且C标准并不要求memcpy完成发生内存重叠的内容拷贝,但编译器也可能对其进行优化。对内存重叠的内容进行拷贝时,可以使用memmove函数。
⚡ 模拟实现memcpy函数:
void* my_memcpy(void* dest, const void* src, size_t n)
{
void* begin = dest;
while (n--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return begin;
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
my_memcpy(arr + 2, arr, 5 * sizeof(int));
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
运行结果:
运行结果也验证了上图中memcpy函数实际上的结果。
头文件:
void* memmove(void* str1, const void* str2, size_t n);//内存移动函数
说明:将 str2 源区域的前 n 个字节拷贝到 str1 目标区域中。
如果目标区域和源区域有重叠的话,memmove() 能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中,但复制后源区域的内容会被更改。
注意事项:
①与memcpy() 不同, memmove() 函数处理的源区域和目标区域是可以重叠的。
C语言标准要求:memcpy() 用来处理不重叠的内存拷贝,而 memmove() 用来处理重叠内存的拷贝。
我就纳闷了,memmove() 既可以拷贝不重叠的数据,又可以拷贝重叠的数据,那我要你memcpy() 有何用?
代码演示:
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
memmove(arr + 2, arr, 20);
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
⚡模拟实现memmove()函数:
1.首先要清楚,memmove()函数要在memcpy()函数的基础上考虑数据重叠的情况。
2.那数据重叠是有几种呢?该如何保护源串在覆盖之前不被修改?
void* my_memmove(void* dest, const* src, size_t n)
{
assert(dest && src);
char* begin = dest;
if (dest > src)
{
//从后往前
while (n--)
{
*((char*)dest + n) = *((char*)src + n);
}
}
else
{
//从前往后
while (n--)
{
*(char*)dest = *(char*)src;
(char*)dest += 1;
(char*)src += 1;
}
}
return begin;
}
验证:
①dest <= src
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
my_memmove(arr, arr + 2, 20);
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
②dest > src
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
my_memmove(arr + 2, arr, 20);
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
头文件:
int memcmp(const void *str1, const void *str2, size_t n);//内存比较函数
说明:将存储区 str1 和存储区 str2 的前 n 个字节进行比较。
返回值:
注意事项:
与strcmp() 不同,memcmp() 遇到 \0 不会停止比较。
⚡模拟实现memcmp()函数:
int my_memcmp(const void* str1, const void* str2, size_t n)
{
assert(str1 && str2);
if (!n)
return 0;
while (--n && (*(char*)str1 == *(char*)str2))
{
(char*)str1 += 1;
(char*)str2 += 1;
}
return *(unsigned char*)str1 - *(unsigned char*)str2;
//unsigned char*保证运算结果的符号正确(指针 - 指针内容)
}
int main()
{
char* arr1 = "\0abc";
char* arr2 = "\0abx";
int ret = my_memcmp(arr1, arr2, 4);
printf("%d\n", ret);
return 0;
}
头文件:
void* memset(void* str, int c, size_t n);//内存初始化函数
说明:复制字符 c(一个无符号字符)到 str 所指向的字符串的前 n 个字节。
注意事项:
参数c以 int 形式传递,但是函数在填充内存块时是使用该值的无符号字符形式。
代码演示:
int main()
{
int arr[10] = { 0 };//共40字节
memset(arr, 1, 20); //将前20个字节全部设置为1
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
⚡模拟实现memset()函数:
思路:将c转化为无符号字符(1个字节)逐个拷贝。
void* my_memset(void* str, int c, int n)
{
void* begin = str;
while (n--)
{
(*(unsigned char*)str) = (unsigned char)c;
(unsigned char*)str += 1;
}
return begin;
}
int main()
{
int arr[10] = { 0 };//共40字节
my_memset(arr, 1, 20); //将前20个字节全部设置为1
return 0;
}
运行结果:
写文和制图不易,恳请各位铁汁们点赞收藏评论加关注,你们的支持是我坚持的动力~
……未完待续
参考资料:
【C语言教程 | 菜鸟教程】
✒️ 笔者:陈汉新
更新: 2022.3.24
❌ 勘误: 暂无
声明:由于作者水平有限,本文错误之处在所难免,敬请读者指正!