C语言常见概念:从编译链接到字符串与注释

前言

C 语言的基础语法并不复杂,但初学阶段有很多概念非常容易混淆:

  • 编译器和 IDE 是一回事吗?
  • .c 文件如何变成可执行程序?
  • 一个工程为什么只能有一个 main 函数?
  • 字符 'a' 和字符串 "a" 有什么区别?
  • 字符串末尾为什么必须有 '\0'?
  • sizeof("abc") 为什么是 4,而 strlen("abc") 是 3?
  • printf 中的 %f 和 %lf 有什么区别?
  • 一个单独的分号为什么也是语句?

本文从 C 语言的基本定位出发,依次介绍开发环境、编译链接、源文件与头文件、第一个 C 程序、main 函数、标准库、关键字、ASCII、字符串、转义字符、语句和注释。

一、C 语言是什么

1.1 计算机语言

人与人交流可以使用汉语、英语等自然语言;人与计算机交流,则需要使用计算机语言。

程序员通过计算机语言描述:

  • 要处理的数据;
  • 要执行的操作;
  • 操作执行的顺序;
  • 不同条件下应该采取的行为。

C、C++、Java、Go 和 Python 都属于计算机语言,只是它们的设计目标、执行方式和使用场景不同。

1.2 C 语言的特点

C 语言具有以下典型特点:

  1. 语法相对精简;
  2. 执行效率高;
  3. 可以进行接近底层的内存操作;
  4. 具有良好的可移植性;
  5. 适合系统软件和性能敏感程序;
  6. 许多现代语言的语法都受到 C 语言影响。

C 语言常见于:

  • 操作系统;
  • 编译器;
  • 数据库;
  • 嵌入式设备;
  • 驱动程序;
  • 网络服务;
  • 高性能基础库。

1.3 C 语言与 Unix

C 语言最初与 Unix 系统的发展密切相关。它既能提供接近机器的控制能力,又比汇编语言更容易编写、维护和移植。

学习 C 语言不仅是在学习语法,也是在逐步理解:

  • 程序怎样被编译;
  • 数据怎样存入内存;
  • 函数怎样调用;
  • 源文件怎样协作;
  • 操作系统怎样运行程序。

二、编译器与集成开发环境

2.1 什么是编译器

C 语言是一门编译型语言。.c 文件本质上是文本,处理器不能直接执行这些源代码。

编译器及相关工具需要把源代码转换为机器能够执行的程序。

常见的 C/C++ 编译器包括:

  • MSVC;
  • GCC;
  • Clang。

2.2 什么是 IDE

IDE 是 Integrated Development Environment 的缩写,中文称为集成开发环境。

一个 IDE 通常集成:

  • 代码编辑器;
  • 编译工具;
  • 调试器;
  • 项目管理;
  • 代码补全;
  • 静态检查;
  • 图形化界面。

常见 IDE 包括 Visual Studio、CLion、Xcode、Code::Blocks 和 Dev-C++。

2.3 编译器和 IDE 的区别

对比项 编译器 IDE
主要作用 将源代码转换为目标代码 提供完整开发环境
是否负责编辑代码 通常不负责 负责
是否包含调试功能 不一定 通常包含
典型例子 GCC、Clang、MSVC Visual Studio、CLion、Xcode

Visual Studio 是 IDE,MSVC 是它常用的编译工具链,二者不能简单地看成同一个概念。

2.4 VS2022 的特点

Visual Studio 2022 适合 Windows 平台上的 C/C++ 学习和开发,主要优点包括:

  • 编辑、编译和调试功能完整;
  • 安装完成后通常不需要复杂配置;
  • 图形化调试功能强大;
  • 可以观察变量、内存和调用栈;
  • 社区版可以免费用于学习。

它的主要缺点是安装体积和磁盘占用相对较大。

2.5 在 VS 中确保使用 C 语言

Visual Studio 会根据文件扩展名决定编译语言:

  • .c 通常按 C 语言编译;
  • .cpp 通常按 C++ 编译。

如果想学习纯 C 语言,应当把源文件保存为:

1
main.c

而不是:

1
main.cpp

否则某些只能在 C++ 中通过的语法可能掩盖 C 语言问题。

三、C 程序如何变成可执行文件

3.1 总体过程

一个 C 程序从源代码到可执行文件,通常经历:

