【C语言常见概念】从第一行代码到字符串结束标志:一次打通编译、main、ASCII、转义字符与注释

🔥 星光编译者 · 个人主页

📚 学习专栏: 《C/C++ 成长笔记》 · 《Linux 实践手册》 · 《数据结构与算法》

🌄 向云端飞扬,编译属于自己的代码星河。


☕ 写在开篇

  你好,这里是 星光编译者

  这里记录我在 C/C++、Linux、数据结构与算法 学习中遇到的真实问题、亲手验证过的代码,以及那些容易被忽略的实现细节。

  比起简单罗列结论,我更愿意从问题出发,把一个知识点的来由讲清楚,把"为什么会这样"和"应该怎样解决"说明白,让每一次踩坑都沉淀成可以复用的经验。

  如果这篇记录能帮你少绕一点路,或让某个模糊的地方忽然变得清晰,那么这次分享便有了意义。愿我们在一次次阅读、编译与调试中稳步向前,慢慢搭起属于自己的技术世界。


🔥 本文定位 :这是 C 语言系列的第一篇。我们不急着堆语法,而是先建立一张完整地图:C 程序写在哪里、怎样从源代码变成可执行文件、为什么从 main 开始、printf 为什么要包含头文件、字符和字符串又怎样落到内存中。

💡 学习目标 :理解 C 语言与编译型语言的基本定位;区分编辑器、编译器、链接器与 IDE;能独立建立 .c/.h 文件并运行第一个程序;掌握 mainprintf、关键字、ASCII、字符串结尾的 \0、常用转义字符、五类语句和两种注释。

📌 阅读说明 :示例使用标准 C,命令行以 GCC/Clang 的习惯写法演示,在 Visual Studio 2022 中也可以直接创建"空项目"并添加 .c 文件。不同编译器的界面和命令不同,但"预处理/编译/汇编/链接"的核心链路相同。


文章目录


一、先建立全景图:这一讲到底学什么

第一次接触 C 语言时,我们很容易把注意力全部放在分号、括号和报错上。其实更重要的是先回答四个问题:

  1. 代码是什么:它是程序员写给编译工具阅读的文本;
  2. 代码放在哪里 :通常放在 .c 源文件和 .h 头文件中;
  3. 代码怎样运行:源文件经过编译,目标文件再经过链接,最终形成可执行程序;
  4. 程序怎样表达数据和动作:字符、字符串、关键字、语句和函数共同构成 C 程序。

这 13 个知识点可以归入四个层次:

  • 语言与工具:C 是什么、为什么使用它、编译器和 IDE 是什么;
  • 程序的形成:源文件、头文件、编译、链接与可执行文件;
  • 最小程序骨架mainprintf、库函数与关键字;
  • 基本表达规则:ASCII、字符串、转义字符、语句与注释。

先有地图,后面的变量、分支、循环、数组和函数才有落脚点。否则我们可能会写出能运行的代码,却说不清编译器究竟做了什么,也无法判断一个错误发生在编辑、编译、链接还是运行阶段。


二、C语言是什么:人与计算机之间的精确表达

人与人交流使用自然语言,计算机的处理器最终执行的却是机器指令。直接手写机器指令既低效又容易出错,于是人们设计了各种程序设计语言,用更容易理解和维护的形式描述计算过程。

C 语言就是其中一种。它既提供函数、分支、循环、结构体等高级抽象,又能通过指针、位运算和明确的数据布局接近底层资源,因此常用于:

  • 操作系统、编译器和数据库的核心部分;
  • 嵌入式设备、单片机与驱动程序;
  • 对性能、内存布局或二进制接口有严格要求的库;
  • 学习数据结构、操作系统与计算机组成时的实验代码。

可以把 C 程序理解成一份精确的指令说明书。程序员用 C 语法描述"要做什么",编译器负责检查并翻译,处理器最终执行生成的机器指令。

不过,"接近底层"不等于"C 代码就是机器指令"。C 仍然是一门高级语言,同一份符合标准的源代码可以由不同平台上的编译器处理;真正生成的机器指令会随着处理器架构、操作系统、编译器和优化选项而变化。

2.1 C语言不替你做什么

C 给程序员较大的控制权,也意味着很多责任需要程序员承担:

  • 数组越界通常不会自动拦截;
  • 动态申请的内存需要明确释放;
  • 字符串依赖结束标志,接口使用不当可能越界读取;
  • 某些表达式的结果与具体实现有关;
  • 未定义行为可能让程序出现任何结果,而不是稳定报错。

因此,学习 C 不只是记住语法,还要逐渐建立边界意识:对象有多大、生命周期到哪里、访问是否合法、编译器能否看到正确声明。


三、C语言为何长盛不衰:历史、优势与边界

