2023-05-20 17:02:21
 ChrisZZ imzhuo@foxmailcom
 Hompage https://github.com/zchrissirhcz
文章目录
- 1. doxygen 版本
- 2. QCString 类简介
- 3. qstr 系列函数浅析
- `qmemmove()`
- `qsnprintf`
- `qstrdup()`
- `qstrfree()`
- `qstrlen()`
- `qstrcpy()`
- `qstrncpy()`
- `qisempty()`
- `qstrcmp()`
- `qstrncmp()`
- `qisspace()`
- `qstricmp()`
- `qstrnicmp()`
 
 
 
 
 
1. doxygen 版本
本次使用的 doxygen 版本如下, 是 1.9.8 正式发布版本对应的 commit
$ git log
commit 5fded4215d4f9271fe92c940fc4532d4704f5be1 (HEAD -> master, upstream/master)
Author: Dimitri van Heesch <doxygen@gmail.com>
Date:   Thu May 18 22:14:30 2023 +0200
    bump version to 1.9.8 for development
2. QCString 类简介
QCString 是一个有意思的名字:
- Q: 代表 Qt, QCString 是 Qt 中的一个类, 而 Qt 广泛流传使用, doxygen 中的 QCString 是基于 std::string 的重新实现, 接口不变
- C: 代表 C 语言, CString 意思是 C 字符串, 以 \0作为结束标识
- String: 代表字符串
QCString 类位的实现, 位于 src/qcstring.h.
下面对 QCString 类的每个函数进行简要分析。函数较多,按数量划分为4部分。
- part1: C函数, 例如 qisempty(),qstrlen()等
- part2: QString类的成员函数
本文给出 part1 的解读。
3. qstr 系列函数浅析

这部分包括很简单的函数, 以 inline() 方式在 qcstring.h 实现; 也包括稍微复杂的函数, 在 qcstring.cpp 中实现。
按 qcstring.h 中出现的顺序,逐一简介。
qmemmove()
 
void *qmemmove( void *dst, const void *src, size_t len );
C 库函数 memmove 的重新实现, 功能是内存内存拷贝, 注意 src 和 dst 允许有重叠区域:
- 如果 dst 的内存位置比 src 大, 则 dst 的最后一个元素肯定不会被 overlap, 因此现往 dst 的最后一个元素拷贝, 于是从后往前逐个字符拷贝
- 反之, 如果 dst 的内存位置比 src 小, 则 dst 的第一个元素不会被 overlap, 因此先往 dst 的第一个元素拷贝, 于是从前往后得逐个字符拷贝
void *qmemmove( void *dst, const void *src, size_t len )
{
  char *d;
  const char *s;
  if ( dst > src ) {
    d = static_cast<char *>(dst) + len - 1;
    s = static_cast<const char *>(src) + len - 1;
    while ( len-- )
      *d-- = *s--;
  }
  else if ( dst < src ) {
    d = static_cast<char *>(dst);
    s = static_cast<const char *>(src);
    while ( len-- )
      *d++ = *s++;
  }
  return dst;
}
qsnprintf
 
此函数直接偷懒,用 snprintf 或 _snprintf
#if defined(_OS_WIN32_)
#define qsnprintf _snprintf
#else
#define qsnprintf snprintf
#endif
整个工程里没找到 _OS_WIN32_ 宏, 看来代码年久失修, 需要清理了。
qstrdup()
 
qstrdup() 功能是字符串拷贝,将输入的字符串的内容, 原样复制一份到新的内存中,返回这块内存。细节上要注意:
- 如果给的输入字符串,本身是空指针, 那么返回空指针,不涉及内存申请
- 如果输入字符串非空, 则申请的内存的大小, 等于输入字符串的 strlen 结果再加1, 加1用于填充 \0
- 内存释放: 由调用者释放, 调用 qstfree().
代码实现如下:
//! Returns a copy of a string \a s.
//! Note that memory is passed to the caller, use qstrfree() to release.
char *qstrdup( const char *s );
char *qstrdup( const char *str )
{
  if ( !str )
    return 0;
  char *dst = new char[qstrlen(str)+1];
  return strcpy( dst, str );
}
qstrfree()
 
qstfree() 功能是释放内存, 准确是是释放字符串内存, 因为字符串内存我们是统一用 new 申请的数组, 因此此处用 delete[] 是配对的。代码实现如下:
//! Frees the memory allocated using qstrdup().
void qstrfree( const char *s );
void qstrfree( const char *str )
{
  delete[](str);
}
qstrlen()
 
