C++内存管理

喜欢而已 提交于 2020-03-05 21:19:36

动态内存

1、new和malloc的区别。(1)

• new是运算符,malloc()是一个库函数;
• new会调用构造函数,malloc不会;
• new返回指定类型指针,malloc返回void*指针,需要强制类型转换;
• new会自动计算需分配的空间,malloc不行;
• new可以被重载,malloc不能。

2、智能指针(1)

  • 智能定义在<memory>头文件中的std命名空间中定义的,目的是更容易(同时也更安全)地使用动态内存,对RAII技术至关重要。
C++标准中引入命名空间的概念,是为了解决不同模块或者函数库中相同标识符冲突的问题。有了命名空间的概念,标识符就被限制在特定的范围(函数)内,不会引起命名冲突。最典型的例子就是std命名空间C++标准库中所有标识符都包含在该命名空间中

RAII是C++的发明者Bjarne Stroustrup提出的概念,RAII全称是“Resource Acquisition is Initialization”,直译过来是“资源获取即初始化”,也就是说在构造函数中申请分配资源,在析构函数中释放资源。因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。

 

智能指针(std::shared_ptr和std::unique_ptr)即RAII最具代表的实现,使用智能指针,可以实现自动的内存管理。

 

内存只是资源的一种,在这里我们讨论一下更加广义的资源管理。比如说文件的打开与关闭、windows中句柄的获取与释放等等。

摘抄:C++中的RAII介绍

  • 智能指针的设计思想:将基本类型指针封装为类对象指针,并在析构函数里编写delete语句删除指针指向的内存空间。
  • unique_ptr只允许基础指针的一个所有者。unique_ptr小巧高效;大小等同于一个指针且支持右值引用,从而可实现快速插入和对STL集合的检索。
  • shared_ptr采用引用计数的智能指针,主要用于要将一个原始指针分配给多个所有者(例如,从容器返回了指针副本又想保留原始指针时)的情况。当所有的shared_ptr所有者超出了范围或放弃所有权,才会删除原始指针。大小为两个指针;一个用于对象,另一个用于包含引用计数的共享控制块。最安全的分配和使用动态内存的方法是调用make_shared标准库函数,此函数在动态分配内存中分配一个对象并初始化它,返回对象的shared_ptr。
智能指针其实不是一个指针。它是一个用来帮助我们管理指针的类,维护其生命周期的类。有了它,妈妈再也不用担心我的内存泄露啦!

 

3、new、delete、malloc、free之间的关系(2)

new/delete/malloc/free都是动态内存分配的方式

 

1)new/delete是运算符,malloc/free是库函数,不能把构造函数和析构函数强加给malloc/free

2)new返回指定类型指针,malloc返回void*指针,需要强制类型转换;

3)new会自动计算需分配的空间,malloc不行,需要指定分配空间大小

4)new/delete可以被重载,malloc/free不能。

4、delete和delete[]的区别(2)

delete p; //p必须指向一个动态分配对象或为空;

delete [] pa; //pa必须执行一个动态分配的数组或为空

  • delete释放的是一个动态分配对象;delete[]释放的是动态分配的数组
  • delete只会调用一次析构函数,而delete[]会调用每个成员的析构函数

内存管理

1、C++的内存分区(1)

  在C++中,内存分成6个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区、代码区

  栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

  堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。

  自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。

  全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。

  常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。

       代码区:存放程序的二进制代码。

2、内存泄漏怎么产生的?如何避免?(1)

内存泄露怎么产生的?
  • 内存泄漏一般是指堆内存的泄漏,也就是程序在运行过程中动态申请的内存空间不再使用后没有及时释放,导致那块内存不能被再次使用。
  • 更广义的内存泄漏还包括未对系统资源的及时释放,比如句柄、socket等没有使用相应的函数释放掉,导致系统资源的浪费。
如何避免?
  • 养成良好的编码习惯和规范,动态内存的申请与释放必须配对,防止内存泄漏。
  • 使用智能指针,share_ptr、auto_ptr、weak_ptr。
  • 结束程序,内存自然就会被操作系统回收。
如果程序结束就自动回收,为什么还要防止内存泄漏?
因为大部分程序是长时间运行的,比如酒店管理系统,长时间内存泄漏会影响机器性能。

3、C/C++中堆和栈的区别(1)

数据结构中的堆和栈

栈(stack):像装数据的桶或箱子

stack一种先进后出的数据结构,允许新增元素、移除元素、获取最顶端元素,但是不允许有遍历行为;

可以以某种既有容器作为底部结构,将其接口改变,使之符合“先进后出”的特点,形成一个stack;

  • deque是双向开口的数据结构,以deque为底部结构并封闭其头端开口
  • list也是双向开口的数据结构,以list为底部结构并封闭其头端开口