C 语言诞生于 20 世纪 70 年代早期,与 Unix 系统的发展密切相关。它继承了更早期语言的思想,又在可移植性、表达能力和执行效率之间取得了非常成功的平衡。Unix 大量使用 C 重写后,操作系统不再只能紧紧绑定某一种机器,C 也随之传播到更多平台。

它长期活跃,不是因为它"在所有场景都最好",而是因为它在若干关键维度上仍然很有竞争力:

维度 C语言的特点 需要付出的代价
性能 抽象成本可控,容易生成高效机器代码 需要理解数据布局与算法成本
可移植性 有语言标准和大量成熟编译器 仍要处理平台差异与实现定义行为
系统能力 可以直接操作内存、位和硬件接口 边界检查与资源管理责任更大
生态 大量操作系统接口和基础库以 C ABI 暴露 历史接口中存在不少易误用设计
学习价值 能帮助理解程序从源码到机器执行的过程 初期报错可能较生硬,调试要求更高

C、C++、Java、Go、Python 并不是简单的"新语言淘汰旧语言"关系。它们面向的问题、运行时模型、抽象层次和生态不同。工程选择看约束,而不是只看语言排行榜。

3.1 标准版本与编译器扩展

C 语言由标准持续演进。常见说法里的"32 个关键字"通常指早期 ANSI C/C90 的核心关键字集合;C99、C11、C17、C23 等版本继续增加或调整了语言与库能力。

学习时要区分三件事:

  • 标准 C:规范明确规定的语法与行为;
  • 实现定义:实现必须选择一种行为并记录下来,例如某些整数类型的范围;
  • 编译器扩展:某个工具额外支持,但换编译器可能失效的写法。

初学阶段优先写标准、清晰、可移植的代码;需要扩展时,再明确它解决了什么平台问题。


四、工具别混淆:编辑器、编译器、链接器与IDE

"我用 VS 写 C"这句话在日常交流中没有问题,但从概念上看,Visual Studio 并不是 C 语言,也不只是编译器。开发链路中有多个角色:

工具 主要职责 常见例子
文本编辑器 编辑 .c.h 等文本文件 VS Code、Vim、记事本
编译器工具链 分析并翻译源代码,生成目标文件 MSVC、GCC、Clang
链接器 合并目标文件和库,解析符号引用 link.exeldlld
调试器 设置断点、单步执行、观察变量 Visual Studio Debugger、GDB、LLDB
IDE 把编辑、构建、调试和项目管理整合起来 Visual Studio、CLion、Xcode

Visual Studio 2022 是 IDE,Windows 上常配合 MSVC 工具链使用;Xcode 常配合 Clang;Code::Blocks、Dev-C++、CLion 则可以根据配置调用相应工具链。

4.1 为什么推荐初学者先用成熟IDE

IDE 可以帮助我们:

  • 创建项目并组织多个文件;
  • 高亮语法、提示声明和定位错误;
  • 一键调用构建工具;
  • 用断点、监视窗口和调用栈观察程序;
  • 区分编译错误、链接错误与运行时异常。

它降低了第一次运行程序的门槛。但不要把"一键运行"当成魔法。知道按钮背后发生了哪些步骤,才能在项目变大后解决真正的问题。


五、源代码怎样变成程序:编译与链接

C 源文件本质上是文本,处理器不能直接执行。一个典型程序从源码到可执行文件,要经过下面的链路:

text 复制代码
.c 源文件
   ↓ 预处理:展开 #include、处理宏和条件编译
预处理结果
   ↓ 编译:语法/语义分析并生成汇编表示
汇编代码
   ↓ 汇编:生成机器代码片段
.obj / .o 目标文件
   ↓ 链接:合并多个目标文件和库,解析符号
.exe / 可执行文件

日常语境里常把前三步统称为"编译"。理解细分阶段,有助于判断错误来源。

5.1 为什么每个源文件通常单独编译

假设项目包含:

text 复制代码
main.c
add.c
print.c

编译器通常分别生成:

text 复制代码
main.obj / main.o
add.obj  / add.o
print.obj / print.o

然后链接器把这些目标文件与需要的库合并,生成最终程序。这种独立编译让大型项目只重新编译发生变化的部分,而不必每次把全部源码从头处理。

5.2 三类常见错误怎样区分

编译错误

源文件本身不符合语法或语义规则,例如:

c 复制代码
int main(void)
{
    printf("hello\n")  /* 漏掉分号 */
    return 0;
}

编译器知道具体文件和行号,通常能直接定位。

链接错误

每个源文件都能分别编译,但目标文件合并时找不到定义或发现重复定义。例如只声明了 Add,却没有把包含其定义的 add.c 加入构建。

text 复制代码
undefined reference to `Add`
unresolved external symbol Add
运行时错误

