引言
C语言是一门古老而强大的编程语言,它诞生于1972年,至今仍是计算机科学教育的重要基石。无论是操作系统、嵌入式系统,还是游戏开发,C语言都扮演着不可或缺的角色。
在学习C语言的过程中,最深的体会是:C语言是一门"小而美"的语言。它的语法简洁,概念清晰,但正是这种简洁,要求我们必须深入理解每一个细节。今天,我将通过自己的学习笔记,系统地回顾C语言的核心知识点。
第一部分:C语言基础
如需查看详细内容,请进入文章:
一、什么是C语言?
C语言是人与计算机进行交流和沟通的一种语言。它是一门面向过程的、编译型的计算机编程语言,诞生于贝尔实验室。
cpp
#include <stdio.h>
int main() {
printf("Hello World!");
return 0;
}
二、C语言的基本数据类型
| 数据类型 | 格式符 | 大小(字节) | 取值范围 |
|---|---|---|---|
char |
%c |
1 | -128 ~ 127 |
short |
%hd |
2 | -32,768 ~ 32,767 |
int |
%d |
4 | -2,147,483,648 ~ 2,147,483,647 |
long |
%ld |
4(32位)/8(64位) | 取决于系统 |
long long |
%lld |
8 | -2^63 ~ 2^63-1 |
float |
%f |
4 | 约 ±3.4e-38 ~ ±3.4e+38 |
double |
%lf |
8 | 约 ±1.7e-308 ~ ±1.7e+308 |
最基本的数据类型: char、int、float
三、变量的初始化
cpp
int main() {
// 方式1:先定义再赋值
int a;
a = 10;
// 方式2:定义时直接赋值
int b = 10;
// 方式3:从键盘输入
int c;
scanf("%d", &c);
return 0;
}
第二部分:流程控制
C语言循环语句完全指南:for、while、do-while
一、分支语句
| 类型 | 语法 | 适用场景 |
|---|---|---|
| 单分支 | if(条件) { } |
条件满足时执行 |
| 双分支 | if(条件) { } else { } |
二选一 |
| 多分支 | if-else if-else 或 switch |
多条件选择 |
注意事项:
-
条件表达式必须加括号
-
else与最近的if结合(就近原则) -
常量放在左边,变量放在右边(防止误写为赋值)
cpp// 推荐写法:常量在左 if (1 == num) { } // 不推荐:变量在左(容易误写为赋值) if (num = 1) { } // 这是赋值,不是判断!二、循环语句
循环类型 语法 执行次数 适用场景 whilewhile(条件){ }0~N次 条件控制 do-whiledo{ }while(条件);1~N次 至少执行一次 forfor(init;条件;update){ }0~N次 已知循环次数 break与continue的区别: -
break:跳出整个循环 -
continue:跳过本次循环剩余的代码,进入下一次循环cpp// for 循环中可以安全使用 continue for (int i = 0; i < 10; i++) { if (i == 5) continue; // 跳过5 printf("%d ", i); } // 输出:0 1 2 3 4 6 7 8 9第三部分:函数
一、函数的五大件
| 组件 | 说明 |
|---|---|
| 返回类型 | 函数产出的数据类型(int、float、void等) |
| 函数名 | 标识函数,用于调用 |
| 参数 | 传入函数的数据 |
| 函数体 | 实现功能的代码块 |
| 返回值 | 使用 return 语句返回结果 |
二、函数的分类
| 分类 | 语法 | 示例 |
|---|---|---|
| 有返回值有参数 | int func(int a, int b) |
int add(int a, int b) |
| 有返回值无参数 | int func(void) |
int getPI() |
| 无返回值有参数 | void func(int a) |
void print(int n) |
| 无返回值无参数 | void func(void) |
void hello() |
三、函数的声明与定义
cpp
// 先声明后定义
int ADD2(int a, int b); // 函数声明
int main() {
int result = ADD2(10, 20);
return 0;
}
// 函数定义
int ADD2(int a, int b) {
return a + b;
}
第四部分:数组
一、数组的定义
数组是由相同类型元素构成的集合。所有数组的空间在内存中是连续的。
cpp
int main() {
// 一维数组
int arr[10]; // 10个int类型的元素
char brr[10]; // 10个char类型的元素
double crr[10]; // 10个double类型的元素
// 多维数组(数组的数组)
int drr[2][3][4][5]; // 四维数组
return 0;
}
二、数组名的含义
核心规则: 除了 sizeof 和 & 之外,数组名都是首元素的地址。
cpp
int main() {
int arr[10];
// arr 的类型:int*(首元素地址)
// &arr 的类型:int(*)[10](整个数组的地址)
// *arr 的类型:int(首元素的值)
printf("arr = %p\n", arr);
printf("arr + 1 = %p\n", arr + 1); // 偏移4字节
printf("&arr + 1 = %p\n", &arr + 1); // 偏移40字节
return 0;
}
三、复杂数组类型解析
cpp
int main() {
// arr2 的类型:int [2][3][4][5] → int (*)[3][4][5]
int arr2[2][3][4][5];
// arr3 的类型:int** (*[5])[5][10] → int** (**)[5][10]
int** (*arr3[5])[5][10];
// 解析技巧:从变量名开始,先右后左,括号优先
// arr3[5] → 数组 → * → 指针 → [5][10] → 数组 → * → 指针 → int**
return 0;
}
第五部分:指针
一、什么是指针?
指针是一个变量,存储的是另一个变量(或数据)的内存地址。指针本身也是一片空间,可以保存一个字节的编号。
cpp
int main() {
int a = 10;
int* p = &a; // p 保存 a 的地址
printf("a 的地址: %p\n", &a);
printf("p 的值: %p\n", p);
printf("*p 的值: %d\n", *p); // 解引用:10
return 0;
}
二、指针的运算规则
| 运算 | 含义 | 示例 |
|---|---|---|
p + n |
向后移动 n 个元素 | p + 1 跳过 sizeof(*p) 字节 |
p - n |
向前移动 n 个元素 | - |
p1 - p2 |
两个指针之间的元素个数 | 结果类型为 ptrdiff_t |
*p |
解引用,获取指针指向的值 | 从当前地址取 sizeof(*p) 字节 |
cpp
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;
printf("p = %p\n", p);
printf("p + 1 = %p\n", p + 1); // 偏移4字节
printf("*(p + 2) = %d\n", *(p + 2)); // 3
int* p1 = &arr[1];
int* p2 = &arr[4];
printf("p2 - p1 = %td\n", p2 - p1); // 3(元素个数)
return 0;
}
第六部分:结构体
一、什么是结构体?
结构体是不同类型元素构成的集合。数组要求所有元素类型相同,而结构体允许不同类型的成员。
cpp
typedef struct jjb {
int arr[10];
char brr[5];
struct jjb* p;
double e;
} bjj;
int main() {
// 方式1:定义时初始化
bjj h = { {1,2,3,4,5,6,7,8,9,10}, {'a','b','c','d','e'}, NULL, 1.6 };
// 方式2:逐个赋值
bjj h2;
for (int i = 0; i < 10; i++) h2.arr[i] = i;
strcpy(h2.brr, "asd");
h2.e = 3.14;
h2.p = NULL;
return 0;
}
二、结构体传参(重要)
结构体传参时,优先使用地址传递,而不是值传递,以避免拷贝整个结构体。
cpp
typedef struct a {
char a;
int b;
float c;
} s;
// ✅ 正确:传地址(只拷贝指针,效率高)
void nc(s* p) {
p->a = '8';
p->b = 4;
p->c = 6.6;
}
// ❌ 错误:传值(拷贝整个结构体,效率低)
void nc_wrong(s p) {
p.a = '8';
p.b = 4;
p.c = 6.6; // 修改的是副本,不影响原对象
}
int main() {
s n;
nc(&n); // 传地址
return 0;
}
三、结构体中的动态内存管理
cpp
typedef struct student {
int* arr;
int length;
} student;
void init(student* u) {
assert(u != NULL);
int* p = (int*)malloc(sizeof(int) * 10);
if (p == NULL) return;
u->arr = p;
u->length = 0;
}
int main() {
student z;
init(&z);
// ... 使用 z.arr
free(z.arr); // 记得释放
return 0;
}
第七部分:字符串与内存函数
一、字符串的本质
字符串是由相同类型的字符构成的集合,并在末尾添加一个 '\0' 作为结束标志。
cpp
// 字符数组(不一定以\0结尾)
char arr1[] = {'a', 'b', 'c'};
// 字符串(自动添加\0)
char arr2[] = "abc"; // 实际存储:'a','b','c','\0'
二、模拟实现 strlen
cpp
size_t my_strlen(const char* arr) {
size_t size = 0;
while (*arr++) {
size++;
}
return size;
}
三、模拟实现 strcpy
cpp
void my_strcpy(char* dest, const char* src) {
while (*dest++ = *src++);
}
四、模拟实现 memmove
memmove 用于内存拷贝,可以处理源和目标内存重叠的情况。
cpp
void my_memmove(void* dest, const void* src, size_t num) {
if (dest == NULL || src == NULL) return;
char* d = (char*)dest;
const char* s = (const char*)src;
// 如果目标地址在源地址之后,从后往前拷贝
if (d > s && d < s + num) {
while (num--) {
d[num] = s[num];
}
} else {
// 否则从前往后拷贝
while (num--) {
*d++ = *s++;
}
}
}
第八部分:动态内存管理
一、三大动态内存分配函数
| 函数 | 功能 | 初始化 | 返回值 |
|---|---|---|---|
malloc(size) |
分配指定字节数的内存 | 不初始化 | void* |
calloc(n, size) |
分配 n 个 size 字节的内存 | 初始化为 0 | void* |
realloc(ptr, new_size) |
调整已分配内存的大小 | 保留原数据 | void* |
二、使用示例
cpp
int main() {
// malloc:分配10个int的空间
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
printf("内存分配失败\n");
return 1;
}
// 使用...
// 释放内存
free(p);
p = NULL; // 释放后置空,防止野指针
return 0;
}
三、realloc 注意事项
cpp
// ❌ 危险:直接对原指针扩容
int* p = (int*)malloc(40);
p = (int*)realloc(p, 80); // 如果 realloc 失败,p 变为 NULL,原内存无法释放
// ✅ 安全:使用临时变量
int* p = (int*)malloc(40);
int* tmp = (int*)realloc(p, 80);
if (tmp != NULL) {
p = tmp;
} else {
// 处理分配失败,p 仍指向原内存
free(p);
}
第九部分:线性表的动态实现
一、动态数组的插入操作
cpp
#include <stdio.h>
#include <string.h>
// 尾插
void insert_back(int arr[], int val, int* length) {
arr[*length] = val;
(*length)++;
}
// 头插
void insert_front(int arr[], int val, int* length) {
memmove(arr + 1, arr, (*length) * sizeof(int));
arr[0] = val;
(*length)++;
}
// 指定位置插入
void insert_at(int arr[], int pos, int val, int* length) {
int newpos = pos - 1;
memmove(arr + newpos + 1, arr + newpos, (*length - newpos) * sizeof(int));
arr[newpos] = val;
(*length)++;
}
void print(int arr[], int length) {
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int arr[1000] = {1, 2, 3, 4, 5};
int length = 5;
insert_back(arr, 6, &length);
insert_back(arr, 7, &length);
insert_back(arr, 8, &length);
insert_back(arr, 9, &length);
insert_back(arr, 10, &length);
// 数组:1 2 3 4 5 6 7 8 9 10
insert_front(arr, 0, &length);
// 数组:0 1 2 3 4 5 6 7 8 9 10
insert_at(arr, 3, 100, &length);
// 数组:0 1 100 2 3 4 5 6 7 8 9 10
print(arr, length);
return 0;
}
总结
一、C语言核心知识点速查
| 知识点 | 核心内容 |
|---|---|
| 数据类型 | char(1)、int(4)、float(4)、double(8) |
| 分支语句 | if-else、switch |
| 循环语句 | while、do-while、for |
| break/continue | break跳出循环,continue跳过本次 |
| 函数 | 返回类型、函数名、参数、函数体、返回值 |
| 数组 | 相同类型元素的连续集合 |
| 数组名 | 除sizeof和&外,都是首元素地址 |
| 指针 | 存储地址的变量 |
| 结构体 | 不同类型元素的集合,传参用地址 |
| 字符串 | 以'\0'结尾的字符数组 |
| 动态内存 | malloc/calloc/realloc/free |
二、常见警告
| 警告 | 说明 |
|---|---|
| 数组名与&数组名 | 类型不同,数值相同 |
| 结构体传值 | 大结构体用传值效率低 |
| realloc直接赋值 | 失败会导致内存泄漏 |
| free后未置空 | 产生野指针 |
C语言的学习是一个循序渐进的过程。从最基础的数据类型,到变量的作用域,到指针和结构体,再到动态内存管理,这些都是构建编程能力的重要基石。
学习建议:
-
理解数组名的深层含义(sizeof、&、其他情况)
-
掌握指针运算的规则(p+n、指针相减、解引用)
-
结构体传参优先使用地址
-
动态内存分配后必须检查返回值,释放后必须置空