C++ 内存与编译问题总结

news2024/9/22 19:34:15

目录

C++内存结构

作用域与生存周期

堆与栈

内存对齐

智能指针

shared_ptr 循环引用问题

编译与链接

内存泄漏

补充问题

include  “ ”与<>

 大端与小端


C++内存结构

C++程序内存分区

  • 代码区
    • 文件中所有的函数代码、常量以及字符串常量
    • 只读,保护程序不会被其他进程恶意修改
  • 全局/静态存储区
    • 存储全局变量和静态变量。这些变量在程序开始时分配对应内存空间,程序结束的时候则释放
    • 该区域还可以被进一步细分,初始化数据段(已经初始化的数据)、未初始化数据段
    • 动态分配的内存空间,类似于new 和 malloc分配的内存空间
    • 堆的大小是动态变化,程序员负责分配和释放
    • 栈的内存空间是编译器来自动管理,函数调用时分配空间,函数返回时释放内存区域
    • 存储函数调用的局部变量以及函数调用的返回地址
  • 常量存储区
    • 用于存储常量,不允许更改,程序结束的时候自动释放(例如初始化的字符串)

作用域与生存周期

总结

  • 全局变量
    • 作用域:全局作用域。全局变量在一个源文件中定义,可以作用其他所有源文件。不包含该定义的源文件,需要借助extern关键字再次声明该全局变量
    • 生命周期:程序运行的整个声明周期都存在,程序结束则自动销毁,由系统对资源进行回收
    • 注意:全局变量定义不要在头文件中定义,当其他文件包含该头文件时,该文件的全局变量就会被定义多次,编译时会导致重复定义出错。
  • 静态全局变量
    • 作用域:文件作用域。只作用于定义它的文件中,不同的源文件即使定义了相同的静态全局变量,两者也是不同的变量(全局变量是可以实现一次定义,全文件使用)
    • 生命周期:存在于程序的整个生命周期,与全局变量相同
  • 局部变量
    • 作用域:局部作用域。与函数的生命周期相对应,并非程序运行期间一直存在
    • 生命周期:生命周期只存在于函数调用的时候,函数调用结束后会自动销毁
  • 静态局部变量
    • 作用域:局部作用域。仅在声明静态局部变量的函数中可见
    • 生命周期:自从初始化后直到程序运行结束都一直存在

静态局部变量实验 

#include <iostream>

void staticVariableExample() {
    static int count = 0;  // 静态局部变量
    count++;
    std::cout << "Count: " << count << std::endl;
}

int main() {
    for (int i = 0; i < 5; ++i) {
        staticVariableExample();
    }
    return 0;
}

堆与栈

函数调用与栈

        当程序调用函数的时候,按照从 右往左的顺序,依次将 函数调用的参数压入栈中,并在栈中压入返回地址与当前的栈帧,然后跳转到调用函数内部。

栈空间分配测试(gdb) 

  • stact level 栈帧层 + 当前栈帧的起始地址、
  • saved rip :调用该函数的指定地址信息
  • called by frame at 地址:调用该函数的栈帧所在的地址,该地址是一个栈帧起始地址(是被哪一个栈调用的,即上一层的地址)
  • arglist at 地址,args:参数列表的地址以及当前函数参数的数值是多少
  • saved registers:保存的寄存器列表
  • rbp at 地址, rip at 地址:基址指针和指令指针的地址

//测试源码
#include <iostream>

void recursiveFunction(int a, int b) {
    int c = a + b;
    std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl;
    if (a > 0) {
        recursiveFunction(a - 1, b + 1);
    }
} 

int main() {
    recursiveFunction(3, 1);
    return 0;
}

 栈溢出(程序运行中,栈的空间被耗尽,导致程序崩溃或者异常)

  • 原因分析
    • 深度递归:函数递归调用过深,每次递归的时候都会消耗响应的栈空间,最终导致栈溢出
    • 局部变量过大:栈空间内如果存储了较大的局部变量,同样会导致栈空间不足
    • 无限循环调用

#include <iostream>