1
2
3
4
5
6
7
8
9
10
11
12
13
源文件
↓
预处理
↓
编译
↓
汇编
↓
目标文件
↓
链接
↓
可执行文件

日常交流中,人们也经常把预处理、编译和汇编统称为“编译阶段”,再与链接阶段进行区分。

3.2 预处理

预处理器主要处理:

  • #include;
  • #define;
  • 条件编译;
  • 注释移除;
  • 宏替换。

例如:

1
2
#include <stdio.h>
#define MAX_VALUE 100

使用 GCC 可以观察预处理结果:

1
gcc -E main.c -o main.i

3.3 编译

狭义的编译阶段负责词法和语法分析、类型检查、语义分析、优化以及生成汇编代码。

1
gcc -S main.i -o main.s

如果变量未声明、括号不匹配或者类型使用错误,通常会在这一阶段报错。

3.4 汇编

汇编器将汇编代码转换成目标文件:

1
gcc -c main.s -o main.o

目标文件已经包含机器指令,但其中可能仍有尚未解析的函数或全局符号,因此通常不能直接作为完整程序运行。

3.5 链接

链接器把多个目标文件、库文件和运行时支持代码组合成最终可执行文件:

1
gcc main.o -o main

如果代码中调用了一个只声明却没有定义的函数,源文件可能编译成功,但链接时会报告未定义符号。

3.6 多文件工程

假设工程中包含:

1
2
main.c
add.c

它们会先分别编译:

1
2
main.c  →  main.o
add.c → add.o

再共同链接:

1
main.o + add.o + 库文件 → 可执行程序

对应命令:

1
2
3
gcc -c main.c -o main.o
gcc -c add.c -o add.o
gcc main.o add.o -o app

也可以让 GCC 驱动程序一次完成:

1
gcc main.c add.c -o app

3.7 Windows 与 Linux 文件扩展名

文件类型 Windows 常见扩展名 Linux 常见扩展名
C 源文件 .c .c
头文件 .h .h
目标文件 .obj .o
可执行文件 .exe 通常没有固定扩展名
静态库 .lib .a
动态库 .dll .so

3.8 编译错误与链接错误

编译错误

常见原因:

  • 语法错误;
  • 类型不匹配;
  • 标识符未声明;
  • 缺少分号;
  • 函数声明不可见。

链接错误

常见原因:

  • 函数只有声明,没有定义;
  • 忘记链接某个目标文件;
  • 库文件没有加入;
  • 同一个全局符号被重复定义;
  • 函数名称或参数不一致。

判断错误发生在编译阶段还是链接阶段,是调试多文件工程的基本能力。

四、项目、源文件和头文件

4.1 源文件

C 语言通常把 .c 文件称为源文件,主要保存函数定义、全局变量定义和具体实现。

1
2
3
4
5
// add.c
int Add(int left, int right)
{
return left + right;
}

4.2 头文件

.h 文件称为头文件,通常保存函数声明、类型声明、结构体定义、宏定义和外部变量声明。

1
2
3
4
5
6
7
// add.h
#ifndef ADD_H
#define ADD_H

int Add(int left, int right);

#endif

4.3 使用头文件

1
2
3
4
5
6
7
8
9
// main.c
#include <stdio.h>
#include "add.h"

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

这里:

  • add.h 告诉编译器 Add 的函数签名;
  • add.c 提供 Add 的真正实现;
  • 链接器最终把 main.o 和 add.o 连接起来。

4.4 尖括号与双引号

1
2
#include <stdio.h>
#include "add.h"

一般约定:

  • <...> 用于标准库或配置的系统头文件;
  • "..." 用于当前项目中的自定义头文件。

具体搜索路径由编译器和项目配置决定。

4.5 不要直接包含 .c 文件

不推荐:

1
#include "add.c"

源文件应当独立编译,接口通过头文件共享。直接包含 .c 文件容易导致重复定义,也会破坏正常的模块边界。

五、第一个 C 语言程序

1
2
3
4
5
6
7
#include <stdio.h>

int main(void)
{
printf("hello C\n");
return 0;
}

运行结果:

1
hello C

5.1 逐行解释

包含标准输入输出头文件

1
#include <stdio.h>

stdio.h 中声明了 printf 等标准输入输出函数。

定义 main 函数

1
int main(void)