栈(heap):像倒立的树

 

heap是一个complete binary tree(完全二叉树),也就是,整棵binary tree除了最底层的叶节点之外,是填满的,而最底层的叶节点由左至右不得有空隙。

根据元素排列方式,heap可分为max-heap和min-heap,前者每个节点的键值都大于或等于其子节点键值,后者每个节点键值都小于等于其子节点键值。

内存分配中的栈区

内存中的栈区处于相对较高的地址以地址的增长方向为上的话,栈地址是向下增长的;堆是向上增长的

//main.cpp 
int a = 0; 全局初始化区 
char *p1; 全局未初始化区 
main() 

int b; 栈 
char s[] = "abc"; 栈 
char *p2; 栈 
char *p3 = "123456"; 123456\0在常量区,p3在栈上。 
static int c =0; 全局(静态)初始化区 
p1 = (char *)malloc(10); 
p2 = (char *)malloc(20); 
分配得来得10和20字节的区域就在堆区。

 
strcpy(p1, "123456"); 123456\0放在常量区。编译器可能会将它与p3所指向的"123456"优化成一个地方。

}

 

4、溢出,越界,泄漏(1)

5、C/C++中分配内存的方法(1)

6、堆和栈的区别(2)

7、什么是内存泄漏?面对内存泄漏和指针越界,你有哪些方法?(2)

8、C++的内存管理(2)

9、C++中内存泄漏的几种情况(2)

10、描述内存分配方式以及它们的区别?(3)

11、栈内存与文字常量区(3)

C++primer

第12章 动态内存

4.3、智能指针

对象分类:

全局变量:定义在函数体外的对象,存在于程序的整个执行过程,程序启动时创建,结束时销毁

局部变量:函数形参与函数体内定义的变量,其声明周期依赖于定义的方式

局部对象:

自动对象:只存在于块执行期间的对象;当函数的控制路径经过变量定义语句时创建该对象,到达定义所在的块末尾时销毁;

形参也是自动变量,函数开始时为形参申请存储空间

初始化方式:

形参用传递给函数的实参初始化对应的自动对象

函数体内:(1)如果变量本身存在初始值,用该初始值进行初始化;(2)不含初始值,进行默认初始化(函数体内内置变量不被初始化,产生未定义的值)

 

局部静态对象:在程序执行路径第一次经过对象定义语句时初始化,并且直到程序终止时才销毁

初始化方式:如果没有显式初始值,执行值初始化,内置变量的局部静态变量初始化为0;

内存
静态内存:保存局部static对象、类static数据成员、全局变量
栈内存:定义在函数内的非static对象
静态内存与栈内存中的对象由编译器自动创建和销毁;栈对象:定义的程序块运行时才存在;static对象在使用之前分配,程序结束时销毁

堆内存:每个程序拥有一个内存池,自由空间,用来存储动态分配的对象

why?1、容器不知道自己需要使用多少对象(eg:容器类)

          2、程序不知道所需数据的准确类型

          3、程序需要在多个对象间共享数据(等到最后一个使用者被销毁时释放数据)

 类的静态成员:

类的静态成员
why?需要一些成员与类本身直接相关,而不是与类的各个对象保持关联(eg:一个银行账户可能需要一个数据成员来表示当前的基准利息);
方式:在成员声明之前加上static,存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据

动态分配对象

动态分配对象
程序运行时分配的对象,声明周期由程序控制,必须显式销毁

管理:(1)运算符new和delete:new,在动态内存中为对象分配空间并返回一个指向该对象的指针;delete,接受一个动态对象的指   针,销毁该对象,并释放与之关联的内存(缺点:忘记释放内存会导致内存泄漏)

           (2)智能指针,自动释放所指向对象;shared_ptr允许多个指针指向同一对象;unique_ptr"独占"所指向的对象

 

shared_ptr

初始化方式

默认初始化,保存着一个空指针

 

 

用new返回的内置指针来初始化智能指针,必须使用直接初始化(因为接受指针参数的智能构造函数时explicit,阻止隐式转换,因此不能将一个内置指针隐式转换为一个智能指针)

同理,一个返回shared_ptr的函数不能在返回语句中隐式转换一个普通指针,必须显示绑定到一个想要返回的指针上

shared_ptr<string> p1;   //默认初始化
shared_ptr<int> p1 = new int(1024);   //错误:拷贝初始化
shared_ptr<int> p2(new int(1024));    //正确:直接初始化,相当于显示绑定

shared_ptr<int> clone(int p){
    return new int(p);   //错误:存在隐式转换
}

shared_ptr<int> clone(int p){
    return shared_ptr<int> (new int(p));   //正确:显式绑定
}

