StringLib C: 为 C 添加字符串类型的库 






4.93/5 (33投票s)
定义字符串类型和字符串操作函数的 C 库

引言
StringLib C 是一个为 C99+(或支持 stdint.h 的 C89 实现)设计的库/函数集,它定义了 string 类型和几个与之配合工作的函数,可以轻松进行字符串操作、读写,而无需担心内存分配(这东西很吓人)。
所有动态分配都是自动完成的:新定义的 string 被视为一个空 string(但将其初始化为 STRING_NEW 更安全),而调用 string_delete(string*) 函数可以释放内存并将 string 再次变为空。
string 类型是一个没有可见成员的结构体,这可以防止用户直接修改它,但它仍然有一个明确定义的尺寸。
C11+ 附加功能(非必需)
该库会检查 _Generic() 关键字支持情况,并(如果存在)将其用于函数宏分配:此功能允许在许多函数中使用不同的参数(例如 string 指针而不是 string_literal/pointer_to_char)。
该头文件还会检查 _Atomic 和 _Thread_local 类型限定符的支持情况,从而启用锁定功能和线程安全操作。
如果不支持,这些选项会自动禁用,但您也可以通过宏单独禁用它们。
字符串类型如何工作
stringlib.h 中的 string 类型不是定义为结构体的指针,而是定义为一个没有成员(隐藏成员,俄罗斯黑客这次不会乱改我的代码了 [如果你是俄罗斯人,请忽略此评论])的实际结构体;这是通过使用 sizeof 关键字的匿名位域实现的。
typedef struct
#ifndef _STRINGLIB_OVERLOADING_WAIT_
{
    uintptr_t:8*sizeof(uintptr_t);
    size_t:8*sizeof(size_t);
    size_t:8*sizeof(size_t);
    uintptr_t:8*sizeof(uintptr_t);
    uintptr_t:8*sizeof(uintptr_t);
    //Thread lock
    size_t:8*sizeof(size_t);
    _Atomic volatile struct
    {
        volatile uintptr_t:8*sizeof(volatile uintptr_t);
        volatile uintptr_t:8*sizeof(volatile uintptr_t);
    };
}_string;
#if (!defined(string) && _STRINGLIB_DEFINE_) || _STRINGLIB_DEFINE_==2
#undef string
#define string _string
#endif // string
而假设的可访问结构体应该是
struct _string_accessible
{
    //actual string
    char *getString;
    //string size in bytes
    size_t getSize;
    //string size in characters (=size in bytes-1)
    size_t getSizeChars;
    //should both point to _STRINGLIB_ALLOCATION_TRUE_ when string is allocated
    void *stringAllocation;
    void *stringSignature;
    //Thread lock
    size_t locklevel;
    _Atomic volatile struct
    {
        volatile uintptr_t thread_id_pt1;
        volatile uintptr_t thread_id_pt2;
    } lock;
};
这使得该类型具有指向 char 的指针(实际 string)、两个表示 string 大小的 size_t 以及两个指向 void 的指针(用于函数内的动态分配)的给定大小,尽管它没有可见成员,这使得 string 类型在声明和调用时不通过 stringlib.h 函数传递或通过强制类型转换为指针读取其内存时都不可编辑,从而防止由于新手用户错误编辑而导致的内存分配失败(注意:string_getString(string*) 返回相应的 char 指针)。
将 char 指针作为第一个成员的选择允许通过简单地将 string 作为 printf、fwrite、fputs、memcpy 等函数的参数来写入 string;然而,这种操作是不被推荐的,我建议改为传递 string_getString(string*) 或使用专门的 string_print(string*, ...)、string_write(string*, ...)、etc. 函数(如果第二个参数为 false,则避免在末尾添加换行符)。
stringlib.c 文件声明了五个宏用于读写该类型成员中的每一个(+2 个其他附加宏用于锁定成员)。
#define _STRINGLIB_ACCESS_STRING_(STRING)\
    (*(*((char***)((_string*[]){(&(STRING))}))))
#define _STRINGLIB_ACCESS_SIZE_(STRING)\
    (*((size_t*)&(((uint8_t*)(&(STRING)))[sizeof(uintptr_t)])))
#define _STRINGLIB_ACCESS_SIZECHARS_(STRING)\
    (*((size_t*)&(((uint8_t*)(&(STRING)))[sizeof(uintptr_t)+sizeof(size_t)])))
#define _STRINGLIB_ACCESS_ALLOCATION_(STRING)\
    (*((void**)&(((uint8_t*)(&(STRING)))[sizeof(uintptr_t)+2*sizeof(size_t)])))
