C++代码基本内存操作及原理

news2024/12/25 1:41:17

文章目录

  • 前言
  • 操作系统内存
    • 地址空间
    • 基本数据类型
    • sizeof运算符
    • 指针运算
  • 内存分配与回收
    • 堆与栈
      • malloc/new与free/delete
    • 基本类型的指针操作
    • 类定义中的内存使用

前言

​ C++凭借其指针变量可以直接操作内存而得到了非常高的效率和程序性能,在一种编程语言里独树一帜。当然,现在很多更高级的语言对底层的内存操作进行了封装,让程序员们不用再考虑内存的分配和回收,可以将自己的精力主要放在业务逻辑的实现上。

​ 但是我觉得,一个好的程序员必须了解程序在内存中的基本逻辑,而了解和使用C++是一个比较有效的方式。我可以不用C++去写代码,但是我可以通过C++的代码来了解程序代码在内存中的基本逻辑,对进一步了解操作系统有很好的帮助,每一个程序员都应该有这方面的基本了解。

​ 我把我对内存和操作系统,还有C++中的一些基本函数的一些小经验分享一下。

操作系统内存

​ 现在计算机不管是服务器,桌面还是嵌入式设备,在体系结构上都是遵循了冯诺依曼提出的基本结构:

在这里插入图片描述

​ 从这个体系结构图中可以看出,存储器是整个体系结构中最核心的部分,在一般的服务器和工作站中,这个存储器就是内存。

地址空间

​ 整个内存的存储空间可以被称之为“地址空间”。一般以十六进制来表示,比如一个8GB的内存,总共有8 * 1024 * 1024 * 1024 B(字节,一个字节8个bit-比特)的存储空间,假设每个字节使用一个地址的话,其地址空间就是从0x0 0000 0000到0x1 FFFF FFFF的十六进制之间。

​ 现在的操作系统访问内存就是通过这样的一个地址去访问内存,对内存进行读写的。比如老的32位操作系统,表示处理器在执行指令时,一次可以处理一个32位长的数据,也就是4个字节。假设这个指令时去读取一个内存的数据,这个内存地址最长就只能是32位,或者说4个字节,其最大的地址为 2 3 2 = 4 G B 2^32 = 4GB 232=4GB。也就是说在32位操作系统上,只能支持4GB的内存,如果要扩大的话,需要有一些其他的手段,比如改造指令集等。

​ 当然,现在流行的是64位系统,内存的空间已经到了 2 6 4 2^64 264的空间了,这个已经是一个海量的数字,现在暂时是用不上了。

基本数据类型

​ 在我们日常的程序开发中,会碰到很多数据类型,也有自定义的数据类型,我觉得C/C++的基本数据类型很好的能说明内存的使用逻辑。

​ C++中我们一般会用到下面几种类型:

  • char,字符型。一般来说,占用一个字节的内存。

  • int,整数型。一般来说,占用4个字节的内存。还有比如int64这种的,就会占用64

  • float,浮点型。一般来说,占用4个字节的内存。

  • 自定义数据类型,比如定义一个结构体:

    typedef struct mydata{

    ​ char x;

    ​ int y;

    ​ float z;

    }mydata;

    这样的一个结构体,占用的内存空间就是三者之和。当然,有些操作系统中会有一些内存对其的操作,可能会比三者之和要大,这个就要看具体操作系统或者编译环境怎么处理了。

  • 指针类型。重点来了:指针类型是存储地址用的,这个地址存的是某种数据类型的地址,占用的内存空间和这种数据类型占用的内存空间没有什么关系,而只和操作系统的寻址空间,也就是地址空间有关系。比如在64位操作系统中,一个指向char类型的指针也会占用8字节的内存空间,因为它必须是一个64位的地址,而不是一个字节。

sizeof运算符

​ 在C/C++中,有一个sizeof的运算符,就是用于计算数据类型的变量所占用的内存数量的,单位为字节。可以用一下代码在32位和64位的操作系统中测试一下上面的描述:

	char c;
    int i;
    float f;
    mydata data;

    char *cPtr;
    int *iPtr;
    float *fPtr;
    mydata *dataPtr;

    printf("%d\n", sizeof(c));
    printf("%d\n", sizeof(i));
    printf("%d\n", sizeof(f));
    printf("%d\n", sizeof(data));

    printf("%d\n", sizeof(cPtr));
    printf("%d\n", sizeof(iPtr));
    printf("%d\n", sizeof(fPtr));
    printf("%d\n", sizeof(dataPtr));

