第 13 章
字符与字符串:cctype、cstring 与 std::string
先把单个字符当作整数来分类与转换,再用 cstring 处理以空字符结尾的字符数组,最后引出 std::string——它把长度与空字符的管理藏到类型内部。
Characters and Strings: cctype, cstring, and std::string
Classify and convert single characters as integers, manipulate null-terminated character arrays with cstring, and then meet std::string, which hides length and terminator bookkeeping inside the type.
篇内目录
这是 31 小时长视频的第 13 章,对应时间轴 12:46:10–14:12:29。原视频没有分章,这一章是重新划分出来的:从「字符分类」讲到「C 字符串工具函数」,最后引出 std::string。
这一章在讲什么
C++ 里处理文本有三层工具,本章沿着它们由低到高走一遍:
- 单个字符:
<cctype>提供判断与大小写转换,函数返回int,非零表示真。 - C 风格字符串:以
'\0'结尾的字符数组,用<cstring>的strlen、strcmp、strchr、strcat、strcpy操作。 std::string:把长度、扩容和空字符全部封装起来,不再需要手动维护边界。
三层之间的关系是本条主线:每一层都在替下一层管理麻烦。strlen 替你数到 '\0',std::string 替你保存长度并在需要时重新分配内存。
时间轴
| 时间 | 内容 |
|---|---|
| 12:46:10 | <cctype> 概览:能做什么、文档在哪 |
| 12:47:23 | std::isalnum:字母或数字 |
| 12:50:08 | std::isalpha:字母 |
| 12:50:44 | std::isblank:空白字符,统计例子 |
| 12:51:52 | std::islower / std::isupper |
| 12:52:49 | std::isdigit |
| 12:53:16 | std::toupper / std::tolower |
| 12:54:44 | VS Code 实机:character manipulation 项目 |
| 12:59:09 | 换用 MSVC 编译,观察不同编译器的差别 |
| 13:00:51 | isblank 计数:为什么计数器该用 size_t |
| 13:03:25 | 大小写计数 |
| 13:06:38 | 大小写转换演示 |
| 13:08:41 | cctype 小结 |
| 13:09:36 | strlen:长度,不含 '\0' |
| 13:11:43 | strcmp:字典序比较 |
| 13:13:38 | strncmp:只比较前 n 个字符 |
| 13:14:37 | strchr:找第一次出现,返回指针 |
| 13:17:36 | strrchr:找最后一次出现 |
| 13:19:05 | VS Code 实机:c string manipulation |
| 13:21:39 | strlen 与 sizeof 的差别 |
| 13:24:17 | 指针可改指向、数组名不可改 |
| 13:30:17 | strchr 的循环细节 |
| 13:36:46 | 反面例子:直接改原指针导致循环 25 次 |
| 13:39:25 | strrchr 从路径里提取文件名 |
| 13:42:30 | strcat:拼接 |
| 13:45:40 | strncat:只拼前 n 个字符 |
| 13:46:37 | strcpy:复制 |
| 13:47:36 | strncpy:只复制前 n 个字符 |
| 13:49:08 | VS Code 实机:c string concatenation and copy |
| 14:01:10 | 为什么需要 std::string |
| 14:03:34 | std::string 的声明与六种初始化 |
| 14:06:23 | VS Code 实机:declaring std::string |
| 14:10:51 | std::string 的自动扩容与内存回收 |
字符分类:<cctype>
<cctype> 里的函数接收一个字符、返回 int:非零表示真,零表示假。不要假设「真」一定是 1——标准只保证非零。
#include <iostream>
#include <cctype>
int main() {
char c = 'c';
char star = '*';
char seven = '7';
std::cout << std::isalnum(c) << "\n"; // 非零
std::cout << std::isalnum(star) << "\n"; // 0
if (std::isalnum(c)) {
std::cout << c << " is alphanumeric\n";
}
// 想得到真正的 bool:
bool ok = static_cast<bool>(std::isalnum(c));
return 0;
}
常见成员:
| 函数 | 判断 |
|---|---|
isalnum | 字母或数字 |
isalpha | 字母 |
isblank | 空格与制表符 |
islower / isupper | 小写 / 大写 |
isdigit | 数字 |
toupper / tolower | 转换大小写 |
一个计数例子,同时演示两个易错点:范围 for 访问字符数组,以及计数器应使用 std::size_t。
char message[]{"Hello there, nice to meet you!"};
std::size_t blank_count{0};
for (std::size_t i{0}; i < std::size(message); ++i) {
if (std::isblank(message[i])) {
std::cout << "Found a blank at index " << i << "\n";
++blank_count;
}
}
std::cout << "Total blanks: " << blank_count << "\n";
- 为什么用
std::size_t:计数不可能为负,int允许负数,语义不对,还可能溢出。 - 为什么用
std::size:这是原生数组,std::size能拿到元素个数;若数组退化成指针,std::size就失效了。
大小写转换:
char original[]{"Hello World"};
char upper[std::size(original)]{};
for (std::size_t i{0}; i < std::size(original); ++i) {
upper[i] = std::toupper(original[i]); // 原数组不变
}
toupper 返回转换后的字符,不修改原字符;要保留结果就得写到另一个数组里。
编译器差异:部分编译器(尤其 MSVC)会把
cctype中以c开头的函数视为不安全并给出警告。课件建议同一段代码在 gcc 与 MSVC 下各编译一次,亲眼看看差异。
C 字符串:以 '\0' 结尾的字符数组
C 字符串不是一种类型,而是一个约定:字符数组的末尾有一个 '\0'。所有 str* 函数都靠它判断结束。
#include <cstring>
char message1[]{"Hello World"}; // 编译期加上 '\0'
const char* message2{"Hello World"}; // 指向字面量
std::cout << std::strlen(message1) << "\n"; // 11
std::cout << sizeof(message1) << "\n"; // 12(含 '\0')
strlen 与 sizeof 的区别是本章最常考的细节:
| 表达式 | 结果 | 原因 |
|---|---|---|
strlen(message1) | 11 | 数到 '\0' 为止,不含它 |
sizeof(message1) | 12 | 整个数组的字节数,含 '\0' |
strlen(message2) | 11 | 解引用指针,逐字节数 |
sizeof(message2) | 8 | 指针本身的大小(64 位平台) |
strlen 的好处是对退化后的指针仍然有效;sizeof 一旦遇到指针就只能给出指针大小。
比较:strcmp 与 strncmp
strcmp(a, b) 按字典序比较:
- 返回负数:
a在b之前; - 返回 0:两串相同;
- 返回正数:
a在b之后。
它不保证返回 -1 或 1,只保证符号。
const char* a{"alabama"};
const char* b{"blabama"};
std::cout << std::strcmp(a, b) << "\n"; // 负数,'a' < 'b'
std::cout << std::strncmp("aaa", "aaanca", 3) << "\n"; // 0,前三个字符相同
char arr3[]{"alabama"};
arr3 = "something else"; // 编译错误:数组名不能重新指向
指针可以改指向(只要不是 const),数组名不行。这个差异在下一节会直接影响内存管理。
查找:strchr 与 strrchr
strchr(s, c) 返回第一次出现 c 的指针;找不到返回 nullptr。strrchr 找最后一次。
const char* message{"Try not. Do or do not. There is no try."};
const char target{'t'};
const char* result{message};
std::size_t iterations{0};
while ((result = std::strchr(result, target)) != nullptr) {
std::cout << "Found '" << target << "' starting at " << result << "\n";
++result; // 指针算术:从下一个字符继续找
++iterations;
}
std::cout << iterations << " iterations\n";
两个容易踩的点:
- 必须用独立指针
result前进,不能直接改原指针message(下面会看到原因)。 std::cout << result会一直打印到'\0',所以输出的是「从匹配位置到结尾」而不是单个字符。
课件特意演示了一个反面写法:
const char* message{"Try not. Do or do not. There is no try."};
const char* result{nullptr};
std::size_t iterations{0};
while ((result = std::strchr(message, 't')) != nullptr) {
std::cout << result << "\n";
++message; // 问题:只前进一格,没有跳到匹配处
++iterations;
}
// 结果循环 25 次,而不是 2 次
原因:每次循环都从整体重新搜索,++message 只挪动一个字符,匹配位置并没有被跳过。正确做法是把返回值赋回搜索起点,或者像上例那样让 result 自己前进。
把原指针声明为 const char* const 可以防止这类意外:
const char* const message{"..."};
// ++message; // 编译错误,安全
strrchr 的经典用途是从路径中取文件名:
const char path[]{"D:\\projects\\cpp\\hello.cpp"};
const char* filename{std::strrchr(path, '\\')};
if (filename != nullptr) {
std::cout << (filename + 1) << "\n"; // hello.cpp
}
filename 指向最后一个反斜杠,+1 跳过它,打印到 '\0' 为止。
拼接与复制:strcat / strcpy
char dest[50]{"Hello "};
char source[]{"World"};
std::strcat(dest, source); // dest = "Hello World"
std::strcat(dest, " and goodbye");
strcat 从 dest 的 '\0' 处开始覆盖,把 source 接上去。调用者必须保证 dest 足够大,否则会越界写内存,这正是这些函数被许多编译器标记为不安全的原因。
char dest[30]{"Hello "};
std::strncat(dest, "there is a bird on my window", 6); // 只接 "there "
char source[]{"C++ is a multi-purpose language"};
char dest[std::strlen(source) + 1]; // +1 给 '\0'
std::strcpy(dest, source);
strcpy 从目标数组的开头覆盖整个数组;strncpy(dest, src, n) 只复制前 n 个字符。课件用一个例子说明覆盖行为:
char dest[]{"abcde f"};
std::strncpy(dest, "hello", 5); // "hellof":前五个字符被覆盖
为什么 strcpy 的目标要单独留 +1:strlen 不数 '\0',而复制需要把 '\0' 一起搬过去。
std::string:把细节藏起来
字符数组的两个日常负担:
- 必须自己记录容量,越界会崩;
- 必须自己保证
'\0'存在。
std::string 把这两件事交给类型本身处理。
#include <string>
std::string empty; // 空串
std::string planet{"Where the sky is blue"};
std::string copy{planet}; // 拷贝
std::string message{"Hello world", 5}; // "Hello"
std::string repeated(4, 'e'); // "eeee"
std::string saying{"Where the sky is blue", 6, 5}; // 从下标 6 取 5 个:"the s"
第六种写法最容易看错:第一个数字是起始下标,第二个是长度。
std::string planet{"Where the sky is blue"};
planet = "A much longer string than before";
赋值会自动扩容:旧内存被释放,新内存按需分配。若用 const char* 做同样的事,旧字面量只是被丢在原地,指针换了指向,内存并没有归还——std::string 没有这个浪费。
本章结论
<cctype>的谓词返回int,非零为真;不要依赖具体数值。- 字符数组是原生数组:
std::size可用,一旦退化成指针就失效。 strlen不含'\0',sizeof含;sizeof(指针)只是指针大小。strcmp只保证返回值的符号:负、零、正。strchr/strrchr返回指针,找不到返回nullptr;搜索时应让返回指针自己前进。strcat/strcpy不检查目标容量,调用者负责保证足够大,并给'\0'留位置。std::string自动管理长度、空字符与扩容,是本章推荐的默认选择。
理解检验
1. 为什么 std::isalnum('7') 在 if 里能直接用,却不该写成 == 1?
参考答案:标准只规定「是字母或数字时返回非零值,否则返回零」,没有规定非零值必须是 1。写 if (std::isalnum('7')) 依赖的是「零为假、非零为真」,这是正确的;写 if (std::isalnum('7') == 1) 则假设了具体返回值,换编译器或标准库可能失效。
2. 对 char s[]{"Hello"},strlen(s)、sizeof(s) 分别是多少?若 const char* p = s;,sizeof(p) 又是多少?
参考答案:strlen(s) 是 5,数到 '\0' 为止且不含它;sizeof(s) 是 6,包含结尾的 '\0';sizeof(p) 是指针本身的大小(64 位平台通常为 8),与它指向的字符串长度无关。
3. strchr 的循环里,为什么课件要用一个单独的 result 指针,而不能直接对原字符串指针做 ++?
参考答案:strchr 每次都会从头搜索传入的指针所指的字符串。若直接 ++message,只是把搜索起点后移一个字符,并没有跳到匹配位置,下一次搜索仍会在几乎整段里重新扫描,于是循环次数变成字符数(25 次)。用 result = strchr(result, target) 把返回值赋回搜索起点,才能让下一轮从匹配处之后继续,同时保持原指针不变。把原指针声明为 const char* const 可以从编译期阻止这种误改。
4. 下面这段代码为什么危险?写出一种修复方式。
char dest[]{"Hello"};
std::strcat(dest, " World");
参考答案:dest 由字面量初始化为长度 6(含 '\0'),strcat 会把 " World" 追加到末尾,需要至少 12 个字节,越界写入属于未定义行为。修复:显式声明足够容量,例如 char dest[32]{"Hello"};,或在 C++ 里改用 std::string dest{"Hello"}; dest += " World";。
5. 用 strcpy 复制一个长度为 n 的字符串时,目标数组至少要多大?为什么不是 n?
参考答案:至少要 n + 1 个字符。strlen 返回的 n 不含结尾的 '\0',而 strcpy 会把 '\0' 一并复制过去,所以目标必须额外留一格存放它。
6. std::string s{"Where the sky is blue", 6, 5}; 得到的 s 是什么?
参考答案:"the s"。第一个数字 6 是起始下标(W 为 0,t 为 6),第二个数字 5 是取多少个字符:t、h、e、空格、s。
与前后章的关系
上一章处理引用与 const,为本章的「指针只读」打下基础;本章最后引出的 std::string 在第 14 章之后会继续使用。下一章转向单一定义规则、链接与声明/定义的区别——那是把多个源文件组织成程序的前提,也是理解 std::string 这类类型如何跨翻译单元工作的基础。
This is chapter 13 of a 31-hour single video, covering 12:46:10–14:12:29. The original video has no chapters; this one is a section of the re-divided timeline, running from character classification through the C-string utilities and finally to std::string.
What this chapter covers
C++ offers three layers for working with text, and the chapter climbs them in order:
- Individual characters:
<cctype>classifies and converts them; the functions returnint, with nonzero meaning true. - C-style strings: null-terminated character arrays, manipulated by
strlen,strcmp,strchr,strcat, andstrcpyfrom<cstring>. std::string: length, growth, and the terminator are all encapsulated, so bounds never have to be tracked by hand.
The through-line is that each layer absorbs bookkeeping the layer below makes you do. strlen counts up to '\0' for you; std::string stores the length and reallocates when needed.
Timeline
| Time | Topic |
|---|---|
| 12:46:10 | <cctype> overview: what it offers and where the docs are |
| 12:47:23 | std::isalnum: letter or digit |
| 12:50:08 | std::isalpha: letters |
| 12:50:44 | std::isblank: blanks, with a counting example |
| 12:51:52 | std::islower / std::isupper |
| 12:52:49 | std::isdigit |
| 12:53:16 | std::toupper / std::tolower |
| 12:54:44 | In VS Code: the character-manipulation project |
| 12:59:09 | Building with MSVC to compare compilers |
| 13:00:51 | Counting blanks, and why the counter should be size_t |
| 13:03:25 | Counting upper- and lowercase letters |
| 13:06:38 | Case conversion demo |
| 13:08:41 | cctype wrap-up |
| 13:09:36 | strlen: length without '\0' |
| 13:11:43 | strcmp: lexicographic comparison |
| 13:13:38 | strncmp: compare only the first n characters |
| 13:14:37 | strchr: first occurrence, returns a pointer |
| 13:17:36 | strrchr: last occurrence |
| 13:19:05 | In VS Code: the C-string-manipulation project |
| 13:21:39 | strlen versus sizeof |
| 13:24:17 | Pointers can be repointed; array names cannot |
| 13:30:17 | The strchr loop, step by step |
| 13:36:46 | A counterexample: advancing the original pointer loops 25 times |
| 13:39:25 | Pulling a filename out of a path with strrchr |
| 13:42:30 | strcat: concatenation |
| 13:45:40 | strncat: concatenate only n characters |
| 13:46:37 | strcpy: copying |
| 13:47:36 | strncpy: copy only n characters |
| 13:49:08 | In VS Code: concatenation and copy |
| 14:01:10 | Why std::string exists |
| 14:03:34 | Declaring and initializing std::string six ways |
| 14:06:23 | In VS Code: declaring std::string |
| 14:10:51 | Automatic growth and memory release |
Character classification: <cctype>
Functions in <cctype> take a character and return an int: nonzero is true, zero is false. Do not assume the true value is exactly 1; the standard only promises nonzero.
#include <iostream>
#include <cctype>
int main() {
char c = 'c';
char star = '*';
char seven = '7';
std::cout << std::isalnum(c) << "\n"; // nonzero
std::cout << std::isalnum(star) << "\n"; // 0
if (std::isalnum(c)) {
std::cout << c << " is alphanumeric\n";
}
// To get an actual bool:
bool ok = static_cast<bool>(std::isalnum(c));
return 0;
}
The common predicates:
| Function | Tests |
|---|---|
isalnum | letter or digit |
isalpha | letter |
isblank | space or tab |
islower / isupper | lowercase / uppercase |
isdigit | digit |
toupper / tolower | case conversion |
A counting example that also illustrates two common mistakes: ranging over a character array, and using std::size_t for a counter.
char message[]{"Hello there, nice to meet you!"};
std::size_t blank_count{0};
for (std::size_t i{0}; i < std::size(message); ++i) {
if (std::isblank(message[i])) {
std::cout << "Found a blank at index " << i << "\n";
++blank_count;
}
}
std::cout << "Total blanks: " << blank_count << "\n";
- Why
std::size_t: a count cannot be negative,intallows negatives, and it can overflow. - Why
std::size: this is a native array, sostd::sizereports the element count; once the array decays to a pointer,std::sizestops working.
Case conversion:
char original[]{"Hello World"};
char upper[std::size(original)]{};
for (std::size_t i{0}; i < std::size(original); ++i) {
upper[i] = std::toupper(original[i]); // original is untouched
}
toupper returns the converted character; it does not modify the input, so the result must be stored somewhere.
Compiler differences: some compilers, MSVC in particular, flag the
c-prefixedcctypefunctions as unsafe and emit warnings. The lecture recommends compiling the same code under both gcc and MSVC to see the difference firsthand.
C strings: null-terminated character arrays
A C string is not a type but a convention: a character array whose last character is '\0'. Every str* function relies on it to find the end.
#include <cstring>
char message1[]{"Hello World"}; // the compiler appends '\0'
const char* message2{"Hello World"}; // points at a string literal
std::cout << std::strlen(message1) << "\n"; // 11
std::cout << sizeof(message1) << "\n"; // 12, including '\0'
The strlen versus sizeof distinction is the detail most likely to be tested:
| Expression | Result | Why |
|---|---|---|
strlen(message1) | 11 | counts up to '\0', excluding it |
sizeof(message1) | 12 | byte size of the array, including '\0' |
strlen(message2) | 11 | dereferences the pointer and counts bytes |
sizeof(message2) | 8 | size of the pointer itself (64-bit) |
The value of strlen is that it still works on a decayed pointer; sizeof on a pointer only reports the pointer's size.
Comparison: strcmp and strncmp
strcmp(a, b) compares lexicographically:
- negative when
acomes beforeb; - zero when they are equal;
- positive when
acomes afterb.
It does not promise -1 or 1, only the sign.
const char* a{"alabama"};
const char* b{"blabama"};
std::cout << std::strcmp(a, b) << "\n"; // negative, 'a' < 'b'
std::cout << std::strncmp("aaa", "aaanca", 3) << "\n"; // 0, first three match
char arr3[]{"alabama"};
arr3 = "something else"; // compile error: an array name cannot be repointed
Pointers can be repointed (unless const); array names cannot. That difference matters directly for the memory discussion below.
Searching: strchr and strrchr
strchr(s, c) returns a pointer to the first occurrence of c, or nullptr if absent. strrchr returns the last.
const char* message{"Try not. Do or do not. There is no try."};
const char target{'t'};
const char* result{message};
std::size_t iterations{0};
while ((result = std::strchr(result, target)) != nullptr) {
std::cout << "Found '" << target << "' starting at " << result << "\n";
++result; // pointer arithmetic: continue after the match
++iterations;
}
std::cout << iterations << " iterations\n";
Two traps:
- Advance a separate pointer
result, never the originalmessage. std::cout << resultprints through to'\0', so the output is "from the match to the end", not the single character.
The lecture deliberately shows the wrong version:
const char* message{"Try not. Do or do not. There is no try."};
const char* result{nullptr};
std::size_t iterations{0};
while ((result = std::strchr(message, 't')) != nullptr) {
std::cout << result << "\n";
++message; // problem: moves one character instead of jumping to the match
++iterations;
}
// loops 25 times instead of 2
The reason: each iteration searches from the start of the pointer passed in, and ++message only shifts the start by one character, so the match is never skipped. The fix is to feed the returned pointer back in, or let a separate pointer walk forward. Declaring the original as const char* const makes the mistake a compile error:
const char* const message{"..."};
// ++message; // compile error, safe
A classic strrchr use is extracting a filename:
const char path[]{"D:\\projects\\cpp\\hello.cpp"};
const char* filename{std::strrchr(path, '\\')};
if (filename != nullptr) {
std::cout << (filename + 1) << "\n"; // hello.cpp
}
filename points at the last backslash; +1 skips it and prints to '\0'.
Concatenation and copy: strcat / strcpy
char dest[50]{"Hello "};
char source[]{"World"};
std::strcat(dest, source); // dest = "Hello World"
std::strcat(dest, " and goodbye");
strcat starts at dest's '\0', overwrites it, and appends source. The caller must ensure dest is large enough; otherwise it writes out of bounds — exactly why many compilers mark these functions unsafe.
char dest[30]{"Hello "};
std::strncat(dest, "there is a bird on my window", 6); // appends "there "
char source[]{"C++ is a multi-purpose language"};
char dest[std::strlen(source) + 1]; // +1 for '\0'
std::strcpy(dest, source);
strcpy overwrites the target from its first character; strncpy(dest, src, n) copies only the first n. The overwrite behaviour is easy to see:
char dest[]{"abcde f"};
std::strncpy(dest, "hello", 5); // "hellof": first five characters replaced
Why the target needs +1: strlen does not count '\0', but the copy must carry the terminator along.
std::string: hiding the details
Character arrays impose two daily burdens:
- You must track capacity; overruns crash.
- You must maintain
'\0'.
std::string puts both under the type's control.
#include <string>
std::string empty; // empty
std::string planet{"Where the sky is blue"};
std::string copy{planet}; // copy
std::string message{"Hello world", 5}; // "Hello"
std::string repeated(4, 'e'); // "eeee"
std::string saying{"Where the sky is blue", 6, 5}; // from index 6, length 5: "the s"
The last form is the easiest to misread: the first number is the start index, the second is the length.
std::string planet{"Where the sky is blue"};
planet = "A much longer string than before";
Assignment grows automatically: the old buffer is freed and a new one allocated as needed. With a const char* the old literal is simply abandoned in place while the pointer is repointed — std::string avoids that waste.
Takeaways
<cctype>predicates returnint; nonzero is true, and the exact value is unspecified.- Character arrays are native arrays:
std::sizeworks until they decay to pointers. strlenexcludes'\0';sizeofincludes it;sizeof(pointer)is just the pointer.strcmppromises only the sign: negative, zero, or positive.strchr/strrchrreturn pointers, ornullptrwhen not found; let the returned pointer advance.strcat/strcpynever check capacity; the caller must reserve room, including for'\0'.std::stringmanages length, terminator, and growth, and is the default choice.
Comprehension check
1. Why can std::isalnum('7') be used directly in an if, while writing == 1 would be wrong?
Reference answer: the standard only specifies "nonzero when the character is alphanumeric, zero otherwise", not that the nonzero value is 1. if (std::isalnum('7')) relies on zero being false and nonzero true, which is guaranteed. if (std::isalnum('7') == 1) assumes a particular return value that another library implementation may not produce.
2. For char s[]{"Hello"}, what are strlen(s) and sizeof(s)? If const char* p = s;, what is sizeof(p)?
Reference answer: strlen(s) is 5 (counts up to but excludes '\0'); sizeof(s) is 6 (includes '\0'); sizeof(p) is the pointer's own size — commonly 8 on 64-bit platforms — independent of the string length.
3. In the strchr loop, why does the lecture use a separate result pointer instead of advancing the original string pointer?
Reference answer: strchr always searches from the pointer it is given. ++message merely shifts the search start by one character; it never jumps past the match, so the next search rescans almost the whole string and the loop runs once per character (25 times). Assigning the return value back — result = strchr(result, target) — makes the next search begin after the match while leaving the original pointer intact. Declaring the original as const char* const turns such accidental modification into a compile error.
4. Why is the following dangerous, and how would you fix it?
char dest[]{"Hello"};
std::strcat(dest, " World");
Reference answer: dest is initialized from a literal, so it holds 6 characters including '\0'. strcat appends " World", which needs at least 12 bytes, so it writes past the array — undefined behaviour. Fix it by giving explicit capacity, e.g. char dest[32]{"Hello"};, or by using std::string dest{"Hello"}; dest += " World";.
5. When copying a string of length n with strcpy, how large must the destination be, and why is n not enough?
Reference answer: it must hold n + 1 characters. strlen returns n without the terminating '\0', but strcpy copies the terminator too, so one extra cell is required.
6. What does std::string s{"Where the sky is blue", 6, 5}; contain?
Reference answer: "the s". The 6 is the starting index (W is 0, t is 6) and the 5 is the count: t, h, e, space, s.
Relation to neighbouring chapters
The previous chapter covered references and const, which underpin the read-only pointer discipline used here; std::string, introduced at the end, is used from here on. The next chapter turns to the one definition rule, linkage, and declarations versus definitions — the groundwork for organizing multiple source files, and for understanding how a type such as std::string works across translation units.