当前位置: 首页 > news >正文

从零开始的C++学习生活 18:C语言复习课(期末速通)

个人主页:Yupureki-CSDN博客

C++专栏:C++_Yupureki的博客-CSDN博客

目录

前言

1.C语言基础

1.1 数据类型

1.1.1 整型家族

1.1.2 浮点型

1.1.3 查看类型范围

1.1.4 自定义类型

1.2 变量和常量

1.3 运算符

关键概念与注意事项

2. 控制结构

2.1 条件语句

2.2 循环语句

3. 函数

3.1 函数定义和调用

函数声明(原型)

函数定义

函数参数传递:值传递

函数调用

4. 数组和字符串(重要)

4.1 数组基础

4.2 二维数组

4.3 字符串

5. 指针(重要)

5.1 指针基础

5.2 指针高级应用

5.2.1 指针作为函数参数

5.2.2 指针的指针(二级指针)

5.2.3 指针数组

5.2.4 数组指针

5.2.5 函数指针

5.3 数组与指针(重要)

5.3.1 数组名

5.3.2 数组和指针笔试题解析

一维数组

二维数组

6. 字符函数和字符串函数(重要)

6.1 strlen - 字符串长度计算

示例

模拟实现

6.2 strcpy - 字符串复制

示例

模拟实现

6.3 strcmp - 字符串比较

示例

模拟实现

6.4 strstr - 字符串查找

示例

模拟实现

7. 结构体和联合体

7.1 结构体

7.1.1 结构的声明

7.1.2 结构体函数

7.1.3 结构体指针

7.1.4 结构体数组

7.2 结构体内存对齐(重要)

7.2.1 为什么需要内存对齐?

7.2.2 结构体内存对齐规则

基本规则

示例分析

示例1:基础结构体

内存布局分析:

示例2:调整成员顺序优化

优化后的内存布局:

复杂结构体示例

示例4:嵌套结构体

内存分析:

7.3 联合体

7.3.1 判断大小端

7.4 枚举

8. 内存函数(重要)

8.1 内存分配函数

8.1.1 malloc - 分配未初始化的内存

8.1.2 calloc - 分配并初始化为0的内存

8.1.3 realloc - 重新分配内存

8.1.4 释放内存

8.2 memcpy和memmove

8.2.1 函数原型和基本概念

函数原型

基本功能

8.2.2 关键区别:内存重叠处理

8.2.3 内存重叠示例

8.2.4 模拟实现

memcpy的模拟实现

memmove的模拟实现

9. 文件操作

9.1 文件的打开和关闭

9.1.1流和标准流

9.1.2标准流

9.1.3 文件指针

写入文件

读取文件

关闭文件

9.2 二进制文件操作

10.编译与链接

10.1编译过程的四个主要阶段

10.2  预处理阶段 (Preprocessing)

10.2.1 预处理的主要工作

