"C语言就像一位老朋友,看似严肃,但和它相处久了,你会发现它其实是个段子手!"
第一章 认识 C 语言
1.1 C 语言的历史
C 语言,这位"老前辈",最初是由丹尼斯·里奇(Dennis Ritchie)在贝尔实验室为开发 UNIX 操作系统而设计的。1972年,它在DEC PDP-11计算机上首次亮相,从此开启了系统编程的新纪元。
趣闻:C语言的命名其实源于B语言(B Language),而B语言的名字又源于BCPL语言。所以C其实是"BCPL的C",但为了听起来更酷,他们就叫它C了!(就像你给宠物取名"小可爱",其实它就是一只普通的猫)
1978年,布莱恩·柯林汉(Brian Kernighan)和丹尼斯·里奇(Dennis Ritchie)发布了C语言的第一个公开描述,也就是著名的 K&R标准。UNIX操作系统、C编译器和几乎所有的UNIX应用程序都是用C语言编写的。
1.2 C 语言的作用
C语言之所以如此流行,是因为它有以下几个超能力:
- 易于学习:比Python难一点,但比汇编简单多了
- 结构化语言:代码清晰,层次分明
- 高效率:生成的代码几乎和汇编一样快
- 底层操作:可以操控硬件,做你想做的任何事
- 跨平台:在各种计算机上都能编译运行
C语言的用武之地:
- 操作系统(UNIX、Linux、Windows内核等)
- 编译器和解释器
- 文本编辑器(Vim、Emacs)
- 网络驱动
- 数据库
- 实体工具(比如计算器)
幽默小剧场:C语言就像一位"全能选手",既能写操作系统,又能写游戏引擎,还能在嵌入式设备上跑得飞快。相比之下,Python就像是个"社交牛逼症",在各种应用中都能见到,但关键时刻(比如系统底层)就掉链子了。
1.3 环境设置
想要学习C语言,你需要:
- 文本编辑器:VS Code、Sublime Text、Notepad++(别用记事本,那是给小学生用的!)
- C编译器:GCC(Linux/Mac)、MinGW(Windows)
小贴士:如果你在Windows上,推荐使用 MinGW 或 MSYS2,它们会让你的C语言学习之旅变得轻松愉快。
第二章 C 语言基础
2.1 C 语言程序结构
一个完整的C程序至少包含两部分:头文件和主函数。
#include <stdio.h> // 正确的头文件,不是stido.h!
int main() {
printf("Hello, world! 你终于来啦!\n");
return 0;
}
重要提示:
#include <stdio.h>是标准输入输出头文件,它包含了printf、scanf等函数的声明。不要写成#include<stido.h>,那是C语言史上最著名的拼写错误之一!
2.2 注释
注释是程序员的"小秘密",它们不会被编译,但能让代码更易读。
// 单行注释:就像在代码中写个便条
/*
多行注释:适合写长篇大论
甚至可以写个笑话:
为什么程序员总是分不清万圣节和圣诞节?
因为Oct 31 == Dec 25!
*/
程序员的幽默:在代码中写注释,就像在朋友的手机里留个"记得帮我买奶茶"的便签。
2.3 头文件和宏定义
2.3.1 头文件
头文件是.h文件,包含函数声明和宏定义。引用头文件有两种方式:
#include <stdio.h> // 系统头文件,从标准路径找
#include "my_header.h" // 自定义头文件,从当前目录找
小知识:
<stdio.h>中的 "stdio" 是 "standard input output" 的缩写,意思是标准输入输出。
2.3.2 宏定义
宏定义是C语言的"魔法咒语",可以让你的代码更简洁。
#define PI 3.1415926 // 无参宏定义
#define SQUARE(x) ((x) * (x)) // 有参宏定义
// 用法:
int radius = 5;
int area = PI * SQUARE(radius);
警告:宏定义会直接替换文本,不会检查类型,所以使用有参宏时要小心括号,否则容易出错。
2.4 关键字和标识符
2.4.1 关键字
C语言的关键字是"保留字",不能用作变量名。
| 关键字 | 说明 |
|---|---|
int |
整型变量 |
char |
字符型变量 |
if |
条件判断 |
for |
循环 |
while |
循环 |
return |
函数返回 |
幽默小剧场:C语言的关键字就像"禁用词",你不能用它们来命名你的变量,否则编译器会像老师一样说:"同学,你这名字太奇怪了!"
2.4.2 标识符
标识符就是变量名、函数名等。命名规则:
- 由字母、下划线和数字组成
- 不能以数字开头
- 不能使用关键字
命名建议:不要用
a、b、c这样简单的名字,试试userAge、totalPrice,这样你的代码会更易读,也更专业。
2.5 基础数据类型
C语言有多种数据类型,主要分为基本类型、枚举类型、void类型和派生类型。
2.5.1 整型
| 类型 | 存储大小 | 值范围 |
|---|---|---|
char |
1字节 | -128 到 127 或 0 到 255 |
int |
4字节 | -2,147,483,648 到 2,147,483,647 |
long |
4字节 | -2,147,483,648 到 2,147,483,647 |
unsigned int |
4字节 | 0 到 4,294,967,295 |
小提示:在64位系统上,
int通常是4字节,但long可能是8字节。要确定大小,用sizeof函数。
#include <stdio.h>
int main() {
printf("int 的大小: %zu 字节\n", sizeof(int));
printf("long 的大小: %zu 字节\n", sizeof(long));
return 0;
}
2.5.2 浮点型
| 类型 | 存储大小 | 值范围 | 精度 |
|---|---|---|---|
float |
4字节 | 1.2E-38 到 3.4E+38 | 6位有效位 |
double |
8字节 | 2.3E-308 到 1.7E+308 | 15位有效位 |
long double |
16字节 | 3.4E-4932 到 1.1E+4932 | 19位有效位 |
幽默提示:
float就像一个"小气鬼",只存6位有效数字;double则是"大方佬",能存15位;而long double则是"土豪",能存19位。
2.5.3 类型转换
C语言有隐式和显式类型转换。
int a = 10;
float b = 3.14;
double c = a + b; // 隐式转换:int -> double
double d = 3.14159;
int e = (int)d; // 显式转换:double -> int
警告:显式转换会丢失小数部分,比如
(int)3.9会变成3。
2.6 运算符
2.6.1 算术运算符
| 运算符 | 描述 | 示例 |
|---|---|---|
+ |
相加 | 5 + 3 = 8 |
- |
相减 | 5 - 3 = 2 |
* |
相乘 | 5 * 3 = 15 |
/ |
相除 | 5 / 3 = 1(整数除法) |
% |
取模 | 5 % 3 = 2 |
++ |
自增 | a++ = 6(先用a的值,再加1) |
-- |
自减 | a-- = 6(先用a的值,再减1) |
小技巧:
++和--有前缀和后缀之分,前缀先变后用,后缀先用后变。
2.6.2 关系运算符
| 运算符 | 描述 | 示例 |
|---|---|---|
== |
相等 | 5 == 5 为真 |
!= |
不相等 | 5 != 3 为真 |
> |
大于 | 5 > 3 为真 |
< |
小于 | 5 < 3 为假 |
>= |
大于等于 | 5 >= 5 为真 |
<= |
小于等于 | 5 <= 3 为假 |
幽默提示:关系运算符就像"比较脸",总是想看看谁更大、谁更小。
2.6.3 逻辑运算符
| 运算符 | 描述 | 示例 |
|---|---|---|
&& |
逻辑与 | 5 > 3 && 3 > 1 为真 |
| ` | ` | |
! |
逻辑非 | !(5 > 3) 为假 |
小剧场:
&&就像"双人组",两个人都要通过才通过;||就像"单人组",只要一个人通过就行;!就像"反派",把结果翻转。
2.6.4 位运算符
位运算符操作二进制位。
| 运算符 | 描述 | 示例 |
|---|---|---|
& |
按位与 | 5 & 3 = 1(二进制:0101 & 0011 = 0001) |
| ` | ` | 按位或 |
^ |
按位异或 | 5 ^ 3 = 6(二进制:0101 ^ 0011 = 0110) |
~ |
按位取反 | ~5 = -6(二进制:~0101 = 1010,补码表示) |
<< |
左移 | 5 << 2 = 20(二进制:0101 << 2 = 10100) |
>> |
右移 | 5 >> 2 = 1(二进制:0101 >> 2 = 0001) |
位运算小技巧:
<<和>>可以快速实现乘除2的幂次方,比如x << 3相当于x * 8。
2.6.5 赋值运算符
| 运算符 | 描述 | 示例 |
|---|---|---|
= |
简单赋值 | a = 5 |
+= |
加后赋值 | a += 5 等价于 a = a + 5 |
-= |
减后赋值 | a -= 5 等价于 a = a - 5 |
*= |
乘后赋值 | a *= 5 等价于 a = a * 5 |
/= |
除后赋值 | a /= 5 等价于 a = a / 5 |
%= |
取模后赋值 | a %= 5 等价于 a = a % 5 |
小提示:
+=、-=等运算符是C语言的"快捷键",让你的代码更简洁。
2.6.6 杂项运算符
| 运算符 | 描述 | 示例 |
|---|---|---|
sizeof() |
返回变量大小 | sizeof(int) = 4 |
& |
返回地址 | &a 返回a的地址 |
* |
指针解引用 | *p 访问p指向的值 |
?: |
条件表达式 | a > b ? a : b |
幽默提示:
sizeof就像一个"身材测量仪",能告诉你变量占了多少空间。
2.7 变量与常量
2.7.1 变量
变量是存储数据的容器。
int age = 25; // 定义并初始化
float height; // 仅定义
height = 1.75; // 初始化
命名建议:用有意义的名字,比如
userAge而不是a。
2.7.2 常量
常量是值不变的变量。
#define PI 3.1415926
const float PI = 3.1415926;
区别:
#define是预处理器指令,在编译前替换;const是C语言关键字,编译时检查类型。
2.8 输入和输出
2.8.1 scanf() 和 printf()
#include <stdio.h>
int main() {
char name[50];
int age;
printf("请输入你的名字: ");
scanf("%s", name);
printf("请输入你的年龄: ");
scanf("%d", &age);
printf("你好, %s! 你今年 %d 岁。\n", name, age);
return 0;
}
小提示:
scanf需要取地址符&,因为它是通过指针修改变量的值。
2.8.2 getchar() 和 putchar()
#include <stdio.h>
int main() {
char c;
printf("请输入一个字符: ");
c = getchar(); // 读取一个字符
printf("你输入的是: %c\n", c);
putchar(c); // 输出一个字符
return 0;
}
有趣事实:
getchar()和putchar()每次只处理一个字符,所以它们就像"单点突破"的勇士。
2.8.3 gets() 和 puts()
#include <stdio.h>
int main() {
char str[100];
printf("请输入一句话: ");
gets(str); // 读取一行字符串
puts(str); // 输出字符串并换行
return 0;
}
警告:
gets()不安全,容易导致缓冲区溢出。在新代码中,应该使用fgets()。
第三章 流程控制语句
3.1 顺序结构
顺序结构就是代码从上到下依次执行,就像走路一样,一步一步来。
3.2 选择结构
3.2.1 if 语句
if (条件) {
// 条件为真时执行
} else {
// 条件为假时执行
}
幽默提示:
if就像"选择题",是A还是B,全看条件。
3.2.2 switch 语句
switch (表达式) {
case 值1:
// 执行代码
break;
case 值2:
// 执行代码
break;
default:
// 默认执行代码
}
小技巧:
switch适合多条件判断,但要注意break,否则会"fall through"(继续执行下一个case)。
3.2.3 三元运算符
int max = (a > b) ? a : b; // 如果a > b,max = a,否则max = b
小幽默:三元运算符就像"精简版"的
if-else,但要小心用,否则代码会变得难读。
3.3 循环结构
3.3.1 while 循环
while (条件) {
// 循环体
}
特点:先判断条件,再执行循环体。
3.3.2 do-while 循环
do {
// 循环体
} while (条件);
特点:先执行循环体,再判断条件,至少执行一次。
3.3.3 for 循环
for (初始化; 条件; 更新) {
// 循环体
}
小提示:
for循环最常用,适合已知循环次数的情况。
3.3.4 循环控制语句
| 语句 | 描述 |
|---|---|
break |
终止循环或switch |
continue |
跳过本次循环,进入下一次 |
goto |
跳转到指定标签(不推荐使用) |
警告:
goto是"编程界的毒药",尽量避免使用,否则代码会变得像" spaghetti code"(意大利面代码)。
第四章 复杂数据类型
4.1 数组
数组是相同类型元素的集合。
int arr[5] = {1, 2, 3, 4, 5}; // 定义并初始化
int a = arr[0]; // 访问第一个元素
数组小贴士:数组下标从0开始,所以第一个元素是
arr[0]。
4.2 字符数组-字符串
在C中,字符串是用\0结尾的字符数组。
char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
char str2[] = "Hello"; // 等同于上面的定义
字符串小知识:
strlen(str)返回字符串长度(不包括\0),strlen("Hello")返回5。
4.3 结构体
结构体是用户自定义的数据类型,可以包含不同类型的数据。
struct Student {
char name[50];
int age;
float gpa;
};
int main() {
struct Student s1 = {"Tom", 20, 3.5};
printf("Name: %s, Age: %d, GPA: %.2f\n", s1.name, s1.age, s1.gpa);
return 0;
}
结构体小剧场:结构体就像"多功能盒子",可以把不同类型的数据装在一起。
4.4 共用体
共用体允许在同一内存位置存储不同类型的数据。
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
data.i = 10;
printf("data.i: %d\n", data.i);
data.f = 220.5;
printf("data.f: %f\n", data.f);
strcpy(data.str, "C Programming");
printf("data.str: %s\n", data.str);
return 0;
}
共用体小提示:共用体中的成员共享同一块内存,所以一次只能存储一个成员的值。
4.5 枚举
枚举是自定义的整型常量集合。
enum Weekday { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday };
int main() {
enum Weekday today = Wednesday;
printf("Today is %d\n", today); // 输出: 2
return 0;
}
枚举小知识:枚举的值从0开始,依次递增。你可以指定值,如
enum Color { Red = 1, Green = 2, Blue = 3 };。
第五章 函数
5.1 函数的使用
5.1.1 定义函数
return_type function_name(parameter_list) {
// 函数体
}
小提示:
return_type可以是void(无返回值)。
5.1.2 函数声明
return_type function_name(parameter_list);
为什么需要声明:告诉编译器函数的参数和返回类型,避免编译错误。
5.1.3 调用函数
int result = max(10, 20); // 调用函数
5.2 参数传递
5.2.1 传值调用
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
注意:传值调用不会修改实际参数,因为传递的是副本。
5.2.2 引用调用
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
小技巧:通过指针传递,可以修改实际参数。
5.3 函数的递归
递归是函数调用自身。
int factorial(int n) {
if (n == 0)
return 1;
else
return n * factorial(n - 1);
}
递归小提示:递归要有终止条件,否则会无限递归导致栈溢出。
第六章 指针
6.1 指针变量
指针是存储内存地址的变量。
int a = 10;
int *p = &a; // p 指向 a 的地址
printf("a 的值: %d\n", a);
printf("a 的地址: %p\n", &a);
printf("p 的值: %p\n", p);
printf("p 指向的值: %d\n", *p);
指针小剧场:指针就像"快递员",它不直接给你东西,但告诉你东西在哪里。
6.2 NULL 指针
int *p = NULL; // 空指针
重要提示:不要解引用空指针,否则会导致程序崩溃。
6.3 指针详解
6.3.1 指针的算术运算
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // p 指向 arr[0]
printf("%d\n", *p); // 1
printf("%d\n", *(p + 1)); // 2
指针小知识:指针加1,地址增加的是该类型大小(如
int是4字节)。
6.4 通过指针引用数组
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
printf("%d\n", *(p + 2)); // 3
数组和指针:数组名是数组首元素的地址,所以
arr等同于&arr[0]。
第七章 动态内存和链表
7.1 动态内存
7.1.1 malloc()
int *p = (int *)malloc(5 * sizeof(int));
if (p == NULL) {
printf("内存分配失败\n");
exit(1);
}
小提示:
malloc返回分配的内存地址,需要强制转换类型。
7.1.2 calloc()
int *p = (int *)calloc(5, sizeof(int));
区别:
calloc会初始化为0。
7.1.3 realloc()
p = (int *)realloc(p, 10 * sizeof(int));
用途:重新分配内存大小。
7.1.4 free()
free(p);
p = NULL; // 避免野指针
重要提示:释放内存后,最好将指针设为
NULL。
7.2 链表
7.2.1 静态链表
(代码示例已修复,更清晰)
7.2.2 动态链表
typedef struct Node {
int data;
struct Node *next;
} Node;
Node *createList(int n) {
Node *head = NULL;
Node *current = NULL;
for (int i = 0; i < n; i++) {
Node *newNode = (Node *)malloc(sizeof(Node));
newNode->data = i;
newNode->next = NULL;
if (head == NULL) {
head = newNode;
current = newNode;
} else {
current->next = newNode;
current = newNode;
}
}
return head;
}
链表小剧场:链表就像"火车",每个节点(车厢)包含数据和指向下一个节点的指针。
第八章 对文件的输入输出
8.1 C文件的有关知识
8.1.1 什么是文件
- 程序文件:源代码文件,如
.c、.h - 数据文件:存储数据的文件,如
.txt、.dat
8.1.2 文件类型指针
FILE *fp;
小提示:
FILE是C标准库定义的结构体类型。
8.2 打开与关闭文件
8.2.1 fopen()
FILE *fp = fopen("file.txt", "r"); // 以只读方式打开文件
文件模式:
r:只读w:写入(覆盖)a:追加r+:读写w+:读写(覆盖)
8.2.2 fclose()
fclose(fp);
重要提示:使用完文件后一定要关闭,否则可能导致数据丢失。
8.3 顺序读写数据文件
8.3.1 读写字符
char c = getc(fp); // 读取一个字符
putc(c, fp); // 写入一个字符
8.3.2 读写字符串
char str[100];
fgets(str, 100, fp); // 读取一行
fputs(str, fp); // 写入字符串
小提示:
fgets比gets安全,因为可以指定缓冲区大小。
🎉 结语
C语言是编程世界的基石,掌握它就像掌握了魔法。虽然它看起来有点严肃,但只要你用心去学,你会发现它其实很有趣!
最后的小幽默:C语言的编译器就像个"严格的老教师",你写错一个分号,它就会给你一个"编译错误"的"大嘴巴"。但别怕,错误是进步的阶梯,每次编译错误都是你成长的机会!
现在,拿起你的键盘,开始你的C语言之旅吧! 💻✨
💬 评论
评论系统接入中...