我们知道std::string
非常好用。不过,对于一些朋友来说他们需要对自己定义的字符串类型进行处理。
使用它们自己的字符串类型显然不是一个好主意,因为对于字符串的安全处理是很困难的。幸运的是,std::string
是std::basic_string
类型的一个特化版本。这个类中包含了所有复杂的内存处理,不过其对字符串的拷贝和比较没有添加任何条件。所以我们可以基于basic_string
,将其所需要包含的自定义类作为一个模板参数传入。
本节中,我们将来看下如何传入自定义类型。然后,在不实现任何东西的情况下,如何对自定义字符串进行创建。
我们将实现两个自定义字符串类:lc_string
和ci_string
。第一个类将通过输入创建一个全是小写字母的字符串。另一个字符串类型不会对输入进行任何变化,不过其会对字符串进行大小写不敏感的比较:
-
包含必要的头文件,并声明所使用的命名空间:
#include <iostream> #include <algorithm> #include <string> using namespace std;
-
然后要对
std::tolower
函数进行实现,其已经定义在头文件<cctype>
中。其函数也是现成的,不过其不是constexpr
类型。C++17中一些string
函数可以声明成constexpr
类型,但是还要使用自定义的类型。所以对于输入字符串,只将大写字母转换为小写,而其他字符则不进行修改:static constexpr char tolow(char c) { switch (c) { case 'A'...'Z': return c - 'A' + 'a'; // 读者自行将case展开 default: return c; } }
-
std::basic_string
类可以接受三个模板参数:字符类型、字符特化类和分配器类型。本节中我们只会修改字符特化类,因为其定义了字符串的行为。为了重新实现与普通字符串不同的部分,我们会以public
方式继承标准字符特化类:
class lc_traits : public char_traits<char> {
public:
-
我们类能接受输入字符串,并将其转化成小写字母。这里有一个函数,其是字符级别的,所以我们可以对其使用
tolow
函数。我们的这个函数为constexpr
:static constexpr void assign(char_type& r, const char_type& a ) { r = tolow(a); }
-
另一个函数将整个字符串拷贝到我们的缓冲区内。使用
std::transform
将所有字符从源字符串中拷贝到内部的目标字符串中,同时将每个字符与其小写版本进行映射:static char_type* copy(char_type* dest, const char_type* src, size_t count) { transform(src, src + count, dest, tolow); return dest; } };
-
上面的特化类可以帮助我们创建一个字符串类,其能有效的将字符串转换成小写。接下来我们在实现一个类,其不会对原始字符串进行修改,但是其能对字符串做大小写敏感的比较。其继承于标准字符串特征类,这次将对一些函数进行重新实现:
class ci_traits : public char_traits<char> { public:
-
eq
函数会告诉我们两个字符是否相等。我们也会实现一个这样的函数,但是我们只实现小写字母的版本。这样'A'与'a'就是相等的:static constexpr bool eq(char_type a, char_type b) { return tolow(a) == tolow(b); }
-
lt
函数会告诉我们两个字符在字母表中的大小情况。这里使用了逻辑操作符,并继续对两个字符使用转换成小写的函数:static constexpr bool lt(char_type a, char_type b) { return tolow(a) < tolow(b); }
-
最后两个函数都是字符级别的函数,接下来两个函数都为字符串级别的函数。
compare
函数与strncmp
函数差不多。当两个字符串的长度count
相等,那么就返回0。如果不相等,会返回一个负数或一个正数,返回值就代表了其中哪一个在字母表中更小。并计算两个字符串中所有字符之间的距离,当然这些都是在小写情况下进行的操作。C++14后,这个函数可以声明成constexpr
类型:static constexpr int compare(const char_type* s1, const char_type* s2, size_t count) { for (; count; ++s1, ++s2, --count) { const char_type diff (tolow(*s1) - tolow(*s2)); if (diff < 0) { return -1; } else if (diff > 0) { return +1; } } return 0; }
-
我们所需要实现的最后一个函数就是大小写不敏感的
find
函数。对于给定输入字符串p
,其长度为count
,我们会对某个字符ch
的位置进行查找。然后,其会返回一个指向第一个匹配字符位置的指针,如果没有找到则返回nullptr
。这个函数比较过程中我们需要使用tolow
函数将字符串转换成小写,以匹配大小写不敏感的查找。不幸的是,我们不能使用std::find_if
来做这件事,因为其是非constexpr
函数,所以我们需要自己去写一个循环:static constexpr const char_type* find(const char_type* p, size_t count, const char_type& ch) { const char_type find_c {tolow(ch)}; for (; count != 0; --count, ++p) { if (find_c == tolow(*p)) { return p; } } return nullptr; } };
-
OK,所有自定义类都完成了。这里我们可以定义两种新字符串类的类型。
lc_string
代表小写字符串,ci_string
代表大小写不敏感字符串。这两种类型与std::string
都有所不同:using lc_string = basic_string<char, lc_traits>; using ci_string = basic_string<char, ci_traits>;
-
为了能让输出流接受新类,我们需要对输出流操作符进行重载:
ostream& operator<<(ostream& os, const lc_string& str) { return os.write(str.data(), str.size()); } ostream& operator<<(ostream& os, const ci_string& str) { return os.write(str.data(), str.size()); }
-
现在我们来对主函数进行编写。先让我们创建一个普通字符串、小写字符串和大小写不敏感字符串的实例,然后直接将其进行打印。其在终端上看起来都很正常,不过小写字符串将所有字符转换成了小写:
int main() { cout << " string: " << string{"Foo Bar Baz"} << '\n' << "lc_string: " << lc_string{"Foo Bar Baz"} << '\n' << "ci_string: " << ci_string{"Foo Bar Baz"} << '\n';
-
为了测试大小写不敏感字符串,可以实例化两个字符串,这两个字符串只有在大小写方面有所不同。当我们将这两个字符串进行比较时,其应该是相等的:
ci_string user_input {"MaGiC PaSsWoRd!"}; ci_string password {"magic password!"};
-
之后,对其进行比较,然后将匹配的结果进行打印:
if (user_input == password) { cout << "Passwords match: \"" << user_input << "\" == \"" << password << "\"\n"; } }
-
编译并运行程序,其输出和我们期望的相符。开始的三行并未对输入进行修改,除了
lc_string
将所有字符转换成了小写。最后的比较,在大小写不敏感的前提下,也是相等的:$ ./custom_string string: Foo Bar Baz lc_string: foo bar baz ci_string: Foo Bar Baz Passwords match: "MaGiC PaSsWoRd!" == "magic password!"
我们完成的所有子类和函数实现,在新手看来十分的不可思议。这些函数签名都来自于哪里?为什么我们为了函数签名,就要对相关功能性的函数进行重新实现呢?
首先,来看一下std::string
的类声明:
template <
class CharT,
class Traits = std::char_traits<CharT>,
class Allocator = std::allocator<CharT>
>
class basic_string;
可以看出std::string
其实就是一个std::basic_string<char>
类,并且其可以扩展为std::basic_string<char, std::char_traits<char>, std::allocator<char>>
。OK,这是一个非常长的类型描述,不过其意义何在呢?这就表示字符串可以不限于有符号类型char
,也可以是其他类型。其对于字符串类型都是有效的,这样就不限于处理ASCII字符集。当然,这不是我们的关注点。
char_traits<char>
类包含basic_string
所需要的算法。 char_traits<char>
可以进行字符串间的比较、查找和拷贝。
allocator<char>
类也是一个特化类,不过其运行时给字符串进行空间的分配和回收。这对于现在的我们来说并不重要,我们只使用其默认的方式就好。
当我们想要一个不同的字符串类型是,可以尝试对basic_string
和char_traits
类中提供的方法进行复用。我们实现了两个char_traits
子类:case_insentitive
和lower_caser
类。我们可以将这两个字符串类替换标准char_traits
类型。
Note:
为了探寻
basic_string
适配的可能性,我们需要查询C++ STL文档中关于std::char_traits
的章节,然后去了解还有那些函数需要重新实现。