​ 输出结果:

​ sizeof©: 1

​ sizeof(i): 4

​ sizeof(f): 4

​ sizeof(data): 12,这就是上面说的,做了一个内存对其,把char的内存也补齐到了4个字节。

​ sizeof(cPtr): 8

​ sizeof(iPtr): 8

​ sizeof(fPtr): 8

​ sizeof(dataPtr): 8

​ 从上面可以看出,所有的指针都是占用了8个字节(64位环境)

指针运算

​ 指针的运算是一个基本知识了,也就是指针的+和-相关的操作符已经被重载了。

​ 前面提到了,指针保存的是地址信息,所以对指针变量的加和减都是对地址信息的加和减,指针的加1,就是指针指向的地址增加或减少其对应类型的占用内存空间的大小。我觉得这个就是给指针指定类型的作用之一了。

​ 举个简单的例子,某个指针的值时0x000001,如果这个指针的类型是char,那么指针加1,就会变成0x000002,以此类推。

	char* x = new char[10];
    printf("%x\n", x);
    x = x+1;
    printf("%x\n", x);

	int* y = new int[10];
    printf("%x\n", y);
    y = y+1;
    printf("%x\n", y);

​ 输出结果是:

​ 48db63c0

​ 48db63c1

​ 48db0940

​ 48db0944

​ 个人觉得在实际使用过程中,有几点注意的事项:

  1. 在void*指针做参数指向的连续空间时,做指针类型变换是,必须注意其移动的步距。
  2. 32位或者64位系统或者编译器可能造成某种类型的步距发生变化,使用sizeof先进行一下计算会比较好。
  3. 注意内存补齐情况的发生。
  4. 这里另外提一点,在体系结构中,有big-endian和little-endian的区别,也就是说是从高地址往低地址排,还是低地址往高地址排。这个和操作系统和体系结构有关系,一般来说不需要管,如果做不同平台之间的代码迁移,可能就需要考虑这个问题了。

​ 因为一旦超过分配的区域进行访问,就会发生内存越界,整个进程崩掉。因为在操作系统中,对每个进程能访问的地址空间是做了严格限制的,一旦越界就会导致进程崩溃(为了安全起见,当然也可以通过一些手段获取到其他进程的内存地址,进行一些非法的操作)

内存分配与回收

​ 所有的程序都需要被操作系统加载到内存中才能执行,所以程序在内存中是有一个存储分布情况的,C代码的程序分布如下:

在这里插入图片描述

​ 程序员接触的比较多的区域就是堆区和栈区这两个部分了。

​ 平时用到的函数指针,我理解指向的地址空间就是位于代码区这一块的地址。

堆与栈

​ 一般来说,由编译器来决定什么时候分配,什么时候收回的这些内存都放在栈区内存中。比如说局部变量,包括了局部的一般数据类型,和指针类型指向的内存地址。因为这部分内存地址是在栈中被分配的。

​ 那么由程序员来决定什么时候分配,什么时候回收的内存就会在堆区内存中了。以下几个标准的操作就会在堆区中分配内存了,如果这些内存没有被程序手动指定释放,操作系统是不会回收这些内存的,这些内存就会一直没滞留在堆区中。如果一直持续下去,就会导致程序崩溃,这种情况就可以称之为内存泄露

malloc/new与free/delete