程序已经生成并开始执行,但出现非法内存访问、除零、断言失败或逻辑错误。此时要借助调试器、日志和测试继续定位。

5.3 命令行看清构建过程

单文件程序可以一步完成:

bash 复制代码
gcc hello.c -o hello

多个文件也可以分开观察:

bash 复制代码
gcc -c main.c -o main.o
gcc -c add.c -o add.o
gcc main.o add.o -o app

第一、二条只编译不链接;第三条执行链接。Windows/MSVC 的文件后缀与命令不同,但目标仍然是"源文件 → 目标文件 → 可执行文件"。


六、项目、源文件和头文件分别是什么

6.1 项目是构建与组织单位

项目不是 C 语言语法的一部分,而是开发工具用来组织文件、编译选项、依赖和输出的方式。不同 IDE 的项目文件格式不同,.c.h 文件本身仍是普通文本。

6.2 .c源文件放实现

源文件通常放函数定义和对象定义:

c 复制代码
/* add.c */
int Add(int left, int right)
{
    return left + right;
}

每个 .c 文件形成一个独立的翻译单元。它可以包含头文件,但不应该通过 #include "add.c" 的方式拼接实现文件。

6.3 .h头文件放共享声明

头文件常放函数声明、类型定义、宏和需要被多个源文件共享的接口:

c 复制代码
/* add.h */
#ifndef ADD_H
#define ADD_H

int Add(int left, int right);

#endif

main.c 通过包含头文件知道怎样调用函数:

c 复制代码
#include <stdio.h>
#include "add.h"

int main(void)
{
    printf("%d\n", Add(2, 3));
    return 0;
}

头文件里的函数声明不是函数实现。编译器看到声明后可以检查调用形式;链接器随后还要找到 Add 的真正定义。

6.4 尖括号与双引号

常见约定是:

c 复制代码
#include <stdio.h>  /* 标准库或系统头文件 */
#include "add.h"    /* 项目自己的头文件 */

两种形式的精确搜索路径由实现和构建选项决定,但双引号形式通常会优先考虑当前项目附近的路径。


七、第一个C程序:逐行读懂Hello-C

先写一个最小但规范的程序:

c 复制代码
#include <stdio.h>

int main(void)
{
    printf("Hello, C!\n");
    return 0;
}

7.1 #include <stdio.h>

这是一条预处理指令。它让当前翻译单元获得标准输入输出接口的声明,其中就包括 printf

不要把它理解成"导入整个库的机器代码"。头文件主要给编译阶段提供声明;真正需要的库实现由工具链在链接阶段处理。

7.2 int main(void)

这里定义了程序入口函数 main

  • int 表示函数返回一个整数状态;
  • main 是约定的入口函数名;
  • (void) 明确表示这个版本不接收参数;
  • { ... } 是函数体,也是一条复合语句。

7.3 printf("Hello, C!\n");

这是一条函数调用语句。双引号中的内容是字符串字面量,\n 表示换行。语句末尾的分号用于结束这条语句。

7.4 return 0;

它把状态码 0 返回给宿主环境,通常表示程序正常结束。现代标准中,从 main 函数末尾自然到达右花括号也等价于返回 0,但初学阶段显式写出 return 0; 更有助于理解返回值。

7.5 VS2022中的基本操作

使用 Visual Studio 2022 时,可以创建空项目,然后:

  1. 在"源文件"下添加新建项;
  2. 文件名使用 .c 后缀,例如 hello.c
  3. 输入代码并保存;
  4. 使用"开始执行(不调试)"运行,常见快捷键为 Ctrl + F5
  5. 出错时先看"错误列表"和"输出"窗口,不要只看最后一行。

如果文件被命名为 .cpp,IDE 通常会按 C++ 方式处理。虽然很多简单代码在两种语言下都能通过,但 C 和 C++ 不是同一种语言,后缀会影响编译模式。


八、main函数:程序从哪里开始

一个普通的托管 C 程序从 main 函数进入。即使项目有许多 .c 文件,最终构建出的一个可执行程序通常也只能有一个 main 定义。

标准中常见的两种形式是:

c 复制代码
int main(void)
{
    return 0;
}

以及接收命令行参数的形式:

c 复制代码
int main(int argc, char *argv[])
{
    return 0;
}

初学阶段不需要立刻展开 argcargv,但要知道 void main() 不是标准 C 为普通托管程序规定的写法。

8.1 main"有且仅有一个"应怎样理解

  • 一个可执行目标需要明确的入口;
  • 多个源文件可以共同构成这个目标;
  • 每个源文件不需要各写一个 main
  • 如果同一目标里出现两个 main 定义,链接阶段通常会报告重复符号;
  • 如果一个都没有,而构建类型又要求可执行程序,链接阶段通常会报告找不到入口。