#define _STRINGLIB_ACCESS_SIGNATURE_(STRING)\
    (*((void**)&(((uint8_t*)(&(STRING)))[2*sizeof(uintptr_t)+2*sizeof(size_t)])))
在接下来的标题中,为了方便和提高可读性,string 类型将被称为假设的可访问结构体。
字符串分配如何工作
库中的每个函数都会通过 string_isAllocated(string*) 函数(用户无需使用)检查 String 分配;这种行为使得可以使用所有函数处理新声明的 string,产生错误的风险极低,因为如上所述,该 string 将被视为一个空的 "" string。
int (string_isAllocated)(_string *string_a)
{
    int ret;
    string_lock(string_a);
    ret=(string_a->stringAllocation == _STRINGLIB_ALLOCATION_TRUE_ &&\
	    string_a->stringSignature == _STRINGLIB_ALLOCATION_TRUE_ &&\
	    (string_a->getSize == string_a->getSizeChars+1));
    string_unlock(string_a);
    return ret;
}
string_isAllocated(string*) 函数的返回值随后在 string_init(string*) 函数内部(有时也在之前)使用(同样,用户无需使用),该函数将 string 设置为空 string,大小为 1 字节。
size_t (string_init)(_string *string_a)
{
    int stringMallocFail = 0;
    char *tempStrMalloc = NULL;
    string_lock(string_a);
    //frees string_a if already allocated
    if (string_isAllocated(*string_a))
    {
        free(string_a->getString);
        //unallocates string
        string_a->stringAllocation = _STRINGLIB_ALLOCATION_FALSE_;
        string_a->stringSignature = _STRINGLIB_ALLOCATION_FALSE_;
    }
    string_a->getString = NULL;
    //initializes string, if initialization fails ends function returning 0
    tempStrMalloc = (char*) malloc(1 * sizeof(char));
    while (tempStrMalloc == NULL)
    {
        tempStrMalloc = (char*) malloc(1 * sizeof(char));
        //should never fail in normal circumstances
        if (++stringMallocFail == _STRINGLIB_MAX_ALLOC_FAILS_)
		{string_unlock(string_a); 
         free(tempStrMalloc); printf("_string memory initialization failed\n"); return 0;};
    }
    string_a->getString = tempStrMalloc;
    string_a->getString[0] = '\0';
    string_a->getSize = 1;
    string_a->getSizeChars = 0;
    //allocates string
    string_a->stringAllocation = _STRINGLIB_ALLOCATION_TRUE_;
    string_a->stringSignature = _STRINGLIB_ALLOCATION_TRUE_;
    string_unlock(string_a);
    return 1;
}
要分配一个 string,它必须同时具有 `stringAllocation` 和 `stringSignature` 成员设置为 `_STRINGLIB_ALLOCATION_TRUE_`,并且 `getSize` 成员必须比 `getSizeChars` 大 1,这使得 string 在首次声明时几乎不可能被视为已初始化(在此之前世界末日也可能不会来临);然而,将新声明的 string 分配给 `STRING_NEW`(定义为空 string `((string){})`)是一个安全且推荐的做法(尤其适用于多线程操作),然后再将其传递给任何函数,以避免任何可能的分配错误;此赋值只能在编辑 string 之前完成,如果之后进行,将导致内存丢失;如果您想重新使用 string,只需将其通过任何库函数(如 string_set(string*) 或 string_read(string*))进行处理。
void (string_delete)(_string *string_a)
{
    string_lock(string_a);
    //frees string_a if already allocated
    if (string_isAllocated(*string_a))
    {
        free(string_a->getString);
        //unallocates string
        string_a->stringAllocation = _STRINGLIB_ALLOCATION_FALSE_;
        string_a->stringSignature = _STRINGLIB_ALLOCATION_FALSE_;
    }
    string_a->getString = NULL;
    string_unlock(string_a);
}
string 的释放是通过调用 string_delete(string*) 函数完成的,该函数释放 char 指针并将分配和签名设置为 _STRINGLIB_ALLOCATION_FALSE_;任何其他函数仍然可以用于之前已删除的 string,并且 string 将被自动重新分配。
文本和二进制文件读写
该库还依赖于特定的专用函数来进行文件读写。
size_t (string_write)(_string *string_a, FILE *file_a, ...)
{
    //new version also buffers the whole string if multithreading
    size_t initPos = 0;
    size_t  pos = 0;
    char c_return = '\r';
    va_list valist;
    string_lock(string_a);
    if (string_isAllocated(string_a))
    {
        while (*(string_a.getString+pos)!= '\0')
        {
            //if string has a new line, prints '\0' char before new line
            if (*(string_a->getString+pos)== '\n')
            {
                *(string_a->getString+pos)='\0';
                fputs(string_a->getString+initPos, file_a);
                fwrite(&c_return, sizeof(char), 1, file_a);
                fputc('\n', file_a);
                *(string_a->getString+pos)='\n';
                initPos = pos+1;
            }
            ++pos;
        }
        //prints string last line (could be whole string)
        fputs(string_a.getString+initPos, file_a);
    }
    string_unlock(string_a);
    //print new line if second argument is not 0
    va_start(valist, file_a);
    if (va_arg(valist, int)) fputc('\n', file_a);
    va_end(valist);
    return pos+1;
}
文本文件写入函数不仅仅是按原样写入 string,它还在 string 中的每个换行符前写入一个额外的回车符(不用担心,它只在十六进制编辑器中可见),以便 string_read 和 string_readAppend 函数能够确定 string 是否在换行符后继续。
size_t (string_writeBin)(_string *string_a, FILE *file_a)
{
    //new version also buffers the whole string if multithreading
    string_lock(string_a);
    if (!string_isAllocated(string_a))
	    {string_a->getSize = 0; fwrite(&(string_a->getSize), 
                                sizeof(size_t), 1, file_a); string_unlock(string_a); return 0;}
    //writes string size
    fwrite(&(string_a->getSize), sizeof(size_t), 1, file_a);
    //writes string
    fwrite(string_a->getString, sizeof(char), string_a->getSize, file_a);
    string_unlock(string_a);
    return string_a.getSize;
}
更简单的二进制写入函数写入 string 的大小,然后是 string 字符,或者简单地为未分配的 string 写入 0;另一方面,string_readBin 先读取第一个数字,然后分配 string。
函数重载
此功能仅在支持 _Generic() 关键字的标准 C11 实现或编译器版本(GNU 4.9+、Clang 3.0+、xlC 12.01[V2R1]+)上运行,可以通过 _STRINGLIB_OVERLOADING_ 宏进行检查。
_Generic() 关键字允许在 C 中进行类型检查,并且可以用于宏函数中以实现重载(就像 C++ 一样,但不是,真的不是),例如:
///Allocation checking inside macro should be removed in next update
#define string_set(A, B)\
    _Generic(B,\
    char*: (string_set)((_string*)A, (char*)B),\
    _string*: (string_isAllocated)((_string*)B)?(string_set)((_string*)A, 
         ((string_getString)(*((_string*)((void*)B+0))))+0):(string_set)((_string*)A, ""),\
    default: (string_set)((_string*)A, ""))
