返回
Featured image of post CS106L-Chap2-Type&Struct

CS106L-Chap2-Type&Struct

C++的设计哲学

要真正理解C++,你必须先理解它的设计哲学,这与Python等解释型语言截然不同。

编译型 vs. 解释型

想象一下翻译一本书:

  • 解释型 (如Python):你找一位同声传译员。你读一句,他翻译一句给听众。优点是即时反馈,但如果书中有错误,只有读到那里时才会发现。
  • 编译型 (如C++):你找一位笔译专家。他会通读整本书,将其完全翻译成另一种语言,并进行润色和优化,最后交付一本完整的译作。优点是最终的译作阅读起来极其流畅高效,并且译者在翻译过程中就已经帮你找出了所有语法和逻辑错误。缺点是你需要等待翻译完成。

在编程中:

  • 源代码 (Source Code):我们写的 .cpp 文件。
  • 机器码 (Machine Code):CPU能直接执行的0和1指令。
  • 编译器 (Compiler, 如g++):就是那位笔译专家。它将你的全部源代码一次性翻译成一个高效的机器码可执行文件(如Windows上的.exe或Linux/macOS上的无后缀文件)。
  • 解释器 (Interpreter, 如python):就是那位同声传译员。它逐行读取你的代码,实时翻译并执行。

核心认知:C++选择编译是为了性能。编译器拥有全局视野,可以进行深度优化,生成运行速度极快的程序。这是C++在游戏引擎、操作系统、金融交易等高性能领域占据主导地位的根本原因。

“编译”或“解释”是语言实现 (Implementation) 的一种特性,而不是语言本身 (Specification) 的固有属性。

“解释型” 语言可以被编译

以 Python 为例,它通常被认为是解释型语言。但是:

  • 标准实现 (CPython):当你运行 python main.py 时,Python 解释器实际上会先将你的 .py 文件编译成一种中间形式,叫做字节码 (bytecode),并保存为 .pyc 文件。然后,一个虚拟机 (Virtual Machine) 会去解释执行这个字节码。所以,它本身就是一个“编译+解释”的混合体!
  • JIT 编译器 (PyPy):PyPy 是 Python 的一个替代实现。它使用即时编译 (Just-In-Time, JIT) 技术。它会先解释代码,但如果发现某段代码(比如一个循环)被频繁执行,它就会在运行时把这段“热点代码”编译成本地机器码,从而获得接近C++的速度。
  • AOT 编译器 (Cython, Numba):这些工具可以将 Python 代码提前编译 (Ahead-of-Time, AOT) 成 C 代码,然后再用 C 编译器编译成机器码,以获得极致的性能。

“编译型” 语言可以被解释

以 C++ 为例,它被认为是编译型语言的典范。但是:

  • C++ 解释器 (Cling):欧洲核子研究组织 (CERN) 为了数据分析的需求,开发了一个名为 Cling 的 C++ 解释器。你可以在一个交互式命令行里,像用 Python 一样,输入一行 C++ 代码,回车,马上就能看到结果。这在科学计算和快速原型验证中非常有用。

静态类型 vs. 动态类型

  • 动态类型 (Dynamic Typing, 如Python):变量的类型是在运行时确定的,并且可以随时改变。
    1
    2
    
    my_var = 10       # my_var是整数
    my_var = "hello"  # 现在my_var变成了字符串,完全合法
    
  • 静态类型 (Static Typing, 如C++):每个变量的类型在编译时就必须明确声明,并且终生不变
    1
    2
    
    int my_var = 10;          // my_var是整数,并且永远是整数
    // my_var = "hello";      // 错误!编译器会直接拒绝编译这段代码。
    

核心认知:C++选择静态类型是为了安全和效率

  1. 安全 (Safety):编译器就像一个严格的语法警察。它在程序运行前就检查所有类型是否匹配,防止你把字符串和数字相乘这类低级错误溜进最终程序。在编译时发现错误,远比在用户使用时程序崩溃要好得多。 这就是幻灯片24中那些冗长错误信息的意义:它们虽然吓人,但却是编译器在帮你把关。
  2. 效率 (Efficiency):编译器在编译时就确切知道每个变量占多少内存、能执行什么操作。这避免了在运行时进行类型检查的开销,进一步提升了性能。

编译时 (Compile-time) vs. 运行时 (Run-time)

  • 编译时:你运行编译器(g++ main.cpp)的阶段。这个阶段主要进行语法检查、类型检查和代码优化。
  • 运行时:你运行编译生成的可执行文件(./a.out)的阶段。这个阶段才是程序逻辑真正执行的时候。

构建你的数据世界 - struct

我们知道了C++的规则,现在来学习如何用它构建东西。

2.1 问题:如何优雅地管理关联数据?

如幻灯片所示,一个学生ID包含姓名、学号、ID号。这三个数据紧密相关。我们希望把它们当作一个整体来处理,比如从一个函数里一次性返回。

2.2 解决方案:struct - 自定义你的类型

struct 允许你将多个不同类型的变量“捆绑”成一个单一的、新的自定义类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <string> // 我们需要使用string类型,所以要包含它的头文件

// 定义一个名为StanfordID的新类型
// 这就像是创建一个房子的蓝图
struct StanfordID {
    std::string name;
    std::string sunet;
    int idNumber;
}; // 注意这里的分号!

