1. 为什么现在还要学C这个问题我几乎每年都会被问到。在Python、JavaScript大行其道的今天一个新手程序员尤其是非科班出身的朋友常常会困惑我为什么要去碰C这个看起来“古老”又“复杂”的家伙直接上手Python做项目不香吗我的回答是如果你想真正理解计算机程序是如何运作的想从“写脚本”进化到“做系统”想拥有在性能、资源、底层控制上无可替代的能力那么C是你绕不开的基石。它不像Python那样给你一个封装好的、功能强大的“魔法工具箱”让你快速拼出成品。C更像是一套精密的机床和原材料它把内存、指针、类型这些最底层的概念直接暴露给你让你亲手去锻造每一个零件。这个过程当然更痛苦但当你完成时你对整个“制造”过程的理解是透彻的。你会发现那些用高级语言看似理所当然的“魔法”背后都是C这类语言在默默支撑。举个例子你现在用Python的list.append()可以轻松添加元素感觉不到任何成本。但在C里使用std::vector时你就必须思考当前容量够不够不够的话是重新分配一块更大的内存把旧数据拷贝过去还是有什么更优的策略这种思考会让你在日后使用任何语言的集合类时都本能地关注其时间复杂度和空间消耗写出更高效的代码。这就是C带来的“内功”修炼。再者看看那些热词游戏编程、OpenCV、多线程、设计模式……这些领域的核心库和引擎大量使用C。高性能计算、量化交易、嵌入式系统、数据库、浏览器引擎这些领域的基石同样是C。学习它是打开这些高薪、核心领域大门的钥匙。它不是过时的语言而是经久不衰的“系统级语言”。所以这个“入门基础篇”我希望带你走的不是一条死记硬背语法的路而是一条理解计算机本质、培养工程思维的路。2. 环境搭建选对工具事半功倍万事开头难而配置环境往往是劝退新手的第一个门槛。网上的教程五花八门有让你装Visual Studio的有让你用VSCode配MinGW的还有直接推荐在线编译器的。别慌我们一步步来搞清楚背后的逻辑。2.1 编译器程序的“翻译官”C代码.cpp文件是人类可读的文本计算机的CPU只认识0和1组成的机器码。编译器Compiler就是负责把前者翻译成后者的工具。没有编译器一切免谈。对于Windows用户最常见的两个选择是Microsoft Visual C (MSVC)这是微软官方编译器集成在Visual Studio IDE中。它的特点是和Windows系统结合紧密对微软自家的技术栈支持最好。你在安装一些软件比如某些游戏或专业软件时系统提示你安装的“Microsoft Visual C Redistributable”就是运行用MSVC编译的程序所必需的运行时库。热词里提到的安装Node.js时遇到的“Microsoft Visual C 2022 x86 minimum runtime安装包不存在”错误根本原因就是系统缺少这个运行库而不是编译器本身。MinGW-w64 / GCC这是GNU编译器套装GCC的Windows移植版。它更贴近Linux/Unix世界的开发环境支持标准的C特性通常比较快。很多跨平台项目比如很多开源库都使用GCC/Clang系列编译器。通过MinGW-w64你可以在Windows上使用GCC。对于初学者我的建议是如果你想专注于学习C语言本身并且希望环境更接近工业界的跨平台开发首选MinGW-w64。它的配置过程虽然稍显繁琐但能让你更清楚地理解编译、链接的各个环节。如果你确定未来主要做Windows桌面应用开发那么直接安装Visual Studio Community免费版本也是一个非常省心的选择它把编译器、编辑器、调试器都打包好了。2.2 编辑器与IDE你的“工作台”选好编译器你需要一个写代码的地方。Visual Studio Code (VSCode)这是一个轻量级但功能强大的编辑器。它本身不是IDE但通过安装插件如C/C、Code Runner可以变身成一个相当好用的C开发环境。它的优点是轻快、免费、插件生态丰富、颜值高。热词里“vscode配置c环境”搜索量巨大正说明了它的流行度和配置的普遍需求。配置VSCode需要你手动指定编译器路径、设置调试环境这个过程本身就是一个很好的学习。Visual Studio (VS)这是一个重量级的集成开发环境 (IDE)。开箱即用功能巨全代码补全、调试、图形界面设计、性能分析等但体积也巨大轻松几十GB。对于纯粹学习C语法有点“杀鸡用牛刀”。CLionJetBrains公司出品的专业C IDE非常智能体验一流但需要付费学生可免费申请。新手实操建议下载并安装MinGW-w64。建议下载离线安装包或者使用像MSYS2这样的包管理器来安装它能帮你更好地管理工具链。下载并安装VSCode。在VSCode中安装C/C扩展由Microsoft发布。配置VSCode。关键步骤是创建一个c_cpp_properties.json文件在里面正确指定你的MinGW-w64编译器路径即g.exe所在位置。再创建一个简单的tasks.json来定义编译任务。注意网上很多教程的配置代码是过时的。最可靠的方法是在VSCode中按CtrlShiftP输入 “C/C: Edit Configurations (UI)”通过图形界面来设置编译器路径和标准如C17这比直接写json文件更不容易出错。2.3 第一个程序从“Hello World”到理解流程环境配好我们来写经典的第一段代码#include iostream int main() { std::cout Hello, World! std::endl; return 0; }别急着运行。我们来拆解每一行#include iostream这是一个预处理指令。它告诉编译器“在编译我的代码之前请先把iostream这个头文件的内容原封不动地拷贝到这里来。”iostream是输入输出流库包含了cout、cin等对象的声明。没有它编译器就不认识std::cout。int main() { ... }这是程序的入口函数。任何一个可执行的C程序都必须有一个main函数。操作系统运行你的程序就是从调用main函数开始的。int表示这个函数执行完毕后会返回一个整数给操作系统通常用0表示成功非零表示错误。std::cout Hello, World! std::endl;std::是一个命名空间。cout和endl都定义在std命名空间里加上前缀是为了避免命名冲突。你可以通过using namespace std;来省略它但在大型项目或头文件中明确写上std::是更好的习惯。cout是“标准输出”对象通常指向你的命令行终端。是“输出运算符”把它右边的数据“流向”左边的cout。Hello, World!是一个字符串字面量。endl的作用是输出一个换行符并刷新输出缓冲区。如果只是换行可以用\n但endl会强制刷新在需要确保信息立即显示时有用比如调试但性能稍差。return 0;main函数结束返回0给操作系统。编译运行流程预处理处理所有#开头的指令展开头文件进行宏替换。生成一个临时的.i文件纯文本。编译将预处理后的C代码翻译成汇编代码.s文件。这个阶段进行语法和语义检查。汇编将汇编代码翻译成机器码生成目标文件.o或.obj文件。一个.cpp文件对应一个目标文件。链接将你的main.obj和标准库如libstdc.a中实现cout的代码等其他目标文件“链接”在一起解决函数和变量的地址引用最终生成可执行文件.exe。在VSCode中你配置的编译任务对应tasks.json就是帮你自动执行g -g hello.cpp -o hello.exe这样的命令完成了编译和链接。-g参数是为了添加调试信息。3. 核心基石变量、类型与运算符过了“Hello World”这一关我们开始接触构建程序的基本砖块。3.1 基本数据类型给数据一个“房子”C是静态类型语言意味着每个变量在使用前都必须声明其类型且类型在编译期就确定了。这保证了安全性和效率。类型含义典型大小64位系统备注bool布尔型1字节取值true或falsechar字符型1字节存储一个ASCII字符如Aint整型4字节最常用的整数类型范围约±21亿float单精度浮点型4字节小数精度约6-7位有效数字double双精度浮点型8字节小数精度约15-16位有效数字默认推荐void无类型N/A用于函数无返回值或指针类型声明与初始化int age 25; // 声明并初始化 double price; // 仅声明值未定义可能是任意值 price 99.8; // 赋值 auto score 95.5; // 使用 auto编译器自动推断为 double const float PI 3.14159f; // 常量值不可修改注意养成“声明即初始化”的好习惯。未初始化的局部变量其值是未定义的直接使用会导致不可预知的行为这是常见的Bug来源。3.2 运算符数据的“加工工具”运算符丰富是C的特点之一。算术运算符,-,*,/,%(取模)。注意整数相除结果仍是整数舍去小数。关系与逻辑运算符,,,!,(与),||(或),!(非)。结果是bool类型。赋值运算符。以及复合赋值,-,*,/等a 5等价于a a 5。自增自减i(前缀自增先加后用),i(后缀自增先用后加)。对于简单类型性能无差异对于复杂类型如迭代器前缀式i通常更优。位运算符(与),|(或),^(异或),~(取反),(左移),(右移)。常用于底层操作、标志位管理或某些算法优化热词中的“树状数组”实现就大量依赖位运算。条件运算符条件 ? 表达式1 : 表达式2唯一的三目运算符。类型转换隐式转换编译器自动进行如int和double运算int会被提升为double。显式转换强制转换C风格(double)numC风格static_castdouble(num)。强烈推荐使用C的static_cast等它们更安全意图更明确。3.3 输入与输出与程序“对话”我们已见过cout。输入使用cin#include iostream #include string // 为了使用 std::string int main() { std::string name; int year; std::cout 请输入你的名字和出生年份: ; std::cin name year; // 从标准输入读取 std::cout 你好 name 你大约 (2024 - year) 岁了。 std::endl; return 0; }注意cin 以空白符空格、制表符、换行为分隔。如果想读入一整行包含空格需要使用std::getline(std::cin, myString)。混合使用cin 和getline时要注意处理cin后留在输入缓冲区里的换行符这是一个经典坑点。通常的解决方法是在getline前使用std::cin.ignore()忽略掉之前的换行。4. 程序逻辑控制让代码“思考”和“重复”程序不能只会顺序执行更需要根据条件分支和循环。4.1 条件分支if-else 与 switchif-else是最直观的分支int score; std::cin score; if (score 90) { std::cout 优秀 std::endl; } else if (score 60) { std::cout 及格 std::endl; } else { std::cout 不及格 std::endl; }switch适用于基于一个整型或枚举类型的多路分支char grade; std::cin grade; switch (grade) { case A: std::cout 优秀; break; // 必须break否则会“贯穿”执行下一个case case B: std::cout 良好; break; case C: std::cout 中等; break; default: // 其他所有情况 std::cout 未知等级; break; }实操心得switch的case标签必须是编译期常量表达式。break语句千万别忘了写这是新手常犯的错误会导致逻辑错误。4.2 循环重复的力量for循环已知循环次数时最清晰。// 计算1到100的和 int sum 0; for (int i 1; i 100; i) { sum i; } // 基于范围的for循环 (C11起)遍历容器或数组极其方便 int arr[] {1, 2, 3, 4, 5}; for (int num : arr) { std::cout num ; }while循环条件在循环开头检查可能一次都不执行。int num; std::cout 请输入一个正数输入负数退出: ; std::cin num; while (num 0) { std::cout 你输入了: num std::endl; std::cout 请继续输入: ; std::cin num; }do-while循环先执行一次循环体再检查条件。适用于至少需要执行一次的场景。int password; do { std::cout 请输入密码123456: ; std::cin password; } while (password ! 123456); std::cout 密码正确 std::endl;循环控制语句break立即跳出当前整个循环。continue跳过本次循环剩余的语句直接进入下一次循环的条件判断。一个综合小例子结合热词“c指定顺序输出”要求输入三个数按从大到小输出。#include iostream #include algorithm // 为了使用 std::swap int main() { int a, b, c; std::cin a b c; // 通过比较和交换确保 a b c if (a b) std::swap(a, b); if (a c) std::swap(a, c); if (b c) std::swap(b, c); std::cout a b c std::endl; return 0; }这里用了std::swap来交换两个变量的值比自己写临时变量交换更简洁安全。这个例子训练了条件判断和变量操作的基本功。5. 复合数据类型数组、字符串与结构体基本类型只能表示单一数据。我们需要更复杂的数据结构。5.1 数组同一类型的集合数组是在内存中连续存储的、固定大小的同类型元素集合。// 声明并初始化一个包含5个整数的数组 int scores[5] {95, 88, 72, 60, 99}; // 访问元素索引从0开始 std::cout 第一个分数: scores[0] std::endl; // 输出95 scores[2] 80; // 修改第三个元素 // 计算数组长度元素个数 int length sizeof(scores) / sizeof(scores[0]);关键点大小固定声明后不能改变。int arr[10];这个10必须是编译期可知的常量。越界访问是未定义行为访问scores[5]或scores[-1]可能导致程序崩溃或数据损坏且编译器不一定报错。这是C/C中最危险的错误之一。数组名在很多情况下如传递给函数数组名会退化为指向其首元素的指针。scores和scores[0]在值上是相同的。5.2 字符串字符的数组C语言风格的字符串本质是char数组以空字符\0结尾。char str1[] Hello; // 编译器自动计算大小包含\0实际大小为6 char str2[20] World; // 可以预留空间 // 操作需要 cstring 头文件 #include cstring strcpy(str2, New World); // 拷贝字符串 int len strlen(str2); // 获取长度不含\0警告C风格字符串操作极易出错缓冲区溢出如strcpy如果目标空间不够就会覆盖其他内存。现代C中应优先使用std::string。std::string(来自string)这才是你应该用的字符串类型。它自动管理内存大小可变提供了丰富的成员函数查找、替换、子串、连接等。#include string std::string s1 Hello; std::string s2 World; std::string s3 s1 s2; // 连接非常方便 s3.append(!); // 追加 int pos s3.find(World); // 查找 if (pos ! std::string::npos) { std::cout Found at: pos std::endl; } std::cout Length: s3.length() std::endl;std::string让字符串处理变得安全和简单是必须掌握的工具。5.3 结构体自定义的数据类型当需要把一些逻辑上相关的、不同类型的数据组合在一起时就需要结构体struct。// 定义一个“学生”结构体类型 struct Student { int id; std::string name; double score; }; // 注意分号 int main() { // 创建结构体变量 Student stu1; stu1.id 1001; stu1.name 张三; stu1.score 92.5; // 初始化列表方式 Student stu2 {1002, 李四, 88.0}; // 访问成员 std::cout stu1.name 的成绩是 stu1.score std::endl; return 0; }结构体是C中“类”class的前身。在C中struct和class的主要区别默认的成员访问权限struct是publicclass是private。热词中提到的“c结构体链表基本语法”链表节点通常就是用结构体来定义的struct ListNode { int val; ListNode* next; // 指向下一个节点的指针 // 构造函数方便创建节点 ListNode(int x) : val(x), next(nullptr) {} };这里出现了ListNode*这引出了C中最核心也最令人头疼的概念之一——指针。我们将在下一篇中深入探讨。6. 函数模块化与代码复用把一段完成特定功能的代码封装起来给它起个名字这就是函数。它是代码复用的基本单位也是实现模块化设计的关键。6.1 函数的定义与调用// 函数定义 // 返回类型 函数名(参数列表) { 函数体 } int add(int a, int b) { // a, b 是形式参数 int sum a b; return sum; // 返回值 } // 无返回值的函数 void printHello() { std::cout Hello! std::endl; // 无需return语句或使用 return; } int main() { // 函数调用 int result add(5, 3); // 5, 3 是实际参数 std::cout 5 3 result std::endl; printHello(); return 0; }函数声明原型如果函数定义在调用之后或者定义在别的文件里需要先声明。// 函数声明告诉编译器有这个函数 int max(int x, int y); int main() { int m max(10, 20); // 编译器知道max接收两个int返回int // ... return 0; } // 函数定义可以在后面或其他文件 int max(int x, int y) { return (x y) ? x : y; }6.2 参数传递的三种方式这是理解函数如何与外界交互的核心。传值将实参的拷贝传递给形参。函数内对形参的修改不影响实参。void swap_by_value(int a, int b) { int temp a; a b; b temp; } int x 5, y 10; swap_by_value(x, y); // x 仍然是5, y 仍然是10交换失败传引用将实参的别名引用传递给形参。函数内对形参的修改直接影响实参。引用在定义时必须初始化且不能重新绑定到其他变量。void swap_by_reference(int a, int b) { // 表示引用 int temp a; a b; b temp; } int x 5, y 10; swap_by_reference(x, y); // x 变为10, y 变为5交换成功传指针将实参的地址传递给形参。函数内通过解引用操作符*来访问和修改该地址处的值。void swap_by_pointer(int *a, int *b) { // * 表示指针类型 int temp *a; // *a 表示取指针a所指向的值 *a *b; *b temp; } int x 5, y 10; swap_by_pointer(x, y); // 取地址 // x 变为10, y 变为5交换成功如何选择内置简单类型int, double等且不需要修改实参传值。开销小安全。需要修改实参或实参是大型对象如std::string,std::vector传常量引用const T或非常量引用T。这是C中最常用、最高效的方式。传引用避免了拷贝整个对象的开销。如果函数承诺不修改对象就用const引用如void print(const std::string str);。传指针在C语言中很常见。在C中除非需要“可选参数”指针可以为nullptr或需要操作动态内存否则优先使用引用。指针更灵活但也更危险可能为空、可能指向已释放的内存。6.3 函数重载与默认参数函数重载允许在同一作用域内定义多个同名函数只要它们的参数列表参数类型、个数、顺序不同即可。编译器根据调用时提供的实参来决定调用哪个版本。void print(int i) { std::cout 整数: i std::endl; } void print(double d) { std::cout 浮点数: d std::endl; } void print(const std::string s) { std::cout 字符串: s std::endl; } // 调用 print(10); // 调用 print(int) print(3.14); // 调用 print(double) print(hello); // 调用 print(const std::string)注意返回值类型不同不能构成重载。重载解析发生在编译期。默认参数允许在函数声明中为参数指定一个默认值。调用时如果省略该参数则使用默认值。默认参数必须从右向左连续设置。// 函数声明中指定默认参数 void setupWindow(int width, int height, const std::string title My App, bool fullscreen false); // 调用 setupWindow(800, 600); // 使用默认的title和fullscreen setupWindow(1024, 768, Game); // 指定titlefullscreen用默认的false setupWindow(1920, 1080, Movie, true); // 指定所有参数 // setupWindow(800); // 错误height没有默认值必须提供6.4 内联函数对于函数体很小、调用频繁的函数函数调用的开销压栈、跳转、返回可能比执行函数体本身还大。inline关键字建议编译器将函数调用处用函数体代码直接替换以空间换时间。inline int square(int x) { return x * x; } int main() { int a square(5); // 编译器可能会将此处替换为 int a 5 * 5; return 0; }注意inline只是一个建议编译器最终决定是否内联。现代编译器优化能力很强即使没有inline关键字也可能对短小函数自动内联。在类定义内部直接实现的成员函数默认就是内联的。7. 实战演练从问题到代码理论学习再多不动手都是空谈。我们结合热词中的一些具体问题来把前面的知识串联起来。7.1 问题一计算快递费问题描述来自热词根据快递的件数和用户是否选择加急计算快递费。计算规则: 件数在10件以内含10件每件8元超过10件超过部分每件5元。加急服务费固定为10元。分析与实现 这是一个典型的条件判断和简单计算问题。我们需要两个输入件数count和是否加急isUrgent可以用布尔型或字符型表示。计算逻辑分两部分基础运费和加急费。#include iostream int main() { int count; char urgentChoice; // 用字符y或n表示是否加急 std::cout 请输入快递件数: ; std::cin count; std::cout 是否需要加急服务? (y/n): ; std::cin urgentChoice; // 计算基础运费 int baseCost; if (count 10) { baseCost count * 8; } else { baseCost 10 * 8 (count - 10) * 5; } // 计算加急费 int urgentCost 0; if (urgentChoice y || urgentChoice Y) { urgentCost 10; } // 总费用 int totalCost baseCost urgentCost; std::cout 基础运费: baseCost 元 std::endl; if (urgentCost 0) { std::cout 加急服务费: urgentCost 元 std::endl; } std::cout 总计: totalCost 元 std::endl; return 0; }思考扩展如果加急费不是固定值而是基础运费的20%呢如果还有会员折扣呢试着修改代码实现。这练习了变量的使用、条件判断和算术运算。7.2 问题二求最大公因数GCD问题描述来自热词求两个正整数的最大公因数。分析与实现 这是一个经典的算法问题。我们可以使用欧几里得算法辗转相除法其原理是gcd(a, b) gcd(b, a % b)直到余数为0此时的除数就是最大公因数。#include iostream // 使用递归实现欧几里得算法 int gcd_recursive(int a, int b) { if (b 0) { return a; } return gcd_recursive(b, a % b); } // 使用循环实现更高效避免递归开销 int gcd_iterative(int a, int b) { while (b ! 0) { int temp b; b a % b; a temp; } return a; } int main() { int num1, num2; std::cout 请输入两个正整数: ; std::cin num1 num2; // 确保num1 num2不是必须的算法本身能处理 if (num1 num2) { std::swap(num1, num2); } int result gcd_iterative(num1, num2); std::cout 最大公因数是: result std::endl; return 0; }思考扩展如何求最小公倍数LCM有一个公式lcm(a, b) a * b / gcd(a, b)。但要注意a * b可能会溢出可以写成a / gcd(a, b) * b来避免。试着实现一下。7.3 问题三质数乘积问题问题描述来自热词已知正整数 n 是两个不同的质数的乘积试求出两者中较大的那个质数。分析与实现 这是一个数论问题。已知n p * q且p和q都是质数p ! q。我们需要找到较大的那个。最直接的方法是从sqrt(n)开始向下找找到的第一个能整除n的数i那么i和n/i就是两个质因数取较大的即可。为什么从sqrt(n)开始因为对于任意一个合数n它至少有一个因子小于等于sqrt(n)。#include iostream #include cmath // 使用 sqrt 函数 int main() { int n; std::cout 请输入一个正整数两个不同质数的乘积: ; std::cin n; int largerPrime 1; // 从 sqrt(n) 向下遍历 int start static_castint(std::sqrt(n)); for (int i start; i 1; --i) { if (n % i 0) { // i 是 n 的因子那么另一个因子是 n/i // 根据题意i 和 n/i 都是质数且 i ! n/i largerPrime (i n / i) ? i : n / i; break; // 找到就退出 } } if (largerPrime ! 1) { std::cout 较大的质数是: largerPrime std::endl; } else { std::cout 输入的数字不符合条件或未找到质因数。 std::endl; } return 0; }思考扩展如何判断一个数i是不是质数可以写一个isPrime函数。在这个问题里由于题目保证n是两个质数的乘积我们找到的因子一定是质数所以可以省略质数判断。但作为一个练习你可以实现isPrime函数并在循环中加入判断使程序更健壮。8. 常见问题与调试初探作为入门篇的结尾我们来聊聊新手几乎一定会遇到的几个典型问题以及最基本的调试方法。8.1 编译错误与链接错误语法错误编译器在编译阶段发现。比如缺少分号、括号不匹配、使用了未声明的变量、类型不匹配等。错误信息会指出文件和行号仔细阅读就能定位。error: expected ‘;’ before ‘}’ token- 某行缺少分号。error: ‘cout’ was not declared in this scope- 可能忘了写std::cout或using namespace std;或者没包含iostream。链接错误编译器在链接阶段发现。比如函数声明了但没定义或者重复定义。undefined reference tofunc()- 找不到func 函数的实现。检查是否写了函数体或者链接时是否包含了对应的源文件/库文件。multiple definition offunc()-func函数被定义了多次。通常是因为头文件里的函数定义没有用inline或函数体放在了.cpp 文件里。8.2 运行时错误与逻辑错误运行时错误程序运行中崩溃。最常见的是段错误访问了不属于你的内存。比如数组越界、解引用空指针或野指针。除零错误整数除以0。逻辑错误程序能运行但结果不对。这是最棘手的需要调试。8.3 最基本的调试方法打印大法与调试器打印大法在怀疑有问题的代码前后插入std::cout打印关键变量的值。这是最简单粗暴也最有效的方法之一。int a 10, b 0; // 怀疑这里b可能为0 std::cout Before division: a a , b b std::endl; int c a / b; // 如果b为0这里会崩溃 std::cout After division: c c std::endl;使用调试器以VSCode为例配置好launch.json后可以设置断点在代码行号左侧点击出现红点。程序运行到这一行会暂停。单步执行F10逐过程不进入函数F11逐语句进入函数。查看变量在调试侧边栏可以看到当前作用域内所有变量的值。监视表达式可以添加你想持续观察的表达式。热词中提到的“trae c调试”可能指的是某种调试工具或插件但对于初学者掌握VSCode或你所用IDE自带的调试器就足够了。调试是程序员最重要的技能之一越早熟悉越好。8.4 初学者的几个“坑”未初始化变量局部变量不会自动初始化为0其值是随机的。务必初始化。混淆和if (a 5)是赋值永远为真C中赋值表达式的结果就是被赋予的值。判断相等要用。数组越界牢记数组索引从0开始到size-1结束。整数除法int a 5, b 2; double c a / b;这里c是2.0而不是2.5因为a/b是整数除法。要得到浮点数结果至少让一个操作数为浮点型double c a / (double)b;。字符串字面量的修改char* p hello; p[0] H;这是未定义行为因为字符串字面量存储在只读区域。应该用字符数组char p[] hello; p[0] H;。学习C就像学习一门严谨的手艺开头磕磕绊绊是常态。理解了变量、类型、运算符、控制流、函数这些基础概念你就已经搭建起了坚实的脚手架。下一篇我们将直面C的灵魂与魔鬼——指针与内存管理并开始接触面向对象编程的雏形。当你跨过那道坎你会看到一个更强大、也更自由的世界。