main 是程序入口,返回类型是 int。

调用 printf

1
printf("hello C\n");

这条语句把字符串输出到标准输出,\n 表示换行。

返回程序状态

1
return 0;

返回 0 通常表示程序正常结束。

六、main 函数

6.1 main 是程序入口

操作系统启动程序后,C 运行时完成必要初始化,然后调用 main 函数。

一个可执行程序只能有一个入口,因此正常工程中只能定义一个 main 函数。即使工程包含多个 .c 文件,也只能有一个 main 定义。

6.2 main 的常见标准形式

不接收命令行参数:

1
2
3
4
int main(void)
{
return 0;
}

接收命令行参数:

1
2
3
4
int main(int argc, char* argv[])
{
return 0;
}

其中:

  • argc 表示命令行参数数量;
  • argv 保存各个参数字符串。

6.3 不要写 void main

下面的写法不属于可移植的标准形式:

1
2
3
void main()
{
}

推荐始终使用:

1
int main(void)

6.4 int main() 与 int main(void)

在 C 语言中,int main() 的空参数列表表示参数信息未指定;int main(void) 则明确表示函数不接收参数。

因此,在 C 代码中推荐使用 int main(void)。

需要注意,这一点与 C++ 不同。在 C++ 中,空参数列表表示没有参数。

6.5 main 的常见错误

  • 把 main 写成 mian;
  • 漏掉参数列表的括号;
  • 使用中文括号、引号或分号;
  • 忘记函数体大括号;
  • 一个工程定义多个 main;
  • 把源文件误建成 .cpp;
  • 写成非标准的 void main。

七、printf 与标准库

7.1 printf 是什么

printf 是 C 标准库中的格式化输出函数,声明在:

1
#include <stdio.h>

它通常把结果写到标准输出。

7.2 常用格式说明符

1
2
3
4
5
6
7
8
9
10
11
int age = 20;
unsigned int count = 100U;
char grade = 'A';
double pi = 3.1415926;
const char* name = "Alice";

printf("%d\n", age);
printf("%u\n", count);
printf("%c\n", grade);
printf("%f\n", pi);
printf("%s\n", name);
格式 对应参数 说明
%d int 有符号十进制整数
%u unsigned int 无符号十进制整数
%c int 输出一个字符
%s char* 输出以 '\0' 结尾的字符串
%f double 输出浮点数
%zu size_t 输出 sizeof 等结果
%p void* 输出地址
%% 无参数 输出百分号

7.3 printf 中的 %f 与 %lf

调用可变参数函数时,float 会发生默认参数提升,转换成 double。

因此,在 printf 中:

1
2
printf("%f\n", 3.14);
printf("%lf\n", 3.14);

%f 与 %lf 对浮点输出具有相同效果,通常推荐写 %f。

但在 scanf 中二者不同:

1
2
3
4
5
float f = 0.0f;
double d = 0.0;

scanf("%f", &f);
scanf("%lf", &d);
  • %f 需要 float*;
  • %lf 需要 double*。

不要把 printf 与 scanf 的规则混在一起。

7.4 格式与参数必须匹配

错误示例:

1
2
3
double value = 3.14;

// printf("%d\n", value); // 类型不匹配

printf 根据格式字符串解释后面的参数。如果格式说明符与实际参数类型不匹配,行为可能未定义。

推荐开启编译器警告:

1
gcc -std=c11 -Wall -Wextra -Wpedantic main.c

7.5 为什么必须包含 stdio.h

头文件中包含 printf 的函数声明。声明让编译器知道函数名称、参数形式和返回值类型。

现代 C 语言不允许依赖隐式函数声明。使用库函数前,应包含对应头文件。

7.6 什么是标准库

为了避免重复实现输入输出、字符串处理和内存管理等常见功能,C 标准规定了一组标准库接口。

头文件 典型功能
stdio.h 输入输出
stdlib.h 内存分配、转换、程序控制
string.h 字符串和内存块处理
math.h 数学函数
ctype.h 字符分类和转换
time.h 时间和日期
assert.h 断言

标准规定函数行为,各编译器和平台提供具体实现。编译器厂商还可能提供扩展函数,但扩展函数不一定能跨平台使用。

八、C 语言关键字

8.1 什么是关键字

关键字是被 C 语言保留并赋予特殊语法意义的单词,例如 int、if、return 和 sizeof。