头文件包含 (#include)

宏定义和展开 (#define)

条件编译 (#if, #ifdef, #ifndef)

其他预处理指令

10.3 编译阶段 (Compilation)

10.3.1 编译过程详解

10.4 汇编阶段 (Assembly)

10.4.1 目标文件的结构

10.5 链接阶段 (Linking)

链接过程详解

10.5.1 创建多个源文件示例

10.5.2 分步编译链接过程

10.5.3 符号解析和重定位

10.6. 预处理中的重要细节

10.6.1防止头文件重复包含

10.6.2 宏使用的注意事项

10.6.3 预定义宏


上一篇:从零开始的C++学习生活 17:异常和智能指针-CSDN博客

前言

C++的知识学习告一段落,但是复习是不可或缺的。其中C语言更是基础中的基础,无论是准备面试、考试,还是希望夯实编程基础,系统性地复习C语言都是非常有价值的。

我将按照C语言的知识体系,从基础语法到高级特性,全面梳理核心知识点,帮助你构建完整的C语言知识框架。每个部分都配有实用的代码示例和注意事项,让复习更加高效。

1.C语言基础

1.1 数据类型

C语言提供了丰富的数据类型来描述生活中的各种数据。 使用整型类型来描述整数,使用字符类型来描述字符,使用浮点型类型来描述小数。 所谓“类型”,就是相似的数据所拥有的共同特征,编译器只有知道了数据的类型,才知道怎么操作数据。

1.1.1 整型家族

char c = 'A';                    // 1字节,-128到127
unsigned char uc = 255;          // 1字节,0到255
short s = 32767;                 // 2字节,-32768到32767  
int i = 2147483647;              // 4字节,-2147483648到2147483647
long l = 2147483647;             // 4或8字节
long long ll = 9223372036854775807; // 8字节

1.1.2 浮点型

float f = 3.14159f;              // 4字节,约6-7位有效数字
double d = 3.141592653589793;    // 8字节,约15-16位有效数字
long double ld = 3.14159265358979323846L; // 10或16字节

1.1.3 查看类型范围

printf("int范围: %d 到 %d\n", INT_MIN, INT_MAX);
printf("float精度: %d 位\n", FLT_DIG);

1.1.4 自定义类型

struct Date{int _year;int _month;int _day;
}

重要提示

  • 注意有符号和无符号类型的区别。有符号包括我们常见的正负数,而无符号只有正数
  • 浮点数比较时避免直接使用==,因为往往计算的精度不同
  • 了解不同平台的类型大小差异

1.2 变量和常量

变量由数据类型和变量名组成

其中常量指具有常属性的变量,这些变量无法被更改。用define定义或者const修饰的变量为常变量

#include <stdio.h>// 宏定义常量
#define PI 3.14159
#define MAX_SIZE 100// const常量
const int MIN_SIZE = 10;int main() {// 变量声明和初始化int count = 0;float price = 99.99;char grade = 'A';// 不同类型常量const int days_in_week = 7;enum boolean { FALSE, TRUE };enum boolean status = TRUE;// 变量作用域演示{int local_var = 42;  // 块作用域printf("局部变量: %d\n", local_var);}// printf("%d\n", local_var);  // 错误:local_var不可见return 0;
}

1.3 运算符

优先级类别运算符名称 / 含义结合性用法示例
1括号、结构体()
[]
.
->
函数调用
数组下标
结构体成员访问
结构体指针成员访问
从左到右func()
arr[5]
obj.member
ptr->member
2单目运算符!
~
++
--
+
-
*
&
(type)
sizeof
逻辑非
按位取反
自增
自减
正号
负号
解引用
取地址
强制类型转换
求类型大小
从右到左!flag
~a
++i / i++
--i / i--
+5
-10
*ptr
&var
(float)i
sizeof(int)
3算术运算符*
/
%
乘法
除法
取模(求余)
从左到右a * b
a / b
a % b
4算术运算符+
-
加法
减法
从左到右a + b
a - b
5位运算符<<
>>
左移
右移
从左到右a << 2
a >> 1
6关系运算符<
<=
>
>=
小于
小于等于
大于
大于等于
从左到右a < b
a <= b
a > b
a >= b
7关系运算符==
!=
等于
不等于
从左到右a == b
a != b
8位运算符&按位与从左到右a & b
9位运算符^按位异或从左到右a ^ b
10位运算符|按位或从左到右a | b
11逻辑运算符&&逻辑与从左到右a && b
12逻辑运算符||逻辑或从左到右a || b
13条件运算符? :三目条件运算符从右到左a > b ? a : b
14赋值运算符=
+= -=
*= /= %=
&= ^= |=
<<= >>=
赋值
复合赋值
从右到左a = 5
a += 3 (即 a = a + 3)
15逗号运算符,逗号(顺序求值)从左到右a=1, b=2

关键概念与注意事项

  1. 优先级 (Precedence)

    • 当表达式中出现多个运算符时,优先级决定了谁先计算。例如,乘除 (*/) 的优先级高于加减 (+-),所以 a + b * c 等价于 a + (b * c)

  2. 结合性 (Associativity)

    • 当相邻运算符的优先级相同时,结合性决定了计算顺序。

    • 从左到右:大部分运算符如此,如 a + b + c 等价于 (a + b) + c

    • 从右到左:单目、赋值和三目运算符如此。如 a = b = c 等价于 a = (b = c)

  3. 单目运算符的“前缀”与“后缀”

    • ++ 和 -- 作为前缀和后缀时,优先级不同

    • 后缀 (i++i--):优先级为 1,与函数调用同级。

    • 前缀 (++i--i):优先级为 2,与其他单目运算符同级。

    • 示例*ptr++ 等价于 *(ptr++),因为后缀++优先级高于解引用*。而 *++ptr 等价于 *(++ptr)

  4. “短路求值 (Short-Circuit Evaluation)”

    • 逻辑与 (&&) 和逻辑或 (||) 具有此特性。

    • 对于 a && b,如果 a 为假,则整个表达式结果已确定为假,不再计算 b

    • 对于 a || b,如果 a 为真,则整个表达式结果已确定为真,不再计算 b

  5. 运算符的“重载”

    • 某些符号根据上下文有不同的含义。

    • *:乘法 (a * b) 或 解引用 (*ptr)。

    • &:按位与 (a & b) 或 取地址 (&var)。

    • -:减法 (a - b) 或 负号 (-5)。

2. 控制结构

2.1 条件语句

C语言提供if-else语句和switch语句来控制代码的执行

#include <stdio.h>int main() {int score = 85;// if-else语句if (score >= 90) {printf("优秀\n");} else if (score >= 80) {printf("良好\n");} else if (score >= 60) {printf("及格\n");} else {printf("不及格\n");}// switch语句char grade = 'B';switch (grade) {case 'A':printf("优秀\n");break;case 'B':printf("良好\n");break;case 'C':printf("及格\n");break;default:printf("不及格\n");}// 条件运算符(三元运算符)int max = (a > b) ? a : b;printf("较大值: %d\n", max);return 0;
}

2.2 循环语句

C语言提供while,do-while和for语句来使一段代码循环执行

#include <stdio.h>int main() {int i;// for循环printf("for循环: ");for (i = 0; i < 5; i++) {printf("%d ", i);}printf("\n");// while循环printf("while循环: ");i = 0;while (i < 5) {printf("%d ", i);i++;}printf("\n");// do-while循环printf("do-while循环: ");i = 0;do {printf("%d ", i);i++;} while (i < 5);printf("\n");// 循环控制:break和continueprintf("break示例: ");for (i = 0; i < 10; i++) {if (i == 5) break;  // 跳出循环printf("%d ", i);}printf("\n");printf("continue示例: ");for (i = 0; i < 5; i++) {if (i == 2) continue;  // 跳过本次循环printf("%d ", i);}printf("\n");return 0;
}

3. 函数

3.1 函数定义和调用

我们日常所执行的程序都在函数内执行。同时函数之间也能相互调用

函数包括返回值,函数名,参数列表和函数体

函数声明(原型)

int add(int a, int b);
void print_hello();
int factorial(int n);

函数定义

int add(int a, int b) {return a + b;
}

函数参数传递:值传递

void swap_wrong(int a, int b) {int temp = a;a = b;b = temp;printf("函数内: a=%d, b=%d\n", a, b);
}

函数调用

int result = add(5, 3);
printf("5 + 3 = %d\n", result);

4. 数组和字符串(重要)

4.1 数组基础

数组是⼀组相同类型元素的集合
在内存中,数组是连续开辟的一段空间,因此数组中的每个元素都是紧挨着的
int numbers[] = {5, 2, 8, 1, 9};

在数组中,下标并不等同于我们所认定的序号,第一个元素下标为0,以此开始

4.2 二维数组

其实二维数组访问也是使用下标的形式的,二维数组是有行和列的,只要锁定了行和列就能唯一锁定数组中的一个元素。
int arr[3][5] = {1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7};

4.3 字符串

其实可以把字符串看作是一种数组,但是对于对于字符串的访问可以直接通过数组首地址来访问,直到遇到尾部的\0停止,因此\0必须存在于字符串中

对于字符数组,末尾添加\0也是必要的

#include <stdio.h>
#include <string.h>int main() {// 字符串初始化char str1[] = "Hello";           // 自动包含'\0'char str2[10] = "World";char str3[] = {'H', 'i', '\0'};  // 手动添加'\0'// 字符串操作printf("str1: %s\n", str1);printf("str1长度: %lu\n", strlen(str1));// 字符串复制char copy[10];strcpy(copy, str1);printf("复制后: %s\n", copy);// 字符串连接strcat(copy, " ");strcat(copy, str2);printf("连接后: %s\n", copy);// 字符串比较printf("比较str1和str2: %d\n", strcmp(str1, str2));// 字符串查找char *pos = strchr(str1, 'l');if (pos) {printf("找到字符'l'在位置: %ld\n", pos - str1);}// 安全的字符串函数(C11)char buffer[10];strncpy(buffer, "Hello World", sizeof(buffer) - 1);buffer[sizeof(buffer) - 1] = '\0';  // 确保以'\0'结尾printf("安全复制: %s\n", buffer);return 0;
}

