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;
}