BIT - 6 自定义类型和动态内存管理( 9000字详解 )

一:自定义类型

1.1:结构体

在生活中,基本数据类型可以描述绝大多数的物体,比如说名字,身高,体重,但是还有一部分物体还不足够被描述,比如说:我们该如何完整的描述一本书呢?一本书有书的名字,价格,作者,页数,等等,由此便有了结构体这个概念

C 语言中的结构体是一种自定义的数据类型,它允许我们将不同类型的数据组合在一起,形成一个逻辑上相关的数据单元。结构体可以包含不同的数据类型,如整型、字符型、浮点型、指针等,并且可以根据需要添加多个成员变量。

结构体的定义使用关键字 struct,后面跟着结构体的名称和花括号。在花括号中定义结构体的成员变量,每个成员变量由数据类型和名称组成,中间用分号分隔。

下面是一个定义了一个简单的学生结构体的例子:

struct Student {
    int id;
    char name[20];
    float score;
};

其中 Student 是结构体的名字,

int id; 
char name[20]; 
float score; 

这些是结构体的成员

1.1.1 结构体成员的赋值

结构体成员的访问和赋值有两种方式:

  • 第一是通过 . 操作符
  • 第二是通过 -> 操作符

下面是代码示例:

#include <stdio.h>

struct Stu
{
	char name[20];
	int age;
	char sex[5]; 
	char id[15];
};

int main()
{
	struct Stu s = { "张三", 20, "男", "20180101" };
	printf("name = %s age = %d sex = %s id = %s\n", s.name, s.age, s.sex, s.id);

	struct Stu* ps = &s;
	printf("name = %s age = %d sex = %s id = %s\n", ps->name, ps->age, ps->sex, ps -> id);

	return 0;
}

当声明结构体的同时声明变量并进行初始化时,可以使用以下方式:

struct Student {
    int id;
    char name[20];
    float score;
} sn1 = { 123456, "John Doe", 85.5 };

在上述代码中,我们声明了一个名为Student的结构体,并创建了名为sn1的变量。同时,我们使用大括号 { } 初始化了sn1的成员变量。

  • sn1.id 被初始化为 123456
  • sn1.name 被初始化为 "John Doe"
  • sn1.score 被初始化为 85.5

请注意,结构体变量的初始化方式与结构体成员的顺序要相同,如果不想相同的话可以这样:

struct Student {
    int id;
    char name[20];
    float score;
} sn1 = { .score = 85.5, .name = "John Doe", .id = 123456};

注意:

#include <stdio.h>

struct Student {
    int id;
    char name[20];
    float score;
};

int main() {
    Student sn1 = { 123456, "John Doe", 85.5 }; //这是错的
    struct Student sn1 = { 123456, "John Doe", 85.5 };//这才是正确的
    return 0;
}

1.1.2 结构体的自引用

下面请问代码1和代码2哪个是正确的:

//代码1
struct Node
{
    int data;
    struct Node next;
};

//代码2
struct Node
{
    int data;
    struct Node* next;
};

代码 1 是错误的,因为在结构体 Node 中,next 成员的类型是 “struct Node”,结构体包含一个嵌套的结构体实例,这会导致无限递归,使结构体的大小变得无法确定。

代码 2 是正确的。在结构体 Node 中,next 成员的类型是指针类型 " struct Node* ",指针的大小是固定的,所以这不会导致无限递归

下面再来看一个代码:

//代码3
typedef struct
{
    int data;
    Node* next;
}Node;

//代码4
typedef struct Node
{
    int data;
    struct Node* next;
}Node;

代码 3 是错误的,因为在结构体定义中使用 Node* next; 时,编译器尚未知道 Node 的定义。

代码 4 是正确的,因为在结构体内部使 struct Node* next;,这样编译器可以识别 Node 作为结构体类型的名称,并分配正确的内存空间。

1.1.3 结构体内存对齐

我们都知道,在 c 语言中,一个 int 类型占4字节,一个 char 类型占1字节,那么对于一个结构体,我们该如何知道它的大小呢?下面看一段代码:

int main() {

    struct S3
    {
        double d;
        char c;
        int i;
    };
    printf("%d\n", sizeof(struct S3));

    return 0;
}

我们可能会觉得,double占8字节,char占1字节,int占4字节,那么这个结构体就应该占13个字节,事实真的是这样吗?请看运行结果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/927727c1a58c453f8df0a71c1949c757.png
为什么是 16 呢?下面讲解一下c语言的内存对齐规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处。

  2. 其他成员变量要对齐到对齐数的整数倍的地址处。

  3. 对齐数 = 编译器对齐数默认值与该成员大小的较小值。

  4. VS中默认的对齐数值为8

  5. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

  6. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

下面通过例子讲解:

int main() {

    //练习1
    struct S1
    {
        char c1;
        int i;
        char c2;
    };
    printf("%d\n", sizeof(struct S1));

    //练习2
    struct S2
    {
        char c1;
        char c2;
        int i;
    };
    printf("%d\n", sizeof(struct S2));

    //练习3
    struct S3
    {
        double d;
        char c;
        int i;
    };
    printf("%d\n", sizeof(struct S3));

    //练习4-结构体嵌套问题
    struct S4
    {
        char c1;
        struct S3 s3;
        double d;
    };
    printf("%d\n", sizeof(struct S4));

    return 0;
}

运行结果是:
在这里插入图片描述

下面进行讲解:
在这里插入图片描述

  1. 首先,第一个成员在 0 偏移量处,所以把 c1 放在 0 处
  2. 接着,其他成员要对齐到对齐数上,vs 默认对齐数是 8,而 int 是 4 字节,所以 int 的对齐数是 4,i 要放在 4 开头处
  3. 同样的,c2 放在 8 开头处,
  4. 所以整个的大小为 9,结构体中的最大对齐数是 4,9 不是 4 的倍数,所以要提升到 12
  5. 所以结果为 12

在这里插入图片描述

  1. 首先,第一个成员在 0 偏移量处,所以把 c1 放在 0 处
  2. 同样的,c2 放在 1 开头处,
  3. 接着,其他成员要对齐到对齐数上,vs 默认对齐数是 8,而 int 是 4 字节,所以 int 的对齐数是 4,i 要放在 4 开头处
  4. 所以整个的大小为 8,结构体中的最大对齐数是 4,8 刚好是 4 的倍数
  5. 所以结果为 8

在这里插入图片描述

  1. 首先,第一个成员在 0 偏移量处,所以把 d 放在 0 处
  2. 同样的,c 放在 8 开头处,
  3. 接着,其他成员要对齐到对齐数上,vs 默认对齐数是 8,而 int 是 4 字节,所以 int 的对齐数是 4,i 要放在 12 开头处
  4. 所以整个的大小为 16,结构体中的最大对齐数是 8,16 刚好是 8 的倍数
  5. 所以结果为 16

在这里插入图片描述

  1. 首先,第一个成员在 0 偏移量处,所以把 c1 放在 0 处
  2. 结构体的对齐数是是这个结构体中的最大对齐数,由上一题可以知道是8
  3. 所以结构体放在 8 开头的地方,长度为 16
  4. d 的对齐数为 8,所以要放在 24 处,长度为 8
  5. 所以和为 32,这个结构体中最大的对齐数是 16,32刚好是16的倍数
1.1.3.1 修改默认对齐数

在 C 语言中,我们可以使用#pragma pack指令来设置对齐规则。该指令可以修改对齐数

下面是一个使用#pragma pack指令修改默认对齐数的示例:

#pragma pack(1)  // 设置对齐数为 1 字节

struct MyStruct {
    char a;
    int b;
    char c;
};

#pragma pack()  // 恢复默认对齐数

int main() {
    // 访问结构体中的成员
    struct MyStruct s;
    s.a = 'A';
    s.b = 10;
    s.c = 'C';

    return 0;
}

1.1.4 结构体传参

struct S
{
	int data[1000];
	int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体传参
void print1(struct S s)
{
	printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
	printf("%d\n", ps->num);
}
int main()
{
	print1(s);  //传结构体
	print2(&s); //传地址
	return 0;
}

上面的 print1 和 print2 哪个好些?答案是 print2 要好些。

函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。所以结构体传参的时候,最好要传结构体的地址,因为地址的大小的固定的 4 或 8 字节。

1.2 位段

C 语言中的位段(bit-fields)是一种特殊的数据类型,用于在结构体中按位对内存进行分配。

位段的主要作用是优化内存空间的利用,可以用较少的位数占用更少的内存,位段可以按照成员的顺序进行紧凑排列,节省内存空间,位段的语法形式如下:

struct {
    type fieldName : width;
};

其中,type表示位段的数据类型,必须是intunsigned intsigned int其中一种;fieldName是位段的名称;width表示位域的宽度,即占用的位数。

下面是一个示例代码,说明了如何使用位段:

#include <stdio.h>

struct {
    unsigned int flag1 : 1;
    unsigned int flag2 : 2;
    unsigned int flag3 : 3;
} myFlags;

int main() {
    myFlags.flag1 = 1;
    myFlags.flag2 = 2;
    myFlags.flag3 = 3;

    printf("flag1: %d\n", myFlags.flag1);
    printf("flag2: %d\n", myFlags.flag2);
    printf("flag3: %d\n", myFlags.flag3);

    return 0;
}

在上述代码中,定义了一个匿名结构体,其中包含了三个位段flag1flag2flag3flag1占用1位,flag2占用2位,flag3占用3位,在main函数中,我们对这些位段进行赋值,并使用printf函数输出它们的值。输出结果为:

flag1: 1
flag2: 2
flag3: 3

这说明了位段可以正常工作,并且按照指定的位宽正确地进行赋值和输出。

需要注意的是,位段的使用具有一定的限制。由于位宽是有限的,因此不能超过位宽所能表示的范围。

例如:

  • flag1 的位数是 1,那么它的取值只有 2 种( 0 ~ 1 )
  • flag2 的位数是 2,那么它的取值只有 4 种( 0 ~ 3 )
  • flag3 的位数是 3,那么它的取值只有 8 种( 0 ~ 7 )

注意:

  • 位宽不能超过所属类型的位数。比如,int类型一般有32位,因此不能超过32位。
  • 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

位段每次开辟空间是以 int(4字节)或者 char(1字节)来开辟空间的,当我们将位段中所有成员的位数加起来,小于等于 32,那么这个位段就是 4 字节,同理,大于 32 小于 64,就是 8 字节,大于 64 小于 96 就是 12 字节,以此类推

1.3:联合(共用体)

联合允许不同的数据类型共享相同的内存空间,联合的大小将取决于其最大成员的大小。

下面是一个简单的代码示例来说明联合的使用:

#include <stdio.h>

// 定义一个联合
union Data {
    int i;
    float f;
    char str[20];
};

int main() {
    union Data data;  // 声明一个联合变量
    printf("Memory size occupied by data : %d\n", sizeof(data));
    data.i = 10;  // 设置 data 的整数成员
    printf("data.i : %d\n", data.i);
    data.f = 220.5;  // 设置 data 的浮点数成员
    printf("data.f : %f\n", data.f);
    strcpy(data.str, "C Programming");  // 设置 data 的字符串成员
    printf("data.str : %s\n", data.str);
    printf("Memory size occupied by data : %d\n", sizeof(data));
    return 0;
}

运行结果如图所示:

在这里插入图片描述

请注意,当我们设置一个成员的值后,其他成员的值就会发生改变,因为它们共享同一内存空间。

二:动态内存管理

首先为什么存在动态内存分配,它的需求是什么?我们已经掌握的内存开辟方式有:

int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间

但是上述的开辟空间的方式有两个特点:

  1. 空间开辟大小是固定的。
  2. 数组在申明的时候,必须指定数组的长度。

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小并不是固定的,而是能够动态变换的,因此便有了动态内存分配

2.1 malloc

malloc() 函数是 C 语言中用于动态分配内存的函数,其原型如下:

void* malloc(size_t size);

该函数用于在运行时从堆中分配指定大小的内存空间(单位是字节),并返回一个指向该内存空间起始位置的指针。

  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个 NULL 指针,因此 malloc 的返回值一定要做检查。
  • 返回值的类型是 void* ,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 如果参数 size 为 0,malloc 的行为是标准是未定义的,取决于编译器。

下面是一个使用 malloc() 函数的示例代码,说明如何动态分配一个整型数组并进行操作:

#include<stdio.h>
#include<stdlib.h>

int main() {
    int n, i;
    int* arr;
    printf("Enter the size of the array: ");
    scanf("%d", &n);
    
    // 分配内存空间
    arr = (int*)malloc(n * sizeof(int));
    
    // 检查内存分配是否成功
    if (arr == NULL) {
        printf("Memory allocation failed.\n");
        return 0;
    }
    printf("Enter %d elements:\n", n);
    
    // 从键盘获取数组元素
    for (i = 0; i < n; i++) {
        scanf("%d", &arr[i]);
    }
    printf("The elements in the array are: ");
    
    for (i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    
    // 释放内存空间
    free(arr);
    arr = NULL;
    return 0;
}

注意:使用完动态分配的内存后,务必调用 free() 函数释放该内存,以避免内存泄漏问题。同时,要确保在释放内存后,将指针设置为 NULL,以防止出现悬挂指针的情况。

2.2 free

C 语言提供了另外一个函数 free,专门是用来做动态内存的释放和回收的,函数原型如下:

void free (void* ptr);

free 函数用来释放动态开辟的内存。

  • 如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的。
  • 如果参数 ptr 是NULL指针,则函数什么事都不做。

malloc 和 free 都声明在 stdlib.h 头文件中。

注意:malloc 申请的空间如果不通过 free 释放掉,那么这个空间是不会被释放的,只有当你退出程序后,才会释放,所以对于动态空间,我们要及时通过 free 释放,并将指向这块空间的指针置空,避免野指针

2.3 calloc

calloc() 是 C 语言中用于动态分配内存空间的函数,它与 malloc() 函数类似。不同之处在于 calloc() 在分配内存空间的同时会将内存块中的每个字节都初始化为零。

calloc() 函数的原型如下:

void* calloc(size_t num, size_t size);

num 参数表示要分配的元素数量,size 参数表示每个元素的大小。calloc() 函数会为 num * size 个连续的字节分配内存空间,并把空间的每个字节初始化为0,返回一个指向分配内存空间起始地址的指针。如果内存不足,calloc() 函数会返回一个空指针(NULL)。

下面是一个使用 calloc() 函数的示例代码:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *nums;
    int length;
    
    printf("请输入数组长度:");
    scanf("%d", &length);
    
    // 动态分配内存空间
    nums = (int *)calloc(length, sizeof(int));
    if (nums == NULL) {
        printf("内存分配失败!\n");
        return 1;
    }
    
    for (int i = 0; i < length; i++) {
        printf("请输入第 %d 个元素:", i + 1);
        scanf("%d", &nums[i]);
    }
    printf("数组元素为:");
    
    for (int i = 0; i < length; i++) {
        printf("%d ", nums[i]);
    }
    printf("\n");
    
    // 释放内存空间
    free(nums);
    return 0;
}

2.4 realloc

realloc() 函数是 C 语言中用于重新分配内存空间的函数。它可以修改先前由 malloc()calloc()realloc() 分配的内存块的大小。realloc() 函数的声明如下:

void *realloc(void *ptr, size_t size);

ptr 是一个指向要重新分配大小的内存块的指针,size 是重新分配后的新大小。函数返回一个指向重新分配后内存块的指针,如果无法分配足够的内存,则返回 NULL

下面是一个示例代码,演示了 realloc() 函数的用法:

#include <stdio.h>
#include <stdlib.h>

int main() {
    // 原始内存块的初始大小为 3
    int *ptr = (int *)malloc(3 * sizeof(int));
    
    // 分配失败的情况下,返回 NULL
    if (ptr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    
    // 打印原始内存块的值
    printf("原始内存块:\n");
    for (int i = 0; i < 3; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");
    
    // 将内存块重新分配为 5,即增加了 2 个元素的空间
    ptr = realloc(ptr, 5 * sizeof(int));
    
    // 重新分配失败的情况下,返回 NULL
    if (ptr == NULL) {
        printf("内存重新分配失败\n");
        return 1;
    }
    
    // 新增加的元素赋值
    ptr[3] = 4;
    ptr[4] = 5;
    
    // 打印重新分配后的内存块的值
    printf("重新分配后的内存块:\n");
    for (int i = 0; i < 5; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");
    
    // 释放内存
    free(ptr);
    
    return 0;
}

注意:realloc 在调整内存空间的是存在两种情况

  • 情况1:原有空间之后有足够大的空间
  • 情况2:原有空间之后没有足够大的空间

在这里插入图片描述

情况 1:当是情况 1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。

情况 2:当是情况 2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。

2.4 有关动态内存分配容易出问题的地方

  1. 对 NULL 指针的解引用操作:
void test()
{
	int *p = (int *)malloc(INT_MAX/4);
	*p = 20;//如果p的值是NULL,就会有问题
	free(p);
}
  1. 对动态开辟空间的越界访问
void test()
{
	int i = 0;
	int *p = (int *)malloc(10*sizeof(int));
	
	if(NULL == p)
	{
		exit(EXIT_FAILURE);
	}
	
	for(i=0; i<=10; i++)
	{
		*(p+i) = i;//当i是10的时候越界访问
	}
	free(p);
}
  1. 对非动态开辟内存使用 free 释放( free 的空间必须是动态开辟的 )
void test()
{
	int a = 10;
	int *p = &a;
	free(p);//ok?
}
  1. 使用 free 释放一块动态开辟内存的一部分( 要释放就释放完 )
void test()
{
	int *p = (int *)malloc(100);
	p++;
	free(p);//p不再指向动态内存的起始位置
}
  1. 对同一块动态内存多次释放
void test()
{
	int *p = (int *)malloc(100);
	free(p);
	free(p);//重复释放
}

6:动态开辟内存忘记释放( 没有 free 动态空间,出了 test 函数 p 就被销毁了,没有指针可以和申请的动态空间关联了,造成了内存泄漏)

void test()
{
	int *p = (int *)malloc(100);

	if(NULL != p)
	{
		*p = 20;
	}

}
int main()
{
	test();
}

三:c/c++ 程序内存开辟

c/c++ 中程序内存区域划分如图所示:
在这里插入图片描述
C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。(栈区开辟效率高)

  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS(操作系统)回收 。分配方式类似于链表。

  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。

  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

  • 14
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ice___Cpu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值