shared_ptr和unique_ptr都支持的操作
shared_ptr<T> sp     空指针,可以指向类型为T的对象
p                               条件判断,判断是否为空指针
*p                             解引用,获得所指对象
p->mem                   等价于(*p).men
swap(p,q)                交换p和q中的指针,  等同于p.swap(q)       

 

shared_ptr独有的操作
make_shared<int> (args)   返回一个shared_ptr,在动态内存中分配一个对象并用args初始化它;如果不传递任何参数,对象会进行值初始化
shared_ptr<T> p(q) p是shared_ptr q的拷贝;会递增q的计数器
p=q 会递减p的引用次数,递增q的引用次数
shared_ptr<int> p3 = make_shared<int>(42);
shared_ptr<int> p4 = make_shared<int>();    //值初始化为0

 

拷贝和赋值
每个shared_ptr中都有一个关联的计数器,称为引用计数;计数器变为0,自动释放自己所管理的对象。
引用次数递增:用一个shared_ptr初始化另一个shared_ptr/将它作为参数传递给一个函数/作为函数返回值
引用次数递减:给一个shared_ptr赋予一个新值/局部的shared_ptr离开其作用域
auto r = make_shared<int>(42);
r = q;          //给r赋值,指向另一个地址;
                //增加q指向对象的引用计数
                //递减r指向对象的引用计数
                //r原来指向的对象已没有引用者,自动释放

 

销毁

析构函数完成销毁工作,释放对象所分配的资源;析构函数会递减它指向对象的引用计数

 

默认使用delete释放它关联对象

 

直接内存管理
使用运算符new分配内存,delete释放New分配的内存
使用new动态分配和初始化对象

在自由空间分配的内存是无名的,因为New无法为其分配的对象命名,而实返回一个指向该对象的指针

int *pi = new int;    //pi指向一个未初始化的int

int *ps = new string; //初始化为空string

动态分配对象进行默认化,所以内置类型或者组合类型的对象的值将是未定义的,类类型对象用默认构造函数进行初始化

 

直接初始化

  • 传统构造方法(圆括号)
  • C++新标准(列表初始化)

int *pi = new int(1024);

vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7};

  • 值初始化,在类型名之后跟一对空括号即可

int *pi1 = new int;  //默认初始化,*pi1的值未定义

int *pi2 = new int();  //值初始化为0,

 

动态分配的const对象

一个动态分配的const对象必须进行初始化(对于定义了默认构造函数的类类型,其const动态对象可以隐式初始化;其他类型对象必须显式初始化)

const int *pci = new const int(1024);

const string *pcs = new const string;

 

内存耗尽
  • 如果new不能分配所要求的内存空间,会抛出一个类型为bad_alloc的异常
  • 可以通过定位new表达式向new传递额外参数,阻止抛出异常

int *p1 = new int;

int *p2 = new(nothrow) int;  //如果分配失败,返回一个空指针(nothrow:标准库dinginess)

 

释放动态内存

delete:接受一个指针,指向我们想要释放的对象

执行两个动作:销毁给定的指针指向的对象;释放对应的内存

 

传递给delete的指针必须指向动态分配的对象,或者是一个空指针

 

对于一个由内置指针管理的动态对象(new分配的),直到被显式释放之前都是存在的

由shared_ptr管理的内存在最后一个shared_ptr销毁时会被自动释放

void use_factory(T arg){

      Foo *p = factory(arg);

}  //p离开作用域,局部变量p被销毁,但是p指向的动态内存不会自动释放

 

delete之后重置指针值

delete之后,指针变成空悬指针:指向一块曾经保存数据但现在已经无效的内存的指针

 

避免方法:

  • 在指针离开作用域之前释放掉它所关联的内存,这样,内存释放,且指针被销毁
  • 如果需要保留指针,可以在delete之后将nullptr赋予指针

!!!但是重置指针保护有限,问题是有可能多个指针指向相同内存(坚持只使用智能指针,就能避免问题了)

智能指针和异常

如果使用内置指针管理内存,且在new之后在对应的delete之前发生了异常, 则内存不会被释放

void f(){

       int *ip = new int(42);

       //这段代码抛出一个异常,且在f中未被捕获

       delete ip;   

}

为了正确使用智能指针,需要坚持一些基本规范:

  • 不使用相同的内置指针值初始化(或reset)多个智能指针。  double free因为这样产生的智能指针是相互独立的

int *p = new int(1);

shared_ptr<int> p1(p);

shared_ptr<int> p2(p);  // 此时p1和p2相互独立且引用计数都为1,离开作用域时会释放同一对象两次

  • 不delete get() 返回的指针。  double free

