C++内存管理

内存分区

C++程序运行时,内存分为几个区域,每个区域负责不同的任务

栈区

由编译器自动分配释放,存放:局部变量、形参和返回值
存放函数的局部变量、函数参数和函数调用信息的区域;

堆区

由程序员进行分配和释放;通过malloc和free或者new和delete;

全局(静态)区

包括未初始化全局区和已初始化全局区
存放全局变量和静态变量,生命周期是整个程序运行期间。程序启动时分配,程序结束时释放

常量区

存放常量字符串等;
也被称为只读区,存放常量数据;

代码区

存放程序的代码

堆和栈的区别

栈和堆都是⽤于存储程序数据的内存区域。栈是⼀种有限的内存区域,⽤于存储局部变量、函数调⽤信息等。堆是
⼀种动态分配的内存区域,⽤于存储程序运⾏时动态分配的数据。
栈上的变量⽣命周期与其所在函数的执⾏周期相同,⽽堆上的变量⽣命周期由程序员显式控制,可以(使⽤ new
或 malloc )和释放(使⽤ delete 或 free )。
栈上的内存分配和释放是⾃动的,速度较快。⽽堆上的内存分配和释放需要⼿动操作,速度相对较慢。

内存泄漏

什么是内存泄漏?

内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使⽤的内存的情况。内存泄漏并⾮指内存在物理上的消失,⽽是应⽤程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。
可以使⽤Valgrind, mtrace进⾏内存泄漏检查

内存泄漏分类

堆内存泄漏

堆内存指的是程序运⾏中根据需要分配通过malloc,realloc new等从堆中分配的⼀块内存,但是完成后必须通过调⽤对应的 free或者 delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使⽤,就会产⽣ Heap Leak

系统资源泄露

主要指程序使⽤系统分配的资源⽐如 Bitmap,handle ,SOCKET 等没有使⽤相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运⾏不稳定。

没有将基类的析构函数定义为虚函数

当基类指针指向⼦类对象时,如果基类的析构函数不是 virtual,那么⼦类的析构函数将不会被调⽤,⼦类的资源没有正确是释放,因此造成内存泄露。

什么骚操作会导致内存泄漏?

指针指向改变,未释放动态分配内存。

如何防止内存泄漏?

将内存的分配封装在类中,构造函数分配内存,析构函数释放内存;使⽤智能指针

智能指针是什么?

智能指针是为了解决动态分配内存导致内存泄露和多次释放同⼀内存所提出的, C11标准中放在< memory>头⽂件。包括:共享指针,独占指针,弱指针

构造函数和析构函数要设为虚函数吗

1)析构函数
析构函数需要。当派⽣类对象中有内存需要回收时,如果析构函数不是虚函数,不会触发动态绑定,只会调⽤基类析构函数,导致派⽣类资源⽆法释放,造成内存泄漏。
(2)构造函数
构造函数不需要,没有意义。虚函数调⽤是在部分信息下完成⼯作的机制,允许我们只知道接⼝⽽不知道对象的确切类型。 要创建⼀个对象,你需要知道对象的完整信息。 特别是,你需要知道你想要创建的确切类型。 因此,构造函数不应该被定义为虚函数。

什么是智能指针

智能指针⽤于管理动态内存的对象,其主要⽬的是在避免内存泄漏和⽅便资源管理。

1. std::unique_ptr 独占智能指针

std::unique_ptr 提供对动态分配的单⼀对象所有权的独占管理。通过独占所有权,确保只有⼀个std::unique_ptr 可以拥有指定的内存资源。移动语义和右值引⽤允许 std::unique_ptr 在所有权转移时⾼效地进⾏转移。

#include <memory>
std::unique_ptr<int> ptr = std::make_unique<int>(42);

2. std::shared_ptr (共享智能指针):

std::shared_ptr 允许多个智能指针共享同⼀块内存资源。内部使⽤引⽤计数来跟踪对象被共享的次数,当计数
为零时,资源被释放。提供更灵活的内存共享,但可能存在循环引⽤的问题。

#include <memory>
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = ptr1;

3. std::weak_ptr (弱引⽤智能指针):