程序员不能把关键字用作变量名、函数名或其他普通标识符。

8.2 C90 的 32 个关键字

1
2
3
4
5
6
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

8.3 按用途分类

基本类型与复合类型

1
2
3
char short int long signed unsigned
float double void
struct union enum

存储类别与类型限定

1
2
auto register static extern
const volatile

流程控制

1
2
3
if else switch case default
for while do
break continue goto return

其他

1
sizeof typedef

sizeof 是运算符,不是普通函数。

8.4 后续标准新增关键字

C99 增加了:

1
inline restrict _Bool _Complex _Imaginary

C11 又增加了与原子操作、泛型选择、静态断言和线程存储等相关的关键字。

学习初期不必一次记住所有关键字。随着类型、循环、函数、结构体和内存管理等内容逐步展开,再理解它们更有效。

九、字符与 ASCII

9.1 字符常量

C 语言使用单引号表示字符常量:

1
2
3
4
'A'
'q'
'0'
'@'

双引号表示字符串:

1
2
"A"
"hello"
写法 含义
'A' 字符常量
"A" 包含 'A' 和 '\0' 的字符串

在 C 语言中,普通字符常量 'A' 的类型是 int,但它通常可以安全地存入 char。

9.2 为什么需要字符编码

计算机最终保存的是数字。字符编码负责建立“字符与数值”之间的对应关系。

ASCII 是常见的字符编码基础。现代系统通常使用 ASCII 兼容编码表示基础英文字符。

9.3 常见 ASCII 范围

字符范围 ASCII 十进制值
'0' 到 '9' 48 到 57
'A' 到 'Z' 65 到 90
'a' 到 'z' 97 到 122
换行 LF 10
空字符 NUL 0

大小写英文字母在 ASCII 中相差 32。不过,编写字符转换代码时更推荐使用 <ctype.h> 中的 tolower 和 toupper,而不是手动加减 32。

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

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

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

return 0;
}

在 ASCII 环境下输出:

1
2
3
Q
81
Q

9.5 输出可打印 ASCII 字符

ASCII 中通常把 32 到 126 视为可打印字符,127 是 DEL 控制字符,不属于普通可打印字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main(void)
{
int code = 0;

for (code = 32; code <= 126; ++code)
{
printf("%c ", code);

if ((code - 31) % 16 == 0)
{
printf("\n");
}
}

return 0;
}

9.6 数字字符转换为整数

数字字符在字符集中连续排列,因此可以写:

1
2
3
4
5
6
7
char ch = '7';

if (ch >= '0' && ch <= '9')
{
int value = ch - '0';
printf("%d\n", value);
}

十、字符串与空字符

10.1 C 语言中的字符串

C 语言没有独立的内置字符串类型。字符串通常表示为:

一段连续字符,并以空字符 '\0' 作为结束标志。

例如 "abcdef" 在内存中实际包含:

1
'a' 'b' 'c' 'd' 'e' 'f' '\0'

10.2 sizeof 与 strlen

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <string.h>

int main(void)
{
printf("%zu\n", sizeof("abc"));
printf("%zu\n", strlen("abc"));
return 0;
}

输出:

1
2
4
3

区别是:

  • sizeof("abc") 计算整个字符数组所占字节数,包含 '\0';
  • strlen("abc") 统计 '\0' 之前的字符数量,不包含 '\0'。

10.3 两种字符数组初始化

1
2
char arr1[] = {'a', 'b', 'c'};
char arr2[] = "abc";

它们的内存内容不同:

1
2
arr1: 'a' 'b' 'c'
arr2: 'a' 'b' 'c' '\0'

因此:

1
2
sizeof(arr1) == 3
sizeof(arr2) == 4

10.4 没有 \0 时不能使用 %s

下面的代码是错误的:

1
2
3
char arr1[] = {'a', 'b', 'c'};

// printf("%s\n", arr1); // 未定义行为

%s 会不断读取字符,直到遇到 '\0'。但 arr1 中没有结束标志,函数可能继续访问数组边界之外的内存。

它有时会打印“随机字符”,也可能崩溃。正式说法不是“必然打印乱码”,而是:

访问越界导致未定义行为。

正确写法:

1
2
3
4
5
char arr1[] = {'a', 'b', 'c', '\0'};
char arr2[] = "abc";