get函数,返回一个内置指针,指向智能指针管理的对象。

 

{

    shared_ptr<int> p = make_shared<int>(1);

    delete p.get();     // 释放一次内存

}   // 又要释放一次内存

  • 不使用get() 初始化或reset另一个智能指针。  double free

 

{

    shared_ptr<int> p1 = make_shared<int>(1);

    shared_ptr<int> p2(p1.get());   // 此时p1和p2相互独立且引用计数都为1

}   // 释放同一内存两次

  • 如果你使用get() 返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。   空悬指针

int *p = nullptr;

{

    shared_ptr<int> p1 = make_shared<int>(1);

    p = p1.get();

}   // 此时释放p1所指向的内存,且p为悬空指针

  • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。    非new类型指针需要自己指定如何来释放指针(因为智能指针的自动释放默认使用delete,而delete只能用于动态内存或nullptr)

 

 unique_ptr

unique_ptr

1、定义一个unique_ptr时,需要将其绑定到一个new返回的指针上,且必须使用直接初始化

2、一个unique_ptr拥有其指向的对象,不支持普通拷贝或赋值操作

unique_ptr<string> p1(new string("gzh"));

unique_ptr<string> p2(p1);  //错误:不支持拷贝

 

unique_ptr<string> p3;

p3 = p1;  //错误:不支持赋值

3、虽然不能拷贝、赋值unique_ptr,但是可以调用release\reset将指针的所有权从一个unique_ptr转移到另一个unique_ptr

unique_ptr<string> p2(p1.release() );   //release返回p1当前保存的指针,并将p1置为空

 

unique_ptr<string> p3(new string ("abc") );

p2.reset(p3.release() );   

//reset接受一个可选的指针参数,令unique_ptr重新指向给定的指针。如果unique_ptr不为空,它原来的对象“abc”被释放;

不能拷贝unique_ptr的例外

可以拷贝或赋值一个将要被销毁的unique_ptr(最常见的例子是函数返回一个unique_ptr,因为编译器知道要返回的对象即将被销毁)

unique_ptr<int> clone(int p){

      return unique_ptr<int>(int new(p));

}

 

unique_ptr<int> clone(int p){

      unique<int> ret(new int(p));

      return ret;      //可以返回一个局部对象的拷贝

}

 

 

weak_ptr

weak_ptr(弱共享)

1、一种不控制所指对象生存周期的智能指针,指向一个shared_ptr管理的对象;

2、将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用次数

3、对象释放跟有没有weak_ptr指向没有关系,最后一个shared_ptr被销毁,对象会被释放

 

4、由于对象可能不存在,不能使用weak_ptr直接访问对象,必须调用lock;

lock检查weak_ptr指向的对象是否存在,如果存在,lock返回一个指向共享对象的shared_ptr

auto p = make_shared<int>(42);

weak_ptr<int> wp(p);

 

if(shared_ptr<int> np = wp.lock() ){   //如果np不为空,条件成立

    //if中,np与p共享

}  //退出作用域,np销毁

 

为啥有了shared_ptr还要使用weak_ptr???
  • weak_ptr被设计为与shared_ptr共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。同样,在weak_ptr析构时也不会导致引用计数的减少,它只是一个静静地观察者。weak_ptr没有重载operator*和->,这是特意的,因为它不共享指针,不能操作资源,这是它弱的原因。但它可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象,从而操作资源。    
  • weak_ptr用于解决”引用计数”模型循环依赖问题,weak_ptr指向一个对象,并不增减该对象的引用计数器。

 

 

std::shared_ptr 和 std::weak_ptr的用法以及引用计数的循环引用问题(见程序)
    在Man类内部会引用一个Woman,Woman类内部也引用一个Man。当一个man和一个woman是夫妻的时候,他们直接就存在了相互引用问题。man内部有个用于管理wife生命期的shared_ptr变量,也就是说wife必定是在husband去世之后才能去世。同样的,woman内部也有一个管理husband生命期的shared_ptr变量,也就是说husband必须在wife去世之后才能去世。这就是循环引用存在的问题:husband的生命期由wife的生命期决定,wife的生命期由husband的生命期决定,最后两人都死不掉,违反了自然规律,导致了内存泄漏。
     解决std::shared_ptr循环引用问题的钥匙在weak_ptr手上。weak_ptr对象引用资源时不会增加引用计数,但是它能够通过lock()方法来判断它所管理的资源是否被释放。
 

#include <iostream>

#include <memory>

 

class Woman;

class Man{

private:

	std::weak_ptr<Woman> _wife;

	//std::shared_ptr<Woman> _wife;

public:

	void setWife(std::shared_ptr<Woman> woman){

		_wife = woman;

	}

 