​ 这两套函数很类似,网上有很多文章都提到了两者的区别,我这里只写一下我自己使用过程中的感受。

  1. malloc是纯C语言时代就存在的函数,我一般用于大片的连续内存的分配。new是为了配合C++的对象分配而提出来的关键字,我一般用于某个对象的创建与内存分配(因为这样的话,会调用类对象的构造函数)。

  2. 还有一个区别是说malloc分配的内存在堆上,new是在自由存储区中(也是为了new单独划出的一块内存区域)。但是我感觉写程序的时候没有太大必要分清这个区别,只要知道这个都是由程序员自己分配,自己去销毁的内存区域即可。

  3. new分配的对象内存,一般用delete来释放,因为这样可以调用到类对象定义的析构函数。

  4. new关键字也可以用于基本类型的内存分配,比如分配一个100个字节长的unsigned char的数组长度,就可以使用:

    unsigned char ptr = new unsigned char[100];
    
  5. delete的使用,delete有两种形式:

    • delete,直接释放指针指向的内存空间或者对象。

    • delete[],如果指针是指向一个对象数组的话,就需要使用这种形式,下面看一下代码。

      比如有这样一个类和代码:

         class A
         {
         private:
            char *m_cBuffer;
             
         public:
            A(){ m_cBuffer = new char[1024 * 1024 * 1024]; }
            ~A() { delete [] m_cBuffer; }
         };
         A *a = new A[3];
         delete a;    
      

      如果是delete的话,只会调用A[0]的析构函数,释放A[0]的内存,因为从语法上来说,数组的名字就代表这个数组第一个元素的地址,所以,delete a就表示释放A[0]的地址指向的内存空间。

      正确的姿势应该是:

      delete [] a;

      看下结果:

      如果不带中括号,在任务管理器中显示的内存占用:

在这里插入图片描述

如果带了中括号,这3个G的内存就会被全部释放掉。

基本类型的指针操作

  • 把一个整形拆成4个字节输出:

    其中&为取地址操作符,获取当前变量的地址。

    *为取值操作符,获取这个地址对应的值(这个就和指针类型强相关了,同一个地址,类型不同,值不同,底下的代码也反应了这一点,就是说到底取几个字节出来翻译的问题)

    	int i=1000;
        char * c = (char *)&i;
    
        printf("%d\n", *c);
        printf("%d\n", *(c+1));
        printf("%d\n", *(c+2));
        printf("%d\n", *(c+3));
    

    输出结果(big-endian和little-endian会不同):

    -24,整型中第一个字节的值

    3,整型中第二个字节的值

    0,整型中第三个字节的值

    0,整型中第四个字节的值

  • 不同的指针经过赋值后指向同样的内存地址。

    	unsigned char* ptr1 = new unsigned char[sizeof(unsigned char) * 1024 * 1024 * 1024];
        unsigned char* ptr2 = new unsigned char[sizeof(unsigned char) * 1024 * 1024 * 1024];
    
        ptr1 = ptr2;
    
        delete ptr1;
        delete ptr2;
    

    在这个例子里,ptr1分配并指向了内存块1,ptr2分配并指向了内存块2。经过ptr1 = ptr2的赋值后,ptr1也指向了内存块2。

    delete ptr1实际上释放的是内存块2,而不是内存块1。

    delete ptr2也是去释放内存块2,这时就会出现内存越界,程序直接崩溃。

    同时,内存块1一直遗留在堆中,无法被释放。

类定义中的内存使用

​ 个人觉得C++类涉及到内存的有下面几种情况:

  • 类对象作为局部变量,作为栈的使用方式出现。比如:

    void MainWindow::on_pushButton_clicked()
    {
        A x;
    
        QMessageBox::about(this, "a", "wait");
    }
    
    

    各位可以自行试一下,在点击弹出框按钮之前,占用的内存在1GB,点击之后,该函数即会执行完成,作为栈里面的变量和内存就都会被释放(实际上是调用了该类的析构函数,如果没有编写析构函数去释放类中指针的内存空间的话,这部分内存是不会被释放的,因为在我的这个例子中,A的成员指针分配的空间时在堆上面的)。

    所以,在C++中,析构函数是非常重要的(我认为C++编程的原则之一就是类的构造函数分配成员内存,在析构函数中统一释放该类用到的内存)。

  • 类对象以指针的方式出现,此时该类对象本身的内存地址就在堆上(不是成员指针指向的内存地址),函数结束后是无法自动释放该对象的地址及调用其析构函数进行成员内存的释放的,必须使用delete关键字进行处理。

    void MainWindow::on_pushButton_clicked()
    {
        A* x = new A();
    
        QMessageBox::about(this, "a", "wait");
    
        delete x;
    }
    
  • 类对象的指针赋值

    实际上就是两个指针指向同一个类对象的地址,记住只能delete一次,然后两个指针同时赋值为null。

    	A* x = new A();
        A* y = x;
    
        QMessageBox::about(this, "a", "wait");
    
        delete x; // 或者delete y,因为是指向同一块地址
        x = nullptr;
        y = nullptr;
    
  • 类对象直接赋值

    这个比较复杂一点,类对象之间的赋值,在没有对=这个操作符进行重载的时候,会对成员一个一个进行默认的赋值操作。参考一下下面的代码:

    void MainWindow::on_pushButton_clicked()
    {
        A x;
        A y = x;
    
        QMessageBox::about(this, "a", "wait");
    }
    

    上面这段代码会直接崩溃,出现内存越界访问。

    原因是:在A y = x这一句代码中,会把对象x的每个成员变量赋值给y这个对象的每个成员变量,赋值之后x和y这两个对象的成员变量cPtr就都指向了同一块地址空间。那么在函数执行完成之后,两个对象的析构函数都会被执行。第一次就已经把这块地址释放掉了,第二次就会造成内存访问越界了。

    解决这个问题的办法就是使用C++中的重载操作符函数:

    A& A::operator =(const A& rl)
    {
        this->cPtr = new char[1024 * 1024 * 1024];
        memcpy(this->cPtr, rl.cPtr, 1024 * 1024 * 1024);
    
        return *this;
    }
    
    

    这样,A y = x这句代码实际上就会执行上面的这个函数,把y这个对象中的成员变量分配一个新的内存地址,把x对象中的成员内存中的内容给复制过去,函数退出的时候,就会分别释放不同的内存地址了。

    一般来说,如果类的成员中有初始化时分配内存地址的,最好是重载一下这个操作符,避免内存泄露或者越界。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/55906.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网络变压器工厂:了解POE POE+ 网络变压器(网络隔离滤波器)