5. 指针(重要)

5.1 指针基础

指针严格来说是指针变量,有自己的类型和值,在内存中开辟了空间

而指针变量的值就是变量的地址,即内存编号

#include <stdio.h>int main() {int num = 42;int *ptr = &num;  // ptr指向num的地址printf("变量值: %d\n", num);printf("变量地址: %p\n", &num);printf("指针值: %p\n", ptr);printf("指针指向的值: %d\n", *ptr);  // 解引用// 指针运算int arr[] = {10, 20, 30, 40, 50};int *arr_ptr = arr;printf("\n数组元素:\n");for (int i = 0; i < 5; i++) {printf("arr[%d] = %d, *(arr_ptr + %d) = %d\n", i, arr[i], i, *(arr_ptr + i));}// 指针和数组的关系printf("\n指针和数组:\n");printf("arr = %p\n", arr);printf("&arr[0] = %p\n", &arr[0]);printf("arr_ptr = %p\n", arr_ptr);return 0;
}

5.2 指针高级应用

5.2.1 指针作为函数参数

函数参数传递的是形参,在函数体内对形参的改变不会影响外部的实参。因此如果要改变实参,就得传递指针

void modify_value(int *ptr) {*ptr = 100;  // 修改指针指向的值
}

5.2.2 指针的指针(二级指针)

我们说过,指针也是变量,因此也可以创建指向指针的指针,俗称二级指针

二级指针可以改变一级指针指向哪个元素

void pointer_to_pointer_demo() {int value = 42;int *ptr = &value;int **pptr = &ptr;printf("\n指向指针的指针:\n");printf("value = %d\n", value);printf("*ptr = %d\n", *ptr);printf("**pptr = %d\n", **pptr);
}

5.2.3 指针数组

指针数组指的是一个数组中存的的元素每个都是指针

由于[]的优先级比*高,因此ptr_arr被优先为数组

void pointer_array_demo() {int a = 1, b = 2, c = 3;int *ptr_arr[] = {&a, &b, &c};printf("指针数组:\n");for (int i = 0; i < 3; i++) {printf("ptr_arr[%d] = %p, *ptr_arr[%d] = %d\n", i, ptr_arr[i], i, *ptr_arr[i]);}
}

5.2.4 数组指针

数组指针是一个指针,指向的是整个数组

int arr[10] = {0}
int (*p)[10] = &arr;

由于p指向的是整个数组,因此p解引用不是arr的首元素,而是整个数组,加1和减1也是跳过一整个数组

5.2.5 函数指针

函数指针指向函数,可以调用函数

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
void function_pointer_demo() {// 函数指针声明int (*operation)(int, int);operation = add;printf("加法: %d\n", operation(5, 3));operation = subtract;printf("减法: %d\n", operation(5, 3));operation = multiply;printf("乘法: %d\n", operation(5, 3));
}

5.3 数组与指针(重要)

5.3.1 数组名

数组名是数组首元素的地址

只有两种情况下数组名不是数组首元素的地址

1. sizeof(数组名)中数组名表示的是整个数组,计算的是整个数组的大小

2.&数组名取出的是整个数组,也就是数组指针

5.3.2 数组和指针笔试题解析

一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));//16
printf("%d\n",sizeof(a+0));//4
printf("%d\n",sizeof(*a));//4
printf("%d\n",sizeof(a+1));//4
printf("%d\n",sizeof(a[1]));//4
printf("%d\n",sizeof(&a));//4
printf("%d\n",sizeof(*&a));//16
printf("%d\n",sizeof(&a+1));//4
printf("%d\n",sizeof(&a[0]));//4
printf("%d\n",sizeof(&a[0]+1));//4

逐行分析

1. printf("%d\n",sizeof(a));

  • a 作为数组名,单独出现在 sizeof 中,表示整个数组

  • 结果16(4个int × 4字节)

