C语言——指针的高级引用

目录

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>当中:

  1. malloc
  2. calloc
  3. 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头文件的使用

头文件主要用于存放以下结构:

  1. 函数的声明
  2. 结构体的定义
  3. 类型别名的定义
  4. 宏定义

头文件中进行声明,源文件中进行实现。可以实现多个源文件之间的共享函数的声明以及结构体、类型别名和宏的定义

实现模块化,复用性

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 = &num;       // 一级指针指向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需要管理它存储的字符串的生命周期。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值