printf("%s\n", arr1);
printf("%s\n", arr2);

10.5 字符串中间的 \0

1
printf("%s\n", "abc\0def");

输出 abc。字符串字面量实际包含:

1
'a' 'b' 'c' '\0' 'd' 'e' 'f' '\0'

因此:

1
2
sizeof("abc\0def") == 8
strlen("abc\0def") == 3

10.6 字符数组与字符串字面量指针

1
2
char arr[] = "hello";
const char* ptr = "hello";
  • arr 是一份可以修改的字符数组副本;
  • ptr 指向字符串字面量;
  • 修改字符串字面量会导致未定义行为。
1
2
3
arr[0] = 'H';      // 正确

// ptr[0] = 'H'; // 错误

实践中应使用 const char* 接收字符串字面量,避免误修改。

10.7 ‘\0’、’0’、0 和 NULL

写法 含义
'\0' 数值为 0 的空字符
'0' 数字字符,在 ASCII 中值为 48
0 整数常量零
NULL 空指针常量宏

'\0' 的数值为 0,但它与字符 '0' 完全不同。NULL 用于指针语境,不应该拿来表示普通字符串结束字符。

十一、转义字符

11.1 什么是转义字符

反斜杠 \ 会改变后续字符的含义:

1
printf("abc\ndef");

其中 \n 不是反斜杠和字母 n 两个普通字符,而是一个换行转义序列。

11.2 常见转义序列

转义序列 含义
\n 换行
\r 回车
\t 水平制表
\v 垂直制表
\b 退格
\f 换页
\a 警报
\\ 反斜杠
\' 单引号
\" 双引号
\? 问号
\0 数值为 0 的空字符

不同终端对 \a、\b、\f 和 \v 的显示效果可能不同。

11.3 输出引号和反斜杠

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void)
{
printf("%c\n", '\'');
printf("\"hello\"\n");
printf("C:\\code\\main.c\n");
return 0;
}

输出:

1
2
3
'
"hello"
C:\code\main.c

11.4 八进制转义

八进制转义形式为 \ddd,最多使用三个八进制数字。

1
printf("%c\n", '\130');

八进制 130 等于十进制 88,在 ASCII 中对应字符 X。\0 也可以看成八进制转义的一种特殊形式。

11.5 十六进制转义

十六进制转义以 \x 开始,后面会继续读取十六进制数字,并不严格限制为两个数字。

1
printf("%c\n", '\x30');

十六进制 30 等于十进制 48,在 ASCII 中对应字符 0。

如果后面紧跟的字符仍然属于十六进制数字,可能被继续吞入同一个转义序列。必要时可以拆分字符串字面量:

1
const char* text = "\x41" "BC";

相邻字符串字面量会在编译阶段自动连接。

11.6 Windows 路径中的反斜杠

字符串 "C:\test\new" 中的 \t 会被解释为制表符,\n 会被解释为换行。

正确写法:

1
"C:\test\new"

或者在部分场景使用正斜杠:

1
"C:/test/new"

十二、C 语言中的语句

12.1 什么是语句

C 程序由声明、表达式和语句等结构组成。语句用于描述程序执行的动作和流程。

入门阶段常见语句包括:

  • 空语句;
  • 表达式语句;
  • 复合语句;
  • 选择语句;
  • 循环语句;
  • 跳转语句。

需要注意,函数调用通常属于表达式,后面加分号后构成表达式语句,并不是与表达式语句完全独立的标准语法类别。

12.2 空语句

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

1
;

例如:

1
2
while (getchar() != '\n')
;

循环体就是空语句。

12.3 空语句造成的常见错误

1
2
3
4
if (score >= 60);
{
printf("pass\n");
}

if 后面的分号已经构成完整的空语句,因此后面的大括号不受 if 控制,会无条件执行。

同样要警惕:

1
2
3
4
for (int i = 0; i < 10; ++i);
{
// 这不是预期的循环体
}

12.4 表达式语句

表达式后面添加分号,就构成表达式语句:

1
2
3
4
5
int a = 10;
int b = 0;

b = a + 5;
++b;

12.5 函数调用语句

函数调用表达式后面加分号:

1
2
printf("hello\n");
Add(2, 3);

从标准语法角度看,它们属于表达式语句。

12.6 复合语句