Hqst盈盛(华强盛)电子导读:这一节给大家分享:POE POE POE 网络变压器(网络滤波器,网络隔离变压器)的相关知识--poe poe 与POE简介及相关标准; 一、网络变压器 POE、 POE 与POE简介 POE 是Power …

Day17-购物车页面-收货地址-把address信息存储到vuex中

1.将address信息存储到vuex中: 我的操作: 1》在 store 目录中,创建用户相关的 vuex 模块,命名为 user.js: ①:新建user.js文件 ②:为user.js编写内容 2》在 store/store.js 模块中&#xff0c…

typora安装和配置PicGo图床

typora安装和配置PicGo图床typora安装和配置PicGo图床typora安装安装picgo图床配置picgo图床图床服务器picgo配置七牛云typora安装和配置PicGo图床 typora安装 windows安装typora 参考:https://www.bilibili.com/read/cv19476097/ m1 mac安装typora比较简单&…

数据结构-栈ArrayDeque的实现

优雅,实在是太优雅了 能把复杂的东西简单化就是功底。 我为何有如此感慨,了解ArrayDeque的实现你就知道,今天我们要讲的是以栈为思想而实现的ArrayDeque,我们都知道栈是先进后出,和队列相反,如下图&#x…

二叉树的中序遍历三种解法(递归+迭代+线索化)

文章目录递归迭代线索二叉树解法传送门: 添加链接描述 给你一颗二叉树,让你实现中序的遍历 递归 递归没什么好说的,直接无脑递归即可,时间复杂度:O(n),空间复杂度:O&am…

搭建环境AI画图stable-diffusion

目录简介环境准备安装conda(方式1)安装conda(方式2,推荐)验证conda安装成功安装stable-diffusion的环境简介 本文旨在记录过程,偶然看见一个AI画图的,体验看看。 stable-diffusion是一个输入简单图片,输出…

【Java难点攻克】「Guava RateLimiter」针对于限流器的入门到实战和源码原理分析

限流器的思路和算法 如果让你来造一个限流器,有啥想法? 漏桶算法 用一个固定大小的队列。比如设置限流为5qps,1s可以接受5个请求;那我们就造一个大小为5的队列,如果队列为满了,就拒绝请求;如…

JRebelXRebel的配置和使用(进阶篇)

JRebel&XRebel的配置和使用嘚吧嘚设置JRebel快捷键XRebel使用嘚吧嘚 之前简单介绍了JRebel&XRebel的安装和使用,不了解的朋友可以补补课😆。 JRebel&XRebel这款插件不仅仅可以用来热部署,所以继续分享一下这款插件的相关使用&a…

12月2日(第四天)

使用myabtis自动生成的时候&#xff0c;发现xml文件只会merge不会覆盖&#xff0c;这时候需要使用插件&#xff1a; <plugin type"org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" />MyBatis Generator配置文件context元素的defaultModelType属性…

封装window10-21H1踩的坑,无法分析或处理pass[specialize]应答文件

