C语言对于字符以及字符串的使用非常频繁,为了方便使用,创造C语言的前辈们为我们书写了很多可以调用的函数,在这篇博客将会详细的介绍这些函数
strlen函数是非常常用的字符串函数,使用方法也很简单,先到MSDN中查看一下他的参数。
参数是一个char类型的指针,其实就是让我们传字符串的地址进去,他的介绍也很简单,就是给字符串求长度,这里有个值得注意的地方,strlen函数的返回值是一个size_t,是无符号的,这是易错点,演示一下它的使用。
int main()
{
char a[] = "abcdefg";
int size = strlen(a);
printf("string length = %d \n", size);
return 0;
}
那么,字符串的长度为什么是7呢,是以什么为界限划分的呢?
其实,我们书写一个字符串的时候,如果字符数组的空间还有剩余(没空间是一种错误的写法,意味着字符串没有结束标志),他会在字符串的末尾添加一个’\0’,表示字符串到这就结束了,相同的,strlen函数就是基于这一点来判断字符串的长度:
可以看到,这个字符数组的大小是8,长度为7,数组的最后一个元素是’\0’。
学习C语言时,最重要的是理解原理,上手操作,所以这篇博客并不仅仅只是介绍字符串函数,还要教大家一步一步的模拟实现字符串函数:
1.理解原理
strlen函数的原理很简单,就是遍历你传输进去的字符串,找到’\0’停止,并记录’\0’的之前的字符个数。
2.函数参数
函数参数直接模拟MSDN中的参数就可以了。
3.返回值
返回值的类型是size_t,是无符号的,这点要特殊注意。
size_t my_strlen(const char *str)
{
assert(str);//首先确定str是一个非空的地址
int cnt = 0;//计算个数
while (*str != '\0')
{
cnt++;
str++;
}//当字符串元素不等于0时,计数器++,指针后移一位
return cnt;//返回长度
}
这种是最简单易懂的写法,还有一起常见的写法如指针法,递归法等这里不再一一赘述,需要的同学可以参考博客
strlen函数的三种写法
我们也可以测试一下我们写的函数对不对:
也可以多试几个例子,大家可以回头自己试。
对于strlen函数还有一点要强调的就是一定要注意’\0’,只有结尾有’\0’的字符串才能正常的求长度。
同样,看到一个我们不认识的函数,先到MSDN查一下:
这回参数是两个char * 类型的指针,一个带const,一个不带,返回值也是一个指针,再看介绍,拷贝一个字符串,可以推测,这个字符串函数的作用是那个带const的字符串拷贝到不带的那个字符串里面,至于是不是,我们可以实验一下:
可以看到函数成功的把b中储存的字符串覆盖到了a所在的空间中,那么这个函数的作用就清晰了,但是还有几个问题,需要解决一下,他是如何实现的?拷贝到哪里为止?拷贝的时候要不要带上’\0’?abcde后的fg是否被置空了?他们去哪里了?当遇到这些问题的时候,我们的思考就不能只浮于表面了,要深挖其中原理。
1.strcpy拷贝的是一个字符串,既然是字符串,那结尾的的标志就是’\0’,所以,拷贝到’\0’的位置就可以了,至于我原本的’\0’有没有拷贝过来,我们可以调试一下:
这时我们将b字符串拷贝到a的空间里面去,我们发现a[4]空间的值被赋成0了,也就是说,源字符串的’\0’被拷贝了过来,并且我们还发现一件事,a中原先有的fg’\0’并没有被删除,还在原先的空间里,所以这个函数只是单纯的把另一个字符串拷贝了过来,并没有改变目标字符串的其他位置。
2.要拷贝的字符串必须有结尾标志’\0’,如果没有这个0,我怎么知道这个字符串到哪里停止呢?
3.目标空间必须够大,不够大的话我按照步骤拷贝,超出的部分不就越界了吗?越界程序是会崩溃的。
4.目标空间必须是可变的不能是常量,例如:
由于’'‘abcdefg’'是一个常量字符串(为什么是一个常量在指针进阶提到过,其实就是先创建"abcdefg"放到静态区再把它首地址给a),存储在静态区,不能被改变,自然,程序就崩溃了。
可以看到,strcpy函数需要注意的地方有很多,但是只要我们理解其中原理,实践一下,记住也就不是事了
模拟实现
函数的返回类型是 char*,参数是char * 和const char*
假如我们的指针一位一位的后移,在后移的过程中把src所指向的值赋给det,那么strcpy函数就完成了
,具体代码应该怎么写呢?
char* my_strcpy(char* det, const char* src)
{
assert(det && src);//与操作,一假全假,用来判断存不存在空指针
char* s1 = det;//函数的返回值是原空间的起始地址,所以保存一下
while (*det++ = *src++)//赋值语句的值是此时被赋值变量的值
{
;
}
return s1;
}
这里有很巧妙的一步,while (*det++ = *src++)
,一条语句,多个作用,我们都知道赋值语句,这个语句做判断条件时,它的值是被赋值变量的值,例如int a = 3; int b = 4; a = b;
此时a = b语句是有值的,而且值就是 3 ,那么,在这里他的值就是 *det的值,这样,这个语句既起到了循环功能,赋值功能,还保证了一件事 ,那就是 src中的’\0’可以拷贝进去,因为赋值语句执行完毕后,while语句才进行判断,此时判断表达式值为0,也就说明已经拷贝完毕了,就可以退出循环了。
对于strcpy,我们还是要注意’\0’,要复制的字符串一定要有’\0’,目标空间一定要充足。
MSDN:
先看描述,追加一个字符串,再看返回值,是char*,应该是返回原空间的首地址,再看两个参数
一个char *,一个const char*
,那就是在char * 的结尾追加const类型的字符串喽,我们可以试一试。
虽然使用成功了,但是其中还有很多隐藏起来的问题,例如,如果追加的字符串没有’\0’,对被追加字符串的空间有没有要求,要是自己给自己追加呢?如果被追加函数在追加后仍剩余空间,那剩余空间的值会是什么?显然,这些问题就不止停留在使用的层面,我们开始探究他的原理,strcat是如何实现的?
1.既然我是一个字符串的追加函数,那我判断追加字符串到那里停止,当然是利用’\0’,如果没有’\0’,我就不知道要追加进去几个字符,可能会追加进去一些很奇怪的东西。
就像这里b字符串只有bbb,没有’\0’,这就导致了程序的错误。
2.被追加字符串的空间需要够大,这点很好理解,因为不够大,我追加进去的字符就越界了,会产生内存访问的错误。
3.一个看起来没必要,但是有可能犯的错误,被追加字符串必须是可修改的,不能是常量字符串。
4.如果被追加函数在追加后仍剩余空间,那他剩余空间的值会被赋成’\0’,所有剩余空间都是。
可以看到 6 7 8 号元素都被赋为’\0’了,而且从监视的窗口我们可以看到,追加是在被追加字符结尾的’\0’处就开始追加,然后还会把自带的’\0’,追加过来。
5.自己追加自己,会如何?首先知道这是一个错误的使用情况,具体为什么错误呢,需要深入了解函数的运行原理才能知道。
模拟实现:
函数的返回类型是 char*,参数是char * 和const char*
strcat函数的原理很简单,补充一下循环的终止条件,那就是arr2的’\0’处。同时细心的看官可以发现,strcat函数与strcpy函数有相似之处,只不过strcat函数是从目标函数的’\0’处开始。
char* my_strcat(char* det, const char* src)
{
assert(det && src);
char* s1 = det;
while (*det)//找到被追加字符串'\0'处
{
det++;
}
while (*det++ = *src++)
{
;
}
return s1;
}
可以看到,strcat的实现就比strcpy多了一步查找被追加字符’\0’的循环,在这里解释一下之前说的自己追加自己的问题,如果是按照我们这个方法实现,他会出现一种情况,无限追加。
可以看到,自己追加自己,直接把’/0’替掉了,由于追加函数和被追加函数都是自己,所以字符串的’\0’直接消失了,没有终止条件,就会一直持续循环,直到越界。
附上strcat函数的源文件
char * __cdecl strcat (
char * dst,
const char * src
)
{
char * cp = dst;
while( *cp )
cp++; /* find end of dst */
while((*cp++ = *src++) != '\0') ; /* Copy src to end of dst */
return( dst ); /* return dst */
}
这个函数很常见,就简单介绍一下,给两个字符串,然后比较大小,既然是比较大小,那么第一个字符串大于第二个字符串就返回正数,等于返回0,小于返回负数。
至于他是怎么比较的,这里解释一下,strcmp函数比较的不是字符串的长度,而是字符串的大小, 例如: char a [] = "abcd"; char b[] = "abcdefg" ;char c = "abcf";
,如果使用strcmp对这三个字符串进行比较,那会表明一种关系 a
这是为什么呢?原因是strcmp函数是一位一位的进行比较的,而且比较的是对应字符的ascll值,直到遇到不同的字符,或者两个字符同时遇见’\0’,就停止了,这三个字符串前三个abc都是相同的,直到第四个字符 ,’ d ‘<’ f ',所以b < c,a < c, 又因为 ’ \0 ‘<’ e '所以a < b。
模拟实现:
原理很简单,就是一个元素一个元素的比较,找到不同的,或者都遇到了’0’停止,函数返回值是一个int,两个参数都是const char *,
int my_strcmp(const char* s1, const char* s2)
{
assert(s1 && s2);//先确定s1和s2不是空指针
while (*s1 == *s2 && *s1)//如果相等就后移,如果相等且都等于'\0'直接返回0
{
s1++;
s2++;
}
int ret = *s1 - *s2;
return ((-ret < 0)) - (ret < 0);//后面有解释
}
另附strcmp源代码
int __cdecl strcmp (
const char * src,
const char * dst
)
{
int ret = 0 ;
while((ret = *(unsigned char *)src - *(unsigned char *)dst) == 0 && *dst)
{
++src, ++dst;
}
return ((-ret) < 0) - (ret < 0); // (if positive) - (if negative) generates branchless code
}
这个 return ((-ret) < 0) - (ret < 0);
稍微解释一下,第一个括号里面如果ret是正值,他为真,第二个括号为假,1 - 0 = 1,大于返回的就是1,如果是负值ret 第一个括号为假,第二个括号为真,假-真 0-1= -1,如果两个都为0,返回值自然就是0。这里第一个括号为什么不用ret > 0这个我暂时也没找到原因,有知道的兄弟可以评论回复一下。
长度受限制的字符串函数有 strncpy,strncat, strncmp,原理和之前的函数大体相同,只是有了一个长度的限制,这里我们着重对比和演示并会附上源代码。
可以看到,与strcpy相比,strncpy多了一个参数,参数类型是size_t,这个参数有什么作用呢?其实就是拷贝多少个字符串进去,你给它1,他就拷贝一个…这时要注意一件事,如果你是想覆盖目标空间的话,你输入的数字一定要比字符串的长度多一个,这一个,当然就是留给’\0’的。
假如不留空间:
就只是把前四个字符替换了,其他的没有变,因为没有把’\0’复制进去。
就算是大体相似的函数,也有值得我们注意的地方,如果我输入的数字大于拷贝字符串的长度,会怎么样?剩下的字符谁提供?如果大于目标空间的大小呢?
面对这些问题,我们调试走起:
可以看到,如果我们输入7,c是没有那么多字符的,那缺的字符找谁补呢?就找’\0’进行替补,那如果超出目标空间呢,毫无疑问,程序就崩溃了,越界访问了。
附源代码
char * __cdecl strncpy (
char * dest,
const char * source,
size_t count
)
{
char *start = dest;
while (count && (*dest++ = *source++) != '\0') /* copy string */
count--;
if (count) /* pad out with zeroes */
while (--count)
*dest++ = '\0';
return(start);
}
可以说和之前的代码很相似,就是多了一个参数做循环的一个条件。
又是多了一个参数 ,参数类型 size_t,限制位数的追加,整数是多少,就追加多少个字符进去,这里演示一下:
可以看到,我们输入的数字是2,他就只把两个字符追加了过去,由此我们还可以知道一件事,strncat函数自动在追加字符串的后面添加了一个’\0’,因为ab之后原本是没有’\0’的,而且strncat函数是从被追加函数’\0’处开始追加的,所以是函数给我们添加了一个’\0’进去。
还有一个问题,如果我们传入的数字大于追加字符串的长度,会发生什么呢?
可以看到,和原先没有什么差别,这是因为追加函数判断无论你输入再大的数,他到’\0’就会停止了,最多也就是把abcd录进去而已,这也是和strncpy函数的一点小小区别。
还有一件事,记不记得在说strcat函数的时候,我们自己追加自己,会出现无限追加的错误,那在strncat中会不会出现呢?不会出现了,这是因为覆盖不覆盖’\0’对我来说已经不重要了我到位数就会停止,而且我还会自己加上一个’\0’
附strncat的源代码:
char * __cdecl strncat (
char * front,
const char * back,
size_t count
)
{
char *start = front;
while (*front++)
;
front--;
while (count--)
if ((*front++ = *back++) == 0)
return(start);
*front = '\0';
return(start);
}
注意 *front = '\0';
这一步,就是在字符串的结尾追加一个’\0’,非常重要。
也是多了一个参数,那这个函数的意思就是对比受限制位数的字符大小,比如我输入1,就前1个元素,输入2,就前两个元素,演示一下:
看,我输入数字3时,他是相等的,因为字符的前三个都是abc,那如果变成4呢?
这时就出现了差异,他们是不同的,‘d’<‘f’,所以值是-1。其他的原理和之前的strcmp函数差不多,这里不再赘述。
附strncmp源代码:
int __cdecl strncmp
(
const char *first,
const char *last,
size_t count
)
{
size_t x = 0;
if (!count)
{
return 0;
}
/*
* This explicit guard needed to deal correctly with boundary
* cases: strings shorter than 4 bytes and strings longer than
* UINT_MAX-4 bytes .
*/
if( count >= 4 )
{
/* unroll by four */
for (; x < count-4; x+=4)
{
first+=4;
last +=4;
if (*(first-4) == 0 || *(first-4) != *(last-4))
{
return(*(unsigned char *)(first-4) - *(unsigned char *)(last-4));
}
if (*(first-3) == 0 || *(first-3) != *(last-3))
{
return(*(unsigned char *)(first-3) - *(unsigned char *)(last-3));
}
if (*(first-2) == 0 || *(first-2) != *(last-2))
{
return(*(unsigned char *)(first-2) - *(unsigned char *)(last-2));
}
if (*(first-1) == 0 || *(first-1) != *(last-1))
{
return(*(unsigned char *)(first-1) - *(unsigned char *)(last-1));
}
}
}
/* residual loop */
for (; x < count; x++)
{
if (*first == 0 || *first != *last)
{
return(*(unsigned char *)first - *(unsigned char *)last);
}
first+=1;
last+=1;
}
return 0;
}
长度受限制的字符串函数和不受限制的字符串函数已经介绍完毕,相信大家对字符串也有了一定的了解,希望大家在看博客之余,能够多多实践,动手练习,也希望能指点指点博主,在下一篇博客中,将会介绍其他三类函数。