【面试题分享】重现 string.h 库常用的函数
在嵌入式软件开发的面试中,编程题往往是考察候选人基本功和实际动手能力的重要环节。而在众多面试题目中,重现标准库中的常用函数,如 string.h
库中的那些函数,既能展示候选人的编程技巧,也能反映其对基础概念的掌握程度。程序实现字符串操作相关的常见函数,是面试中比较常见的笔试题。本文也将重点介绍如何实现 string.h
库中的常用函数,主要包括字符串复制、连接、比较、搜索、长度计算和内存操作。通过这些实例,不仅帮助读者更好地备战面试,也能加深对标准库函数内部实现的理解。
[!CAUTION]
本文并不解释
string.h
里所有的函数,只介绍一些较为常见或面试常考的函数
一、字符串复制
1. strcpy(复制字符串直到遇到 null 终止符)
strcpy
,即 string copy(字符串复制)的缩写,是把从 src
地址开始且含有 NULL
结束符的字符串复制到以 dest
开始的地址空间,函数原型声明如下:
char *strcpy(char *dest, const char *src);
如果要自己实现一个与之功能相同的函数,可以如下编码:
char *myStrcpy(char *dest, const char *src)
{
if (NULL == dest || NULL == src)
return dest;
char *temp = dest;
do {
*temp = *src;
temp++;
} while (*src++);
return dest;
}
上述代码中,首先要判断 src
和 dest
是不是空指针;其次用临时指针 temp
代替 dest
指针做边遍历,这样返回值时可以直接用 dest
指针。
[!NOTE]
为什么要用
do......while
,而不是while
呢?根据
strcpy
函数的功能,当复制完所有字符串后,还会再复制一个'\0'
(空字符)。如果使用while
循环,一旦判断到src
指针已经指到空字符,就会立刻结束遍历,并不会把空字符复制到目标地址。所以使用do......while
可以先复制再判断,确保空字符被复制。
面试中能现场写出上述的代码就算完成了,当然也可以使用下面的简化版本:
char *myStrcpy(char *dest, const char *src)
{
if (NULL == dest || NULL == src)
return dest;
char *temp = dest;
while (*temp++ = *src++);
return dest;
}
while (*temp++ = *src++);
这一句,使用了指针操作来遍历和复制字符。其中,*dest++ = *src++
语句做了以下操作:
*src
获取源字符串当前字符。*dest
存储该字符到目标字符串当前字符位置。- 然后两个指针分别自增,指向下一个字符位置。
[!NOTE]
为什么这里又用了
while
了呢?因为当
src
指向空字符时,会先复制给dest
,然后while
再判断*dest
的值是否为空字符。
2. strncpy(复制固定长度的字符串)
strncpy
函数(string copy with n)也是复制字符串的函数,只是多了一个指定长度,把 src
所指向的字符串中以 src
地址开始的前 n 个字节复制到 dest
所指的数组中,并返回被复制后的 dest
。函数原型声明如下:
char *strncpy(char *dest, const char *src, size_t n)
实现一个与之功能相同的函数,可以如下编码:
char *myStrncpy(char *dest, const char *src, size_t n)
{
if (NULL == dest || NULL == src || 0 == n)
return dest;
char *temp = dest;
while (n && (*temp++ = *src++))
n--;
while (n--)
*temp++ = '\0';
return dest;
}
代码前半部分与 strcpy
相同,后半部分的第一个 while
循环,判断 *src
是否为空字符、以及拷贝的字符数量是否指定的字符数之内。第二个 while
循环执行的前提是第一个 while
循环提前结束(即 src
中的字符数小于 n
),第二个 while
循环用于填充目标字符串的剩余部分为空字符。
[!NOTE]
为什么第一个
while
不写成while (n-- && (*temp++ = *src++))
,这样不是更简洁吗?有一种情况我们要考虑到,那就是当
src
指向的字符串数量大于 n 时,如果使用while (n-- && (*temp++ = *src++))
这种写法,假设 n 此时为 1,while
先判断 n 的值,发现不为零,于是继续执行(*temp = *src)
,然后再执行n--
、temp++
、src++
,之后 n 的值就已经为 0 了。这时循环开始下一轮,又判断了一次 n 的值,发现为零,结束while
循环,再执行n--
。这时就会出现,n 的值变成了 -1,第二个while
循环判断的是 n 的值不为零,就会进入循环,并执行n--
,n 再减一就会变成 -2,如此减下去,n 只会变成更小的负数,最后导致temp
越界,造成内存踩踏。
二、字符串连接
1. strcat(将一个字符串连接到另一个字符串的末尾)
strcat
是 string concatenation 的缩写。这个函数用于将一个字符串连接到另一个字符串的末尾。具体来说,strcat
函数将源字符串的内容复制到目标字符串的末尾,并自动添加一个 null 终止符来结束新的组合字符串。函数原型声明如下:
char *strcat(char *dest, const char *src);
实现一个与之功能相同的函数,可以如下编码:
char *myStrcat(char *dest, const char *src)
{
if (NULL == dest || NULL == src)
return dest;
char *temp = dest;
while (*temp)
temp++;
while (*temp++ = *src++);
return dest;
}
第一个 while
循环的作用是为了将 temp
指针移动到目标字符串的末尾(即空字符的位置),之后就是将源字符串的内容逐字符复制到目标字符串的末尾。由于大部分代码的功能与字符串复制的一样,这里就不过多赘述。
2. strncat(将指定长度的字符串连接到另一个字符串的末尾)
strncat
是 “string concatenate with n” 的缩写,全称是 “string concatenate with length limit”。这个函数用于将源字符串的指定长度的字符追加到目标字符串的末尾,并自动添加一个 null 终止符来结束新的组合字符串。函数原型声明如下:
char *strncat(char *dest, const char *src, size_t n);
实现一个与之功能相同的函数,可以如下编码:
char *myStrncat(char *dest, const char *src, size_t n)
{
if (NULL == dest || NULL == src || 0 == n)
return dest;
char *temp = dest;
while (*temp)
temp++;
while (n && (*temp++ = *src++))
n--;
while (n--)
*temp++ = '\0';
return dest;
}
代码具体描述与前面的 myStrncpy
和 myStrcat
相同,这里不展开说明。
三、字符串比较
1. strcmp(比较两个字符串的内容)
strcmp
是 “string compare” 的缩写。 这个函数用于比较两个字符串的字典顺序。它根据两个字符串的字符逐一比较,直到找到不同的字符或遇到终止的 null 字符。函数原型声明如下:
int strcmp(const char *str1, const char *str2);
strcmp
的返回值是整数类型,有三种情况,分别是 -1、0 和 1,以上三种情况均是在比较到不同字符时,将两个字符直接做差得到的结果,具体情况如下:
- 如果
str1
在str2
之前,则返回值为 -1。 - 如果
str1
等于str2
,则返回值为 0。 - 如果
str1
在str2
之后,则返回值为 1。
实现一个与之功能相同的函数,可以如下编码:
int myStrcmp(const char *str1, const char *str2)
{
if (str1 == str2)
return 0;
while (*str1 && (*str1 == *str2)) {
str1++;
str2++;
}
return ((*str1 - *str2) ? ((*str1 - *str2) > 0 ? 1 : -1) : 0);
}
上述代码中,先判断两个地址是否相同,如果是一样,那字符串也是一样的,直接返回 0 即可。while
循环中,除了判断 str1
当前所指的字符是否为空字符之外,还要判断 str1
和 str2
各自所指的字符是否相同,同时满足两个条件,两个指针各自递增偏移。
最后返回时,用三目运算符内嵌另一个三目运算符做一个差值判断,先判断是否为 0,如果是则返回 0,否则再判断是正数还是负数,正数返回 1,负数返回 -1。
[!NOTE]
如果两个参数中有一个传参传入了
NULL
,会导致段错误,为什么这个代码没有体现出解决方法?
strcmp
也是如此,只要参数中有且只有一个参数是NULL
,就会出现段错误。传入两个NULL
则返回 0,因此,我只是保留了原本strcmp
该有功能写了这个myStrcmp
。
2. strncmp(比较指定长度的两个字符串的内容)
strncmp
在 strcmp
基础上,加上了需要比较的长度,该函数用于比较两个字符串的前 n 个字符的字典顺序。它根据两个字符串的字符逐一比较,直到找到不同的字符、比较了指定的 n 个字符或遇到空字符。函数原型声明如下:
int strncmp(const char *str1, const char *str2, size_t n);
实现一个与之功能相同的函数,可以如下编码:
int myStrncmp(const char *str1, const char *str2, size_t n)
{
if (str1 == str2)
return 0;
while (n && (*str1 && (*str1 == *str2))) {
str1++;
str2++;
n--;
}
return ((*str1 - *str2) ? ((*str1 - *str2) > 0 ? 1 : -1) : 0);
}
四、字符串搜索
1. strchr(在字符串中查找第一个出现的字符)
strchr
是 “string character” 的缩写,全称是 “string character search”。这个函数用于在字符串中查找第一个出现的指定字符,并返回一个指向该字符的指针。函数原型声明如下:
char *strchr(const char *str, int c);
在被搜索的字符串 str
中,查找 c
指定的字符,虽传入的类型为 int
,但实际比较时会被转换为 char
。如果在字符串 str
中找到 c
,则返回一个指向字符串中第一个出现的字符 c
的指针;如果没有没有找到,则返回 NULL
。
实现一个与之功能相同的函数,可以如下编码:
char *myStrchr(const char *str, int c)
{
while (*str && (*str != (char)c))
str++;
if (*str == (char)c)
return (char *)str;
return NULL;
}
上述代码中,while
循环除了判断 *str
,同时还判断 *str
是否与 (char)c
是否匹配,所以退出循环就存在两个情况:一是 str
已经遍历结束,如果 c
是空字符(c
有可能是 '\0'
),则返回 str
当前所指的地址,否则返回 NULL
;二是在 str
遍历结束之前,找到了与 c
匹配的字符,返回 str
当前所指的地址。
[!NOTE]
为什么返回
str
要强转类型为char *
,直接返回不行吗?函数的返回类型是
char *
,而str
是const char *
类型,直接返回并没有什么问题,只是在编译时会警告类型不匹配,所以这里强转只是为了消除警告。
2. strrchr(在字符串中查找最后一个出现的字符)
strrchr
是 “string reverse character” 的缩写,全称是 “string reverse character search”。该函数用于在字符串中查找最后一次出现的指定字符,并返回一个指向该字符的指针。函数原型声明如下:
char *strrchr(const char *str, int c);
与 strchr
函数类似,如果在字符串 str
中找到 c
,则返回一个指向字符串中最后一个出现的字符 c
的指针;如果没有没有找到,则返回 NULL
。
实现一个与之功能相同的函数,可以如下编码:
char *myStrrchr(const char *str, int c)
{
const char *last = NULL;
while (*str) {
if (*str == (char)c) {
last = str;
}
str++;
}
if (*str == (char)c)
return (char *)str;
return (char *)last;
}
上述代码依然使用了 while
循环遍历的方法,在遍历的过程中查找相匹配的字符,找到字符后保存最新的地址到 last
指针并继续查找,一旦再次找到指定字符立刻更新 last
指针,直到字符串全部遍历结束。
3. strstr(在字符串中查找子字符串)
strstr
是 “string substring” 的缩写,全称是 “string substring search”。这个函数用于在字符串中查找第一次出现的子字符串,并返回一个指向该子字符串起始位置的指针。函数原型声明如下:
char *strstr(const char *haystack, const char *needle);
其中,haystack
是要搜索的主字符串,needle
是要查找的子字符串。如果 needle
是 haystack
的子字符串,则返回一个指向主字符串中第一次出现的子字符串的指针,否则返回 NULL
。
char *myStrstr(const char *haystack, const char *needle)
{
if (!*needle)
return (char *)haystack;
while (*haystack) {
if (*haystack == *needle) {
char *h = (char *)haystack;
char *n = (char *)needle;
while (*h && *n && (*h == *n)) {
h++;
n++;
}
if (!*n)
return (char *)haystack;
haystack++;
} else {
haystack++;
}
}
return NULL;
}
可以看出,strstr
函数的代码要远比前面提到的所有函数都要复杂,所以以下是代码的剖析:
- 空字符串检查:首先检查
needle
是否为空字符串(原strstr
函数也是如此),如果是,直接返回haystack
的指针。 - 逐字符比较:
- 外层
while (*haystack)
循环遍历haystack
字符串。 - 当出现第一个匹配的字符后,进入内层
while (*h && *n && *h == *n)
循环逐字符比较haystack
和needle
,直到字符不匹配或到达needle
末尾。
- 外层
- 匹配检查:如果
needle
的所有字符都匹配,即!*n
,返回haystack
当前的位置。 - 继续搜索:如果当前字符不匹配,
haystack
移动到下一个字符继续搜索。 - 未找到返回
NULL
:如果遍历完haystack
仍未找到needle
,返回NULL
。
五、字符串长度计算
1. strlen(计算字符串的长度)
strlen
是 “string length” 的缩写,该函数用于计算字符串的长度,即字符串中字符的数量(不包括终止空字符)。函数原型声明如下:
size_t strlen(const char *str);
实现一个与之功能相同的函数,可以如下编码:
size_t myStrlen(const char *str)
{
const char *temp = str;
while (*temp)
temp++;
return temp - str;
}
代码非常简短,这里就不过多讲解了。
2. strnlen(在指定数量范围内计算字符串的最大长度)
strnlen
是 “string length with n” 的缩写,全称是 “string length with length limit”。该函数用于计算字符串的长度,但最多检查指定的最大字符数。函数原型声明如下:
size_t strnlen(const char *str, size_t maxlen);
实现一个与之功能相同的函数,可以如下编码:
size_t myStrnlen(const char *str, size_t maxlen)
{
const char *temp = str;
size_t num = 0;
while (num < maxlen && *temp) {
temp++;
num++;
}
return num;
}
六、内存操作
1. memcpy(复制内存块的内容)
memcpy
是 “memory copy” 的缩写,该函数用于从源地址复制指定数量的字节到目标地址。函数原型声明如下:
void *memcpy(void *dest, const void *src, size_t n);
dest
是指向目标内存块的指针,src
是指向源内存块的指针,n
是要复制的字节数。最后是返回目标内存块 dest
的指针。
实现一个与之功能相同的函数,可以如下编码:
void *myMemcpy(void *dest, const void *src, size_t n)
{
unsigned char *d = (unsigned char *)dest;
const unsigned char *s = (const unsigned char *)src;
while (n--)
*d++ = *s++;
return dest;
}
这里要注意的就是类型问题,由于是内存复制,因此要强转为 unsigned char
类型。
2. memset(用指定字符填充内存块)
memset
是 “memory set” 的缩写,该函数用于将指定值填充到内存块中,通常用于初始化或重置内存。通常也管这个函数叫 “填充” 函数,函数原型声明如下:
void *memset(void *str, int c, size_t n);
str
是指向要填充的内存块的指针。c
是要设置的值,传递为 int
类型,填充的时候会转换为 unsigned char
类型。n
是要填充的字节数。最后是返回指向内存块 str
的指针。
实现一个与之功能相同的函数,可以如下编码:
void *myMemset(void *str, int c, size_t n)
{
unsigned char *s = (unsigned char *)str;
while (n--)
*s++ = (unsigned char)c;
return str;
}
附录
1. 英文惯用缩写
在函数的参数列表中有两个高频出现的参数——src 和 dest,其中 src 指源操作数,是 source 的缩写,dest 指目标操作数,是 destination 的缩写。
2. size_t
size_t
是一种用于表示对象大小或数组索引的无符号整数类型。在 C 和 C++ 标准库中,对于 size_t
的定义有所不同。在不同的编译器和平台上,size_t
可能通过一些中间文件间接定义,但主要头文件通常是 stddef.h
(C)和 cstddef
(C++)。
在 C 语言中,stddef.h
可能包含以下内容:
#ifndef _STDDEF_H
#define _STDDEF_H
typedef unsigned long size_t;
#endif // _STDDEF_H
在 C++ 中,cstddef
可能包含以下内容:
#ifndef _CSTDDEF_
#define _CSTDDEF_
#include <stddef.h>
namespace std {
using ::size_t;
}
#endif // _CSTDDEF_
3. 很多字符串函数明明不需要返回值,但为什么还是有返回值?
字符串函数返回值的设计有以下几个实用的理由,返回值的设计提供了一些附加的好处,使函数的使用更加灵活和方便。以下以 strcpy
函数为例进行说明。
-
链式调用:
strcpy
返回目标字符串的指针,这使得链式调用成为可能。链式调用允许多个字符串操作函数连续使用,从而简化代码。char dest[100]; strcpy(dest, "Hello, "); strcat(dest, "world!"); // 使用链式调用 strcat(strcpy(dest, "Hello, "), "world!");
-
更好的函数组合:返回目标字符串的指针使得
strcpy
可以与其他需要字符串指针作为输入的函数更好地组合在一起。例如,许多字符串处理函数(如strlen
)需要一个字符串指针作为参数。char dest[100]; size_t len = strlen(strcpy(dest, "Hello, world!"));
-
代码一致性和便利性:在 C 标准库中,许多字符串操作函数都返回与输入相关的指针。例如,
strcat
和strtok
都返回指向结果字符串的指针。strcpy
也遵循这个设计模式,使得整个库的设计更一致和直观。 -
调试和错误处理:尽管
strcpy
本身没有提供错误处理机制,但在某些情况下,返回值可以在调试时提供便利。例如,在检查链式调用的中间结果时,可以打印返回值来验证复制操作是否成功。char dest[100]; printf("Copied string: %s\n", strcpy(dest, "Hello, world!"));
-
兼容历史习惯:许多早期的 C 函数,包括
strcpy
,都是遵循 UNIX 函数设计习惯的。这种习惯通常会返回一个指向结果的指针。遵循这种习惯,使得库函数更容易被广泛接受和使用。