目录
前言
C语言中对字符和字符串的处理很是频繁,但是C语言本身是没有字符串类型的,字符串通常放在常量字符串 中或者 字符数组 中。
字符串常量 适用于那些对它不做修改的字符串函数.
1. 字符串函数
1.1 长度不受限制的字符串函数
1.1.1 strlen
//返回类型:无符号整形,参数:字符串起始地址
size_t strlen ( const char * str );
- 求字符串长度
- 字符串已经 ‘\0’ 作为结束标志,strlen函数返回的是在字符串中 ‘\0’ 前面出现的字符个数(不包含 ‘\0’ )。
如果没有\0,strlen会一直向后寻找,直到找到\0,所以如果初始化不带\0,求出的是随机值。
- 参数指向的字符串必须要以 ‘\0’ 结束。
- 注意函数的返回值为size_t,是无符号的( 易错 )
用
strlen("abc") - strlen("abcde")
相减比较字符串长度是错误的,返回的是无符号整形,如果是个负数,将会被视为一个非常大的正数。
因此直接比较strlen("abc") > strlen("abcde")
或者把返回值强制类型转换为有符号整形都可以。
- 学会strlen函数的模拟实现
目前已经接触到了三种模拟实现strlen的方法:
- 计数器:
size_t my_strlen(const char* str)
{
assert(str);
int count = 0;
while (*str)
{
++count;
++str;
}
return count;
}
int main()
{
char str[] = "abcdef";
size_t len = my_strlen(str);
return 0;
}
- 递归法:
size_tmy_strlen(const char* str)
{
assert(str);
if (*str)
{
return 1 + my_strlen(str + 1);
}
return 0;
}
int main()
{
char str[] = "abcdef";
size_t len = my_strlen(str);
return 0;
}
- 指针法:
size_tmy_strlen(const char* str)
{
assert(str);
char* start = str;
while (*str)
{
++str;
}
return str - start;
}
int main()
{
char str[] = "abcdef";
size_t len = my_strlen(str);
return 0;
}
1.1.2 strcpy
//返回值:char*指针,参数:char*目标地址,char*源头地址
char* strcpy(char * destination, const char * source );
- 字符串拷贝
- 把源头地址所指向的空间数据拷贝到目标地址所指向的空间里去,包括\0
- 源字符串必须以 ‘\0’ 结束。
- 会将源字符串中的 ‘\0’ 拷贝到目标空间。
- 目标空间必须足够大,以确保能存放源字符串。
- 目标空间必须可变。
- 学会模拟实现。
char* my_strcpy(char* dest, const char* src)
{
assert(dest && src);
char* ret = dest;
while (*dest++ = *src++);
return ret;
}
int main()
{
char dest[20] = { 0 };
char src[] = "you are a joke";
char* ret = my_strcpy(dest, src);
puts(ret);
return 0;
}
1.1.3 strcat
//返回类型:char*指针,参数:char*目标地址,char*源头地址
char * strcat ( char * destination, const char * source );
- 字符串追加
- 源字符串必须以 ‘\0’ 结束。
- 目标空间必须有足够的大,能容纳下源字符串的内容。
- 目标空间必须可修改。
- 模拟实现strcat
char* my_strcat(char* dest, const char* src)
{
assert(dest && src);
char* ret = dest;
//先找到目标字符串\0的位置
while (*dest)
{
++dest;
}
//再拷贝
while (*dest++ = *src++);
return ret;
}
int main()
{
char dest[70] = "you are a joke,";
char src[] = "you are so dumb and you don't even know it";
int len = strlen(src);
char* ret = my_strcat(dest, src);
puts(ret);
return 0;
}
- 字符串自己给自己追加,如何?
不能,自己再给自己追加过程中会覆盖掉\0,然后陷入死循环。
1.1.4 strcmp
//返回值:整形,参数类型:要比较的两个字符串的起始地址
int strcmp ( const char * str1, const char * str2 );
- 比较字符串大小
分别拿str1和str2的第一个字符相比,相等就比较下一对,直到不相等或者到\0
- strcmp的返回值:
str1 > str2,返回大于0的数字
str1 = str2,返回0
str1 < str2,返回小于0的数字
- strcmp的模拟实现
int my_strcmp(const char* s1, const char* s2)
{
assert(s1 && s2);
while (*s1 && * s2 && *s1 == *s2)
{
++s1;
++s2;
}
return *s1 - *s2;
}
int main()
{
char str1[] = "abcdef";
char str2[] = "abcdef";
int ret = my_strcmp(str1, str2);
if (ret > 0)
puts(">");
else if (ret < 0)
puts("<");
else
puts("==");
return 0;
}
strcpy\strcat\strcmp都是长度不受限制的字符串函数,比如说strcpy是拷贝完\0,strcat是追加完\0等,并没有办法指定拷贝或者追加几个字符,比如给定的目标空间不够大等等,这种情况会带来一些潜在的问题。
因此C语言提供了几个长度受限制的字符串函数:
1.2 长度受限制的字符串函数
1.2.1 strncpy
//返回类型:char*指针,参数:前两个参数和strcpy相同
//只不过最后多了一个要拷贝的字节数
char * strncpy(char* destination, const char* source, size_t num);
- 拷贝num个字符从源字符串到目标空间。
- 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。
- strncpy的模拟实现
char* my_strncpy(char* dest, const char* src, int num)
{
assert(dest && src);
char* ret = dest;
while (num && (*dest++ = *src++))
{
--num;
}
while (num > 0)
{
*dest++ = 0;
--num;
}
return ret;
}
int main()
{
char str1[20] = "abcdef";
char str2[] = "hello world";
char* ret = my_strncpy(str1, str2, 5);
puts(ret);
return 0;
}
1.2.2 strncat
//返回类型:char*指针,参数:两个参数和strcat相同
//最后多了一个要追加的字节数
char* strncat (char* destination, const char* source, size_t num);
- 追加结束后,会自动补上一个\0以形成一个完整的字符串。
- 如果源字符串的长度小于num,则追加完源字符串之后,只会在目标的后边追加一个\0,不会追加多个0
- strncat的模拟实现
char* my_strncat(char* dest, const char* src, int num)
{
assert(dest && src);
char* ret = dest;
while (*dest)
{
++dest;
}
while (num && *src)
{
*dest++ = *src++;
--num;
}
*dest = 0;
return ret;
}
int main()
{
char str1[20] = "hello \0xxxxxxx";
char str2[] = "world";
char* ret = my_strncat(str1, str2, 5);
puts(ret);
return 0;
}
1.2.3 strncmp
//返回类型:整形,参数:两个参数和strcpy相同
//最后多了一个要比较的字节数
int strncmp(const char* str1, const char* str2, size_t num);
- 比较原理和strcmp相似
- 比较到出现另个字符不一样或者一个字符串结束或者num个字符全部比较完。
- strncmp的模拟实现
int my_strncmp(const char* str1, const char* str2, int num)
{
assert(str1 && str2);
while (num && *str1 && *str2 && (*str1 == *str2))
{
--num;
if (num)
{
++str1;
++str2;
}
}
return *str1 - *str2;
}
int main()
{
char str1[] = "abccdef";
char str2[] = "abcd";
int ret = my_strncmp(str1, str2, 5);
if (ret > 0)
puts(">");
else if (ret < 0)
puts("<");
else
puts("==");
return 0;
}
1.3 字符串函数
1.3.1 strstr
//返回类型:char*指针,参数:要查找字符串的首地址,子串的首地址
char * strstr ( const char *str1, const char * str2);
//查找字符子串的函数
- 如果str2在str1找到了str2这个子串,该函数就会返回子串的起始地址,找不到返回空指针NULL。
- strstr的模拟实现
char* my_strstr(const char* str1, const char* str2)
{
assert(str1 && str2);
while (*str1)
{
const char* tmpStr1 = str1;
const char* tmpStr2 = str2;
while (*tmpStr2 && *tmpStr1 == *tmpStr2)
{
++tmpStr1;
++tmpStr2;
}
if (*tmpStr2 == 0)
{
return (char*)str1;
}
++str1;
}
return NULL;
}
int main()
{
char str1[] = "abcdefg";
char str2[] = "efg";
char* ret = my_strstr(str1, str2);
if (ret == NULL)
puts("子串不存在");
else
puts(ret);
return 0;
}
实现原理:
1.3.2 strtok
///返回类型:char*指针,参数:待切割的字符串,分隔符字符串集合
char * strtok ( char * str, const char * sep );
简单理解就是,根据sep里的分隔符来切割字符串
- sep参数是个字符串,定义了用作分隔符的字符集合
"123&456|789"
,比如说要把这个字符串分割成三份,串里的&和|符号好像就已经把字符串分割了,所以就可以把&和|符号当作分隔符,放到sep分隔符字符串中,利用strtok来分割字符串。
-
第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记。
-
strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注:strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)
strtok找到分隔符标记时会把它用\0代替,
"123 \0 456|789"
并返回1的地址,
- strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
第一次调用strtok时,第一个参数为待切割的字符串地址。
- strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
之后调用strtok函数时,第一个参数传空指针,根据上一次查找到的\0开始继续向后寻找。
- 如果字符串中不存在更多的标记,则返回 NULL 指针。
这种写法非常冗余,是否可以利用循环呢?
因为只有第一次strtok函数不传空指针,后面都是传的空指针,因此可以利用for循环,for循环的初始化部分只会执行一次,可以完美匹配:
int main()
{
char str[] = "123&456|789";
const char sep[] = "&|";
char cp[20] = { 0 };
strcpy(cp, str);
for ( char* ret = strtok(cp, sep);
ret != NULL;
ret = strtok(NULL, sep) )
{
puts(ret);
}
return 0;
}
1.3.3 strerror
错误报告函数。
//返回类型:错误码的地址,参数:错误码
char * strerror ( int errnum );
//error - C语言设置的一个全局错误码存放的变量
//包含头文件:error.h
错误报告函数,返回错误码,所对应的错误信息。
C语言的库函数,在执行失败的时候都会设置错误码,如果想查看错误信息,这时就需要该函数:传过去错误码,然后函数把错误信息的字符串首地址返回来。
2. 字符函数分类
函数 | 如果它的参数符合下列条件返回真 |
---|---|
iscntrl | 任何控制字符 |
isspace | 空白字符:空格‘ ’,换页‘\f’,换行’\n’,回车‘\r’,制表符’\t’或者垂直制表符’\v’ |
isdigit | 十进制数字 0~9 |
isxdigit | 十六进制数字,包括所有十进制数字,小写字母a~f,大写字母A~F |
islower | 小写字母a~z |
isupper | 大写字母A~Z |
isalnum | 字母或者数字,a~z,A~Z,0~9 |
ispunct | 标点符号,任何不属于数字或者字母的图形字符(可打印) |
isgraph | 任何图形字符 |
isprint | 任何可打印字符,包括图形字符和空白字符 |
字符转换:
int tolower ( int c );
//转小写字母
int toupper ( int c );
//转大写字母
3. 内存函数
3.1 memcpy
内存拷贝,它不同于strcpy只能拷贝字符型,memcpy什么类型都可以使用。
//返回类型:void*类型的地址,参数:待拷贝目标的首地址,源数据首地址
//待拷贝的字节个数
void* memcpy (void* destination, const void* source, size_t num);
之所以返回类型和参数类型都设计为void类型的指针是因为,设计者在设计的时候并不知道未来使用者在使用的时候会用什么类型的数据,所以设计为void类型最合理,只需要强制类型转换为目标类型然后按照需要拷贝的字节数一个个拷贝即可。
- 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
- 这个函数在遇到 ‘\0’ 的时候并不会停下来。
- 如果source和destination有任何的重叠,复制的结果都是未定义的。
- memcpy的模拟实现
void* my_memcpy(void* dest, const void* src, size_t numofbit)
{
assert(dest && src);
void* ret = dest;
while (numofbit--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
int main()
{
int arr[10] = { 0 };
int arr1[] = { 0,1,2,3,4,5,6,7,8,9 };
int* ret = my_memcpy(arr, arr1, sizeof(arr1));
for (int i = 0; i < sizeof(arr1) / sizeof(arr1[0]); ++i)
{
printf("%d ", ret[i]);
}
return 0;
}
注:memcpy 负责拷贝两个独立空间的中的数据,同一个空间中不可以重叠拷贝。
3.2 memmove
该函数是专门用来实现重叠内存之间的数据拷贝 。
//返回类型参数与memcpy一模一样,用法也极其相似
void * memmove ( void * destination, const void * source, size_t num );
- 和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
- 如果源空间和目标空间出现重叠,就得使用memmove函数处理。
- memmove的模拟实现
void* my_memcpy(void* dest, const void* src, size_t numofbit)
{
assert(dest && src);
void* ret = dest;
if (dest < src)
{
while (numofbit--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
else
{
while (numofbit--)
{
*((char*)dest + numofbit) = *((char*)src + numofbit);
}
}
return ret;
}
int main()
{
int arr1[] = { 0,1,2,3,4,5,6,7,8,9 };
my_memcpy(arr1, arr1+2, 20);
for (int i = 0; i < sizeof(arr1) / sizeof(arr1[0]); ++i)
{
printf("%d ", arr1[i]);
}
return 0;
}
主要就是判断一下什么时候从前从后往前或者从前往后拷贝?
地址是随着下标的增长由低到高变化的,当dest地址大于src的时候,这时候从前往后拷贝会造成数据覆盖,因此只能选择从后向前拷,而当dest小于src或者其它情况都可以选择从前往后拷贝。
3.3 memcmp
//该函数与strcmp相似,只不过参数多了个比较num个字节
int memcmp( const void* ptr1, const void* ptr2, size_t num );
-
一对一对字节的比较,比较num个字节
-
该函数任意类型都可以进行比较
-
strcmp的返回值:
str1 > str2,返回大于0的数字
str1 = str2,返回0
str1 < str2,返回小于0的数字
3.4 memset
//返回类型:void*指针,参数:待初始化数组的首地址
//初始化内容,初始化字节数
void * memset ( void * ptr, int value, size_t num );
- memset是按照字节来初始化的,一个字节一个字节的操作