此第一版是为 Lua 5.0 编写的。虽然在很大程度上仍然适用于后续版本,但有一些差异。
第四版针对 Lua 5.3,可在 亚马逊 和其他书店购买。
购买本书,您还可以帮助 支持 Lua 项目


27.2 – 字符串处理

当 C 函数从 Lua 接收字符串参数时,它必须遵守两个规则:在访问字符串时不将其从堆栈中弹出,并且绝不修改字符串。

当 C 函数需要创建字符串返回给 Lua 时,事情变得更加困难。现在,C 代码需要负责缓冲区分配/释放、缓冲区溢出等。不过,Lua API 提供了一些函数来帮助完成这些任务。

标准 API 为两个最基本的字符串操作提供支持:子字符串提取和字符串连接。要提取子字符串,请记住基本操作 lua_pushlstring 将字符串长度作为额外参数获取。因此,如果您想将字符串 s 的子字符串(从位置 ij(包括))传递给 Lua,您只需

    lua_pushlstring(L, s+i, j-i+1);
例如,假设您想要一个函数,根据给定的分隔符(单个字符)分割字符串,并返回一个包含子字符串的表。例如,调用
    split("hi,,there", ",")
应返回表 {"hi", "", "there"}。我们可以编写一个简单的实现,如下所示。它不需要额外的缓冲区,并且对其可以处理的字符串大小没有限制。
    static int l_split (lua_State *L) {
      const char *s = luaL_checkstring(L, 1);
      const char *sep = luaL_checkstring(L, 2);
      const char *e;
      int i = 1;
    
      lua_newtable(L);  /* result */
    
      /* repeat for each separator */
      while ((e = strchr(s, *sep)) != NULL) {
        lua_pushlstring(L, s, e-s);  /* push substring */
        lua_rawseti(L, -2, i++);
        s = e + 1;  /* skip separator */
      }
    
      /* push last substring */
      lua_pushstring(L, s);
      lua_rawseti(L, -2, i);
    
      return 1;  /* return the table */
    }

为了连接字符串,Lua 在其 API 中提供了一个特定函数,称为 lua_concat。它等同于 Lua 中的 .. 运算符:它将数字转换为字符串,并在必要时触发元方法。此外,它可以一次连接两个以上的字符串。调用 lua_concat(L, n) 将连接(并弹出)堆栈顶部的 n 个值,并将结果留在顶部。

另一个有用的函数是 lua_pushfstring

    const char *lua_pushfstring (lua_State *L,
                                 const char *fmt, ...);
它有点类似于 C 函数 sprintf,因为它根据格式字符串和一些额外参数创建字符串。但是,与 sprintf 不同,您无需提供缓冲区。Lua 会根据需要动态创建字符串。无需担心缓冲区溢出等问题。该函数将结果字符串压入堆栈并返回指向它的指针。目前,此函数仅接受指令 %%(用于字符 %´)、%s(用于字符串)、%d(用于整数)、%f(用于 Lua 数字,即双精度)和 %c(接受整数并将其格式化为字符)。它不接受任何选项(例如宽度或精度)。

当我们只想连接几个字符串时,lua_concatlua_pushfstring 都很有用。但是,如果我们需要连接多个字符串(或字符),那么逐个连接的方法效率很低,正如我们在 第 11.6 节 中所看到的。相反,我们可以使用辅助库提供的缓冲区工具。Auxlib 在两个级别实现这些缓冲区。第一级类似于 I/O 操作中的缓冲区:它在一个本地缓冲区中收集小字符串(或单个字符),并在缓冲区填满时将它们传递给 Lua(使用 lua_pushlstring)。第二级使用 lua_concat 和我们在 第 11.6 节 中看到的堆栈算法的一个变体,来连接多个缓冲区刷新结果。

为了更详细地描述 auxlib 中的缓冲区工具,让我们看一个简单的使用示例。下面的代码展示了 string.upper 的实现,直接来自文件 lstrlib.c

    static int str_upper (lua_State *L) {
      size_t l;
      size_t i;
      luaL_Buffer b;
      const char *s = luaL_checklstr(L, 1, &l);
      luaL_buffinit(L, &b);
      for (i=0; i<l; i++)
        luaL_putchar(&b, toupper((unsigned char)(s[i])));
      luaL_pushresult(&b);
      return 1;
    }
使用 auxlib 中的缓冲区的第一个步骤是声明一个类型为 luaL_Buffer 的变量,然后使用 luaL_buffinit 调用对其进行初始化。初始化后,缓冲区会保留状态 L 的副本,因此在调用其他操作缓冲区的函数时,我们不需要传递它。宏 luaL_putchar 将单个字符放入缓冲区。Auxlib 还提供了 luaL_addlstring,将具有显式长度的字符串放入缓冲区,以及 luaL_addstring,将以零结尾的字符串放入缓冲区。最后,luaL_pushresult 刷新缓冲区,并将最终字符串留在堆栈顶部。这些函数的原型如下
    void luaL_buffinit (lua_State *L, luaL_Buffer *B);
    void luaL_putchar (luaL_Buffer *B, char c);
    void luaL_addlstring (luaL_Buffer *B, const char *s,
                                          size_t l);
    void luaL_addstring (luaL_Buffer *B, const char *s);
    void luaL_pushresult (luaL_Buffer *B);

使用这些函数,我们不必担心缓冲区分配、溢出和其他此类细节。正如我们所看到的,连接算法非常高效。str_upper 函数可以毫无问题地处理大字符串(超过 1 MB)。

当您使用 auxlib 缓冲区时,您必须注意一个细节。当您将内容放入缓冲区时,它会在 Lua 堆栈中保留一些中间结果。因此,您不能假设堆栈顶部会保留在您开始使用缓冲区之前的状态。此外,尽管您可以在使用缓冲区时将堆栈用于其他任务(甚至构建另一个缓冲区),但每次访问缓冲区时,这些用途的 push/pop 计数都必须平衡。有一个明显的情况,这种限制过于严格,即当您想要将 Lua 返回的字符串放入缓冲区时。在这种情况下,您不能在将字符串添加到缓冲区之前将其弹出,因为您永远不应该在从堆栈中弹出字符串后使用它;但您也不能在弹出字符串之前将其添加到缓冲区,因为堆栈将处于错误的级别。换句话说,您不能执行类似这样的操作

    luaL_addstring(&b, lua_tostring(L, 1));   /* BAD CODE */
由于这是一个常见的情况,auxlib 提供了一个特殊函数,用于将栈顶的值添加到缓冲区中
    void luaL_addvalue (luaL_Buffer *B);
当然,如果栈顶的值不是字符串或数字,则调用此函数将出错。