测试框架、操作系统内核、嵌入式裸机或某些特殊运行环境可能有不同入口约定,但这不改变普通入门程序使用标准 main 的规则。

8.2 初学者最常见的入口错误

  • main 拼成 mian
  • 漏掉函数名后的圆括号;
  • 使用中文全角括号、引号或分号;
  • 把函数体花括号写坏;
  • 在同一项目中保留多个练习文件,而每个文件都有 main
  • 新建了 .cpp 文件,却误以为自己始终在编译 C。

九、printf与标准库:为什么使用函数前要包含头文件

printf 是标准库中的格式化输出函数。最简单的用法是打印固定文本:

c 复制代码
printf("Hello, C!\n");

也可以把后续参数按格式写入输出:

c 复制代码
int age = 18;
char grade = 'A';
double score = 95.5;

printf("age = %d\n", age);
printf("grade = %c\n", grade);
printf("score = %.1f\n", score);

常见占位符:

占位符 对应输出 示例
%d 有符号十进制 int printf("%d", 42);
%u 无符号十进制 unsigned int printf("%u", 42u);
%c 单个字符 printf("%c", 'A');
%s 以 NUL 结尾的字符串 printf("%s", "C");
%f 浮点格式,实参按可变参数规则传入 double printf("%.2f", 3.14);
%p 指针值,参数应转换为 void * printf("%p", (void *)ptr);

这里有一个高频易错点:对 printf 来说,double 使用 %f;很多实现也接受 %lf 并按相同方式处理。对 scanf 来说二者却不同:%f 对应 float *%lf 对应 double *。不要把输入函数与输出函数的格式规则混在一起。

9.1 格式与实参不匹配不是"小问题"

printf 接收可变数量的参数,编译器不能总在运行时帮它恢复真实类型。格式字符串就是读取后续参数的说明书。如果说明书写错,行为可能未定义。

c 复制代码
double value = 3.14;
printf("%d\n", value); /* 错误:格式与实参类型不匹配 */

应开启编译器警告并认真处理:

bash 复制代码
gcc -std=c17 -Wall -Wextra -Wpedantic hello.c -o hello

9.2 什么是库函数

许多常见操作不必由每个程序员重复实现。C 标准规定了一组库接口,不同平台的实现负责提供相应功能。例如:

  • <stdio.h>:输入输出;
  • <string.h>:字符串和内存块操作;
  • <stdlib.h>:内存分配、转换、程序控制;
  • <math.h>:常见数学函数;
  • <ctype.h>:字符分类和转换。

使用库函数时包含正确头文件,编译器才能看到准确声明并检查调用。


十、关键字与标识符:哪些名字不能自己使用

关键字是语言保留的词,编译器赋予它们固定语法意义。例如:

text 复制代码
int  if  else  for  while  return  struct  typedef  const  sizeof

程序员不能把关键字重新用作变量名、函数名或类型名:

c 复制代码
int return = 0; /* 错误:return 是关键字 */

我们自己创建的名字称为标识符:

c 复制代码
int student_count = 42;
double average_score = 88.5;

初学时可遵循这些命名原则:

  • 使用字母、数字和下划线,不能以数字开头;
  • C 区分大小写,countCount 是两个名字;
  • 名字表达用途,不要长期使用 a1x2 这类无语义命名;
  • 不要创建以下划线开头且可能落入实现保留范围的名字;
  • 不要试图"发明新关键字",需要抽象时使用函数、类型、宏等语言机制。

10.1 为什么不同资料中的关键字数量不同

资料可能针对不同 C 标准。早期入门表格常列出 32 个核心关键字,后续标准增加了 _Boolinlinerestrict_Atomic 等能力,较新的标准还在继续调整。

因此,学习关键字的正确方式不是只背一个永远不变的数字,而是:

  1. 掌握当前课程常用关键字的语义;
  2. 知道项目采用哪个语言标准;
  3. 遇到陌生词时查阅对应标准或可靠参考资料;
  4. 让编译器以明确的标准模式和警告级别工作。

十一、字符与ASCII:字符为什么可以参与运算

C 中的字符常量使用单引号:

c 复制代码
'A'
'q'
'0'
'@'

计算机内存最终保存的是数值。字符与数值之间需要一套共同约定,ASCII 就是最基础、最常见的字符编码之一。

11.1 建议记住的ASCII区间

字符 十进制范围 规律
'0' ~ '9' 48 ~ 57 连续递增
'A' ~ 'Z' 65 ~ 90 连续递增
'a' ~ 'z' 97 ~ 122 连续递增
换行 \n 10 控制字符
NUL \0 0 字符串终止标志常用值

在 ASCII 中,对应大小写字母的编码相差 32,但工程代码不应为了"聪明"而到处硬编码 32。做字符分类和大小写转换时,优先使用 <ctype.h> 中的接口,并按其参数要求传值。

