[持续更新] C语言 从入门到入土

author_cherry 25 2025-08-30

01. C语言程序的初始结构

1.1 程序的入口:主函数 main()

主函数的各种写法

主函数,即 main(),是C语言程序的入口,启动任何一个C语言程序都要从主函数开始。

下方代码块展示了标准的主函数写法

// ...上方代码...
int main() {

	return 0;
}
// ...下方代码...

而在其他教程书中可能采取了古老的写法,下方代码块展示了现不推荐使用主函数写法

// ...上方代码...
void main() {

}
// ...下方代码...

总结 main() 函数作为程序的入口有且只有1个

1.2 程序的依赖:头文件

引入外部库的写法

头文件,即在程序中可能使用了一些需要引用外部库的函数,而C语言为了编译和运行效率的最大化,不会将所有外部库全部引入,需要编写程序时通过语法手动引入。

下方代码块以使用 printf()函数为例,展示引入外部库的写法

#include <stdio.h>

int main() {
	printf("Hello World!");
	return 0;
}

外部库 stdio.h 的名称意义

.h是英文"head"的缩写,表示头文件的后缀名称;

std是英文"standard"的缩写,含义为“标准”;

io分别是英文"input"和"output"的缩写,含义为"输入输出";

那么,stdio.h的含义为“标准输入输出头文件”。

总结 使用不同的函数需要引入对应的库

02. 内置的数据类型

2.1 常见的数据类型

char        字符型

short       短整型
int         整型
long        长整型
long long   更长的整型

float       单精度浮点型
double      双精度浮点型

2.2 打印内容包含参数:%作占位符

打印函数 printf()可以在打印内容中包含程序运算出结果的参数,基本语法如下所示:

// 语法示例
printf("[内容] %[占位符类型]", [传入参数])

下方代码块展示常用的占位符

%d        整型占位符
%f        浮点型占位符
%.1f      精确到小数点后1位的浮点型占位符
%.5f      精确到小数点后5位的浮点型占位符

%c        字符型占位符
%s        字符串型占位符

结合下方示例,便能理解。

2.3 度量数据类型:sizeof()函数

上述对于数据类型的大小或精度使用了定性的说法,如“短”、“更长”和“双”等等,而在程序中对于数据类型是有明确的大小的。

事实上,sizeof()严格意义上属于操作符(具体的概念后置),其处理的操作数(即操作的对象)为数据类型。

下方代码块展示了 sizeof()函数的语法示例

// 语法示例
sizeof(char)

上述写法含义为:返回字符型所占内存中的大小,单位为字节。

下方代码块展示了度量数据类型的写法

#include <stdio.h>

int main() {
	printf("%d \n", sizeof(char));

	printf("%d \n", sizeof(short));
	printf("%d \n", sizeof(int));
	printf("%d \n", sizeof(long));
	printf("%d \n", sizeof(long long));

	printf("%d \n", sizeof(float));
	printf("%d \n", sizeof(double));

	return 0;
}

VS 2022软件中编译并运行的结果:

>>> 1
>>> 2
>>> 4
>>> 4
>>> 8
>>> 4
>>> 8

由此可见,我们便能整理出C语言中内置了的不同数据类型所占内存的大小。

char        字符型            占1个字节

short       短整型            占2个字节
int         整型              占4个字节
long        长整型            占4个字节
long long   更长的整型         占8个字节

float       单精度浮点型       占4个字节
double      双精度浮点型       占8个字节

总结 sizeof()函数返回数据类型在内存中所占空间的大小

2.4 声明数据类型以定义变量

在C语言中创建变量时,需要声明变量的数据类型,其本质目的是为了确定在内存中申请空间来存储变量所需要的大小。

下方代码块展示了创建不同类型变量的写法

int main() {
	int age = 20;
	char a = 'A';
	char sex[] = "male";
	float height = 174.54;

	return 0;
}

总结 定义变量时声明数据类型本质上是向内存申请空间

03. 变量

3.1 定义变量的方法

在C语言中,定义变量需在变量名前声明数据类型,来方便编译器根据不同的数据类型向内存申请不同大小的空间存放变量的内容。

[数据类型] [变量名] = [变量的内容]

下方代码块展示了定义变量的一些示例:

int main() {
	int age = 20; // 年龄为20
	char a = 'A'; // 一个字符变量
	char sex[] = "male"; // 性别为男性
	float height = 174.54; // 身高为174.54cm

	return 0;
}

3.2 修改变量的方法

在C语言中,同一作用域的变量不能被重复定义。

// 错误示范
#include <stdio.h>

int main() {
	int a = 1;
	int a = 0;

	return 0;
}

VS 2022 软件中编译并运行的结果:

>>> “a”: 重定义;多次初始化

由此可见,若强制重复定义变量,会报错:“重定义;多次初始化”。

下方的代码块展示了修改变量的写法

// ...上方代码...
	int a = 0;
	a = 1;
// ...下方代码...

总结 变量不可被重复定义,但可以被修改

3.3 变量的作用域和生命周期

在C语言中,变量分为全局变量局部变量,两种类型的变量有不同的作用范围,当程序运行超出了某个变量的作用域,则会销毁该变量。

3.3.1 局部变量可与全局变量的名称相同

#include <stdio.h>

int a = 0; // 全局变量,其作用域是整个工程内部

int main() {
	int a = 1; // 局部变量,其作用域是main()函数内部
	printf("%d\n", a);

	return 0;
}

VS 2022 软件中编译并运行的结果:

>>> 1

由此可见,程序运行到何处,变量的赋值就采用该处所属作用域的变量值。

形式上看,变量的赋值依从局部优先,但在生产环境中极不推荐使用此写法。

3.3.2 局部变量的作用域划定方式

下方代码展示了局部变量作用域划定示例

// ...上方代码...
int main() {
	int a = 0;
	printf("a = %d\n", a);
	{
		int a = 1;
		printf("a = %d\n", a);
	}
	printf("a = %d\n", a);

	return 0;
}
// ...下方代码...

VS 2022 软件中编译并运行的结果:

>>> a = 0
>>> a = 1
>>> a = 0

由此可见,局部变量可以使用大括号来划定作用域;

并且一个局部变量的生命周期在且仅在其作用域内,离开作用域其赋值便被销毁,并赋值离开后的所处作用域的变量值。

下方代码块可以帮助理解离开作用域被销毁

int main() {
	{
		int a = 1;
		printf("a = %d\n", a);
	}
	printf("a = %d\n", a);

	return 0;
}

VS 2022 软件中编译并运行的结果:

>>> “a”: 未声明的标识符

由此可见,当变量 a离开其作用域后,便立即被销毁,外层作用域无法使用内层作用域的变量。

3.3.3 全局变量的作用域是整个工程

如下图所示,在同一个工程中创建了两个C语言程序文件:

250813_03_03_3.3.3_1.png

现设想实现:在 Test250813.c中声明全局整型变量 a并能在 Test250806.c中调用。

// Test250813.c

int a = 1;
// Test250806.c

#include <stdio.h>
extern int a; //声明外部符号

int main() {
	printf("a = %d\n", a);

	return 0;
}

VS 2022 软件中编译并运行的结果:

>>> a = 1

全局变量的声明周期是整个程序。

3.4 变量存储用户输入

下方代码块展示了使用 scanf()函数来实现加法计算的示例

// ...上方代码...
int main() {
	int a = 0;
	int b = 0;
	scanf("%d", &a);
	scanf("%d", &b);
	printf("两数之和为 %d\n", a + b);

	return 0;
}
// ...下方代码...

其中,&是取地址符,在 scanf()函数中指将用户输入的内容存储至取地址符后的变量中。

小问题:解决 VS2022 中 scanf()函数不安全的问题

当使用C语言中的 scanf() 函数时出现了如下的报错信息:

>>> 'scanf': This function or variable may be unsafe. Consider using scanf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.

只需要且必须在项目文件的第一行加入:

#define _CRT_SECURE_NO_WARNINGS 1

但不推荐采用报错信息中提供的 scanf_s() 函数解决,因为这只是在 VS2022 上的特殊情况,不适用于其他编译器。

04. 常量

在C语言中 ,常量可以分为如下四种:

  • 字面常量
  • const 修饰的常变量
  • #define 定义的标识符常量
  • 枚举常量

4.1 字面常量