std::weak_ptr ⽤于解决 std::shared_ptr 可能导致的循环引⽤问题。
std::weak_ptr 可以从 std::shared_ptr 创建,但不会增加引⽤计数,不会影响资源的释放。
通过 std::weak_ptr::lock() 可以获取⼀个 std::shared_ptr 来访问资源。

#include <memory>
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::weak_ptr<int> weakPtr = sharedPtr;

new和malloc的区别

类型安全性:
new 是C++的运算符,可以为对象分配内存并调⽤相应的构造函数。
malloc 是C语⾔库函数,只分配指定⼤⼩的内存块,不会调⽤构造函数。
返回类型:
new 返回的是具体类型的指针,⽽且不需要进⾏类型转换。
malloc 返回的是 void* ,需要进⾏类型转换,因为它不知道所分配内存的⽤途。
内存分配失败时的⾏为:
new 在内存分配失败时会抛出 std::bad_alloc 异常。
malloc 在内存分配失败时返回 NULL 。
内存块⼤⼩:
new 可以⽤于动态分配数组,并知道数组⼤⼩。
malloc 只是分配指定⼤⼩的内存块,不了解所分配内存块的具体⽤途。
释放内存的⽅式:
delete 会调⽤对象的析构函数,然后释放内存。
free 只是简单地释放内存块,不会调⽤对象的析构函数

delete和free的区别?

类型安全性:
delete 会调⽤对象的析构函数,确保资源被正确释放。
free 不了解对象的构造和析构,只是简单地释放内存块。
内存块释放后的⾏为:
delete 释放的内存块的指针值会被设置为 nullptr ,以避免野指针。
free 不会修改指针的值,可能导致野指针问题。
数组的释放:
delete 可以正确释放通过 new[] 分配的数组。
free 不了解数组的⼤⼩,不适⽤于释放通过 malloc 分配的数组。

什么是野指针,怎么产生的,如何避免

野指针是指指向已被释放的或⽆效的内存地址的指针。使⽤野指针可能导致程序崩溃、数据损坏或其他不可预测的⾏为。通常由以下⼏种情况产⽣:

释放后没有置空指针

int* ptr = new int;
delete ptr;
// 此时 ptr 成为野指针,因为它仍然指向已经被释放的内存
ptr = nullptr; // 避免野指针,应该将指针置为 nullptr 或赋予新的有效地址

返回局部变量的指针

int* createInt() {
int x = 10;
return &x; // x 是局部变量,函数结束后 x 被销毁,返回的指针成为野指针
}
// 在使⽤返回值时可能引发未定义⾏为

函数参数指针被释放

void foo(int* ptr) {
// 操作 ptr
delete ptr;
}
int main() {
int* ptr = new int;
foo(ptr);
// 在 foo 函数中 ptr 被释放,但在 main 函数中仍然可⽤,成为野指针
// 避免:在 foo 函数中不要释放调⽤⽅传递的指针
}

如何避免野指针

在释放内存后将指针置为 nullptr
避免返回局部变量的指针,而是使用new

int* createInt() {
int* x = new int;
*x = 10;
return x;
}

使⽤智能指针(如 std::unique_ptr 和 std::shared_ptr ):

#include <memory>
std::unique_ptr<int> ptr = std::make_unique<int>(42);
// 使⽤ std::unique_ptr,避免显式 delete,指针会在超出作⽤域时⾃动释放

注意函数参数的⽣命周期, 避免在函数内释放调⽤⽅传递的指针,或者通过引⽤传递指针

野指针和悬浮指针

野指针是指向已经被释放或者⽆效的内存地址的指针。通常由于指针指向的内存被释放,但指针本身没有被置为nullptr 或者重新分配有效的内存,导致指针仍然包含之前的内存地址。使⽤野指针进⾏访问会导致未定义⾏为,可能引发程序崩溃、数据损坏等问题
悬浮指针是指向已经被销毁的对象的引⽤。当函数返回⼀个局部变量的引⽤,⽽调⽤者使⽤该引⽤时,就可能产⽣悬浮引⽤。访问悬浮引⽤会导致未定义⾏为,因为引⽤指向的对象已经被销毁,数据不再有效
区别
关联对象类型:
野指针涉及指针类型。
悬浮指针涉及引⽤类型。
问题表现:
野指针可能导致访问已释放或⽆效内存,引发崩溃或数据损坏。
悬浮指针可能导致访问已销毁的对象,引发未定义⾏为。
产⽣原因:
野指针通常由于不正确管理指针⽣命周期引起。
悬浮指针通常由于在函数中返回局部变量的引⽤引起。
如何避免悬浮指针
避免在函数中返回局部变量的引⽤。
使⽤返回指针或智能指针⽽不是引⽤,如果需要在函数之外使⽤函数内部创建的对象

