与字符串相关的基本操作包括strlen, strcpy, strcat, strcmp, strchr, strspn, strcspn, strpbrk, strstr, strtok等。在有些平台上,strcpy等使用较多的操作可能直接使用汇编代码编写,本文采用C语言来编写这些函数,然后说说与其相关的一些内容。
strlen
strlen这个函数用于计算C风格字符串的长度,而C风格字符串的结束标志是一个NULL字符,通过计算起始处到结束位置的字符数量即可求得长度。起初,我以为在这里采用末尾指针与起始指针相减的方法来计算长度,而不使用计数会提高效率,因为可以避免在每次比较后访问内存两次。然而,在我写了一段代码进行测试验证后,我发现我错了。反汇编后发现,后一种方法的汇编指令更短一点,自然执行时间更短。虽然访问一次内存需要的时间比执行一条指令要多,但是在有着多级缓存的CPU上,难以确定一次访问内存跟几条指令相当,不通过实际比较难以确定那种效率更高。可以肯定的是,使用汇编语言编写优化过的肯定效率更高。
size_t strlen(const char * str)
{
const char * i = str;
while (*i) i++;
return i - str;
}
strnlen
这个函数是strlen的扩展,可以通过第二个参数指定字符串的最大值,这样就能避免由于错误参数引起的一系列问题(可能会引起程序崩溃)。
size_t strnlen(const char *str, size_t maxlen)
{
const char *i = str;
while (*i && maxlen--) {
i++;
}
return (i - str);
}
strcpy
字符串拷贝是一个使用很频繁的函数,它可以使用rep movsb指令来实现,这里还是使用C语言来实现。值得注意的问题是源地址空间和目的地址空间有可能重叠,这样需要分情况进行出来。
char *strcpy(char *dst, const char *src)
{
char *o = dst;
if (src > dst) {
while (*src) *dst++ = *src++;
*dst = '\0';
} else {
const char *i = src;
while (*i++);
dst = dst + (i - src);
while (i >= src) *dst-- = *i--;
}
return o;
}
strncpy
strncpy就像strnlen一样,通过额外的参数指定了字符串的最大长度,可以避免一些通过strcpy实施的栈溢出攻击。
char *strncpy(char *dst, const char *src, size_t n)
{
char *o = dst;
if (src > dst) {
while (*src && n--) *dst++ = *src++;
*dst = '\0';
} else {
const char *i = src;
while (*i++ && n--);
dst = dst + (i - src);
while (i >= src) *dst-- = *i--;
}
return o;
}
strcat
这个函数是在目的字符串末尾追加字符,可以转换为strcpy操作。
char *strcat(char *dst, const char *src)
{
char *str = dst;
while (*str++);
strcpy(str, src);
return dst;
}
strncat
strncat也可转换为strncpy操作。
char *strncat(char *dst, const char *src, size_t n)
{
char *str = dst;
while (*str++);
strncpy(str, src, n);
return dst;
}
strcmp
strcmp返回两个字符串比较的结果,返回值是首个不同字符的差值,相同则返回0。
int strcmp(const char *str1, const char *str2)
{
const unsigned char *s1 = (const unsigned char *)str1;
const unsigned char *s2 = (const unsigned char *)str2;
int delta = 0;
while (*s1 || *s2) {
delta = *s2 - *s1;
if (delta)
return delta;
s1++;
s2++;
}
return 0;
}
strncmp
有额外参数指定最大长度的比较也是有必要的。
int strncmp(const char *str1, const char *str2, size_t n)
{
const unsigned char *s1 = (const unsigned char *)str1;
const unsigned char *s2 = (const unsigned char *)str2;
int delta = 0;
while ((*s1 || *s2) && n--) {
delta = *s2 - *s1;
if (delta)
return delta;
s1++;
s2++;
}
return 0;
}
strchr
strchr用于查找指定字符在字符串中首次出现的位置。
char *strchr(const char *str, int value)
{
char *i = (char *)str;
while (*i) {
if (*i == value)
return i;
i++;
}
return 0;
}
strrchr
strrchr用于反向查找指定字符首次出现的位置。
char *strrchr(const char *str, int value)
{
char *i = (char *)(str + strlen(str));
while (i != str) {
if (*i == value)
return i;
i--;
}
return 0;
}
strspn
strspn用于计算指定字符集中字符出现的次数,这里采用了位图来标记,虽然使用了32个字节的空间,但是时间复杂度下降到了O(n+m)。
size_t strspn(const char *str1, const char *str2)
{
size_t count;
unsigned char map[32];
for (count = 0; count < 32; count++)
map[count] = 0;
while (*str2) {
map[*str2 >> 3] |= (1 << (*str2 & 7));
str2++;
}
count = 0;
while (*str1) {
if (map[*str1 >> 3] & (1 << (*str1 & 7)))
count++;
str1++;
}
return count;
}
strcspn
类似于strspn,strcspn用于获得指定字符集中字符首次出现的偏移。
size_t strcspn(const char *str1, const char *str2)
{
size_t count;
unsigned char map[32];
const char *pstr = str1;
for (count = 0; count < 32; count++)
map[count] = 0;
while (*str2) {
map[*str2 >> 3] |= (1 << (*str2 & 7));
str2++;
}
while (*pstr) {
if (map[*pstr >> 3] & (1 << (*pstr & 7))) {
return pstr - str1;
}
pstr++;
}
return pstr - str1;
}
strpbrk
strpbrk用于获得指定字符集中字符首次出现的地址。
char *strpbrk(const char *str1, const char *str2)
{
size_t count;
unsigned char map[32];
char *pstr = (char *)str1;
for (count = 0; count < 32; count++)
map[count] = 0;
while (*str2) {
map[*str2 >> 3] |= (1 << (*str2 & 7));
str2++;
}
while (*pstr) {
if (map[*pstr >> 3] & (1 << (*pstr & 7))) {
return pstr;
}
pstr++;
}
return 0;
}
strstr
匹配一个模式串有很多中方法,包括朴素算法,RK,KMP,BM/BMG,Sunday,AC等方法。这里采用了比较简单的算法,首先查找模式串首字符出现的位置,然后比较模式串,最后通过一次或多次比较得到结果。
char *strstr(const char *str1, const char *str2)
{
char * str = (char *)str1;
size_t len = strlen(str2);
while ((str = strchr(str, *str2))) {
if (!strncmp(str, str2, len))
return str;
str++;
}
return 0;
}
strtok
strtok用于截断字符串,由于使用了一个静态变量,所以这个函数不是可重入的。
char *strtok(char *str, const char *delimiters)
{
static char *last;
if (!str) {
if (!last)
return 0;
str = last;
}
last = strpbrk(str, delimiters);
if (last)
*last++ = '\0';
return str;
}
memset
memset用于填充一块内存区域,将目标区域中的每一个字节填充为指定字符,一般用于清零。
void *memset(void *dst, int value, size_t n)
{
char * p = (char *)dst;
while (n--)
*p++ = (char)value;
return dst;
}
memcpy
memcpy用于拷贝一块内存区域,跟strcpy不同,memcpy不考虑源和目的重叠的问题。
void *memcpy(void *dst, const void *src, size_t n)
{
char *p = (char *)dst, *q = (char *)src;
while (n--) *p++ = *q++;
return dst;
}
memmove
memmove类似于strcpy,它不保证不修改源地址空间的数据。
void *memmove(void *dst, const void *src, size_t n)
{
char *p = (char *)dst, *q = (char *)src;
if (p > q) {
while (n--) *p++ = *q++;
} else {
p = p + n;
q = q + n;
while (n--) *(--p) = *(--q);
}
return dst;
}
memcmp
类似于strcmp,用于比较两块内存区域。
int memcmp(const void *str1, const void *str2, size_t n)
{
const unsigned char *s1 = (const unsigned char *)str1;
const unsigned char *s2 = (const unsigned char *)str2;
int delta = 0;
while ((*s1 || *s2) && n--) {
delta = *s2 - *s1;
if (delta)
return delta;
s1++;
s2++;
}
return 0;
}
memchr
memchr类似于strchr。
void *memchr(const void *ptr, int value, size_t n)
{
char *i = (char *)ptr;
while ((*i != value) && n--) {
i++;
}
return i;
}