// 使用我们自定义的类型
int main() {
    // 根据蓝图建造一个名为"id"的房子
    StanfordID id; 

    // 给房子的各个房间(成员)赋值
    id.name = "Jacob Roberts-Baca";
    id.sunet = "jtrb";
    id.idNumber = 6504417;

    return 0;
}

核心认知struct 的本质是创造新类型。它将数据的是什么(成员)它叫什么(类型名) 封装在一起,极大地提高了代码的组织性和可读性。

2.3 现代C++初始化:拥抱 {}

逐个成员赋值太繁琐了。现代C++推荐使用统一初始化 (Uniform Initialization),也叫列表初始化 (List Initialization)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 以前的方式
StanfordID id_old;
id_old.name = "Jacob Roberts-Baca";
// ...

// 现代、简洁且更安全的方式
StanfordID id_new { "Jacob Roberts-Baca", "jtrb", 6504417 }; 
// 或者 StanfordID id_new = { ... }; 也可以

// 值的顺序必须与struct中成员声明的顺序完全一致!

为什么 {} 更好?

  • 简洁:一行代码完成创建和初始化。
  • 安全:它可以防止“窄化转换”,比如 int x {3.14}; 编译器会发出警告或错误,而 int x = 3.14; 则会悄无声息地截断小数,可能隐藏bug。

C++标准库 (std)

C++提供了一个极其丰富的标准库 (Standard Library),里面有无数预先写好的高效工具。

揭秘 std#includenamespace

  • #include <...>:它的工作方式非常简单粗暴——复制粘贴#include <string> 就是告诉编译器:“在编译我的代码前,请把 <string> 这个文件的全部内容复制粘贴到我这里。” 如果不#include,编译器就不认识 std::string 是什么。
  • namespace std:为了避免命名冲突(比如你自己也想定义一个叫 vector 的东西),标准库把所有的工具都放在一个叫 std命名空间里。所以,标准库的 string 的完整名称是 std::string:: 是作用域解析运算符,表示“std 里的 string”。
  • 最佳实践:坚持使用 std:: 前缀。永远不要在头文件(.h 文件)中使用 using namespace std;,在源文件(.cpp 文件)中也要谨慎使用,因为它可能导致难以察觉的命名冲突。

std::pair

很多时候,我们只需要一个包含两个成员的简单 struct。标准库已经为我们准备好了,它就是 std::pair

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <string>
#include <utility> // std::pair 在 <utility> 头文件中定义

// 使用自定义 struct
struct Order {
    std::string item;
    int quantity;
};
Order my_order {"Eggs", 12};

// 使用 std::pair,效果相同,但无需自己定义
std::pair<std::string, int> my_pair_order {"Eggs", 12};

// 访问成员
// std::string item_name = my_pair_order.first; // "Eggs"
// int item_quantity = my_pair_order.second; // 12

初探模板 (Templates)

你可能好奇 std::pair<std::string, int> 里的尖括号是什么。这是C++的强大特性——模板。 模板可以被看作是类型的蓝图std::pair 的定义大致如下:

1
2
3
4
5
template <typename T1, typename T2>
struct pair {
    T1 first;
    T2 second;
};

当你写 std::pair<std::string, int> 时,编译器会根据这个蓝图,在编译时自动为你生成一个具体的 struct,就好像你亲手写了下面这个一样:

1
2
3
4
struct pair_string_int {
    std::string first;
    int second;
};

模板是C++实现泛型编程、编写高度可复用代码的核心。

编写更智能、更简洁的代码

std::pair<bool, std::pair<double, double>> 这样的长类型既痛苦又容易出错。现代C++提供了两个利器来解决这个问题。

using:为类型起个别名

using 就像是给一个很长的名字起一个简单好记的“昵称”。

1
2
3
4
5
6
7
8
// 没有 using,代码难以阅读
std::pair<bool, std::pair<double, double>> solveQuadratic(...) { ... }

// 使用 using,代码清晰明了
using Zeros = std::pair<double, double>;        // Zeros 现在是那对 double 的别名
using Solution = std::pair<bool, Zeros>;      // Solution 是最终结果的别名

Solution solveQuadratic(...) { ... } // 清爽!

核心认知using 是提升代码可读性可维护性的强大工具。

auto:让编译器替你写类型

auto 关键字告诉编译器:“这个变量的类型,你根据等号右边的表达式自己推断吧!”

1
2
3
4
5
// 之前,必须手动写出完整的类型
Solution result = solveQuadratic(1.0, -3.0, 2.0);

// 使用 auto,编译器自动推断
auto result = solveQuadratic(1.0, -3.0, 2.0); // 编译器知道 result 的类型是 Solution

关于 auto 的重要澄清

  • auto 仍然是静态类型! 类型推断发生在编译时。一旦 result 的类型被推断为 Solution,它就永远是 Solution 类型,不能再改变。
  • auto 只是让我们少打字,并没有改变C++的静态类型本质。

auto 使用指南:

  • 推荐使用:当类型名称非常长、复杂,并且从等号右边能清晰地看出类型时(如函数返回值、迭代器等)。
  • 谨慎使用:当类型很简单时(如 int i = 1;),直接写出 int 通常比 auto 更清晰。不要为了用auto而用auto。目标永远是代码清晰性
Licensed under CC BY-NC-SA 4.0
© 2023 - 2025 壹壹贰捌· 0Days
共书写了257k字·共 96篇文章 京ICP备2023035941号-1