字面常量,即指单独的常量,一般是人为约定的。
比如,在一个工程中我们约定 π 取 3.14等等。

4.2 const 修饰的常变量

const修饰的常变量,可以将在工程中不希望修改的变量赋予只读的属性;
在修饰时,需要声明数据类型,并且遵守作用域规则;
其拥有相对于常量更灵活的优势。

通俗来讲,const 修饰的常变量,本质是常量,也具备常量的属性,但同时具有常量的只读属性。

const [数据类型] [变量名] = [变量的内容]

下方代码块展示了 const修饰的常变量的示例

// ...上方代码...
	const float pi = 3.14;

	float r = 0;
	printf("圆的半径 r = ");
	scanf("%f", &r);
	float s = pi * r * r;
	printf("\n圆的面积为 %f", s);
// ...下方代码...

此时,工程中约定了 π 取 3.14,并且不希望被修改。

有关 const 的内容将在 指针 一节深化

4.3 #define 定义的常量

#define 定义的常量,即在工程文件的开头,使用 #define 定义常量;
其对于常量的处理,仅仅是在编译前对工程代码进行简单计算并文本替换,故相较于 const 修饰的常变量,在程序运行时不会将常量放入内存中;
因而,使用 #define 定义的常量是无符号的,即无数据类型的,或者任意类型都可的。

#define [常量名] [常量内容]

4.4 枚举常量

枚举常量,即常量的可能取值有多个。
比如,三原色的可能取值有:红,绿,蓝。

下方代码块展示了枚举常量的创建和使用示例

// ...上方代码...
enum Color {
	RED,
	GREEN,
	BLUE
};

int main() {
	enum Color R = RED;

	return 0;
}
// ...下方代码...

05. 字符串

5.1 创建字符和字符串

在C语言中,严格地区分了字符和字符串。

下方代码块展示了创建字符和字符串的示例:

// ...上方代码...
	char a = 'A';
	char b[] = "A";
	char A[] = "ThisIsA";
// ...下方代码...

字符,即单个字符,使用单引号引起;
字符串,即由多个单独的字符组成的一串内容,使用双引号引起;
字符串的内容也可以是单独的一个字符。

5.2 字符串的本质

实际上,C语言中不存在字符串数据类型,只存在字符数据类型。
而字符串,仅仅是将每个单字符按照顺序放入数组,并在结尾加入 \0 来声明字符串的结尾。所以,字符串的本质是数组

下面通过调试功能来直观地看:

250815_03_05_5.2_1.png

由此可见,字符串的本质是数组,并且在末尾添加了 \0来声明结束位置。

250815_03_05_5.2_2.png

5.3 字符串的大小

当我们创建字符串数组时,

// 语法示例
char a[] = "ABCDEF"

中括号引起是指定数组的长度(大小),当中括号内为空,则根据定义的数组内容编译器自动分配数组大小。

下方代码块展示了指定数组大小和打印函数的运行逻辑

// ...上方代码...
int main() {
	char a1[] = "ABCDE";
	char a2[] = { 'A', 'B', 'C', 'D', 'E' };
	char a3[] = { 'A', 'B', 'C', 'D', 'E', '\0'};
	char b1[5] = "ABCDE";
	char b2[6] = "ABCDE";

	printf("%s\n", a1);
	printf("%s\n", a2);
	printf("%s\n", a3);
	printf("%s\n", b1);
	printf("%s\n", b2);

	return 0;
}
// ...下方代码...

VS 2022 软件中编译并运行的结果:

>>> ABCDE
>>> ABCDE烫烫烫烫烫烫烫烫烫烫烫烫烫藺BCDE // 本行结果在不同的环境下运行的结果可能不同
>>> ABCDE
>>> ABCDE烫烫烫烫烫烫烫烫烫烫烫烫烫藺BCDE // 本行结果在不同的环境下运行的结果可能不同
>>> ABCDE

通过调试监视器可知:

250815_03_05_5.3_1.png

数组 a1、数组 a3、数组 b2,三个数组相互等价,属于字符串;
数组 a2、数组 b1,两个数组相互等价,属于由字符组成的数组。

通过输出内容可知:

printf()函数打印字符串时,其运行机制是一直打印,直到遇到字符串截止符号 \0
而数组 a2、数组 b1只是单纯地由字符组成的数组,当 printf()函数强制接收时,由于其末尾不存在 \0,因而在内存中继续打印,直到在内存的某处遇到了 \0则停止,于是输出的内容中就不可避免地打印一些乱码符号。

5.3 度量字符串长度:strlen()函数

strlen() 函数,其可以传入一个字符串数组,统计传入字符串的字节数,且不计 \0
需注意,中文字符占 2 个字节,英文字符占 1 一个字节;
所以,若传入的内容均为英文,则输出即为实际字数;若均为中文,则输出的是实际字数的两倍;若中英混杂,则不推荐使用此函数来计算字数;
并且其计算字符串长度的机制仍然是与 printf() 函数类似,即直到遇到 \0 停止。

下方代码块给出了使用示例:

// ...上方代码...
	char arr1[] = "使用示例";
	printf("%d\n", strlen(arr1));
// ...下方代码...
// ...上方代码...
	char arr2[] = "Example";
	printf("%d\n", strlen(arr2));
// ...下方代码...

VS 2022 软件中编译并运行的结果:

>>> 8
>>> 7

06. 转义字符

6.1 常用的转义字符

转义字符 描述
\a 响铃(Alert/BEL)
\b 退格(Backspace)
\f 换页(Form feed)
\n 换行(Newline)
\r 回车(Carriage return)
\t 水平制表(Tab)
\v 垂直制表(Vertical tab)
\\ 反斜杠
\' 单引号
\" 双引号
\? 问号(避免三字符组)
\0 空字符(NULL)
\ooo 八进制转义(1-3位)
\xhh 十六进制转义

小问题:解决 \f 换页字符在 Windows 命令行中无效

换页转义字符 \f 可以实现类似清屏的效果,而由于版本不同,\f 可能无效,于是有以下替代方案:

使用 windows.h 库中的 sysytem() 函数,向 windows 命令行发送一个 cls 清屏命令以实现,下方代码块展示了该方法:

#include <stdio.h>
#include <Windows.h>

int main() {
	printf("测试转义字符 1");
	system("cls");
	printf("测试转义字符 2");
}

07. 分支语句

7.1 C 语言中的符号

在 C 语言中,特别规定了以下几种符号

  1. 整数 0 为假,非 0 为真。
  2. && 表示逻辑且,|| 表示逻辑或。

7.2 if...else... 语句

7.2.1 分支语句的基本写法

在分支语句中,若其条件表达式结果为真,则对应的语句即被执行。

下方代码块展示 if...else... 语句的基本写法

// 语法示例
if ([条件]) {
	[语句];
}
else if ([条件]) {
	[语句];
}
else {
	[语句];
}

7.2.2 C 语言不区分缩进

观察下方代码块和运行结果:

// ...上方代码...
int main() {
	int a = 0;
	int b = 1;
	if (a == 1)
		if (b == 1)
			printf("1");
	else
		printf("2");
	
	return 0;
}
// ...下方代码...

VS 2022 软件中编译并运行的结果:

>>> 

发现输出了非分支语句的结果,这是因为上述代码在格式化后:

// ...上方代码...
int main() {
	int a = 0;
	int b = 1;
	if (a == 1)
		if (b == 1)
			printf("1");
		else
			printf("2");
	
	return 0;
}
// ...下方代码...

else 语句实际上匹配的是 b == 1 条件的 if 语句,而 a == 1 条件的 if 语句实则控制了 b == 1 条件的 if 语句和 else 语句,因此大分支不满足,则不执行任何小分支语句。

由此可见,C 语言不区分缩进。即 else 语句匹配与其最近的 if 语句。

注意 在实际的代码编写中,应当时刻注意代码的可读性!

7.2.3 return 语句是函数的一次性出口

// ...上方代码...
int test() {
	int a = 1;
	if (a == 1) {
		return 1;
	}
	return 0;
}

int main() {
	int r = test();
	printf("%d", r);

	return 0;
}
// ...下方代码...

VS 2022 软件中编译并运行的结果:

>>> 1

由此可见,return 语句是函数的一次性出口,一旦执行到 return 语句就结束该函数且与该语句所在位置无关,并返回给定值。

上述代码更好的呈现风格:

// ...上方代码...
int test() {
	int a = 1;
	if (a == 1) {
		return 1;
	}
	else {
		return 0;
	}
}
// ...下方代码...

7.2.4 等号比较符与赋值符易混的后果

观察如下代码块并预测运行结果:

// ...上方代码...
	int a = 1;
	if (a = 0) {
		printf("执行语句");
	}
// ...下方代码...

VS 2022 软件中编译并运行的结果:

>>> 

发现输出了非分支语句的结果,这是因为 if 语句的条件是赋值表达式,而赋值的结果为 0,于是导致条件为假,则不执行分支语句。

由此可见,若将变量写在左侧,将常量写在右侧,一旦将比较运算符误写为赋值符,编译器在运行时不会报错,则可能导致难以排查的 bug。

因此,可以将代码风格该为如下风格:

[常量] == [变量]

如此,当误写情况发生时,编译器则会报错提醒。

7.3 switch 语句

7.3.1 分支语句的基本写法

下方代码块展示了

// 语法示例
switch ([整型表达式]) {
case [整型]:
	[语句];
case [整型]:
	[语句];
// ...
}

switch 语句中,case 仅仅规定了语句的入口,而不规定出口,即选择了一个 case 语句将一直顺序执行其他语句(包括其他 case 中的语句),直到遇到 break 语句结束,这种机制适合用于多个语句匹配同一语句

7.3.2 default 字句

当输入的整型没有一个 case 语句与之对应时,可以使用 default 子句来定义其他所有整型的默认处理语句。

// 语法示例
switch ([整型表达式]) {
case [整型]:
	[语句];
default:
	[语句];
// ...
}

下方代码块用 switch 语句实现了根据用户输入以输出对应的工作与非工作日

#include <stdio.h>
#include <Windows.h>

int main() {
	int day = 0;
	scanf("%d", &day);
	switch (day) {
	case 1:
	case 2:
	case 3:
	case 4:
	case 5:
		printf("工作日");
		break;
	case 6:
	case 7:
		printf("非工作日");
		break;
	default:
		printf("选择错误");
		break;
	}
	return 0;
}

08. 循环语句

8.1 while 语句

8.1.1 循环语句的基本写法

while 循环语句中,若条件为真,则循环执行循环体内的语句。

下方代码块展示了 while 循环语句的基本写法

// 语法示例
while ([条件]) {
	[语句];
}

8.1.2 while 循环的执行流程

250823_03_08_8.1.2_1.png

8.1.3 continuebreak 语句

观察下方代码块并预测结果:

// ...上方代码...
int main() {
	int i = 1;
	while (i <= 10) {
		printf("%d ", i);
		if (i == 5) {
			break;
		}
		i++;
	}

	return 0;
}
// ...下方代码...

VS 2022 软件中编译并运行的结果:

>>> 1 2 3 4 5

由此可见,当计数器 i 增加到了 5 后,满足 if 语句条件直接结束循环。

观察下方代码块并预测结果:

// ...上方代码...
int main() {
	int i = 1;
	while (i <= 10) {
		if (i == 5) {
			continue;
		}
		printf("%d ", i);
		i++;
	}

	return 0;
}
// ...下方代码...

VS 2022 软件中编译并运行的结果:

>>> 1 2 3 4 _

由此可见,当计数器 i 增加到了 5 后,满足 if 语句条件跳过本次循环,也跳过了计数器自增语句,导致计数器 i 一直停留在 5,于是进入无限循环。

8.1.4 等待获取用户缓冲区:getchar() 方法

getchar() 方法的运行机制是,等待用户键盘输入内容,先传入到键盘缓冲区中,直到用户敲下回车键,结束等待并将缓冲区提交至 getchar() 方法,获取并消耗一个任意字符。

// ...上方代码...
	int i = 0;
	while ((i = getchar()) != EOF) {
		printf("%c", i);
	}
// ...下方代码...

VS 2022 软件中编译并运行的结果:

// 以输入"QWQ"并回车为例
>>> QWQ
>>> QWQ
>>> 

上方代码块的运行机制如下图所示:

250825_03_08_8.1.4_1.png

由此可见,当缓冲区不为空时,getchar() 方法每访问一次缓冲区时,就会释放一个缓冲区中的字符。