11.2 字符与整数可以互相观察

c 复制代码
#include <stdio.h>

int main(void)
{
    printf("%c\n", 'Q');
    printf("%d\n", 'Q');
    printf("%c\n", 81);
    return 0;
}

在 ASCII 环境中,'Q' 对应 81,因此三行分别输出字符、编码值和由编码解释出的字符。

不过,严格地说,C 标准保证基本执行字符集及若干相对顺序关系,不要求所有实现都使用 ASCII。今天常见桌面和服务器环境基本使用 ASCII 兼容编码,但写可移植代码时,'0' 比魔法数字 48 更清晰。

11.3 可打印范围不要写到127

标准 7 位 ASCII 的编码范围是 0~127:

  • 0~31 是控制字符;
  • 32~126 通常是可打印字符;
  • 127 是 DEL 控制字符,不是普通可打印字符。

因此演示可打印 ASCII 时,更严谨的循环是:

c 复制代码
#include <stdio.h>

int main(void)
{
    for (int code = 32; code <= 126; ++code)
    {
        printf("%c%c", code, code % 16 == 15 ? '\n' : ' ');
    }
    return 0;
}

11.4 字符、字节与中文不是同一个问题

ASCII 只覆盖很小的字符集合。中文通常使用 UTF-8、GBK 等编码表示,一个显示字符可能对应多个字节。char 的大小定义为一个字节,但"一项 char 就等于一个人类可见字符"只在有限场景成立。

初学阶段先掌握 ASCII 与单字节字符串规则,后续处理中文文本时再进入编码、代码点和多字节序列的世界。


十二、字符串与NUL:看不见的结束标志

C 语言没有一个内建的、自动记录长度的"字符串对象"类型。最常见的 C 字符串表示,是一段连续字符序列,并用值为 0 的字符作为结束标志。

c 复制代码
char text[] = "abc";

它在内存中实际占用 4 个 char

text 复制代码
索引       0      1      2      3
内容      'a'    'b'    'c'    '\0'
数值       97     98     99      0

\0 常读作"反斜杠零",它是数值为 0 的字符;NUL 是这个控制字符的常用名称。不要把它与空指针 NULL、字符 '0' 或"什么都没有"混为一谈。

12.1 字符数组不一定是字符串

比较下面两个数组:

c 复制代码
char arr1[] = {'a', 'b', 'c'};
char arr2[] = "abc";

arr1 只有 3 个元素,没有结束标志;arr2 有 4 个元素,最后一个是 \0

如果执行:

c 复制代码
printf("%s\n", arr1);

问题不只是"可能打印随机字符"。%s 会继续读取后续内存,直到偶然遇见一个 0 字节,但读取越过 arr1 的边界已经构成未定义行为。程序可能打印乱码、泄露相邻数据、崩溃,也可能看似正常;"这次没出错"不能证明代码正确。

正确写法可以显式补上终止符:

c 复制代码
char arr1[] = {'a', 'b', 'c', '\0'};

也可以在已知长度的情况下不用 %s,而采用带长度的处理方式。

12.2 strlen为什么不计算\0

c 复制代码
#include <stdio.h>
#include <string.h>

int main(void)
{
    char text[] = "abc";

    printf("strlen = %zu\n", strlen(text));
    printf("sizeof = %zu\n", sizeof text);
    return 0;
}

典型输出:

text 复制代码
strlen = 3
sizeof = 4
  • strlen 从起点逐个查找,返回第一个 \0 之前的字符数量;
  • sizeof text 得到整个数组占用的字节数,因此包括终止符;
  • 如果输入不是以 \0 结尾的有效字符串,调用 strlen 同样可能越界读取。

12.3 字符串中间出现\0会怎样

c 复制代码
printf("%s\n", "abc\0def");

字符串字面量中确实还存在 def 和最后的终止符,但 %s 在第一个 \0 处停止,所以只输出 abc。这说明"数组里有多少字节"和"C 字符串的逻辑长度"是两个概念。

12.4 三个最重要的字符串问题

每次处理 C 字符串前,都先问:

  1. 缓冲区容量是多少?
  2. 当前有效内容长度是多少?
  3. 在需要把它当字符串使用时,是否保证存在可访问的 \0

这三个问题贯穿后面的数组、指针、动态内存和字符串函数。


十三、转义字符:反斜杠如何改变字符含义

在源代码中,某些字符难以直接书写,或本身承担引号、换行等特殊作用。转义序列让我们用反斜杠开头的形式表达它们。

c 复制代码
printf("abc\ndef");

这里的 \n 在源代码中由两个字符写成,但表示一个换行控制字符。

13.1 常用转义序列

