尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C语言代码逐句拆解:从main函数到数组排序的语法精讲与PTA实战

C语言代码逐句拆解:从main函数到数组排序的语法精讲与PTA实战 写C语言题目的时候我最常被问到的一句话是“学长这句代码到底什么意思”很多同学看教材能看懂概念一放到具体题目里人就开始懵了。语法点是一回事逐句读代码是另一回事。这篇我想用大量基础题当例子把每一句代码拆开揉碎讲清楚讲透“这一行到底在干嘛、为什么这样写”。看完你再去刷PTA、做课后题起码不会再对着代码发愁。先说明一下适用对象刚开始学C语言的大学生、自学编程想打牢基础的人、以及准备期末机考但代码阅读能力偏弱的同学。内容从最基础的main函数一路讲到数组、字符串、二维数组和排序所有例子都带逐句解释你可以当成一本“能翻着看的代码注释”来用。1. C语言语法框架先把骨架搭起来1.1 一个最短的完整C程序逐句看很多初学者第一个问题不是“怎么写逻辑”而是“C语言程序长什么样”。我们先从一个最简单的程序开始每一句都标注释看完你就知道C语言的基本盘长啥样。#include stdio.h // 头文件引入标准输入输出库printf、scanf都在里面 int main() // 主函数程序执行的入口所有代码从这里的{开始跑 { // 函数体左花括号表示函数内容的起点 printf(Hello\n); // 调用printf函数把字符串打印到屏幕上\n是换行 return 0; // 返回0给操作系统表示程序正常结束 } // 函数体右花括号表示函数内容的终点这里每一句都值得说两句。第一句#include stdio.h是预处理指令它做的事情是“把标准输入输出库的声明复制到当前文件里”。你可能觉得这是废话但请记住一个关键点没有这一句你写printf和scanf编译器会直接报“隐式声明”警告或错误。为什么后面要加.h因为.h是头文件header里面存放的是函数的声明真正的函数实现在系统库文件里编译链接阶段会自动去找。第二句int main()是函数定义。int是返回类型表示主函数结束时要返回一个整数给操作系统main是函数名空的括号表示这个函数不需要参数。按C标准主函数写法有两种合法形式int main(void)或int main(int argc, char *argv[])。考试时写int main()也行但更严谨的写法是int main(void)明确告诉编译器“我没有参数”。第三句和第四句就是函数体内的语句。C语言规定每条语句以分号结尾分号是语句结束标志不是装饰品。很多新手写printf(Hello\n)漏掉分号编译直接报错这属于“语法错误”编译器会提醒你“expected ; before }”。return 0是整个程序的收尾动作如果你漏写了部分编译器会自动补一个返回值但严格的考试环境比如PTA、OJ建议按标准写全。1.2 头文件、代码块与“每一行代码的职责”接下来我们需要建立一个底层认知C语言程序不是“一堆代码的堆积”而是由头文件、宏定义、函数定义、语句四层结构组成的。理解这个分层你读题时就知道哪些句子该忽略、哪些句子才是核心。头文件区以#开头的行比如#include、#define它们不是运行时执行的语句而是编译前被处理的指令。你可以把#include理解成“复制粘贴”把#define理解成“查找替换”。函数区C程序至少有一个main函数也可以有多个辅助函数。main是总入口辅助函数负责具体某件事。语句区函数体内部由一条条语句组成按顺序从上往下执行。语句分三类表达式语句、选择语句、循环语句。读代码时你要学会“按职责分区”。比如看到#include limits.h你要立刻知道这个程序接下来很可能用到INT_MAX、INT_MIN这类整数极限值。看到int a[5][5];你要知道程序准备处理二维数组。看到for (int i 0; i 5; i)你要知道这是一个循环控制行控制变量是i从0走到4。我经常跟学生说一句话逐句解释代码本质上是在做“翻译工作”。把每一行从“C语言语法”翻译成“人类能理解的意图”。比如这句int max a[0][0];翻译过来就是定义整型变量max把二维数组a的第一个元素第0行第0列赋值给它。为什么初始值要设成a[0][0]而不是0因为在5×5鞍点之类的题目里待比较的数可能是负的如果用0当初始值比较结果就会出错。这就是“逐句解释”背后真正的价值你在解释语法更在理解为什么这句代码要这样写。2. 基础语法逐句拆解从变量到循环2.1 变量定义、输入输出与limits.h的边界意识C语言里最频繁出现的就是变量。变量定义有固定格式数据类型 变量名;也可以在定义时直接赋值叫初始化。来看一个综合例子#include stdio.h #include limits.h // 包含INT_MAX、INT_MIN等常量定义 int main(void) { int score 90; // 定义int类型变量score初始值为90 double average 0.0; // 定义double类型变量average初始值为0.0 char grade A; // 定义char类型变量grade初始值为字符A printf(score%d\n, score); // %d是int类型的格式占位符输出score的值 printf(average%.2f\n, average); // %.2f表示保留两位小数输出double printf(grade%c\n, grade); // %c是char类型的格式占位符 printf(int最大值%d, 最小值%d\n, INT_MAX, INT_MIN); return 0; }这里你需要重点掌握三件事。第一变量名是标识符只能由字母、数字、下划线组成且数字不能开头不能用C语言关键字当变量名如int、for、return都不行。第二printf的格式占位符必须和变量类型匹配int用%ddouble用%fchar用%c字符串用%s。万一你把double用%d输出数据会被错误解释轻则输出乱码重则程序崩溃。第三limits.h里的INT_MAX和INT_MIN是很有用的边界常量。为什么牵涉到“逐句解释”因为很多题目会考察“数据溢出”这个点。比如int a 2147483647; // int类型的最大值 printf(%d\n, a); // 输出2147483647 printf(%d\n, a 1); // 输出-2147483648这就叫整型溢出int类型用32位二进制存数最高位是符号位所以最大值是2^31 - 1再加1就翻到负数一侧去了。遇到涉及大数运算的题目你要么用long long要么在运算前做边界判断。这就是逐句读代码时才能发现的隐藏考点。再看输入函数scanf。很多人的错误是忘记写符号int n; scanf(%d, n); // 错误n前面缺少程序可能崩溃 scanf(%d, n); // 正确n表示取n的地址逐句解释一下scanf想要的是“往哪里存数据”的地址不是数据本身。n表示“n的值”n表示“变量n在内存中的地址”。初学者容易把printf和scanf搞混因为printf不需要它只需要值而scanf必须有字符串数组名除外因为数组名本身就是地址。这个点几乎是各校机考必考陷阱。2.2 运算符、自增自减和优先级读懂表达式这一句表达式是C语言里最容易让人头大的部分。看这句经典代码sum i;如果只看语法它同时做了三件事把i当前的值加到sum上i自增1整个表达式的值等于自增前的i。但是很多人会纠结到底是先加还是后加这就是i和i的区别。i先取i的旧值参与运算运算结束后i再自增1。i先让i自增1再用新值参与运算。举个例子int i 5; int a i; // 先把i5赋值给a然后i变成6所以a5i6 int b i; // 先让i从6变成7再把7赋值给b所以b7i7类似地sum i等价于sum sum ix * 2等价于x x * 2。这类复合赋值运算符的写法在题目里出现频率极高读题时可以把它们“展开”理解。优先级也是个老大难。我直接给你一张考试高频优先级表按从高到低排优先级运算符说明最高()[]-.括号、下标、结构体成员访问高!--*取反、自增自减、取地址、解引用中高*/%乘除取余中-加减中低移位运算低关系比较更低!相等比较很低逻辑与最低||逻辑或赋值-等赋值运算符这里我要特别提醒一个坑是相等判断是赋值。C语言里if (a 1)是合法的它会把1赋值给a然后判断a是否为真非零即真所以条件永远成立。很多程序“莫名其妙”跑错就是这个等号的问题。逐句检查时碰到if后面跟括号第一件事看它是还是。再看“短路求值”。和||有短路特性如果左边已经能决定整个逻辑表达式的真假右边就不会执行。比如if (i ! 0 a / i 1) { // 当i0时i ! 0为假a / i不会执行避免了除零错误 }这句代码的妙处在于的短路特性天然帮你做了一次“除数非零检查”。如果你写成if (a / i 1 i ! 0)当i0时程序已经先除零崩溃了后面的i ! 0还没来得及执行。这个顺序问题是考试和实际项目里都容易踩的雷。2.3 三大流程控制语句逐句理解流程控制是C语言语法的核心舞台。先说if-else。看这个例子if (score 60) { // 如果score大于等于60进入这个分支 printf(及格\n); // 打印及格 } else { // 否则score小于60 printf(不及格\n); // 打印不及格 }逐句解释if后面括号里是条件表达式表达式的值为真非0就执行后面{}里的内容为假0就跳过。else就是“否则”它不需要条件表示“上面条件不成立时执行我这里”。注意一点如果if或else后面只有一条语句花括号可以省略但我不建议省。为什么因为后期加语句时忘了加花括号会改变逻辑结构。比如if (a b) printf(a大\n); printf(这句不属于if\n); // 这句永远会执行第二个printf不在if的控制范围内。很多人读代码时以为它被if控制这就是没有逐句分析导致的误读。然后是for循环。for (int i 0; i 10; i) { printf(%d\n, i); }这句的语法结构拆开看是三段式初始化int i 0循环条件i 10每次循环结束后的步进i。执行顺序是先初始化再判断条件条件为真则进入循环体循环体结束后执行步进然后再次判断条件。当条件为假时整个循环结束。这里你要学会“模拟执行”——拿支笔在纸上画一个表i0打印0i1打印1……直到i10时条件不成立退出。while循环和do-while循环也常用。区别一句话说清while先判断后执行可能一次都不执行do-while先执行后判断至少执行一次。看例子int n 1; while (n 0) { // 条件一开始就不成立 printf(不会打印\n); } do { printf(至少打印一次\n); // 先执行 } while (n 0); // 再判断虽然n0不成立但已经执行过了遇到需要“不管条件成立与否先做一次再说”的场景比如菜单选择、密码输入校验do-while是天然的选择。break和continue也要从“这一句”的层面理解。break用于跳出当前这一层循环或switchcontinue是结束本轮循环体直接进入下一轮。我说个最容易混淆的点break不会跳出if它只能跳出循环或switch。遇到嵌套循环break只跳一层不是全部跳完。比如for (int i 0; i 3; i) { for (int j 0; j 3; j) { if (j 1) break; // 只跳出内层j的循环外层i继续 printf(i%d, j%d\n, i, j); } }这段输出只有i0,j0、i1,j0、i2,j0三行因为内层循环到j1就断了。想一次跳出所有循环常用的办法是加一个标志变量或者用goto不推荐新手用。3. 经典练习题逐句实战这5道题吃透语法3.1 九九乘法表嵌套循环的入门九九乘法表是C语言教材里出现频率最高的题目。先看完整代码再逐句解释。#include stdio.h int main(void) { for (int i 1; i 9; i) { // 外层循环i从1到9 for (int j 1; j i; j) { // 内层循环j从1到i printf(%d*%d%-2d , i, j, i * j); // 输出i*j的结果左对齐占2位 } printf(\n); // 每一行结束换行 } return 0; }逐句拆解。外层循环控制行数i代表乘法表的行号从1到9共9行。内层循环的终止条件j i是关键它保证了第几行就输出几个算式第1行输出1个第2行输出2个第9行输出9个。printf里面%-2d的-表示左对齐2表示最小宽度为2这样3*39和3*412不会挤在一起排版好看。最后一句printf(\n)表示一行算式输出完就换行。你可能会问为什么内层循环条件不是j 9如果改成j 9每一行都会输出9个算式那就不是三角形而是9×9的矩形了。这里恰恰是“逐句解释”的核心理解条件的边界如何影响结果。本题如果颠倒内外层输出顺序也会变你想输出“列式”还是“行式”全看内外层谁控制行谁控制列。3.2 整数逆序输出取余与整除的配合数字逆序是基础题里的“思维体操”比如输入12345输出54321。看代码#include stdio.h int main(void) { int n, ans 0; scanf(%d, n); // 读入一个整数放到变量n里 while (n ! 0) { // 只要n不为0就继续循环 ans ans * 10 n % 10; // 取出n的最后一位加到ans的末尾 n n / 10; // n去掉最后一位 } printf(%d\n, ans); // 输出逆序后的结果 return 0; }逐句说ans ans * 10 n % 10;是核心语句。n % 10拿到的是n的个位数字除以10取余ans * 10是把之前拼好的结果往左移动一位腾出个位位置然后加新取到的数字。假设n123第一轮ans0*1033n12第二轮ans3*10232n1第三轮ans32*101321n0循环结束。这就是“逐句模拟”的最好练习。这里有一个坑如果n是负数呢-123除以10取余C语言规则里结果是-3C99标准向零取整所以ans会变成-3最后结果-321逻辑上也对。但如果题目要求忽略负号你需要先判断if (n 0) { printf(-); n -n; }另外如果输入的数是1200这个程序输出的是21而不是0021因为ans是整数前导零自动消失。如果你要保留前导0就得用字符串处理这就是另一套思路了。3.3 冒泡排序数组与交换逻辑冒泡排序是C语言里最经典的算法题也是逐句解释价值最大的一道题因为每一行都体现了语法和算法的结合。#include stdio.h int main(void) { int arr[] {5, 3, 8, 1, 2}; // 定义一个数组并初始化 int n 5; // 数组元素个数 for (int i 0; i n - 1; i) { // 外层循环进行n-1轮冒泡 for (int j 0; j n - 1 - i; j) { // 内层循环比较相邻元素 if (arr[j] arr[j 1]) { // 如果前一个比后一个大 int temp arr[j]; // 暂存前一个元素 arr[j] arr[j 1]; // 后一个赋给前一个 arr[j 1] temp; // 暂存的值赋给后一个完成交换 } } } for (int i 0; i n; i) { // 遍历数组 printf(%d , arr[i]); // 依次输出每个元素 } return 0; }外层循环i从0到n-2共n-1轮。为什么不是n轮因为每一轮会把当前最大值“浮”到最后面所以最后一轮之前只剩两个数需要比较第n-1轮完成后数组已经有序再多一轮是浪费。内层循环的终止条件j n - 1 - i更值得解释每完成一轮最后面的i1个位置已经排好了不需要再参与比较所以比较范围缩小i个。交换三句是重复出现频率最高的代码片段temp暂存、前覆盖后、后再覆盖前。没有temp行不行直接arr[j] arr[j1]; arr[j1] arr[j];会让两个元素变成同一个值。很多初学者在这里卡壳你可以用生活经验类比两杯水互换必须借助第三个空杯子。这就是temp的意义。3.4 字符串逆序PTA风格题目拆解PTA拼题A上有大量字符串题目。比如“字符串逆序”这一题输入一个字符串输出它的逆序。核心代码如下#include stdio.h #include string.h // 引入strlen函数的头文件 int main(void) { char str[100]; // 定义字符数组能存储99个字符1个结束符 gets(str); // 从键盘读入一行字符串PTA考试老版可用 int len strlen(str); // 用strlen获取字符串实际长度 for (int i len - 1; i 0; i--) { // 从最后一个字符开始往前遍历 printf(%c, str[i]); // 逐个输出字符 } return 0; }这里头文件string.h出现在程序第一行作用就是告诉你我这个程序要用strlen函数。逐句看char str[100]定义了容量100的字符数组。gets(str)把整行输入读入数组并在末尾自动加\0字符串结束标志。strlen(str)返回的是字符串中字符的个数不包括结束符。所以如果输入“abc”len3数组内容是str[0]a、str[1]b、str[2]c、str[3]\0最后一个有效字符下标是len-12。循环从len-1往0走逐字符倒着输出。这就是“逐句解释”里最需要强调的数组边界访问str[i]时i绝对不能等于len因为str[len]是结束符不该被当普通字符输出。很多人的逆序输出结果末尾带乱码就是因为越界访问到了字符串结束符之后的内存区域。顺便说一句PTA的新版编译器可能不再支持gets因为不安全可以用fgets(str, 100, stdin)代替它会保留换行符使用时需要手动去掉末尾的\n。这也属于“逐句读懂后才知道怎么写”的坑。3.5 5×5鞍点问题二维数组与逻辑判断这是热搜词里专门提到的题“使用stdio.h和limits.h用c语言解决计算5*5鞍点问题”。鞍点的定义在该行上最大、同时在该列上最小的元素位置。这个题非常适合用来讲二维数组和逻辑判断的结合代码也比前面的题长一些。#include stdio.h int main(void) { int a[5][5]; // 定义5行5列的二维数组 int i, j; // 输入部分两层循环读入25个数 for (i 0; i 5; i) { for (j 0; j 5; j) { scanf(%d, a[i][j]); } } // 查找鞍点部分 int found 0; // 标记变量0表示未找到1表示已找到 for (i 0; i 5; i) { // 遍历每一行 int rowMax a[i][0]; // 先假设当前行第一个元素是最大值 int maxCol 0; // 记录最大值所在列的下标 for (j 1; j 5; j) { // 从第1列开始遍历该行 if (a[i][j] rowMax) { // 如果发现更大的值 rowMax a[i][j]; // 更新行最大值 maxCol j; // 更新列下标 } } // 判断这个行最大值是否是该列最小值 int isMin 1; // 先假设它就是列最小值 for (int k 0; k 5; k) { // 遍历这一列的所有行 if (a[k][maxCol] rowMax) { // 一旦发现更小的数 isMin 0; // 说明它不是列最小 break; // 提前退出不用再比较 } } if (isMin) { // 如果isMin仍为1说明是鞍点 printf(鞍点是 a[%d][%d] %d\n, i, maxCol, rowMax); found 1; // 标记已找到 break; // 找到第一个鞍点后退出外层循环 } } if (!found) { // 如果found为0即取反为真 printf(没有鞍点\n); } return 0; }逐句解释重点。int a[5][5]定义了一个5×5矩阵a[i][j]表示第i行第j列。输入部分用嵌套循环先固定行i再让列j从0到4依次读入。rowMax a[i][0]是初始化技巧不直接设0是为了防止矩阵里全是负数时最大值被漏算。maxCol必须跟着rowMax一起更新否则后面找列的时候不知道是哪一列。判断列最小那一小段是新手最容易写错的地方。有人会问能不能找到行最大值后直接说它是鞍点不行。鞍点的判定是“行最大且列最小”行最大是必要条件不是充分条件。所以代码里要用第二个嵌套循环把maxCol这一列的所有元素都检查一遍。isMin是一个标志变量初始假设成立一旦发现反例就置0。break的使用也体现了一个原则已经确定不是最小值就没有必要继续比较下去可以提前结束这一轮的检查。最外层找到鞍点后break意味着“只求第一个鞍点”。如果题目要求输出所有鞍点你得把break去掉改用found在每次找到时置1最后统一判断。这个“只找一个”和“找全部”的处理差异是很多考试题的变体来源。最后的if (!found)利用了!逻辑非运算符found为0时!found为1真所以输出“没有鞍点”。这就是C语言里“非零即真、零即假”的直接体现。4. 逐句看懂代码之后调试与常见编译报错4.1 GCC编译与gdb调试的逐句价值逐句读代码的能力不是靠背语法规则练出来的而是靠“调试”练出来的。我自己带学生的经验是代码写错了不要急着问别人先把每一句打印出来看中间结果。如果你在Windows上用Dev-C或Visual Studio在Linux虚拟机上用GCC编译命令都是一样的思路gcc program.c -o program ./programgcc program.c -o program这一句的意思是用GCC编译器把program.c编译成可执行文件program。-o是“output”的缩写后面跟输出文件名。编译时如果报错会提示你行号和错误类型。比如error: expected ; before }直接定位到某个}之前少写了分号。调试时我强烈建议新手学会gdb。在Ubuntu虚拟机上用gcc -g program.c -o program编译-g表示生成调试信息然后执行gdb program。gdb里几个最常用的命令是break 行号设置断点、run运行到断点、print 变量名查看变量值、next单步执行当前行遇到函数不进入、step单步执行遇到函数进入、continue继续运行到下个断点。这些命令的价值在于你可以像放慢镜头一样看每一行代码执行完以后变量发生了什么变化。比如冒泡排序那个例子里你在内层交换处设个断点print arr[0]、print arr[1]就能亲眼看交换前后数值怎么变。很多学生说“代码逻辑我看懂了但我不知道错在哪”其实就是缺少逐句观察变量的过程。我常说gdb是长在你眼睛上的放大镜让每一句代码的执行结果都变得可见。4.2 新手最容易踩的5个语法坑结合我批改过的几十份作业和PTA提交记录下面这些坑出现的频率最高。我整理成一张速查表方便你逐条核对。错误类型错误代码示例正确写法原因与教训缺少分号printf(hi)printf(hi);C语句以分号结尾编译器会在下一行报错scanf漏scanf(%d, n);scanf(%d, n);scanf需要变量地址是取地址运算符等号和赋值混淆if (a 1)if (a 1)是赋值才是相等判断数组越界int a[5]; a[5]10;下标只能在0到4之间数组长度为5下标从0开始字符串忘加结束符char s[3]{a,b,c};char s[4]{a,b,c,\0};字符串必须用\0结尾否则strlen会读到越界逐个细说一下。分号漏写是入门第一周最常见的报错但有趣的是编译器经常把错误定位到下一行因为编译器以为上一句话还没结束。所以看到error: expected ;先检查上一行是不是漏了分号。scanf漏的后果很隐蔽程序可能不崩溃也可能崩溃取决于运气。根本原因是它把一个非法地址传给了scanf往不该写的内存里写数据可能导致系统保护错误。如果你在PTA上提交这类代码可能输出乱码或者“运行时错误”。数组越界是C语言里最“阴险”的错误。C语言不像Java或Python会自动检查下标int a[5]; a[5]10;在编译阶段通常不会报错但运行时可能把数组后面的内存改坏了。这类错误用gdb调试时也能观察到变量值莫名其妙被改。字符串结束符是PTA字符串题的经典坑。char s[3] {a, b, c};看起来没问题但strlen(s)会一直往后数内存里的字节直到遇到一个恰好是0的字节为止结果完全不可预测。定义字符数组时长度一定要预留1给\0。这是“每一句代码解释”里必须永远记住的隐性规则——数组定义这里不写\0但它存在于使用逻辑中。5. 从语法到工程工具链、学习路径与我的经验5.1 VSCode/Ubuntu环境配置与多文件项目的组织热搜词里还有一条“虚拟机(ubuntu)配置c语言环境”确实很多学校上机环境就是Ubuntu虚拟机。我建议你从一开始就用标准工具链Ubuntu GCC VSCode gdb。配置步骤不复杂我这里只强调几个关键点。安装编译器用sudo apt install gcc。验证是否成功用gcc --version。VSCode里写C语言需要一个简单的tasks.json用来配置编译任务。如果你只学课内题目不走大型项目完全不需要复杂的CMake配置一个gcc命令就够了。很多人在配置环境时卡在“中文编码”上。Ubuntu终端默认UTF-8printf输出中文一般没问题Windows的CMD默认GBK中文经常乱码。这不是你代码写错了是编码不匹配。遇到这种问题最省心的办法是在VSCode里把终端编码调到UTF-8或者干脆全用英文输出调试信息。等到你开始写超过300行的代码就要学会拆文件。把函数声明放进.h头文件把函数实现放进.c源文件主程序再#include myfunctions.h。编译多文件时用gcc main.c functions.c -o programmain.c和functions.c两个源文件会分别编译再链接成一个可执行文件。逐句解释层面你要记住#include myfunctions.h两边是双引号表示先搜索当前目录#include stdio.h两边是尖括号表示只在系统目录搜索。这个区别很多人学了半年都没注意过。5.2 “语法糖”与进阶之路从基础语法到真正的程序思维“语法糖”这个词是编程语言界的一个常用调侃它指的是那些“写起来更舒服但并没有本质新功能”的语法形式。比如C语言里的a b就是a a b的语法糖for (int i 0; i n; i)里的C99风格定义int i也是语法糖老标准要在循环外先声明。理解语法糖的意义在于你见到简洁写法时心里立刻能“展开”成最基础的形式这样逐句解释就不再是问题。学完本文这些内容后我建议你按这个顺序继续走先把指针学透指针是C语言区别于其他语言的核心语法主体然后学结构体和链表——热搜词里出现了“c结构体链表基本语法”C语言的结构体正是C的前置基础再往后是文件操作玩明白fopen、fgets、fprintf后你就能处理真实数据而不是只靠键盘输入。编程学习有一个规律语法是骨架题目是肌肉调试是神经。你会写代码不算什么能从“看不懂别人代码”变成“逐句看懂”才说明你真的懂了。我在实际带教过程中的体会是最快进步的人往往不是最先写出正确答案的人而是愿意在PEPresentation Error或WAWrong Answer之后把代码一行一行读出来复述给自己听的那些人。这个习惯希望你能养成。
返回列表