内存对齐

内存对⻬是指数据在内存中的存储起始地址是某个值的倍数。
在C语⾔中,结构体是⼀种复合数据类型,其构成元素既可以是基本数据类型(如int、 long、 float等)的变量,也可以是⼀些复合数据类型(如数组、结构体、联合体等)的数据单元。在结构体中, 编译器为结构体的每个成员按其⾃然边界(alignment)分配空间。 各个成员按照它们被声明的顺序在内存中顺序存储,第⼀个成员的地址和整个结构体的地址相同。
为了使CPU能够对变量进⾏快速的访问,变量的起始地址应该具有某些特性, 即所谓的“对⻬”,⽐如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除,也即“对⻬”跟数据在内存中的位置有关。如果⼀个变量的内存地址正好位于它⻓度的整数倍,他就被称做⾃然对⻬。

为什么要考虑内存对齐

⼤多数计算机硬件要求基本数据类型的变量在内存中的地址是它们⼤⼩的倍数。例如,⼀个 32 位整数通常需要在内存中对⻬到 4 字节边界。
内存对⻬可以提⾼访问内存的速度。当数据按照硬件要求的对⻬⽅式存储时, CPU可以更⾼效地访问内存,减少因为不对⻬⽽引起的性能损失。
许多计算机体系结构使⽤缓存⾏(cache line)来从内存中加载数据到缓存中。如果数据是对⻬的,那么⼀个缓存⾏可以装载更多的数据,提⾼缓存的命中率。
有些计算机架构要求原⼦性操作(⽐如原⼦性读写)必须在特定的内存地址上执⾏。如果数据不对⻬,可能导致⽆法执⾏原⼦性操作,进⽽引发竞态条件。

题目分析

1、

void GetMemory1(char* p)
{
p = (char*)malloc(100);
}
void Test1(void)
{
char* str = NULL;
GetMemory1(str);
strcpy(str, "hello world");
printf(str);
}

这个参数传递是值传递,函数中的p只是str的副本,只修改了副本,没有修改str;要想修改str要传递指针的指针;

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

void GetMemory1(char** p)
{
    *p = (char*)malloc(100);
}

void Test1(void)
{
    char* str = NULL;
    GetMemory1(&str);
    if (str != NULL) {
        strcpy(str, "hello world");
        printf("%s\n", str);
        free(str); // 别忘了释放分配的内存
    } else {
        printf("Memory allocation failed\n");
    }
}

使用指向指针的指针来进行修改;
2、

char *GetMemory2(void)
{
char p[] = "hello world";
return p;
}
void Test2(void)
{
char *str = NULL;
str = GetMemory2();
printf(str);
}

错误:函数返回了一个指向局部变量的指针,局部变量在函数返回时被销毁,所以返回的指针指向一个无效的内存区域;可以使用动态内存和静态内存进行处理;
3、

void Test4(void)
{
    char *str = (char*)malloc(100);
    strcpy(str, "hello");
    free(str); // 释放内存
    if(str != NULL) { // str 仍然不是 NULL,但指向的内存已经被释放
        strcpy(str, "world"); // 未定义行为
        cout << str << endl; // 未定义行为
    }
}


在 free(str) 之后,str 仍然保持其值,但它指向的内存已经被释放,这意味着尝试访问该内存将导致未定义行为。检查 str 是否为 NULL 也不能检测出它是否已经被释放。通常情况下,在释放内存后,将指针设置为 NULL 可以避免这种问题:

void Test4(void)
{
    char *str = (char*)malloc(100);
    if (str == NULL) {
        // 如果 malloc 失败,处理错误
        return;
    }
    
    strcpy(str, "hello");
    free(str);
    str = NULL; // 避免悬空指针

    if (str != NULL) {
        // 这块代码永远不会执行,因为 str 被设置为 NULL
        strcpy(str, "world");
        cout << str << endl;
    } else {
        cout << "Pointer is NULL, memory has been freed." << endl;
    }
}

