目录
0.前言
1. 函数介绍
1.1 strlen
求字符串长度
size_t strlen ( const char * str );
基本使用方式:
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = "hello bit";
int len = strlen(arr); //string length
printf("len = %d", len); //len = 9
return 0;
}
注:
① 字符串以 '\0' 作为结束标志,strlen函数返回的是在字符串中 '\0' 前面出现的字符个数(不包含 '\0' )
② 参数指向的字符串必须要以 '\0' 结束 (不然得到的是随机值)
③ 注意函数的返回值为size_t (unsigned int) ,是无符号的,即字符串长度不可能为负( 易错点 ),表达式中包含无符号整数时,表达式的结果也是无符号整数,这样就可能出现问题
看这样一段代码:
//达不到预期效果的代码
int main()
{
char* p1 = "abc";
char* p2 = "abcdef";
if (strlen(p1) - strlen(p2) > 0)
{
printf("hehe\n");
}
else
{
printf("haha\n");
}
return 0;
//执行结果为 hehe
}
//修改后的代码
int main()
{
char* p1 = "abc";
char* p2 = "abcdef";
if (strlen(p1) > strlen(p2) )
{
printf("hehe\n");
}
else
{
printf("haha\n");
}
return 0;
//执行结果为 haha
}
strlen(p1) - strlen(p2) 理论上讲应该为 -3,而当看作无符号数时,-3则成了一个很大的正数
解决办法:1.在用strlen计算之前先强制转换成 int 类型;2.不用加减,直接用> <比较大小
④ 模拟实现:
#include <stdio.h>
#include <string.h>
#include <assert.h>
//方法一:
//size_t 就相当于unsigned int,定义无符号整型时可以直接使用
size_t my_strlen1(const char* str) //用const修饰*str, 使该指针指向的内容不能被修改了
{
assert(str != NULL);
int count = 0;//计数器
while (*str != '\0')
{
count++;
str++;
}
return count;
}
//方法二:
//不能使用计数器的版本(递归)
//my_strlen("hello");
//1 + my_strlen(ello);
//1+1+my_strlen(llo);
//1+1+1+my_strlen(lo);
//1+1+1+1+my_strlen(o);
//5+my_strlen('\0');
//5 + 0 = 5
size_t my_strlen2(const char* str) //用const修饰*str, 使该指针指向的内容不能被修改了
{
assert(str != NULL);
if (*str != '\0')
{
return 1 + my_strlen2(str + 1);
}
else
{
return 0;
}
}
//方法三:
//指针减指针的方法
size_t my_strlen3(const char* str)
{
assert(str != NULL);
const char* start = str; //const的目的:把受保护的数据,交给一个"安全的"指针
while (*str != '\0')
{
str++;
}
return str - start;
}
int main()
{
char arr[] = "hello";
int len = my_strlen3(arr);
printf("len = %d", len);
return 0;
}
1.2 strcpy
字符串拷贝
char* strcpy(char * destination, const char * source );
基本使用方式:
int main()
{
char arr1[20] = { 0 };
char arr2[] = "hello bit";
strcpy(arr1, arr2); //arr1为目标字符串,arr2为源字符串
return 0;
}
① 源字符串必须以 '\0' 结束。
如,上述示例中如果这样定义arr2 :
char arr2[] = {'b','i','t'};
则会出现问题,因为strcpy函数会把源字符串的 '\0' 也拷贝进入目标字符数组,如果定义时不加'\0'
系统会继续往后拷贝直到找到'\0'为止,此时不仅达不到目标效果,且还有越界访问的风险
② 会将源字符串中的 '\0' 拷贝到目标空间。
③ 目标空间必须足够大,以确保能存放源字符串。
④ 目标空间必须可变。
如果目标空间不能修改,则达不到目的,如:
char* arr1 = "xxxxxxxxxx"; //常量字符串,不能被修改
⑤ 模拟实现 :
//模拟实现strcpy
//设置char*为返回类型,便于函数的链式访问(嵌套调用)
char* my_strcpy(char* dest,const char* src)
{
char* ret = dest;
assert(dest != NULL);
assert(src != NULL);
while (*dest++ = *src++)
{
;
}
return ret;
}
int main()
{
char arr1[20] = { 0 };
char arr2[] = "hello bit";
my_strcpy(arr1, arr2);
printf("%s\n", my_strcpy(arr1, arr2));//链式访问
return 0;
}
1.3 strcat
字符串追加/连接
char * strcat ( char * destination, const char * source );
基本使用方式:
int main()
{
char arr[20] = "hello ";
strcat(arr, "world");
printf("%s\n", arr); //hello world
return 0;
}
① 源字符串必须以 '\0' 结束。(因为strcat是从源字符串的 ‘\0’ 处开始向后追加)
② 目标空间必须有足够的大,能容纳下源字符串的内容。
③ 目标空间必须可修改。
④ 字符串自己给自己追加,会怎么样?
追加时,追加字符串的第一个字符会取代源字符串的'\0',也就是说,在进行拷贝时,本该作为结束标志的'\0'丢失了,代码会一直循环追加操作,直到超出字符数组的范围,越界访问,程序会崩溃。
⑤ 模拟实现:
//模拟实现strcat
char* my_strcat(char* dest, const char* src)
{
char* ret = dest;
assert(*dest && *src);
//1.找目的地空间的'\0'
while (*dest) //当*dest != '\0'为真
{
dest++;
}
//2.拷贝数据
while (*dest++ = *src++) //由'w'替代arr中'\0'的位置,依次向后拷贝
{
;
}
return ret;
}
int main()
{
char arr[20] = "hello ";
printf("%s\n", my_strcat(arr, "world")); //hello world
return 0;
}
1.4 strcmp
字符串比较
int strcmp ( const char * str1, const char * str2 );
① 该函数如何评判两字符串的大小?
从首字符开始比较两者对应字符ASCII码值,若不相等,则ASCII码值大的字符所在字符串大于另一字符串,若相等则比较下一个字符,若所有字符串均相等,则两个字符串相等
② 判断后的返回值是什么?
第一个字符串大于第二个字符串,则返回大于0的数字
第一个字符串等于第二个字符串,则返回0
第一个字符串小于第二个字符串,则返回小于0的数字
基本使用方式:
int main()
{
int ret1 = strcmp("abc", "awe");
int ret2 = strcmp("abc", "abc");
int ret3 = strcmp("abc", "abb");
printf("%d %d %d\n", ret1, ret2, ret3); //-1 0 1
return 0;
}
③ 模拟实现:
int my_strcmp(const char* s1, const char* s2)
{
assert(s1 && s2);
while (*s1 == *s2)
{
if (*s1 == '\0')
return 0;
s1++;
s2++;
}
return *s1 - *s2;
/*if (*s1 > *s2) //vs的实现方式
{
return 1;
}
else
{
return -1;
}*/
}
int main()
{
printf("%d\n", my_strcmp("abc", "abb"));
return 0;
}
以上strcpy、strcat、strcmp函数均为长度不受限制的字符串操作函数,即该函数在使用的时候不会考虑给定空间是否够大,它们只会找'\0',所以在使用时可能经常会出现警告或者错误
所以接下来介绍的是长度受限制的字符串操作函数:strncpy、strncat、strncmp
1.5 strncpy
字符串拷贝
char * strncpy ( char * destination, const char * source, size_t num );
可见,相比strcpy,strncpy多了一个参数 : size_t num 用于对拷贝字符的数量进行限制:
① 拷贝num个字符从源字符串到目标空间。
② 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。
例如:
int main()
{
char arr[10] = "aaaaaaaaa";
strncpy(arr, "hello", 7);
return 0;
}
调试窗口可以看到:
基本使用方式:
int main()
{
char arr1[20] = { 0 };
char arr2[20] = { 0 };
char arr3[] = "hello";
strncpy(arr1, arr3, 3);//3 表示只拷贝3个字符进arr1
strncpy(arr2, arr3, 7);//7 超出了arr3的字符串长度,则会在后面补'\0'
printf("%s\n", arr1); //hel
printf("%s\n", arr2); //hello
return 0;
}
1.6 strncat
字符串追加/连接
char * strncat ( char * destination, const char * source, size_t num );
num表示追加几个字符
① 从目标字符串的第一个'\0'处开始追加,追加num个字符
② 追加结束后会自动补'\0'
基本使用方式:
int main()
{
char arr1[20] = "abcdef";
char arr2[] = "qwer";
strncat(arr1, arr2, 4);
printf("%s\n", arr1);
return 0;
}
练习:判断一个字符串是否可由另一个字符串左旋得到
//字符串左旋操作
void left_move(char* str, int n)
{
int i = 0;
int len = strlen(str);
char tmp = *str;
int j = 0;
for (i = 0; i < n; i++)
{
//旋转一次操作:
for (j = 0; j < len - 1; j++)
{
*(str + j) = *(str + j + 1);//从第二个字符开始前移
}
*(str + len - 1) = tmp; //第一个字符放到最后去
}
}
暴力方式 : 每一次旋转后都比较一次
//int main()
//{
// char arr1[] = "abcdef";
// char arr2[] = "cdefab";
// int i = 0;
// int len = strlen(arr1);
//
// for (i = 0; i < len; i++)
// {
// left_move(arr1, 1);
// if (strcmp(arr1, arr2) == 0)
// {
// printf("YES\n");
// break;
// }
// }
// if (i == len) //每一趟比较均不相等,则说明不是旋转得到的
// {
// printf("NO\n");
// }
//
// return 0;
//}
//方法二 :
//在arr1后再追加一次arr1 : abcdefabcdef
//那么所有左旋转的结果,都被包含在上述字符串的子串中
int main()
{
char arr1[20] = "abcdef";
char arr2[] = "cdefab";
if (strlen(arr1) != strlen(arr2)) //如果两字符串长度都不等,则肯定无法通过旋转得到
{
printf("NO\n");
return 0;
}
strncat(arr1, arr1, 6);//arr1追加自己
char* ret = strstr(arr1, arr2); //strstr : 寻找子字符串,若找到子串则返回子串的起始位置,若不是子串则返回空指针
if (ret == NULL)
{
printf("NO\n");
}
else
{
printf("YES\n");
}
return 0;
}
1.7 strncmp
字符串比较
int strncmp ( const char * str1, const char * str2, size_t num );
比较两字符串的前num个字符
比较到出现另个字符不一样或者一个字符串结束或者num个字符全部比较完
基本使用方式:
int main()
{
char arr1[] = "abcdef";
char arr2[] = "abcdef";
int ret = strncmp(arr1, arr2, 5);//比较前5个字符
printf("%d\n", ret); //0
return 0;
}
1.8 strstr
寻找字符串的子串
char * strstr ( const char *str1, const char * str2);
寻找子字符串,若找到子串则返回子串的起始位置,若不是子串则返回空指针
基本使用方式:
int main()
{
char arr[] = "abcdefabcdef";
char* ret = strstr(arr, "cd");
if (ret != NULL)
{
printf("%s\n", ret); //cdefabcdef
}
return 0;
}
模拟实现:
//找到子串返回字串起始位置, 找不到返回空指针
char* my_strstr(const char* str1, const char* str2)
{
assert(str1 && str2); //保证str1和str2不为空指针
//建两个指针,分别指向起始位置,用于保存 下一次尝试匹配的位置
const char* s1 = str1;
const char* s2 = str2;
//用于保存 : 下一次开始匹配的位置
const char* cp = str1;
if (*s2 == '\0') //strstr函数规定如果str2为空字符串,则返回str1的地址
{
return (char*) str1;
}
while (*cp) //*cp != '\0'说明字符串中还有字符没进行查找
{
//匹配查找一次的操作
s1 = cp;
s2 = str2;
while (*s1 && *s2 && *s1 == *s2) //*s1 != '\0',*s2 != '\0'且s1 == s2时向后判断
{
s1++;
s2++;
}
//之后分别处理三种跳出循环的情况
if (*s2 == '\0') //说明匹配成功了//返回此时的cp地址
{
return (char*) cp; //co是由const char*修饰的,而该函数返回类型又是char*,即把安全的指针赋给了不安全的,会报警告。所以此时强制类型转换成char*
}
cp++; //s1 != s2时的处理
}
return NULL; //s1 == '\0'说明没找到子串返回空指针
}
int main()
{
char* str1 = "abcdef";
char* str2 = "cde";
char* ret = my_strstr(str1, str2);
printf("%s\n", ret);
return 0;
}
1.9 strtok
分解字符串
char * strtok ( char * str, const char * sep );
① sep参数是个字符串,定义了用作分隔符的字符集合。
② 第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记。
③ strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。
(注: strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)
④ strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串 中的位置。
⑤ strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
⑥ 如果字符串中不存在更多的标记,则返回NULL 指针
int main()
{
char arr1[] = "zqf@XJTU.com";
char arr2[] = "@.";
char tmp[30] = { 0 };
//由于strtok会修改原字符串,所以我们先拷贝,只对拷贝的内容进行修改
strcpy(tmp, arr1);
char* a = strtok(tmp, arr2);
//第一次调用
//由于tmp不为空指针, 则会开始向后找第一个标记'@',并将其修改为'\0'
//同时: 1. 会返回: 首字符'z'的地址 2.会记住@的位置(作为下次调用的起始位置)
printf("%s\n", a); //zqf
char* b = strtok(NULL, arr2);
//第二次调用
//同一个字符串中被保存的位置开始,查找下一个标记
//返回q的地址,记住'.'的位置
printf("%s\n", b);//XJTU
char* c = strtok(NULL, arr2);
printf("%s\n", c); //com
//第三次调用
//从被保存的位置开始,查找下一个标记,找到'\0'
//返回c的地址
//如果再调用也只能返回空指针了
//注 : 如果这样定义char arr1[] = "zqf@\0qq.com";
//找到第一个'\0'后,如果再调用strtok函数,该函数也不会继续访问内存中的剩余字符,而是返回空指针
return 0;
}
基本使用方式:
int main()
{
char arr1[] = "zqf@XJTU.com";
char tmp[30] = { 0 };
strcpy(tmp, arr1);
char arr2[] = "@.";
char* p = NULL;
for (p = strtok(tmp, arr2); p != NULL; p = strtok(NULL, arr2))
{
printf("%s\n", p);
}
return 0;
}
1.10 strerror
获得错误的描述字符串
char * strerror ( int errnum );
基本使用方法:
int main()
{
//当调用库函数,发生错误的时候,就会有些错误码
//错误码会放在errno这个全局变量中(如果没有错误则errno默认为0)
//需要头文件 #include <errno.h>
//正确的情况:
printf("%d\n", errno); // errno 没有错误默认为0
printf("%s\n", strerror(errno)); //No error 没有错误
//错误的情况:
//文件操作
//打开文件
FILE* pf = fopen("test.tst", "r");
//打开一个文件, 传参为1.文件名, 2.打开形式
// (此处为"读"的方式打开,如果文件不存在则会打开失败,如果是以"写"的方式打开,文件不存在时会创建一个该文件)
// 当打开成功,则会返回一个有效的指针,打开失败则返回空指针
//并且用FILE定义一个结构体变量接受fopen的返回值
if (pf == NULL)
{
printf("%s\n", strerror(errno)); //No such file or directory
perror("91班打印错误信息"); //
}
else
{
printf("打开成功\n");
fclose(pf); //关掉打开的文件
pf = NULL;
}
return 0;
}
1.11 字符分类函数
函数 | 如果他的参数符合下列条件就返回真 |
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 | 任何可打印字符,包括图形字符和空白字符 |
以tolower函数为例:
int main()
{
char ch = 'A';//65 'a'为97
//printf("%c\n", ch + 32);
//
printf("%c\n", tolower(ch));
//注意 : tolower(ch)返回值是小写字母的ASCII码值,而不是把ch从大写改变成了小写
//需要头文件 #include <ctype.h>
//把一个字符串改成全小写
char str[] = "Test String.\n";
char c;
int i = 0;
while (str[i]) //没遇到'\0'就转换并打印
{
c = str[i];
if (isupper(c))
{
c = tolower(c);
}
putchar(c);
i++;
}
return 0;
}
1.12 memcpy
内存拷贝(内存不重叠部分)
void * memcpy ( void * destination, const void * source, size_t num );
num表示需要拷贝的字节
①②③④⑤⑥⑦⑧⑨⑩
① 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。(一个字节一个字节的拷贝过去)
② 目标空间必须足够大且可修改。
③ 源字符串不需要有 '\0' 且该函数在遇到 '\0' 的时候并不会停下来。
④ 模拟实现:
void* my_memcpy(void* dest, const void* src, size_t count)
{
void* ret = dest;
while (count--)
{
//拷贝一个字节
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char* )src + 1;
}
return ret;
}
int main()
{
int arr1[] = { 1, 2, 3, 4, 5 };
int arr2[10] = { 0 };
//对于整型数组,肯定不能用strcpy进行拷贝 :
//01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00 小端在内存中的存储
//strcpy遇到00就停止了
//此时则需要内存拷贝
my_memcpy(arr2, arr1, sizeof(arr1));
return 0;
}
1.13 memmove
内存拷贝(两内存块间可重叠)
void * memmove ( void* destination, const void * source, size_t num );
① 和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的,如果源空间和目标空间出现重叠,就得使用memmove函数处理。
② 模拟实现:
void* my_memmove(void* dest, const void* src, size_t count)
{
void* ret = dest;
assert(dest && src);
//当两块空间未重叠的时候,怎样拷贝都无所谓
//只有空间重叠时需要考虑拷贝的方式:
if (dest < src)//当dest在src左边的时候,src应该从前向后开始拷贝
{
while (count--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
else//反之,src从后向前拷贝
{
while (count--)
{
*((char*)dest + count) = *((char*)src + count);
}
}
return ret;
}
int main()
{
int arr1[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
//此时我想把1 2 3 4 拷贝到3 4 5 6的位置上去
my_memmove(arr1 + 2, arr1, 16);
return 0;
}
dest 在 src 左边时:从前向后拷贝
dest 在 src 右边时:从后向前拷贝
基本使用方式:
int main()
{
int arr1[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
//此时我想把1 2 3 4 拷贝到3 4 5 6的位置上去
memmove(arr1 + 2, arr1, 16);
return 0;
}
1.14 memcmp
内存比较
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
比较从 ptr1 和 ptr2 指针开始的num个字节 ,返回类型和strcmp一个道理
基本使用方法:
int main()
{
int arr1[] = { 1, 2, 3, 4, 5};
int arr2[] = { 1, 2, 3, 3, 3};
int ret = memcmp(arr1, arr2, 20);
printf("%d\n", ret); // 1
return 0;
}
1.15 memset
初始化函数
void * memset ( void *dest, int value, size_t num );
将某一块内存中的内容(从dest开始到dest+num的全部字节)全部设置为指定的值, 该函数通常为新申请的内存做初始化工作。
基本使用方式:
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
memset(arr, 0, sizeof(arr));
return 0;
}
注意,该函数是按照给定字节进行修改,如果上述函数这样使用:
memset(arr, 1, sizeof(arr));
并不是把数组的每一个元素都置为1,而是把每一个字节修改成了 01