【C进阶 三】字符串函数与内存函数
码字不易,对你有帮助 点赞/转发/关注 支持一下作者
微信搜公众号:不会编程的程序圆
看更多干货,获取第一时间更新
思维导图
image
目录
@[toc]
正文
- strlen & strlen_s
- getchar() & putchar()
- strcmp & strncmp()
- strcpy() & strncpy()
- strcat & strncat()
以上这几个函数在我的另一篇文章中我已经详细讲过了。文章链接
这篇文章中,我们先主要来简单实现一下这几个函数,然后再讨论其他函数。
序 老朋友们
myStrlen & myStrcat & myStrcpy
#include<stdio.h>
#include<assert.h>
int myStrlen(const char* str);
char* myStrcat(char* dest, const char* src);
char* myStrcpy(char* dest, const char* src);
int main(void) {
char str1[12] = "Today ";
char str2[6] = "sucks";
//str1 和 str2 不是空指针
if (str1 && str2) {
printf("str1 is :%s str2 is :%s\n", str1, str2);
printf("str1 has %d characters, str2 has %d characters\n", myStrlen(str1), myStrlen(str2));
printf("str1 + str2 = %s\n", myStrcat(str1, str2));
printf("Copy str2 to str1,now str1 is:%s\n", myStrcpy(str1, str2));
}
return 0;
}
int myStrlen(const char* str) {
assert(str != NULL);
const char* start = str;
while (*++str);
return str - start;
}
char* myStrcat(char* dest, const char* src) {
assert(dest && src);
char* ret = dest;
//找到 dest 中 '\0' 的位置
while (*++dest);
while (*dest++ = *src++);
return ret;
}
char* myStrcpy(char* dest, const char* src) {
assert(dest && src);
char* ret = dest;
while (*dest++ = *src++);
return ret;
}
MyStrcmp
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
#include<assert.h>
// strcmp 返回值:
// 如果 str1 > str2, 返回的是一个比 0 大的数(不一定是 1)
// 如果 str1 < str2, 返回的是一个比 0 小的数(不一定是 -1)
// str1 > str2 在 VS 上可能是 1,在 linux gcc 中不一定是 1
// 为了方便实现,我们就选择返回 -1 和 1
int MyStrcmp(char* str1, char* str2) {
assert(str1 != NULL && str2 != NULL);
while (*str1 != '\0' && *str2 != '\0') {
if (*str1 > *str2) {
return 1;
}
else if (*str1 < *str2) {
return -1;
}
str1++;
str2++;
}
// 当循环退出时,str1 或 str2 达到字符串末尾 '\0'
// 对于 strcmp 来说,如果前面的字符都相同,那么,短的字符串就小
// 因为 '\0' 的 ASCII 值是 0,它是最小的(达到字符串结尾的字符串用'\0'来进行最后一次比较)
if (*str1 > * str2) {
return 1;
}
else if (*str1 < *str2) {
return -1;
}
else {
return 0;
}
}
int main(void) {
char str1[] = "haha";
char str2[] = "haha";
int ret = MyStrcmp(str1, str2);
if (ret > 0) {
printf("str1 > str2\n");
}
else if (ret < 0) {
printf("str1 < str2\n");
}
else {
printf("str1 == str2\n");
}
return 0;
}
始 新朋友们
1. strstr
char *strstr( const char* str, const char* substr );
定义于头文件:<string.h>
查找
substr
所指的空终止字节字符串在str
所指的空终止字节字符串中的首次出现。不比较空终止字符。若
str
或substr
不是指向空终止字节字符串的指针,则行为未定义。参数:
str - 指向要检验的空终止字节字符串的指针
substr - 指向要查找的空终止字节字符串的指针返回值:
指向于
str
中找到的子串首字符的指针,或若找不到该子串则为 NULL 。若substr
指向空字符串,则返回str
。
想象实现 MyStrstr 是我们产品经理的需求,先来看看他的需求是什么:
- 查找 字符串 substr 在字符串 str 中首次出现的位置(返回找到的字串的首字符的指针)
思路
- 我们先在 str 中找到 substr 的第一个元素
- 比较 str 的下一个字符与 substr 的下一个字符是否相等(可以循环实现)
- 如果 substr 中有一个字符与 str 中的是不一样的,那么 substr 应该从首个元素开始重新在 str 中继续向下寻找,直到找到或者 str 结束
- 我忽略了一个条件:当 substr 重新从第一个元素开始在 str 中寻找时,str 应该重置为 上一次 str 所在位置的下一个字符处(比如 "cacacat" 中寻找 "cacat")
实现
#include<stdio.h>
#include<string.h>
#include<assert.h>
char* MyStrstr(const char* str, const char* substr) {
const char* substr_begin = substr;
const char* str_next = str;
assert(str != NULL && substr != NULL);
if (*str == '\0' || *substr == '\0') {
return NULL;
}
while (*str_next != '\0') {
// substr 从头开始
substr = substr_begin;
str = str_next;
if (*str == *substr) {
while (*str == *substr && *str != '\0' && *substr != '\0') {
str++;
substr++;
}
// 循环退出三种情况
if (*substr == '\0') {
return str_next;
}
if (*str == '\0') {
return NULL;
}
// 剩下的一种就是 str 的字串 和 substr 不是完全匹配的,重新找 substr 的第一个元素
}
str_next++;
}
}
int main(void) {
char str1[] = "Hello World";
char str2[] = "lo";
char* ptr = MyStrstr(str1, str2);
if (MyStrstr != NULL) {
printf("%s\n", ptr);
}
else {
printf("str2 在 str1 中不存在\n");
}
return 0;
}
2. strtok
了解这个函数即可。
char *strtok( char *str, const char *delim )
定义于头文件 < string.h >
参数:
str - 指向要记号化的空终止字节字符串的指针 delim - 指向标识分隔符的空终止字节字符串的指针 返回值:
返回指向下个记号起始的指针,或若无更多记号则返回 NULL 。
注意:
此函数是破坏性的:它写入 '\0' 字符于字符串
str
的元素。特别是,字符串字面量不能用作strtok
的首参数。每次对
strtok
的调用都会修改静态对象:它不是线程安全的。
strtok 中有个 static 修饰的变量记录下来上次位置
int main(void) {
char str[] = "World is so beautiful!Hi,Look!";
char* pch;
pch = strtok(str, ",. !");
while (pch != NULL) {
printf("%s\n", pch);
pch = strtok(NULL, ",. !");
}
return 0;
}
// 输出:
World
is
so
beautiful
Hi
Look
3. memcpy
void* memcpy( void *dest, const void *src, size_t count );
定义于头文件 :<string.h>
从
src
所指向的对象复制count
个字符到dest
所指向的对象。两个对象都被转译成 unsigned char 的数组。若访问发生在 dest 数组结尾后则行为未定义。若对象重叠(这违背
restrict
契约) (C99 起),则行为未定义。若dest
或src
为非法或空指针则行为未定义。参数:
dest - 指向要复制的对象的指针 src - 指向复制来源对象的指针 count - 复制的字节数 返回值:
返回
dest
的副本,本质为更底层操作的临时内存地址,在实际操作中不建议直接使用此地址,操作完成以后,真正有意义的地址是dest本身。注意:
memcpy
可用于设置分配函数所获得对象的有效类型。
memcpy
是最快的内存到内存复制子程序。它通常比必须扫描其所复制数据的 strcpy ,或必须预防以处理重叠输入的 memmove 更高效。许多 C 编译器将适合的内存复制循环变换为
memcpy
调用。在严格别名使用禁止检验同一内存为二个不同类型的值处,可用
memcpy
转换值。
注:
void* 只包含地址,没有内存空间大小这样的信息,所以 void* 不能解引用,也不能进行运算
void* 是为了兼容各种类型的指针,算是一种简单的“泛型编程”。
简单的用法:
int main(void) {
char src[] = "Once upon a midnight dreary", dest[4];
memcpy(dest, src, sizeof dest);
for (size_t i = 0; i < sizeof dest; i++)
putchar(dest[i]);
int* p = malloc(3 * sizeof(int));// 分配的内存没有有效类型
int arr[3] = { 1, 2, 3 };
memcpy(p, arr, 3 * sizeof(int));// 分配到内存有了有效类型 int
return 0;
}
实现
#include<stdio.h>
#include<assert.h>
void* MyMemcpy(void* dest, const void* src, size_t count) {
assert(dest != NULL && src != NULL);
void* ret = dest;
for (size_t i = 0; i < count; i++) {
*(char*)dest = *(char*)src;// 逐字节复制
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return dest;
}
int main(void) {
char src[] = "Once upon a midnight dreary", dest[4];
MyMemcpy(dest, src, sizeof dest);
for (size_t i = 0; i < sizeof dest; i++)
putchar(dest[i]);
int* p = malloc(3 * sizeof(int));// 分配的内存没有有效类型
int arr[3] = { 1, 2, 3 };
MyMemcpy(p, arr, 3 * sizeof(int));// 分配到内存有了有效类型 int
for (int i = 0; i < 3; i++) {
printf("%d ", *(p + i));
}
return 0;
}
思考
请看下例:
#include<stdint.h>
#include<inttypes.h>
#include<stdio.h>
#include<string.h>
int main(void) {
double d = 1.0;
int64_t i = 1;
i = d;
// 输出:0.000000 5509945
printf("%f %d\n", i, i);
memcpy(&i, &d, sizeof d);
printf("%f %"PRId64" ", i, i);//#define PRId64 "lld"
//输出:1.000000 4607182418800017408
return 0;
}
memcpy 该表了 i 作为整型的内存布局,所以 i 可以直接用 %f 输出
4. memmove
void* memmove( void* dest, const void* src, size_t count )
定义于头文件 :< string.h >
从
src
所指向的对象复制count
个字节到dest
所指向的对象。两个对象都被转译成 unsigned char 的数组。对象可以重叠:如同复制字符到临时数组,再从该数组到dest
一般发生复制。若出现 dest 数组末尾后的访问则行为未定义。若
dest
或src
为非法或空指针则行为未定义。参数:
dest - 指向复制目的对象的指针 src - 指向复制来源对象的指针 count - 要复制的字节数 返回值:
返回
dest
的副本,本质为更底层操作的临时内存地址,在实际操作中不建议直接使用此地址,操作完成以后,真正有意义的地址是dest本身。注意:
memmove
可用于设置由分配函数获得的对象的有效类型。尽管说明了“如同”使用临时缓冲区,此函数的实际实现不会带来二次复制或额外内存的开销。常用方法( glibc 和 bsd libc )是若目标在源之前开始,则从缓冲区开始正向复制,否则从末尾反向复制,完全无重叠时回落到更高效的 memcpy。
在严格别名时用禁止检验同一内存为二个不同类型的值时,可使用
memmove
转换值。
重叠的含义
在这里插入图片描述实现
#include<stdio.h>
#include<assert.h>
void* MyMemcpy(void* dest, const void* src, size_t count) {
assert(dest != NULL && src != NULL);
void* ret = dest;
for (size_t i = 0; i < count; i++) {
*(char*)dest = *(char*)src;// 逐字节复制
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return dest;
}
void* MyMemmove(void* dest, const void* src, size_t count) {
assert(dest != NULL && src != NULL);
void* ret = dest;
char* pdest = (char*)dest;
char* psrc = (char*)src;
// 先判断有没有重叠
// 如果没有重叠,应该用更为高效的 memcpy
if (dest >= (char*)src + count || src >= dest) {
MyMemcpy(dest, src, count);
}
else {
// 分别指向 dest 和 src 的最后一个字符
pdest = pdest + count - 1;
psrc = psrc + count - 1;
while (count--) {
*pdest = *psrc;
pdest--;
psrc--;
}
return dest;
}
}
int main(void) {
char str[] = "123456789";
puts(str + 1);
MyMemcpy(str + 1, str, 3);
puts(str + 1);
MyMemcpy(str, "123456789", sizeof(str));
MyMemmove(str + 1, str, 3);
puts(str + 1);
return 0;
}
//输出:
23456789
11156789
12356789
5. memcmp
int memcmp( const void* lhs, const void* rhs, size_t count );
参数:
lhs, rhs - 指向要比较的对象的指针 count - 要检验的字节数 返回值:
若
lhs
以字典序出现前于rhs
则为负值。若
lhs
与rhs
比较相等,或 count 为零则为零。若
lhs
以字典序出现后于rhs
则为正值。注意:
此函数读取对象表示,而非对象值,而且典型地只对字节数组有意义:结构体可以含有填充字节而其值不确定,存储于联合体最近存储成员后的任何字节的值是不确定的,且一个类型可以对相同值拥有二种或多种表示(对于 +0 和 -0 或 +0.0 和 –0.0 的相异编码、类型中不确定填充位)。
简单的应用:
#include<stdio.h>
#include<string.h>
int main(void) {
int arr1[] = { 0, 1, 2, 3 };
int arr2[] = { 0, 1, 2, 3 };
int arr3[] = { -0, 1, 2, 3 };
if(memcmp(arr1, arr2, sizeof arr1) == 0){
printf("arr1 == arr2\n");
}
else {
printf("arr1 != arr2\n");
}
if (memcmp(arr1, arr3, sizeof arr1) == 0) {
printf("arr1 == arr3\n");
}
else {
printf("arr1 != arr3\n");
}
return 0;
}
6. memset
void *memset( void *dest, int ch, size_t count );
定义于头文件 <string.h>
复制值
ch
(如同以 (unsigned char)ch 转换到 unsigned char 后)到dest
所指向对象的首count
个字节。若出现 dest 数组结尾后的访问则行为未定义。若
dest
为空指针则行为未定义。参数:
dest - 指向要填充的对象的指针 ch - 填充字节 count - 要填充的字节数 返回值:
dest
的副本,本质为更底层操作的临时内存地址,在实际操作中不建议直接使用此地址,操作完成以后,真正有意义的地址是dest本身。
简单应用:
#include<stdio.h>
#include<string.h>
int main(void) {
char str[] = "Hello World";
memset(str, '\0', sizeof str);
puts(str);
return 0;
}
在 Github 上看更全的目录:
https://github.com/hairrrrr/C-CrashCourse
以后的这个系列的代码都会上传上去,欢迎 star
以上就是本次的内容。
如果文章有错误欢迎指正和补充,感谢!
最后,如果你还有什么问题或者想知道到的,可以在评论区告诉我呦,我可以在后面的文章加上你们的真知灼见。
关注我,看更多干货!
我是程序圆,我们下次再见。