上述代码解决了悬空指针的问题。释放内存后,将指针设置为 NULL,这样就可以避免使用已经释放的内存。

内存拷贝函数的实现

char* strcpy(char *dst, const char *src) {
    // 确保目标和源字符串指针都不为 NULL
    assert(dst != NULL && src != NULL);
    // 保存目标字符串的起始地址
    char *ret = dst;
    // 逐字符复制源字符串到目标字符串,直到遇到字符串结束符 '\0'
    while ((*dst++ = *src++) != '\0');
    // 返回目标字符串的起始地址
    return ret;
}

[1] const修饰:
(1)源字符串参数⽤const修饰,防⽌修改源字符串。
[2] 空指针检查:
(1)不检查指针的有效性,说明答题者不注重代码的健壮性。
(2)检查指针的有效性时使⽤ assert(!dst && !src);
char *转换为 bool 即是类型隐式转换,这种功能虽然灵活,但更多的是导致出错概率增⼤和维护成本升⾼。
3)检查指针的有效性时使⽤ assert(dst != 0 && src != 0);
直接使⽤常量(如本例中的0)会减少程序的可维护性。⽽使⽤NULL代替0,如果出现拼写错误,编译器就会检查
出来。
[3] 返回⽬标地址:
(1)忘记保存原始的strdst值。
[4] ‘\0’:
(1)循环写成 while (*dst++=*src++); 明显是错误的。
(2)循环写成 while (*src!=‘\0’) *dst++ = *src++;
循环体结束后, dst字符串的末尾没有正确地加上’\0’。
(3)为什么要返回char *?
返回dst的原始值使函数能够⽀持链式表达式
链式表达式的形式如:
int l=strlen(strcpy(strA,strB));
⼜如:
char * strA=strcpy(new char[10],strB);
返回strSrc的原始值是错误的。
理由:

  1. 源字符串肯定是已知的,返回它没有意义
  2. 不能⽀持形如第⼆例的表达式
  3. 把 const char *作为char * 返回,类型不符,编译报错

dst和src内存重叠情况

这段代码实现了一个安全且处理内存重叠的字符串拷贝函数

char * strcpy(char *dst,const char *src)
{
assert(dst != NULL && src != NULL);
char *ret = dst;
my_memcpy(dst, src, strlen(src)+1);//strlen(src) + 1是为了保证\0也能包含进去;
return ret;
}
/* my_memcpy的实现如下 */
char *my_memcpy(char *dst, const char* src, int cnt)
{
assert(dst != NULL && src != NULL);
char *ret = dst;
/*内存重叠,从⾼地址开始复制*/
if (dst >= src && dst <= src+cnt-1)
{
dst = dst+cnt-1;
src = src+cnt-1;
while (cnt--)
{
*dst-- = *src--;
}
}
else //正常情况,从低地址开始复制
{
while (cnt--)
{
*dst++ = *src++;
}
}
return ret;
}

类内构造函数赋值运算符的函数写法

class String
{
public:
String(const char *str = NULL);
String(const String &other);
~ String(void);
String & operate =(const String &other);
private:
char *m_data;
};
// 构造函数
String::String(const char *str)
{
if(str==NULL)
{
m_data = new char[1]; //对空字符串⾃动申请存放结束标志'\0'
*m_data = '\0';
}
else
{
int length = strlen(str);
m_data = new char[length + 1];
strcpy(m_data, str);
}
}
// 析构函数
String::~String(void)
{
delete [] m_data; // 或delete m_data;
}
//拷⻉构造函数
String::String(const String &other)
{
int length = strlen(other.m_data);
m_data = new char[length + 1];
strcpy(m_data, other.m_data);
}
//赋值函数
String &String::operate =(const String &other)
{
if(this == &other)
{
return *this; // 检查⾃赋值
}
delete []m_data; // 释放原有的内存资源
int length = strlen(other.m_data);
m_data = new char[length + 1]; //对m_data加NULL判断
strcpy(m_data, other.m_data);
return *this; //返回本对象的引⽤
}

进程空间分布

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值