Lecture 2 (Week 1 - Thursday): 类型与结构体 (Types & Structs)
Lecture 2 (Week 1 - Thursday): 类型与结构体 (Types & Structs)
概述
本讲正式进入 C++ 语言本身,核心是回答两个问题:C++ 程序什么时候出错误(编译期 vs 运行期)与 如何组织数据(struct 与 std::pair)。课堂先用 Python 与 C++ 的对比阐明”C++ 是编译型、静态类型语言,错误尽量在编译期暴露”,然后引入 struct 把多个字段捆绑成新类型,用 std::pair 与 using/auto 让代码更简洁。最后以”求解二次方程”的课堂代码 Demo 串起全部知识:std::pair 嵌套返回多值、using 类型别名、auto 类型推导。这些是后续所有讲座的地基。
核心特性与语法详解
- 编译期(Compile Time)vs 运行期(Run Time)
- 定义与目的:解释型语言(如 Python)逐行翻译、逐行执行,全程都在运行期;编译型语言(如 C++)先把整个源码翻译成机器码打包成可执行文件(编译期),再执行(运行期)。
- 核心语法:
g++ main.cpp(编译)→./a.out(运行)。 - 设计意图与最佳实践:错误发生在哪个阶段决定了代价。Python 的
print("hello" * "world")会先打印Running...再抛TypeError(运行期才发现);同样的std::cout << hello * world在 C++ 中直接编译失败(error: no match for 'operator*' (operand types are 'std::string' and 'std::string'))。编译期错误 = 免费的错误检查。
- 静态类型(Static Typing)
- 定义与目的:C++ 中每个变量都必须声明类型,声明后类型不可变;编译器在生成机器码之前就检查类型。
- 核心语法:
int a = 3;/std::string b = "test";。 - 设计意图与最佳实践:对比 Python(动态类型,
d = 106; d = "hello"合法),C++ 中int d = 106; d = "hello";是编译错误。静态类型带来三点好处:更高效(运行期无需类型标签与检查)、更易理解与推理、错误检查更早更彻底(如int add_3(int x)传入字符串在编译期就被拒绝)。
- 内置类型(Built-in Types)
- 定义与目的:类型是变量的”类别”。C++ 自带基础类型:
int(106)、double(71.4)、std::string(”Welcome to CS106L!”)、bool(true/false)、size_t(12,非负,常用于索引与大小)。 - 核心语法:
double b = 3.2 * 5 - 1;(注意:3.2是 double,所以整条表达式是 double)。 - 设计意图与最佳实践:注意
int c = 5 / 2;中整数除法截断,结果是2而不是2.5——这是静态类型下最常见的”意外”之一。
- 定义与目的:类型是变量的”类别”。C++ 自带基础类型:
- 函数重载(Function Overloading)
- 定义与目的:定义同名但参数列表不同的多个函数,编译器根据实参类型选择版本。
- 核心语法:
double axolotl(int x)与double axolotl(double x)可共存。 - 设计意图与最佳实践:
axolotl(2)选 int 版本返回5.0((double)2 + 3);axolotl(2.0)选 double 版本返回6.0(2.0 * 3)。重载解析发生在编译期,是”静态类型”的直接红利。
- struct(结构体)
- 定义与目的:把多个命名变量捆绑成一个新类型。解决”一个函数如何返回多个值”的根本问题——例如学生 ID 需要同时返回 name、sunet、idNumber。
- 核心语法:
#include <string> struct StanfordID { std::string name; // 字段(field):有名字有类型 std::string sunet; int idNumber; }; int main() { StanfordID id; // 声明一个结构体变量 id.name = "THE Stanford Tree"; // 用 '.' 访问字段 return 0; } - 设计意图与最佳实践:
issueNewID()的返回类型直接写StanfordID,函数体内构造并return id;即可返回”三个值”。THE BIG IDEA:struct 把命名变量捆绑成新类型。
- 列表初始化 / 统一初始化(List Initialization,C++11 起)
- 定义与目的:用花括号
{}一次性初始化全部字段,替代逐字段赋值。 - 核心语法:
#include <string> struct StanfordID { std::string name; std::string sunet; int idNumber; }; int main() { StanfordID tree = { "THE Stanford Tree", "theTREE", 0000002 }; // '=' 可省略 StanfordID lelandjr { "Leland Stanford Jr", "thejunior", 5430282 }; return 0; } - 设计意图与最佳实践:值的顺序必须与字段声明顺序一致;
=可选。这是 Lecture 3 的重点(uniform initialization 与窄化转换),本讲先用起来。
- 定义与目的:用花括号
- std::pair(标准库的”通用双字段结构体”)
- 定义与目的:
std::pair<T1, T2>是标准库提供的模板结构体,只有两个字段first和second,用于”任意两个值打包”。 - 核心语法:
#include <string> #include <utility> int main() { std::pair<std::string, int> dozen { "Eggs", 12 }; std::string item = dozen.first; // "Eggs" int quantity = dozen.second; // 12 return 0; } - 设计意图与最佳实践:其实现本质就是一个模板:
template <typename T1, typename T2> struct pair { T1 first; T2 second; };所以课堂上的
struct Order { string item; int quantity; };、struct Name {...};、struct Point {...};都可以用std::pair表达——”Notice anything?” 它们都只是”两个字段”。模板细节(Lecture 8 深入),本讲只需会使用。
- 定义与目的:
- std —— C++ 标准库与命名空间
- 定义与目的:标准库提供内置类型、函数等;使用前必须
#include对应头文件,并加std::前缀。 - 核心语法:
#include <string>→std::string;#include <utility>→std::pair;#include <iostream>→std::cout, std::endl。 - 设计意图与最佳实践:
using namespace std;虽可省略前缀,但会引入歧义(若自己定义了sort,与std::sort冲突),是坏风格。#include的机制是文本替换:把头文件内容原样粘贴进源文件,之后才能使用其中定义的名字。查文档认准 cppreference.com(幻灯片明确提醒避开过时且广告多的 cplusplus.com)。
- 定义与目的:标准库提供内置类型、函数等;使用前必须
- using 类型别名(Type Alias,C++11 起)
- 定义与目的:给长类型名起短名字。
- 核心语法:
using Zeros = std::pair<double, double>; - 设计意图与最佳实践:
using就像”类型的变量”。把std::pair<bool, std::pair<double, double>> solveQuadratic(...)拆成using Zeros = ...; using Solution = std::pair<bool, Zeros>;后,签名变成Solution solveQuadratic(double a, double b, double c);,可读性大增。
- auto 类型推导(C++11 起)
- 定义与目的:让编译器从初始化表达式推断变量类型。
- 核心语法:
auto result = solveQuadratic(a, b, c); - 设计意图与最佳实践:
result的类型仍是std::pair<bool, std::pair<double, double>>(编译器查solveQuadratic的返回类型填入),与手写完全等价。auto仍然是静态类型:auto i = 1; i = "hello!";编译失败。幻灯片对比了两种写法:长类型名用auto更清晰(auto result = ...),短类型名手写更清晰(int i = 1;)——按需选用。
代码示例与逐步解说(核心)
示例 1:struct 打包多字段并返回(C++11,改写自课堂 StanfordID 例)
#include <iostream>
#include <string>
struct StanfordID {
std::string name;
std::string sunet;
int idNumber;
};
StanfordID issueNewID() {
// 列表初始化:顺序与字段声明一致
StanfordID id { "THE Stanford Tree", "theTREE", 0000002 };
return id;
}
int main() {
StanfordID id = issueNewID();
std::cout << id.name << " (" << id.sunet << ") #" << id.idNumber << std::endl;
return 0;
}
// THE Stanford Tree (theTREE) #2
代码做什么:
struct StanfordID声明三个字段;issueNewID()用花括号列表初始化一个 ID 并返回;main中issueNewID()的返回值被拷贝初始化到id,再用.访问三个字段并输出。
特性机制解说:
- 花括号初始化按字段顺序填入(name → sunet → idNumber),编译器在编译期检查数量与类型是否匹配;
return id;返回的是整个 struct 的值拷贝——struct 像内置类型一样可以整体赋值、传参、返回;0000002是十进制整数 2(前导零只是字面量写法),所以输出#2。
示例 2:课堂代码——求解二次方程(C++11/17,cs106l_data/lecture_code/lecture02/main.cpp)
#include <cmath>
#include <iostream>
#include <utility>
using Zeros = std::pair<double, double>;
using Solution = std::pair<bool, Zeros>;
// 求解 ax^2 + bx + c = 0
// 返回:first 表示是否有解;若有,second 为两个根
Solution solveQuadratic(double a, double b, double c) {
double discrim = b * b - 4 * a * c;
if (discrim < 0) return { false, { 106, 106 } }; // 无解:first=false
double root = sqrt(discrim);
return { true, { (-b - root) / (2 * a), (-b + root) / (2 * a) } };
}
int main() {
double a, b, c;
std::cout << "a: "; std::cin >> a;
std::cout << "b: "; std::cin >> b;
std::cout << "c: "; std::cin >> c;
auto result = solveQuadratic(a, b, c); // auto 推导出 Solution 类型
if (result.first) {
auto solutions = result.second;
std::cout << "Solutions: " << solutions.first << ", "
<< solutions.second << std::endl;
} else {
std::cout << "No solutions" << std::endl;
}
return 0;
}
代码做什么:
- 用户输入系数 a、b、c;
solveQuadratic计算判别式b²-4ac,无解时返回{false, {106, 106}}(占位值),有解时用求根公式(-b ± √Δ)/(2a)返回两个根; main中用auto result接收,检查result.first决定打印根还是 “No solutions”。
特性机制解说:
- 嵌套 pair:
Solution是std::pair<bool, Zeros>,Zeros又是std::pair<double, double>——两个返回值被”缝合”成一个返回值; - 花括号返回:
return { true, { ... } };利用 C++11 的列表初始化按序构造pair,比std::make_pair更简洁; using别名:把三层嵌套的类型名抽象成Zeros/Solution,签名一目了然;auto推导:result、solutions的类型由编译器静态推导,等价于手写完整类型名;sqrt来自<cmath>(课堂代码还演示了不加std::前缀调用 C 风格数学函数——在<cmath>下应写std::sqrt更规范)。
示例 3:auto 仍是静态类型(C++11)
#include <string>
int main() {
auto i = 1; // i 被推导为 int
// i = "hello!"; // ❌ 编译错误:不能把 const char* 赋给 int
auto s = std::string("test"); // s 被推导为 std::string
return 0;
}
代码做什么:演示 auto 的推导结果在编译期固定。
特性机制解说:auto 不引入任何动态行为——它只是”让编译器替你写字面类型”。把被注释的行取消注释即可看到编译错误,这印证了”auto 是静态类型”这一幻灯片结论。
示例 4:函数重载(C++11,来自课堂 axolotl 例)
#include <iostream>
// (1) int 版本:返回 double
double axolotl(int x) {
return static_cast<double>(x) + 3; // 类型转换:int → double
}
// (2) double 版本
double axolotl(double x) {
return x * 3;
}
int main() {
std::cout << axolotl(2) << std::endl; // 实参是 int → 调用 (1),输出 5
std::cout << axolotl(2.0) << std::endl; // 实参是 double → 调用 (2),输出 6
return 0;
}
// 5
// 6
代码做什么:同名函数 axolotl 有两个版本(int / double 参数),两次调用分别命中不同版本。
特性机制解说:
- 函数重载 = 同名函数、不同参数列表(数量或类型不同即可共存);
- 重载解析(overload resolution)在编译期依据实参的静态类型完成:
2是 int → 精确匹配 (1),2.0是 double → 精确匹配 (2); - 幻灯片用这个例子演示了
(int)x式类型转换(截断小数)与static_cast的作用:(1)中(double)2 + 3 == 5.0;若实参2.5传入 int 版本则会先被截断为2——类型不匹配时编译器按”隐式转换代价”排序选择,这正是 Lecture 3 窄化转换的伏笔。
示例 5:std::pair 与结构化绑定(C++17)
#include <iostream>
#include <string>
#include <utility>
std::pair<std::string, int> makeOrder() {
return { "Eggs", 12 }; // 列表初始化构造 pair
}
int main() {
auto order = makeOrder(); // order: std::pair<std::string, int>
std::cout << order.first << " x" << order.second << std::endl; // 传统访问
// C++17 结构化绑定:一次性拆开 first / second
auto [item, quantity] = makeOrder();
std::cout << item << " x" << quantity << std::endl;
return 0;
}
// Eggs x12
// Eggs x12
代码做什么:用 std::pair 打包”商品 + 数量”,再用两种方式取出字段。
特性机制解说:
return { "Eggs", 12 };利用列表初始化按序构造pair<std::string, int>,"Eggs"被隐式转换为std::string;auto [item, quantity] = ...是 C++17 结构化绑定(structured bindings):把pair的两个成员按声明顺序解包到两个新变量,省去.first/.second的样板代码;- 结构化绑定同样适用于 struct、数组、tuple——本讲先用 pair 建立直觉,后续讲座会反复用到;它再次体现了”用
auto交给编译器、保持代码简洁”的现代风格。
与旧标准(如C++98)的对比
usingvstypedef:C++98 用typedef std::pair<double, double> Zeros;(类型名在中间,嵌套别名晦涩难读);C++11 的using Zeros = ...;更直观、支持模板别名,是现代 C++ 的推荐写法。
#include <utility>
int main() {
// C++98 风格:typedef(类型名夹在中间)
typedef std::pair<double, double> Zeros98;
typedef std::pair<bool, Zeros98> Solution98;
// 现代 C++ 风格:using 别名(C++11 起,= 号让阅读顺序更自然)
using Zeros = std::pair<double, double>;
using Solution = std::pair<bool, Zeros>;
Solution s { true, { 1.0, 2.0 } }; // 两种写法等价,现代风格更清晰
return 0;
}
auto语义完全不同:C++98 中auto是存储类说明符(auto int x;,表示自动存储期,毫无用处);C++11 起才表示类型推导。- 初始化方式:C++98 只有
StanfordID id; id.name = ...;逐字段赋值或对聚合类型使用T x = { ... };;C++11 的列表初始化适用于一切类型、写法统一(T x { ... };、return { ... };),并附赠窄化检查(详见 Lecture 3)。 std::pair本身 C++98 就有(<utility>中的std::make_pair),但 C++98 没有{}初始化,必须写std::make_pair("Eggs", 12)且类型推导能力弱("Eggs"会被推导为const char*而非std::string)。- 结构化绑定是 C++17 才有的:C++98 解包 pair 只能写
pair.first/pair.second或借助std::tie(C++11)——都没有auto [a, b]直观。 - range-based for / 现代容器用法:C++98 遍历容器只能手写索引或迭代器循环,代码冗长(详见后续讲座)。
关键要点
- C++ 是编译型、静态类型语言:错误尽量在编译期暴露,类型声明后不可变,运行高效。
- struct 把命名变量捆绑成新类型,用
.访问字段;列表初始化{ ... }按字段顺序一次性填值,=可选。 std::pair<T1, T2>是”通用双字段 struct”(first/second),可嵌套使用以返回多个值。- 使用标准库 =
#include对应头文件 +std::前缀;避免using namespace std;。 using造类型别名、auto推导类型(C++11 起)是提升可读性的现代工具,且auto仍是静态类型。- 函数重载(同名不同参)在编译期按实参类型选择版本——静态类型的又一红利。
- 记住内置类型常识:
size_t是非负整数(常用于索引/大小);5 / 2整数除法结果为2。
常见陷阱与注意事项
- 整数除法截断:
int c = 5 / 2;得到2而不是2.5;想得到小数至少一个操作数要写成double。 - 忘记
#include <utility>:用std::pair不包含头文件会编译失败;同理std::string需要<string>。 - 滥用
using namespace std;:与自己的sort等函数重名时产生歧义;规范写法是显式std::。 - 列表初始化顺序错乱:花括号值必须与字段声明顺序一致,否则数据张冠李戴(编译器按序填入,不会替你”按名匹配”)。
- 误以为
auto是动态类型:auto i = 1; i = "hello";编译错误;auto只是编译期推导,不改变静态类型语义。 - 把 struct 当作”只有数据”而忽略初始化:默认声明
StanfordID id;时,内置类型字段(如idNumber)的值是未初始化的,读取是未定义行为——养成用{ }初始化的习惯(Lecture 3 重点)。 - 重载时类型不匹配导致的”意外”调用:如
axolotl(2.5)若只有 int 版本可用,会先把2.5截断成2再调用——留意隐式转换的方向,必要时显式static_cast。
关联作业提示
None(本讲无直接对应作业)。不过 A1: SimpleEnroll 会大量用到本讲知识,可提前预热:
- A1 的
Coursestruct(Title、Number of Units、Quarter 三个字段)正是”struct 打包数据”的实战——用列表初始化构造每个 Course 记录; std::pair与using别名在 A1 的工具代码(如split返回std::vector<std::string>)中会反复出现;- 建议现在就练熟:声明一个 struct → 用
{}初始化 → 用.访问字段 → 用auto接收函数返回值,A1 拿到手就能直接上手。