void recursiveFunction() {
    int largeArray[10000]; // 大型局部变量
    std::cout << "Recursion" << std::endl;
    recursiveFunction(); // 无终止条件的递归调用
}

int main() {
    recursiveFunction();
    return 0;
}

 

  • 含义:进程运行时,需要动态申请内存空间存放数据时使用的空间,堆的内存空间由操作系统来管理(资源的释放由操作系统自动管理)
  • malloc 和 new从堆中申请内存,使用free 和 delete释放空间

 堆与栈的对比

  • 分配方式:栈编译器自动分配,堆由程序员手动分配
  • 生命周期:栈随函数调用来决定,堆则灵活,是由程序员控制
  • 内存大小:栈的内存空间通常固定且较小,堆的空间相对较大
  • 线程安全:栈的线程安全,堆的线程不安全(因为同一个进程下的内存,多线程都可以访问)
  • 场景:栈适用于局部变量、函数参数以及返回地址;堆则适用于动态数据结构以及大的数据块

内存对齐

含义

  • 编译器将程序中的每个数据的地址,都放在机器字长的整数被的地址所指向的内存中(数据按照计算机系统大小的倍数进行对齐)
  • 计算机内存的地址空间按照比特划分,但是实际情况上并不是严格按照顺序进行排放

内存对齐原因分析

  • 提高性能
    • 减少内存访问次数,从而提高性能
  •  硬件限制
    • 确保程序可以在不同硬件平台的正确性和兼容性
    • CPU访问按照机器字长为单位,不以字节为单位,机器字长又是由总线宽度决定
  • 简化硬件设计
    • 对齐的数据访问,减少了访问内存次数,从而提高内存访问效率

内存对齐规则

  • 基本对齐
    • 数据类型地址必须是该类型大小的倍数
  • 结构体对齐(重点)
    • 第一个成员在结构体变量偏移量的0位置
    • 结构体中的每个成员必须是该成员大小的整数倍
      • VS系统中,要与8比较,取较小值
    • 结构体的总大小必须是该结构体中最大成员大小的倍数,从而保证数组中每个结构体实例的对齐 
    • 例子分析
      • char a :占用1个字节,因为下一个类型是int,所以该处需要填充3字节,从而保证int的对齐
      • int b:占用4个字节
      • short c :占用2字节,所以向下填充两个字节,最终大小要保证是最大字节的倍数,即4的倍数,所以需要填充2个字节
      • 所以最终计算的结果是12字节

智能指针

智能指针管理动态分配的内存和其他资源,可以实现避免内存泄漏,其本身遵循RAII原则,实现自动创建内核和销毁内存。

unique_ptr 

  • 独占对象所有权:该指针指向的对象,只可以被该指针所占有,不能够拷贝构造和复制,但是可以移动构造和移动复制构造(即一个uinque_ptr 的对象赋值给另一个uinque_ptr 对象)
  • 作用:轻量,因为只指向一个资源,开销小;适用于资源独占的场景,利于在socket编程中以及互斥锁

#include <memory>
#include <iostream>

struct Foo {
    Foo() { std::cout << "Foo constructed\n"; }
    ~Foo() { std::cout << "Foo destroyed\n"; }
};

int main() {
    std::unique_ptr<Foo> ptr1(new Foo());
    // std::unique_ptr<Foo> ptr2 = ptr1; // 错误,不能复制
    // std::unique_ptr<Foo> ptr2(ptr1);..错误
    std::unique_ptr<Foo> ptr3 = std::move(ptr1); // 转移所有权
    return 0;
}

 shared_ptr

  • 资源共享:多个shared_ptr指针可以指向同一个对象,指针内部会维护一个引用计数
  • 使用
    • use_count():查看资源使用者个数
    • release():释放资源所有权,每一次释放会将计数减1,直到引用计数减到0,才对资源进行释放
    • shared_ptr不是线程安全的,其内部的引用计数是原子性的

#include <memory>
#include <iostream>

struct Foo {
    Foo() { std::cout << "Foo constructed\n"; }
    ~Foo() { std::cout << "Foo destroyed\n"; }
};