2. printf("%d\n",sizeof(a+0));

  • a+0 中,数组名 a 退化为指向首元素的指针(int*

  • a+0 仍然是 int* 类型

  • 结果4(32位系统下指针的大小)

3. printf("%d\n",sizeof(*a));

  • *a 等价于 a[0],即数组的第一个元素

  • 类型是 int

  • 结果4(int类型的大小)

4. printf("%d\n",sizeof(a+1));

  • a+1 中,a 退化为指针,指向第二个元素

  • 类型是 int*

  • 结果4(指针大小)

5. printf("%d\n",sizeof(a[1]));

  • a[1] 是数组的第二个元素

  • 类型是 int

  • 结果4

6. printf("%d\n",sizeof(&a));

  • &a 是取整个数组的地址

  • 类型是 int(*)[4](指向包含4个int的数组的指针)

  • 但仍然是指针,所有数据指针在32位系统都是4字节

  • 结果4

7. printf("%d\n",sizeof(*&a));

  • *&a 先取数组地址再解引用,等价于 a

  • 所以就是整个数组

  • 结果16

8. printf("%d\n",sizeof(&a+1));

  • &a+1 指向数组末尾的下一个位置

  • 类型仍然是 int(*)[4]

  • 结果4(指针大小)

9. printf("%d\n",sizeof(&a[0]));

  • &a[0] 取第一个元素的地址

  • 类型是 int*

  • 结果4

10. printf("%d\n",sizeof(&a[0]+1));

  • &a[0]+1 指向第二个元素

  • 类型是 int*

  • 结果4

二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));//48
printf("%d\n",sizeof(a[0][0]));//4
printf("%d\n",sizeof(a[0]));//16
printf("%d\n",sizeof(a[0]+1));//4
printf("%d\n",sizeof(*(a[0]+1)));//4
printf("%d\n",sizeof(a+1));//4
printf("%d\n",sizeof(*(a+1)));//4
printf("%d\n",sizeof(&a[0]+1));//4
printf("%d\n",sizeof(*(&a[0]+1)));//16
printf("%d\n",sizeof(*a));//16
printf("%d\n",sizeof(a[3]));//16

逐行分析

1. printf("%d\n",sizeof(a));

  • a 作为二维数组名,单独出现在 sizeof 中,表示整个二维数组

  • 结果48(3×4=12个int × 4字节)

2. printf("%d\n",sizeof(a[0][0]));

  • a[0][0] 是第一行第一列的元素

  • 类型是 int

  • 结果4(int类型的大小)

3. printf("%d\n",sizeof(a[0]));

  • a[0] 是第一行的数组名,单独出现在 sizeof 中

  • 类型是 int[4](包含4个int的一维数组)

  • 结果16(4个int × 4字节)

4. printf("%d\n",sizeof(a[0]+1));

  • a[0]+1 中,a[0] 退化为指向第一行第一个元素的指针(int*

  • a[0]+1 指向第一行的第二个元素

  • 类型是 int*

  • 结果4(指针大小)

5. printf("%d\n",sizeof(*(a[0]+1)));

  • *(a[0]+1) 解引用得到第一行第二个元素的值

  • 类型是 int

  • 结果4

6. printf("%d\n",sizeof(a+1));

  • a+1 中,二维数组名 a 退化为指向第一行的指针(int(*)[4]

  • a+1 指向第二行

  • 类型是 int(*)[4]

  • 结果4(指针大小)

7. printf("%d\n",sizeof(*(a+1)));

  • *(a+1) 解引用得到第二行

  • 类型是 int[4](包含4个int的一维数组)

  • 结果16(4个int × 4字节)

8. printf("%d\n",sizeof(&a[0]+1));

  • &a[0] 取第一行的地址,类型是 int(*)[4]

  • &a[0]+1 指向第二行

  • 类型是 int(*)[4]

  • 结果4(指针大小)

9. printf("%d\n",sizeof(*(&a[0]+1)));

  • *(&a[0]+1) 解引用得到第二行

  • 类型是 int[4]

  • 结果16

10. printf("%d\n",sizeof(*a));

  • *a 中,a 退化为指向第一行的指针,解引用得到第一行

  • 类型是 int[4]

  • 结果16

11. printf("%d\n",sizeof(a[3]));

  • a[3] 是访问第四行(数组越界),但 sizeof 在编译时确定类型

  • a[3] 的类型是 int[4]

  • 结果16(编译时根据类型确定,不实际访问内存)

6. 字符函数和字符串函数(重要)

6.1 strlen - 字符串长度计算

#include <string.h>
size_t strlen(const char *str);

strlen用来计算一个字符串的长度,即从头开始直到遇到\0,计算这之间的长度,但是不计入\0的长度

示例

#include <stdio.h>
#include <string.h>int main() {char str[] = "Hello, World!";printf("字符串长度: %zu\n", strlen(str)); // 输出: 13return 0;
}

模拟实现

// 方法1: 计数器方式
size_t my_strlen1(const char *str) {size_t count = 0;while (*str != '\0') {count++;str++;}return count;
}// 方法2: 指针相减方式
size_t my_strlen2(const char *str) {const char *start = str;while (*str != '\0') {str++;}return str - start;
}// 方法3: 递归方式
size_t my_strlen3(const char *str) {if (*str == '\0') {return 0;}return 1 + my_strlen3(str + 1);
}

6.2 strcpy - 字符串复制

#include <string.h>
char *strcpy(char *dest, const char *src);

示例

#include <stdio.h>
#include <string.h>int main() {char src[] = "Hello, World!";char dest[20];strcpy(dest, src);printf("复制后的字符串: %s\n", dest); // 输出: Hello, World!return 0;
}

模拟实现