一对大括号中的零条或多条内容构成复合语句,也叫代码块:

1
2
3
4
{
int x = 10;
printf("%d\n", x);
}

代码块还会形成作用域,块内声明的普通局部变量在块外不可见。

12.7 控制语句

选择语句

1
2
3
4
5
6
if (condition)
{
}
else
{
}
1
2
3
4
5
6
7
switch (value)
{
case 1:
break;
default:
break;
}

循环语句

1
2
3
4
5
6
7
8
9
10
11
12
while (condition)
{
}

do
{
}
while (condition);

for (int i = 0; i < 10; ++i)
{
}

跳转语句

  • break:结束循环或 switch;
  • continue:跳过本轮循环剩余部分;
  • goto:跳转到函数内标签;
  • return:结束函数并返回。

十三、注释

13.1 注释的作用

注释用于帮助人理解代码,编译器不会把注释当作正常程序逻辑执行。

好的注释应该重点解释:

  • 为什么这样设计;
  • 有什么特殊边界;
  • 为什么不能采用看似更简单的方案;
  • 参数和返回值有什么约束;
  • 哪些行为容易误用。

不需要为一眼就能看懂的代码逐行翻译。

不推荐:

1
++count; // count 加一

更有价值:

1
2
3
4
// 跳过下标 0,因为它保存的是协议头而不是有效数据。
for (int i = 1; i < size; ++i)
{
}

13.2 块注释

1
/* 这是一条注释 */

块注释可以跨行:

1
2
3
4
5
/*
* 计算两个整数之和。
* 返回相加结果。
*/
int Add(int left, int right);

13.3 块注释不能嵌套

下面的写法有问题:

1
2
3
4
5
/*
printf("A\n");
/* 内层注释 */
printf("B\n");
*/

编译器遇到第一个 */ 就会结束块注释,后面的代码可能产生语法错误。

13.4 单行注释

1
2
// 这是一条单行注释
int value = 10; // 行尾注释

// 注释从双斜杠开始,一直延伸到当前行末尾。它从 C99 开始成为标准 C 语法。

13.5 字符串中的注释符号

1
printf("// hello /* world */\n");

双引号内部的 // 和 /* */ 都只是普通字符串内容,不具有注释作用。

13.6 注释会被替换为空白

在翻译过程中,注释会被替换为空白字符。

1
min/* comment */Value

不会连接成 minValue,而会形成近似于 min Value 的两个记号。

13.7 临时屏蔽大段代码

因为块注释不能嵌套,使用 /* */ 临时包住本身含有块注释的代码很容易出错。

可以考虑条件编译:

1
2
3
#if 0
printf("temporarily disabled\n");
#endif

正式代码仍应及时删除无用逻辑,并依靠版本控制保存历史,而不是长期保留大量注释掉的代码。

十四、完整示例

下面的程序综合演示 main、printf、字符和整数、字符串、sizeof 与 strlen、转义字符、语句和代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <string.h>

int Add(int left, int right)
{
return left + right;
}

int main(void)
{
int age = 20;
char grade = 'A';
const char message[] = "hello C";

printf("age: %d\n", age);
printf("grade: %c, ASCII: %d\n", grade, grade);
printf("message: %s\n", message);

printf(
"sizeof(message): %zu\n",
sizeof(message));
printf(
"strlen(message): %zu\n",
strlen(message));

printf("2 + 3 = %d\n", Add(2, 3));
printf("path: C:\\code\\main.c\n");

{
char digit = '7';

if (digit >= '0' && digit <= '9')
{
printf(
"digit value: %d\n",
digit - '0');
}
}

return 0;
}

运行结果:

1
2
3
4
5
6
7
8
age: 20
grade: A, ASCII: 65
message: hello C
sizeof(message): 8
strlen(message): 7
2 + 3 = 5
path: C:\code\main.c
digit value: 7

十五、常见错误汇总

15.1 文件扩展名写成 .cpp

文件可能被当成 C++ 编译,导致学习环境与目标语言不一致。

15.2 main 拼写错误

把 main 写成 mian 时,链接器找不到程序入口。

15.3 使用中文符号

错误:

1
printf(“hello”);

正确:

1
printf("hello");

15.4 忘记分号

编译器可能在下一行附近报错,因此要同时检查报错位置的上一行。

15.5 格式说明符不匹配