	void doSomthing(){

		if(_wife.lock()){

		}

	}

 

	~Man(){

		std::cout << "kill man\n";

	}

};

 

class Woman{

private:

	//std::weak_ptr<Man> _husband;

	std::shared_ptr<Man> _husband;

public:

	void setHusband(std::shared_ptr<Man> man){

		_husband = man;

	}

	~Woman(){

		std::cout <<"kill woman\n";

	}

};

 

 

int main(int argc, char** argv){

	std::shared_ptr<Man> m(new Man());

	std::shared_ptr<Woman> w(new Woman());

	if(m && w) {

		m->setWife(w);

		w->setHusband(m);

	}

	return 0;

}

动态数组

动态数组

分配一个对象数组的方法:

  1. C++语言定义了一种new表达式语法,可以分配并初始化一个对象数组
  2. 标准库中包含一个allocator的类,允许将分配和初始化分离
new和数组
int *pia = new int[get_size()];   //pia指向第一个int

类型名后面跟一对方括号,指明要分配的对象的数目,大小必须是整型,但不必是常量;

返回指向第一个对象的指针

 

可以用一个表示数组类型的类型别名来分配一个数组

typedef int arrT[42];

int *p = new arrT;

!!!动态数组并不是数组类型,用new分配一个数组,并未得到一个数组类型的对象,而是得到一个数组元素类型的指针

因此不能对动态数组调用begin\end,也不能调用范围for语句处理动态数组中的元素

 

初始化动态分配对象的数组

默认情况下进行默认初始化;可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号

int *pia = new int[10];   //默认初始化,内置类型未定义,10个未初始化的int

int *pia2 = new int[10]();   //10个值初始化为0的int

string *psa = new string[10];    //10个空string

string *psa2 = new string[10]();    //10个空string

新标准中,可以使用元素初始化器的花括号列表

int *pia3 = new int[10]{0,1,2,3,4,5};   //初始化器数目要小于等于元素数目,剩余元素进行值初始化

 

动态分配一个空数组是合法的

不能创建一个大小为0的静态数组,但是可以用new分配一个大小为0的数组,返回一个合法的非空指针,像尾后指针

char arr[0];    //错误:不能定义大小为0的静态数组

char *cp = new char[0];   //正确:但cp不能解引用——因为它不指向任何元素

 

释放动态数组

delete——在指针前面加一个空方括号对(必须的,指示编译器此指针指向一个对象数组的第一个元素;若忽略了方括号,其行为是未定义的)

delete p; //p必须指向一个动态分配对象或为空;

delete [] pa; //pa必须执行一个动态分配的数组或为空

销毁pa指向的数组中的元素,并释放对应的内存;数组中的元素按逆序销毁,最后一个元素首先被销毁,然后倒数第二个。。。

 

智能指针和动态数组

标准库提供了一个可以管理New分配的数组的unique_ptr

unique_ptr<int[]> up(int new[10]);   //up指向一个包含10个未初始化int的数组

up.release();  //自动调用delete[]销毁其指针

shared_ptr不直接支持动态数组,必须提供自己定义的删除器

//为了使用shared_ptr,必须提供自己定义的删除器(lambda作为删除器)

shared_ptr<int> sp(new int[10], [](int *p){delete[] p;});

sp.release();  //使用删除器,其使用delete[]

 

 

allocator类

new分配动态数组的局限性:

1、内存分配和对象构造组合在一起了,有时候会导致赋值两次

2、没有默认构造函数的类不能动态分配数组 

 

allocator:

模板,定义在memory中,将内存分配和对象构造分离开;

必须指明这个allocator可以分配的对象类型

allocator<string> alloc;   //可以分配string的allocator对象

auto const p = alloc.allocate(n);  //分配n个未初始化的string!!!allocator分配的内存是未构造的

 

allocator分配未构造的内存

construct成员函数结构一个指针和零个或多个额外参数,在给定位置构造一个元素

auto p = q;   //p指向最后构造的元素之后的位置

alloc.construct(q++);

alloc.construct(q++, 10, 'c');  //10, 'c'用来初始化构造的对象,必须是与构造对象类型想匹配的合法的初始化器

使用未构造的内存,其行为是未定义的

cout << *p << endl;   //正确:已分配的

cout << *q << endl;   //错误:q指向未构造的内存

对构造的元素使用destroy销毁(!!!销毁的是元素,没有释放内存,可以用来保存其他string)

while (q != p){          //p是const指针,指向分配内存的开始位置,q指向最后构造元素的开始位置

      alloc.destroy(--q);   

}

释放内存通过调用deallocator完成

alloc.deallocator(p, n);   //p指向由allocate分配的内存

 