在此示例中,通用选择允许将 string 指针而不是 char 指针传递给函数,从而为用户提供了更大的灵活性。
简单重载
这种简单的重载是通过宏定义函数实现的,这些函数只检查传递参数的数量(或最后一个参数是否被传递),而不检查参数类型;这是一种替代上述重载方法的方案,如果不支持 _Generic() 关键字,并且在某些仅需要参数数量检查的函数中使用。
这里有两个简单重载的例子
//Overloading of string_appendPos
#define string_appendPos(A, B, C...) ((string_appendPos)(A, B, (size_t)C+0))
//Overloading of string_print
#define string_print(A, B...) ((string_print)(A, (sizeof((int[]){B}))?B+0:1))
前者只是传递参数 C 或在为空时传递参数 0,而后者会创建一个包含额外参数的新数组并检查其大小。
通用选择重载和简单重载都可以通过在调用函数时将函数名加上括号来避免。
线程锁定
函数 string_lock(string*)、string_unlock(string*) 和 string_multilock(string_count, string*...) 会锁定字符串供调用线程使用。
库中的每个函数都会调用它们——使得所有操作都线程安全——并且可以嵌套,允许连续调用多个函数。
锁类似于互斥锁:每个字符串保存一个唯一的 ID,指示哪个线程正在访问它,以及一个锁计数,指示该线程调用的嵌套锁的数量;解锁函数会减少锁计数,当锁计数达到零时释放锁;多锁提供了一种安全地锁定多个字符串的方法,所有可能的嵌套锁都应为单个锁。
实现示例
int main()
{
    //unrecommended (unsafe)
    string string_a;
    //recommended (safe)
    string string_b = STRING_NEW;
    
    string_set(&string_a, "hello world\nnew line test");
    string_newline(&string_a, "this is the third line");
    
    string_print(&string_a);
    printf("SIZE: %d\n", string_getSize(&string_a));
    
    FILE *foo = fopen("text.txt", "w");
    string_write(&string_a, foo);
    fclose(foo);
    
    foo = fopen("text.txt", "r");
    string_read(&string_b, foo);
    string_print(&string_b);
    printf("SIZE: %d\n", string_getSize(&string_b));
    string_delete(&string_a);
    string_delete(&string_b);
    return 0;
}
附加信息
StringLib C 是一个开源函数集,最初是为了练习目的而创建,后来我决定分享它;任何帮助或建议都深受欢迎。
如果您尝试使用该库,请提供您的反馈(在这里或在 sourceforge 页面上……或者两者都提供)。
完整文档
锁定函数
void string_lock(_string *string_a);
锁定当前线程的 string,支持多级锁定
void string_unlock(_string *string_a);
当 locklevel==0 时解锁 string(锁定和解锁次数相同)
void string_multilock(int string_count, _string *string_a, ...);
安全地锁定多个 strings(按地址顺序)
基本函数
const char *const string_getString(_string *string_a);
将 string 转换为 char 指针。
size_t string_getSize(_string *string_a); 
返回 string 的大小;检查函数
int string_contains(_string *string_a, const char *string_b, ...);
检查 string 是否包含 string 字面量,返回找到的位置加一,传递第三个参数(可选)从 string 的不同位置开始搜索。
int string_containsChars(_string *string_a, int checkall, ...)
检查 string 是否包含所有传入的字符(如果参数 checkall 为 1 或 true)或其中一个字符(checkall = 0 或 false);字符从第三个参数传入。
int string_equals(_string *string_a, const void *string_v);
检查 string 是否与 string 字面量相同。
输入/输出函数
size_t string_set(_string *string_a, const void *string_b); 
将 string 设置为 string 字面量。
size_t string_scan(_string *string_a); 
将 string 设置为用户输入。
size_t string_append(_string *string_a, const void *string_v);
size_t string_appendPos(_string *string_a, const void *string_v, ...);
size_t string_appendCiclePos(_string *string_a, const void *string_v, unsigned int repeatTimes, ...);
将 string 字面量追加到 string。
size_t string_scanAppend(_string *string_a);
size_t string_scanAppendPos(_string *string_a, ...); 
将用户输入追加到 string。
size_t string_newline(_string *string_a, ...); 
创建新行,可能会追加 string 字面量(可以传递 stdin 以追加用户输入)。
size_t string_cut(_string *string_a, size_t pos, ...); 
从位置 ‘pos’ 开始切割 string 直到最终结束位置。
size_t string_override(_string *string_a, const void* string_v, ...); 
将 string 字面量覆盖在 string 上,从可能的起始位置(如果指定)开始。
void string_swap(_string *string_a, _string *string_b); 
交换两个 string。
void string_delete(_string *string_a); 
删除一个 string,释放内存。
void string_print(_string *string_a, ...); 
将 string 打印到输出控制台。
文件输入/输出函数
size_t string_write(_string *string_a, FILE *file_a, ...);
写入文本文件
size_t string_writeBin(_string *string_a, FILE *file_a);
写入二进制文件
size_t string_read(_string *string_a, FILE *file_a, ...); 
size_t string_readAppend(_string *string_a, FILE *file_a, ...);
size_t string_readBuffered(_string *string_a, FILE *file_a, size_t buffersize, ...);
size_t string_readAppendBuffered(_string *string_a, FILE *file_a, size_t buffersize, ...);
从文本文件读取
size_t string_readBin(_string *string_a, FILE *file_a);
从二进制文件读取
不必要的函数(由其他函数使用,但用户无需使用)
size_t string_init(_string *string_a);
初始化字符串
int string_isAllocated(_string *string_a);
检查字符串是否已分配
历史
- [2017/03/21] 首次 beta 上传
- [2017/03/30] 结构体访问方法重大更改
- [2017/04/09] 更改了 string访问方法,优化了算法复杂度
- [2017/04/19] 添加了简单重载(防止 Linux 下的 bug)
- [2017/05/09] 分割了 string_contains函数;string_contains现在返回位置+1
- [2017/05/13] String_contains现在可以从任何起始位置进行检查
- [2017/05/28] 添加了 string_appendCiclePos,清理了代码
- [2017/07/08] 定义了 STRING_NEW并添加了缓冲读取函数
- [2018/01/15] 线程安全函数和锁
- [2018/03/08] 多锁函数和小改动