// 基础版本
char *my_strcpy1(char *dest, const char *src) {char *ret = dest;while (*src != '\0') {*dest = *src;dest++;src++;}*dest = '\0'; // 添加字符串结束符return ret;
}// 简化版本
char *my_strcpy2(char *dest, const char *src) {char *ret = dest;while ((*dest++ = *src++) != '\0') {// 空循环体}return ret;
}// 更简洁版本
char *my_strcpy3(char *dest, const char *src) {char *ret = dest;while (*dest++ = *src++) {// 空循环体}return ret;
}

6.3 strcmp - 字符串比较

#include <string.h>
int strcmp(const char *str1, const char *str2);

返回值:

  • < 0:str1 < str2

  • = 0:str1 = str2

  • > 0:str1 > str2

示例

#include <stdio.h>
#include <string.h>int main() {char str1[] = "apple";char str2[] = "banana";char str3[] = "apple";printf("str1 vs str2: %d\n", strcmp(str1, str2)); // 负数printf("str1 vs str3: %d\n", strcmp(str1, str3)); // 0printf("str2 vs str1: %d\n", strcmp(str2, str1)); // 正数return 0;
}

模拟实现

// 标准实现
int my_strcmp1(const char *str1, const char *str2) {while (*str1 && *str2 && *str1 == *str2) {str1++;str2++;}return *(unsigned char*)str1 - *(unsigned char*)str2;
}// 更易理解的版本
int my_strcmp2(const char *str1, const char *str2) {while (*str1 != '\0' && *str2 != '\0') {if (*str1 != *str2) {return *str1 - *str2;}str1++;str2++;}// 处理一个字符串已结束的情况if (*str1 == '\0' && *str2 == '\0') {return 0;} else if (*str1 == '\0') {return -1;} else {return 1;}
}

6.4 strstr - 字符串查找

#include <string.h>
char *strstr(const char *haystack, const char *needle);

示例

#include <stdio.h>
#include <string.h>int main() {char str[] = "Hello, World! Welcome to C programming.";char substr[] = "World";char *result = strstr(str, substr);if (result != NULL) {printf("找到子串: %s\n", result); // 输出: World! Welcome to C programming.printf("位置: %ld\n", result - str); // 输出: 7} else {printf("未找到子串\n");}return 0;
}

模拟实现

