C语言中对字符和字符串的处理很是频繁,但C语言本身没有字符串类型,故字符串通常放在
常量字符串中或者字符数组中。而字符串常量适用于那些对它不做修改的字符串函数。
本博客总结了最常见的十五个相关库函数的功能或特点进行了简单分类:“求字符串长度”、“长度不受限制”、“长度受限制”、“字符串查找”、“字符操作”、“内存操作”,并在下文中逐个介绍。
目录
一、求字符串长度
1.1 strlen
-
基本介绍
size_t strlen ( const char * str );
[小结]
1.功能:统计"\0"之前的字符个数
2.头文件:<string.h>
3.返回类型:size_t,返回字符串个数。
4.参数类型:字符串/字符指针/字符串数组名
[ps]
1.参数指向的字符串必须要以 '\0' 结束。一般情况下,字符串结尾会默认携带'\0',但仍需留心。
2.函数的返回值为size_t,是无符号的。
3.strlen 是求字符串长度的,求出的长度是不可能为负数的,
所以返回类型设置为size_t合情合理。
//[补]size_t即为无符号的整型,在64位中是unsigned long int,
// 在32位中是unsigned int。可以大致认为size_t是为了简化
// 类型名方便使用而存在的。
//typedef unsigned int size_t
//我们可以通过下面这个情景更好地理解strlen的返回类型:
int main()
{
if (strlen("abc") - strlen("abcdef") > 0)
//理论上,strlen("abc")的结果为3,strlen("abcdef")”的结果为6
//strlen("abc") - strlen("abcdef")的结果应该为3-6=-3
{
printf(">\n");
}
else
{
printf("<=\n");
}
return 0;
}
//然而,实际打印结果:>
//这既说明了strlen的返回类型的特点,也说明了,strlen在某些情境下是有利有弊的。
[补]为什么参数类型中使用了const?
常量字符串在表达式中的值其实不是字符串的内容,而是字符串中首字符的地址。
例如:
char* p="abcdef";
其中,"abcdef"是一个常量字符串,p是一个字符指针变量。
常量字符串是不可被修改的,赋值给p后,理论上p也是不应该被修改的。
若后续继续修改p的值,虽然在语法上是合理的,但实际运行中编译器会报错,程序会崩溃。
且,许多编译器会在定义变量之初提示添加上“const”。
故对于一个字符指针,定义方式最好为:
const char* p="abcdef";
-
模拟实现
//模拟strlen的关键要点在于:
//1.字符串长度即字符串中"\0"之前的所有字符的个数
//2.strlen的具体功能是,统计"\0"之前的字符个数
#include<stdio.h>
#include<string.h>
#include <assert.h>
//法1:
//计数器
int my_strlen1(const char* str)
//返回类型为int,参数类型为字符指针
{
assert(str != NULL);
//暴力检查str是否为空。
//assert函数可以帮助快速定位bug代码所在的位置,头文件为#include <assert.h>,
//表达式为false时则报错,并提示bug代码的所在位置。
int count = 0;
//此变量用于计算字符个数
while (*str != '\0')
//当str指针的当前位置为'\0'时,循环结束
{
count++;
//若str指针当前位置的字符不为'\0',则说明此处有字符,故使count自增1
str++;
//str指针“++”并配合循环,从头往后一个字符一个字符地遍历字符串
}
return count;
//最终循环结束,此时的count即为字符串长度
}
//法2:
//递归实现
int my_strlen2(const char* str)
{
assert(str != NULL);
if (*str != '\0')
return 1 + my_strlen2(str + 1);
//若str指针当前位置不为'\0',则使str指针指向下一个字符,继续递归调用my_strlen2函数,
//并将递归返回值+1
else
return 0;
}
//法3:
//指针 - 指针
int my_strlen3(const char* str)
{
const char* start = str;
//先让start将字符串的首字符地址记下来
assert(str != NULL);
while (*str)
{
str++;
}
//将str指针指向字符串的末尾
return str - start;
//此时“str - start”的结果即为字符串长度
}
int main()
{
char arr[] = "abcdef";
// int len = my_strlen1(arr);
// int len = my_strlen2(arr);
int len = my_strlen3(arr);
printf("%d\n", len);
return 0;
}
二、长度不受限制
2.1 strcpy
-
基本介绍
char* strcpy(char * destination, const char * source );
[小结]
1.功能:将源字符串复制至目标空间中
2.头文件:<string.h>
3.返回类型:char*,返回目标空间的起始地址。
4.参数类型:目标空间(地址)、需拷贝的源字符串
[ps]
1.源字符串须以"\0"结尾,且源字符串中的 '\0' 会拷贝到目标空间。
2.目标空间必须足够大,以确保能存放源字符串;
3.目标空间必须可变(常量字符串不可变,一般使用数组)
例:
//int main()
//{
// char arr1[20] = "";
// //char*p="qwerasdfgzxcvzzz";
// char arr2[] = "hello world";
// strcpy(arr1,arr2);//足够大的字符数组为目标空间,可运行
// //strcpy(p,arr2);//常量字符串不可修改,为目标空间时程序崩溃
// printf("%s\n", arr1);
//
// return 0;
//}
-
模拟实现
//模拟实现strcpy的关键要点在于:
//将源字符串中的字符逐个拷贝至目标空间中,包括'\0'
#include<stdio.h>
#include<string.h>
#include <assert.h>
char* my_strcpy(char* dest, const char* src)
//返回类型为目标空间(字符指针),参数分别为目标空间、源字符串
{
char* ret = dest;
//先让ret将目标空间给记下来
assert(dest && src);
//暴力检查dest和src中任意一个是否为空
while (*dest++ = *src++)
//循环开始前,dest指针未赋值,src指针指向字符串中首字符的地址
//将src指针当前所指位置的字符传值给dest指针当前所指位置
//本次传值结束后,使src指针和dest分别自增1,指向下一个字符的位置
//当src指针指向'\0'时(即src指针已指向末尾),表达式的结果为'\0'(即为false),循环结束
//循环结束前,'\0'依然会被传给dest指针所指位置
{
;
}
return ret;
//循环结束,此时ret所指的目标空间已拷贝好源字符串
}
int main()
{
char arr1[20] = "";
char arr2[] = "hello bit";
my_strcpy(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
2.2 strcat
-
基本介绍
char* strcat(char* destination, const char* source);
[小结]
1.头文件:<string.h>
2.功能:将源字符串追加到目标空间的数据之后
3.参数类型:目标空间(地址)、追加的源字符串
4.返回类型:char*,返回目标空间的起始地址。
[ps]
1.源字符串必须以 '\0' 结尾,且目标空间内必须有'\0'。
2.目标空间必须足够的大,能容纳下源字符串的内容,且必须可修改。
3.当源字符串自己给自己追加时,源字符串也即目标空间,
目标空间中'\0'会被覆盖,最终会使程序陷入死循环。
故万不可给自己追加。
例:
//int main()
//{
// char arr[20] = "hello ";
// strcat(arr, "world");
// printf("%s\n", arr);
//
// return 0;
//}
-
模拟实现
//模拟实现strcat的关键要点在于:
//1.找到目标字符串中'\0'的位置
//2.将源字符串的字符从'\0'处开始逐个追加(要覆盖'\0')
//3.最终返回目标字符串的起始地址
char* my_strcat(char* dest, const char* src)
{
assert(dest && src);
//记录dest的起始位置
char* ret = dest;
//step1. 找目标空间的\0
while (*dest)//*dest!='\0'
{
dest++;
}
//step2. 从'\0'处开始追加
while (*dest++ = *src++)
{
;
}
return ret;
}
2.3 strcmp
-
基本介绍
int strcmp ( const char * str1, const char * str2 );
[小结]
1.头文件:<string.h>
2.功能:比较两个字符串的大小。
按字符串从头到尾的顺序依次比较,
比较两字符串相应位置上字符的ASCII码值。
3.返回类型:int
第一个字符串大于第二个字符串,则返回大于0的数字
第一个字符串等于第二个字符串,则返回0
第一个字符串小于第二个字符串,则返回小于0的数字
4.参数类型:字符串或字符串数组名
例:
//int main()
//{
// char arr1[] = "abq";
// char arr2[] = "abcdef";
// int ret = strcmp(arr1, arr2);
// //VS环境下:
// //> 返回 1
// //= 返回 0
// //< 返回 -1
//
// printf("%d\n", ret);
//
// return 0;
//}
-
模拟实现
//模拟实现strcmp的关键要点在于:
//1.若两字符串前半部分相同怎么办?
// - 两字符串相应位置上字符相等,则跳过到下一个字符的位置,直到相应位置上字符不相等
//2.进行比较,大于则返回大于0的值,小于则返回小于0的值,等于则返回0
int my_strcmp(const char* str1, const char* str2)
{
//确保str1和str2不是空指针
assert(str1 && str2);
//比较两个字符串相应位置上的字符大小,若相同则跳到下一位比较
while (*str1 == *str2)
{
if (*str1 == '\0')
return 0;
str1++;
str2++;
}
//循环结束,str1和str2指向相应位置上不相同的字符
//开始比较
if (*str1 > *str2)
return 1;
else
return -1;
//也可以:
//return *str1 - *str2;
}
//验证函数是否符合预期
int main()
{
char arr1[] = "abq";
char arr2[] = "abcdef";
int ret = my_strcmp(arr1, arr2);
if (ret>0)
printf("arr1>arr2\n");
printf("%d\n", ret);
return 0;
}
[小结] 以上字符串函数处理的长度不受限制,均以'\0'为运行的终止标志,故函数本身并不安全,使用需谨慎。没有“#define _CRT_SECURE_NO_WARNINGS”时,编译器报错。
三、长度受限制
3.1 strncpy
-
基本介绍
char* strncpy(char* destination, const char* source, size_t num);
[小结]
1.头文件:<string.h>
2.功能:拷贝num个字符从源字符串到目标空间
3.参数类型:目标空间、源字符串、拷贝字符个数
4.返回类型:char*,返回目标空间的起始地址。
(类似于改进版strcpy)
[ps]
如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到第num个。
例:
//int main()
//{
// char arr1[20] = "xxxxxxx";
// strncpy(arr1, "abcdef", 3);//拷贝结果为abcxxx,并不拷贝'\0'
// printf("%s\n", arr1);
// return 0;
//}
//
//int main()
//{
// char arr1[20] = "xxxxxxxxxxxxxx";
// strncpy(arr1, "abcdef", 10);//从arr1[5]后全拷贝'\0'
// printf("%s\n", arr1);
// return 0;
//}
3.2 strncat
-
基本介绍
char * strncat ( char * destination, const char * source, size_t num );
[小结]
1.头文件:<string.h>
2.功能:追加num个字符从源字符串到目标空间
3.参数类型:目标空间、源字符串、追加字符个数
4.返回类型:char*,返回目标空间的起始地址。
(类似于改进版strcat)
[ps]
字符串追加完后,会在末尾添加\0(保证追加后仍为字符串)。
例:
//int main()
//{
// char arr1[20] = "abc";
// strncat(arr1, "defqwer", 4);//拷贝结果为abcdefq\0\0...
// printf("%s\n", arr1);
//
// return 0;
//}
//
//int main()
//{
// char arr1[20] = "abc\0xxxxxxxx";
// strncat(arr1, "defqwer", 4);//拷贝结果为abcdefq\0xxx...
// printf("%s\n", arr1);
//
// return 0;
//}
3.3 strncmp
-
基本介绍
int strncmp ( const char * str1, const char * str2, size_t num );
[小结]
1.头文件:<string.h>
2.功能:比较限定长度内的两个字符串的大小。
按字符串从头到尾的顺序依次比较,
比较两字符串相应位置上字符的ASCII码值。
3.参数类型:字符串或字符串数组名、比较字符个数。
4.返回类型:int
第一个字符串大于第二个字符串,则返回大于0的数字
第一个字符串等于第二个字符串,则返回0
第一个字符串小于第二个字符串,则返回小于0的数字
(类似于改进版strcmp)
例:
//int main()
//{
// char* p1 = "abcdef";
// char* p2 = "abcqwer";
// int ret = strncmp(p1, p2, 4);
// printf("%d\n", ret);
// return 0;
//}
四、字符串查找
4.1 strstr
-
基本介绍
char * strstr ( const char *str1, const char * str2);
[小结]
1.头文件:<string.h>
2.功能:在目标字符串中找源字符串
3.参数类型:目标字符串、源字符串
4.返回类型:char*,返回源字符串在目标字符串中第一次出现的位置
[ps]
若源字符串在目标字符串中不存在,则返回空指针
例:
int main()
//{
// char arr1[] = "abcdcef";
// char arr2[] = "bc";
// char* ret = strstr(arr1, arr2);
//
// if (ret == NULL)
// {
// printf("找不到\n");
// }
// else
// {
// printf("%s\n", ret);
// }
//
// return 0;
//}
-
模拟实现
//模拟实现strstr的关键要点在于:
//1.怎么在目标字符串中匹配源字符串?
// - 暴力匹配。从目标字符串的起始字符开始,往后与源字符串逐个比对。
// 若匹配不成功,则跳至目标字符串的下一个字符,重新比对,
// 以此往复直到匹配成功或跳至目标字符串尾部。
//2.返回值的情况与源字符串有关。
// 若源字符串为'\0',则直接返回目标字符串的起始位置。
// 若匹配成功,则返回目标字符串的起始位置。
// 若匹配失败,则返回空指针。
char* my_strstr(const char* str1, const char* str2)
{
assert(str1 && str2);
//若源字符串str2为'\0',则直接返回目标字符串str1
if (*str2 == '\0')
{
return (char*)str1;
}
//定义两个新的常量字符串,替代str1、str2跑腿匹配
const char* s1 = NULL;
const char* s2 = NULL;
//记录str1的起始位置
const char* cp = str1;
//外部循环,控制cp在str1字符中的所指位置
while (*cp)//*cp!='\0'
{
//将cp反复赋给s1,相当于从头,或跳到下一个位置开始匹配str2
//str2反复赋给s2,相当于将str2从头开始,再一次与cp当前位置及其后的字符串匹配
s1 = cp;
s2 = str2;
//开始匹配
//相应位置上字符相等,则跳至下一个字符去匹配
while (*s1 !='\0' && *s2!='\0' && *s1 == *s2)
{
s1++;
s2++;
}
//当上面的内部循环结束,s2当前所指位置为'\0',则返回cp
//即返回源字符串在目标字符串中第一次出现的位置
if (*s2 == '\0')
{
return (char*)cp;
}
cp++;
}
//没有在str1中匹配到str2则返回NULL
return NULL;
}
4.2 strtok
-
基本介绍
char* strtok(char* str, const char* sep);
[小结]
1.头文件:<string.h>
2.功能:分割字符串
3.参数类型:目标字符串、分割位置
4.返回类型:char*,返回源字符串在目标字符串中第一次出现的位置
[ps]
1.若源字符串在目标字符串中不存在,则返回空指针
2.sep参数是个字符串,定义了用作分隔符的字符集合
第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标
记。
strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注:
strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容
并且可修改。)
strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串
中的位置。
strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标
记。
如果字符串中不存在更多的标记,则返回 NULL 指针。
例:
//int main()
//{
// char arr[] = "hahahehe@163.net";
// char* p = "@.";
// char buf[20] = { 0 };//"hahahehe\0163\0net"
// strcpy(buf, arr);
//
// char* ret = strtok(buf, p)
// printf("%s\n", ret);//hahahehe
//
// ret = strtok(NULL, p);
// printf("%s\n", ret);//163
//
// ret = strtok(NULL, p);
// printf("%s\n", ret);//net
//
// return 0;
//}
//int main()
//{
//
// char arr[] = "192#168.120.85";
// char* p = "#.";
// char buf[20] = { 0 };
// strcpy(buf, arr);
//
// char* ret = NULL;
// for (ret = strtok(buf, p); ret != NULL; ret=strtok(NULL, p))
// {
// printf("%s\n", ret);
// }
// return 0;
//}
五、字符操作
5.1 错误信息报告 - strerror
-
基本介绍
char * strerror ( int errnum );
[小结]
1.头文件:#include<string.h>
2.功能:将错误码翻译成错误信息
3.参数类型:错误码数字
4.返回类型:char*,返回错误信息(字符串)的起始地址。
[ps]
C语言的库函数在运行的时候,如果发生错误,就会将错误码存在一个变量中,这个变量是:errno。
使用errno时要添加头文件#include <errno.h>
例:
//int main()
//{
// printf("%s\n", strerror(0));//No error
// printf("%s\n", strerror(1));//Operation not permitted
// printf("%s\n", strerror(2));//No such file or directory
// printf("%s\n", strerror(3));//No such process
// printf("%s\n", strerror(4));//Interrupted function call
// printf("%s\n", strerror(5));//Input/output error
//
// return 0;
//}
//#include <errno.h>
//int main()
//{
//
// FILE* pf = fopen("test.txt", "r");//打开文件
// //[补]fopen函数
// // FILE* fopen(const char* filename,const char* mode);
// //如果打开文件成功,就返回一个有效的指针;
// //失败则返回一个NULL指针。
//
// if (pf == NULL)
// {
// //printf("%s\n", strerror(errno));
// perror("fopen");
// //[补]
// //perror函数能直接打印错误信息,且打印前会先打印自定义的信息
// //相当于:perror = printf + strerror
// return 1;
// }
// //读文件
// //关闭文件
// fclose(pf);
// pf = NULL;
//
// return 0;
//}
5.2 字符分类函数
例:
//int main()
//{
// int ret = isdigit('Q');//判断字符是否为数字
// printf("%d\n", ret);
//
// return 0;
//}
5.3 字符转换函数 - tolower 、toupper
例:
//#include <ctype.h>
//int main()
//{
// printf("%c\n", toupper('a'));//转大写
// printf("%c\n", tolower('A'));//转小写
//
// return 0;
//}
//将“I Have An Apple.”打印为全小写
//int main()
//{
// char arr[] = "I Have An Apple.";
// int i = 0;
// while (arr[i])
// {
// if (isupper(arr[i]))
// {
// arr[i] = tolower(arr[i]);
// }
// printf("%c", arr[i]);
// i++;
// }
// return 0;
//}
六、内存操作
6.1 memcpy
-
基本介绍
void * memcpy ( void * destination, const void * source, size_t num );
[小结]
1.头文件:#include <string.h>
2.功能:从source的位置开始向后复制num个字节的数据到destination的内存位置。
3.参数类型:目标空间、源空间、拷贝个数
4.返回类型:void*,返回目标空间
[ps]
1.这个函数在遇到 '\0' 的时候并不会停下来。
2.如果source和destination有任何的重叠,复制的结果都是未定义的。
例:
//int main()
//{
// int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
// int arr2[10] = { 0 };
// memcpy(arr2, arr1 + 2, 20);
// //VS调试监视中:arr2[0]=3,arr2[1]=4,arr2[2]=5,arr2[3]=6,arr2[4]=7,其余仍为0。
//
// return 0;
//}
-
模拟实现
//模拟实现memcpy的关键要点在于:
//1.怎样将源空间的数据逐个拷贝到目标空间相应位置?
// void*类型的指针怎么控制跳向下一个位置?
// - 强转指针成char*
//2.拷贝结束返回目标空间
void* my_memcpy(void* dest, const void* src, size_t num)
{
//定义ret来记录目标空间dest的起始位置
void* ret = dest;
assert(dest && src);
//拷贝源空间数据到目标空间
//前->后
while (num--)
{
//拷贝
*(char*)dest = *(char*)src;
//指针分别跳至下一个位置
dest = (char*)dest + 1;
src = (char*)src + 1;
}
//拷贝结束,返回目标空间
return ret;
}
//测试函数是否符合预期
void test1()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[10] = { 0 };
my_memcpy(arr2, arr1 + 2, 17);
}
void test2()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
my_memcpy(arr1 + 2, arr1, 20);//存在重叠
}
int main()
{
test1();
test2();
return 0;
}
6.2 memmove
-
基本介绍
void * memmove ( void * destination, const void * source, size_t num );
[小结]
1.头文件:#include <string.h>
2.功能:从source的位置开始向后复制num个字节的数据到destination的内存位置。
3.参数类型:目标空间、源空间、拷贝个数
4.返回类型:void*,返回目标空间
[ps]
和memcpy的差别就是,memmove函数处理的源内存块和目标内存块是可以重叠的,例如把自己拷贝到自己。
如果源空间和目标空间出现重叠,就得使用memmove函数处理。
-
模拟实现
//模拟实现memmove的关键要点在于:
//在目标空间和源空间重叠时,如何将源空间数据拷贝至目标空间?
// - 分类讨论:
// 目标空间在源空间之前,两者部分重叠;
// 目标空间在源空间之后,两者部分重叠;
// 目标空间和源空间的起始位置重叠,两个空间部分重叠或完全重叠
void* my_memmove(void* dest, const void* src, size_t num)
{
void* ret = dest;
assert(dest && src);
//dest在src的左边
if (dest < src)
{
//前-->后
//目标空间落在源空间前面(两者有部分重叠)
//从前向后拷贝源空间,目标空间从前向后接收
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
//dest与src重叠,或在src的右边
else
{
//后->前
//目标空间落在源空间后面(两者有部分重叠),或两者起始位置重叠,
//从后向前拷贝源空间,目标空间从后向前接收
while (num--)
{
*((char*)dest + num) = *((char*)src + num);
}
}
return ret;
}
//测试函数是否符合预期
void test3()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
my_memmove(arr1+2, arr1, 20);
}
void test4()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
memcpy(arr1, arr1+2, 20);
}
int main()
{
test3();
test4();
return 0;
}
[补]vs环境下,memcpy与memmove并无太大区别
6.3 memcmp
-
基本介绍
void* memset(void* ptr,int value,size_t num);
1.头文件:#include <string.h>
2.功能:以字节为单位来设置内存中的数据
3.参数类型:目标空间、数据、数据个数
4.返回类型:void*,返回目标空间
例:
//int main()
//{
// //char arr[] = "hello world";
// //memset(arr, 'x', 5);
// //printf("%s\n", arr);
// //memset(arr+6, 'y', 5);
// //printf("%s\n", arr);
//
// int arr[10] = { 0 };
// memset(arr, 1, 40);
// //不可行,memset以字节为单位挨个字节改变数据,
// //一个字节的空间放不下整型数据。
// //调试,内存中:arr的每一个元素为01 01 01 01
//
// return 0;
//}