int main() {
    std::shared_ptr<Foo> ptr1(new Foo());
    std::shared_ptr<Foo> ptr2 = ptr1; // 共享
    return 0;
}

weak_ptr

  • 作用:指向share_ptr指针所指向的对象,目的是解决shared_ptr所带来的循环引用(两个shared_ptr指针互相指)问题
  • 使用
    • weak_ptr 转换为shared_ptr的时候,可以访问shared_ptr所指向的资源,但是不会影响share_ptr指针的计数
    • shared_ptr被weak_ptr指向时的释放:一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象,对象也还是会被释放。
    • 使用weak_ptr访问对象的时候,需要调用lock去检查weak_ptr所指向的对象是否存在:如果存在则会返回一个shared_ptr的指针

shared_ptr 循环引用问题

总结:两个对象相互引用对方的shared_ptr的时候,双方的引用计数都不会变成0,所以导致对象都不会被销毁,最终会导致内存泄漏

#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> ptrB;
    ~A() { std::cout << "A destroyed" << std::endl; }
};

class B {
public:
    std::shared_ptr<A> ptrA;
    ~B() { std::cout << "B destroyed" << std::endl; }
};

int main() {
    {
        std::shared_ptr<A> a = std::make_shared<A>();
        std::shared_ptr<B> b = std::make_shared<B>();
        a->ptrB = b;
        b->ptrA = a;
    } // a和b出了作用域,但不会被销毁
    std::cout << "End of main" << std::endl;
    return 0;
}

weak_ptr打破循环引用思路

  • a持有一个指向b的shared_ptr指针,这样就增加了b的引用计数
  • b持有一个指向a的weak_ptr指针,但是这样不会增加a的引用计数
  • 销毁过程
    • a和b两者超出作用域后,其shared_ptr引用计数会递减
    • 引用计数降为0的时候,a被销毁
    • a销毁后,其内部成员ptrB(指向b的shared_ptr)也会被销毁,所以此时b的引用计数也就会降至0,所以b也就被销毁了
#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> ptrB;
    ~A() { std::cout << "A destroyed" << std::endl; }
};

class B {
public:
    std::weak_ptr<A> ptrA; // 使用weak_ptr打破循环引用
    ~B() { std::cout << "B destroyed" << std::endl; }
};

int main() {
    {
        std::shared_ptr<A> a = std::make_shared<A>();
        std::shared_ptr<B> b = std::make_shared<B>();
        a->ptrB = b;
        b->ptrA = a; // 使用weak_ptr
    } // a和b出了作用域,会被正确销毁
    std::cout << "End of main" << std::endl;
    return 0;
}

编译与链接

编译的处理过程代码转换为二进制指令==编译器读取源文件,源文件翻译成ELF,ELF文件经过操作系统进行加载执行)

  • 预处理:处理源代码中的预处理指令。取出注释以及头文件、条件编译指令、宏的替换等
  • 编译:CPP源文件翻译成.s的汇编代码。
  • 汇编:汇编代码.s 翻译成机器指令.o文件,一个cpp文件只可以生成一个.o文件
  • 链接:将生成的.o文件打包成一个整体,从而生成一个可被操作系统加载执行的ELF程序文件。

静态链接

  • 静态链接是将代码运行所需要的库函数在编译的时候,全部放到可执行文件中。生成的可执行文件就包含的所依赖的库代码,所以在运行的时候,不需要外部库的支持。
  • 过程
    • 编译源文件,生成目标文件
    • 链接器将目标文件和库文件合并成一个可执行文件
  • 编译链接实验步骤说明
    • g++ -c library.cpp -o library.o :仅编译,将cpp文件输出称为library.o文件
    • ar rcs libstatic.a library.o:创建静态库 libstatic.a,并将library.o文件添加进去
    • g++ main.cpp -L. -lstatic -o static_example:main文件连接上创建的静态库,创建出可执行文件
