想象一下,我们身处的数字世界,如同一座座宏伟的建筑。操作系统、编译器、数据库、嵌入式设备乃至绚丽的游戏引擎,它们都是这座大厦的重要组成部分。而C语言,正是构建这一切的坚固基石。自丹尼斯·里奇于贝尔实验室孕育出这颗编程界的明星以来,C语言凭借其高效性、灵活性以及对计算机底层那份极致的掌控力,历经数十载风雨洗礼,依旧是无数开发者心中的不二之选。
学习C语言,不仅仅是掌握一门编程技能,更是一次深入探索计算机灵魂的旅程。它能让你理解内存如何管理、程序如何与硬件交互,为你后续学习其他高级语言、深入计算机体系结构打下坚实的基础。本教程将引领你,从C语言的基础语法出发,逐步揭开其神秘面纱,助你在这条充满挑战与机遇的道路上稳步前行。
一、初识C语言:搭建你的第一个“Hello, World!”
万丈高楼平地起,我们从最经典的程序开始。
1.1 第一个C程序:向世界问好
#include <stdio.h> // 引入标准输入输出头文件
// main函数是程序的入口点,操作系统会从这里开始执行
int main() {
// printf是一个标准库函数,用于向控制台输出文本
// "\n" 是一个转义字符,表示换行
printf("Hello, World!\n");
// main函数返回0表示程序正常结束
return 0;
}
代码解析:
-
#include <stdio.h>:这是一个预处理指令。它告诉编译器在实际编译之前,将stdio.h(标准输入输出头文件)的内容包含进来。这个头文件里声明了我们后面用到的printf等函数。 -
int main():这是C程序的主函数,每个C程序都必须有且只有一个main函数。程序从main函数开始执行,到main函数结束。int表示main函数执行完毕后会返回一个整数值给操作系统。 -
printf("Hello, World!\n");:调用printf函数,将引号内的字符串输出到屏幕上。\n是一个特殊的字符,代表换行。 -
return 0;:表示main函数执行成功并正常退出。通常,返回0代表成功,非0代表出现某种错误。 -
//和/* ... */:这些是注释。//用于单行注释,/* ... */用于多行注释。注释是给程序员看的,编译器会忽略它们。
1.2 开发环境的选择与搭建
要编译和运行C代码,你需要一个C编译器和开发环境。
-
编译器:
-
GCC (GNU Compiler Collection):Linux和macOS上最常用的开源编译器,也可通过MinGW/Cygwin在Windows上使用。
-
Clang:LLVM项目的一部分,以其快速编译和优秀的错误提示著称。
-
Visual C++ (MSVC):微软Windows平台下的编译器,集成在Visual Studio中。
-
-
集成开发环境 (IDE):
-
Visual Studio Code (VS Code):轻量级且强大的跨平台编辑器,通过插件支持C/C++开发。
-
Visual Studio:功能全面的Windows平台IDE。
-
CLion:JetBrains出品的专业C/C++跨平台IDE。
-
Dev-C++:简单易用的Windows平台IDE,适合初学者快速上手。
-
选择一个你用着顺手的即可。对于初学者,VS Code配合GCC/Clang或者Dev-C++都是不错的选择。
1.3 从源代码到可执行程序:编译与链接之旅
当你写完C代码(.c文件)后,它并不能直接运行,需要经历以下步骤:
-
预处理 (Preprocessing):处理
#include,#define等预处理指令,展开宏,删除注释等。生成.i文件。 -
编译 (Compilation):将预处理后的代码转换成汇编语言。生成
.s文件。 -
汇编 (Assembly):将汇编代码转换成机器可以执行的二进制指令(目标代码)。生成
.o或.obj文件。 -
链接 (Linking):将你的目标代码和程序中用到的库函数(比如
printf)的目标代码组合起来,生成最终的可执行文件(如Windows下的.exe或Linux下的无后缀文件)。
二、C语言的基石:数据类型与变量
程序处理的是数据,而数据有不同的类型。
2.1 基本数据类型:构建数据的砖瓦
C语言提供了多种基本数据类型来存储不同种类的数据:
| 类型 |
关键字 |
大致字节数 (常见32/64位系统) |
典型范围 (有符号signed) |
用途描述 |
|---|---|---|---|---|
| 字符型 |
|
1 |
-128 ~ 127 |
存储单个字符 |
| 短整型 |
|
2 |
-32,768 ~ 32,767 |
存储较小范围整数 |
| 整型 |
|
4 |
-2,147,483,648 ~ 2,147,483,647 |
常用的整数类型 |
| 长整型 |
|
4 或 8 |
依赖系统和编译器 |
存储较大范围整数 |
| 更长整型 |
|
8 |
约 -9x10^18 ~ 9x10^18 |
存储非常大整数 |
| 单精度浮点型 |
|
4 |
约 ±3.4e±38 (6-7位有效数字) |
存储带小数的数 |
| 双精度浮点型 |
|
8 |
约 ±1.7e±308 (15-16位有效数字) |
存储更高精度小数 |
| 无类型 |
|
N/A |
N/A |
特殊用途,如指针 |
注意:long的大小在不同系统和编译器下可能不同(32位系统通常4字节,64位系统通常8字节)。可以使用sizeof运算符来查看特定类型在当前系统上占用的字节数。
2.2 类型修饰符:数据的更多面貌
-
signed:表示有符号数(可以表示正、负、零),对char和整型类型默认即为signed。 -
unsigned:表示无符号数(只能表示非负数)。同样的字节数,无符号类型可以表示更大的正数范围。例如unsigned int。unsigned char u_char_val = 200; // 范围 0 ~ 255 signed char s_char_val = -100; // 范围 -128 ~ 127 -
const:定义常量,其值在初始化后不能被修改。const double PI = 3.14159; // PI = 3.14; // 错误!PI是常量,不可修改
2.3 变量:存储数据的容器
变量是内存中用于存储数据的一块具名空间。
-
声明:告诉编译器变量的名字和类型。
数据类型 变量名; -
初始化:在声明变量时给它一个初始值。
数据类型 变量名 = 初始值;
#include <stdio.h>
int main() {
int age; // 声明一个整型变量 age
age = 30; // 给 age 赋值
float salary = 5000.50f; // 声明并初始化一个浮点型变量 salary (f后缀表示float)
char grade = 'A'; // 声明并初始化一个字符型变量 grade (单引号括起字符)
// 变量在使用前通常需要初始化,否则其值是不确定的(垃圾值)
int uninitialized_var;
// printf("%d\n", uninitialized_var); // 行为未定义,可能输出任意值
printf("年龄: %d\n", age);
printf("薪水: %.2f\n", salary); // .2f 表示保留两位小数
printf("等级: %c\n", grade);
return 0;
}
2.4 常量:不变的值
-
#define宏常量 (预处理指令):在预处理阶段进行文本替换。#define MAX_USERS 100 int users[MAX_USERS]; // 预处理后变为 int users[100]; -
const限定符:定义类型化的常量,编译器会进行类型检查。更推荐使用const。const int MAX_SCORE = 100; // MAX_SCORE = 99; // 编译错误
三、运算符与表达式:数据的加工厂
运算符用于对数据进行操作,表达式则是由数据和运算符组成的计算式。
3.1 算术运算符
+ (加), - (减), * (乘), / (除), % (取模/取余数)
int a = 10, b = 3;
printf("a + b = %d\n", a + b); // 13
printf("a / b = %d\n", a / b); // 3 (整数除法,结果截断小数)
printf("a %% b = %d\n", a % b); // 1 (10除以3的余数)
float c = 10.0f, d = 3.0f;
printf("c / d = %f\n", c / d); // 3.333333 (浮点数除法)
自增 ++ 和自减 --:
-
前缀:
++a(先自增,后使用新值) -
后缀:
a++(先使用原值,后自增)
int x = 5;
int y = ++x; // x先变成6,然后y被赋值为6。此时 x=6, y=6
int z = x++; // z先被赋值为6(x的当前值),然后x变成7。此时 x=7, z=6
printf("x=%d, y=%d, z=%d\n", x, y, z); // 输出 x=7, y=6, z=6
3.2 关系运算符
用于比较两个值,结果为真(1)或假(0)。 == (等于), != (不等于), > (大于), < (小于), >= (大于等于), <= (小于等于)
int num1 = 5, num2 = 10;
printf("num1 == num2 is %d\n", num1 == num2); // 0 (假)
printf("num1 < num2 is %d\n", num1 < num2); // 1 (真)
3.3 逻辑运算符
用于连接或修改关系表达式,结果也为真(1)或假(0)。
-
&&(逻辑与):两边都为真,结果才为真。 -
||(逻辑或):一边为真,结果就为真。 -
!(逻辑非):真变假,假变真。
int age = 25;
float height = 1.75f;
// 年龄大于18 并且 身高大于1.7
if (age > 18 && height > 1.7) {
printf("符合条件\n");
}
短路求值:
-
对于
A && B,如果A为假,则B不会被求值。 -
对于
A || B,如果A为真,则B不会被求值。
3.4 位运算符 (了解即可,进阶内容)
直接对数据的二进制位进行操作。 & (按位与), | (按位或), ^ (按位异或), ~ (按位取反), << (左移), >> (右移)
3.5 赋值运算符
= (简单赋值), +=, -=, *=, /=, %= (复合赋值) a += 5; 等价于 a = a + 5;
3.6 条件运算符 (三目运算符)
C语言中唯一的三目运算符:条件 ? 表达式1 : 表达式2 如果条件为真,则整个表达式的值为表达式1的值;否则为表达式2的值。
int score = 85;
char grade = (score >= 60) ? 'P' : 'F'; // P (Pass), F (Fail)
printf("成绩等级: %c\n", grade); // 输出 P
优点:简洁。缺点:嵌套过多时可读性差,不宜滥用。
3.7 sizeof 运算符
返回一个类型或一个变量所占用的内存字节数。它是一个编译时运算符(大多数情况)。
printf("sizeof(int) = %zu bytes\n", sizeof(int));
printf("sizeof(double) = %zu bytes\n", sizeof(double));
int arr[10];
printf("sizeof(arr) = %zu bytes\n", sizeof(arr)); // 输出 10 * sizeof(int)
```%zu` 是 `sizeof` 结果的推荐格式说明符。
### 3.8 运算符优先级与结合性
当一个表达式中包含多个运算符时,优先级决定了运算顺序(类似数学中的先乘除后加减)。结合性决定了相同优先级的运算符从左到右还是从右到左执行。
**经验法则**:不确定优先级时,多用括号 `()` 来明确运算顺序,提高代码可读性。
例如,`*p++` 和 `(*p)++` 是面试中常考的:
* `*p++`:后缀`++`优先级高于`*`,且结合性从左到右。相当于 `*(p++)`。它先取得`p`指向的值,然后`p`指针自增(指向下一个元素)。
* `(*p)++`:括号优先级最高。它先取得`p`指向的值,然后对这个值进行自增操作。`p`指针本身不移动。
---
## 四、程序的眼睛和嘴巴:标准输入与输出
程序需要与用户交互,接收输入并展示结果。
### 4.1 标准输出 `printf()`
我们已经多次使用过`printf`函数了。它的原型在`<stdio.h>`中。
`printf("格式控制字符串", 参数列表);`
**格式控制符**:
* `%d` 或 `%i`:输出有符号十进制整数。
* `%u`:输出无符号十进制整数。
* `%f`:输出浮点数 (默认6位小数)。
* `%.2f`:输出浮点数,保留2位小数。
* `%e` 或 `%E`:以科学计数法输出浮点数。
* `%c`:输出单个字符。
* `%s`:输出字符串 (字符数组,直到遇到`\0`)。
* `%p`:以十六进制形式输出指针地址。
* `%x` 或 `%X`:以十六进制形式输出无符号整数。
* `%%`:输出一个 `%` 字符。
```c
#include <stdio.h>
int main() {
int item_count = 10;
float price = 19.99f;
char item_name[] = "C语言教程"; // 字符串本质是字符数组
printf("商品名称: %s\n", item_name);
printf("数量: %d\n", item_count);
printf("单价: %.2f 元\n", price);
printf("总价: %.2f 元\n", item_count * price);
printf("商品地址(内存中): %p\n", (void*)item_name);
return 0;
}
4.2 标准输入 scanf()
用于从键盘读取用户输入。 scanf("格式控制字符串", &变量1的地址, &变量2的地址, ...); 关键点:
-
scanf的参数必须是变量的地址,所以变量名前要加取地址符&。 (数组名本身代表地址,通常不需要&)。 -
输入数据时,各项数据之间默认用空格、制表符或回车分隔。
-
scanf在遇到不匹配的输入时可能会停止读取。 -
安全隐患:
scanf("%s", str)读取字符串时,若输入过长,会导致缓冲区溢出,非常危险。应使用限制长度的读取方式或fgets。
#include <stdio.h>
int main() {
int age;
float height;
char name[50]; // 字符数组用于存储字符串
printf("请输入您的姓名: ");
scanf("%s", name); // 读取字符串时,name是数组名,代表地址,不用&
// 注意:这里有缓冲区溢出风险!
printf("请输入您的年龄和身高 (用空格隔开): ");
scanf("%d %f", &age, &height); // 读取整数和浮点数,需要&
printf("\n--- 您的信息 ---\n");
printf("姓名: %s\n", name);
printf("年龄: %d 岁\n", age);
printf("身高: %.2f 米\n", height);
// 更安全的字符串输入方式
char safe_name[50];
printf("\n再次输入您的姓名 (安全方式): ");
// fgets会读取换行符,如果不需要可以处理掉
if (fgets(safe_name, sizeof(safe_name), stdin) != NULL) {
// 移除可能存在的换行符
// size_t len = strlen(safe_name);
// if (len > 0 && safe_name[len-1] == '\n') {
// safe_name[len-1] = '\0';
// }
printf("安全获取的姓名: %s\n", safe_name);
}
return 0;
}
关于scanf的返回值:scanf返回成功读取并赋值的参数个数。如果发生错误或到达文件末尾,会返回EOF。检查scanf的返回值是一个好习惯。
4.3 字符输入输出
-
getchar():从标准输入读取一个字符,返回其ASCII码(int类型),或在出错/文件结束时返回EOF。 -
putchar(int c):将字符c(实际是其ASCII码)输出到标准输出。
#include <stdio.h>
int main() {
char ch;
printf("请输入一个字符: ");
ch = getchar(); // 读取一个字符
printf("您输入的字符是: ");
putchar(ch); // 输出该字符
putchar('\n'); // 输出换行
return 0;
}
注意清空输入缓冲区:连续使用scanf和getchar时,scanf可能会留下未读取的换行符在缓冲区中,影响后续getchar的读取。例如 scanf("%d", &num); char c = getchar(); c可能会直接读到换行符。
五、程序的骨架:流程控制语句
流程控制语句决定了代码的执行顺序。
5.1 条件判断:if-else 语句
根据条件是否为真来执行不同的代码块。
if (条件1) {
// 条件1为真时执行
} else if (条件2) {
// 条件1为假,且条件2为真时执行
} else {
// 以上条件都为假时执行
}
示例:判断一个数的奇偶性
#include <stdio.h>
int main() {
int number;
printf("请输入一个整数: ");
scanf("%d", &number);
if (number % 2 == 0) {
printf("%d 是偶数。\n", number);
} else {
printf("%d 是奇数。

最低0.47元/天 解锁文章

被折叠的 条评论
为什么被折叠?



