那会儿还没有 C++14。
很多人写 C++。
说白了。
就是在写 C。
只是多了点 class。
字符串也没变聪明。
你眼里是“字符串”。
它手里攥着的。
常常只是一个地址。
地址平时挺乖。
你一拿它去做 +。
它就开始装成“拼接”。
直到线上啪一下。
当年没有它的时候:"hello" 到底是什么
先把这句话放回 C 的年代。
"hello" 本体更像一个数组。
你可以先把它想成 const char[6]。
末尾还自带一个 \0。
也就是 C 里那种“字符串结束符”。
但你一把它放进表达式里。
它就会“退化”成指针。
变成 const char*。
auto p = "hello";
p 看起来像“字符串”。
其实只是“指向第一个字符的指针”。
你能拿它去喂 C API。
也能拿它去给 printf("%s")。
当年大家就是这么活下来的。
这里新手最容易误会的一点是:
下面两行看起来很像。
但它们走的是两条路。
#include <string>
auto p = "hello";
std::string s = "hello";
第一行你拿到的是地址。
第二行你才真的构造了一个 std::string 对象。
对象会把内容拷贝进去。
自己记长度。
也能安全地拼接。
你可能会问:
那 C++ 为什么不干脆让字面量默认就是 std::string?
因为它背着历史包袱。
一大堆接口还停在 C 的世界。
它们就认 const char*。
另外 std::string 是对象。
要构造。
有时还要分配内存。
让所有字面量默认变成对象。
对老代码很不友好。
性能也不总是划算。
坑是怎么来的:指针的 + 不是拼接
在 C 里。
pointer + integer 是合法语法。
意思是“指针往后挪几格”。
auto p = "hello" + 1;
这行不是 "hello1"。
它是指向 e 的指针。
所以你再把它当字符串打印。
你看到的是 "ello"。
最吓人的是:
它不一定报错。
它只是悄悄把意思换了。
而你还以为自己在拼接。
线上啪一下:我在小项目里把整数“拼”进了字符串
我当年写过一个小工具。
就干一件事:打日志。
日志里要带错误码。
我当时手一滑。
写成了这样。
#include <string>
int main() {
int code = 404;
std::string s = "E:" + code;
}
这行能编过。
所以更坑人。
因为 "E:" 会先退化成 const char*。
然后 + code 走的是指针运算。
注意。
这时候 std::string 甚至还没开始构造。
你只是先把地址算歪了。
如果你想看清它到底在干嘛。
把 404 换成 1。
#include <string>
int main() {
int code = 1;
std::string s = "E:" + code;
}
这次它大概率不崩。
但 "E:" + 1 指向的是 ':'。
所以 s 变成了 ":"。
你本来想拼错误码。
结果只是把指针往后挪了一格。
那 404 呢?
它会把指针挪到字面量后面很远。
严格来说这已经踩到未定义行为了。
也就是:会发生什么,全看运气。
还有个细节更扎心。
std::string 这个构造方式。
习惯上是按 C 字符串读的。
也就是一直读到 \0 才停。
你给它一个乱飞的指针。
它就会一直读。
读到某个碰巧是 0 的字节为止。
所以你看到乱码、崩溃。
甚至“看起来还能跑”。
都不奇怪。
(有人调侃:C 让你打中脚;C++ 让你更难打中,但打中了就炸一整条腿。)
当年大家怎么爬出来:强行把左边变成 std::string
那会儿的自救思路很朴素。
别让左边留在指针世界。
你先把它变成对象。
再谈拼接。
#include <string>
int main() {
int code = 404;
std::string s = std::string("E:") + std::to_string(code);
}
这里又有两个新东西。
std::string("E:"):显式构造一个字符串对象。
std::to_string(code):把整数变成字符串。
别笑。
当年很多事故就是因为有人忘了“整数得先转成字符串”。
这次左边是 std::string。
+ 才会走字符串拼接那条路。
不会再变成指针乱跳。
代价也很明显。
项目里开始满屏 std::string("...")。
像贴膏药。
贴在每个可能出事的地方。
还有一个更常见的坑:两个字面量相加
你以为自己在做拼接。
编译器却一脸问号。
auto s = "user:" + "alice";
两边都是字面量。
退化完就是两个指针。
指针和指针没法相加。
所以它通常会直接编译不过。
而下面这种能工作。
因为左边已经是 std::string 了。
#include <string>
auto s = std::string("user:") + "alice";
但你看。
又是一圈 std::string("...")。
写久了人会烦。
C++14:"..."s(让字面量直接变成 std::string)
C++14 做的事其实很小。
它给字符串字面量加了个后缀。
就一个字母:s。
#include <string>
using namespace std::string_literals;
int main() {
int code = 404;
std::string s = "E:"s + std::to_string(code);
}
"E:"s 一出来就是 std::string。
你不需要先手动 std::string("E:")。
也就少了很多“靠自觉防坑”的地方。
如果你对 using namespace std::string_literals; 皱眉。
先别跟它较劲。
你可以把它理解成一句话:把这个 s 后缀的能力打开。
但你可能还会问:
这个 s 到底从哪来的?
这东西怎么来的:先有 C++11 的“用户自定义字面量”
C++11 先开了一个口子。
叫“用户自定义字面量”。
意思是:
你可以自己发明后缀。
让字面量直接变成你想要的类型。
标准库后来的 "..."s。
就是沿着这条路走出来的。
你感受一下这个味道。
#include <cstddef>
#include <string>
std::string operator"" _tag(const char* p, std::size_t n) {
return std::string(p, n) + "#";
}
这不是关键字。
它就是个函数。
p 是指向那段字符的指针。
n 是长度。
不算结尾的那个 \0。
std::size_t 先把它当成“装得下长度的整数”就行。
专门用来表示大小、下标这类东西。
写完你就能用。
auto s = "hi"_tag;
所以 "..."s 本质也一样。
只是这个后缀,标准库帮你写好了。
你只需要把它引进来。
横向对比:三种写法,三种心智负担
如果你刚学 C++。
我建议你先记住三种“站队”。
auto a = "hi";
这是指针世界。
轻。
快。
但你要时刻记得:它不是对象。
auto b = std::string("hi");
这是对象世界。
清晰。
能拼接。
代价是你得手动套一层。
using namespace std::string_literals;
auto c = "hi"s;
这是“我还在写字面量,但我想要对象”的世界。
少写字。
也少踩坑。
顺手提醒:"..."s 不是免费的
"hi"s 会构造一个 std::string。
大概率会拷贝一次。
也可能会分配内存。
它解决的是“类型和重载把你带沟里”的问题。
不是性能魔法。
一句话的结论
字面量默认是指针。
"..."s 才是你以为的“字符串对象”。
最后留个亮点
我越来越觉得。
C++ 的演进很多时候不是“发明新东西”。
而是把默认值往安全那边挪一厘米。
"..."s 就是这种一厘米。
你看起来只多了一个字母。
但它能把你从“指针运算”那条岔路上。
提前拽回来。