// 暴力匹配算法
char *my_strstr1(const char *haystack, const char *needle) {if (*needle == '\0') {return (char*)haystack;}const char *h = haystack;const char *n = needle;while (*h != '\0') {const char *h_pos = h;n = needle;// 尝试匹配while (*h_pos != '\0' && *n != '\0' && *h_pos == *n) {h_pos++;n++;}// 如果needle完全匹配if (*n == '\0') {return (char*)h;}// 如果haystack已到结尾if (*h_pos == '\0') {return NULL;}h++;}return NULL;
}// 更简洁的版本
char *my_strstr2(const char *haystack, const char *needle) {if (*needle == '\0') return (char*)haystack;for (int i = 0; haystack[i] != '\0'; i++) {int j = 0;while (needle[j] != '\0' && haystack[i + j] == needle[j]) {j++;}if (needle[j] == '\0') {return (char*)(haystack + i);}}return NULL;
}

7. 结构体和联合体

7.1 结构体

7.1.1 结构的声明

struct tag
{member-list;
}variable-list;
例如描述⼀个学生:
struct Student {char name[50];int age;float score;
};

7.1.2 结构体函数

结构体对象使用 . 来访问成员变量

void print_student(struct Student s) {printf("姓名: %s, 年龄: %d, 分数: %.2f\n", s.name, s.age, s.score);
}

7.1.3 结构体指针

结构体的指针用 -> 来访问成员变量

void modify_student(struct Student *s) {strcpy(s->name, "修改后的名字");s->age = 25;s->score = 95.5;
}

7.1.4 结构体数组

void struct_array_demo() {struct Student class[3] = {{"张三", 20, 88.5},{"李四", 21, 92.0},{"王五", 19, 76.5}};printf("\n学生信息:\n");for (int i = 0; i < 3; i++) {print_student(class[i]);}
}

7.2 结构体内存对齐(重要)

7.2.1 为什么需要内存对齐?

内存对齐是为了提高CPU访问内存的效率。大多数CPU访问对齐的数据(地址是数据大小的整数倍)比访问非对齐的数据要快得多。

7.2.2 结构体内存对齐规则

基本规则
  1. 第一个成员:从结构体起始地址的0偏移处开始

  2. 其他成员:对齐到 min(#pragma pack指定的数值, 这个成员自身长度) 的整数倍地址

  3. 结构体总大小:必须是 min(#pragma pack指定的数值, 最大成员长度) 的整数倍

  4. 默认对齐值:visual studio中默认为8

示例分析
示例1:基础结构体
#include <stdio.h>struct Example1 {char a;     // 1字节int b;      // 4字节  short c;    // 2字节
};int main() {printf("sizeof(struct Example1) = %zu\n", sizeof(struct Example1));return 0;
}
内存布局分析
偏移地址:  0   1   2   3   4   5   6   7   8   9   10  11
成员:     [a] 填充 填充 填充 [b] [b] [b] [b] [c] [c] 填充 填充
大小:     1字节 + 3字节填充 + 4字节 + 2字节 + 2字节填充 = 12字节

示例2:调整成员顺序优化
struct Example2 {char a;     // 1字节short c;    // 2字节int b;      // 4字节
};int main() {printf("sizeof(struct Example2) = %zu\n", sizeof(struct Example2));return 0;
}
优化后的内存布局
偏移地址:  0   1   2   3   4   5   6   7
成员:     [a] 填充 [c] [c] [b] [b] [b] [b]
大小:     1字节 + 1字节填充 + 2字节 + 4字节 = 8字节
复杂结构体示例
示例4:嵌套结构体
struct Inner {char x;     // 1字节double y;   // 8字节
};              // 大小: 16字节struct Outer {char a;         // 1字节struct Inner b; // 16字节int c;          // 4字节
};int main() {printf("sizeof(Inner) = %zu\n", sizeof(struct Inner));printf("sizeof(Outer) = %zu\n", sizeof(struct Outer));return 0;
}
内存分析
Outer布局:
[char a] + 7字节填充 + [Inner结构体(16字节)] + [int c] + 4字节填充 = 32字节

7.3 联合体

像结构体一样,联合体也是由⼀个或者多个成员构成,这些成员可以不同的类型。
但是编译器只为最大的成员分配足够的内存空间。联合体的特点是所有成员共用同⼀块内存空间所
以联合体也叫:共用体。 给联合体其中⼀个成员赋值,其他成员的值也跟着变化。
union Data {int i;float f;char str[20];
};

7.3.1 判断大小端

int check_sys()
{union{int i;char c;}un;un.i = 1;return un.c;//返回1是⼩端,返回0是⼤端
}

7.4 枚举

枚举顾名思义就是一一列举。
enum Color { RED, GREEN, BLUE };
enum Week { MON = 1, TUE, WED, THU, FRI, SAT, SUN };
// 枚举使用enum Color c = RED;enum Week today = WED;printf("颜色: %d\n", c);printf("今天是星期: %d\n", today);

8. 内存函数(重要)

8.1 内存分配函数

8.1.1 malloc - 分配未初始化的内存

int *arr1 = (int*)malloc(5 * sizeof(int));
if (arr1 == NULL) {
printf("内存分配失败\n");
return 1;
}
// 使用分配的内存
for (int i = 0; i < 5; i++) {arr1[i] = i * 10;
}printf("malloc分配数组: ");
for (int i = 0; i < 5; i++) {printf("%d ", arr1[i]);
}
printf("\n");

8.1.2 calloc - 分配并初始化为0的内存

int *arr2 = (int*)calloc(5, sizeof(int));
printf("calloc分配数组(初始为0): ");
for (int i = 0; i < 5; i++) {printf("%d ", arr2[i]);
}
printf("\n");

8.1.3 realloc - 重新分配内存

arr1 = (int*)realloc(arr1, 10 * sizeof(int));
for (int i = 5; i < 10; i++) {arr1[i] = i * 10;
}printf("realloc扩展后数组: ");
for (int i = 0; i < 10; i++) {printf("%d ", arr1[i]);
}
printf("\n");

8.1.4 释放内存

free(arr1);
free(arr2);

8.2 memcpy和memmove

8.2.1 函数原型和基本概念

函数原型
#include <string.h>void *memcpy(void *dest, const void *src, size_t n);
void *memmove(void *dest, const void *src, size_t n);
基本功能

两个函数都用于从源内存区域复制n个字节到目标内存区域。

8.2.2 关键区别:内存重叠处理

特性memcpymemmove
内存重叠处理不处理,行为未定义安全处理,保证正确复制
性能通常更快稍慢(需要额外检查)
使用场景确定内存不重叠时不确定内存是否重叠时

memcpy从source的位置开始向后复制num个字节的数据到destination指向的内存位置。

这个函数在遇到 '\0' 的时候并不会停下来。

如果source和destination有任何的重叠,复制的结果都是未定义的,因此需要memmove来处理。

8.2.3 内存重叠示例

#include <stdio.h>
#include <string.h>int main() {char str1[] = "Hello, World!";char str2[] = "Hello, World!";// 内存重叠情况:目标在源之后printf("原始字符串: %s\n", str1);// 使用memcpy(可能有问题)memcpy(str1 + 7, str1, 6);printf("memcpy结果: %s\n", str1);// 使用memmove(安全)memmove(str2 + 7, str2, 6);printf("memmove结果: %s\n", str2);return 0;
}

输出结果可能不同,因为memcpy在重叠时的行为是未定义的。

8.2.4 模拟实现

memcpy的模拟实现
// 基础版本 - 逐字节复制
void *my_memcpy(void *dest, const void *src, size_t n) {if (dest == NULL || src == NULL) {return NULL;}char *d = (char *)dest;const char *s = (const char *)src;for (size_t i = 0; i < n; i++) {d[i] = s[i];}return dest;
}// 优化版本 - 使用字长复制(假设系统为32位)
void *my_memcpy_optimized(void *dest, const void *src, size_t n) {if (dest == NULL || src == NULL || n == 0) {return dest;}char *d = (char *)dest;const char *s = (const char *)src;// 按字节复制直到对齐边界while (((uintptr_t)d % sizeof(int)) != 0 && n > 0) {*d++ = *s++;n--;}// 按字长(4字节)复制int *d_int = (int *)d;const int *s_int = (const int *)s;while (n >= sizeof(int)) {*d_int++ = *s_int++;n -= sizeof(int);}// 复制剩余的字节d = (char *)d_int;s = (const char *)s_int;while (n > 0) {*d++ = *s++;n--;}return dest;
}

memmove的模拟实现
// memmove的模拟实现 - 处理内存重叠
void *my_memmove(void *dest, const void *src, size_t n) {if (dest == NULL || src == NULL || n == 0) {return dest;}char *d = (char *)dest;const char *s = (const char *)src;// 判断内存是否重叠以及重叠的方式if (d < s) {// 目标在源之前,从前往后复制(不会覆盖未复制数据)for (size_t i = 0; i < n; i++) {d[i] = s[i];}} else if (d > s) {// 目标在源之后,从后往前复制(避免覆盖未复制数据)for (size_t i = n; i > 0; i--) {d[i - 1] = s[i - 1];}}// 如果地址相等,不需要复制return dest;
}// 更简洁的memmove实现
void *my_memmove_simple(void *dest, const void *src, size_t n) {if (dest == NULL || src == NULL || n == 0) {return dest;}char *d = (char *)dest;const char *s = (const char *)src;// 创建临时缓冲区避免重叠问题char *temp = (char *)malloc(n);if (temp == NULL) {return NULL; // 内存分配失败}// 先复制到临时缓冲区for (size_t i = 0; i < n; i++) {temp[i] = s[i];}// 再从临时缓冲区复制到目标for (size_t i = 0; i < n; i++) {d[i] = temp[i];}free(temp);return dest;
}

