1. 项目概述为什么今天还要学C如果你刚接触编程或者是从Python、Java这类语言转过来心里可能犯嘀咕都202X年了各种高级语言层出不穷为什么还要啃C这块“硬骨头”我干了十几年系统开发从游戏引擎到高频交易系统C一直是工具箱里最锋利的那把刀。简单说当你需要极致性能、需要直接操作硬件、需要对内存和CPU周期精打细算时C几乎是唯一的选择。看看你手机里的操作系统内核、电脑上的游戏大作、金融交易所里每秒处理百万笔订单的交易系统甚至你正在用的Chrome浏览器它们的核心部分都大量使用了C。它不像Python那样“开箱即用”写个网页爬虫可能几分钟搞定但C给你的是一种“掌控感”——你知道你写的每一行代码最终是如何变成机器指令的你知道每一个字节的内存是如何分配和释放的。这种从底层理解计算机运作的能力是其他高级语言很难赋予的。学习C不仅仅是学一门语言语法更是学习计算机科学的思维方式理解从代码到硬件执行的完整链条。这对于想深入理解计算机原理、从事系统软件、游戏开发、嵌入式、高性能计算等领域的开发者来说是绕不开的基石。2. 环境搭建与第一个程序从“Hello World”到理解编译过程很多新手卡在第一步环境怎么配代码写好了怎么运行这一步看似简单但背后涉及的概念是理解C乃至所有编译型语言的关键。我们不只满足于让程序跑起来更要弄明白它为什么能跑起来。2.1 编译器选择与安装GCC、Clang还是MSVC写C代码你需要一个编译器。编译器就像个翻译官把你写的、人能看懂的C源代码.cpp文件翻译成计算机能直接执行的机器码。主流选择有三个GCC (GNU Compiler Collection) 这是Linux世界的标配免费、开源、强大。在Windows上可以通过MinGW或MSYS2来安装GCC。它的好处是标准遵循得非常好生态成熟。Clang/LLVM 近年来势头很猛编译速度快错误提示信息比GCC更清晰、更友好对新手特别友好。macOS系统自带的clang命令其实就是它。在Windows上同样可以通过MSYS2安装。MSVC (Microsoft Visual C) 这是微软自家的编译器和Visual Studio IDE深度绑定。如果你主要在Windows平台做开发特别是开发带有图形界面的Windows应用MSVC是首选因为它对Windows SDK和平台特性的支持最好。注意 你可能会在安装某些软件比如游戏时系统提示你安装“Microsoft Visual C Redistributable”。这是运行时库不是编译器。它包含了程序运行所需要的动态链接库DLL确保别人用MSVC编译好的程序能在你的电脑上运行。而我们要安装的是开发工具是用于把源代码变成可执行文件的。对于纯新手我建议在Windows上使用MSYS2 MinGW-w64 GCC这套组合。MSYS2提供了一个类似Linux的终端环境可以方便地用包管理器pacman安装GCC、Clang、Make等一整套开发工具链避免了去官网下载、手动配置环境变量的麻烦。安装后你就能在终端里使用g命令了。2.2 编辑器的选择轻量VS Code还是全功能IDE写代码需要一个趁手的编辑器或集成开发环境IDE。Visual Studio Code (VS Code) 轻量、免费、插件生态极其丰富。通过安装C/C扩展Microsoft官方出品你可以获得代码高亮、智能提示IntelliSense、代码格式化、调试等功能。它需要你稍微配置一下主要是告诉它编译器路径和头文件路径但一旦配好非常灵活适合各种项目。Visual Studio (社区版免费) 微软的“重型”IDE功能全到令人发指特别是调试器非常强大。它自带MSVC编译器安装时勾选“使用C的桌面开发”即可基本是“一键配置”。适合中大型项目尤其是Windows桌面开发。CLion JetBrains家的产品收费但对学生和开源项目有免费许可。以智能、重构功能强大著称对CMake项目支持极好。如果你是初学者想专注于语言本身不被复杂的IDE界面干扰我推荐VS Code GCC。这个过程能让你更清楚地理解编译和构建的步骤。2.3 第一个程序解剖“Hello World”让我们写下经典的第一个程序并逐行理解// hello.cpp #include iostream // 1. 预处理指令包含输入输出流库的头文件 int main() { // 2. 程序入口main函数 // 3. 输出语句 std::cout Hello, World! std::endl; // 4. 返回值 return 0; // 通常返回0表示程序正常结束 }现在打开终端或VS Code的集成终端切换到hello.cpp所在的目录执行编译命令g -o hello hello.cpp这条命令的意思是调用g编译器将源文件hello.cpp编译-o指定输出文件名成一个名为helloWindows下是hello.exe的可执行文件。然后运行它# Linux/macOS/MSYS2 ./hello # Windows 命令提示符 hello.exe你应该会看到屏幕上输出Hello, World!。我们来拆解一下这个过程中发生了什么预处理 编译器首先处理所有以#开头的指令。#include iostream就是把iostream这个头文件的内容“复制粘贴”到你的hello.cpp文件的开头。这个头文件里声明了cout、endl等对象和函数的原型告诉编译器它们的存在。编译 编译器检查你的代码语法是否正确然后将C代码现在已经是包含了头文件内容的完整代码翻译成汇编语言一种更接近机器码的低级语言。汇编 汇编器将上一步生成的汇编代码翻译成目标文件.o或.obj文件里面是二进制的机器码但还不是最终可运行的程序。链接 链接器把你程序的目标文件和用到的库文件比如iostream对应的库“链接”在一起解决函数和变量的地址问题最终生成一个完整的、可以加载到内存中执行的可执行文件.exe或无后缀。实操心得 很多初学者遇到的“未定义的引用”错误往往发生在链接阶段。这通常是因为你调用了某个函数比如sqrt数学函数编译时通过了因为头文件声明了它但链接时找不到这个函数的实现代码。这时你需要告诉链接器去链接对应的库例如使用g -o myprog myprog.cpp -lm来链接数学库。3. C核心语法与概念精讲掌握了如何让程序跑起来我们正式进入C的世界。这部分是地基必须打牢。我会用对比和实际例子帮你理解那些容易混淆的概念。3.1 变量与基本数据类型内存的“格子”变量就是给一块内存空间起的名字用来存储数据。C是静态类型语言意味着你必须在用变量前声明它的类型。这听起来麻烦但能让编译器提前发现很多错误并生成更高效的代码。基本数据类型主要有整型int通常4字节、short2字节、long、long long8字节。还有unsigned版本表示无符号只存正数。浮点型float单精度约6-7位有效数字、double双精度约15-16位有效数字。做科学计算或图形处理默认用double精度更高。字符型char1字节存放一个ASCII字符。注意char本质上也是个整数存储字符的ASCII码。布尔型bool只有true和false两个值。声明与初始化int age 25; // 声明并初始化 double price 19.99; char grade A; bool isReady true; // C11之后的更现代的初始化方式 int count{10}; // 列表初始化能防止窄化转换比如用double初始化int会警告 auto name Alice; // auto关键字让编译器自动推断类型这里name是const char*注意事项未初始化的变量值是未定义的俗称“垃圾值”。直接使用它可能导致程序行为不可预测。养成声明时即初始化的好习惯。注意整数溢出short s 32767; s s 1;对于有符号的short最大值是32767加1会变成-32768溢出。使用足够大的类型或检查边界。浮点数的精度问题float a 0.1f; float b 0.2f; float c a b;c可能不等于0.3而是0.30000001之类。比较浮点数是否相等时不要用而应该判断两者差的绝对值是否小于一个很小的数如1e-9。3.2 运算符与控制流程序的决策与循环运算符和控制语句决定了程序的逻辑走向。算术运算符,-,*,/,%取模。注意整数相除结果还是整数舍去小数。关系与逻辑运算符,!,,,,,与||或!非。这些表达式的结果是bool类型。条件语句if,else if,else。还有switch用于基于一个整型或枚举值的多路分支。循环语句for循环 当你知道循环次数时使用。for (int i 0; i 10; i)while循环 当循环条件在循环开始时检查时使用。do...while循环 当循环体至少需要执行一次时使用条件在末尾检查。流程跳转break跳出当前循环或switchcontinue跳过本次循环剩余部分进入下一次循环。一个综合例子判断素数#include iostream #include cmath // 用于sqrt函数 using namespace std; // 为了避免每次都写std::可以这样声明但大型项目慎用 int main() { int num; cout Enter a positive integer: ; cin num; bool isPrime true; if (num 1) { isPrime false; } else { // 只需检查到 sqrt(num) 即可 for (int i 2; i sqrt(num); i) { if (num % i 0) { isPrime false; break; // 发现因子立即跳出循环 } } } if (isPrime) { cout num is a prime number. endl; } else { cout num is not a prime number. endl; } return 0; }3.3 函数模块化与代码复用函数是把一段完成特定功能的代码封装起来以便重复使用。这是结构化编程的核心。函数定义// 返回值类型 函数名(参数列表) { 函数体 } int add(int a, int b) { int sum a b; return sum; // 使用return语句返回值 }关键概念参数传递传值 默认方式。函数内对参数的修改不影响外部实参。适用于基本数据类型和小型结构。传引用 在参数类型后加。函数内操作的是实参的别名修改会影响外部。用于需要修改实参或传递大型对象避免拷贝时。void swap(int x, int y)传常量引用const T。用于传递大型对象且不希望函数修改它也避免拷贝开销。void printVector(const std::vectorint vec)传指针 C风格C中更多用引用替代。函数重载 允许同一作用域内有多个同名函数只要它们的参数列表不同类型、数量、顺序。编译器根据调用时提供的实参来决定调用哪个。返回值类型不同不足以构成重载。默认参数 可以在函数声明中为参数指定默认值。调用时若省略该参数则使用默认值。注意默认参数必须从右向左连续设置。递归函数示例计算斐波那契数列#include iostream using namespace std; long long fibonacci(int n) { // 基准情况 if (n 1) { return n; } // 递归情况 return fibonacci(n - 1) fibonacci(n - 2); } int main() { int n; cout Enter the position: ; cin n; cout Fibonacci number at position n is fibonacci(n) endl; // 注意这个递归实现效率极低指数级n稍大就会非常慢。实际应用中会用迭代或记忆化搜索。 return 0; }实操心得 递归思维是编程中的一个重要能力。写递归函数时一定要先想清楚两件事1.基准情况什么时候递归应该停止2.递归情况如何把大问题分解成一个或多个更小的同类问题像上面的斐波那契数列虽然直观但存在大量的重复计算。在实际项目中对于这类有重叠子问题的情况通常会使用“记忆化搜索”用一个数组缓存已计算的结果或动态规划来优化。3.4 数组、字符串与标准模板库STL入门处理一组数据是编程的常态。C提供了多种方式。1. 原生数组固定大小在栈上或静态存储区分配内存。int scores[5] {95, 88, 92, 78, 100}; // 声明并初始化一个包含5个整数的数组 // 访问和修改 scores[0] 96; // 第一个元素索引为0 int firstScore scores[0]; // 遍历 for (int i 0; i 5; i) { cout scores[i] ; }缺点大小固定无法在运行时改变作为函数参数传递时会退化为指针丢失大小信息。2. C风格字符串本质是字符数组以空字符\0结尾。char greeting[] Hello; // 实际上是 {H,e,l,l,o,\0}操作它们需要使用cstring库里的函数如strcpy,strcat,strlen等容易出错缓冲区溢出。3. C标准库字符串std::string这是你应该主要使用的字符串类型定义在string头文件中。它动态管理内存大小可变提供了丰富的成员函数查找、替换、子串、比较等安全又方便。#include string #include iostream using namespace std; int main() { string name Bob; name Smith; // 拼接 cout Name: name endl; cout Length: name.length() endl; // 获取长度 cout First char: name[0] endl; // 依然可以像数组一样访问 // 查找 size_t pos name.find(Smith); if (pos ! string::npos) { // npos表示未找到 cout Smith found at position: pos endl; } return 0; }4. 标准模板库STL容器std::vector这是C中最常用、最重要的动态数组定义在vector头文件中。它封装了动态内存管理可以随时增加或删除元素。#include vector #include iostream using namespace std; int main() { vectorint numbers; // 创建一个空的int向量 numbers.push_back(10); // 在末尾添加元素 numbers.push_back(20); numbers.push_back(30); cout Size: numbers.size() endl; // 当前元素个数 cout Capacity: numbers.capacity() endl; // 当前分配的存储空间大小 // 遍历方式1下标 for (size_t i 0; i numbers.size(); i) { cout numbers[i] ; } cout endl; // 遍历方式2范围for循环 (C11) for (int num : numbers) { cout num ; } cout endl; // 插入和删除 numbers.insert(numbers.begin() 1, 15); // 在第二个位置插入15 numbers.pop_back(); // 删除最后一个元素 return 0; }vector会自动处理内存的重新分配当容量不足时你几乎可以把它当作一个“超级数组”来用。它是学习其他STL容器如list,map,set的基础。4. 面向对象编程OOP核心类与对象C之所以强大面向对象特性是关键。OOP的核心思想是将数据和对数据的操作封装在一起形成“类”然后通过创建“对象”来使用。4.1 类的基本结构数据与行为的结合类就像一张蓝图对象是根据这张蓝图建造出来的具体房子。#include string #include iostream using namespace std; class Rectangle { private: // 访问修饰符私有成员只能在类内部访问 double length; double width; public: // 公有成员可以在类外部访问 // 构造函数在创建对象时自动调用用于初始化 Rectangle(double len, double wid) : length(len), width(wid) { // 成员初始化列表 : length(len), width(wid) 是更高效的初始化方式 cout A rectangle is created. endl; } // 成员函数方法 double calculateArea() const { // const成员函数承诺不修改对象状态 return length * width; } double calculatePerimeter() const { return 2 * (length width); } // Getter 和 Setter提供对私有成员的安全访问 double getLength() const { return length; } void setLength(double len) { if (len 0) { // 可以加入有效性检查 length len; } } // ... 类似的getWidth和setWidth // 析构函数在对象销毁时自动调用用于清理资源如动态内存 ~Rectangle() { cout A rectangle is destroyed. endl; } }; int main() { // 根据类创建对象实例化 Rectangle rect(5.0, 3.0); // 调用构造函数 // 使用对象的公有成员函数 double area rect.calculateArea(); double perimeter rect.calculatePerimeter(); cout Area: area endl; cout Perimeter: perimeter endl; // 不能直接访问私有成员 // rect.length 10.0; // 错误length是private的 // 通过公有接口访问 rect.setLength(10.0); cout New area: rect.calculateArea() endl; return 0; } // main函数结束rect对象超出作用域自动调用析构函数关键点解析封装 将数据length,width和操作它们的方法calculateArea捆绑在一起并通过public和private控制访问权限。这隐藏了内部实现细节只暴露必要的接口提高了代码的安全性和可维护性。构造函数 与类同名无返回值。用于初始化对象的状态。可以有多个重载。析构函数~加类名无参数无返回值。用于在对象生命周期结束时释放资源。对于管理动态内存的类至关重要。const成员函数 在函数声明后加const表示这个函数不会修改对象的任何成员变量。这既是给编译器的承诺编译器会检查也是给使用者的承诺让他们放心调用。4.2 继承与多态构建层次与实现灵活继承允许我们基于已有的类创建新类新类继承原有类的特性并可以添加新的特性或重写已有的行为。多态则允许我们使用父类的指针或引用来操作子类对象并根据对象的实际类型来调用相应的方法。#include iostream #include string using namespace std; // 基类父类 class Shape { protected: // 保护成员派生类可以访问外部不能 string name; public: Shape(const string shapeName) : name(shapeName) {} virtual ~Shape() {} // 虚析构函数确保正确释放派生类对象 // 虚函数允许在派生类中被重写 virtual double getArea() const { cout Shape area calculation not defined. endl; return 0.0; } void display() const { cout Shape: name endl; } }; // 派生类子类 class Circle : public Shape { // 公有继承 private: double radius; public: Circle(double r) : Shape(Circle), radius(r) {} // 重写基类的虚函数 double getArea() const override { // C11引入override关键字明确表示重写更安全 return 3.14159 * radius * radius; } }; class Square : public Shape { private: double side; public: Square(double s) : Shape(Square), side(s) {} double getArea() const override { return side * side; } }; int main() { Circle circle(5.0); Square square(4.0); // 静态多态编译时直接调用 cout Circle area: circle.getArea() endl; cout Square area: square.getArea() endl; // 动态多态运行时使用基类指针/引用 Shape* shape1 circle; Shape* shape2 □ cout \nUsing base class pointers: endl; cout Shape1 area: shape1-getArea() endl; // 调用Circle::getArea() cout Shape2 area: shape2-getArea() endl; // 调用Square::getArea() // 如果没有virtual关键字这里调用的将是Shape::getArea()输出0。 // virtual使得程序在运行时根据指针实际指向的对象类型来决定调用哪个函数这就是多态。 return 0; }多态的实现条件继承关系。基类中有虚函数使用virtual关键字。派生类重写override了该虚函数。通过基类的指针或引用来调用虚函数。override关键字C11 它不是必须的但强烈建议使用。它告诉编译器你意图重写一个虚函数。如果你拼错了函数名、参数类型不匹配或者基类中没有对应的虚函数编译器会报错这能防止很多难以察觉的错误。5. 内存管理、指针与引用理解C的“利刃”这是C最强大也最容易出错的部分。理解它们你才能真正驾驭C。5.1 指针直接操作内存地址指针是一个变量其值是另一个变量的内存地址。int number 42; int* ptr number; // ptr是一个“指向int的指针”是取地址运算符 cout Value of number: number endl; // 42 cout Address of number: number endl; // 比如 0x7ffeed7c cout Value of ptr (an address): ptr endl; // 同上 cout Value at the address ptr points to: *ptr endl; // 42, *是解引用运算符 *ptr 100; // 通过指针修改其指向的变量的值 cout Now number is: number endl; // 100指针的用途动态内存分配 在堆Heap上申请内存生命周期由程序员控制。int* dynamicArray new int[100]; // 在堆上分配100个int的空间 // ... 使用 dynamicArray delete[] dynamicArray; // 必须手动释放否则内存泄漏传递大型对象给函数 传指针或引用比传值更高效避免拷贝整个对象。实现数据结构 如链表、树等节点之间通过指针连接。多态 如前所述基类指针指向派生类对象。指针的陷阱空指针 指针值为nullptrC11或NULL。解引用空指针会导致程序崩溃段错误。野指针 指针指向已被释放或无效的内存。解引用野指针行为未定义极其危险。内存泄漏 用new分配的内存没有用delete释放。程序长时间运行会耗尽内存。双重释放 对同一块内存调用delete两次会导致程序崩溃。5.2 引用更安全的“别名”引用是某个已存在变量的别名。一旦引用被初始化为一个变量就不能再指向其他变量。它本身不占用存储空间通常由编译器实现为指针但语法上更安全。int value 10; int ref value; // ref是value的引用 ref 20; // 通过引用修改value cout value is now: value endl; // 20 // int ref2; // 错误引用必须在定义时初始化。 // ref anotherValue; // 错误不能改变引用绑定的对象这行代码是把anotherValue的值赋给ref即value。引用的主要用途函数参数 用于修改实参或避免大型对象拷贝。void swap(int a, int b) { // 使用引用交换两个变量的值 int temp a; a b; b temp; }函数返回值 可以返回引用但绝不能返回局部变量的引用因为局部变量在函数结束后就销毁了。通常用于返回类成员、静态变量或传入的引用参数。指针 vs 引用特性指针引用初始化可以不初始化危险必须初始化可重定向可以指向其他变量一旦绑定不可改变空值可以为nullptr不能为空必须绑定有效对象操作符使用*解引用-访问成员像普通变量一样使用内存占用占用存储空间存放地址通常不额外占空间编译器实现安全性相对较低易出错相对较高语法更简洁安全现代C的建议 优先使用引用除非你需要“可能为空”或“需要重定向”的特性这时再用指针。对于动态内存管理更推荐使用智能指针std::unique_ptr,std::shared_ptr它们能自动管理内存生命周期极大减少内存泄漏和双重释放的风险。5.3 动态内存管理与智能指针手动new和delete配对很容易出错。C11引入了智能指针它们位于memory头文件中是管理动态内存的“管家”。std::unique_ptr 独占所有权的智能指针。同一时间只能有一个unique_ptr指向一个对象。当unique_ptr被销毁时它指向的对象也会被自动删除。它不能被复制只能被移动std::move。#include memory { std::unique_ptrint uptr(new int(42)); // auto uptr std::make_uniqueint(42); // C14更推荐的方式 cout *uptr endl; } // 离开作用域uptr自动销毁它管理的int也被deletestd::shared_ptr 共享所有权的智能指针。多个shared_ptr可以指向同一个对象内部通过引用计数来跟踪有多少个shared_ptr指向该对象。当最后一个shared_ptr被销毁时对象才会被删除。{ std::shared_ptrint sptr1 std::make_sharedint(100); // 引用计数1 { std::shared_ptrint sptr2 sptr1; // 引用计数2 cout *sptr2 endl; } // sptr2销毁引用计数1 cout *sptr1 endl; } // sptr1销毁引用计数0内存被释放std::weak_ptr 弱引用指针指向由shared_ptr管理的对象但不增加引用计数。用于打破shared_ptr的循环引用比如双向链表或观察者模式中防止内存泄漏。重要经验 在现代C中尽量避免使用裸指针raw pointer来管理所有权。对于动态分配的资源优先考虑使用智能指针。new和delete应该只出现在底层资源管理类的实现中如自定义容器而不是在普通的业务逻辑代码里。使用make_unique和make_shared来创建智能指针它们更安全防止内存泄漏异常且可能更高效单次内存分配。6. 常见问题与调试技巧实录即使理解了所有概念实际编码时也一定会遇到各种问题。这里记录一些我踩过的坑和解决方法。6.1 编译与链接错误undefined reference to ...错误现象 编译成功链接失败。提示某个函数特别是库函数找不到定义。原因 编译器找到了函数声明在头文件中但链接器在所有的目标文件和库中找不到该函数的实现体。排查检查是否包含了正确的头文件。检查是否链接了必要的库。例如使用了数学函数sqrt需要在编译命令后加-lmGCCg -o prog prog.cpp -lm。检查函数名是否拼写错误包括大小写。如果是你自己的函数检查其定义实现的源代码文件是否被编译并参与了链接。multiple definition of ...错误现象 链接时提示某个符号被多次定义。原因 通常是因为将变量或函数的定义而非声明放在了头文件中并且该头文件被多个源文件包含。解决对于变量 在头文件中用extern声明在一个源文件中定义。// common.h extern int globalCounter; // 声明 // common.cpp int globalCounter 0; // 定义对于函数 确保函数定义在源文件.cpp中头文件中只有声明。或者将函数标记为inline适用于短小的函数。对于类 类的成员函数如果在类体内定义默认是inline的可以放在头文件。但如果成员函数体较大建议在类体外定义并将定义放在源文件中。6.2 运行时错误与调试段错误 (Segmentation Fault)这是C/C程序员最常见的运行时错误之一。原因 程序试图访问其无权访问的内存区域如空指针解引用、数组越界访问、访问已释放的内存。调试方法使用调试器 这是最有效的手段。在VS Code或Visual Studio中设置断点单步执行查看变量值和调用栈。当崩溃发生时调试器会停在出错的那一行。打印日志 在怀疑的代码段前后打印变量值和状态。检查指针 对所有指针在使用前检查是否为nullptr。检查数组边界 确保循环索引和数组访问在合法范围内。对于vector使用.at(i)而不是[i]因为.at()会进行边界检查并抛出异常。使用工具valgrindLinux或AddressSanitizerGCC/Clang的-fsanitizeaddress选项可以检测内存错误如越界、使用未初始化内存、内存泄漏等。内存泄漏现象 程序运行时间长了内存占用不断增长。原因 用new或malloc分配的内存没有用delete或free释放。排查与预防使用智能指针 这是根治手动管理内存导致泄漏的最佳方法。遵循RAII原则 “资源获取即初始化”。将资源内存、文件句柄、锁等的获取放在对象的构造函数中释放放在析构函数中。利用对象的生命周期自动管理资源。使用工具检测valgrind --leak-checkfull可以详细报告内存泄漏的位置。6.3 面向对象编程中的典型问题对象切片 (Object Slicing)现象 将派生类对象赋值给基类对象按值传递时派生类特有的部分会被“切掉”只保留基类部分。class Base { public: int x; }; class Derived : public Base { public: int y; }; Derived d; d.x 1; d.y 2; Base b d; // 对象切片发生b只有xy丢失了。解决 在需要多态的地方始终使用基类的指针或引用而不是对象本身。虚析构函数问题现象 通过基类指针删除派生类对象时如果基类的析构函数不是虚函数则只会调用基类的析构函数派生类的析构函数不会被调用可能导致资源泄漏。class Base { public: ~Base() { cout Base destructor\n; } }; class Derived : public Base { public: ~Derived() { cout Derived destructor\n; } int* data new int[100]; // 派生类独占资源 }; Base* ptr new Derived(); delete ptr; // 如果Base的析构函数不是virtual这里只会输出Base destructorDerived的data数组内存泄漏黄金法则 如果一个类有可能被继承即作为基类并且它拥有虚函数那么它的析构函数必须声明为虚函数virtual ~Base()。如果类不打算作为基类或者没有虚函数则不必使用虚析构函数以避免不必要的虚函数表开销。学习C就像学习一门内功心法初期会觉得语法繁杂、细节众多但一旦掌握你对计算机系统的理解会达到一个新的层次。它不会过时因为对性能和控制力的追求是永恒的。从今天起多写代码多思考多调试。遇到问题别怕每一个编译错误和段错误都是你进步的阶梯。试着用C去实现一些经典的数据结构和算法或者写一个小型的命令行工具实践是巩固知识最好的方式。当你第一次用C写出一个运行高效、结构清晰的程序时那种成就感是无与伦比的。