跳转至

C语言学习笔记

本笔记内容主要来源于《C Primer Plus》。

00.前言

本节为学习动机、语言背景等内容,与具体语法无关,若想快速上手可跳过。

0.0 作者的话

很感谢你能在茫茫互联网之中看到我的这篇笔记,记录笔记同时也是我学习的过程。我之前自学过Python,JavaScript,PHP等编程语言,大多在自学编程中的问题我也遇到过,我会用尽量简洁高效的语言梳理C语言的知识点,如果你有问题,可以在主页找到我的联系方式。

m310ct
2025 年 6 月 11 日 00:41

0.1 C语言的起源

C语言由贝尔实验室的丹尼斯·里奇(Dennis Ritchie)和肯·汤普逊(Ken Thompson)于1971年在开发 UNIX 操作系统时设计。
它是在 B 语言的基础上发展而来,是现代多数编程语言的祖先之一。

0.2 为何选择C

  • 高效性:相对于如 Python 这类解释型语言,C语言会直接编译为机器码,运行效率更高。
  • 可移植性:使用标准C编写的程序可以在多种操作系统和硬件平台上编译运行。
  • 底层控制力强:C语言既可以操作硬件、管理内存,又比汇编语言更易读、易写,学习成本低。

0.3 学习C语言之后能做什么

  • 编写系统级软件:如操作系统内核、驱动程序、嵌入式系统等。
  • 参与底层开发:如网络协议实现、编译器、虚拟机等。
  • 开发嵌入式设备程序:如智能家电、物联网设备上的固件。
  • 开发游戏引擎核心模块:许多高性能游戏底层使用 C 或 C++ 编写。
  • 刷算法题/参加编程竞赛:C 是高效、紧凑的算法实现语言。
  • 逆向工程与漏洞挖掘:C 是理解内存布局、二进制安全的基础。
  • 学习操作系统与计算机组成原理:深入理解内存、指针、汇编。
  • 移植开源项目或开发高性能库:如参与 Git、Redis、Nginx 等项目。
  • 作为跳板语言:C 是理解 C++、Rust、Go 等现代系统语言的基础。

在开始学习之前,希望你已经充满热情。

如果你已经准备好了,那就让我们一起踏上 C 语言的学习之旅吧!


01.Hello World

本章内容为C语言的环境配置,包括windows、MacOS、Linux平台,以及C语言程序从代码的编译过程

1.1 你的第一个程序

# include <stdio.h>
int main(){
    printf("Hello World");
    return 0;
}

现在你不需要知道这个程序中代码的具体意思,你只需要在你的电脑上编译并且运行这个程序。

1.2 C语言程序编译过程

C语言程序的编译分为四个阶段:

  • 预处理:执行宏替换、头文件展开、条件编译等操作,#include, #define, #ifdef等指令会被先执行。
  • 编译:把C语法(如变量定义、函数调用)翻译成汇编指令,不涉及CPU具体编码,只是人类可读的汇编语言。
  • 汇编:把汇编代码翻译成机器码,生成与 CPU 架构对应的二进制指令。
  • 链接:把目标文件与库函数链接成最终可执行程序。

logo

1.3 配置开发环境

MacOS/Linux已经预安装gcc,打开终端即可使用。

Windows

CLion/VSCode

配置C环境

  • 下载并安装 MSYS2:https://www.msys2.org/,类 UNIX 环境开发平台,下载后默认安装,安装时可修改安装目录,记录默认目录,后续要用

  • 打开MSYS2,输入以下命令:

pacman -Syu   # 初始更新(可能需重启)
pacman -S mingw-w64-ucrt-x86_64-gcc gdb make
  • 将MSYS2添加到环境变量。win+s,搜索编辑系统环境变量,点击环境变量,选中系统变量中的Path编辑新建,输入MSYS2的路径下/ucrt64/bin的完整路径,比如我的是E:\SDK\MSYS2\ucrt64\bin确定

  • 打开CMD,输入gcc --version,显示版本号即为成功。

1.4 gcc编译程序

单文件编译

