Lecture 2 (Week 1 - Thursday): 类型与结构体 (Types & Structs)

目录 · ← l1 · l3 →

Lecture 2 (Week 1 - Thursday): 类型与结构体 (Types & Structs)

概述

本讲正式进入 C++ 语言本身,核心是回答两个问题:C++ 程序什么时候出错误(编译期 vs 运行期)与 如何组织数据(struct 与 std::pair)。课堂先用 Python 与 C++ 的对比阐明”C++ 是编译型、静态类型语言,错误尽量在编译期暴露”,然后引入 struct 把多个字段捆绑成新类型,用 std::pairusing/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——这是静态类型下最常见的”意外”之一。
  • 函数重载(Function Overloading)
    • 定义与目的:定义同名但参数列表不同的多个函数,编译器根据实参类型选择版本。
    • 核心语法double axolotl(int x)double axolotl(double x) 可共存。
    • 设计意图与最佳实践axolotl(2) 选 int 版本返回 5.0(double)2 + 3);axolotl(2.0) 选 double 版本返回 6.02.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> 是标准库提供的模板结构体,只有两个字段 firstsecond,用于”任意两个值打包”。
    • 核心语法
      #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 并返回;
  • mainissueNewID() 的返回值被拷贝初始化到 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”。

特性机制解说

  • 嵌套 pairSolutionstd::pair<bool, Zeros>Zeros 又是 std::pair<double, double>——两个返回值被”缝合”成一个返回值;
  • 花括号返回return { true, { ... } }; 利用 C++11 的列表初始化按序构造 pair,比 std::make_pair 更简洁;
  • using 别名:把三层嵌套的类型名抽象成 Zeros/Solution,签名一目了然;
  • auto 推导resultsolutions 的类型由编译器静态推导,等价于手写完整类型名;
  • 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)的对比

  • using vs typedef: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 的 Course struct(Title、Number of Units、Quarter 三个字段)正是”struct 打包数据”的实战——用列表初始化构造每个 Course 记录;
  • std::pairusing 别名在 A1 的工具代码(如 split 返回 std::vector<std::string>)中会反复出现;
  • 建议现在就练熟:声明一个 struct → 用 {} 初始化 → 用 . 访问字段 → 用 auto 接收函数返回值,A1 拿到手就能直接上手。