转义序列 含义 常见场景
\n 换行 输出下一行
\t 水平制表 简单列对齐
\r 回车 光标回到本行开头
\b 退格 终端控制,效果依环境而异
\a 警报 终端可能响铃或闪烁
\\ 一个反斜杠 Windows 路径、转义序列展示
\" 双引号 字符串内部写引号
\' 单引号 字符常量内部写单引号
\0 数值为 0 的字符 C 字符串终止

示例:

c 复制代码
#include <stdio.h>

int main(void)
{
    printf("第一行\n第二行\n");
    printf("name\tscore\n");
    printf("路径:C:\\code\\demo.c\n");
    printf("他说:\"Hello, C!\"\n");
    printf("单引号字符:%c\n", '\'');
    return 0;
}

13.2 八进制与十六进制转义

字符还可以通过数值形式写出:

c 复制代码
printf("%c\n", '\130'); /* 八进制 130,常见 ASCII 环境中是 X */
printf("%c\n", '\x30'); /* 十六进制 30,常见 ASCII 环境中是 0 */

需要注意:

  • 八进制转义最多读取规定数量的八进制数字;
  • 十六进制转义会继续读取后续十六进制数字,边界容易被忽略;
  • 代码可读性通常比炫技重要,能直接写清楚的普通字符不必改成数字转义;
  • \0 可以看作八进制转义的一种简写,其值为 0。

13.3 源代码、内存与终端是三层

看到转义字符时要分清三层:

  1. 源代码层 :程序员写下 \n
  2. 字符值层:编译器把它解释成换行字符值;
  3. 设备表现层:终端收到字符后决定怎样移动光标。

因此,\a\b\r\v 等控制字符的可见效果可能因终端而异。语言规定字符值,不保证每种现代界面都以同样方式展示。


十四、C语言的五类语句

C 程序由声明、表达式和语句等结构组成。入门资料常把语句概括为五类:空语句、表达式语句、函数调用语句、复合语句与控制语句。

14.1 空语句

单独一个分号就是空语句:

c 复制代码
;

它表示这里需要一条语句,但不执行动作。空语句偶尔有合理用途,但更常见的问题是误写:

c 复制代码
if (score >= 60); /* 危险:if 控制的是空语句 */
{
    printf("pass\n"); /* 这个代码块会无条件执行 */
}

14.2 表达式语句

表达式后加分号形成表达式语句:

c 复制代码
sum = left + right;
++count;
value *= 2;

分号不是"每行末尾都要写"的装饰,而是特定语法结构的一部分。

14.3 函数调用语句

函数调用作为完整表达式使用时,可以形成表达式语句:

c 复制代码
printf("hello\n");
SaveResult(result);

从严格语法分类看,它也是表达式语句的一种;入门时单独列出,是为了突出函数调用这一常见动作。

14.4 复合语句

一对花括号包围的若干声明和语句构成复合语句,也常称代码块:

c 复制代码
{
    int result = 2 + 3;
    printf("%d\n", result);
}

函数体、循环体和分支体经常使用复合语句。块还会影响名字的作用域和对象的生命周期。

14.5 控制语句

控制语句改变默认的顺序执行流程,常分三组:

  • 分支:ifswitch
  • 循环:whiledo ... whilefor
  • 跳转:breakcontinuegotoreturn
c 复制代码
for (int i = 0; i < 3; ++i)
{
    if (i == 1)
    {
        continue;
    }
    printf("%d\n", i);
}

十五、注释:写给人看,也会影响词法边界

注释用于解释意图、约束和不容易从代码本身看出的原因。编译器不会把注释当作运行逻辑,但这不意味着注释可以随意书写。

15.1 两种注释形式

块注释可以跨行:

c 复制代码
/* 这是块注释 */

/*
 * 也可以写成多行
 */

行注释从 // 延续到行尾:

c 复制代码
int count = 0; // 记录当前元素数量

// 注释从 C99 起进入标准。在需要兼容非常老的 C90 工具链时要留意标准模式,现代项目通常可以直接使用。

15.2 块注释不能嵌套

下面的写法不是"外层包住内层":

c 复制代码
/*
printf("A\n");
/* 临时注释 */
printf("B\n");
*/

编译器遇到第一个 */ 就结束最早的块注释,后续内容会重新进入代码,从而造成语法错误。临时屏蔽大段代码时,条件编译通常更可控:

c 复制代码
#if 0
/* 这里即使含有普通注释,也更容易管理 */
printf("temporarily disabled\n");
#endif

15.3 引号里的注释符号只是普通字符

c 复制代码
printf("// hello /* world */\n");

字符串字面量中的 ///**/ 都不会开始或结束注释。编译器必须先识别字符串和字符常量的边界,不能只按肉眼看到的斜杠判断。

15.4 注释会保留词法分隔

在早期翻译阶段,每条注释会被替换为空白字符。因此:

c 复制代码
int min/* explanation */Value = 0;

