C 语言处理字符串常用的方式

目录汇总:C 语言零基础入门教程

处理字符串的函数是特别丰富的惯用法资源。本文将探索几种最著名的惯用法,并利用它们编写 strlen 函数和 strcat 函数。当然,我们可能永远都不需要编写这两个函数,因为它们是标准函数库的一部分,但类似的函数还是有可能需要编写的。

本文使用的简洁风格是在许多 C 程序员中流行的风格。即使不准备在自己的程序中使用,也应该掌握这种风格,因为很可能会在其他程序员编写的程序中遇到。

在开始之前最后再说一点。如果你想在本文尝试自己编写 strlenstrcat,请修改函数的名字(比如,把 strlen 改成 my_strlen)。如 编写一个简单的 C 程序 中解释的那样,我们不可以编写与库函数同名的函数,即使不包含该函数所属的头也不行。事实上,所有以 str 和一个小写字母开头的名字都是保留的(以便在未来的 C 标准版本中往 <string.h> 头里加入函数)。

一、搜索字符串的结尾

许多字符串操作需要搜索字符串的结尾。strlen 函数就是一个重要的例子。下面的 strlen 函数搜索字符串参数的结尾,并且使用一个变量来跟踪字符串的长度:

1
2
3
4
5
6
7
8
size_t strlen(const char *s)
{
  size_t n;

  for (n = 0; *s != '\0'; s++)
    n++;
  return n;
}

指针 s 从左至右扫描整个字符串,变量 n 记录当前已经扫描的字符数量。当 s 最终指向一个空字符时,n 所包含的值就是字符串的长度。

现在看看是否能精简 strlen 函数的定义。首先,把 n 的初始化移到它的声明中:

1
2
3
4
5
6
7
8
size_t strlen(const char *s)
{
  size_t n = 0;

  for (; *s != '\0'; s++)
    n++;
  return n;
}

接下来注意到,条件 *s != '\0'*s != 0 是一样的,因为空字符的整数值就是 0。而测试 *s != 0 与测试 *s 是一样的,两者都在 *s 不为 0 时结果为真。这些发现引出 strlen 函数的又一个版本:

1
2
3
4
5
6
7
8
size_t strlen(const char *s)
{
  size_t n = 0;

  for (; *s; s++)
    n++;
  return n;
}

然而,就如同在 C 语言指针用于数组处理 中见到的那样,在同一个表达式中对 s 进行自增操作并且测试 *s 是可行的:

1
2
3
4
5
6
7
8
size_t strlen(const char *s)
{
  size_t n = 0;

   for (; *s++;)
     n++;
   return n;
}

while 语句替换 for 语句,可以得到如下版本的 strlen 函数:

1
2
3
4
5
6
7
8
size_t strlen(const char *s)
{
  size_t n = 0;

  while (*s++)
    n++;
  return n;
}

虽然前面已经对 strlen 函数进行了相当大的精简,但是可能仍没有提高它的运行速度。至少对于一些编译器来说下面的版本确实会运行得更快一些:

1
2
3
4
5
6
7
8
size_t strlen(const char *s)
{
  const char *p = s;

  while (*s)
    s++;
  return s - p;
}

这个版本的 strlen 函数通过定位空字符位置的方式来计算字符串的长度,然后用空字符的地址减去字符串中第一个字符的地址。运行速度的提升得益于不需要在 while 循环内部对 n 进行自增操作。请注意,在 p 的声明中出现了单词 const,如果没有它,编译器会注意到把 s 赋值给 p 会给 s 指向的字符串造成一定风险。

语句

1
2
3
4
[惯用法]

while (*s)
  s++;

和相关的

1
2
3
4
[惯用法]

while (*s++)
  ;

都是“搜索字符串结尾的空字符”的惯用法。第一个版本最终使 s 指向了空字符。第二个版本更加简洁,但是最后使 s 正好指向空字符后面的位置。

二、复制字符串

复制字符串是另一种常见操作。为了介绍 C 语言中的“字符串复制”惯用法,这里将开发 strcat 函数的两个版本。首先从直接但有些冗长的 strcat 函数写法开始:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
char *strcat(char *s1,  const char *s2)
{
  char *p = s1;

  while (*p != '\0')
    p++;
  while (*s2 != '\0') {
    *p = *s2;
    p++;
    s2++;
  }
  *p = '\0';
  return s1;
}

strcat 函数的这种写法采用了两步算法:(1) 确定字符串 s1 末尾空字符的位置,并且使指针 p 指向它;(2) 把字符串 s2 中的字符逐个复制到 p 所指向的位置。

函数中的第一个 while 语句实现了第(1)步。程序中先把 p 设定为指向 s1 的第一个字符。假设 s1 指向字符串 "abc",则有下图:

字符串

接着 p 开始自增直到指向空字符为止。循环终止时,p 指向空字符:

字符串

第二个 while 语句实现了第(2)步。循环体把 s2 指向的一个字符复制到 p 指向的地方,接着 ps2 都进行自增。如果 s2 最初指向字符串 "def",第一次循环迭代之后各字符串如下所示:

字符串

s2 指向空字符时循环终止:

字符串

接下来,程序在 p 指向的位置放置空字符,然后 strcat 函数返回。

类似于对 strlen 函数的处理,也可以简化 strcat 函数的定义,得到下面的版本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
char *strcat(char *s1, const char *s2)
{
  char *p = s1;

  while (*p)
    p++;
  while (*p++ = *s2++)
    ;
  return s1;
}

改进的 strcat 函数的核心是“字符串复制”的惯用法:

1
2
3
4
[惯用法]

while (*p++ = *s2++)
  ;

如果忽略了两个 ++ 运算符,那么圆括号中的表达式会简化为普通的赋值:

1
*p = *s2

这个表达式把 s2 指向的字符复制到 p 所指向的地方。正是由于有了这两个 ++ 运算符,赋值之后 ps2 才进行了自增。重复执行此表达式所产生的效果就是把 s2 指向的一系列字符复制到 p 所指向的地方。

但是为什么会使循环终止呢?因为圆括号中的主要运算符是赋值运算符,所以 while 语句会测试赋值表达式的值,也就是测试复制的字符。除空字符以外的所有字符的测试结果都为真,因此,循环只有在复制空字符后才会终止。而且因为循环是在赋值之后终止,所以不需要单独用一条语句来在新字符串的末尾添加空字符。

请参阅

(完)

comments powered by Disqus