最近在研究封装镜像&#xff0c;无奈公司不给用win11&#xff0c;只能封装win10 2022年全新Windows11系统封装图文教程&#xff08;一&#xff09;定制母盘 - 小鱼儿yr系统 (yrxitong.com) 坑1&#xff0c;封装好出现无法分析或处理pass[specialize]应答文件 解决办法&#x…

Java基础:String类、static关键字、Arrays类、Math类

第一章 String类 1.1 String类概述 概述 java.lang.String类代表字符串。Java程序中所有的字符串文字&#xff08;例如"abc"&#xff09;都可以被看作是实现此类的实例。 类String中包括用于检查各个字符串的方法&#xff0c;比如用于比较字符串&#xff0c;搜索…

[附源码]计算机毕业设计JAVA新冠疫苗线上预约系统

[附源码]计算机毕业设计JAVA新冠疫苗线上预约系统 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM my…

Web3.0 DApp(去中心化应用程序)设计架构

先来回顾下 Web2.0 应用程序架构&#xff0c;一图胜千言&#xff1a; 图示是对大多数 Web 2.0 应用程序如何工作的一个很好的抽象总结。以一个博客平台为例&#xff1a; 首先&#xff0c;必须有一个地方来存储基本数据&#xff0c;也就是数据库&#xff1b; 其次&#xff0c;…

快速串联 RNN / LSTM / Attention / transformer / BERT / GPT(未完待续)

参考&#xff1a; 李宏毅2021/2022春机器学习课程王树森 RNN & Transformer 教程 文章目录0. 背景&#xff1a;序列数据及相关任务1. 早期序列模型1.1 循环神经网络 RNN1.2 长短期记忆网络 LSTM1.3 改善 RNN/LSTM 的三个技巧1.3.1 通过堆叠扩展为深度模型1.3.2 使用双向模…

使用学校的服务器跑深度学习

&#x1f31e;欢迎来到深度学习的世界 &#x1f308;博客主页&#xff1a;卿云阁 &#x1f48c;欢迎关注&#x1f389;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f31f;本文由卿云阁原创&#xff01; &#x1f320;本阶段属于练气阶段&#xff0c;希望各位仙友顺利完…

原型工具与代码实现的差距及改进设想

背景 最近经常根据墨刀设计的原型开发微信小程序页面&#xff0c;使用的多了总感觉原型只能参考&#xff0c;原型跟代码实现总差一小步。原型中提供的CSS并不能直接复制到代码中&#xff0c;导致编码人员复刻原型设计时总有差距。本文先列举出一些原型和实现的差距&#xff0c;…

RS232/RS485信号转12路模拟信号 隔离D/A转换器YL34

特点&#xff1a; ● RS-485/232接口&#xff0c;隔离转换成12路标准模拟信号输出 ● 可选型输出4-20mA或0-10V控制其他设备 ● 模拟信号输出精度优于 0.2% ● 可以程控校准模块输出精度 ● 信号输出 / 通讯接口之间隔离耐压3000VDC ● 宽电源供电范围&#xff1a;10 ~ 3…

信息收集的工具简介和常见操作

目录 前言 域名信息 工具 子域名信息 工具 域名DNS解析信息 工具 ip信息 工具 CDN绕过 方法 工具 端口服务信息 常见端口总结 基本概念 扫描工具 指纹识别 识别对象 cms指纹识别 识别思路 工具 其他工具 cdn识别 常用工具 waf识别 触发 工具 Google…

营业利润里首次突破两位数,瑞幸能否延续神话?

近期&#xff0c;瑞幸咖啡公布了Q3财报&#xff0c;三季度继续延续了上半年良好的增长势能。总净收入39亿元&#xff0c;同比增长65.7%&#xff0c;营业利润率首次突破双位数达到了15%。 门店数量增长&#xff1a;Q3新增651家&#xff0c;达到7846家门店。从开店节奏看&#…

RCNN算法思想简单讲解概述————(究极简单的讲述和理解)

学习的过程中发现一个问题&#xff0c;如果不能大概的了解一下一个算法的思想直接去看他的论文&#xff0c;或者去看他算法的讲解就很痛苦&#xff0c;看不懂&#xff0c;学的效率也非常低&#xff0c;类似我之前发的RCNN论文精度的博客。RCNN目标检测算法内容详解&#xff08;…