//! Returns the length of string \a str, or 0 if a null pointer is passed.
inline uint32_t qstrlen( const char *str )
{ return str ? static_cast<uint32_t>(strlen(str)) : 0; }
C语言标准库有一个函数 strlen(), 它获取字符串长度, 不过它的输入不能是空指针。在 cppferencen 上给出了一个参考实现:
// https://en.cppreference.com/w/cpp/string/byte/strlen
std::size_t strlen(const char* start) {
   // NB: no nullptr checking!
   const char* end = start;
   for( ; *end != '\0'; ++end)
      ;
   return end - start;
}
可以看到, 判断字符串结束的条件是 *end != '\0', 而如果 start=NULL, 则 *end 直接等于对 0地址做 dereference 操作, 也就是 invalid memory access 了。因此 qstrlen() 特判了这种情况: 当输入空指针,也当做是字符串长度为0,返回0。
qstrcpy()
 
C 语言标准库提供了字符串拷贝函数 strcpy(), 它拷贝输入字符串的内容到输出字符串, 包括终止符号 \0, 但是留了两个 UB(未定义行为):
 (https://en.cppreference.com/w/cpp/string/byte/strcpy)
- 存放拷贝结果的内存 dst 是用户提供的, 如果 dst 内存不够大, 行为是未定义的
- 如果输入 src 和输出 dst 两块内存有重叠, 那也是未定义行为
在 doxygen 的 qstrcpy() 中并没有解决这两个 UB 问题; 解决了另一个问题: 当输入 src 是空指针时, 返回空指针。
inline char *qstrcpy( char *dst, const char *src )
{ return src ? strcpy(dst, src) : nullptr; }
qstrncpy()
 
C语言标准库提供了 strncpy 函数(https://en.cppreference.com/w/cpp/string/byte/strncpy), 功能和限制如下:
- 拷贝最多 n 个字符, 终结字符 \0也会拷贝
- 如果拷贝了 n 个字符, 但是还没有拷贝到 \0, 那么拷贝结果就不包含终结符\0, 换言之后续如果用 strlen 操作这个结果, 将导致 UB
- 如果从 src 拷贝了所有字符(包括 \0)后, 拷贝的数量少于 n, 那么继续填充终结字符\0
- 如果输入内存和结果内存有重叠,行为是未定义的(UB)
相比之下, qstrnpy() 增加了两个特判:
- 输入字符串为空指针, 则返回空指针
- 如果 len 大于0, 则不管 strncpy 是否拷贝了 \0,qstrncpy都会把最后一个字符置为\0, 确保了后续使用 strlen 处理结果时不会出现 UB, 缺点是字符串内容可能少拷贝了一个字符, 并不能很好的发现。实现代码如下:
char *qstrncpy(char *dst,const char *src, size_t len);
char *qstrncpy( char *dst, const char *src, size_t len )
{
  if ( !src )
    return nullptr;
  strncpy( dst, src, len );
  if ( len > 0 )
    dst[len-1] = '\0';
  return dst;
}
qisempty()
 
C标准库没有类似的函数。 qisempty() 检查输入的字符串指针本身是否为空指针, 或者它的首个字符是否为0.
doxygen 原版实现:
inline bool qisempty( const char *s)
{ return s==0 || *s==0; }
个人认为更合理的实现,是使用 nullptr 和 \0, 提升一下可读性:
inline bool qisempty( const char *s)
{ return s==nullptr || *s=='\0'; }
qstrcmp()
 
C 标准库提供了 strcmp() 函数 (https://en.cppreference.com/w/cpp/string/byte/strcmp):
- 根据 lhs 减去 rhs 的结果的符号, 返回 -1, 0, 1 三种取值: 
  - 0: 相等
- -1: 小于
- 1: 大于
 
- 如果 lhs 或 rhs 不是以 \0结束的字符串, 则行为是未定义的(UB)
相比之下, qstrcmp() 提供了 str1 和 str2 是否为空指针的判断:
- 如果都为空指针, 则判断为相等
- 如果其中一个为空, 则认为空的较小
具体代码实现如下:
inline int qstrcmp( const char *str1, const char *str2 )
{ return (str1 && str2) ? strcmp(str1,str2) :     // both non-empty
         (qisempty(str1) && qisempty(str2)) ? 0 : // both empty
         qisempty(str1) ? -1 : 1;                 // one empty, other non-empty
}
qstrncmp()
 
C语言标准库提供了 strncmp() 函数 (https://en.cppreference.com/w/cpp/string/byte/strncmp), 用于比较两个字符串, 并且比较过程中最多不超过n个字符长度
- 返回结果仍然是用 lhs 减去 rhs, 等于0表示相等, -1表示小于, 1表示大于
- 如果比较过程中,超出了 lhs 或 rhs 的长度, 行为是未定义的
- 如果 lhs 或 rhs 是空指针, 行为是未定义的
相比之下, qstrncmp() 按照和 qstrcmp() 一样的策略, 特别判断了 str1 和 str2 是否为空指针:
- 如果都为空指针, 则判断为相等
- 如果其中一个为空, 则认为空的较小
inline int qstrncmp( const char *str1, const char *str2, size_t len )
{ return (str1 && str2) ? strncmp(str1,str2,len) :  // both non-empty
         (qisempty(str1) && qisempty(str2)) ? 0 :   // both empty
         qisempty(str1) ? -1 : 1;                   // one empty other non-empty
}
qisspace()
 
C标准库没提供类似的函数。 qisspace() 判断单个字符是否为空白字符:
- 空格
- 制表符
- 两种换行符: \r和\n
inline bool qisspace(char c)
{ return c==' ' || c=='\t' || c=='\n' || c=='\r'; }
qstricmp()
 
qstrimcp() 的 i 表示 case-insensitive, 大小写不敏感的比较。它的具体实现是,如果判断为大写字母则转为小写字母,然后对应位置比较。这就引入了 toLowerChar() 函数
inline char toLowerChar(char c)
{
  return c>='A' && c<='Z' ? c|0x20 : c;
}
其中 A 是65开始的字符, c|0x20 表示 c + 32, 32等于97-65, a 对应到97。有点炫技的意思。
再来看 qstrimp() 的实现,感觉有点草率, 虽然判断了 *s1 到达终结符 \0, 执行 break; 但没考虑 *s2 到达 \0 的情况。
int qstricmp( const char *str1, const char *str2 );
int qstricmp( const char *s1, const char *s2 )
{
    if ( !s1 || !s2 )
    {
      return s1 == s2 ? 0 : static_cast<int>(s2 - s1);
    }
    int res;
    char c;
    for ( ; !(res = ((c=toLowerChar(*s1)) - toLowerChar(*s2))); s1++, s2++ )
    {
      if ( !c )				// strings are equal
        break;
    }
    return res;
}
更合理的实现如下:
int qstricmp( const char *s1, const char *s2 )
{
  if ( !s1 || !s2 )
  {
    return s1 == s2 ? 0 : static_cast<int>(s2 - s1);
  }
  int res;
  char c1, c2;
  for ( ; ; s1++, s2++ )
  {
    c1 = toLowerChar(*s1);
    c2 = toLowerChar(*s2);
    res = c1 - c2;
    if ( res!=0 )
      break;
    if ( c1==0 ) {
      res = -1;
      break;
    }
    if ( c2==0 ) {
      res = 1;
      break;
    }
  }
  return res;
}
qstrnicmp()
 
和 qstricmp() 情况一样, doxygen 的原始实现也是有问题的, 没考虑第二个字符串的提前到达终结符 \0.
int qstrnicmp( const char *str1, const char *str2, size_t len );
int qstrnicmp( const char *s1, const char *s2, size_t len )
{
    if ( !s1 || !s2 )
    {
      return static_cast<int>(s2 - s1);
    }
    for ( ; len--; s1++, s2++ )
    {
        char c = toLowerChar(*s1);
        int res = c-toLowerChar(*s2);
	if ( res!=0 ) // strings are not equal
	    return res;
	if ( c==0 ) // strings are equal
	    break;
    }
    return 0;
}
合理的实现如下:
int qstrnicmp( const char *s1, const char *s2, size_t len )
{
  if ( !s1 || !s2 )
  {
    return static_cast<int>(s2 - s1);
  }
  for ( ; len--; s1++, s2++ )
  {
    char c1 = toLowerChar(*s1);
    char c2 = toLowerChar(*s2);
    int res = c1 - c2;
    if ( res!=0 ) // strings are not equal
      break;
    if ( c1==0 ) {
      res = -1;
      break;
    }
    if (c2!=0 ) {
      res = 1;
      break;
    }
  }
  return 0;
}



