不会变成标识符 minValue,而会形成彼此分开的记号,最终导致语法问题。注释虽然不进入可执行逻辑,却可能隔开原本相邻的记号。

15.5 好注释解释"为什么"

低价值注释只是重复代码:

c 复制代码
++count; // count 加一

更有价值的注释说明约束和原因:

c 复制代码
/* 跳过索引 0:该位置保留给协议头。 */
for (size_t i = 1; i < packet_size; ++i)
{
    /* ... */
}

注释应与代码同步更新。过期注释比没有注释更危险,因为它会给阅读者一个看似可信的错误解释。


十六、常见错误、面试题与练习

16.1 常见误区

误区一:Visual Studio就是编译器

不准确。Visual Studio 是 IDE,通常调用 MSVC 工具链完成编译和链接。区分角色有助于理解配置与错误来源。

误区二:.h文件里放代码,.c文件里写main就够了

过于粗糙。头文件主要承担接口共享,源文件承担实现。某些定义可以合理出现在头文件中,但需要理解链接规则和多重包含问题,不能机械搬运。

误区三:程序有几个.c文件,就应该有几个main

错误。一个可执行目标通常只有一个入口,其余源文件通过函数和对象共同组成程序。

误区四:能编译就说明函数调用正确

错误。缺少原型、格式字符串与实参不匹配、越界读取等问题可能只触发警告,甚至在某些配置下暂时沉默。警告应视为早期质量门槛。

误区五:字符'0'、整数0和'\0'相同

错误。'0' 是数字字符,在 ASCII 中值为 48;整数常量 0 的值为 0;'\0' 是值为 0 的字符常量,常用于终止字符串。

误区六:字符数组都能用%s打印

错误。%s 要求传入的地址指向一个可访问、以 NUL 结尾的字符序列。没有终止符会发生未定义行为。

误区七:strlen能知道数组容量

错误。strlen 只查找第一个 \0 并计算之前的长度,它不知道底层缓冲区容量。

误区八:ASCII 0~127全部可打印

错误。常见可打印区间是 32~126,127 是 DEL 控制字符。

误区九:反斜杠总会原样输出

错误。反斜杠可能开始转义序列。要输出一个反斜杠,字符串中通常写成 \\

误区十:注释完全不会影响源码结构

错误。注释会被当作空白处理,可以分隔记号;块注释还不能嵌套。它不产生运行动作,但仍参与源文件的词法处理。

16.2 高频问题

问题1:编译与链接的核心区别是什么?

编译主要把一个翻译单元变成目标代码并检查该单元中的语法与类型关系;链接把多个目标文件和库组合起来,解析跨文件符号引用,形成最终程序。

问题2:声明与定义有什么区别?

声明告诉编译器某个名字及其类型;定义还为函数提供函数体,或为对象提供实际实体。一个函数可以在多个翻译单元通过头文件被声明,但整个程序中应有符合规则的定义。

问题3:为什么printf需要stdio.h

头文件提供 printf 的函数声明,使编译器知道参数和返回类型并检查调用。函数实现通常来自标准库,在链接或加载阶段接入程序。

问题4:为什么一个项目只能有一个main?

普通可执行程序需要唯一入口。两个 main 会形成重复定义,一个都没有则缺少所需入口。

问题5:"abc"占几个字节?

作为普通窄字符字符串字面量,它包含 abc 和末尾 \0,用于初始化数组时通常得到 4 个 charstrlen("abc") 为 3,sizeof "abc" 为 4。

问题6:NULLNUL'0'分别是什么?

NULL 是空指针常量宏;NUL 通常指值为 0 的字符;'0' 是可见的数字字符。三者名字相似,语义完全不同。

问题7:函数调用语句为什么也能算表达式语句?

函数调用是表达式的一种;当它后面加分号并独立出现时,就构成表达式语句。入门分类把它单列,是为了帮助识别常见代码形态。

问题8:注释会进入可执行文件吗?

普通注释在翻译早期就被处理,不作为运行逻辑存在。不过注释内容仍可能出现在调试信息、源码归档或其他工具产物中,不应在注释里写密钥等敏感信息。

16.3 建议练习

练习一:观察每个构建阶段

使用编译器选项分别生成预处理结果、汇编文件、目标文件和可执行文件,记录每一步输入输出的后缀和用途。

练习二:制造链接错误

创建 main.cadd.hadd.c,先只编译和链接 main.c,观察未解析符号;再把 add.c 加入构建并修复。

练习三:比较.c.cpp

在 IDE 中分别建立同样内容的 .c.cpp 文件,观察项目属性、预定义宏或某些语法检查的差异,理解文件后缀如何选择语言模式。

练习四:检查格式警告

故意把 %ddouble%s 与整数错配,开启高等级警告,阅读编译器给出的期望类型与实际类型。