C与指针 第11章 动态内存分配

malloc和free

malloc:从内存池中取一块合适的内存,返回一个指向被分配内存块起始位置的指针,参数是需要分配的内存字节数(分配的是一块连续的内存,如果系统无法向malloc提供更多的内存,malloc返回一个NULL指针)

free:归还分配的内存

 

//定义在头文件stdlib.h

void *malloc(size_t size);

void free(void *pointer);

为啥返回的是void*???

因为malloc不知道申请的内存是要存储什么类型,而void*可以转换为其他任意类型的指针

calloc和realloc

calloc:在返回指向内存的指针之前把申请到的内存的每一位(bit)都初始化为 0;输入参数为所需元素个数和每个元素的字节数

 

realloc:用于修改一块已经分配的内存块的大小(扩大或缩小);

扩大内存块时,这块内存原先的内容依然保留,新增加的内容添加到原先内存块后面,新内存并未以任何方法进行初始化;

缩小内存块时,内存块尾部内容被拿掉,剩余部分内存的原先内容依然保留;

void *calloc(size_t num_elements, size_t element_size);

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

 

void *realloc(NULL, size_t new_size);   //当第一个参数是NULL, realloc等同于malloc

 

在使用realloc之后,不能再使用指向旧内存的指针,应该使用realloc返回的新指针(因为原先的内存块无法改变大小时,realloc将分配另一块正确大小的内存,并把原先那块内存的内容复制到新的快上)

 

alloc

动态内存分配常见的错误:忘记检查所请求的内存是否分配成功;解决:alloc

MALLOC宏接收元素的数目及元素的类型,计算需要的内存节数,并调用alloc获取内存;

alloc调用malloc并进行检查,确保返回的指针不是NULL

 

程序见C和指针 P224

free注意事项

1、传递给free的指针必须是一个从malloc\calloc\realloc\alloc函数返回的指针(即动态分配的内存)

2、释放一块内存的一部分也是不允许的,动态分配的内存必须整块一起释放

 

3、释放后将指针置空

free(p);

p = nullptr;

 

C++ primer19.1 控制内存分配

string *sp = new string("a value");  

string *arr = new string[10];

 

实际上执行3个操作:

1、调用一个名为operator new(或operator new[])的标准库函数,分配一块足够大的、原始的、未命名的内存空间

2、编译器运行相应的构造函数构造对象

3、对象被分配了空间并构造完,返回一个指向该对象的指针

 

delete sp;

实际上执行了两个操作:

1、对sp所指的对象执行对应的析构函数

2、编译器调用名为operator delete的标准库函数释放内存空间

 

重载new和delete

应用程序可以在全局作用域中定义operator new函数和operator delete函数,也可以定义为成员函数

 

当编译器发现一条new表达式之后,将在程序中查找可以调用的operator函数

  • 如果被分配的对象是类类型,编译器首先在类及其基类的作用域中查找
  • 否则,编译器在全局作用域中查找
  • 最后,使用标准库定义的版本

::new只在全局作用域中查找匹配的

19.1.2定位new表达式

。。。。(待补充)

 

内存分配方式:(摘抄:C++内存管理(超长,例子很详细,排版很好)

分配方式简介

  在C++中,内存分成6个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区、代码区

  栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

  堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。

  自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。

  全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。

  常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。

       代码区:存放程序的二进制代码。

明确区分堆与栈

 

  在bbs上,堆与栈的区分问题,似乎是一个永恒的话题,由此可见,初学者对此往往是混淆不清的,所以我决定拿他第一个开刀。

  首先,我们举一个例子:

void f() { int* p=new int[5]; }

  这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:(汇编需结合深入理解操作系统)

00401028 push 14h

0040102A call operator new (00401060)

0040102F add esp,4

00401032 mov dword ptr [ebp-8],eax

00401035 mov eax,dword ptr [ebp-8]

00401038 mov dword ptr [ebp-4],eax

  这里,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p么?澳,错了,应该是delete []p,这是为了告诉编译器:我删除的是一个数组,VC6就会根据相应的Cookie信息去进行释放内存的工作。

堆和栈究竟有什么区别?

  好了,我们回到我们的主题:堆和栈究竟有什么区别?

  主要的区别由以下几点:

  1、管理方式不同;

  2、空间大小不同;

  3、能否产生碎片不同;

  4、生长方向不同;

  5、分配方式不同;

  6、分配效率不同;

  管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。

 

  空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:

  打开工程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最大值和commit。

  注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。

 

  碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。

 

  生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

 

  分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

 

  分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

  从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。

  虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。

 

无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的:)

重载全局的new和delete操作符

可以很容易在全局作用域重载new和delete操作符,如下:

