1. 字符串大小写转换
string没有直接提供to_upper或to_lower这样的方法,不过有更通用的方法:
std::string s("hello");
transform(s.begin(), s.end(), s.begin(), toupper);
transform(s.begin(), s.end(), s.begin(), tolower);
std::vector<char> v(s.begin(), s.end());
transform(v.begin(), v.end(), v.begin(), toupper);
transform(v.begin(), v.end(), v.begin(), tolower);
string没有to_upper方法是因为,不是每个string里的成员都是有独立意义的。比如string里可以存放utf8编码的中文字符串,这样对单个的char元素进行大小写转换是有风险的,因为每个char元素之间在utf8编码含义上存在相互依赖关系。注意这个例子中使用了C++的ADL技术,下面的例子还将出现。
2. 字符串的trim方法
string没有直接提供trim方法,但要实现也是比较容易的;但是要trim一个vector<char>就不大容易了:
const std::string source(" \t hello \t\t ");
// trim string, some what simple
std::string s(source.begin(), source.end());
// trim left
s.erase(0, s.find_first_not_of(' ')); // "\t hello \t\t "
s.erase(0, s.find_first_not_of(" \t")); // "hello \t\t "
// trim right
s.erase(s.find_last_not_of(' ') + 1); // "hello \t\t"
s.erase(s.find_last_not_of(" \t") + 1); // "hello"
// trim character vector, a bit complicated
std::vector<char> v(source.begin(), source.end());
const auto is_blank = bind2nd(std::equal_to<char>(), ' ');
const auto is_space = bind1st(std::ptr_fun<const char*>(strchr), " \t");
v.erase(v.begin(), find_if_not(v.begin(), v.end(), is_blank)); // "\t hello \t\t "
v.erase(v.begin(), find_if_not(v.begin(), v.end(), is_space)); // "hello \t\t "
v.erase(find_if_not(v.rbegin(), v.rend(), is_blank).base(), v.end()); // "hello \t\t"
v.erase(find_if_not(v.rbegin(), v.rend(), is_space).base(), v.end()); // "hello"
这里把复杂的functor提取出一个变量,以免一行代码过长。如果不这样的话代码会很复杂,这大概就是string为什么要提供find_??_of函数的一个原因吧。最后注意下,is_blank/is_space与C语言库中定义的isblank/isspace含义不同,参见http://www.cplusplus.com/reference/cctype/。
3. erase/remove_if 惯用手法
这是C++标准库的一个经典手法之一,常用于序列容器的元素删除。
const auto source = std::string("http://Blog.CSDN.NET");
// erase/remove_if for string
auto s = std::string(source.begin(), source.end());
s.erase(remove_if(s.begin(), s.end(), isupper), s.end()); // "http://log.."
// erase/remove_if for vector
auto v = std::vector<char>(source.begin(), source.end());
v.erase(remove_if(v.begin(), v.end(), islower), v.end()); // "://B.CSDN.NET"
4. 字符串替换
通常有两种情况,一种是将字符串中的特定内容替换为另外的内容,另外一种是单个字符的替换。前者用string的replace方法即可,后者则要动用algorithm了。可惜的是,string的replace方法一次只能替换掉一次出现,不能一次性替换全部。如果要这样做,可以自己手写一个for循环。
auto s = std::string("http://blog.csdn.net");
const auto src = std::string("csdn");
const auto dst = std::string("chinaunix");
s.replace(s.find(src), src.size(), dst); // "http://blog.chinaunix.net"
replace(s.begin(), s.end(), '.', '&'); // "http://blog&chinaunix&net"
const auto ampersand = std::string("&");
const auto html_escape = std::string("&");
for (auto offset = std::string::size_type();
(offset = s.find(ampersand, offset)) != s.npos;
(offset += html_escape.size()))
{
s.replace(offset, ampersand.size(), html_escape);
} // "http://blog&chinaunix&net"
5. string的begin_with和end_width方法
这是比较常用的功能,对于简单的要求只要用compare方法,就不必动用正则表达式了。特别注意的是,在判断end_with时要注意越界检查。
const auto s = std::string("http://blog.csdn.net");
const auto head = std::string("http://");
const auto tail = std::string(".net");
const bool is_begin_with = s.compare(0, head.size(), head) == 0;
const bool is_end_with = s.size() >= tail.size()
&& s.compare(s.size() - tail.size(), tail.size(), tail) == 0;
6. 字符串和数字之间的转换
这个功能实在是太常用了,不过标准库已经提供了相当完整的支持。如strtod/strtoul, stod/stoul, atoi/atof, std::to_string, sscanf/sprintf等,这里要指出的是,strto?/sto? 函数在进制填写为0时会自动识别字符串的进制信息,这在应用时比较方便;顺便提醒一下,这里sto?可能会抛异常!确切的说,sscanf/sprintf能实现所有数字和字符串之间的转换,但是其他函数在命名上则更为直观。
strtoul("10", nullptr, 0); // return 10
strtoul("0x10", nullptr, 0); // return 16
7. 强大的assign方法
标准库中string, vector, deque, list, forward_list都提供了assign方法,并且有相应构造函数对应。这些方法都有(InputIterator first, InputIterator last)的形式,初看不怎么诱人,但如果结合标准库中的iostream, fstream, stringstream等流时,其功能非常强大、灵活。
比如大家都熟悉的从一个文件中读取以空白符分割的数字字符串,假设文件内容为 "30\v 43\n 50\t 64 71"。比较土的方法是将文件的内容一次性读取到一个buffer中,然后一个一个提取;或者直接用fscanf之类的函数一个一个提取;或者逐个调用用ifstream的>>(int)方法;等等。这些方法的缺点是要自己写循环。有了assign方法后,事情就容易多了。
const auto s = std::string("30\v 43\n 50\t 64 71");
std::vector<std::string> vs;
auto ss1 = std::istrstream(s.data(), s.size());
vs.assign(std::istream_iterator<std::string>(ss1),
std::istream_iterator<std::string>());
std::vector<int> vi;
auto ss2 = std::istrstream(s.data(), s.size());
vi.assign(std::istream_iterator<int>(ss2), std::istream_iterator<int>());
// file input.txt has the same content as $s
std::forward_list<int> fli1;
fli1.assign(std::istream_iterator<int>(std::ifstream("input.txt")),
std::istream_iterator<int>());
// another way: use constructor, be care of syntax! more parenthesis required!
std::forward_list<int> fli2((std::istream_iterator<int>(std::ifstream("input.txt"))),
std::istream_iterator<int>());
注意最后一句中,构造函数的第1个参数必须加上额外的一对圆括号,否则fli2就不是一个变量,而是一个函数声明了。比如int f(char());表示的是函数声明,其函数名为f,参数有1个,参数类型为char(*)()即函数指针,返回类型为int;而int g((char()))则表示定义一个int类型的变量g,其初始值为char()即0。
以上例子是字符串的不同split方法(按字符串split,按转换值split),但不如s.split(vs, isspace)直观。也许你要问了,如果输入是逗号分隔的内容即CSV格式,那怎么拆分呢?亲,自己写一个有点复杂哦,还是动用强大的boost::algorithm::split 吧!如果您想不开,或者感觉生活无味,也可以用 boost::spirit 这把牛刀试试!
8. 字符串的split和join
学过PHP的人都知道有一组函数叫做explode/split和implode函数,比如将vector<string>拼接成CSV格式的字符串。这里就不费口舌了,想偷懒就用boost的split和join函数吧!
9. 文件后缀名提取
这实在是太简单了,甚至都可以不用C++了,直接上代码吧!
const auto s = std::string("readme.txt");
const auto extension = strrchr(s.c_str(), '.');
这里输出为".txt";当没有后缀名的时候,extension为nullptr,这样检查文件是否有后缀名就容易了。
10. 小心掉进erase的陷阱
几乎所有标准库中的序列容器和关联容器都有erase方法。但一些容器在erase掉元素时,会使相关迭代器失效。C++11对erase方法进行了改进,使erase能返回后续的迭代器。
11. 快速输出容器中的内容
如果有一个vector<int>,如何用简单快捷的方法打印出其内容到屏幕上呢?
const auto vi = std::vector<int>{11, 22, 33, 44, 55};
copy(vi.begin(), vi.end(), std::ostream_iterator<int>(std::cout, "\n"));
未完待续