一、预处理指令
预处理指令在编译前执行,除了#include
,还有以下常用指令:
1. #define
宏定义
无参宏:定义常量或代码片段,编译时直接替换(无类型检查)。
#define PI 3.1415926 // 定义常量
#define MAX(a,b) (a > b ? a : b) // 定义简单逻辑(注意括号,避免运算优先级问题)
注意:宏替换是 “文本替换”,可能导致副作用,例如
MAX(a++, b++)
会导致a
或b
多自增一次。带参宏与函数的区别:
特性 带参宏 函数 执行时机 编译前替换 运行时调用 开销 无调用开销(代码膨胀) 有栈帧开销 类型检查 无 有 返回值 无(替换结果直接使用) 有明确返回值类型
2. 条件编译
用于控制代码是否参与编译,常用于跨平台开发或调试。
#define DEBUG 1 // 定义宏DEBUG#ifdef DEBUG // 如果定义了DEBUG,则编译以下代码printf("调试信息:变量x的值为%d\n", x);
#else // 否则编译以下代码// 无调试信息
#endif#ifndef _HEADER_H // 如果未定义_HEADER_H(防止头文件重复包含)
#define _HEADER_H
// 头文件内容
#endif
二、数据类型进阶
1. 类型修饰符
short
/long
:修饰整数长度(short int
通常 2 字节,long int
通常 4/8 字节,取决于系统)。signed
/unsigned
:signed int
可表示正负(默认),unsigned int
只表示非负(范围更大)。
unsigned int num = 10; // 范围:0 ~ 4294967295(32位系统)
signed char c = -12; // 范围:-128 ~ 127(8位)
2. 自定义类型
结构体(
struct
):组合不同类型数据,用于描述复杂对象(如学生、坐标)。
// 定义结构体类型,此时的student是一种自定义的新数据类型,像int一样
struct Student {char name[20]; // 姓名int age; // 年龄float score; // 成绩
};int main() {// 声明结构体变量并初始化struct Student stu = {"张三", 18, 90.5f};// 访问成员(用.运算符)printf("姓名:%s,年龄:%d\n", stu.name, stu.age);// 结构体指针(用->运算符访问成员)struct Student *p = &stu;p->score = 95.0f; // 等价于 (*p).score = 95.0freturn 0;
}
- 枚举(
enum
):定义命名的整数常量,提高代码可读性。
enum Weekday {MON, // 默认为0TUE, // 1WED=5, // 显式赋值5THU // 6(自动递增)
};int main() {enum Weekday day = WED;printf("%d\n", day); // 输出5return 0;
}
- 共用体(
union
):所有成员共享同一块内存(大小为最大成员的大小),用于节省空间。
union Data {int i;float f;char c;
}; // 大小为4字节(float和int通常4字节)int main() {union Data d;d.i = 10;printf("d.i = %d, d.f = %f\n", d.i, d.f); // f的值会混乱(内存被i覆盖)return 0;
}
3. 结构体的高级用法
- 结构体嵌套与自引用(链表基础):结构体可嵌套其他结构体,自引用(包含自身类型的指针)是实现链表、树等数据结构的核心。
示例:单向链表节点
#include <stdio.h>
#include <stdlib.h>// 结构体自引用(必须用指针,否则会无限递归定义)
struct Node {int data; // 数据域struct Node *next; // 指针域:指向 next 节点
};// 创建新节点
struct Node* createNode(int data) {struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));newNode->data = data;newNode->next = NULL; // 初始指向NULLreturn newNode;
}int main() {// 创建3个节点并链接struct Node *n1 = createNode(10);struct Node *n2 = createNode(20);struct Node *n3 = createNode(30);n1->next = n2; // n1 指向 n2n2->next = n3; // n2 指向 n3// 遍历链表struct Node *current = n1;while (current != NULL) {printf("%d ", current->data); // 输出:10 20 30current = current->next;}// 释放链表内存(从首节点依次释放)current = n1;while (current != NULL) {struct Node *temp = current;current = current->next;free(temp);}return 0;
}
三、函数与指针深入
1. 函数参数与返回值
传值调用:函数接收参数的副本,修改副本不影响原变量。
void swap(int a, int b) {int temp = a;a = b;b = temp; // 仅修改副本,原变量不变
}
- 传址调用:通过指针传递变量地址,函数可修改原变量。
void swap(int *a, int *b) {int temp = *a;*a = *b;*b = temp; // 直接修改原变量
}int main() {int x=3, y=5;swap(&x, &y); // 传递地址printf("x=%d, y=%d\n", x, y); // 输出x=5, y=3return 0;
}
- 指针函数(返回指针的函数):返回值为指针(需注意:不能返回局部变量的地址,因其生命周期随函数结束而结束)。
int* getStaticPtr() {static int num = 10; // static变量生命周期为整个程序,地址有效return #
}
- 函数指针(指向函数的指针):函数指针存储函数的地址,可实现 “回调函数”(将函数作为参数传递),是 C 语言实现多态的重要方式。
#include <stdio.h>// 加法函数
int add(int a, int b) { return a + b; }// 乘法函数
int multiply(int a, int b) { return a * b; }// 函数指针作为参数(回调函数)
int calculate(int a, int b, int (*func)(int, int)) {return func(a, b); // 调用传入的函数
}int main() {int x=3, y=4;// 用函数指针调用不同函数,实现不同逻辑printf("加法结果:%d\n", calculate(x, y, add)); // 7printf("乘法结果:%d\n", calculate(x, y, multiply)); // 12return 0;
}
2. 函数递归
函数自身调用自身,需满足终止条件和递归关系(如阶乘、斐波那契数列)。
// 计算n的阶乘:n! = n * (n-1)!,终止条件n=1时返回1
int factorial(int n) {if (n == 1) return 1; // 终止条件return n * factorial(n-1); // 递归关系
}
注意:递归深度过大会导致栈溢出(栈内存有限),复杂场景建议用循环替代。
3. 指针深入
二级指针(指针的指针):用于处理 “指针的集合”(如动态二维数组、指针数组的修改)。
示例:动态创建二维数组(用二级指针)
#include <stdio.h>
#include <stdlib.h>int main() {int rows = 2, cols = 3;int **arr; // 二级指针:指向int*类型的指针// 第一步:分配存放指针的内存(rows个int*)arr = (int**)malloc(rows * sizeof(int*));// 第二步:为每个指针分配数组内存for (int i=0; i<rows; i++) {arr[i] = (int*)malloc(cols * sizeof(int));}// 赋值arr[0][0] = 1; arr[0][1] = 2; arr[0][2] = 3;arr[1][0] = 4; arr[1][1] = 5; arr[1][2] = 6;// 打印for (int i=0; i<rows; i++) {for (int j=0; j<cols; j++) {printf("%d ", arr[i][j]);}printf("\n");}// 释放内存(先释放内层,再释放外层)for (int i=0; i<rows; i++) {free(arr[i]);}free(arr);return 0;
}
- const 修饰的指针:区分 “指向 const 的指针” 和 “const 指针”,避免意外修改数据。
#include <stdio.h>int main() {int a = 10, b = 20;// 1. 指向const的指针(不能通过指针修改指向的值,但指针可指向其他地址)const int *p1 = &a;// *p1 = 30; // 错误:不能修改指向的值p1 = &b; // 正确:可指向其他地址// 2. const指针(指针本身不能修改指向,但可修改指向的值)int *const p2 = &a;*p2 = 30; // 正确:可修改指向的值// p2 = &b; // 错误:指针本身是const,不能改指向// 3. 指向const的const指针(既不能改指向,也不能改值)const int *const p3 = &a;return 0;
}
四、数组与指针深入
1. 数组与指针的关系
数组名本质是首元素地址(常量指针,不可修改),可通过指针访问数组元素。
int arr[5] = {1,2,3,4,5};
int *p = arr; // 等价于 p = &arr[0]// 访问arr[2]的三种方式
printf("%d\n", arr[2]); // 数组下标
printf("%d\n", *(arr+2)); // 数组名+偏移量
printf("%d\n", *(p+2)); // 指针+偏移量
2. 字符数组与字符串
字符串是以'\0'
(ASCII 值 0)结尾的字符数组,printf
和strlen
等函数通过'\0'
判断结束。
char str1[] = "hello"; // 自动添加'\0',长度为6(h e l l o \0)
char str2[] = {'h','i','\0'}; // 必须显式添加'\0',否则不是字符串// 字符串处理函数(需包含<string.h>)
printf("长度:%d\n", strlen(str1)); // 输出5(不包含'\0')
char dest[20];
strcpy(dest, str1); // 复制字符串(注意dest容量足够)
strcat(dest, " world"); // 拼接字符串
3. 指针数组与数组指针
指针数组:数组元素是指针(如存储多个字符串的地址)。
char *strs[] = {"apple", "banana", "cherry"}; // 每个元素是字符串常量的地址
for (int i=0; i<3; i++) {printf("%s\n", strs[i]); // 输出三个字符串
}
- 数组指针:指向整个数组的指针(需指定数组长度)。
int arr[3] = {1,2,3};
int (*p)[3] = &arr; // p指向包含3个int的数组
printf("%d\n", (*p)[1]); // 输出2(访问数组第2个元素)
也就是说,指针可以指向数组的某个元素,也可以指向整个数组,也可以作为元素本身构成数组。当指针直接指向数组名的时候,存储的实际上是数组的第一个元素的地址。
4. 多维数组的指针访问
二维数组在内存中是连续存储的(按行优先排列),可通过指针灵活访问,而不仅限于arr[i][j]
的形式。
示例:用指针遍历二维数组
#include <stdio.h>int main() {int arr[2][3] = {{1,2,3}, {4,5,6}};int *p = &arr[0][0]; // 指向首元素的指针(或直接用 int *p = arr[0];)// 遍历整个二维数组(共2×3=6个元素)for (int i=0; i<2*3; i++) {printf("%d ", *(p+i)); // 输出:1 2 3 4 5 6}return 0;
}
- 二维数组名
arr
是数组指针(类型为int (*)[3]
),指向第一行的整个数组,arr+1
会跳过一整行(3 个 int)。 - 易错点:
arr[i][j]
等价于*(*(arr+i)+j)
,需注意指针类型匹配。
五、内存管理
C 语言需手动管理内存,通过stdlib.h
中的函数操作堆内存(堆内存需手动申请和释放,否则内存泄漏)。
1. 动态内存分配
malloc(size)
:分配size
字节的内存,返回void*
(需强制类型转换),失败返回NULL
。calloc(n, size)
:分配n
个size
字节的内存,初始化为 0。realloc(ptr, new_size)
:调整已分配内存的大小,可能移动内存块。
2. 内存释放
free(ptr)
:释放malloc
/calloc
/realloc
分配的内存,释放后ptr
应置为NULL
(避免野指针)。
示例:
#include <stdio.h>
#include <stdlib.h>int main() {// 分配10个int的内存(40字节)int *arr = (int*)malloc(10 * sizeof(int));if (arr == NULL) { // 检查分配是否成功printf("内存分配失败\n");return 1;}// 使用内存for (int i=0; i<10; i++) {arr[i] = i;}// 重新分配为20个int的内存int *new_arr = (int*)realloc(arr, 20 * sizeof(int));if (new_arr != NULL) {arr = new_arr; // 重新分配成功,更新指针}// 释放内存free(arr);arr = NULL; // 避免野指针return 0;
}
六、文件操作
通过stdio.h
中的函数读写文件,核心是文件指针(FILE*
)。
1. 文件打开与关闭
fopen(filename, mode)
:打开文件,mode
为打开模式("r"
读,"w"
写,"a"
追加等),失败返回NULL
。fclose(fp)
:关闭文件,必须调用(否则可能丢失数据)。
2. 文件读写
- 字符级:
fgetc(fp)
(读一个字符)、fputc(c, fp)
(写一个字符)。 - 字符串级:
fgets(buf, size, fp)
(读一行)、fputs(str, fp)
(写字符串)。 - 格式化:
fscanf(fp, format, ...)
、fprintf(fp, format, ...)
。
#include <stdio.h>int main() {// 写文件FILE *fp = fopen("test.txt", "w"); // 以写模式打开if (fp == NULL) {printf("文件打开失败\n");return 1;}fprintf(fp, "Hello, File!\n"); // 写入内容fclose(fp); // 关闭文件// 读文件fp = fopen("test.txt", "r");char buf[100];fgets(buf, 100, fp); // 读取一行printf("读取内容:%s", buf); // 输出Hello, File!fclose(fp);return 0;
}
七、其他的一些名词
- 野指针:访问已释放或未初始化的指针(可能崩溃)。
- 数组越界:访问超出数组长度的元素(可能修改其他内存)。
- 内存泄漏:未用
free
释放堆内存(长期运行的程序会耗尽内存)。