目录
1.概述
2.虚拟内存空间
2.1存储期限
2.2栈区管理
2.3堆区域的使用
3.动态内存分配和释放(重点)
3.1通用指针类型void
3.2内存分配malloc函数
3.2.1 malloc函数(memory allocation)(注意len*size,if(*p == NULL)!)
3.2.2不要用数组类型接收返回值
3.3 内存泄漏
3.4free函数(free(p);)
3.4.1尽量不要移动原始指针
3.4.2 不要两次free同一片内存区域,会导致未定义行为。
3.4.3 悬空指针
3.4.4 避免不可到达内存空间
4. 清零内存分配函数calloc(cleared allocation)动态分配初始化为0的数组
5. 内存重分配函数 realloc(reallocation) 数组和数据结构动态扩容收缩必备
6. 手动实现C++中的vector
6.1思路讲解
6.2头文件的使用
6.2.2 头文件保护
6.2.3 包含头文件与实现函数
6.3.1 实现vector_creat()
6.3.2vector_destroy()
6.3.3vector_push_back()
1.概述
动态内存分配
二级指针:啥叫
函数指针:啥叫
2.虚拟内存空间
2.1存储期限
1.自动存储期限:栈,调用期间有效
2.静态存储期限:存在数据段的内容,主要指全局变量、静态局部变量以及static修饰的全局变量
3.动态存储期限:堆上的空间需要手动控制
2.2栈区管理
管理需要用到栈指针寄存器(Stack Pointer寄存器,简称SP寄存器)始终指向栈的顶部
sp+-已经确定大小的栈的空间
栈的优点: 简单高效,自动管理,线程安全(不能共享)
缺点:大小有限,不能当运行时才能确定大小的东西
堆区域弥补栈区域的缺点
栈区就是需要在编译前确定并且要很小(这点很难),所以说有时是堆区
2.3堆区域的使用
堆区域需要借助于malloc函数进行手动管理
优点:区域大,共享,灵活分配
缺点:管理繁琐,性能相比栈会差,线程 不安全(因为共享)
一般优先使用栈空间,除非不合条件,因为它的优点
3.动态内存分配和释放(重点)
动态内存分配主要应用于链式的结构像是链表,树,图。
一般来说,如无特别需求,不要在堆上为基本数据类型动态分配空间。
3.1通用指针类型void
1.可以存储任意类型数据的地址
2.将void转换为其他类型的指针在C++中需要加上显示类型,但是c语言中不需要
float* float_ptr = (float*)void_ptr;
3.不能直接进行操作,因为没有具体的类型不能通过void进行操作
进行类型的转换的时候也可能会出现问题,像是double->void->int就是错误的。
3.2内存分配malloc函数
在C语言中,想要在堆上动态分配内存空间,主要依赖三个函数来完成,它们都声明在头文件<stdlib.h>当中:
- malloc
- calloc
- realloc
3.2.1 malloc函数(memory allocation)(注意len*size,if(*p == NULL)!)
堆空间上分配一块连续的空间,若分配成功会返回指向内存首字节地址的指针,类型为void(因此在操作之前要类型转换),若分配失败会返回NULL(因为在malloc之后都会进行判断是否成功),且malloc函数不会进行初始化(vs中 标记为cd)
int* arr_p = malloc(ARR_LEN * sizeof(int));
3.2.2不要用数组类型接收返回值
C语言的数组类型,其长度必须在编译时期确定,数组名本身在声明时就和一块固定大小的栈内存区域绑定。而malloc是在运行时动态分配内存的,数组类型变量显然不能用malloc函数进行初始化赋值。
在使用动态内存分配函数时,认准指针类型即可,不要考虑使用其它类型。
使用malloc可能会出现内存泄漏的问题
3.3 内存泄漏
数组和结构体是在堆上保存的,栈上保存的是指向的指针,
内存泄漏是指程序在运行过程中,未能适时释放不再使用的内存区域,导致这部分内存在程序的生命周期内始终无法被重用。
3.4free函数(free(p);)
若分配的内存不再使用,需要free函数及时释放。
1.参数必须是堆上申请内存块的地址(首字节地址),不能传递别的指针,否则会引发未定义行为。
2. free函数:只是标记区域为可用不修改,free函数不会修改传入的实参指针的指向
因为得到的是p指针的拷贝,当然不会修改指向
3.4.1尽量不要移动原始指针
尽量不要移动指向内存块的原始指针,若有移动指针的操作,可以创建副本指针来使用。
int* p = malloc(sizeof(int) * ARR_LEN); // 定义一个临时指针用于移动指针操作 int* tmp = p;
void (void* arr){}这是arr是可以进行移动,因为是拷贝指针
vs中 free函数之后区间会标记为dd
3.4.2 不要两次free同一片内存区域,会导致未定义行为。
往往出现在在一个函数中free过,后来忘记了又在另一个中free。
所以说free交给特定函数销毁(就像专门的人做清洁)和creat函数一样,需要注意这点!
需要考一些技巧避免:
3.4.3 悬空指针
free后的实参指针就变成了指向一片已释放区域的指针,这就是"悬空指针",使用空指针导致未定义行为。
为了避免悬空指针为程序安全带来隐患,推荐在free掉指针指向的内存块后,及时将指针置为空指针。
3.4.4 避免不可到达内存空间
会导致更严重的内存泄漏,p就是以后都不可以到达的内存块,所以应该先free q再赋值
p = malloc(...); q = malloc(...); p = q;
puts函数会返回字符串的长度包含末尾的/0;
c语言一般不会返回错误,所以返回值很重要
4. 清零内存分配函数calloc(cleared allocation)动态分配初始化为0的数组
最大特点:分配时会自动初始化为0,其余与malloc一致
void* calloc(size_t num, size_t size); num是元素数量, size是每个元素内存大小
因此常用于在堆上分配数组空间
//也可以分配结构体数组
typedef struct {
int x;
int y;
} Node;
Node* node_arr_p = calloc(3, sizeof(Node));
对比malloc:malloc性能好,calloc更安全
5. 内存重分配函数 realloc(reallocation) 数组和数据结构动态扩容收缩必备
void* realloc(void* ptr, size_t new_size); ptr:指向已分配内存,new_size新内存大小
1.表现:
ptr为空,与malloc一致(不要这样用)
size为0,与free一致(不要这样用)
其他,调整已分配内存块的大小,尽量进行在原位置扩容实在不够再复制找大的地方,扩容部分不会初始化(截断丢弃高地址端就是指针另一端 和 扩容需要复制时丢弃的部分,那部分会自动进行free)
成功会返回新内存的指针,否则返回NULL(失败不会改变旧内存块)
错误写法:
代码块 15. 正确使用realloc函数-演示代码1
int len = 5;
int* arr_p = calloc(len, sizeof(int));
if (calloc == NULL){
// 分配失败处理
}
// 代码运行到这里,arr_p一定不是空指针
规范写法:
// p和arr_p指针类型一致
p = realloc(arr_p, new_size);
if (p == NULL){
// 分配失败处理
return 1;
}
// 代码运行到这里,realloc分配内存成功
arr_p = p;
规范行为:这样写代码既避免了(分配成功时)arr_p成为悬空指针,也不会因为realloc(分配失败)导致内存泄漏。不能用原始指针,而是用临时指针!
// 重分配内存缩减,惯用法
int new_size = 3;
int* tmp = realloc(arr, new_size * sizeof(int));
if (tmp == NULL) {
printf("realloc failed!\n");
exit(-1);
}
arr = tmp;
print_arr(arr, new_size);
// 重分配内存扩容,惯用法
int new_size2 = 10;
int* tmp2 = realloc(arr, new_size2 * sizeof(int));
if (tmp2 == NULL) {
printf("realloc failed!\n");
exit(-1);
}
arr = tmp2;
补:int* p2 = arr + size,一个指针加上数字表示从数组的下标为size开始的位置
6. 手动实现C++中的vector
数组在初始时就需要确定大小,vector可以进行动态扩容,c语言可以借助于malloc和realloc实现
6.1思路讲解
首先定义结构体
// 使用别名来命名元素类型,如果未来需要改变元素类型,只需修改这个别名即可。
// 这么做提升代码的可维护性和扩展性,这实际上是模拟了C++的泛型编程
typedef int ElementType;
//以后可能不是int类型这样,以后可以进行修改,但下文一定都用ElementType
typedef struct {
ElementType *data; // 指向动态分配数组的指针
int size; // 当前动态数组中元素的数量
int capacity; // 动态数组当前分配的最大容量
} Vector;
还需要定义相关的操作
// 初始化一个Vector动态数组.这实际上是模拟了C++的默认构造函数
Vector* vector_create();
// 销毁一个Vector动态数组,释放内存。这实际上模拟了C++的析构函数
void vector_destroy(Vector *v);
// 向动态数组末尾添加一个元素
void vector_push_back(Vector *v, ElementType element);
6.2头文件的使用
头文件主要用于存放以下结构:
- 函数的声明
- 结构体的定义
- 类型别名的定义
- 宏定义
- 等
头文件中进行声明,源文件中进行实现。可以实现多个源文件之间的共享函数的声明以及结构体、类型别名和宏的定义
实现模块化,复用性
6.2.2 头文件保护
头文件会互相依赖,源文件包含多个头文件,一个头文件可能会被包含多次
C/C++的头文件包含本质上是一种文本替换的过程,一个头文件被包含多次,就相当于一段代码在同一个文件中被书写多次,这在很多时候都是不允许,会引发编译错误。
c语言中头文件保护机制防止头文件出现多次
// 保护机制
#ifndef VECTOR_H
#define VECTOR_H
// 头文件中定义的函数的声明、结构体的定义、类型别名的定义等
#endif // !VECTOR_H
#pragma once 不是c/c++标准库中的内容,一般不用
宏命名时不能使用字符".",所以使用"_"替代文件后缀名中的"."
6.2.3 包含头文件与实现函数
1.使用" " 用于自定义的头文件
2.使用< > 用于标准库的头文件
6.3.1 实现vector_creat()
#define DEFAULT_CAPACITY 10 // 设置动态数组的默认最小容量
// 初始化一个Vector动态数组.这实际上是模拟了C++的默认构造函数
Vector* vector_create() {
// 先在堆上分配结构体Vector
Vector* v = calloc(1, sizeof(Vector)); // malloc需要手动初始化每一个成员,calloc方便安全一些
if (v == NULL){
printf("calloc failed in vector_create.\n");
return NULL; // 创建失败返回空指针
}
// 申请动态数组,并赋值给Vector的data成员
v->data = calloc(DEFAULT_CAPACITY, sizeof(ElementType)); // 此时数组中的元素都具有0值,而不是随机值
if (v->data == NULL){
printf("malloc failed in vector_create.\n");
// 不要忘记free结构体Vector,否则会导致内存泄漏
free(v);
return NULL; // 创建失败返回空指针
}
// 继续初始化Vector的其它成员
v->capacity = DEFAULT_CAPACITY;
// size已自动初始化为0值,所以不需要再次赋值了。但如果用malloc就不要忘记初始化它
return v;
}
当分配数组不能成功的时候,需要free(v)不然的话会内存泄漏
6.3.2vector_destroy()
// 销毁一个Vector动态数组,释放内存。这实际上模拟了C++的析构函数
void vector_destroy(Vector* v) {
free(v->data);
free(v);
}
应该先free动态数组,再free结构体。否则动态数组空间会无法释放,导致内存泄漏!
6.3.3vector_push_back()
#define THRESHOLD 1024
// 在C语言中,static修饰函数表示此函数仅在当前文件内部生效
// 类似于C++或Java中的访问权限修饰符private
static void vector_resize(Vector* v) {
// 只要调用这个函数肯定就是需要扩容的
int old_capacity = v->capacity;
int new_capacity = (old_capacity < THRESHOLD) ?
(old_capacity << 1) : // 容量还未超出阈值每次扩容2倍
(old_capacity + (old_capacity >> 1)); // 容量超出阈值每次扩容1.5倍
// 利用realloc重新分配动态数组
ElementType *tmp = realloc(v->data, new_capacity * sizeof(ElementType)); // realloc惯用法
if (tmp == NULL){
printf("realloc failed in resize_vector.\n");
exit(1); // 扩容失败,退出整个程序。或者也可以做别的处理
}
// 扩容成功,重新赋值Vector成员
v->data = tmp;
v->capacity = new_capacity;
}
// 向动态数组末尾添加一个元素
void vector_push_back(Vector* v, ElementType element) {
// 先判断是否需要扩容
if (v->capacity == v->size) {
vector_resize(v);
}
// 扩容完成后或不需要扩容,即向末尾添加元素
v->data[v->size] = element;
v->size++;
}
采用位运算的方式,速度更快
(old_capacity + (old_capacity >> 1)); 必须要加括号,因为+的优先级最高
所以它既不需要声明在头文件中,在实现它时也应该使用static修饰,以避免它被外界所调用。
static可以修饰全局变量,也可以用来修饰函数用于隐藏函数,因为在别的源文件不会使用这个函数也尽量不去修改!!
7.二级指针(解引用一次为了修改一级指针指向)
二级指针,或称为指针的指针,也就是一个指向另一个指针的指针,也就是存储了另一个指针变量地址的指针。通过两个星号(**)定义
7.1头插法实现单向链表
typedef int DataType;
typedef struct node { // 这里的名字不能省略
DataType data;
// 编译到该行时,别名Node还未定,所以这里仍然需要使用struct关键字来声明指向下一个结点的结构体指针
// Error: Node* next;
struct node* next;
} Node;
Node* insert_head(Node* list, DataType data) {
// 1.创建新节点
Node *new_node = malloc(sizeof(Node));
if (new_node == NULL){
printf("malloc failed in insert_head.\n");
exit(1);
}
// 2.初始化新节点的数据域
new_node->data = data;
// 3.新结点的next指针指向原本第一个节点
new_node->next = list;
// 4.返回新结点指针(头指针)
return new_node;
}
【不行】 如果有返回值的话,就需要不断接,以及修改头指针,所以想直接修改头指针
// main函数中
Node *list = NULL; // 表示链表为空,一个结点都没有
insert_head(list, 1);
insert_head(list, 2);
insert_head(list, 3);
insert_head(list, 4);
//错误的,不能够实现,因为以为修改的头指针实际上是指针的副本,所以采用二级指针
void insert_head(Node* list, E data) {
// 1.创建新节点
Node* new_node = malloc(sizeof(Node));
if (new_node == NULL) {
printf("malloc failed in insert_head.\n");
exit(1);
}
// 2.初始化新节点的数据域
new_node->data = data;
// 3.新结点的next指针指向原本第一个节点
new_node->next = list;
// 4.将头指针指向新结点
list = new_node;
}
【还是不行】因为是指针的副本,所以修改的是副本的指向,是不对的,要用二级指针
因为二级指针是可以改变一级指针指向的地址
二级指针 = 一级指针的位置
*二级 = 一级指针方向(一级指针的内容)
**二级 = 一级指针指向地址的内容
int *p; // 一级指针,一般直接叫指针即可
int **pp; // 二级指针
int num = 10;
p = # // 一级指针指向value变量
pp = &p; // 二级指针指向指针变量p
int another_value = 20;
p = &another_value; // 通过一级指针修改指向
*pp = &another_value; // 通过二级指针修改一级指针p的指向
**pp = 100; // 通过二级指针修改num的值
这在函数调用中尤其有用,因为即使在值传递,函数只得到副本的情况下,也可以通过二级指针的副本来修改原始指针。
7.3 利用二级指针实现无返回值头插函数
void insert_head(Node** p, DataType data) {
// 1.创建新节点
Node* new_node = malloc(sizeof(Node));
if (new_node == NULL) {
printf("malloc failed in insert_head.\n");
exit(1);
}
// 2.初始化新节点的数据域
new_node->data = data;
// 3.新结点的next指针指向原本第一个节点
new_node->next = *p;
// 4.将头指针指向新结点
*p = new_node;
}
【可以】虽然不能修改二级指针的指向,但是可以修改一级指针(头指针)的指向
【注意】1.二级指针记得对于一级指针修改地址
2.悬空指针问题
需要使用二级指针借助上面的内容进行理解
8. 【了解除了回调】函数指针 (Pointer to Function))(存储函数的地址:指令序列起始位置)
将一个函数像是一个参数一样传给另外一个函数,这样的函数叫做回调函数
形式:函数返回值类型 (*函数指针名)(函数形参列表);(了解)
// 声明一个指向"返回值类型是void、不接受任何参数的函数"的指针
void (*fun_ptr)(void);
void (*a)(void) = fun_ptr; fun_ptr当作指针来使用
test函数需要传入一个"返回值类型是void、不接受任何参数的函数"的指针
void test(void fun_ptr(void)){}*可以省略
8.2【了解】 给函数指针类型起别名
使用函数指针时,最好起别名增强可读性。
typedef void(*FunctionPtrype)(void);
void(*p)(void) = test;
FuntionPtrType p2 = test;
//使用别名FuntionPtrType表明一种函数,使用别名可以简洁的定义
【回调函数】 函数指针的使用情景:让work表示一类函数
8.5 函数指针的经典应用:qsort函数【结构体排序】
不稳定,会改变元素的相对位置
void qsort(void *base, size_t num, size_t size, int (*compare)(const void *, const void *));
base数组,num元素数量,size元素大小,compare用于比较各种元素大小(数据结构,int)
例子:
typedef struct {
int stu_id;
char name[25];
int age;
int total_socre;
} Student;
如何c语言输入:
// 用于初始化一个结构体元素
void init_student(Student* stu, int stu_id, const char* name, int age, int total_socre) {
stu->stu_id = stu_id;
strncpy(stu->name, name, sizeof(stu->name) - 1);
stu->name[sizeof(stu->name) - 1] = '\0'; // 确保字符数组以空字符结束,能够表示一个字符串
stu->age = age;
stu->total_socre = total_socre;
}
// 用于打印结构体数组
void print_stus(Student* stus, int len) {
for (int i = 0; i < len; i++) {
printf("Student %d: ID=%d, Name=%s, Age=%d, Score=%d\n",
(i + 1), stus[i].stu_id, stus[i].name, stus[i].age, stus[i].total_socre);
}
}
// main函数当中:
int len = 10;
Student stus[10] = { 0 };
// 初始化结构体元素
init_student(stus, 1, "ZS", 18, 600);
init_student(stus + 1, 3, "Maria", 17, 620);
init_student(stus + 2, 9, "Mark", 20, 600);
init_student(stus + 3, 6, "LS", 18, 700);
init_student(stus + 4, 4, "BS", 18, 600);
init_student(stus + 5, 7, "WS", 30, 600);
init_student(stus + 6, 10, "TS", 18, 600);
init_student(stus + 7, 2, "ABC", 16, 600);
init_student(stus + 8, 5, "AA", 18, 600);
init_student(stus + 9, 8, "GG", 18, 400);
【理解】对于cmp函数的理解可以借助于c++中一般定义的static bool cmp {};
区别c++中是<
//c++中的应用
static bool cmp(vector<int>& a, vector<int>& b){
if(a[0] == b[0]) return a[1] < b[1];
return a[0] < b[0];
}
sort(intervals.begin(), intervals.end(), cmp);
【理解】如果从小到大就把一开始的放前面,用 - 号
// qsort函数会将学生数组按照学号,从小到大排序
// 该比较规则认为: 学号越小,学生越小
int my_cmp(const void* a, const void* b) {
// void指针需要类型转换后才能使用
Student* s1 = a;
Student* s2 = b;
return (s1->stu_id) - (s2->stu_id);
}
// qsort函数会将学生数组按照成绩由高到低排序
// 该比较规则认为: 成绩越高,学生越小
int my_cmp2(const void* a, const void* b) {
// void指针需要类型转换后才能使用
Student* s1 = a;
Student* s2 = b;
return (s2->total_socre) - (s1->total_socre);
}
/*
* qsort函数的排序规则是
* 先按总分从高到低进行排序,成绩相同则按照年龄从低到高排序
* 若仍然相同,按照名字的字典顺序排序
*/
int my_cmp3(const void* a, const void* b) {
// void指针需要类型转换后才能使用
Student* s1 = a;
Student* s2 = b;
if (s1->total_socre != s2->total_socre) {
return s2->total_socre - s1->total_socre;
}
// 运行到这里,总分一定是相同的,继续依据年龄来比较
if (s1->age != s2->age) {
return s1->age - s2->age;
}
// 运行到这里,总分和年龄都相同了,继续根据名字的字典顺序排序
return strcmp(s1->name, s2->name);
}
qsort(stus, len, sizeof(Student), my_cmp3); // 按照my_cmp3函数的比较规则从小到大排序
c语言中函数名就是指针
练习:1. 错误反馈标志:
写宏函数进行实现 这样的话无论是指针还是int类型数字都是可以的而不能用函数
可以不用dowhile,只是为了防止else指针悬空。
2. 字面值字符串、全局变量静态存储期限字符,不用考虑生命周期问题整个程序运行期间都生效。
局部变量字符串,那么这个Vector将不能跨函数使用。
堆字符串,那么这个Vector需要管理它存储的字符串的生命周期。