void *operator new(size_t size){

       void *p = malloc(size);

       return p;

}

 

void *operator delete(void *p){

        free(p);

}

也可以在类中进行重载new和delete,如下:

class TestClass{

public:

void *operator new(size_t size);

void *operator delete(void *p);

};

void *TestClass::operator new(size_t size){

       void *p = malloc(size);

       return p;

}

 

void *TestClass::operator delete(void *p){

        free(p);

}

所有TestClass 对象的内存分配都采用这段代码。更进一步,任何从TestClass 继承的类也都采用这一方式,除非它自己也重载了new 和 delete 操作符。通过重载new 和 delete 操作符的方法,你可以自由地采用不同的分配策略,从不同的内存池中分配不同的类对象

为单个的类重载 new[ ]和delete[ ]

必须小心对象数组的分配。你可能希望调用到被你重载过的new 和 delete 操作符,但并不如此。内存的请求被定向到全局的new[ ]和delete[ ] 操作符,而这些内存来自于系统堆。

class TestClass{

public:

void *operator new[](size_t size);

void *operator delete[](void *p);

};

void *TestClass::operator new[](size_t size){    

       void *p = malloc(size);

       return p;

}

 

void *TestClass::operator delete[](void *p){

        free(p);

}

 

int main() {

       TestClass *p = new TestClass[10];

       delete []p;

}

 

常见的内存错误及对策
  • 内存分配未成功,却使用了它

解决办法:在使用之前,检查指针是否未NULL,如果指针P是函数的参数,在函数入口处使用(assert(p != NULL));(如果系统无法向malloc提供更多的内存,malloc返回一个NULL指针;类似地,如果分配失败,new返回一个空指针,且抛出std::bad_alloc)

  • 内存虽然分配成功,但是尚未初始化就使用它

犯这种错误的原因:一是没有初始化的概念;二是以为内存的缺省初值都为0;内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。

  • 内存分配成功,但是操作越过了内存的边界
  • 忘记释放内存,造成内存泄露
  • 释放了内存之后继续使用
 

指针与数组的对比

修改内容

1、数组名是指向数组首地址的指针,是常量指针,不可修改,但是数组内容可以修改;

char a[] = “hello”; 

a[0] = ‘X’;

cout << a << endl;

char *p = “world”; // 注意p指向常量字符串

p[0] = ‘X’; // 编译器不能发现该错误,但是运行会出错

cout << p << endl;

“hello”是一个初始化字符数组的初始化列表;"world"是字符串常量;

分辨字符串常量和初始化列表快速记法:

当它初始化一个字符数组时,它就是一个初始化列表;其他任何地方,它都是一个字符串常量(位于静态存储区,内容不可修改)

 

 

内容复制与比较

1、不能将数组内容拷贝给其他数组作为初始值,也不能用数组为其他数组赋值,应该用标准库函数strcpy进行复制。同理,比较b和a的内容是否相同,不能用if(b==a) 来判断,应该用标准库函数strcmp进行比较。

 

b == a;   //运算符会比较数组的开始内存地址,而不是数组的内容。

 

2、可以使用C语言标准库提供的一组函数来操纵C风格字符串(primer p109)