9. 文件操作

9.1 文件的打开和关闭

9.1.1流和标准流


C语言中,所有的I/O操作都是通过“流”来进行的。流是数据在输入输出设备之间流动的抽象概念。

一般情况下,我们要向流里写入数据或读取数据,都需要打开流

9.1.2标准流

stdin:标准输入流(键盘)

stdout:标准输出流(屏幕)

stderr:标准错误流(屏幕)

我们使用scanf利用键盘读取数据,所利用的就是stdin标准输入流

我们使用printf向屏幕输出数据,所利用的就是stdout标准输出流

而C语言默认打开了这三个流,因此我们无需说明打开什么流,系统会默认为我们操作
 

9.1.3 文件指针

我们通过FILE*类型的指针来操作文件。

写入文件
FILE *file;
char buffer[100];
file = fopen("example.txt", "w");
if (file == NULL) {printf("无法打开文件\n");return 1;}

读取文件
file = fopen("example.txt", "r");
if (file == NULL) {printf("无法打开文件\n");return 1;
}

关闭文件
fclose(file);

9.2 二进制文件操作

struct Student {char name[50];int age;float score;
};struct Student students[3] = {{"张三", 20, 88.5},{"李四", 21, 92.0},{"王五", 19, 76.5}
};// 写入二进制文件file = fopen("students.dat", "wb");if (file) {fwrite(students, sizeof(struct Student), 3, file);fclose(file);}// 读取二进制文件struct Student read_students[3];file = fopen("students.dat", "rb");if (file) {fread(read_students, sizeof(struct Student), 3, file);fclose(file);printf("\n从二进制文件读取的学生信息:\n");for (int i = 0; i < 3; i++) {printf("姓名: %s, 年龄: %d, 分数: %.2f\n",read_students[i].name, read_students[i].age, read_students[i].score);}

10.编译与链接

10.1编译过程的四个主要阶段

源代码(.c) → 预处理(.i) → 编译(.s) → 汇编(.o) → 链接(可执行文件)

10.2  预处理阶段 (Preprocessing)

预处理是编译过程的第一步,由预处理器完成。

10.2.1 预处理的主要工作

头文件包含 (#include)

预处理阶段编译器会将包含的头文件展开到.c文件中,可以理解为复制粘贴

// 示例:math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H#define PI 3.14159double circle_area(double radius);
double circle_circumference(double radius);#endif
// main.c
#include <stdio.h>      // 系统头文件
#include "math_utils.h" // 用户头文件int main() {double r = 5.0;printf("面积: %.2f\n", circle_area(r));return 0;
}

预处理后

// 预处理后的main.i文件(简化版)
// stdio.h的数千行代码被插入到这里
// math_utils.h的内容被插入到这里double circle_area(double radius);
double circle_circumference(double radius);int main() {double r = 5.0;printf("面积: %.2f\n", circle_area(r));return 0;
}

宏定义和展开 (#define)

编译器会将宏的内容直接覆盖到代码中

#include <stdio.h>#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
#define DEBUG 1int main() {int x = 5, y = 10;#if DEBUGprintf("调试信息: x=%d, y=%d\n", x, y);#endifprintf("较大值: %d\n", MAX(x, y));printf("平方: %d\n", SQUARE(x + 1)); // 展开为 ((x + 1) * (x + 1))return 0;
}

预处理后

// stdio.h内容...
int main() {int x = 5, y = 10;printf("调试信息: x=%d, y=%d\n", x, y);printf("较大值: %d\n", ((x) > (y) ? (x) : (y)));printf("平方: %d\n", ((x + 1) * (x + 1)));return 0;
}

条件编译 (#if, #ifdef, #ifndef)
#include <stdio.h>#define VERSION 2
#define DEBUGint main() {#if VERSION == 1printf("版本1\n");#elif VERSION == 2printf("版本2\n");#elseprintf("未知版本\n");#endif#ifdef DEBUGprintf("调试模式开启\n");#endif#ifndef RELEASEprintf("不是发布版本\n");#endifreturn 0;
}

其他预处理指令
#include <stdio.h>// #error 在条件不满足时报错
#ifndef __STDC__
#error "需要ANSI C编译器"
#endif// #pragma 编译器特定指令
#pragma pack(1) // 设置结构体对齐方式// #line 改变行号信息
#line 100 "myfile.c"int main() {printf("当前行号: %d\n", __LINE__);printf("文件名: %s\n", __FILE__);return 0;
}

10.3 编译阶段 (Compilation)

编译阶段将预处理后的C代码转换为汇编代码。

10.3.1 编译过程详解

// 示例代码:compute.c
int add(int a, int b) {return a + b;
}double multiply(double x, double y) {return x * y;
}int main() {int result = add(5, 3);double product = multiply(2.5, 4.0);return 0;
}

编译为汇编代码

gcc -S compute.c -o compute.s

生成的汇编代码示例(x86架构):

	.file	"compute.c".text.globl	add.type	add, @function