练习五:打印ASCII关键区间

分别打印数字、大写字母和小写字母的字符与数值,验证连续性;把上界设为 126,并观察制表、换行等控制字符的效果。

练习六:比较strlensizeof

对数组 char text[] = "hello"; 输出两者,再把数组作为函数参数传入,观察 sizeof 语义为什么发生变化。

练习七:使用调试器观察\0

在监视窗口查看 char arr1[] = {'a','b','c'};char arr2[] = "abc"; 的元素数量和内存值,不要对 arr1 调用 %s

练习八:设计转义字符输出

输出一张三列小表格,内容包含双引号、反斜杠和 Windows 风格路径,确保源代码与终端显示都正确。

练习九:复现空语句陷阱

iffor 条件后误加分号,单步调试观察控制流,再修复并总结"分号属于哪条语句"。

练习十:改写低价值注释

找一段充满"变量加一""调用函数"式注释的代码,把注释改成解释协议约束、边界条件和设计原因。

16.4 一份入门自检清单

  • 我能说清编辑器、编译器、链接器和 IDE 的区别;
  • 我知道 .c.h 文件通常各自承担什么职责;
  • 我能判断错误发生在编译、链接还是运行阶段;
  • 我使用标准形式的 main,并理解返回值含义;
  • 我在调用库函数前包含了正确头文件;
  • 我会让 printf 的格式与实参类型匹配;
  • 我不会把关键字当作自己的标识符;
  • 我知道 ASCII 中数字和英文字母的关键连续区间;
  • 我能区分 '0''\0'、0 和 NULL
  • 我知道字符数组不一定是 C 字符串;
  • 我不会对缺少 NUL 终止符的数组调用 %sstrlen
  • 我能正确书写换行、引号和反斜杠的转义;
  • 我能识别空语句、表达式语句、复合语句和控制语句;
  • 我知道块注释不能嵌套,字符串里的注释符号只是字符;
  • 我会开启编译器警告并处理警告,而不是只追求"运行成功"。

16.5 参考资料


总结

这一讲看似只是在解释"常见概念",实际上已经把一个 C 程序从文本到运行的完整骨架串了起来:

text 复制代码
程序员编写 .c / .h 文件
        ↓
预处理器处理包含、宏和条件编译
        ↓
编译器检查并翻译各个源文件
        ↓
汇编器生成 .obj / .o 目标文件
        ↓
链接器合并目标文件与库
        ↓
操作系统加载可执行程序
        ↓
从 main 进入,调用库函数并执行语句
        ↓
通过返回值把结束状态交回宿主环境

最值得记住的是下面十点:

  1. C 源代码是文本,必须经过工具链翻译和链接,处理器才能执行。
  2. IDE 是整合环境,编译器和链接器是构建链路中的具体工具。
  3. 多个 .c 文件分别编译成目标文件,再与库一起链接成程序。
  4. 头文件主要共享声明,源文件主要提供实现;声明能通过编译不代表链接时一定找到定义。
  5. 普通 C 程序从标准形式的 main 进入,一个可执行目标通常只有一个入口定义。
  6. 调用 printf 等库函数要包含正确头文件,格式字符串必须与实参类型匹配。
  7. 关键字属于语言,标识符属于程序员;标准版本不同,关键字集合也会演进。
  8. 字符以数值编码表示;ASCII 的常见可打印范围是 32~126。
  9. C 字符串依赖 \0 终止,没有终止符却按 %s 读取会触发未定义行为。
  10. 转义、语句和注释都是词法与语法的一部分,理解边界比机械背写法更重要。

第一行 Hello, C! 并不只是一句输出。它背后已经站着预处理器、编译器、链接器、标准库、操作系统与处理器。看见这条完整链路,才算真正迈进 C 语言的大门。

相关推荐
Navigator_Z1 小时前
LeetCode //C - 1250. Check If It Is a Good Array
c语言·算法·leetcode
bnmoel1 小时前
C++ 基础入门篇(三):内联函数与空指针
c++·内联函数·nullptr·低层
码匠许师傅1 小时前
【C++三方组件】TinyXML2:两个文件的极简 XML 解析器
xml·c++
free-elcmacom1 小时前
C++学习<1>程序分区
开发语言·c++
SuperByteMaster3 小时前
freertos 任务创建方式
c语言
susplus4 小时前
【传感器DS18B20】51单片机上实现温度采集
c语言·51单片机·ds18b20·温度采集
xxwxx__4 小时前
C++ STL set 与 map 全套详解:关联式容器、红黑树底层、代码坑点、刷题实战
数据结构·c++
Logic1015 小时前
C语言/数据结构位运算题解:异或XOR找出流水线上的“独特零件编号“——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质
山下梅子酒2255 小时前
洛谷-入门-B2043
c语言