gcc hello.c -o hello
./hello   # Windows: hello.exe
  • gcc:调用编译器

  • hello.c:源文件

  • -o hello:输出文件名(不加会默认生成 a.out

请直接再cmd或终端中运行程序,不然程序可能会一闪而过!

编译多个文件

gcc main.c utils.c -o app
./app

添加调试信息

gcc -g main.c -o debug_app
gdb ./debug_app
  • -g:包含调试信息,用于 GDB/LLDB 调试器

常用编译选项

选项 含义
-o <file> 指定输出文件名
-g 添加调试信息
-Wall 打开所有警告
-Werror 把警告当成错误
-O2 / -O3 启用优化
-std=c99 / -std=c11 指定 C 标准版本
-I <dir> 添加头文件搜索路径
-L <dir> / -l<lib> 添加库搜索路径 / 链接库

现在你已经成功完成了 C 语言的第一个程序,并了解了编译原理和开发环境的配置!

接下来,将正式进入语法与实战阶段的学习。


02.C语言程序的基本构成

本章通过一个极简示例介绍C程序的基本组成部分——头文件、main() 函数、变量声明、printf()、格式控制符%d、转义字符\n

/*我的C语言练习程序
自我介绍程序*/
#include <stdio.h>
int main(){
    int age;//声明age
    age = 17; //age变量表示年龄
    printf("你好我是m310ct\n");
    printf("我今年%d岁了",age);
    return 0;
}

这是一段简单输出自我介绍的代码,你可以先编译在终端中运行看看效果。代码运行成功你将看到如下图的结果。下文将详细解释这段代码。

执行结果

⚠️乱码问题

如果在cmd中运行出现乱码,主要引起的原因是vscode自动以utf-8保存,而windows的默认编码是gbk。在运行程序前输入chcp 65001将cmd的编码改成utf-8。

CLion中出现乱码,请将右下角UTF-8转换为gbk

2.1 引入头文件

#include <stdio.h>

这行代码的用途就是引入头文件。

#include

  • 预处理器指令。这个指令放在代码的最前面,在编译器的预处理步骤中执行。除了引入头文件,还可以宏定义,条件编译等。类似的预处理器指令还有 #define(宏定义)、#ifdef(条件编译)等。

stdio.h

  • 这里用尖括号包裹的就是头文件。头文件以.h作为文件后缀,用来声明函数、变量、结构体等接口的,帮助编译器知道你用的东西“长什么样”。

  • stdio的全称是标准输入输出(standar input output)其中存放了常用的一些函数,比如打印用到的函数printf()就存放在其中,打印的本质就是输出。如果你引入stdio.h你就无法在程序中使用打印函数。

这里你可能想问:为什么不内置输出函数呢?答:并非所有程序都需要打印功能,这体现出C语言“轻装上阵”的哲学。

2.2 main()函数

int main(){
    /*skip*/
    return 0;
}
  • main()函数是C语言程序的入口函数,程序从main()函数开始执行,以return 0;结尾。

  • int:C语言中的数据类型,代表整数,后文会讲解。

  • 大括号:大括号表示其中的内容为这个函数的代码。

return 0

  • return 表示程序运行完后将结果返回给操作系统。

  • return 0;:表示程序正常结束(成功)。

  • return 1; 或其他非零值:表示程序出错或异常退出。

  • 这个返回值叫做“退出码”,可以被操作系统或其他程序读取,用来判断程序是否运行成功。

2.3 变量

int age;//声明age
age = 17; //age变量表示年龄

声明

int age;就是一个声明语句。声明就是告诉电脑要在内存中开辟一块空间,为之后存取数据所用。声明时要明确数据类型, 以后变量只能存放对应数据类型的数据。

关于数据类型的知识在下一章。

赋值

使用赋值运算符=来给变量进行赋值。简而言之,就是将=右边的值给到=左边的变量。

初始化变量

初始化就是在声明变量的同时给予变量一个初始值。

int age = 17;

2.4 字符串打印

printf("你好我是m310ct\n");
printf("我今年%d岁了",age);

printf函数

printf函数的作用是格式化输出各种数据,以字符串的形式打印到终端

注意 :printf打印字符串时需要用英文状态的双引号包裹,这是新手常见错误!

03. 数据类型

本章将介绍 C 语言中的数据类型、sizeof 函数与 scanf 输入函数。


3.1 数据类型的作用

在之前的代码中,我们已经在定义函数时接触过了数据类型——int

数据类型的作用是告诉计算机:这个值是什么类型、可以进行哪些操作、以及应该如何在内存中存储它


3.2 int 类型

  • int 是有符号整数类型,可以存储正整数、负整数或 0。

八进制和十六进制

  • 八进制:逢 8 进 1,比如十进制下的 9 转换为八进制是 11
  • 十六进制:逢 16 进 1,超过 9 的部分用字母 a-f 表示,例如十进制的 15 转为十六进制是 f

在 C 语言中,要输出不同进制的数,只需将 %d(十进制)替换为: - %o:八进制输出 - %x:十六进制输出


3.3 其他整数类型

C 语言提供 shortlongunsigned 等关键字来修饰 int 类型,表示不同的存储大小或取值范围:

  • short intshort:占用的存储空间通常比 int 少。
  • long intlong:用于存储更大的整数。
  • long long intlong long:通常占用至少 64 位空间,用于极大数值。
  • unsigned intunsigned:只能存储非负整数(0 及正数)。
  • 同样也有 unsigned longunsigned long long 等组合形式。

(具体大小依平台而定,见后续表格)


3.4 打印整数类型

下面是各种整数类型的输出格式说明:

类型 十进制 十六进制 八进制 无符号输出
int %d %x %o %u
long %ld %lx %lo %lu
short %hd %hx %ho %hu
long long %lld %llx %llo %llu

示例代码:

#include <stdio.h>

int main(){
    short a = 1;
    int b = 100;
    long c = 1000000;
    long long d = 10000000000000;

    printf("我是 a,十进制:%hd,十六进制:%hx,八进制:%ho\n", a, a, a);
    printf("我是 b,十进制:%d,十六进制:%x,八进制:%o\n", b, b, b);
    printf("我是 c,十进制:%ld,十六进制:%lx,八进制:%lo\n", c, c, c);
    printf("我是 d,十进制:%lld,十六进制:%llx,八进制:%llo\n", d, d, d);

    return 0;
}

⚠️ 注意:输出结果依赖于平台(如 32 位或 64 位系统),不同平台下类型大小可能略有差异。

运行结果:

我是 a,十进制:1,十六进制:1,八进制:1 
我是 b,十进制:100,十六进制:64,八进制:144 
我是 c,十进制:1000000,十六进制:f4240,八进制:3641100 
我是 d,十进制:10000000000000,十六进制:9184e72a000,八进制:221411634520000

3.5 char 字符类型

char 类型用于表示单个字符,但本质上它是一种整数类型,每个字符都有一个整数编码值(ASCII)。

ASCII 简介

在 C 语言中,字符会被转换为对应的 ASCII 码值。通过%c打印字符,例如:

char a = 'A'; // 实际存储的是整数 65
printf("%c",a);

完整的 ASCII 表如下:

ASCII图

你可以通过字符直接赋值,无需记忆码表:

char ch1 = 'A';      // 存储 ASCII 码 65
char ch2 = 66;       // 直接存储整数 66,输出时为 'B'

转义字符

有些字符(比如换行、引号等)不能直接写在字符串中,需要使用转义字符(以 \ 开头):

转义字符 含义 示例说明
\n 换行 输出后光标跳到下一行
\t 制表符(Tab) 相当于按了一个 Tab 键
\\ 反斜杠字符 \ 输出一个反斜杠
\' 单引号 ' 输出一个单引号
\" 双引号 " 输出一个双引号
\r 回车符(光标回到行首) 通常配合 \n 用于 Windows 换行
\b 退格符(删除前一个字符) 有些终端有效,等价于 Backspace
\a 响铃(beep) 有些终端会响一声或闪烁
\0 空字符(null terminator) 表示字符串结束,用于字符串结尾标志

3.6 浮点类型

小数或科学计数法的类型就是浮点型,浮点型包括floatdoublelong double

科学计数法

当一个数过大或过小,位数过多难以直接表达时便使用科学计数法。比如在数学中,10000可表示为 $1 \times 10^5$,0.00321则表示为 $3.21 \times 10^-3$ 在C语言中,用e代表$10^n$,比如$1 \times 10^5$就是1e5,$3.21 \times 10^-3$ 就是3.21e-3。

各种浮点类型

  • float:单精度浮点数,至少表示 6 位有效数字,打印时使用 %f(普通浮点数格式)或 %e(科学计数法格式)。
  • double:双精度浮点数,至少表示 15 位有效数字,打印时同样使用 %f 或 %e
  • long double:扩展精度浮点数,至少表示 18 位有效数字,打印时使用 %Lf(普通格式)或 %Le(科学计数法格式)。

浮点数溢出

当浮点数的值超出其类型所能表示的最大限度(比如 float 的最大值约为 ( 3.4 \times 10^{38} ))时,计算结果会变成 inf,这称为 上溢(Overflow)
值得注意的是:只有当计算结果本身超出最大范围才会溢出,例如将 3.0e38f 乘以 2 会得到 inf。但如果只是对一个非常大的浮点数加上一个相对较小的值(如 3.4e38f + 1000),由于 float 精度有限,这个加数可能被忽略,因此 不会造成溢出,只是“精度丢失”。

类似地,如果浮点数的结果非常接近 0,小于其最小的非零正数(对 float 而言约为 ( 1.4 \times 10^{-45} )),就会变成 0.000000,这称为 下溢(Underflow)
例如将 1.0e-40f 连续除以很大的数,最终结果可能过小到无法表示,就会发生下溢。

float large = 3e38f;
float overflow = large * 2;

// 浮点下溢:小于 float 最小非零正数约为 1.4e-45
float small = 1.0e-40f;
float underflow = small / 1.0e10f;

printf("上溢示例:%f\n", overflow);    // 输出 inf
printf("下溢示例:%f\n", underflow);   // 输出 0.000000
return 0;

浮点数精度误差

浮点数不能进行完全精确的计算:

double a = 0.1;
double b = 0.2;
double c = a + b;
printf("%.20f",c);//输出:0.30000000000000004441,这里.20是保留20位小数的意思

由于 float 使用有限的 23 位二进制尾数(mantissa)表示有效数字,当数值过大时,它的最小可区分单位也变大,导致较小的加数(如 +1)无法被表示,从而被舍弃。举一个例子:你用一个只能显示 8位数 的计算器,它能显示 99999999,那你再按 +1 它还是 99999999,因为已经“装不下”更小的差别了。