root@hcss-ecs-b4a9:/home/test/test_o# tree
.
├── library.cpp
├── library.o
├── libstatic.a
├── main.cpp
└── static_example
g++ -c library.cpp -o library.o
ar rcs libstatic.a library.o
g++ main.cpp -L. -lstatic -o static_example
./static_example

//library.cpp

#include <iostream>

void libraryFunction() {
    std::cout << "Library function called!" << std::endl;
}

//main.cpp
#include <iostream>
extern void libraryFunction();

int main() {
    std::cout << "Hello, static linking!" << std::endl;
    libraryFunction();
    return 0;
}

动态链接

  • 运行时将程序与库函数链接。只要在文件运行的时候,再加载对应的动态库
  • 过程分析
    • 编译源文件生成目标文件(.o)
    • 编译器生成的可执行文件中包含对动态库引用
    • 程序运行的时候,动态加载器加载所需要的动态库
  • 编译链接步骤说明
    • g++ -fPIC -c library.cpp -o library.o
      • -fPIC:因为动态库加载的时候可能会被映射到内存的不同位置,所以需要生成位置无关代码
      • -c :仅编译不链接
    • g++ -shared -o libdynamic.so library.o(创建动态库)
      • -shared:生成动态库
      • 指定.so的名字,并输入目标文件
    • g++ main.cpp -L. -ldynamic -o dynamic_example((编译链接main,同时生成可执行文件)
      • -L:指定链接时搜索库文件的目录,在此处表示库文件在当前目录
      • -ldynamic:动态库的名字" l+ dynamic)
    • export LD_LIBRARY_PATH=.
      • 将动态库的目录添加到LD..这个环境变量中

 

//library.cpp

#include <iostream>

void libraryFunction() {
    std::cout << "Library function called!" << std::endl;
}


//main.cc
#include <iostream>
extern void libraryFunction();

int main() {
    std::cout << "Hello, dynamic linking!" << std::endl;
    libraryFunction();
    return 0;
}

 动静态链接对比

特征静态链接动态链接
文件大小
运行速度
依赖性不依赖外部库依赖外部库
更新困难(重新编译和链接)容易(直接更新静态库即可)
内存       大(每个程序都有独立副本)少(多程序共享库)
版本确保一致(编译时确定)可能不一致(运行时确定)
使用方便复杂

内存泄漏

内存泄漏即是在堆中动态申请的内存,程序使用结束时没有及时释放,生命周期已结束,但是该变量在堆中的空间没有释放,从而导致堆中可使用的内存越来越少,最终导致系统变慢或者内存不足而崩溃。

防止内存泄漏

  • 内部封装: 内存分配封装到类中,构造时创造,析构时释放
  • 智能指针:本身就是一个防止内存泄漏的工具,内部封装了自动释放机制
  • 良好编码习惯、使用内存泄漏检测工具

Valgrind 工具检测内存泄漏

//测试代码
#include <stdio.h>
#include <stdlib.h>

void memory_leak() {
    int *leak = (int *)malloc(100 * sizeof(int)); // 分配内存,但没有释放
    if (leak == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        exit(1);
    }
}

int main() {
    for (int i = 0; i < 100; ++i) {
        memory_leak();
    }
    printf("Memory leak example finished\n");
    return 0;
}

补充问题

include  “ ”与<>

#include(关键字用于标识源代码编译时所需引用头文件,编译器自动查找头文件中信息)

  • #include<>:用于包括标准库头文件。编译器在预先指定的搜索目录中进行搜索,/usr/include目录。
  • #include" " :用于包括标准库头文件。先在当前源文件目录中查找。如果没有,再到当前已经添加的系统目录中(编译时-I 指定的目录)查找,最后在 /usr/include目录查找

__has_include

  • C++17的特性,用于检查是否已经包含了某个文件
#include <iostream>

int main()
{
#if __has_include(<cstdio>)
    printf("c program");
#endif

#if __has_include("iostream")
    std::cout << "c++ program" << std::endl;
#endif

    return 0;
}

 大端与小端

大小端含义

  • 大端字节序:最高有效字节存储在最低地址
  • 小端字节序:最低有效字节存储在最高地址

 

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

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

相关文章

在invidia jetpack4.5.1上运行c++版yolov8(tensorRT)

心路历程(可略过) 为了能在arm64上跑通yolov8,我试过很多很多代码,太多对库版本的要求太高了; 比如说有一个是需要依赖onnx库的,(https://github.com/UNeedCryDear/yolov8-opencv-onnxruntime-cpp) 运行成功了报错error: IOrtSessionOptionsAppendExecutionProvider C…

力扣高频SQL 50题(基础版)第十八题

文章目录 力扣高频SQL 50题&#xff08;基础版&#xff09;第十八题1633. 各赛事的用户注册率题目说明思路分析实现过程准备数据实现方式结果截图 力扣高频SQL 50题&#xff08;基础版&#xff09;第十八题 1633. 各赛事的用户注册率 题目说明 用户表&#xff1a; Users --…

嵌入式Python、ROS、SLAM、WebSocket和Node.js:智能巡逻监控安防机器人设计流程(代码示例)

项目概述 随着智能技术的发展&#xff0c;智能巡逻机器人在安防、监控和巡逻等领域的应用越来越广泛。本文将介绍一个结合嵌入式系统、机器人技术和后端开发的智能巡逻机器人。该机器人能够自主导航&#xff0c;实时检测异常情况&#xff08;如火灾或入侵者&#xff09;&#…

免费【2024】springboot 超市在线销售系统的设计与实现

博主介绍&#xff1a;✌CSDN新星计划导师、Java领域优质创作者、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行前辈交流✌ 技术范围&#xff1a;SpringBoot、Vue、SSM、HTML、Jsp、PHP、Nodejs、Python、爬虫、数据可视化…

Adobe Photoshop(Ps)安装包软件下载

一、Adobe Photoshop简介 Adobe Photoshop&#xff08;简称PS&#xff09;是由Adobe Systems公司开发的图像处理软件&#xff0c;它是一款集图像扫描、编辑修改、图像制作、广告创意、图像输入与输出于一体的图形图像处理软件。广泛应用于专业测评、平面设计、广告摄影、影像创…

通过限制访问,实现纯私有Docker镜像

怎么会不过审呢?没有敏感信息呀。 For obvious reasons,Many Docker image repositories are inaccessible,The official warehouse has also been filtered by the firewall,So write about how to build a self use Docker image using CloudFlares Workers and Pages. …

SQL Server 设置端口号:详细步骤与注意事项

目录 一、了解SQL Server端口号的基础知识 1.1 默认端口号 1.2 静态端口与动态端口 二、使用SQL Server配置管理器设置端口号 2.1 打开SQL Server配置管理器 2.2 定位到SQL Server网络配置 2.3 修改TCP/IP属性 2.4 重启SQL Server服务 三、注意事项 3.1 防火墙设置 3…

VSCode 解决 pylint 报错 No name QWidget in module PyQt5.QtWidgets

问题 启用了 VSCode 的 Pylint 插件, 即便 Python 环境中安装了 PyQt5, 也无法正确解析 PyQt5 的导入 PyQt5 底层代码是用 C/C 写的, pylint 默认不会深入解析 pylint doesn’t load any C extensions by default, because those can run arbitrary code. 解决 修改 Settings…

Internet Download Manager(IDM)2024中文版本有哪些新功能?6.42版本功能介绍

1. Internet Download Manager&#xff08;IDM&#xff09;是一款功能强大的下载管理器&#xff0c;支持所有流行的浏览器&#xff0c;并可提升下载速度高达5倍。 2. IDM具有智能下载逻辑加速器&#xff0c;可以设置文件下载优先级、分块下载等&#xff0c;提高下载效率。 IDM…

网站用HTTP访问的危害以及如何升级HTTPS访问

在互联网世界中&#xff0c;数据传输的安全性是至关重要的。我们每天都在网络上进行各种操作&#xff0c;从浏览网页、购物到银行转账&#xff0c;每一项活动都涉及敏感信息的传递。然而&#xff0c;在这个过程中&#xff0c;我们的数据可能面临被窃取、篡改或滥用的风险。这正…

Vue的安装配置

1.安装node js Node.js — 在任何地方运行 JavaScript (nodejs.org) 2.测试nodejs是否安装成功 node -v npm -v3.通过npm 安装 vue npm install -g vue/cli4.测试vue是否安装成功 vue --version5.打开PyCharm&#xff0c;创建项目&#xff1a;flask-web vue create flask…

深入理解Python装饰器:从基础到进阶

引言 在Python中&#xff0c;装饰器是一种强大的工具&#xff0c;它允许程序员以一种简洁而优雅的方式修改函数或类的行为。装饰器本质上是一个接受函数作为参数的函数&#xff0c;并返回一个新的函数。本文将从装饰器的基础开始介绍&#xff0c;并逐步深入到一些高级用法。 …

鸿蒙应用框架开发【多线程任务】

多线程任务 介绍 本示例通过ohos.taskpool和ohos.worker接口&#xff0c;展示了如何启动worker线程和taskpool线程。 效果预览 使用说明 在主界面&#xff0c;可以点击字符串排序和拷贝文件按钮进入对应的界面&#xff1b; 点击字符串排序按钮进入多线程界面&#xff1a; w…

数据库连接断开后,DBAPI的数据源如何自动重连

现象 在使用DBAPI的过程中&#xff0c;如果网络抖动导致数据库连接不上&#xff0c;发现DBAPI的数据源不能重连&#xff0c;必须重启DBAPI才能连上数据库 解决办法 在数据源的连接池参数配置druid.breakAfterAcquireFailurefalse注意在企业版的4.1.1及以上版本才可以配置连接…

7. LangChain4j如何使用统一api调用?

前言 当我们对接LangChain4j的时候&#xff0c;面对复杂的各种各样的大模型的api的对接&#xff0c;让很多开发者感到力不从心。在每个大模型的api都不一样的时候&#xff1f;该如何快捷的切换模型的使用呢&#xff1f; 这时&#xff0c;One-API应运而生&#xff0c;它以其简洁…

Linux中如何用ida调试fork后的子进程

原文链接 > https://redqx.github.io/linux/2024/07/24/linux-debugfork.html 本文的一些图片引用可能有一些问题, 比如数据不对劲,但无伤大雅 自己懒得粘贴图片了 环境: wsl-kali-2024 ida-7.7 插件: Lazy_ida, 还有一个什么插件不知道什么名字, 可以把汇编转字节码 …

聚焦智慧出行,TDengine 与路特斯科技再度携手

在全球汽车行业向电动化和智能化转型的过程中&#xff0c;智能驾驶技术正迅速成为行业的焦点。随着消费者对出行效率、安全性和便利性的需求不断提升&#xff0c;汽车制造商们需要在全球范围内实现低延迟、高质量的数据传输和处理&#xff0c;以提升用户体验。在此背景下&#…

java学习--练习题

在类中this.属赋值&#xff0c;则外部创建对象调用其值也会随之一样 package com.test01;/* author:我与java相爱相杀---c语言梦开始的地方 今天又是努力学习的一天&#xff01;&#xff01;&#xff01;&#xff01; */ /*1. 在Frock类中声明私有的静态属性currentNum[int类型…

谷粒商城-性能压测

1.压力测试 在项目上线前对其进行压力测试(以每个微服务为单元) 目的:找到系统能承载的最大负荷,找到其他测试方法更难发现的错误(两种类型:内存泄漏,并发与同步). 1.性能指标 响应时间(Response Time (RT)): 响应时间 指用户从客户端发起一个请求开始,到客户端接收到从服务…

学习笔记-系统框图传递函数公式推导

目录 *待了解 现代控制理论和自动控制理论区别 自动控制系统的组成 信号流图 1、系统框图 1.1、信号线、分支点、相加点 1.2、系统各环节间的连接 1.3、 相加点和分支点的等效移动&#xff08;比较点、引出点&#xff09; 2、反馈连接公式推导 2.1、前向通路传递函数…