第 02 章
第 2 章:注释、错误、语句与函数
本章对应长视频 1:55:35–3:00:43。从注释与三类错误讲起,建立语句、函数与输入输出流的基本模型,再用一个简化内存模型解释程序是怎样被 CPU 逐条执行的,最后区分核心语言、标准库与 STL。
Chapter 2: Comments, Errors, Statements and Functions
Covers 1:55:35–3:00:43 of the long video. Starting from comments and the three kinds of problems, it builds the model of statements, functions and I/O streams, uses a simplified memory model to explain how the CPU executes a program line by line, and separates the core language, the standard library and the STL.
篇内目录
这一章在讲什么
第 2 章覆盖 1:55:35–3:00:43。它把"能编译"推进到"能组织":注释怎么写,出错时怎么判断是哪一类错误,语句和函数怎样构成程序,数据怎样从键盘进来、又怎样送到终端。章末用一个简化内存模型解释程序运行的真实过程,并厘清三个常被混用的名词:核心语言、标准库、STL。
时间轴
| 时间 | 内容 |
|---|---|
| 1:55:37 | 单行注释与块注释,块注释不能嵌套 |
| 2:01:51 | 三类问题:编译期错误、运行期错误、警告 |
| 2:13:01 | 语句:以分号结尾,按顺序执行 |
| 2:16:10 | 函数:返回值、名字、参数、函数体 |
| 2:31:29 | 输出流 cout / cerr / clog,输入流 cin |
| 2:37:16 | 带空格输入与 getline |
| 2:50:02 | 执行模型与简化内存模型 |
| 2:56:50 | 核心语言、标准库、STL 的区别 |
注释
注释是写给人看的,编译器会完全忽略。
// 单行注释:从 // 到行尾
/*
块注释:
可以跨多行
*/
int main() {
std::cout << "Hello World" << std::endl; // 也可以写在语句后面
return 0;
}
一个必须记住的限制:块注释不能嵌套。/* ... /* ... */ ... */ 会在第一个 */ 处结束,后面的内容被当成代码,产生一堆莫名其妙的编译错误。
注释要克制。课程的说法很直白:注释太多会把程序变成小说。好的注释解释"为什么这样做",而不是重复代码已经说清楚的事情。
三类问题
把代码变成可执行文件的过程里,问题只可能出现在三个位置:
| 类型 | 出现时机 | 后果 | 例子 |
|---|---|---|---|
| 编译期错误 | 编译时 | 不生成二进制 | 漏写分号、变量未声明 |
| 警告 | 编译时 | 仍然生成二进制 | 整数除以 0、隐式窄化 |
| 运行期错误 | 运行时 | 结果错误,或程序崩溃 | 除以 0 后使用结果 |
编译期错误
std::cout << "Hello World" << std::endl // 漏了分号
return 0;
编译器会报 expected ';' before 'return'。这类错误的特征是:构建失败,没有可执行文件。这其实是好事——编译器在问题进入运行阶段之前就拦住了它。
警告
int value = 7 / 0; // 编译通过,但给出警告
gcc 会警告 "division by zero",构建仍然成功。警告不是装饰:它说明代码里存在一个编译器认为可疑的地方,通常应该修掉,而不是忽略。
运行期错误
把上面的除法结果打印出来:
int value = 7 / 0;
std::cout << value << std::endl;
程序可能直接异常终止。整数除以零在 C++ 里是未定义行为,编译器不做强制检查,运行时就可能崩。所谓"运行期错误",就是编译阶段看不出来、跑起来才发现的问题。
一句话总结: 编译期错误让你拿不到程序,警告让你拿到一个有隐患的程序,运行期错误让你拿到一个会出错的程序。
语句
语句是程序的基本执行单位,以分号结束,按从上到下的顺序执行。
int first_number {3}; // 语句 1
int second_number {7}; // 语句 2
int sum = first_number + second_number; // 语句 3
std::cout << sum << std::endl; // 语句 4
漏掉任意一个分号,都会变成编译期错误。一条语句可以跨行书写,编译器只认分号,不认换行。
多语句函数体的最后一个右花括号表示 main 结束;main 结束意味着程序结束,返回值交给操作系统。
函数
函数是一个可以反复调用的代码块:给它输入(参数),它返回输出(返回值)。
int add_numbers(int first, int second) {
int result = first + second;
return result;
}
四个组成部分:
- 返回类型:
int,表示返回一个整数。 - 函数名:
add_numbers。 - 参数列表:
(int first, int second),每个参数都要写类型。 - 函数体:花括号内部,以
return结束并交出结果。
调用:
int main() {
int sum = add_numbers(25, 7);
std::cout << sum << std::endl; // 32
std::cout << add_numbers(3, 42) << std::endl; // 45
return 0;
}
两条规则必须记住:
- 函数必须先定义(或先声明)再使用。把
add_numbers放到main后面而不声明,会直接编译失败。 - 参数是值传递:调用时把
25和7复制给first、second,函数内部改的是副本。
把加法从 main 搬到函数里,换来的是复用:换个数字再调用一次即可,不必复制粘贴整段逻辑。
输入输出流
C++ 把输入输出统一建模成流:数据沿着一条管道流动。
输出:cout、cerr、clog
std::cout << "Hello C++20" << std::endl;
std::cerr << "Something went wrong" << std::endl;
std::clog << "A log message" << std::endl;
三者都使用 <<,区别在用途:
| 流 | 用途 |
|---|---|
std::cout | 普通输出 |
std::cerr | 错误信息,不带缓冲 |
std::clog | 日志信息,带缓冲 |
不少终端会把 cerr 显示成红色,这就是"用不同的流表达不同的意图"的价值。目前只需知道它们都通向终端。
箭头方向也值得注意:cout 的 << 指向左边,表示数据从程序流向流对象;cin 的 >> 指向右边,表示数据从流对象进入变量。
输入:cin
int age {0};
std::string name;
std::cout << "Please type your name and age: " << std::endl;
std::cin >> name >> age;
std::cout << "Hello " << name << ", you are " << age << " years old" << std::endl;
cin >> name >> age 演示了链式输入:一次读入两个值,按空格分隔。需要 #include <string> 才能使用 std::string。
带空格的输入:getline
cin >> 遇到空格就停止。输入 Daniel Gakwaya 时,name 只会拿到 Daniel,剩下的 Gakwaya 会被当成年龄,程序随即错乱。
解决办法是 std::getline,它读到换行为止:
std::string full_name;
std::cout << "Please type your full name: ";
std::getline(std::cin, full_name);
参数是"从哪个流读"和"存到哪个变量"。之后再用 cin >> age 读年龄即可。
补充: 混用
cin >>和getline时有个经典陷阱:cin >>会把换行符留在缓冲区,紧接着的getline立刻读到空行。常见修法是先std::cin.ignore()吃掉残留的换行。本章还没涉及,但值得先记住。
程序是怎样运行的
课程给了一个简化模型,足以解释后面要讲的内存、指针与函数调用。
- 编译器把
main.cpp变成二进制rooster.exe,存放在硬盘上。 - 运行时,操作系统把二进制整体载入内存(RAM)。
- CPU 从
main的第一条语句开始,逐条执行。 - 遇到变量声明,CPU 在内存里划一块空间并写入初始值。
- 遇到函数调用,CPU 记下返回地址,跳到函数所在的位置执行,执行完再跳回来。
举例:
int add_numbers(int a, int b) { return a + b; }
int main() {
int a {10};
int b {5};
int c;
std::cout << "Statement 1" << std::endl;
c = add_numbers(a, b);
std::cout << "Statement 2" << std::endl;
return 0;
}
执行 c = add_numbers(a, b) 时,CPU 先把返回地址存起来,再跳到函数体:把 10、5 分别放进参数 a、b,相加得到 15,把结果交回 c,然后跳回原来的位置继续。
这个模型解释了两件事:
- 未初始化的
int c;里是垃圾值——那块内存只是被划出来,没人写过它。 - 函数调用有成本:跳转、传参、保存返回地址。编译器可能把简单函数内联,即在调用点直接展开,省掉跳转。理解内存模型才能理解这类优化。
核心语言、标准库与 STL
三个词经常被混用,课程专门区分:
| 名称 | 含义 | 例子 |
|---|---|---|
| 核心语言 | C++ 语法与基本类型,规则本身 | int、函数定义规则、语句、作用域 |
| 标准库 | 现成的高质量组件,随编译器提供 | iostream、string、vector |
| STL | 标准库中处理"容器 + 算法 + 迭代器"的那部分 | std::vector、std::sort、迭代器 |
iostream 属于标准库;写 int 属于使用核心语言;std::vector 与 std::sort 属于 STL。这个区分在查文档时很有用:查核心语言问题看语法规范,查 setw 这类工具看标准库文档。
本章结论
- 注释用
//与/* */,块注释不能嵌套。 - 问题分三类:编译期错误(拿不到程序)、警告(能编译但有隐患)、运行期错误(运行才出错)。
- 语句以分号结尾,按顺序执行;函数有返回类型、名字、参数和函数体,必须先定义后使用。
cout/cerr/clog是三条输出流,cin是输入流;带空格的输入用getline。- 程序被载入内存后由 CPU 逐条执行;函数调用要保存返回地址,因此有成本。
- 核心语言、标准库、STL 是三个层次,不要混为一谈。
理解检验
1. 编译期错误和警告都会在编译阶段出现,为什么课程说前者"更好"?
参考答案:编译期错误会阻止生成可执行文件,问题被迫在运行前解决;警告则允许程序生成并运行,隐患被带进了运行阶段。前者的失败是确定的、可定位的,后者的失败取决于输入和运行环境,可能很久以后才暴露。所以"编译不过"在工程上往往比"带着警告上线"更安全。
2. 为什么块注释不能嵌套?写出 /* a /* b */ c */ 的实际解析结果。
参考答案:注释由词法分析器按"遇到 */ 就结束"处理,不做括号配对。上面这段在第一个 */ 处结束,注释内容只有 a /* b;剩下的 c */ 被当作代码,其中 c 是未声明的标识符、*/ 是非法记号,于是产生编译错误。
3. 把 add_numbers 的定义放到 main 之后、且不在前面声明,会发生什么?为什么?
参考答案:编译失败,报"add_numbers was not declared in this scope"一类错误。C++ 按从上到下的顺序处理源文件,编译到 main 里的调用时还没有见过这个名字,无法确定参数与返回类型。解决办法是在 main 之前放函数声明 int add_numbers(int, int);,或者直接把定义移到前面。
4. cin >> name 输入 Daniel Gakwaya 之后,name 里是什么?接下来的 cin >> age 会得到什么?
参考答案:name 只得到 Daniel,因为 >> 以空白字符为分隔,遇到空格就停止。缓冲区里还留着 Gakwaya\n,接下来的 cin >> age 试图把 Gakwaya 解析成整数,解析失败,age 保持原值(C++11 起失败时会被置为 0),后续的输入也会失效。正确做法是用 std::getline(std::cin, name) 读取整行。
5. 未初始化的 int c; 为什么可能含有"垃圾值"?这和内存模型有什么关系?
参考答案:声明 int c; 只是让编译器在栈上划出 4 个字节,并没有写入任何值。那块内存之前可能被别的函数用过,残留着旧数据,所以读出来是一个没有意义的数。传统内存模型里局部变量不会自动清零(static 变量和全局变量才会),因此必须显式初始化,例如 int c{}; 或 int c = 0;。
6. 函数调用为什么有成本?编译器用什么手段降低这个成本?
参考答案:调用时要保存返回地址、把实参复制到形参、跳转到函数代码,执行完再跳回原位置,这些都需要指令和时间。编译器可以把短小、频繁调用的函数内联:把函数体直接展开到调用点,省掉跳转和返回。课程的内存模型正是理解内联的前提——只有看到"跳转"这一步,才知道内联省掉的是什么。
与前后章的关系
本章搭好了"程序由语句和函数组成、通过流与外界交换数据"的骨架。第 3 章转向数据本身:变量是什么、内存里的 0 和 1 怎样用二进制、八进制、十六进制表示,以及整数、浮点数各自占多少字节、能存多大范围。第 4 章继续讲布尔、字符与 auto,把基本类型补全。
What this chapter covers
Chapter 2 spans 1:55:35–3:00:43. It moves from "it compiles" to "it is organised": how to write comments, how to tell which of the three kinds of problems you are looking at, how statements and functions compose a program, and how data enters from the keyboard and leaves for the terminal. The chapter closes with a simplified memory model that explains what actually happens at run time, and with a clear separation of three terms that are often confused: core language, standard library, STL.
Timeline
| Time | Topic |
|---|---|
| 1:55:37 | Line and block comments; block comments do not nest |
| 2:01:51 | Three kinds of problems: compile-time, run-time, warnings |
| 2:13:01 | Statements: semicolon-terminated, executed in order |
| 2:16:10 | Functions: return type, name, parameters, body |
| 2:31:29 | Output streams cout / cerr / clog, input stream cin |
| 2:37:16 | Reading input with spaces: getline |
| 2:50:02 | Execution model and simplified memory model |
| 2:56:50 | Core language, standard library, STL |
Comments
A comment is for humans; the compiler ignores it completely.
// A line comment: from // to the end of the line
/*
A block comment:
may span several lines
*/
int main() {
std::cout << "Hello World" << std::endl; // also fine after a statement
return 0;
}
One limit must be remembered: block comments do not nest. In /* ... /* ... */ ... */ the comment ends at the first */; everything after is parsed as code and produces a pile of confusing errors.
Use comments sparingly. The lecturer's phrasing is blunt: too many comments turn a program into a novel. A good comment explains why, not what the code already says.
Three kinds of problems
When turning source into an executable, problems can only appear at three points:
| Kind | When | Consequence | Example |
|---|---|---|---|
| Compile-time error | at build time | no binary produced | missing semicolon, undeclared variable |
| Warning | at build time | binary still produced | integer division by zero, implicit narrowing |
| Run-time error | while running | wrong result or crash | using the result of a division by zero |
Compile-time errors
std::cout << "Hello World" << std::endl // semicolon missing
return 0;
The compiler reports expected ';' before 'return'. The defining feature: the build fails and there is no executable. That is a good thing — the problem is stopped before it can run.
Warnings
int value = 7 / 0; // compiles with a warning
gcc warns "division by zero" but the build succeeds. A warning is not decoration: it says the compiler finds something suspicious, and it should normally be fixed rather than ignored.
Run-time errors
Print the result of that division:
int value = 7 / 0;
std::cout << value << std::endl;
The program may terminate abruptly. Integer division by zero is undefined behaviour in C++; the compiler is not obliged to detect it, so it surfaces only when the program runs. A run-time error is precisely something invisible at build time that appears while running.
In one line: a compile-time error leaves you with no program, a warning leaves you with a risky program, a run-time error leaves you with a program that misbehaves.
Statements
A statement is the basic unit of execution, ends with a semicolon, and runs top to bottom.
int first_number {3}; // statement 1
int second_number {7}; // statement 2
int sum = first_number + second_number; // statement 3
std::cout << sum << std::endl; // statement 4
Miss any semicolon and you get a compile-time error. A statement may span several lines; the compiler follows semicolons, not newlines.
The closing brace of main marks the end of main, and the end of main is the end of the program; the return value goes to the operating system.
Functions
A function is a reusable block: give it inputs (parameters), get an output (return value).
int add_numbers(int first, int second) {
int result = first + second;
return result;
}
Four parts:
- Return type:
int— it returns an integer. - Name:
add_numbers. - Parameter list:
(int first, int second); every parameter needs a type. - Body: inside the braces, ending with
returnto hand back the result.
Calling it:
int main() {
int sum = add_numbers(25, 7);
std::cout << sum << std::endl; // 32
std::cout << add_numbers(3, 42) << std::endl; // 45
return 0;
}
Two rules matter:
- A function must be defined (or declared) before use. Putting
add_numbersaftermainwithout a declaration fails to compile. - Parameters are passed by value. At the call,
25and7are copied intofirstandsecond; the function modifies copies.
Moving the addition out of main buys reuse: call it again with other numbers instead of copying the logic.
I/O streams
C++ models input and output as streams: data flows along a pipe.
Output: cout, cerr, clog
std::cout << "Hello C++20" << std::endl;
std::cerr << "Something went wrong" << std::endl;
std::clog << "A log message" << std::endl;
All three use <<; only the intent differs:
| Stream | Purpose |
|---|---|
std::cout | ordinary output |
std::cerr | errors, unbuffered |
std::clog | log messages, buffered |
Many terminals render cerr in red, which is exactly the value of expressing intent through different streams. For now, all three reach the terminal.
The arrow direction is worth noticing: cout's << points left, data flows from program into the stream; cin's >> points right, data flows from the stream into a variable.
Input: cin
int age {0};
std::string name;
std::cout << "Please type your name and age: " << std::endl;
std::cin >> name >> age;
std::cout << "Hello " << name << ", you are " << age << " years old" << std::endl;
cin >> name >> age shows chained input: two values read at once, separated by whitespace. #include <string> is required for std::string.
Input with spaces: getline
cin >> stops at whitespace. Type Daniel Gakwaya and name receives only Daniel; Gakwaya is then handed to age, and the program falls apart.
std::getline reads up to the newline instead:
std::string full_name;
std::cout << "Please type your full name: ";
std::getline(std::cin, full_name);
The two arguments are the stream to read from and the variable to store into. Afterwards cin >> age reads the age normally.
Supplement: mixing
cin >>andgetlinehas a classic trap:cin >>leaves the newline in the buffer, and the followinggetlineimmediately reads an empty line. The usual fix isstd::cin.ignore()first. It is not needed yet, but worth remembering.
How a program runs
The course gives a simplified model that is enough to explain memory, pointers and function calls later.
- The compiler turns
main.cppinto the binaryrooster.exeon disk. - At run time the operating system loads the whole binary into RAM.
- The CPU starts at the first statement of
mainand executes statements one after another. - A variable declaration reserves space in memory and writes the initial value.
- A function call records the return address, jumps to the function, runs it, and jumps back.
Example:
int add_numbers(int a, int b) { return a + b; }
int main() {
int a {10};
int b {5};
int c;
std::cout << "Statement 1" << std::endl;
c = add_numbers(a, b);
std::cout << "Statement 2" << std::endl;
return 0;
}
At c = add_numbers(a, b) the CPU saves the return address, jumps into the body, copies 10 and 5 into a and b, adds them to get 15, hands the result back to c, and returns to where it left off.
Two consequences:
- The uninitialised
int c;holds garbage — the space was reserved but never written. - Function calls cost something: the jump, the parameter copies, the saved return address. The compiler may inline a small function, expanding it at the call site and removing the jump. Understanding the memory model is what makes such optimisations intelligible.
Core language, standard library, STL
Three terms are routinely mixed up; the course separates them:
| Name | Meaning | Examples |
|---|---|---|
| Core language | C++ syntax and fundamental types, the rules themselves | int, function definition rules, statements, scope |
| Standard library | Ready-made, high-quality components shipped with the compiler | iostream, string, vector |
| STL | The container + algorithm + iterator part of the standard library | std::vector, std::sort, iterators |
iostream is standard library; writing int uses the core language; std::vector and std::sort belong to the STL. The distinction helps when reading documentation: language questions go to the grammar reference, setw-style tools to the library reference.
Chapter summary
- Comments use
//and/* */; block comments do not nest. - Problems are compile-time errors (no program), warnings (a program with a hidden risk), or run-time errors (a program that fails while running).
- Statements end with semicolons and run in order; functions have a return type, a name, parameters and a body, and must exist before use.
cout/cerr/clogare three output streams,cinis the input stream;getlinereads spaces.- The program is loaded into memory and executed statement by statement; calls save a return address and therefore cost something.
- Core language, standard library and STL are three layers; do not conflate them.
Comprehension check
1. Both compile-time errors and warnings appear during the build. Why does the course call the former "better"?
Reference answer: A compile-time error prevents the executable from being produced, forcing the issue to be fixed before anything runs. A warning lets the program be built and shipped with the hidden risk intact. The former failure is deterministic and localisable; the latter depends on inputs and environment and may surface much later. In engineering terms, "it does not compile" is usually safer than "it ships with warnings".
2. Why can block comments not nest? What exactly does /* a /* b */ c */ parse as?
Reference answer: Comment handling is lexical: the first */ closes the comment, with no bracket matching. Here the comment contains only a /* b; the remaining c */ is code, where c is an undeclared identifier and */ is an invalid token, producing compile errors.
3. What happens if add_numbers is defined after main with no declaration in between, and why?
Reference answer: The build fails with "add_numbers was not declared in this scope". C++ processes a source file top to bottom; at the call inside main the name has not been seen, so the compiler cannot know its parameter and return types. The fix is a declaration before main (int add_numbers(int, int);) or moving the definition earlier.
4. After cin >> name receives Daniel Gakwaya, what is in name, and what does the following cin >> age get?
Reference answer: name holds only Daniel, because >> stops at whitespace. The buffer still holds Gakwaya\n; cin >> age tries to parse Gakwaya as an integer, fails, and (since C++11) sets age to 0, with the stream left in a failed state. Reading the whole line with std::getline(std::cin, name) avoids the problem.
5. Why may an uninitialised int c; contain "garbage", and how does the memory model explain it?
Reference answer: Declaring int c; only reserves four bytes on the stack; nothing is written. That memory may have been used by another function and still holds old data, so reading it yields a meaningless value. In the conventional model, automatic local variables are not zero-initialised (only static and global variables are), so they must be initialised explicitly — int c{}; or int c = 0;.
6. Why does a function call have a cost, and how does the compiler reduce it?
Reference answer: A call must save the return address, copy arguments into parameters, jump to the function, then jump back — all of which take instructions and time. The compiler can inline short, frequently called functions, expanding the body at the call site and removing the jump and return. The memory model in this chapter is what makes the saved "jump" visible, and therefore what makes inlining comprehensible.
How it connects
This chapter builds the skeleton: a program made of statements and functions, exchanging data with the world through streams. Chapter 3 turns to the data itself — what a variable is, how zeros and ones are written in binary, octal and hexadecimal, and how many bytes integers and floating-point numbers occupy. Chapter 4 completes the fundamental types with booleans, characters and auto.