C风格字符串:从C继承而来的,为了表达和使用字符串而形成的一种约定俗成的写法。按此习惯书写的字符串存放在字符数组中并以空字符结束。空字符结束的意思是在字符串最后一个字符后面跟着一个空字符('\0').(字符串字面值是一种C风格字符串

 

C风格字符串的函数

strlen(p)            返回p的长度,空字符不计算在内(沿着p在内存中的位置不断向前寻找,直到遇到空字符才停下来)

strcmp(p1, p2)  返回p1和p2的相等性,若p1 == p2,返回0;若p1 > p2, 返回一个正值; 若p1 < p2, 返回一个负值

strcat(p1, p2)    将p2附加到p1上,返回p1

strcpy(p1, p2)   将p2拷贝给p1,返回p1

传入此类函数的指针必须指向以空字符作为结束的数组:

char a[] = {'g', 'z', 'h'};    //不以空字符结束

cout << strlen(ca) << endl; //错误

3、语句p = a 并不能把a的内容复制指针p,而是把a的地址赋给了p。要想复制a的内容,可以先用库函数malloc为p申请一块容量为strlen(a)+1个字符的内存,再用strcpy进行字符串复制。同理,语句if(p==a) 比较的不是内容而是地址,应该用库函数strcmp来比较。

//数组

char a[] = "hello";

char b[10];

strcpy(b, a);   //b必须足够大以便容纳下结果字符串及末尾的空字符

 

//指针

int len = strlen(a);

char *p = (char *)malloc(sizeof(char *)*(len + 1));  //+1是加上空字符

strcpy(p, a);

 

 

计算内存容量

用运算符sizeof可以计算出数组的容量(字节数)。如下示例中,sizeof(a)的值是12(注意别忘了’’)。指针p指向a,但是sizeof(p)的值却是4。这是因为sizeof(p)得到的是一个指针变量的字节数,相当于sizeof(char*),而不是p所指的内存容量。C++/C语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。

char a[] = "hello world";      //空格占一个字节,空字符'\0'占一个字节

char *p = a;

cout<< sizeof(a) << endl; // 12字节

cout<< sizeof(p) << endl; // 4字节

注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。如下示例中,不论数组a的容量是多少,sizeof(a)始终等于sizeof(char *)。

void Func(char a[100])

{

 cout<< sizeof(a) << endl; // 4字节而不是100字节

}

 

指针参数如何传递内存的?

如果函数的参数是一个指针,不要指望用该指针去申请动态内存。如下示例中,Test函数的语句GetMemory(str, 200)并没有使str获得期望的内存,str依旧是NULL,为什么?

void GetMemory(char *p, int num)

{

 p = (char *)malloc(sizeof(char) * num);

}

void Test(void)

{

 char *str = NULL;

 GetMemory(str, 100); // str 仍然为 NULL

 strcpy(str, "hello"); // 运行错误

}

因为传递给GetMemory的相当于指针p的拷贝_p,都指向同一块内存;如果修改了_p指向的内容,p指向的内容也会相应的改变;但是malloc,_p申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存。

 

解决方式:

  • 使用“指向指针的指针”

void GetMemory2(char **p, int num)

{

 *p = (char *)malloc(sizeof(char) * num);

}

void Test2(void)

{

 char *str = NULL;

 GetMemory2(&str, 100); // 注意参数是 &str,而不是str

 strcpy(str, "hello");

 cout<< str << endl;

 free(str);

}

  • 用函数返回值来传递动态内存。这种方法更加简单

char *GetMemory3(int num)

{

 char *p = (char *)malloc(sizeof(char) * num);

 return p;

}

void Test3(void)

{

 char *str = NULL;

 str = GetMemory3(100);

 strcpy(str, "hello");

 cout<< str << endl;

 free(str);

}

  • 用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把return语句用错了。这里强调不要用return语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡

char *GetString(void)

{

 char p[] = "hello world";

 return p; // 编译器将提出警告

}

void Test4(void)

{

 char *str = NULL;

 str = GetString(); // str 的内容是垃圾

 cout<< str << endl;

}

 

杜绝野指针

野指针不是NULL指针,是指向垃圾内存的指针,成因主要有两种:

  • 指针变量没有被初始化, 缺省值是随机的,可能乱指一气(指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存)

char *p = NULL;

char *p = (char *)malloc(100);

  • 指针p被free或者delete之后,没有置为NULL,让人误以为p是一个合法的指针

有了malloc、free为啥还要有new\delete

malloc\free是C++\C的标准库函数,new\delete是c++运算符

对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。

因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。我们先看一看malloc/free和new/delete如何实现对象的动态内存管理

 

内部数据类型和非内部数据类型

内部数据类型是编译器本来就认识的,不需要用户自己定义,如int,char,double

非内部数据类型不是编译器本来就认识的,需要用户自己定义才能让编译器识别,如enum,union,class、struct

 

运算符使用是否正确,编译器在编译扫描分析时就可以判定

库函数是已编译的代码,编译器不会编译检查,由链接器将库同用户写的代码合成exe文件
 

 

class Obj{

public:

       Obj(void){ cout << “Initialization” << endl; }

  ~Obj(void){ cout << “Destroy” << endl; }

  void Initialize(void){ cout << “Initialization” << endl; }

  void Destroy(void){ cout << “Destroy” << endl; }

};

void useMallocFree(viod){

      Obj *p = (Obj *)malloc(sizeof(Obj));   //调用malloc申请内存

      p->Initialize();  //初始化对象

   

      p->Destroy();  //清除对象

      free(p);

}

 

void useNewDelete(void){

      Obj *p = new Obj;    //申请动态内存并且初始化

      delete p;    //析构对象并且释放内存

 

}

既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。

malloc\free的使用要点

函数malloc的原型如下:

void * malloc(size_t size);

用malloc申请一块长度为length的整数类型的内存,程序如下:

int *p = (int *) malloc(sizeof(int) * length);  //其实是进行显式转换,将void *转换成需要的指针类型

我们应当把注意力集中在两个要素上:“类型转换”和“sizeof”。

* malloc返回值的类型是void *,所以在调用malloc时要显式地进行类型转换,将void * 转换成所需要的指针类型。

* malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!