1. 项目概述:为什么是C++,以及“入门”究竟意味着什么
每次看到“C++入门”这个标题,我都能回想起自己当年面对那一堆指针、内存和复杂语法时的迷茫。很多新手朋友,尤其是从Python、Java这类更现代、更“友好”的语言转过来的,往往会被C++的“入门”二字所迷惑,以为看几页语法就能上手。但我想说,C++的“入门”和其他语言的“入门”完全是两个概念。它更像是在学习一门手艺的“基本功”,比如木匠的刨、凿、锯,而不是直接去组装一个现成的柜子。这门手艺的核心,是让你直接与计算机的“灵魂”——内存和硬件——对话。
C++之所以历经数十年而不衰,甚至在游戏开发、高频交易、嵌入式系统、大型基础设施(如数据库、浏览器引擎)等领域占据统治地位,根本原因就在于它提供的“零成本抽象”。简单来说,就是它既能让你用接近人类思维的高级抽象(如类、模板)来组织代码,又能保证最终生成的机器码效率极高,几乎等同于手写的汇编。这种“带着镣铐跳舞”的能力,是其他语言难以企及的。因此,学习C++,你学到的不仅仅是一门语言的语法,更是一套关于系统资源管理、性能优化和底层原理的思维方式。
那么,这篇“入门(上)”的目标是什么?它不是带你速成一个“Hello World”就结束。我希望通过这篇内容,帮你搭建起一个正确、稳固的C++世界观。我们会从最核心、最本质的概念讲起,避开那些华而不实的语法糖,直击要害。如果你是一名计算机相关专业的学生,或者是一名希望深入理解系统原理、提升自己技术深度的开发者,那么这篇内容就是为你准备的。我们将从“为什么需要编译”开始,一步步揭开C++的面纱,直到你能亲手构建并理解一个简单的程序。过程中,我会穿插大量我踩过的坑和总结出的“生存技巧”,这些是你在标准教材里很难看到的。
2. 环境准备:不只是安装一个编译器
在真正写下第一行C++代码之前,环境的搭建是第一个,也是至关重要的一个环节。很多新手在这里折戟沉沙,不是因为问题多难,而是因为缺乏一个清晰的路径。网上教程五花八门,有让你用Visual Studio的,有推荐CLion的,还有执着于VSCode配置的。我的建议是:入门阶段,请务必选择“一体化”的IDE,而不是“编辑器+配置”的模式。
2.1 编译器的选择与安装:MSVC vs. GCC/MinGW
C++代码需要被编译成机器码才能执行,这个工具就是编译器。在Windows平台上,主要有两大阵营:
- Microsoft Visual C++ (MSVC):这是微软官方编译器,与Visual Studio IDE深度绑定。它的优势在于对Windows平台特性支持最好,调试器强大无比,并且安装相对省心。
- GCC 或 MinGW-w64:这是GNU编译器套件,MinGW是其在Windows上的一个移植版本。它的优势在于更贴近Linux/Unix世界的标准,很多开源库默认使用GCC编译链。
我的实操心得:对于纯新手,尤其是在Windows上学习,我强烈推荐直接安装Visual Studio Community版(免费),并选择使用MSVC编译器。原因有三:第一,安装过程几乎是一键式的,自动配置好编译、调试、标准库所有环境,极大降低了入门门槛。第二,其集成的调试器是业界标杆,对于理解程序运行过程(如变量监视、内存查看、单步执行)有不可替代的作用。第三,避免了在VSCode中手动配置
c_cpp_properties.json,tasks.json,launch.json这一系列令人头疼的步骤,那些是等你对构建流程有概念后再去折腾的东西。
如果你因为某些原因(比如课程要求,或打算后续无缝过渡到Linux开发)必须使用GCC,那么我推荐使用MSYS2来安装MinGW-w64。MSYS2提供了一个优秀的包管理工具pacman,可以让你轻松安装和维护工具链。
# 在MSYS2终端中,安装MinGW-w64 GCC编译器 pacman -S mingw-w64-ucrt-x86_64-gcc安装后,需要将C:\msys64\mingw64\bin这样的路径添加到系统的PATH环境变量中,才能在命令行中使用g++命令。
2.2 IDE的抉择:Visual Studio vs. 其他
IDE(集成开发环境)是你的主战场。除了Visual Studio,常见的还有JetBrains的CLion(付费,但对学生免费)和轻量级的VSCode。
- Visual Studio:巨无霸,功能全面,重量级。它非常适合Windows平台的C++开发,特别是涉及GUI(如MFC、Qt)、游戏(DirectX)或大型项目。它的“解决方案资源管理器”和属性页对于管理项目非常直观。对于入门,创建一个“空项目”或“控制台应用”即可。
- CLion:跨平台,智能提示(IntelliSense)非常强大,对CMake的支持是原生级的。如果你已经熟悉JetBrains系列产品(如IntelliJ IDEA, PyCharm),或者项目主要使用CMake构建,CLion是绝佳选择。
- VSCode:轻量,灵活,免费。但它本质上是一个高级编辑器,需要自行配置编译器路径、构建任务和调试器。这给了你极大的自由度,但也带来了复杂的配置负担。我不建议纯新手从VSCode开始学C++,你很容易把大量时间浪费在解决环境问题上,而不是学习语言本身。
结论:请下载Visual Studio Installer,在“工作负载”中勾选“使用C++的桌面开发”,然后安心等待安装完成。这是最稳妥、最高效的起点。
2.3 第一个项目的创建与“Hello World”的深层含义
安装好后,打开Visual Studio,点击“创建新项目”,选择“控制台应用”,给项目起个名字,比如HelloCpp。
你会看到一个已经生成好的main.cpp(或.cpp)文件,里面是这样的:
#include <iostream> int main() { std::cout << "Hello World!\n"; return 0; }点击绿色的运行按钮(或按F5),你会在终端窗口看到输出。恭喜,你的第一个C++程序运行成功了!
但请先别急着高兴,让我们深挖一下这几行代码背后的故事,这才是“入门”的开始:
#include <iostream>:这是一个预处理指令。在编译之前,预处理器会找到名为iostream的头文件,并将其内容“复制粘贴”到当前文件中。iostream是输入输出流库,提供了cout、cin等对象。理解“预处理”是理解C/C++构建过程的第一步。int main():这是程序的入口函数。操作系统加载你的程序后,就从这里开始执行。int是返回类型,表示这个函数执行完毕后会返回一个整数给操作系统(通常0表示成功)。std::cout << "Hello World!\n";:std是一个命名空间,用来防止不同库中的同名标识符冲突。cout是“character output”的缩写,代表标准输出流(通常是你的终端)。<<是输出操作符,你可以理解为把右边的字符串“送入”cout这个流中。\n是换行符。return 0;:main函数结束,返回0给操作系统。
注意事项:很多教程会教你写
using namespace std;,这样后面就可以直接写cout而不用加std::。在入门阶段,我强烈反对这种做法。虽然方便,但它污染了全局命名空间,把std里的所有名字都引入了,这在稍大一点的项目或使用多个库时极易引发命名冲突。从一开始就养成使用std::前缀的习惯,是良好的编程实践。
3. 核心概念拆解:从“变量”到“指针”的认知跃迁
掌握了如何让程序跑起来,我们终于可以开始接触C++的语法核心。这部分内容,很多教材会平铺直叙地介绍,但我认为必须建立起一个层次分明、由浅入深的理解框架。
3.1 基础数据类型与变量:内存的“格子”
C++是一种静态类型语言,这意味着你在使用一个变量前,必须明确声明它的类型。这个类型决定了:
- 这个变量在内存中占多大空间(多少字节)。
- 这些字节将被如何解释(是整数、浮点数,还是字符)。
常见的基础类型有:
- 整型:
int(通常4字节)、short(2字节)、long、long long。还有unsigned变体,表示无符号(仅非负)。 - 浮点型:
float(单精度,4字节)、double(双精度,8字节)。用于表示小数。 - 字符型:
char(1字节),用于存放单个字符(如'A')。实际上它存储的是字符的ASCII码。 - 布尔型:
bool,只有true和false两个值。
声明与定义:
int age = 25; // 定义并初始化一个int型变量age double salary; // 声明一个double型变量salary(未初始化,值是未定义的!) salary = 8000.5; // 给salary赋值这里有一个至关重要的坑:在C++中,未初始化的局部变量(在函数内部声明的)的值是未定义的。它可能是0,也可能是一个随机的垃圾值。直接使用它会导致不可预知的程序行为。
int bad; std::cout << bad; // 危险!bad的值是随机的。最佳实践:定义变量时,总是立即进行初始化。int count = 0;double total = 0.0;
3.2 常量、引用与指针:理解“别名”和“地址”
这是C++入门的第一道分水岭,也是理解内存模型的关键。
1. 常量(const):用于定义值不可改变的变量。编译器会帮你检查,防止意外修改。
const double PI = 3.14159; // PI = 3.14; // 错误!编译不通过。使用const是一种良好的意图声明,能提高代码的可读性和安全性。
2. 引用(Reference):为已存在的变量起一个别名。引用必须在定义时初始化,且一旦绑定到一个变量,就不能再指向其他变量。
int original = 42; int& ref = original; // ref是original的引用,即别名 ref = 100; // 修改ref,等同于修改original std::cout << original; // 输出 100引用的主要用途是作为函数参数,实现“按引用传递”,避免大对象的拷贝开销,并允许函数修改实参。
void swap(int& a, int& b) { // 接收引用 int temp = a; a = b; b = temp; } int x=1, y=2; swap(x, y); // x和y的值被交换3. 指针(Pointer):这是C++的灵魂,也是最容易让人困惑的部分。指针本身是一个变量,但这个变量里存储的值是另一个变量的内存地址。
你可以通过一个生活类比来理解:变量是房子,变量的值是房子里的内容,而指针是写着这个房子地址的纸条。
int house = 42; // 有一栋房子,门牌号(变量名)叫house,里面放着42 int* address_note = &house; // &是“取地址符”。我拿到house的地址,写在一张叫address_note的纸条上。int*:声明一个“指向int的指针”类型。&house:获取变量house的内存地址。address_note:这个指针变量,里面存储着house的地址。
有了地址纸条(指针),我们能做什么?
- 解引用(Dereference):根据地址,去找到那栋房子并访问里面的内容。操作符是
*。std::cout << *address_note; // 输出 42。*address_note 等价于访问 house 本身。 *address_note = 100; // 通过地址纸条,找到房子,把里面的值改成100。 std::cout << house; // 输出 100。 - 指针运算:指针可以加减整数,移动的是它指向的“位置”。这常用于数组遍历。
int arr[3] = {10, 20, 30}; int* ptr = arr; // 数组名在多数情况下可视为指向其首元素的指针 std::cout << *ptr; // 输出 10 (arr[0]) ptr++; // 指针移动到下一个int的位置(地址增加 sizeof(int) 字节) std::cout << *ptr; // 输出 20 (arr[1])
核心避坑指南:
- 空指针(Null Pointer):指针变量在定义后,如果没有有效的地址可指,应该立即将其设置为
nullptr(C++11引入,推荐使用)或NULL。解引用一个空指针会导致程序崩溃(段错误)。int* dangerous_ptr; // 未初始化,指向随机地址 // *dangerous_ptr = 5; // 灾难!可能覆盖任意内存。 int* safe_ptr = nullptr; // 安全的做法 if (safe_ptr != nullptr) { // 使用前检查 *safe_ptr = 5; }- 野指针(Dangling Pointer):指针指向的内存已经被释放(例如,一个局部变量已经离开其作用域),但指针本身还在。此时解引用它,行为是未定义的。
int* createPointer() { int local = 99; return &local; // 错误!返回局部变量的地址。函数结束,local被销毁,返回的指针是野指针。 }- 指针 vs 引用:引用更像一个“永久别名”,安全但不够灵活;指针是一个可以存储地址的实体,更强大但也更危险。函数参数中,如果希望函数内修改实参且实参不可能为空,用引用;如果实参可能为空,或者你需要操作地址本身(如动态内存分配),用指针。
4. 函数与作用域:构建程序的积木
函数是将代码组织成可重用块的基本方式。在C++中,理解函数如何声明、定义、调用,特别是参数传递和返回值机制,至关重要。
4.1 函数的基本结构
// 函数声明(通常在头文件.h中),告诉编译器有这个函数 int add(int a, int b); // 函数定义(通常在源文件.cpp中) int add(int a, int b) { int sum = a + b; return sum; // 返回值 } // 调用 int result = add(3, 5); // result = 84.2 参数传递的三种方式:值、指针、引用
这是函数部分的核心难点,决定了函数内外数据的交互方式。
传值(Pass by Value):函数收到的是实参的一个副本。函数内对参数的修改,不影响函数外的原始变量。
void increment(int x) { x = x + 1; std::cout << "Inside function, x = " << x << "\n"; // 输出 6 } int main() { int num = 5; increment(num); std::cout << "Outside function, num = " << num << "\n"; // 输出 5,未改变! return 0; }适用于基本数据类型(
int,double等)和小型结构体,且函数不需要修改原始数据的情况。传指针(Pass by Pointer):函数收到的是实参的地址副本。通过这个地址,函数可以找到并修改原始数据。
void increment(int* ptr) { if (ptr != nullptr) { // 必须检查! *ptr = *ptr + 1; // 解引用并修改 } } int main() { int num = 5; increment(&num); // 传递地址 std::cout << num; // 输出 6,改变了! return 0; }优点是可以修改原始数据,且避免了大型对象的完整拷贝(只拷贝了一个地址,通常4或8字节)。缺点是语法稍显复杂,且需要处理空指针。
传引用(Pass by Reference):函数收到的是实参的别名。操作引用就是直接操作原始变量。
void increment(int& ref) { ref = ref + 1; // 直接修改,无需解引用 } int main() { int num = 5; increment(num); // 直接传递变量,语法简洁 std::cout << num; // 输出 6 return 0; }这是最推荐用于“需要修改实参”场景的方式。它拥有指针修改数据的能力,但语法像传值一样简洁安全(引用不能为空)。
选择策略:
- 输入参数(函数只读):如果参数是内置类型或小型对象,用传值或常量引用(
const T&)。常量引用避免了拷贝,同时防止函数内误修改。 - 输出参数或输入输出参数(函数需要修改):用非常量引用(
T&)。 - 可选参数或需要操作地址时:用指针(
T*),并记得检查nullptr。
4.3 作用域与生命周期:变量在哪里“活”着
- 局部作用域:在函数或代码块
{}内部声明的变量。它们在进入作用域时创建,离开作用域时自动销毁。生命周期最短。 - 全局作用域:在所有函数和代码块之外声明的变量。它们在程序开始时创建,程序结束时销毁。生命周期最长。应谨慎使用,因为它们破坏了封装性,可能导致代码难以维护。
- 静态局部变量:在函数内用
static关键字声明的变量。它在第一次进入函数时初始化,之后函数调用结束它也不会销毁,生命周期持续到程序结束。常用于记录函数被调用的次数。void callCount() { static int count = 0; // 只初始化一次 count++; std::cout << "Called " << count << " times.\n"; }
理解作用域是避免“野指针”和“重复定义”错误的关键。一个常见的错误是返回局部变量的引用或指针,如前所述,这会产生野指针。
5. 复合数据类型初探:数组、结构体与字符串
当基本类型不够用时,我们需要将它们组合起来。
5.1 数组:同一类型元素的集合
数组在内存中是连续存储的。
int scores[5] = {95, 88, 92, 79, 100}; // 声明并初始化一个包含5个int的数组- 访问元素:
scores[0]是第一个元素(索引从0开始)。 - 数组名
scores在大多数表达式中会退化为指向其首元素的指针(int*类型)。 - 关键限制:数组的大小必须在编译时确定,且不能改变。同时,数组没有边界检查,访问
scores[5](越界)会导致未定义行为,这是许多程序崩溃的根源。
5.2 结构体(struct):不同类型数据的打包
结构体允许你将多个不同类型的变量组合成一个逻辑单元。
struct Student { int id; std::string name; // 注意,这里用了std::string,见下文 double score; }; // 分号不能少 Student stu1; // 定义一个Student类型的变量 stu1.id = 1001; stu1.name = "Alice"; stu1.score = 95.5;结构体使得管理相关数据变得清晰。你可以将整个结构体作为参数传递给函数(传值或传引用)。
5.3 字符串:从C风格字符串到std::string
处理文本是编程的常见需求。C++有两种主要的字符串表示方式:
C风格字符串:本质是字符数组,以空字符
'\0'结尾。char greeting[] = "Hello"; // 实际上是 {'H','e','l','l','o','\0'}操作它们需要使用
<cstring>库中的函数,如strlen,strcpy,strcat等。这种方式非常原始且容易出错(缓冲区溢出)。std::string:这是C++标准库提供的字符串类,位于
<string>头文件中。对于新手,我强烈建议你从学习的第一天起就使用std::string,并暂时忘记C风格字符串。#include <string> std::string name = "Bob"; std::string hello = "Hello, " + name + "!"; // 支持方便的拼接 int length = hello.length(); // 获取长度 if (name == "Bob") { ... } // 支持直接比较std::string自动管理内存,提供了丰富的成员函数(查找、替换、子串等),安全且易用。它是你处理文本的首选工具。
6. 流程控制:让程序“思考”和“重复”
程序不能只按顺序执行,还需要根据条件分支和循环重复。
6.1 条件分支:if-else 和 switch
// if-else int score = 85; if (score >= 90) { std::cout << "优秀"; } else if (score >= 60) { std::cout << "及格"; } else { std::cout << "不及格"; } // switch (适用于对单一整型或枚举值进行多路分支) char grade = 'B'; switch (grade) { case 'A': std::cout << "完美"; break; // 必须用break跳出,否则会“贯穿”执行下一个case case 'B': case 'C': std::cout << "良好"; break; default: std::cout << "其他"; }6.2 循环:for, while, do-while
// for循环:明确知道循环次数时使用 for (int i = 0; i < 10; ++i) { // 前置递增++i通常效率略高于i++ std::cout << i << " "; } // while循环:条件满足时执行,可能一次都不执行 int i = 0; while (i < 10) { std::cout << i << " "; ++i; } // do-while循环:先执行一次,再判断条件。至少执行一次。 int input; do { std::cout << "请输入一个正数: "; std::cin >> input; } while (input <= 0);循环控制语句:
break:立即跳出整个循环。continue:跳过本次循环剩余的语句,直接进入下一次循环的条件判断。
7. 头文件与多文件编程:项目组织的起点
当代码量增长,把所有代码写在一个.cpp文件里会变得难以维护。C++使用**头文件(.h或.hpp)和源文件(.cpp)**来分离接口声明和实现。
7.1 头文件的作用
头文件(例如math_utils.h)主要包含:
- 函数声明(原型)
- 类声明
- 常量定义
- 内联函数
- 模板(后续会学到)
它的核心思想是:告诉编译器“有什么”,而不关心“怎么做”。
7.2 一个简单的多文件项目示例
假设我们有一个简单的数学工具库。
math_utils.h (头文件)
// 防止头文件被多次包含的预处理指令(非常重要!) #ifndef MATH_UTILS_H #define MATH_UTILS_H // 函数声明 int add(int a, int b); double multiply(double a, double b); #endif // MATH_UTILS_Hmath_utils.cpp (源文件)
#include "math_utils.h" // 包含对应的头文件 // 函数定义 int add(int a, int b) { return a + b; } double multiply(double a, double b) { return a * b; }main.cpp (主程序)
#include <iostream> #include "math_utils.h" // 包含我们自己的头文件 int main() { std::cout << "Sum: " << add(10, 20) << "\n"; std::cout << "Product: " << multiply(3.14, 2.0) << "\n"; return 0; }7.3 编译与链接
在Visual Studio这样的IDE中,你只需要将math_utils.cpp和main.cpp都添加到项目中,IDE的构建系统会自动处理以下步骤:
- 编译(Compile):编译器分别将每个
.cpp文件(称为一个翻译单元)编译成目标文件(.obj)。这个过程会检查语法,处理#include,生成机器码,但函数调用(如add)的地址还未确定。 - 链接(Link):链接器将所有的目标文件以及标准库文件合并,解析函数和变量的地址(比如找到
add函数在math_utils.obj中的具体位置),生成最终的可执行文件(.exe)。
如果链接时出现“无法解析的外部符号”错误,通常意味着函数只有声明(在头文件里),但没有定义(在任何一个.cpp文件里),或者对应的.cpp文件没有被编译进项目。
头文件防卫式声明详解:
#ifndef(如果未定义)、#define、#endif这一套组合拳,被称为“Include Guards”。它的作用是防止同一个头文件在同一个翻译单元中被多次包含。假设main.cpp直接或间接地两次包含了math_utils.h,如果没有防卫,那么add函数就会被声明两次,导致编译错误。有了防卫,第一次包含时定义了MATH_UTILS_H宏,第二次包含时#ifndef条件为假,头文件内容就被跳过了。这是编写头文件的铁律。
8. 输入输出基础:与用户交互
我们已多次使用std::cout进行输出。输入则使用std::cin。
#include <iostream> #include <string> int main() { int age; double weight; std::string name; std::cout << "请输入您的姓名: "; std::getline(std::cin, name); // 读取一行字符串,包含空格 std::cout << "请输入年龄和体重(用空格分隔): "; std::cin >> age >> weight; // 连续读取,以空格/换行为分隔 std::cout << "你好," << name << "!你" << age << "岁,体重" << weight << "公斤。\n"; // 注意:混合使用getline和>>时的一个常见坑 std::cout << "请输入你的城市: "; std::cin.ignore(); // 忽略之前>>操作留下的换行符 std::string city; std::getline(std::cin, city); return 0; }std::cout是标准输出流对象,<<是流插入运算符。std::cin是标准输入流对象,>>是流提取运算符。std::getline用于读取包含空格的整行文本。std::cin.ignore()是一个常用技巧,用于清空输入缓冲区中残留的换行符,防止它被后续的getline误读。
9. 调试入门:让Visual Studio成为你的显微镜
编程不可能一遍写对。调试(Debugging)是定位和修复错误的核心技能。Visual Studio的调试器极其强大。
基本操作:
- 设置断点:在代码行号左侧点击,出现红点。程序运行到这一行时会暂停。
- 开始调试:按F5(“开始调试”)或工具栏上的绿色三角(带虫子图标)。
- 逐语句执行:暂停后,按F11可以“步入”函数内部,一行一行执行。
- 逐过程执行:按F10,执行当前行,但不进入函数内部。
- 监视变量:在调试状态下,下方“监视”窗口可以添加变量名,实时查看其值的变化。
- 查看调用堆栈:“调用堆栈”窗口显示了程序是如何一步步执行到当前断点的,对于理解函数调用链和定位崩溃点非常有用。
调试心智模型:调试不是漫无目的地加打印语句。你应该先根据错误现象(崩溃、结果不对)提出假设(“可能是这个变量在这里变成了错误的值”),然后通过设置断点、单步执行、监视变量来验证或推翻你的假设。这是一个科学的排查过程。
10. 常见编译与链接错误速查
入门阶段,你会频繁遇到各种错误。别怕,每个程序员都是这么过来的。这里是一些典型错误和解决思路:
| 错误类型 | 典型提示 | 可能原因与解决方案 |
|---|---|---|
| 语法错误 | error C2143: syntax error: missing ';' before '}' | 编译器在编译阶段发现代码不符合语法规则。仔细检查错误信息指出的行号及附近几行,常见于缺少分号、括号不匹配、关键字拼写错误。 |
| 未定义标识符 | error C2065: 'cout': undeclared identifier | 1. 拼写错误。2. 未包含必要的头文件(如<iostream>)。3. 未使用正确的命名空间(如std::cout)。 |
| 链接错误 | error LNK2019: unresolved external symbol "int __cdecl add(int,int)" | 函数有声明(在.h中),但找不到定义(在.cpp中)。检查:1. 对应的.cpp文件是否已加入项目。2. 函数签名(返回类型、参数类型)在声明和定义中是否完全一致。 |
| 运行时错误 | 程序崩溃或无输出 | 1.段错误/访问冲突:通常是指针错误(解引用空指针、野指针、数组越界)。2.无限循环:循环条件永远为真。3.逻辑错误:程序能运行,但结果不对。需要借助调试器逐步排查。 |
面对错误,一定要仔细阅读编译器或链接器给出的错误信息。它们通常包含了文件名、行号和具体的错误描述。从第一个错误开始解决,因为后面的错误可能是由前面的错误引发的。
写到这里,我们已经覆盖了C++入门阶段最核心、最本质的一批概念。从“程序如何运行”的环境搭建,到“数据如何存放”的变量与指针,再到“代码如何组织”的函数与文件,最后是“如何与错误共处”的调试与排错。这些知识构成了你C++大厦的地基。在“入门(下)”中,我们将向更抽象、更强大的领域迈进:面向对象编程(类与对象)、内存管理(动态内存new/delete)、标准库容器(vector,map)等。记住,理解原理和反复练习是唯一捷径,动手把这里的每一个例子敲一遍,并尝试修改它们,看看会发生什么,你会有完全不同的收获。编程的世界里,编译器是最好的老师,而错误信息就是它给你的习题。