printf 格式字符串与参数类型不匹配可能导致未定义行为。

15.6 字符数组没有 \0

不带 '\0' 的字符数组不能直接交给要求 C 字符串的接口。

15.7 if 或 for 后误写分号

这个分号会形成空语句,使后续代码块脱离原来的控制结构。

15.8 使用库函数却不包含头文件

编译器无法获得正确函数声明,可能产生诊断或错误的调用行为。

15.9 认为块注释可以嵌套

/* */ 遇到第一个结束符就结束,不能直接嵌套。

15.10 把转义序列当成两个字符

'\n' 是一个字符转义序列,不是反斜杠和字母 n 两个独立字符。

十六、常见面试与复习问题

16.1 编译器和 IDE 有什么区别?

编译器负责把源代码转换为目标代码;IDE 提供代码编辑、项目管理、编译和调试等完整开发环境。

16.2 C 程序生成可执行文件经过哪些阶段?

通常包括预处理、编译、汇编和链接。多个源文件分别生成目标文件,再由链接器组合成可执行程序。

16.3 头文件和源文件分别放什么?

头文件主要放声明和公共类型,源文件主要放定义和实现。

16.4 为什么一个程序只能有一个 main?

main 是程序入口。一个可执行程序必须具有唯一、明确的入口。

16.5 printf 为什么需要包含 stdio.h?

stdio.h 提供 printf 的声明,让编译器能够检查和生成正确调用。

16.6 sizeof 是函数吗?

不是。sizeof 是 C 语言运算符。

16.7 字符常量 ‘A’ 在 C 中是什么类型?

普通字符常量在 C 中的类型是 int,其值对应执行字符集中的编码。

16.8 字符串为什么必须以 \0 结尾?

C 字符串接口通常不额外保存长度,而是通过 '\0' 判断字符串终点。

16.9 sizeof(“abc”) 和 strlen(“abc”) 分别是多少?

1
2
sizeof("abc") == 4
strlen("abc") == 3

前者包含字符串结尾的 '\0',后者不统计 '\0'。

16.10 char arr[] = {‘a’,’b’,’c’} 是字符串吗?

严格来说,它只是一个包含三个字符的数组,不是合法的以 '\0' 结尾的 C 字符串。

16.11 ‘\0’ 和 ‘0’ 有什么区别?

'\0' 的数值是 0,表示空字符;'0' 是数字字符,在 ASCII 中数值是 48。

16.12 printf 中 %f 和 %lf 有什么区别?

在 printf 中通常没有实际区别,二者都用于输出 double。在 scanf 中有区别:%f 对应 float*,%lf 对应 double*。

16.13 为什么 %s 输出无 \0 数组是未定义行为?

%s 会继续向后读取,直到找到 '\0'。如果数组内没有终止符,就可能越界访问不属于该数组的内存。

16.14 注释可以嵌套吗?

标准块注释 /* */ 不能嵌套;// 只持续到当前行末尾。

16.15 函数调用语句是不是独立语法类别?

函数调用本身是表达式,函数调用表达式加分号后构成表达式语句。“函数调用语句”是便于初学者理解的常用教学分类。

十七、总结

本文需要重点掌握以下内容:

  1. C 源代码需要经过预处理、编译、汇编和链接才能运行;
  2. 编译器负责翻译代码,IDE 提供完整开发环境;
  3. 多个 .c 文件分别编译,再链接成一个程序;
  4. 头文件主要放声明,源文件主要放实现;
  5. 一个可执行程序只能有一个 main;
  6. 推荐使用标准形式 int main(void);
  7. 使用标准库函数前要包含对应头文件;
  8. printf 的格式说明符必须与实际参数匹配;
  9. C 关键字不能用作自定义标识符;
  10. 字符使用单引号,字符串使用双引号;
  11. C 字符串以 '\0' 作为结束标志;
  12. sizeof 计算对象大小,strlen 统计首个 '\0' 前的字符数量;
  13. 没有 '\0' 的字符数组不能直接作为 %s 参数;
  14. 转义字符使用反斜杠改变字符含义;
  15. 一个分号也可以构成空语句;
  16. 函数调用加分号后属于表达式语句;
  17. 块注释不能嵌套,字符串中的注释符号只是普通字符;
  18. 注释应该解释设计意图和特殊约束,而不是逐行翻译代码。