add:pushq	%rbpmovq	%rsp, %rbpmovl	%edi, -4(%rbp)movl	%esi, -8(%rbp)movl	-4(%rbp), %edxmovl	-8(%rbp), %eaxaddl	%edx, %eaxpopq	%rbpret.size	add, .-add.globl	multiply.type	multiply, @function
multiply:// ... 类似代码.size	multiply, .-multiply.globl	main.type	main, @function
main:// ... 主函数代码.size	main, .-main

10.4 汇编阶段 (Assembly)

汇编阶段将汇编代码转换为机器代码(目标文件)。

10.4.1 目标文件的结构

# 生成目标文件
gcc -c compute.s -o compute.o# 查看目标文件信息
objdump -t compute.o    # 符号表
nm compute.o           # 符号表(另一种格式)
readelf -h compute.o   # ELF头信息

目标文件包含

  • 代码段(.text):编译后的机器指令

  • 数据段(.data):已初始化的全局变量

  • BSS段(.bss):未初始化的全局变量

  • 符号表:函数和变量名及其地址信息

  • 重定位表:需要链接时解析的符号引用

10.5 链接阶段 (Linking)

链接阶段将一个或多个目标文件与库文件合并,生成可执行文件。

链接过程详解

10.5.1 创建多个源文件示例

math_ops.c

#include "math_ops.h"int add(int a, int b) {return a + b;
}int subtract(int a, int b) {return a - b;
}

math_ops.h

#ifndef MATH_OPS_H
#define MATH_OPS_Hint add(int a, int b);
int subtract(int a, int b);#endif

main.c

#include <stdio.h>
#include "math_ops.h"int main() {int x = 10, y = 5;printf("%d + %d = %d\n", x, y, add(x, y));printf("%d - %d = %d\n", x, y, subtract(x, y));return 0;
}

10.5.2 分步编译链接过程
# 1. 预处理
gcc -E main.c -o main.i
gcc -E math_ops.c -o math_ops.i# 2. 编译为汇编代码
gcc -S main.i -o main.s
gcc -S math_ops.i -o math_ops.s# 3. 汇编为目标文件
gcc -c main.s -o main.o
gcc -c math_ops.s -o math_ops.o# 4. 链接为可执行文件
gcc main.o math_ops.o -o program# 或者一步完成
gcc main.c math_ops.c -o program

10.5.3 符号解析和重定位

查看符号表

# 查看目标文件中的符号
nm main.o
nm math_ops.o# 查看可执行文件中的符号
nm program

输出示例

main.o:U add           # 未定义符号,需要链接时解析U printf        # 未定义符号,来自C标准库
0000000000000000 T main          # 已定义符号math_ops.o:
0000000000000000 T add           # 已定义符号
0000000000000014 T subtract      # 已定义符号program:
0000000000001139 T add           # 链接后已解析
000000000000114d T main
0000000000001159 T subtractU printf@@GLIBC_2.2.5  # 动态链接符号

10.6. 预处理中的重要细节

10.6.1防止头文件重复包含

// math.h
#ifndef MATH_H
#define MATH_H// 头文件内容...#endif

10.6.2 宏使用的注意事项

// 错误的宏定义
#define SQUARE(x) x * x
// 问题:SQUARE(1+2) 展开为 1+2*1+2 = 5,而不是9// 正确的宏定义
#define SQUARE(x) ((x) * (x))// 多行宏定义
#define SWAP(a, b) do { \typeof(a) temp = a; \a = b; \b = temp; \
} while(0)

10.6.3 预定义宏

#include <stdio.h>int main() {printf("文件名: %s\n", __FILE__);printf("行号: %d\n", __LINE__);printf("编译日期: %s\n", __DATE__);printf("编译时间: %s\n", __TIME__);printf("函数名: %s\n", __func__);#ifdef __STDC__printf("ANSI C兼容: %d\n", __STDC__);#endifreturn 0;
}

http://www.dtcms.com/a/548481.html

相关文章:

  • 跳水不改大趋势!盘后出利好!
  • phpstudy(PHP 集成开发环境工具)下载安装教程
  • 题解:CF2150B Grid Counting
  • 远程操作怕“泄密“?深度解析ToDesk/向日葵/Splashtop/AnyDesk安全隐私防护力
  • SQL之表的查改(上)
  • 专业制作网站 郑在电脑上做苗木网站
  • wordpress 主题采集网站大图片优化
  • 快站app制作教程钟表玻璃东莞网站建设
  • 网站首页一般做多大尺寸小程序开发兼职
  • 菜鸟网站建设行业网站建设哪家好
  • 自助建站源码下载直播软件视频软件
  • 安徽省建设干部学校网站关停手机网站开发软件下载
  • 电子手工外发加工网成都黑帽seo
  • 常德seowordpress评论框优化
  • 石家庄企业建站哪家好可以做用户调研的网站
  • 广西网站建设定制医疗营销网站建设
  • 企业网站内容如何搭建wordpress模板本地怎么安装
  • 免费网站在线观看人数在哪直播中小企业网站建设价格
  • 郑州陆港开发建设有限公司网站wordpress 关闭警告
  • 做韩国的跨境电商网站如何k掉网站
  • 佛山免费网站建设网站如何优化
  • 企业网网站怎么做保定建网站公司
  • 建设银行辽宁招聘网站社科联网站建设方案策划书
  • 如何建网站要什么条件wordpress 多地址插件
  • 有没有专门做尾料回收的网站宁德网站建设
  • 有关天猫网站开发的论文搭建钓鱼网站教程
  • 连云港网站建设培训班ajax瀑布流网站模板
  • 咸阳企业网站设计开发制作做网站外包的公司好干嘛
  • 唐山市住房城乡建设局网站怎么网上宣传自己的产品
  • 金泉网普通会员可以建设网站吗网站从哪几个方面维护