✨ 把代码写进星轨,用逻辑丈量宇宙。
| 导航 | 链接 |
|---|
| 个人主页 | 🏠 星轨初途 |
| 基础语言专栏 | 💻 C语言 、 📚 数据结构 |
| C++ 进阶专栏 | 🏆 C++学习(竞赛类) 、 ⚙️ C++专栏(开发类) |
| 刷题实战专栏 | 🚀 算法及编程题分享 、 📝 LeetCode 每日一题 |
前言
嗨,欢迎来到 C 语言的第一站!👋
第一次接触 C 语言,我们通常会先敲下这样一段代码:
- #include <stdio.h>
- int main(void)
- {
- printf("hello C\n");
- return 0;
- }
复制代码
点一下运行,屏幕上出现 hello C。看起来很顺利,但代码里的每一部分都在冒问题:
- .c 文件明明只是一份文本,为什么能运行?
- 程序为什么从 main 开始?
- printf 是从哪里来的?
- 单引号和双引号有什么区别?
- 字符串 "abc" 明明只有 3 个字母,为什么内存里要占 4 个字节?
别急,这一讲就沿着这些问题往下走。我们先认识 C 语言和编译器,再回到第一个程序,最后把字符、字符串、语句与注释这些常见概念串起来。
一、C 语言是什么?
人和人交流会使用汉语、英语等自然语言;想让计算机替我们完成任务,也得找到一种双方约定好的表达方式,这就是计算机语言。
C、C++、Java、Go 和 Python 都属于计算机语言。它们的语法和工作方式各不相同,但目标一致:把我们的想法写成程序,再交给计算机执行。
C 语言有一个很鲜明的特点:它既提供函数、变量、循环等方便人理解的语法,又允许我们接触内存、地址和字节这些更靠近机器的东西。
这份控制力很有吸引力,同时也意味着程序员要更仔细。数组越界、野指针、忘记字符串结尾等问题,C 语言不会每次都替我们兜底。
学习 C 语言,不只是记住语法,更要慢慢习惯“这段代码在内存里做了什么”。
二、C 语言从哪里来?
C 语言与 Unix 的关系很深,而且它并不是某一天突然冒出来的。我们顺着时间线,看看它是怎样一步步长出来的。
- 1969 年:贝尔实验室的 Ken Thompson 和 Dennis Ritchie 参与开发 Unix。为了让系统更容易移植,Ken Thompson 在 BCPL 的基础上设计了 B 语言。
- 1972 年:Dennis Ritchie 继续改进 B 语言,设计出一门新的语言。它既然接在 B 后面,名字也就顺理成章地变成了 C。
- 1973 年:Unix 内核的大部分代码开始使用 C 语言重写。C 语言既能处理接近硬件的工作,又比纯汇编更容易阅读和移植,很快跟着 Unix 走向更多平台。
- 1988 年前后:ANSI 推动 C 语言标准化,C 语言开始有更稳定、统一的规范。后来又形成 C89/C90、C99、C11、C17 等版本。
几十年过去,C 语言仍然活跃在操作系统、嵌入式、编译器和各种基础软件中。它当然不是最年轻的语言,却一直没有真正离开开发者的工具箱。
这段历史不用死记年份。先记住 Dennis Ritchie、Unix 和标准化这三个关键词就够啦。接下来看看更实际的问题:我们写好的代码,到底怎么跑起来?
三、编译器与 Visual Studio 2022
3.1 .c 文件为什么不能直接运行?
先在记事本里打开一份 .c 文件,你会发现它就是普通文本。处理器并不认识 printf、int 或 return,它最终执行的是机器指令。
于是,中间需要一个“翻译员”。这个角色就是编译工具链。
注:
- 每个源文件(.c)单独经过编译器处理生成对应的目标文件(.obj为后缀的文件)
- 多个目标文件和库文件经过链接器处理生成对应的可执行程序(.exe文件)
这就是,在Windows电脑上C语言程序生成的exe可执行文件
先把流程压成两步来看:
- 每个 .c 源文件分别经过预处理、编译和汇编,生成目标文件;
- 链接器把目标文件与需要的库组合起来,生成最终的可执行程序。
Windows 上使用 MSVC 时,经常能看到 .obj 和 .exe;使用 GCC 或 Clang 时,目标文件常见后缀是 .o。后缀虽然不同,所处的阶段是一样的。
源文件是我们写的文本,目标文件已经含有机器代码,链接完成后才得到最终程序。
3.2 编译器和 IDE,别再傻傻分不清啦
初学时很容易把 Visual Studio 直接叫作“编译器”。平时这么说大家能听懂,不过严格一点拆开看,它其实是一个 IDE(集成开发环境)。
| 工具 | 负责什么 | 常见例子 |
|---|
| 编辑器 | 编写和修改代码 | VS Code、Vim |
| 编译器 | 把源代码翻译成目标代码 | MSVC、GCC、Clang |
| 调试器 | 断点、单步执行、观察变量 | GDB、LLDB、VS Debugger |
| IDE | 把编辑、构建、调试等能力装进一个界面 | Visual Studio、CLion、Xcode |
Visual Studio 2022 通常配合 MSVC 工具链工作。我们点击“本地 Windows 调试器”或按下 Ctrl + F5 时,IDE 已经在背后帮忙组织好了编译、链接和运行。
对于刚开始在 Windows 上学习 C 语言的同学,VS 2022 的优点是安装后配置较少、调试功能完整;缺点也很直接:体积比较大,界面里的按钮第一次看会有点多。
不用被它吓到。第一阶段我们会新建项目、添加 .c 文件、运行代码,就已经够用了。
四、VS 项目、源文件和头文件
在 VS 2022 中写代码时,我们通常先创建一个项目,再向项目中添加源文件和头文件。
- 以 .c 结尾的文件叫作源文件,主要放函数定义和具体实现;
- 以 .h 结尾的文件叫作头文件,通常放函数声明、类型声明和宏定义。
举个很小的例子。如果我们把加法函数单独放进 add.c,项目可以这样组织:
- 项目
- ├─ main.c
- ├─ add.c
- └─ add.h
复制代码
add.h 负责告诉其他源文件:“这里有一个 Add 函数可以使用。”
- #ifndef ADD_H
- #define ADD_H
- int Add(int x, int y);
- #endif
复制代码
add.c 再给出它真正的实现:
- #include "add.h"
- int Add(int x, int y)
- {
- return x + y;
- }
复制代码
最后在 main.c 中包含头文件并调用函数:
- #include <stdio.h>
- #include "add.h"
- int main(void)
- {
- printf("%d\n", Add(2, 3));
- return 0;
- }
复制代码
这里先不用死磕头文件保护和链接规则。只要记住一个朴素的分工:头文件偏向“告诉别人有什么”,源文件负责“把它真正做出来”。
五、第一个 C 语言程序
现在回到开头那段代码,这次不只是把它敲出来,我们逐行看看它在做什么。
- #include <stdio.h>
- int main(void)
- {
- printf("hello C\n");
- return 0;
- }
复制代码
5.1 #include
这一行包含标准输入输出头文件。后面调用的 printf 函数,就声明在 stdio.h 中。
5.2 int main(void)
这里定义了程序入口 main。前面的 int 表示它执行结束后会返回一个整数,括号里的 void 表示这个版本的 main 不接收参数。
5.3 一对大括号 {}
大括号围出的区域叫作代码块。main 要执行的语句就放在这里。
5.4 printf("hello C\n");
调用 printf 输出一段文字,其中 \n 表示换行。语句末尾的分号别漏掉,它相当于告诉编译器:“这一句到这里结束。”
5.5 return 0;
main 返回 0,通常表示程序正常结束。
第一次写代码时,下面这些小错误特别常见:
- 把 main 写成 mian;
- 漏掉 main 后面的括号;
- 混入中文分号 ; 或中文括号;
- 忘记在语句末尾写英文分号 ;。
编译报错并不可怕。看清第一条错误提示,再回到对应行检查,往往比盯着整个屏幕有效得多。
六、为什么程序从 main 开始?
一个项目可以有很多 .c 文件,也可以写很多函数,但作为一个普通的可执行程序,它需要一个明确的入口。这个入口就是 main。
- int main(void)
- {
- return 0;
- }
复制代码
可以先记住三点:
- main 是程序入口;
- 一个可执行程序只能有一个 main 定义;
- main 结束时返回的整数会交给运行环境,0 通常表示成功。
如果一个项目里完全没有 main,链接器会找不到入口;如果写了两个 main,链接器又会不知道该选哪一个。
这就像一栋楼可以有很多房间,但总得先告诉访客正门在哪里。门可以通向更多函数,正门本身只能确定一个。
七、printf 与库函数
7.1 printf 不只能打印一句话
printf 是 C 标准库提供的输出函数。除了直接打印字符串,它还可以借助格式说明符输出不同类型的数据:
- #include <stdio.h>
- int main(void)
- {
- int age = 18;
- char grade = 'A';
- double price = 3.14;
- printf("age = %d\n", age);
- printf("grade = %c\n", grade);
- printf("price = %.2f\n", price);
- return 0;
- }
复制代码
| 格式 | 常见用途 |
|---|
| %d | 输出 int |
| %c | 输出单个字符 |
| %s | 输出字符串 |
| %f | 输出浮点数 |
格式说明符必须和后面的参数匹配。把整数交给 %s,编译器可能会发出警告,运行时也可能直接出问题。
⚠️ 容易混淆:printf 输出 double 使用 %f;scanf 读取 double 才使用 %lf。两个函数长得像,格式要求却不能直接照搬。
7.2 什么是库函数?
像输出文字、计算字符串长度这些常见工作,没必要让每个程序员都从头实现。C 标准规定了一组可供程序使用的函数接口,不同工具链按照标准提供实现,它们共同组成了 C 标准库。
库函数通常按功能声明在不同头文件中:
- printf 在 中声明;
- strlen 在 中声明;
- sqrt 在 中声明。
头文件不是把整个函数实现复制到当前位置,它首先让编译器知道函数叫什么、需要什么参数、会返回什么类型。真正的实现会在后面的链接阶段接进程序。
八、关键字:这些名字已经被 C 语言预订了
int、if、return 看起来都是单词,但在 C 语言里,它们已经被赋予了特殊含义,这些保留名称叫作关键字。
因此,下面这种写法不行:
- int return = 10; // 错误:return 是关键字
复制代码
经典的 C90 标准有 32 个关键字:
- auto break case char const continue
- default do double else enum extern
- float for goto if int long
- register return short signed sizeof static
- struct switch typedef union unsigned void
- volatile while
复制代码
后来的 C99、C11 和 C23 又增加了新关键字,所以“C 语言只有 32 个关键字”只适用于经典的 C90 范围。
现在不用背整张表。写代码时遇到 int、char、if、for、return,知道它们由语言保留;起变量名时别和关键字撞车,就够用了。
C语言关键字的全部介绍可以从这里查看详细介绍,我们后面也会讲的
九、字符与 ASCII 编码
9.1 字符在内存里也得变成数字
C 语言使用单引号表示字符:
- char ch1 = 'A';
- char ch2 = '@';
- char ch3 = '7';
复制代码
注意,'7' 表示字符,7 表示整数,它们不是同一个值。
计算机最终存储的是二进制数据,所以字符和数字之间需要一套约定。常见运行环境对基础字符采用 ASCII 编码,例如:
- 'A' 到 'Z' 对应 65 到 90;
- 'a' 到 'z' 对应 97 到 122;
- '0' 到 '9' 对应 48 到 57;
- 换行符 \n 的编码值是 10。
我们不需要把整张表背下来。掌握字母和数字的连续范围,其他值用到时再查。
下面是ASCll码表,便于大家了解,参考ASCll码表
9.2 同一个值,可以按数字看,也可以按字符看
先猜一下下面两行会输出什么:
- printf("%c\n", 'Q');
- printf("%c\n", 81);
复制代码
结果都是 Q。%c 会把对应的整数值按字符解释,而在 ASCII 中,Q 的编码正好是 81。
如果想看看可打印的 ASCII 字符,可以写一个小循环:
- #include <stdio.h>
- int main(void)
- {
- for (int code = 32; code <= 126; ++code)
- {
- printf("%c ", code);
- if (code % 16 == 15)
- {
- printf("\n");
- }
- }
- return 0;
- }
复制代码
这里把上限写成 126,因为 ASCII 中的 127 是 DEL 控制字符,并不是普通的可打印字符。
字符可以参与整数运算,因为它在内存中保存的本来就是一个整数编码。
十、字符串与藏在末尾的 \0
10.1 单引号和双引号差在哪?
单引号括起来的是一个字符:
使用双引号括起来的一串字符就被称为字符串:
问题来了:"abc" 看起来只有 3 个字符,word 数组到底有多大?
答案是 4。
C 字符串会在末尾自动保存一个空字符 \0,它的数值是 0,用来标记字符串结束。因此 word 的实际内容是:
可以用代码验证:
- #include <stdio.h>
- #include <string.h>
- int main(void)
- {
- char word[] = "abc";
- printf("strlen = %zu\n", strlen(word));
- printf("sizeof = %zu\n", sizeof(word));
- return 0;
- }
复制代码
输出为:
strlen 数到 \0 就停,只统计前面的有效字符;sizeof 看的是整个数组实际占用的字节数,所以会把 \0 算进去。
10.2 少了 \0,%s 就不知道该停在哪
看这里的两种初始化:
- char arr1[] = {'a', 'b', 'c'};
- char arr2[] = "abc";
复制代码
arr1 只有 3 个字符,没有结尾标记;arr2 则由字符串字面量初始化,编译器会自动补上 \0。
如果把 arr1 直接交给 %s:
- printf("%s\n", arr1); // 错误示例:不要这样使用
复制代码
printf 会继续向后读取,试图寻找并不存在于数组范围内的 \0。截图里出现了随机字符,但那只是未定义行为的一种表现;它也可能看似正常,甚至可能让程序崩溃。
正确做法是给数组留出并写入结束标记:
- char arr1[] = {'a', 'b', 'c', '\0'};
- char arr2[] = "abc";
复制代码
还有一个很直观的例子:
- printf("%s\n", "abc\0def");
复制代码
屏幕上只会看到 abc,因为 %s 遇到中间的 \0 就停下来了。
\0 不是可有可无的装饰,它是 C 字符串能够被正确识别的结束标志。
十一、转义字符:反斜杠让字符换了一份工作
转义字符参考:转义字符参考
先看两段非常像的文本:
- printf("abcndef");
- printf("abc\ndef");
复制代码
第一行中的 n 只是普通字母,输出 abcndef;第二行里的 \n 是一个整体,表示换行,因此 def 会跑到下一行。
反斜杠 \ 会和后面的字符组合成转义序列。常见的几种如下:
| 转义序列 | 含义 | 示例用途 |
|---|
| \n | 换行 | 打印下一行 |
| \t | 水平制表 | 对齐文本 |
| \r | 回到当前行开头 | 控制终端输出位置 |
| \' | 单引号 | 在字符常量中表示 ' |
| \" | 双引号 | 在字符串中打印 " |
| \\ | 反斜杠 | 输出 Windows 路径 |
| \0 | 数值为 0 的空字符 | 标记字符串结束 |
例如,路径里的反斜杠不能直接随便写:
- printf("C:\\test\\code\\main.c\n");
复制代码
如果想在屏幕上打印双引号:
- printf("他说:"hello C"\n");
复制代码
字符还可以使用八进制或十六进制转义表示:
- printf("%c\n", '\130'); // 八进制 130,即十进制 88,对应 'X'
- printf("%c\n", '\x30'); // 十六进制 30,即十进制 48,对应 '0'
复制代码
这些写法现在不必全部背住。看到反斜杠时先别把后面的字符拆开读,问一句“它们是不是一个转义序列?”,很多字符串问题就能躲过去。
十二、语句与语句分类
C 程序由一条条语句组成。初学阶段可以先认识下面几类。
12.1 空语句
单独一个分号就是空语句:
它什么也不做,但语法上确实是一条语句。以后看到这样的代码要格外小心:
- while (condition); // 循环体是空语句
- {
- // 这个代码块已经不属于 while 的循环体
- }
复制代码
那个不起眼的分号,可能会让循环一直空转。💣
12.2 表达式语句
表达式后面加上分号,就构成表达式语句:
- b = a + 5;
- count++;
- Add(2, 3);
复制代码
这里顺便修正一个容易混淆的说法:函数调用后加分号,常被教学资料单独叫作“函数调用语句”,便于理解;从 C 语法分类看,它仍然属于表达式语句。
12.3 复合语句
一对大括号中的若干声明和语句组成一个代码块,也叫复合语句:
- {
- int value = 10;
- printf("%d\n", value);
- }
复制代码
函数体、循环体和分支中的大括号,都是我们经常遇到的复合语句。
12.4 控制语句
程序如果只能从上往下一路执行,很快就不够用了。控制语句负责改变执行流程:
- 分支:if、switch;
- 循环:while、do while、for;
- 跳转:break、continue、goto、return。
这一讲先认识名字,后面学习分支和循环时再逐个拆开。
十三、注释:写给人看的代码说明
注释会被编译器忽略,不参与程序的实际运行。它主要写给未来的自己、队友,以及接手代码的人看。
不过注释也不是越多越好:
这句注释只是把代码翻译了一遍,没有提供新信息。更有价值的注释应该解释“为什么”:
- // 先跳过下标 0,因为它保存的是哨兵节点
- for (int i = 1; i < size; ++i)
- {
- // ...
- }
复制代码
13.1 /* ... */ 块注释
块注释可以跨行:
- /* 这是一段注释 */
- /*
- 这是一段
- 跨行注释
- */
复制代码
它也能写在一行代码中间:
- int open_file(const char *path /* 文件路径 */, int mode);
复制代码
使用时注意两件事:
- /* 开始后必须有对应的 */;
- 块注释不能嵌套,遇到第一个 */ 就结束。
13.2 // 单行注释
从 // 开始到当前行末尾都属于注释:
- // 这是一行注释
- int value = 1; // 也可以放在语句后面
复制代码
这种写法从 C99 开始进入 C 标准,日常代码里非常常见。
注释符号如果出现在双引号中,就只是字符串的一部分:
- printf("// hello /* world */\n");
复制代码
这行代码会把斜杠和星号都打印出来,它们不会突然把字符串“注释掉”。
13.3 注释被去掉后,不一定直接拼接
在预处理阶段,注释会被替换为空白。因此:
处理后相当于:
而不是 minValue。这个细节平时不常用,但能帮助我们理解:注释不是简单把字符粗暴删除,编译器还要避免它两边的记号意外粘在一起。
总结与思考
好啦,C 语言第一讲到这里就走完了!🎉
我们沿着顺序,认识了 C 语言的来历和开发工具,也亲手拆开了第一个程序。现在再看到那几行代码,里面已经不只是一堆陌生符号了:
- .c 源文件要经过翻译和链接,才能变成可执行程序;
- 项目可以有多个源文件和头文件,但可执行程序只有一个 main 入口;
- printf 等库函数需要正确的头文件和格式说明符;
- 字符在内存中保存为编码值,字符串末尾则藏着决定边界的 \0;
- 转义字符、语句和注释虽然基础,却埋着不少很容易踩中的小坑。
我觉得这一讲最值得记住的,不是 32 个关键字,也不是整张 ASCII 表,而是两个习惯:看到代码时多问一句“它怎样变成机器能执行的程序”,处理字符串时永远别忘了检查 \0。
第一站拿下啦。下一讲,我们就可以正式进入变量、数据类型和运算符,开始让程序处理真正的数据了!🚀