一、C语言基础知识点
1. **C语言概述**
1.C语言的特点
面向过程编程,主要依赖按照特定的顺序调用函数以实现特定的任务或功能,更侧重于算法和逻辑的实现,而不是将数据和行为封装在对象中
2.C语言的标准
K&RC、C89、C99、C11等
2. **数据类型**
1.基本数据类型
整型(short、int、long)、字符型(char)、浮点型(float、double)
2.数据类型的取值范围和大小
1.整型(Integer)
char:通常用于表示字符,但也可以表示小整数。大小为1字节,取值范围通常为-128到127(有符号)或0到255(无符号)。
short:短整型,大小为2字节,取值范围通常为-32768到32767(有符号)或0到65535(无符号)。
int:整型,通常为4字节,取值范围如上所述(有符号和无符号)。
long:长整型,通常为4或8字节,在32位系统中,long通常为4字节,取值范围如上所述(有符号和无符号)。
long long:更长的整型,通常为8字节,取值范围如上所述(有符号和无符号)。
2.字符型(Character)
char:如上所述,既可以表示字符(与ASCII码表对应),也可以表示小整数。
3.浮点型(Floating-point)
float:单精度浮点型,大小为4字节,取值范围约为-3.4E+38到3.4E+38,精度约为7位有效数字。
double:双精度浮点型,大小为8字节,取值范围约为-1.7E+308到1.7E+308,精度约为15位有效数字。
long double:扩展精度浮点型,大小通常为8或16字节,取值范围和精度取决于编译器和系统。
3.类型转换
显式转换和隐式转换
//显式类型转换
float a = 10.55f;
float b = 13.14f;
float c = (int)a + (int)b
//隐式转换
float a = 10.55f;
float b = 13.14f;
int c = a + b;
3. **变量和常量**
1.变量、常量的定义和使用:
#include <stdio.h>
#define PI 3.14159 // 使用#define预处理指令定义常量PI
int main() {
const int MAX_SIZE = 100; // 使用const关键字定义整型常量MAX_SIZE
double radius = 5.0;
double area;
// 使用常量PI来计算圆的面积
area = PI * radius * radius;
// 输出计算结果
printf("The area of the circle is: %.2f\n", area);
// 尝试修改常量(这将导致编译错误,因为MAX_SIZE是const)
// MAX_SIZE = 200; // 错误:不能给const变量赋值
return 0;
}
#include <stdio.h>
#include <string.h>
int main() {
// 定义变量
int age; // 定义一个整型变量age,用于存储年龄
float height; // 定义一个浮点型变量height,用于存储身高
char name[50]; // 定义一个字符数组name,用于存储名字,长度为50个字符
// 使用变量
age = 25; // 为变量age赋值25
height = 5.9; // 为变量height赋值5.9
strcpy(name, "Alice");
// 输出变量的值
printf("Name: %s\n", name);
printf("Age: %d\n", age);
printf("Height: %.1f\n", height);
return 0;
}
2.变量的作用域和生命周期
1.变量的作用域:
作用域(scope)是程序设计中的一个基本概念,它定义了变量、函数等标识符在程序中的可见性和可用性范围。
全局作用域(Global Scope):
定义在函数外部或文件顶部的变量具有全局作用域。
全局变量可以在整个程序中被访问和修改。
全局作用域的生命周期是整个程序的生命周期,从程序开始执行到程序结束。
函数作用域(Function Scope):
定义在函数内部或代码块内部的变量(不使用static关键字)具有函数作用域或局部作用域。
这些变量只能在定义它们的函数或代码块内部被访问。
函数作用域的变量在函数调用时创建,在函数返回时销毁。
块作用域(Block Scope):
块作用域是函数作用域的一个子集,特指在代码块(如if语句、while循环等)内部定义的变量。
这些变量只能在它们所在的代码块内部被访问。
块作用域的变量在代码块执行时创建,在代码块结束时销毁。
2.变量的生命周期
生命周期(lifetime)指的是变量从创建到销毁的时间段。
全局变量的生命周期:
全局变量在程序开始执行时被创建。
它们在程序的整个生命周期内都存在,直到程序结束时才被销毁。
局部变量的生命周期:
局部变量在它们所在的函数或代码块被调用或执行时创建。
它们在函数或代码块结束时被销毁,释放所占用的内存空间。
静态变量的生命周期:
静态变量(使用static关键字定义的变量)具有特殊的生命周期。
静态局部变量在它们所在的函数或代码块第一次被调用或执行时创建。它们在程序的整个生命周期内都存在,即使函数或代码块已经执行完毕。
静态全局变量(在全局作用域中使用static关键字定义的变量)的作用域被限制在定义它们的文件内,但它们的生命周期仍然是整个程序的生命周期。
4. **运算符**
1.运算符类型
算术运算符:+、-、*、/、%等。
关系运算符:>、<、==、!=等。
逻辑运算符:&&、||、!等。
位运算符:<<、>>、&、|、^等。
赋值运算符:=、+=、-=、*=等。
条件运算符:? :。
逗号运算符:,。
2.运算符的优先级
括号运算符 ()
单目运算符(如 +, -, , &, !, ~ 等,注意在这里仅作为示例,它作为算术乘法运算符时优先级不同)
算术运算符(*, /, %)
算术运算符(+, -)
移位运算符(<<, >>)
关系运算符(>, <, ==, !=, >=, <=)
位运算符(&, |, ^)
逻辑运算符(&&, ||)
条件运算符(?:)
赋值运算符(=, +=, -=, *= 等)
逗号运算符(,)
5. **基本语句**
1.条件语句(if、else)
#include <stdio.h>
int main() {
int number;
// 提示用户输入一个整数
printf("请输入一个整数: ");
scanf("%d", &number);
// 使用 if-else 语句判断整数的值
if (number > 0) {
// 如果整数是正数,输出正数消息
printf("您输入的是正数。\n");
} else if (number < 0) {
// 如果整数是负数,输出负数消息
printf("您输入的是负数。\n");
} else {
// 如果整数是零,输出零的消息
printf("您输入的是零。\n");
}
// 程序结束
return 0;
}
2.循环语句(for、while、do-while)
for
#include <stdio.h>
int main() {
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
while
#include <stdio.h>
int main() {
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
int i = 0;
while (i < size) {
printf("%d ", arr[i]);
i++;
}
printf("\n");
return 0;
}
do-while
#include <stdio.h>
int main() {
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
int i = 0;
do {
printf("%d ", arr[i]);
i++;
} while (i < size);
printf("\n");
return 0;
}
3.多路分支语句(switch)
#include <stdio.h>
int main() {
int day;
// 提示用户输入一个整数(1-7),代表星期几
printf("请输入一个整数(1-7),代表星期几:");
scanf("%d", &day);
// 使用 switch 语句根据输入的整数打印对应的星期几
switch (day) {
case 1:
printf("星期一\n");
break;
case 2:
printf("星期二\n");
break;
case 3:
printf("星期三\n");
break;
case 4:
printf("星期四\n");
break;
case 5:
printf("星期五\n");
break;
case 6:
printf("星期六\n");
break;
case 7:
printf("星期日\n");
break;
default:
// 如果输入的整数不在1到7之间,打印错误信息
printf("输入无效,请输入1到7之间的整数。\n");
break;
}
return 0;
}
4.goto语句
#include <stdio.h>
int main() {
int num;
printf("请输入一个正整数:");
scanf("%d", &num);
// 使用goto语句处理非法输入
if (num <= 0) {
goto error; // 跳转到标签error处
}
// 如果输入是正整数,则执行以下代码
printf("您输入的是正整数 %d。\n", num);
// 正常结束程序
goto end; // 跳转到标签end处以结束程序
// 错误处理代码
error:
printf("错误:请输入一个正整数。\n");
// 程序结束代码
end:
printf("程序结束。\n");
return 0;
}
6. **数组**
1. 一维数组、二维数组、多维数组的定义和使用
仅展示一维数组:
#include <stdio.h>
int main() {
// 定义一个一维数组,包含5个整数
int arr1[5] = {1, 2, 3, 4, 5};
// 遍历并打印一维数组中的每个元素
for (int i = 0; i < 5; i++) {
printf("arr1[%d] = %d\n", i, arr1[i]);
}
return 0;
}
7. **指针**
1.指针的概念和定义
指针是一个变量,它存储的是内存地址,而不是数据值本身。这个内存地址指向的是另一个变量的位置,通过该地址,我们可以间接访问或修改该变量的值
int *ptr;
int a = 10;
int *ptr = &a; // &a 获取变量a的地址,并将其赋值给指针ptr
int value = *ptr; // 通过指针ptr获取它所指向的变量的值,并赋值给变量value
2.指针的运算和指向关系
//指针加法(指针保存的只是地址,所以只对地址产生作用)
//假设arr[5]的首地址(arr)是0x0000,那么指向arr的指针*ptr保存的就是0x0000,ptr++就是地址0x0001,也就是arr[1]的地址
int arr[5] = {1, 2, 3, 4, 5};
//指向关系
int *ptr = arr; //ptr指向arr[0]
ptr++; // 现在ptr指向arr[1]
//指针减法(它们之间元素的数量(基于指针指向的数据类型的大小))
int diff = ptr - arr; // 计算ptr和arr之间的元素数量差
//指针的关系运算(可以使用<, >, <=, >=, ==, !=等关系运算符来比较两个指针,比较的是指针保存的地址)
if (ptr1 < ptr2) {
// ptr1指向的地址小于ptr2指向的地址
}
3.指针数组和数组指针
/*
指针数组是一个数组,其元素是指针。这意味着每个数组元素都存储了一个内存地址,这些地址可以指向变量、数组或其他数据结构。
*/
int *ptrArray[5]; // 声明一个包含5个int类型指针的数组
int a = 1, b = 2, c = 3, d = 4, e = 5;
ptrArray[0] = &a;
ptrArray[1] = &b;
ptrArray[2] = &c;
ptrArray[3] = &d;
ptrArray[4] = &e;
/*
数组指针是一个指针,它指向一个数组。这意味着该指针存储了一个数组的首地址,并且知道该数组的类型和大小(或至少知道如何根据类型推断大小)
*/
int (*arrayPtr)[5]; // 声明一个指向包含5个int元素的数组的指针
int arr[5] = {1, 2, 3, 4, 5};
arrayPtr = &arr; // 让arrayPtr指向arr
4.函数指针和函数指针数组
/*
函数指针是一个变量,它存储了一个函数的地址。通过这个函数指针,你可以调用它所指向的函数
*/
// 声明一个指向返回int类型,接受两个int参数的函数的指针
int (*funcPtr)(int, int);
// 假设有一个符合这个签名的函数
int add(int a, int b) {
return a + b;
}
// 将函数地址赋给函数指针
funcPtr = add;
// 通过函数指针调用函数
int result = funcPtr(3, 4); // result将是7
/*
函数指针数组是一个数组,其元素是函数指针。这意味着你可以存储多个函数的地址,并通过数组索引来调用它们
*/
// 声明一个包含5个指向返回int类型,接受两个int参数的函数的指针的数组
int (*funcPtrArray[5])(int, int);
// 假设有三个符合这个签名的函数
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; }
int divide(int a, int b) { return b != 0 ? a / b : 0; } // 避免除以零错误
int modulo(int a, int b) { return b != 0 ? a % b : 0; } // 避免取模零错误
// 将函数地址赋给函数指针数组
funcPtrArray[0] = add;
funcPtrArray[1] = subtract;
funcPtrArray[2] = multiply;
funcPtrArray[3] = divide;
funcPtrArray[4] = modulo;
// 通过函数指针数组调用函数
int result = funcPtrArray[2](6, 7); // result将是42(6乘以7)
8. **函数**
1.函数的定义、声明和调用
/*
函数的声明是告诉编译器函数的存在、名称、返回类型以及参数类型的一种方式。它允许你在调用函数之前引用它,而不需要知道函数的具体实现。函数声明通常放在文件的开头或头文件中
*/
// 声明一个名为add的函数,它接受两个int类型的参数并返回一个int类型的结果
int add(int, int);
/*
函数的定义是函数的具体实现,包含了函数的名称、返回类型、参数列表以及函数体。函数体是由花括号{}包围的语句块,这些语句定义了函数的行为
*/
// 定义一个名为add的函数,它接受两个int类型的参数并返回一个int类型的结果
int add(int a, int b) {
return a + b; // 函数体,计算并返回两个参数的和
}
/*
函数的调用是在程序中执行函数的一种方式。通过函数名后跟一对圆括号以及必要的参数(如果有的话),你可以调用一个函数。函数调用的结果(如果有的话)可以被赋值给变量或用于表达式中。
*/
int result = add(3, 4); // 调用add函数,传递3和4作为参数,并将返回的结果赋值给result变量
2.函数的参数传递(值传递和地址传递)
/*
值传递是指函数调用时,实际参数(实参)的值被复制一份,然后将复制的值传递给函数的形式参数(形参)
特点:
1.在函数内部,对形参的修改不会影响实参的值
2.因为需要复制实参的值,所以在函数调用时会产生额外的内存开销
3.如果函数内部不需要修改实参的值,或者参数较小且复制开销较小,可以使用值传递
*/
#include <stdio.h>
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
// 这里的交换操作只影响形参a和b,不影响实参
}
int main() {
int x = 5;
int y = 10;
swap(x, y); // 调用后,x的值仍然是5,y的值仍然是10
printf("x = %d, y = %d\n", x, y);
return 0;
}
/*
地址传递(有时在C++中称为引用传递的指针实现方式)是指函数调用时,实际参数的地址被传递给函数的形式参数。函数内部可以通过指针来访问和修改实际参数的值
特点:
1.在函数内部,对形参(即指针所指向的内存地址)的修改会影响实参的值
2.因为不需要复制实参的值,所以在函数调用时不会产生额外的内存开销
3.如果函数需要修改实参的值,或者参数较大,可以使用地址传递
4.需要注意指针的正确使用和解引用操作,以避免野指针和内存泄漏等问题
*/
#include <stdio.h>
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
// 这里的交换操作会影响实参
}
int main() {
int x = 5;
int y = 10;
swap(&x, &y); // 调用后,x的值变为10,y的值变为5
printf("x = %d, y = %d\n", x, y);
return 0;
}
3.函数的返回值和返回类型
/*
返回类型是指函数执行完毕后返回给调用者的值的类型,在函数定义时,返回类型被明确指定在函数名之前
特点:
1.返回类型可以是基本数据类型(如int、float、char等),也可以是复合数据类型(如数组、结构体、类等,但需要注意,数组通常通过指针返回,因为数组名本身就是地址)
2.如果函数不需要返回任何值,其返回类型可以指定为void
*/
int add(int a, int b) {
return a + b; // 返回一个整型值
}
void printMessage() {
printf("Hello, World!\n"); // 不返回任何值
}
/*
返回值是函数执行完毕后实际返回给调用者的值,在函数体中,通过return语句指定返回值
1.返回值必须与函数的返回类型相匹配
2.如果函数返回类型为void,则不能在函数体中使用return语句返回任何值(但可以使用return语句来结束函数的执行)
3.在某些情况下,即使函数的返回类型不是void,也可能不需要显式地返回任何值(这通常是不推荐的做法,因为它可能导致未定义行为),然而,在C和C++等语言中,如果函数有返回类型但不是void,并且没有执行到任何return语句,那么编译器通常会报错
*/
int multiply(int a, int b) {
return a * b; // 返回一个整型值,即a和b的乘积
}
char getChar() {
return 'A'; // 返回一个字符型值
}
void exitFunction() {
// 不返回任何值,但可以使用return语句结束函数执行
return;
}
4.库函数的使用(如字符串处理函数、时间函数、随机数函数等)
/*
库函数是预先编写好并封装在库中的函数,它们提供了执行常见任务(如字符串处理、时间管理、随机数生成等)的便捷方法,通过使用库函数,程序员可以避免重复编写代码,提高开发效率,并确保代码的质量和可靠性
*/
/*
strcpy():复制字符串
*/
#include <string.h>
#include <stdio.h>
int main() {
char source[] = "Hello, World!";
char destination[50];
strcpy(destination, source);
printf("Destination: %s\n", destination);
return 0;
}
/*
strcmp():比较字符串
*/
#include <string.h>
#include <stdio.h>
int main() {
char str1[] = "Apple";
char str2[] = "Banana";
int result;
result = strcmp(str1, str2);
if (result < 0)
printf("str1 is less than str2\n");
else if (result > 0)
printf("str1 is greater than str2\n");
else
printf("str1 is equal to str2\n");
return 0;
}
/*
strcat():连接字符串
*/
#include <string.h>
#include <stdio.h>
int main() {
char str1[100] = "Hello ";
char str2[] = "World";
strcat(str1, str2);
printf("Concatenated string: %s\n", str1);
return 0;
}
//剩下的库函数可以自己查找
9. **预处理**
1.宏定义、宏函数和条件编译
/*
宏定义是预处理器指令的一种,它允许你为代码中的常量或代码片段定义别名,宏定义通常使用#define指令来实现
*/
#define PI 3.14159
#define SQUARE(x) ((x) * (x))
/*
条件编译允许你根据特定的条件来选择性地编译代码的一部分,这通常用于在不同的编译环境下编译不同的代码,或者为了调试目的而包含或排除特定的代码段
*/
#define DEBUG
#ifdef DEBUG
printf("Debug mode is on.\n");
#else
printf("Debug mode is off.\n");
#endif
2.头文件的包含和编译指令
/*
头文件通过#include预处理指令包含到源文件中,这个指令告诉预处理器在编译之前将指定的头文件内容插入到包含它的源文件中
*/
#include <stdio.h> //使用尖括号包含标准库头文件
#include <stdlib.h> //使用尖括号包含标准库头文件
#include "myheader.h" //双引号包含自定义头文件
/*
编译指令通常不是预处理指令(如#include),而是命令行工具(如gcc、clang等)的参数,用于指导编译器如何编译和链接程序,然而,有时我们也会在源文件中使用特定的预处理指令来影响编译行为,例如条件编译
*/
//条件编译(虽然它不是一个直接的编译指令,但它是通过预处理指令实现的)
#ifdef DEBUG
// Debug-specific code
#else
// Production code
#endif
/*
1.gcc是编译器命令
2.-I/path/to/headers告诉编译器在/path/to/headers目录中查找头文件
3.-L/path/to/libs指定了库文件的搜索路径
4.-lmylib告诉编译器链接名为libmylib.so或libmylib.a的库(取决于操作系统和编译器的链接选项)
5.-o myprogram指定了输出文件的名称
6.myprogram.c是源文件
*/
//编译单个源文件
gcc -o myprogram myprogram.c
//包含头文件搜索路径
gcc -I/path/to/headers -o myprogram myprogram.c
//链接库
gcc -L/path/to/libs -lmylib -o myprogram myprogram.c
10. **输入输出**
1.标准输入输出函数(printf、scanf等)
/*
%d:有符号十进制整数
%f:浮点数(默认精度为6位小数)
%s:字符串
%c:单个字符
%x:无符号十六进制整数
*/
//format:一个C字符串,包含了格式说明符,用于指定输入输出数据的类型和格式
//...:表示可变数量的参数,这些参数应该是指向变量的指针,用于存储输入的数据
int printf(const char *format, ...);
int scanf(const char *format, ...);
/*
1.使用%s读取字符串时,要确保为目标字符串分配了足够的空间,以避免缓冲区溢出
2.使用%c读取字符时,可能需要使用空格字符' '来跳过前一个输入留下的空白字符
3.scanf不会检查输入的长度,因此可能会导致缓冲区溢出等安全问题,在需要更安全的输入时,可以考虑使用fgets和sscanf等函数
*/
*fgets(char *buf, int bufsize, FILE *stream);
int sscanf(const char *buffer, const char *format, ...);
2.文件输入输出操作(fopen、fclose、fread、fwrite等)
/*
fopen函数用于打开一个文件,并返回一个指向该文件的文件指针,如果文件打开失败,则返回NULL
*/
//filename:要打开的文件的名称
//mode:文件的打开模式,如读模式("r")、写模式("w")、追加模式("a")等。还可以结合使用文本模式("t")和二进制模式("b")的标识符
FILE *fopen(const char *filename, const char *mode);
/*
fclose函数用于关闭一个打开的文件,并释放与该文件相关的资源,如果文件在关闭前没有被正确写入(例如,使用了缓冲的输出函数但缓冲区还未被刷新),fclose会尝试刷新这些输出
*/
//1.stream:要关闭的文件的文件指针
int fclose(FILE *stream);
/*
fread函数用于从文件中读取数据,它通常用于二进制文件的读取,但也可以用于文本文件
*/
//1.ptr:指向存储读取数据的缓冲区的指针
//2.size:要读取的每个数据项的大小(以字节为单位)
//3.nmemb:要读取的数据项的数量
//4.stream:要从中读取数据的文件的文件指针
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
/*
fwrite函数用于向文件中写入数据。它通常用于二进制文件的写入,但也可以用于文本文件
*/
//1.ptr:指向要写入的数据的缓冲区的指针
//2.size:要写入的每个数据项的大小(以字节为单位)
//3.nmemb:要写入的数据项的数量
//4.stream:要向其中写入数据的文件的文件指针
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *file;
char buffer[100];
const char *data = "Hello, World!";
// 打开文件用于写入(如果文件不存在则创建)
file = fopen("example.txt", "w");
if (file == NULL) {
perror("Failed to open file for writing");
return EXIT_FAILURE;
}
// 写入数据到文件
if (fwrite(data, sizeof(char), strlen(data), file) != strlen(data)) {
perror("Failed to write data to file");
fclose(file);
return EXIT_FAILURE;
}
// 关闭文件
fclose(file);
// 打开文件用于读取
file = fopen("example.txt", "r");
if (file == NULL) {
perror("Failed to open file for reading");
return EXIT_FAILURE;
}
// 从文件中读取数据
if (fread(buffer, sizeof(char), sizeof(buffer) - 1, file) > 0) {
// 确保字符串以空字符结尾
buffer[sizeof(buffer) - 1] = '\0';
// 输出读取的数据
printf("Data read from file: %s\n", buffer);
} else {
perror("Failed to read data from file");
}
// 关闭文件
fclose(file);
return EXIT_SUCCESS;
}
二、C语言进阶知识点
1. **内存管理**
1.动态内存分配(malloc、free、calloc、realloc)
/*
1.malloc()函数用于在堆上分配指定大小的内存块
2.calloc函数用于在堆上分配指定数量的内存块,并将它们初始化为零(和malloc的区别在于malloc分配内存后没有对内存初始化为0)
3.realloc函数用于调整之前通过malloc、calloc或realloc函数分配的内存块的大小
4.free函数用于释放之前通过malloc、calloc或realloc函数分配的内存
*/
int main() {
int *arr;
int n = 5;
// 分配内存
arr = (int *)malloc(n * sizeof(int));
// 使用内存
for (int i = 0; i < n; i++) {
arr[i] = i * 2;
}
// 调整内存大小
arr = (int *)realloc(arr, m * sizeof(int));
// 初始化新分配的内存部分(如果需要)
for (int i = n; i < m; i++) {
arr[i] = i * 2;
}
// 释放内存
free(arr);
arr = NULL; // 避免野指针
return 0;
}
2.内存泄漏的防止和检查
1.确保每个malloc、calloc和realloc调用都有对应的free调用
1.在分配内存后,确保在不再需要该内存时释放它
2.使用free函数后,将指针设置为NULL,以避免野指针问题(尽管这不会防止内存泄漏,但有助于避免使用已释放的内存)
2.使用智能指针或类似机制(在C++中)
1.C++中可以使用智能指针(如std::unique_ptr和std::shared_ptr)来自动管理内存,减少手动new和delete的使用,从而降低内存泄漏的风险
3.避免异常路径导致的内存泄漏
1.在处理错误和异常时,确保内存仍然被正确释放
2.使用goto语句(尽管不推荐)或结构化错误处理(如C++中的try-catch块)来确保在发生错误时能够释放资源
3.malloc底层实现原理
当进行内存分配时,Malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。
1、空闲存储空间以空闲链表的方式组织(地址递增),每个块包含一个长度、一个指向下一块的指针以及一个指向自身存储空间的指针。( 因为程序中的某些地方可能不通过malloc调用申请,因此malloc管理的空间不一定连续。)
2、当有申请请求时,malloc会扫描空闲链表,直到找到一个足够大的块为止(首次适应)(因此每次调用malloc时并不是花费了完全相同的时间)。
3、如果该块恰好与请求的大小相符,则将其从链表中移走并返回给用户。如果该块太大,则将其分为两部分,尾部的部分分给用户,剩下的部分留在空闲链表中(更改头部信息)。因此malloc分配的是一块连续的内存。
4、释放时,首先搜索空闲链表,找到可以插入被释放块的合适位置。如果与被释放块相邻的任一边是一个空闲块,则将这两个块合为一个更大的块,以减少内存碎片。
所以malloc采用的是内存池的管理方式(ptmalloc),Ptmalloc 采用边界标记法将内存划分成很多块,从而对内存的分配与回收进行管理。为了内存分配函数malloc的高效性,ptmalloc会预先向操作系统申请一块内存供用户使用,当我们申请和释放内存的时候,ptmalloc会将这些内存管理起来,并通过一些策略来判断是否将其回收给操作系统。这样做的最大好处就是,使用户申请和释放内存的时候更加高效,避免产生过多的内存碎片。
4.栈和堆的使用原则
2. **数据结构**
1.链表:单向链表、双向链表、循环链表等
/*
单向链表
*/
// 定义节点结构
typedef struct Node {
int data;
struct Node* next;
} Node;
// 创建新节点
Node* createNode(int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = NULL;
return newNode;
}
// 添加节点到链表末尾
void append(Node** head, int data) {
Node* newNode = createNode(data);
if (*head == NULL) {
*head = newNode;
} else {
Node* temp = *head;
while (temp->next != NULL) {
temp = temp->next;
}
temp->next = newNode;
}
}
// 遍历链表
void printList(Node* head) {
Node* temp = head;
while (temp != NULL) {
printf("%d -> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
int main() {
Node* list = NULL;
append(&list, 1);
append(&list, 2);
append(&list, 3);
printList(list); // 输出: 1 -> 2 -> 3 -> NULL
return 0;
}
389

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