8.1.5 使用 getchar() 和循环语句清空缓冲区

利用 getchar() 语句的特性,配合循环语句,来实现清空缓存区的效果。

下方代码块展示了清空缓存区的写法

int ch = 0;
while ((ch = getchar) != '\n') {}

当在使用 getchar() 方法,遇到了不在意料内的跳过语句现象时,可以尝试在代码中添加清空键盘缓冲区的方法。

8.1.6 打印字符:putchar() 方法

putchar() 方法可以让命令行输出一个字符。

下方代码实现了只打印用户输入中的数字部分

// ...上方代码...
int main() {
	char i = '\0';
	while ((i = getchar()) != EOF) {
		if (i < '0' || i > '9') {
			continue;
		}
		putchar(i);
	}

	return 0;
}
// ...下方代码...

VS 2022 软件中编译并运行的结果:

// 以输入"QWQ 1234 AWA"为例
>>> QWQ 1234 AWA
>>> 1234

注意 有关命令行中的输入输出可以不过多研究

8.2 for 语句

下方代码块展示了 for 语句的基本写法

// 语法示例
for ([初始化语句]; [条件判断语句]; [调整语句]) {
	[语句];
}

for 循环中的语句为空时,则按恒为真处理。

250825_03_08_8.2.1_1.png

8.3 do...while 语句

下方代码块展示了 do...while 语句的基本写法

do {
	[语句];
} while ([条件表达式]);

其语句的特点是至少执行一次语句。

250825_03_08_8.3_1.png

这种循环结构在实际使用中相对较少,一般用于游戏菜单等情况。

8.4 利用循环以解决问题

8.4.1 计算阶乘之和

计算表达式 1! + 2! + 3! + ... + n! 指定 n 的结果:

实现方法一:先计算阶乘,再求和

#include <stdio.h>

int main() {

	// 计算阶乘之和
	// 计算 n! + (n-1)! + ... + 1!

	// 用户输入
	int n = 0; // 定义目标数
	scanf("%d", &n);
	int p = n; // 同步内外层for循环的目标数

	// 初始化阶乘和求和的初始值
	int fac = 1;
	int sum = 0;

	// for 循环计数器
	int i = 0;
	int j = 0;

	for (i = 1; i <= n; i++) {
		fac = 1;
		for (j = 1; j <= p; j++) {
			fac = fac * j;
		}
		sum = sum + fac;
		p = p - 1;
	}

	printf("%d\n", sum);

	return 0;
}

实现方法二:利用变量的累计特性

#include <stdio.h>

int main() {

	int n = 0;
	scanf("%d", &n);

	int fac = 1;
	int sum = 0;

	int i = 0;

	for (i = 1; i <= n; i++) {
		fac = fac * i;
		sum = sum + fac;
	}

	printf("%d\n", sum);
}

8.4.2 二分查找

实现方法一:歪门邪道

#include <stdio.h>

int main() {

	// 二分查找

	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 
	int target = 0;
	scanf("%d", &target);
	int i = 0;

	int length = sizeof(arr) / sizeof(arr[0]);
	int half = (length / 2);

	for (i = 0; i < length; i++) {
		if (target == arr[half] - 1) {
			printf("目标数在有序数组的第%d个处", half);
			break;
		}
		else if (target > arr[half]) {
			half = (half + length) / 2;
			continue;
		}
		else if (target < arr[half]) {
			half = (half + 0) / 2;
			continue;
		}
		else {
			printf("目标数在有序数组的第%d个处", half + 1);
			break;
		}
	}

	return 0;
}

实现方法二:正统写法

8.4.3 文字特效

#include <stdio.h>

int main() {

	// 文字特效

	char arr1[] = "Welcome to BIT!!!!";
	char arr2[] = "#################";

	// 初始化左右界限
	int left = 0;
	int right = strlen(arr1) - 1;

	while (left <= right + 2) {
	
		//重复打印一个字符串
		printf("%s", arr2);
		Sleep(250);
		system("cls"); // 清屏

		// 更新字符串内容
		arr2[left] = arr1[left];
		arr2[right - 1] = arr1[right - 1];

		// 更新左右界限
		left = left + 1;
		right = right - 1;
	}

	return 0;
}