• 10_C++_《数据类型和内存分区》_笔记整理


    Part 1 数据类型

    数据类型概念

    • 数据类型是为了更好进行内存的管理,让编译器能确定分配多少内存
    • 类型相同的数据具有相同的表示形式存储格式以及相关操作
    • 数据类型可以理解创建变量的模具 → \rightarrow 固定大小内存的别名

    typedef

    • 主要用途:给类型起别名 → \rightarrow 简化struct关键字
    • 可以区分数据类型 → \rightarrow char * p1,p2:p1是字符指针、p2是字符变量;解决方法:typedef char CHAR,这样就不会出现上述情况
    • 提高代码移植性 → \rightarrow 变量类型发生不兼容可以直接改typedef的内容即可

    void

    • 无类型,不可以创建变量,无法分配内存 → \rightarrow void a = 10;错误
    • 限定函数返回值和参数列表 → \rightarrow void sum(void)相当于告诉用户这里不需要传入参数,当在main函数里面传参的时候会警告,函数返回值同理
    • void * → \rightarrow 万能指针 → \rightarrow 不需要强制类型转换即可给其他指针赋值

    sizeof

    • 本质:不是函数,而是一个操作符 → \rightarrow
      sizeof a这么写也可以
      统计类型占内存空间必须要加小括号
      统计变量占内存空间时候可以不加
    • 返回值:无符号整型unsigned int
    • 作用:统计数组、结构体的大小
    • 注意:数组名称如果在参数列表中,会退化为指针,指向数组的第一个元素

    变量的修改方式

    1. 直接修改 → \rightarrow 直接对赋值进行修改
    2. 间接修改 → \rightarrow 通过指针对内存进行修改
    void test01()
    {
    	int a = 10;
    	//直接修改
    	a = 20;
    	printf("a = %d\n", a);
    	//间接修改
    	int * p = &a;
    	*p = 100;
    
    	printf("a = %d\n", a);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    1. 对自定义数据类型进行了修改
    //对于自定义数据类型
    struct Person
    {
    	char a; 
    	int b;  
    	char c; 
    	int d; 
    };
    void test02()
    {
    	struct Person p1 = { 'a', 10, 'b', 20 };
    	//直接修改,用点域
    	p1.d = 1000;
    	//间接修改,用“箭头”
    	struct Person * p = &p1;
    	p->d = 2000;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    1. 结构体指针说明
      (1)当定义结构体指针struct Person * p;时候,p++表示访问下一个同类结构体
      (2)当需要用指针访问结构体成员时,可char *p = &p1,获得结构体的首地址,然后按char型进行遍历访问,当访问到int型成员时,再强制转换。
    void test02()
    {
    	struct Person p1 = { 'a', 10, 'b', 20 };
    	struct Person * p = &p1;
    	char * pPerson = p;
    	// 用char指针遍历,遇到需要的数组直接强制转换
    	printf("d = %d\n", *(int*)(pPerson + 12));
    	// 先强制转换成int型指针,然后再将指针指向的数据强制int
    	printf("d = %d\n", *(int*)((int*)pPerson +3) );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    运行之前的内存分区

    编写c程序转成exe文件的过程

    • 预处理 → \rightarrow 宏定义展开、头文件展开、条件编译,这里并不会检查语法
    • 编译 → \rightarrow 检查语法,将预处理后文件编译生成汇编文件
    • 汇编 → \rightarrow 将汇编文件生成目标文件(二进制文件)
    • 链接 → \rightarrow 将目标文件链接为可执行程序

    二进制文件结构

    001

    • 【代码区(text)】共享性 → \rightarrow 可以多次执行程序;只读性 → \rightarrow 防别人修改指令
    • 【静态数据/全局初始化数据区(data)】已经初始化的静态变量、全局变量、静态全局变量、常量(初始化的字符型)
    • 【未初始化的初始化区域(bss)】未初始化的全局变量、静态变量,在程序开始前为0或者NULL

    运行之后的内存分区

    • 【栈区(stack)】先进后出的内存结构,存放函数的参数值、返回值、局部变量等,局部变量的生存周期为申请到释放该段栈空间
    • 【堆区(heap)】容量要远远大于栈,一般由程序员分配(malloc)和释放(free),若程序员不释放,程序结束时由操作系统回收。

    栈区注意事项

    不要返回局部变量的地址
    局部变量在函数执行之后就释放了,我们没有权限取操作释放后的内存

    案例1

    //栈,不要返回局部变量的地址
    int * func()
    {
    	int a = 10;
    	return &a;
    }
    void test01()
    {
    	int * p = func();
    	//a的内存已经被释放了,我们没有权限去操作这块内存
    	//这两个打印的结果虽然是对的,但是原理不准确
    	printf("a = %d\n", *p);
    	printf("a = %d\n", *p);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    案例2

    char * getString()
    {
    	char str[] = "hello world";
    	// 返回字符串名字相当于返回字符串的首地址
    	// 同理,在程序释放后就消失了
    	return str;
    }
    void test02()
    {
    	char * p = NULL;
    	// 会受到无效的指针
    	p = getString();
    	printf("%s\n", p);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    堆区的注意事项

    1. 在堆区开辟的数据,记得手动开辟(malloc),手动释放(free
    int * getSpace()
    {
    	// 实际上开辟的是一个具有5个元素的数组
    	int * p = malloc(sizeof(int)* 5);
    	if (p == NULL)
    	{
    		return NULL;
    	}
    	for (int i = 0; i < 5;i++)
    	{
    		p[i] = i + 100;
    	}
    	// 将堆区的地址指针返回给main函数
    	// 自身p在getSpace()结束后消失
    	return p;
    }
    void test01()
    {
    	int * p = getSpace();
    	for (int i = 0; i < 5;i++)
    	{
    		printf("%d\n", p[i]);
    	}
    	// 手动在堆区创建的数据,记得手动释放
    	free(p);
    	// free(p)后指针p指向“无依靠”,因此设置NULL承接p规范一些
    	p = NULL;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    1. 如果在主调函数中没有给指针分配内存,那么被调函数中需要利用高级指针给主调函数中指针分配内存
    // 比主调函数高一级的指针承接
    void allocateSpace2(char ** pp)
    {
    	char * temp = malloc(100);
    	memset(temp, 0, 100);
    	strcpy(temp, "hello world");
    	*pp = temp;
    }
    void test03()
    {
    	char * p = NULL;
    	allocateSpace2(&p);
    	// 这样才能打印出字符串
    	printf("%s\n", p);
    	free(p);
    	p = NULL;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    数据区注意事项

    static

    编译阶段分配内存,只能在当前文件内使用,只初始化一次

    extern

    全局变量,C语言下默认的全局变量前都隐藏的加了该关键字

    //1、静态变量
    static int a = 10; 
    //特点:只初始化一次,在编译阶段就分配内存,属于内部链接属性,只能在当前文件中使用
    void test01()
    {
    	static int b = 20; 
    	//局部静态变量,作用域只能在当前test01中
    }
    //2、全局变量
    extern int g_a = 100; 
    //在C语言下 全局变量前都隐藏加了关键字  extern,属于外部链接属性
    void test02()
    {
    	extern int g_b;
    	//告诉编译器 g_b是外部链接属性变量,下面在使用这个变量时候不要报错
    	printf("g_b = %d\n", g_b);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    const

    const修饰全局变量const修饰局部变量
    直接修改失败失败
    间接修改失败 → \rightarrow 常量区,受到保护成功 → \rightarrow 放在栈区,本身是伪常量 → \rightarrow 伪常量不能用来定义数组

    字符串常量

    • 不同的编译器可能有不同的处理方式。ANSI没有指定出标准。有些编译器可以修改字符串常量,有些不可以。
    • 有些编译器将相同的字符串常量看成同一个。VS的编译器是将重复定义的字符串看成一个内存,目的是便于节省空间。

    Part 2 内存分区

    宏函数

    • 在一定程度上会比普通函数效率高,普通函数会有入栈出栈的时间开销
    • 将比较频繁短小的函数 写为宏函数,相当于直接跑源码
    • 优点: 以空间时间
    • 缺点:注意运算的完整性
    # define sum(x,y) x+y
    void test()
    {
    	int res = 0;
    	int a = 10;
    	int b = 20;
    	res = sum(a,b)*20;
    	// 结果显示是410
    	// 这是因为:10+20*20 = 10+400 = 410
    	// 宏定义会出现这样的简单替换
    	// 解决方法:增加括号
    	printf("The result is %d.\n",res)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    函数调用过程

    一个函数调用过程所需要的信息一般包括以下几个方面:

    1. 函数的返回地址 → \rightarrow 跑完一个函数后要能回来main里面
    2. 函数的参数 → \rightarrow 对于形式参数需要入栈
    3. 局部变量 → \rightarrow 在被调函数中生成的局部变量要入栈
    4. 保存的上下文 → \rightarrow 包括在函数调用前后需要保持不变的寄存器

    函数调用惯例

    主调函数被调函数都必须有一致的约定 → \rightarrow 正确的调用函数 → \rightarrow 调用惯例

    调用惯例出栈方(对函数而言)参数传递名字修饰
    cdecl(默认调用方从右至左参数入栈下划线+函数名
    stdcall本身从右至左参数入栈下划线+函数名+@+参数字节数
    fastcall本身前两个参数由寄存器传递,其余参数通过堆栈传递@+函数名+@+参数的字节数
    pascal函数本身从左至右参数入栈较为复杂,参见相关文档

    变量传递分析

    要根据程序的生命周期决定

    栈的生成方向以及内存存储方式

    1. 栈底 → \rightarrow 高地址
    2. 栈顶 → \rightarrow 低地址
    3. 高位字节数据 → \rightarrow 高地址
    4. 低位字节数据 → \rightarrow 低地址
    5. 小端对齐(家用电脑);大端对齐(大型网络服务器);
  • 相关阅读:
    Pytorch实战教程(二)-PyTorch基础
    【软考学习5】流水线基本概念、周期执行时间、吞吐率、加速比和效率的计算
    数据探索的新前沿:可视化大屏交互功能
    LAL v0.32.0发布,更好的支持纯视频流
    [附源码]Python计算机毕业设计 楼盘销售管理系统
    【POJ No. 1330】 最近公共祖先 Nearest Common Ancestors
    两种高效的事件处理模式——Reactor和Proactor
    Jupyter如何开启Debug调试功能
    Google Earth Engine(GEE)——一个简单的多指数影像的加载和下载以北京市为例
    CSS篇九
  • 原文地址:https://blog.csdn.net/m0_48948682/article/details/125993846