C++编程入门:从环境配置到核心语法与面向对象实战
1. 为什么C依然是你的“硬通货”选择如果你点开了这篇文章大概率是想学编程或者想从其他语言转到C。你可能会听到一些声音说C太老了、太复杂了现在流行Python、Go、JavaScript。这话对但也不全对。想象一下你学开车是直接学开F1赛车还是从手动挡的家用车开始F1极快但上手门槛高得离谱家用车虽然速度没那么快但能让你深刻理解离合、换挡、油离配合以后开什么车都容易上手。C就是编程世界里的那辆“手动挡性能车”——它给你无与伦比的掌控力让你能看清计算机到底是如何工作的从内存里每一个字节的分配到CPU指令的执行。当你用Python写list.append()时你可能不知道背后发生了什么但在C里你用std::vector::push_back()时如果空间不够你会亲眼看到它如何重新分配一块更大的内存把老数据搬过去。这种“知其然更知其所以然”的能力是C送给学习者最宝贵的礼物。看看那些热搜词和网络热词吧c小游戏、opencv c、c多线程、qt c 绘制k线图、yolo v3 c 使用教程。这揭示了一个事实C的战场从未消失只是转移到了更需要性能、更贴近硬件的核心地带。游戏引擎Unity的底层是C、高频交易系统、自动驾驶感知算法、嵌入式设备、数据库、操作系统……这些领域的“基础设施”几乎都由C把持。它就像编程语言里的“重工业”你可以用更高级的语言Python、C#快速搭建漂亮的“轻工业”产品但当你需要建造一座大桥、一艘航母时你绕不开C。学习C不仅仅是学习一门语言的语法更是构建一套关于计算机系统、关于性能、关于资源管理的底层思维模型。这套模型能让你在使用任何其他语言时都更加得心应手因为你知道了“黑盒”内部可能的运作方式。所以这篇教程的目标很明确带你快速、清晰地跨过C最初也是最令人望而生畏的门槛。我们不搞“八股文”式的死记硬背也不一开始就堆砌复杂的模板和标准库细节。我们会像组装一台电脑一样从最基础的零件变量、类型开始教你如何把它们连接起来运算符、函数然后让它们协同工作控制流、数组最后初步领略如何设计一个复杂的部件类与对象。我们的路径是“快速入门”意味着我们会直奔核心概念用最直白的例子和类比避开早期容易让人困惑的历史包袱聚焦在现代CC11及之后更清晰、更安全的写法上。当你跟着走完这一程你将不仅拥有编写简单C程序的能力更会获得一把打开系统级编程世界大门的钥匙。2. 搭建你的第一个C战场开发环境配置详解在开始写代码之前我们必须把“战场”布置好。对于C来说这个战场主要由三部分组成编译器、代码编辑器和构建系统。很多新手卡在第一步就是因为环境没配好看到一堆错误信息就放弃了。别担心我们一步步来给你最稳妥的方案。2.1 编译器代码的翻译官你的C源代码.cpp文件是人类可读的文本计算机的CPU看不懂。编译器Compiler的工作就是把它翻译成机器可执行的二进制指令。在Windows上最主流的选择是MinGW-w64它提供了GCC编译器套件。你不需要单独安装Visual Studio那个庞大的IDE我们只需要它的编译器部分。为什么选择MinGW-w64而不是其他轻量且独立它只是一个工具链不绑定任何特定的IDE你可以用任何文本编辑器配合它。跨平台一致性GCC在Linux和macOS上也是主流编译器在Windows上使用MinGW-w64有助于保持开发环境的一致性减少平台差异带来的困扰。对标准支持好GCC对新C标准的跟进非常迅速。实操安装步骤访问 MinGW-w64官网 的下载页面。对于新手我推荐一个更简单的途径下载MSYS2。安装MSYS2后打开MSYS2 MinGW 64-bit终端。输入命令pacman -S mingw-w64-x86_64-gcc。这个命令会安装64位的GCC编译器套件。安装完成后输入g --version。如果能看到类似g (Rev10, Built by MSYS2 project) 13.2.0的输出恭喜你编译器安装成功注意务必把MinGW-w64的bin目录例如C:\msys64\mingw64\bin添加到系统的PATH环境变量中。这样你才能在任意命令行窗口中使用g命令。这是最常见的失败点很多同学安装后命令行依然找不到g问题就出在这里。2.2 代码编辑器你的写作台编译器有了我们需要一个写代码的地方。强烈推荐Visual Studio Code (VSCode)。它轻量、免费、插件生态极其丰富是目前最受开发者欢迎的编辑器之一。VSCode配置C环境的核心步骤安装VSCode从官网下载安装即可。安装必要插件C/C(Microsoft)提供代码智能提示IntelliSense、跳转定义、错误检查等核心功能。Code Runner可以一键运行单个C文件非常适合学习和测试小片段代码。关键配置告诉VSCode编译器在哪这是配置的核心。在VSCode中按F1输入C/C: Edit Configurations (UI)会打开一个配置界面。在编译器路径一项中点击浏览找到你安装的g.exe通常是C:\msys64\mingw64\bin\g.exe。IntelliSense 模式选择gcc-x64。在includePath和compilerPath正确设置后VSCode的代码提示和错误检查就会基于你的MinGW-w64编译器工作非常准确。一个更简单的选择Online Compiler如果你不想在本地折腾环境完全可以先用在线编译器来学习基础语法。像Compiler Explorer (godbolt.org)这类工具非常强大它不仅能即时编译运行还能看到代码生成的汇编指令对理解底层非常有帮助。但对于正式学习和做项目本地环境是必须的。2.3 构建与运行从源代码到程序环境齐备我们来创建第一个程序。新建一个文件命名为hello.cpp。// hello.cpp #include iostream // 引入输入输出流库 int main() { // 每个C程序都必须有一个main函数它是程序执行的起点 std::cout Hello, C World! std::endl; // 向控制台输出字符串 return 0; // 向操作系统返回0表示程序正常结束 }如何运行它命令行方式推荐初学者掌握 打开终端cmd或PowerShell导航到hello.cpp所在的目录。 输入命令g hello.cpp -o hello.exe。g调用编译器。hello.cpp我们的源代码文件。-o hello.exe指定输出的可执行文件名为hello.exe。 然后运行.\hello.exeWindows或./helloLinux/macOS。你将看到Hello, C World!输出。使用VSCode Code Runner插件 安装好Code Runner后在hello.cpp文件中直接点击右上角的“播放”按钮或者按CtrlAltN插件会自动完成编译和运行并在下方的“输出”面板显示结果。第一个程序拆解#include iostream 这是预处理指令。可以理解为“拷贝粘贴”。它把iostream这个头文件的内容里面定义了cout,cin等插入到我们的代码中我们才能使用输入输出功能。int main() 主函数。程序从这里开始执行。int表示这个函数执行完毕后会返回一个整数。std::cout ... std::endl;std是标准库的命名空间cout是“字符输出”对象是流插入运算符把后面的内容“输送”到cout即屏幕。std::endl是换行并刷新输出缓冲区。return 0; 主函数返回0这是一个约定俗成的“成功”标志。实操心得不要满足于点击按钮运行。一定要尝试在命令行中手动使用g编译几次。这个过程能让你直观地理解“编译”这个步骤未来遇到复杂的、需要链接多个文件的工程时你才能理解构建过程Makefile, CMake在做什么。这是从“使用者”迈向“理解者”的关键一步。3. C核心语法基石从变量到函数掌握了如何让程序跑起来我们终于可以深入C的语法内部了。这一部分是构建所有程序的砖瓦理解它们是如何工作和交互的至关重要。3.1 变量与数据类型给数据一个“家”程序的核心是处理数据。变量就是内存中一个具有名称、类型和值的存储位置。你可以把它想象成一个有标签的盒子标签是变量名盒子的大小和格式由数据类型决定里面装的东西就是值。C的基本数据类型类型含义典型大小说明int整数4字节最常用的整数类型如int age 25;float单精度浮点数4字节小数精度约6-7位有效数字如float price 19.99f;double双精度浮点数8字节更精确的小数精度约15位默认的浮点类型如double pi 3.1415926;char字符1字节存储单个字符如char grade A;bool布尔值1字节只有true或false用于逻辑判断变量的声明、定义与初始化int count; // 声明/定义了一个int型变量count此时它的值是不确定的垃圾值 int score 100; // 声明并初始化这是好习惯。 auto name Alice; // C11起可以用auto让编译器自动推导类型这里name是const char*。 const double TAX_RATE 0.13; // const定义常量不可修改。重要注意事项永远、永远记得初始化你的变量未初始化的变量如上例中的count包含的是之前残留在内存地址上的随机值使用它会导致不可预知的程序行为这是最常见的Bug来源之一。养成int i 0;这样的习惯。3.2 运算符数据的加工工具运算符让你可以对数据进行计算和比较。算术运算符,-,*,/,%(取模求余数)。注意整数除法会截断小数部分5 / 2结果是2不是2.5。关系运算符,!,,,,。用于比较结果为bool类型。逻辑运算符(与)||(或)!(非)。用于组合布尔条件。赋值运算符是基本的赋值。还有复合赋值如,-,*等a 5等价于a a 5。一个综合例子int a 10, b 3; int sum a b; // 13 int quotient a / b; // 3 (整数除法) int remainder a % b; // 1 bool isGreater (a b); // true bool logicTest (a 5) (b 5); // true true true a 2; // a 现在为 123.3 控制流让程序学会“思考”和“重复”程序不能只从上到下执行需要根据条件分支和循环。条件语句 (if-else)int score 85; if (score 90) { std::cout 优秀 std::endl; } else if (score 60) { std::cout 及格 std::endl; } else { std::cout 不及格 std::endl; } // 输出及格循环语句for循环当你知道循环次数时使用。for (int i 0; i 5; i) { // 初始化条件更新 std::cout i ; // 输出 0 1 2 3 4 }技巧习惯使用前置递增i。对于内置类型它和i没区别但对于某些自定义类型如迭代器前置递增性能可能更好养成这个习惯有益无害。while循环当循环次数未知取决于某个条件时使用。int num 5; while (num 0) { std::cout num-- ; // 输出 5 4 3 2 1 }do-while循环先执行一次循环体再判断条件。适用于至少需要执行一次的场景。int input; do { std::cout 请输入一个正数: ; std::cin input; } while (input 0);3.4 数组与字符串数据的集合数组存储同一类型元素的固定大小集合。int numbers[5] {1, 2, 3, 4, 5}; // 声明一个包含5个整数的数组 numbers[0] 10; // 通过索引访问和修改索引从0开始 for (int i 0; i 5; i) { std::cout numbers[i] ; }关键点C风格数组的大小必须在编译时确定且不会检查越界访问访问numbers[5]会导致未定义行为可能崩溃也可能悄无声息地破坏其他数据。这是C中一个著名的“坑”。字符串在C中你有两种主要选择。C风格字符串本质是字符数组以空字符\0结尾。char str1[] Hello; // 编译器会自动在末尾添加\0 char str2[20]; // 操作C风格字符串需要使用cstring库的函数如strcpy, strcat, strlen容易出错且不安全。Cstd::string强烈推荐使用这个它是标准库提供的字符串类动态管理内存使用方便安全。#include string // 必须包含此头文件 std::string name Bob; name Smith; // 轻松拼接 std::cout name.length(); // 获取长度 std::cout name[0]; // 访问字符依然是B if (name Bob Smith) { ... } // 可以直接比较std::string几乎解决了所有C风格字符串的痛点是现代C编程的首选。3.5 函数代码的模块化函数是将一段完成特定任务的代码封装起来以便重复使用。它是结构化编程的核心。函数的定义// 函数定义 int add(int a, int b) { // 返回类型 函数名(参数列表) int result a b; return result; // 返回值 } // 函数声明通常在头文件中 int add(int a, int b); int main() { int sum add(3, 4); // 函数调用 std::cout sum; // 输出 7 return 0; }函数参数传递的两种重要方式传值函数获得参数的一个副本。修改副本不影响原值。void swapByValue(int x, int y) { int temp x; x y; y temp; } // 调用后实参a,b的值不变。传引用函数获得参数的一个别名引用直接操作原数据。void swapByReference(int x, int y) { int temp x; x y; y temp; } // 调用后实参a,b的值被交换。当你希望函数修改实参或者传递大型数据结构如std::vector避免拷贝开销时使用传引用。如果不需要修改且想避免拷贝可以使用const引用如void print(const std::string str)。函数重载C允许在同一作用域内定义多个同名函数只要它们的参数列表参数类型、数量或顺序不同。编译器会根据调用时提供的实参来决定调用哪个函数。int max(int a, int b) { return (a b) ? a : b; } double max(double a, double b) { return (a b) ? a : b; } // 调用 max(1, 2) 会调用第一个max(1.5, 2.5)会调用第二个。4. 面向对象编程入门用类构建你的世界C被称为“带类的C”面向对象编程是其核心范式。类Class是一种将数据属性和操作这些数据的函数方法捆绑在一起的自定义数据类型。它是蓝图而对象Object是根据这个蓝图创建的具体实例。4.1 类的定义与对象的创建想象你要开发一个游戏里面有“敌人”这种实体。用面向过程的方式你可能需要维护一堆分散的变量enemy1_health,enemy1_x,enemy1_y...。用面向对象你可以定义一个Enemy类。#include string class Enemy { // 访问修饰符控制类成员的可见性 public: // public成员在类外部可以访问 // 成员变量属性 std::string name; int health; int attackPower; // 成员函数方法 void takeDamage(int damage) { health - damage; if (health 0) health 0; std::cout name 受到 damage 点伤害剩余生命 health std::endl; } bool isAlive() const { // const成员函数承诺不修改对象状态 return health 0; } // 构造函数在创建对象时自动调用用于初始化 Enemy(const std::string n, int h, int ap) : name(n), health(h), attackPower(ap) { std::cout 敌人 name 出现了 std::endl; } private: // private成员只能在类的内部访问实现了“封装” int secretCode; // 外部无法直接访问 }; // 注意类定义结尾的分号 int main() { // 创建对象调用构造函数 Enemy goblin(哥布林, 50, 10); Enemy dragon(巨龙, 200, 35); // 使用对象 std::cout goblin.name 的生命值是 goblin.health std::endl; dragon.takeDamage(30); if (dragon.isAlive()) { std::cout dragon.name 还活着 std::endl; } // goblin.secretCode 123; // 错误secretCode是private的不能直接访问。 return 0; }关键概念解析封装将数据health和操作数据的方法takeDamage捆绑在一起并通过public和private关键字隐藏内部实现细节。private数据只能通过public方法来访问或修改这保护了数据的完整性。构造函数与类同名没有返回类型。用于初始化对象的状态。上面例子中使用了初始化列表: name(n), health(h), attackPower(ap)这是一种更高效、更推荐的初始化方式尤其在初始化const成员或引用成员时必须使用。const成员函数在函数声明后加const表示这个函数不会修改调用它的对象。这既是给编译器的承诺也是给使用者的保证可以让const对象也能调用这些函数。4.2 动态内存管理初探new与delete到目前为止我们的对象如goblin都是在栈上创建的生命周期由作用域自动管理。但有时我们需要在程序运行时动态创建对象这时就需要用到堆内存和new/delete操作符。Enemy* pBoss new Enemy(最终Boss, 500, 50); // new在堆上分配内存并调用构造函数 // pBoss是一个指向Enemy对象的指针 pBoss-takeDamage(100); // 通过指针访问成员使用 - 运算符 // 等价于 (*pBoss).takeDamage(100); // ... 使用这个对象 ... delete pBoss; // 使用完毕后必须用delete释放内存 pBoss nullptr; // 一个好习惯将指针置为nullptr防止“悬空指针”为什么需要动态内存对象很大栈空间有限通常几MB堆空间大得多GB级别。需要在运行时决定创建对象的数量例如根据关卡难度生成不同数量的敌人。对象需要存活于其创建的作用域之外。一个极其重要的警告new和delete必须成对出现如果你new了却不delete就会发生内存泄漏——分配的内存再也无法被程序使用最终可能导致程序因内存耗尽而崩溃。在现代C中我们强烈建议尽量避免直接使用裸new和delete而是使用智能指针如std::unique_ptr,std::shared_ptr它们能自动管理内存生命周期极大地减少了内存泄漏的风险。这是从C11开始最重要的最佳实践之一。#include memory std::unique_ptrEnemy smartBoss std::make_uniqueEnemy(智能Boss, 300, 40); // 不需要手动delete当smartBoss离开作用域时它会自动释放内存。4.3 继承与多态构建层次关系面向对象另外两大支柱是继承和多态。它们允许你创建层次化的类结构并让代码更灵活。继承一个类派生类可以继承另一个类基类的成员。class FlyingEnemy : public Enemy { // FlyingEnemy 公有继承自 Enemy public: int flyHeight; FlyingEnemy(const std::string n, int h, int ap, int fh) : Enemy(n, h, ap), flyHeight(fh) { // 先初始化基类部分 std::cout name 飞到了 flyHeight 米高 std::endl; } void fly() { std::cout name 正在空中盘旋。 std::endl; } // 可以重写基类的方法 void takeDamage(int damage) override { // override关键字明确表示重写 // 飞行敌人可能闪避 if (rand() % 100 30) { // 30%几率闪避 std::cout name 灵活地闪避了攻击 std::endl; return; } Enemy::takeDamage(damage); // 调用基类的版本 } }; int main() { FlyingEnemy harpy(鹰身女妖, 80, 15, 10); harpy.takeDamage(20); // 调用的是FlyingEnemy::takeDamage harpy.fly(); // 派生类特有的方法 // harpy.isAlive(); // 可以调用继承自基类的方法 }多态通过基类的指针或引用来操作派生类对象并在运行时决定调用哪个类的方法。这需要虚函数的支持。class Enemy { public: virtual void attack() { // virtual关键字声明虚函数 std::cout 敌人发动了普通攻击 std::endl; } virtual ~Enemy() {} // 基类的析构函数通常应该是虚函数 }; class Mage : public Enemy { public: void attack() override { // 重写虚函数 std::cout 法师吟唱咒语发射火球 std::endl; } }; class Archer : public Enemy { public: void attack() override { std::cout 弓箭手拉满弓射出利箭 std::endl; } }; void battle(Enemy* enemy) { // 参数是基类指针 enemy-attack(); // 这里调用哪个attack()取决于实际传入的对象类型 } int main() { Mage mage; Archer archer; battle(mage); // 输出法师吟唱咒语发射火球 battle(archer); // 输出弓箭手拉满弓射出利箭 // 或者使用指针数组/容器 Enemy* army[] {new Mage(), new Archer()}; for (Enemy* e : army) { e-attack(); delete e; } }多态是设计灵活、可扩展系统的关键。它允许你编写操作基类接口的通用代码而无需关心具体的派生类类型。5. 标准库初体验站在巨人的肩膀上C的强大不仅在于语言本身更在于其丰富的标准模板库。STL提供了大量通用的、经过高度优化的容器、算法和函数对象能极大提升开发效率。我们接触几个最常用的组件。5.1 容器std::vector是你的新朋友前面提到C风格数组的缺陷固定大小、不安全。std::vector是一个动态数组可以随时增长和缩小并且自动管理内存。#include vector #include algorithm // 用于排序等算法 int main() { // 创建一个空的vector std::vectorint scores; // 添加元素 scores.push_back(85); scores.push_back(92); scores.push_back(78); scores.emplace_back(95); // C11, 更高效直接在容器内构造元素 // 访问元素 (和数组一样安全会做边界检查) std::cout scores[0] std::endl; // 85 快速但不检查越界 std::cout scores.at(1) std::endl; // 92 .at()会检查越界越界则抛出异常 // 遍历vector for (int i 0; i scores.size(); i) { // .size()获取元素个数 std::cout scores[i] ; } std::cout std::endl; // 更现代的“范围for循环” (C11) for (int score : scores) { std::cout score ; } std::cout std::endl; // 使用STL算法排序 std::sort(scores.begin(), scores.end()); // 从小到大排序 // 查找 auto it std::find(scores.begin(), scores.end(), 92); if (it ! scores.end()) { std::cout 找到了92位置是 (it - scores.begin()) std::endl; } // 删除元素 scores.pop_back(); // 删除最后一个 // scores.erase(scores.begin() 1); // 删除第二个元素 return 0; }std::vector是你应该最先掌握和使用的容器。在大多数需要数组的场景下它都是更好的选择。5.2 输入输出进阶std::cin与格式化我们已用过std::cout输出。输入使用std::cin。int age; double salary; std::string fullName; std::cout 请输入你的年龄和工资: ; std::cin age salary; // 连续输入以空格/换行分隔 std::cout 年龄 age 工资 salary std::endl; // 注意 运算符会跳过开头的空白符空格、换行等 std::cout 请输入你的全名: ; std::cin.ignore(); // 忽略之前输入留下的换行符 std::getline(std::cin, fullName); // 读取一整行包括空格 std::cout 你好 fullName ! std::endl;常见问题混合使用和getline时会在缓冲区留下一个换行符导致接下来的getline直接读到空行。解决方法是在getline前使用std::cin.ignore()清空缓冲区。5.3 简单的文件操作C使用fstream库进行文件操作。#include fstream // 写入文件 std::ofstream outFile(log.txt); // 打开文件用于写入如果不存在则创建 if (outFile.is_open()) { outFile 这是一条日志。 std::endl; outFile 错误代码 404 std::endl; outFile.close(); // 关闭文件 } else { std::cerr 无法打开文件进行写入 std::endl; // cerr用于错误输出 } // 读取文件 std::ifstream inFile(log.txt); std::string line; if (inFile.is_open()) { while (std::getline(inFile, line)) { // 逐行读取 std::cout line std::endl; } inFile.close(); }文件操作后检查是否成功打开以及记得关闭文件是良好的编程习惯。6. 从入门到实践常见问题与调试技巧学习语法是一回事写出能运行的程序是另一回事。最后这部分我们分享一些新手必然会遇到的“坑”和基本的排查方法。6.1 编译与链接错误速查语法错误编译器在编译阶段发现。错误信息会包含文件名和行号。常见示例缺少分号;、括号不匹配{} ()、拼写错误caut、类型不匹配等。排查仔细阅读错误信息从第一个错误开始修正因为一个错误可能引发后面一连串错误。链接错误编译器通过但链接器找不到函数或变量的定义。常见示例undefined reference tofunc()。原因声明了函数但没定义定义了函数但没编译进项目比如.cpp文件没加入编译命令库文件没链接。排查检查函数名拼写确认所有用到的源文件都参与了编译确认链接了必要的库如使用数学函数需加-lm。6.2 运行时错误与调试段错误这是最令人头疼的错误之一通常是访问了非法内存地址。常见原因解引用空指针或野指针。数组越界访问C风格数组。使用已释放的内存悬空指针。递归函数无限递归导致栈溢出。调试工具gdbLinux/macOS下的命令行调试器。MinGW-w64也带gdb。常用命令gdb ./your_program-run-backtrace(查看调用栈) -print variable-quit。VSCode集成调试配置好launch.json后可以设置断点、单步执行、查看变量非常直观。这是新手最应该学会使用的调试方式。逻辑错误程序能运行但结果不对。排查方法打印调试在关键位置插入std::cout输出变量的值。使用断言#include cassert用assert(condition)在调试版本中检查假设如果条件为假程序会中止并报错。代码审查逐行检查逻辑特别是循环条件和边界情况。6.3 初学者的“避坑”指南变量未初始化重申一遍这是万恶之源。int i;之后马上使用i是危险的。养成int i 0;的习惯。和混淆在条件判断中误写if (a 5)这会将5赋值给a并且条件永远为真。编译器可能会警告。整数除法5 / 2结果是2。如果需要小数结果至少让一个操作数为浮点数5.0 / 2或5 / 2.0。字符串字面量与std::stringhello是C风格字符串常量const char*而std::string s hello;是C字符串对象。两者可以混合使用得益于隐式转换但理解其区别很重要尤其在涉及函数参数时。内存管理尽量使用std::vector,std::string, 智能指针来管理资源避免手动new/delete。如果必须用确保new和delete配对new[]和delete[]配对。头文件重复包含可能导致重复定义错误。使用头文件守卫或#pragma once。// myclass.h #ifndef MYCLASS_H // 如果MYCLASS_H未定义 #define MYCLASS_H // 定义它 class MyClass { ... }; #endif // MYCLASS_H或者更简单大多数编译器支持#pragma once class MyClass { ... };学习C就像学习一门内功深厚的外语初期会觉得语法繁杂规则众多。但每当你理解一个概念比如引用、多态、RAII并成功用它解决了一个实际问题时那种对计算机系统掌控力提升的感觉是无与伦比的。不要试图一次性记住所有细节先掌握主干然后通过实际项目比如热搜词里提到的c小游戏、用opencv c处理图像去驱动学习在实践中遇到问题再回头查阅资料、深入理解这才是最有效的路径。这篇教程为你铺好了第一段路剩下的精彩旅程需要你亲手用代码去书写。