IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++ 堆/栈区别,new的用法,以及与malloc的区别 -> 正文阅读

[C++知识库]C++ 堆/栈区别,new的用法,以及与malloc的区别

1、堆与栈的区别

首先从虚拟内存分布上可以看出,栈是由高地址向低地址扩展,堆是由低地址向高地址扩展。

在管理上:

堆内存由程序员手动管理,用new/delete 或者 malloc/free 手动进行分配与回收。如果不进行回收的话,会造成内存泄漏的问题。

栈内存是由编译器管理,不需要程序员管理,会自动分配回收空间,一般保存的是局部变量和函数参数。相比于堆,栈的空间小很多,但空间是连续的,不会有内存碎片的存在。

1、函数调用时,栈帧发生的变化

?在其中,有一个问题需要注意,在函数调用的时候,对参数的压栈是从参数列表的右边开始压栈。至于为什么从右边进行压栈,而不是从左边进行,有一个说法是,在C++/C 中,有可变参数的函数,如果是从左向右压栈,第?个参数(即描述可变参数表各变类型的那个参数)将被放在 栈底,由于可变参的函数第?步就需要解析可变参数表的各参数类型,即第?步就需要得到上述参数,因此,将它放在栈底是很不?便的。)在函数调用的过程中,也不会产生内存碎片。

2、操作系统管理申请内存

实际上系统中有?个空闲链表,当有程序申请的时候,系统遍历空闲链表找到第?个?于等于申请 ??的空间分配给程序,?般在分配程序的时候,也会空间头部写?内存??,?便 delete 回收空间??。当然 如果有剩余的,也会将剩余的插?到空闲链表中,这也是产?内存碎?的原因。

参考:https://zhuanlan.zhihu.com/p/464435500

对于内存池的应用:

在C/C++中我们通常使用malloc,free或new,delete来动态分配内存。

一方面,因为这些函数涉及到了系统调用,所以频繁的调用必然会导致程序性能的损耗;

另一方面,频繁的分配和释放小块内存会导致大量的内存碎片的产生,当碎片积累到一定的量之后,将无法分配到连续的内存空间,系统不得不进行碎片整理来满足分配到连续的空间,这样不仅会导致系统性能损耗,而且会导致程序对内存的利用率低下。

当然,如果我们的程序不需要频繁的分配和释放小块内存,那就没有使用内存池的必要,直接使用malloc,free或new,delete函数即可。

2、认识new

? ? ? ? 在侯捷老师的课程中,new被分为?new?expression;operator?new 和 placement?new 三种。其中new?expression既要分配内存,还有调用构造函数;operator?new仅分配内存,不调用构造函数;placement?new?不分配内存,仅调用构造函数。

在cpp reference中,operator new有三种重载:下面介绍其中的两个,抛异常的new 与 指定地址的new

throwing (1)	
void* operator new (std::size_t size);
nothrow (2)	
void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) noexcept;
placement (3)	
void* operator new (std::size_t size, void* ptr) noexcept;

1、new 的类型和使用

定义Myclass类。

#include <iostream>
struct MyClass {
 ?MyClass() {std::cout <<"MyClass constructed\n";}
 ?~MyClass() {std::cout <<"MyClass destroyed\n";}
};

new?expression:new/delete

int main () {
 ?MyClass * pt = new MyClass();
 ?delete pt; ? ?// implicitly calls ::operator delete(pt)
} 

output: MyClass constructed
         MyClass destroyed

new和delete会先分配内存,然后调用构造函数。因此输出调用的语句如上。

::operator new()/::operator delete()

int main () {
 ?MyClass * pt = (MyClass*) ::operator new(sizeof(MyClass));
 ?::operator delete(pt);
 ?return 0;
}

上述代码不会输出任何结果,因为operator?new并不会调用构造函数,上面的代码只是创建了一个MyClass类型的指针,然后就释放了,没有其余操作。

placement?new

int main() {
 ? ?MyClass* p1 = (MyClass*) ::operator new (sizeof(MyClass));
 ? ?new(p1) MyClass;//placement new
 ? ?p1->~MyClass();//指针可以直接调用析构函数
 ? ?::operator delete(p1);
 ? ?p1 = NULL;//防止产生野指针
 ? ?return 0;
}
[output]    MyClass constructed
            MyClass destroyed

2、cpp reference的例子


#include <iostream>  
struct MyClass {
    int *data = 0;
    MyClass() {std::cout << "constructed [" << this << "]\n";}
    ~MyClass() {delete data;std::cout << "destroyed [" << this << "]\n";}
};
int main () {
    std::cout << "1: ";
    MyClass * p1 = new MyClass;
    // allocates memory by calling: operator new (sizeof(MyClass))
    // and then constructs an object at the newly allocated space

    std::cout << "2: ";
    MyClass * p2 = new (std::nothrow) MyClass;
    // allocates memory by calling: operator new (sizeof(MyClass),std::nothrow)
    // and then constructs an object at the newly allocated space

    std::cout << "3: ";
    new (p2) MyClass;
    // does not allocate memory -- calls: operator new (sizeof(MyClass),p2)
    // but constructs an object at p2

    // Notice though that calling this function directly does not construct an object:
    std::cout << "4: ";
    MyClass * p3 = (MyClass*) ::operator new (sizeof(MyClass));
    // allocates memory by calling: operator new (sizeof(MyClass))
    // but does not call MyClass's constructor
    std::cout<<std::endl;
    std::cout << "5: ";
    delete p1;
    std::cout << "6: ";
    delete p2;
    std::cout << "7: ";
    ::operator delete(p3);
    //delete p3; 
    return 0;
}

以上代码是c++ reference摘过来的,原文中使用的是delete p3,此时就会调用p3的析构函数,然而p3并未构造任何对象,引发错误如下,摧毁了一个未知区域未定义的对象。改为::operator delete(p3),应为正确。

3、placement new如何用?

? ? ? ? placement new是对operator new的重载,对于效率和容错要求比较高的程序来说placement new是很有用的:

1)placement new能够有效的解决内存碎片的问题;

2)malloc、new等申请内存有失败的可能,对有些程序来说是不允许的,而placement new可以做到;

3)placement new不需要执行申请内存的过程,速度更快;

普遍的使用方法如下:
class A{
 ? ? ? …
public:
 ? ? ? doSomething();
};

1)分配内存
char* buff = new char[ sizeof(A) *N+sizeof(int)) ];
memset( buff, 0, sizeof(A)*N +sizeof(int));

2)构建对象
A* pa = new (buff)A;

3)使用对象
pa->doSomething();

4)析构对象,显式的调用类的析构函数。
pa->~A();

5)销毁内存
delete [] buff;

对于buff这块内存可以反复使用,只要重复2)、3)、4)步骤即可。在C++标准中,对于placement operator new []有如下的说明:placement operator new[] needs implementation-defined amount of additional storage to save a size of array. 所以我们必须申请比原始对象大小多出sizeof(int)个字节来存放对象的个数,或者说数组的大小。

4、为什么没有placement delete

因为很简单的道理,先看placement new的作用,定位放置:“new (pointer) ClassName”,旨在使用pointer调用构造函数,并不分配内存;如果有对应的placement delete的话就需要只调用析构函数,而不释放内存,而指针是可以直接显式的调用析构函数的(尽管并不推荐)。此时placement delete的设计就会很多余。

5、总结

平时使用:new+delete???????

MyClass * p1 = new MyClass;
delete p1;

自己写库:operator new+placement new+析构+operator delete

MyClass * p2 = (MyClass*) ::operator new (sizeof(MyClass));
new (p2) MyClass;//placement new
p2->~MyClass();
::operator delete(p2);

记得operator new和operator delete要成对出现!!???????

第三四行可以被替代为:

 delete p2;// 或者delete(p2),两者等同

参考:https://mp.weixin.qq.com/s/M2f2k70mZbkaxcl0ZoawcQ

3、new/delete 与 malloc/free 的区别

? ? ? ? 它们都用于申请动态内存和释放内存。malloc/free 是 c/c++ 中的标准库函数,new/delete是c++中的运算符。

  • new 与 malloc 的区别:

执行内容

? ? ? ? malloc,是按字节来分配内存的(意思是分配的大小,由自己指定)

? ? ? ? new,是先开辟内存(由编译器根据你的对象类型,来决定开辟内存的大小),然后对内存进行初始化(执行构造函数),返回正确指向数据的指针。

返回类型

? ? ? ? malloc,返回类型是void* ,因此在使用的时候,需要强制转换类型;如果申请内存失败,返回nullptr指针;

? ? ? ? new,返回的是申请对象的指针,如果申请内存失败,抛出 bad_alloc类型的异常;因此两者判断是否开辟成功的所做也不一样。

  • delete 与 free 的区别

执行过程

? ? ? ? delete:先调用析构函数,然后释放内存; free 只释放内存。

? ? ? ? 根据delete的工作步骤,如果是针对于内置数据类型,例如 int,float等,new并不需要执行构造函数;delete并不需要执行析构函数,因此和malloc,free此时并没有什么区别。

参考:

https://blog.csdn.net/JACKSONMHLK/article/details/123298801?spm=1001.2014.3001.5501

4、有了malloc/free 为何还需要new/delete?

? ? ? ? 对于非内部数据(与内部数据并列理解,内部数据就是编译器本身就认识,比如int、float、char等,非内部数据比如struct,需要自己定义后,编译器才能认识),malloc/free 无法满足动态创建对象的要求。

? ? ? ?我们希望在创建对象的时候,不仅开辟了内存,同时也完成了构造函数。对象在消亡时,也要自动执行析构函数。而由于 malloc/free是库函数,不在编译器的控制权限之内,也就不能自动执行构造函数与析构函数。所以,在c++中需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理和释放内存工作的运算符delete。

为什么库函数不在编译器的控制范围内?

库函数是已经编译好的代码,编译器不会再去编译检查,由链接器将库与用户写的代码合成obj文件。而运算符,编译器在编译的时候,可以进行判定与操作。

5、STL中,容器是怎样处理内存的?

在看c++ STL的时候发现 很多容器例如vector,map,deque等都有分配器allocator,为什么需要分配器呢?我们下面进行讲解:

自己实现一个模板类 vector,不包括 allocator,

template<typename T>
class Vector {
public:
	Vector(int size_ = 10) {
		first_ = new T[size_];
		last_ = first_;
		end_ = first_ + size_;
	}
	~Vector() {
		delete[] first_;
		first_ = last_ = end_ = nullptr;
	}

	Vector(const Vector<T>& vec) {
		int size_ = vec.end_ - vec.first_;
		first_ = new T[size_];

		int len = vec.last_ - vec.first_;
		for (int i = 0; i < len; ++i) {
			first_[i] = vec.first_[i];
		}
		last_ = first_ + len;
		end_ = first_ + size_;
	}

	Vector<T>& operator=(const Vector<T>& vec) {
		if (this == &vec)
			return *this;
		delete[] first_;

		int size_ = vec.end_ - vec.first_;
		first_ = new T[size_];
		int len = vec.last_ - vec.first_;
		for (int i = 0; i < len; ++i) {
			first_[i] = vec.first_[i];
		}
		
		last_ = first_ + len;
		end_ = first_ + size_;
		return *this;
	}

	void pushBack(const T& val) {
		if (full()) {
			expand();
		}
		*last_++ = val;
	}
	void popBack() {
		if (empty()) {
			return;
		}
		last_--;
	}

	T Back() {
		return *(last_ - 1);
	}

	bool empty() {
		return first_ == last_;
	}
	bool full() {
		return last_ == end_;
	}
private:
	T* first_;
	T* last_;
	T* end_;

	void expand() {
		int size_ = end_ - first_;
		T* newfirst_ = new T[size_ * 2];
		int len = last_ - first_;
		for (int i = 0; i < len; ++i) {
			newfirst_[i] = first_[i];
		}
		delete[] first_;
		first_ = newfirst_;
		last_ = first_ + len;
		end_ = first_ + size_ * 2;
	}
};

从上面的实现中,我们可以看出,在构造一个无指定大小的vector时候,我们传入数据类型,便会直接在堆中默认初始化指定大小个数据类型,之后我们在push_back 的时候,就会执行的是赋值构造函数,而不是拷贝构造函数。下面我们测试一下,我们只是声明一个vector,就将构造函数执行了十次。

class Myobj {
public:
	Myobj() {
		cout << "Myobj" << endl;
	}


	Myobj(const Myobj& obj) {
		cout << "const Myobj&" << endl;
	}
	~Myobj() {
		cout << "~Myobj" << endl;
	}
};

int main()
{
	Vector<Myobj> vec;

	return 0;
}
Myobj
Myobj
Myobj
Myobj
Myobj
Myobj
Myobj
Myobj
Myobj
Myobj
~Myobj
~Myobj
~Myobj
~Myobj
~Myobj
~Myobj
~Myobj
~Myobj
~Myobj
~Myobj

上面未加空间配置器的代码,还存在一个问题,当我们pop_back一个数据的时候,只是将last_指针往前移了一位,如果数据在堆中有内存开辟的话,并没有释放,会造成内存泄漏的问题的存在。

从上面,我们可以知道,需要一个空间配置器,它的作用是将容器的内存开辟与数据类型对象构造?分开,将容器的内存释放与数据类型的对象析构分开。定义一个容器的时候,只开辟内存,等到真正push_back的时候,才构造对象,同理,pop_back的时候,析构对象,容器过了生命周期,在释放内存。

template<typename T>
class Allocator {
public:
	//开辟内存
	T* allocate(size_t size_) {
		return (T*)malloc(sizeof(T) * size_);
	}

	//释放内存
	void deallocate(void* p) {
		free(p); //为什么要用void ,能释放正确吗?
	}

	//负责对象构造
	void construct(T* p, const T& val) {
		new(p) T(val);//定位new
	}

	//负责对象析构
	void destory(T* p) {
		p->~T();
	}
};
template<typename T, typename Alloc = Allocator<T>>
class Vector {
public:
	Vector(int size_ = 10) {
		//first_ = new T[size_];
		first_ = allocator_.allocate(size_);
		last_ = first_;
		end_ = first_ + size_;
	}
	~Vector() {
		//delete[] first_;
		for (T* p = first_; p != last_; ++p) {
			allocator_.destory(p);
		}
		allocator_.deallocate(first_);
		first_ = last_ = end_ = nullptr;
	}

	Vector(const Vector<T>& vec) {
		int size_ = vec.end_ - vec.first_;
		//first_ = new T[size_];
		first_ = allocator_.allocate(size_);

		int len = vec.last_ - vec.first_;
		for (int i = 0; i < len; ++i) {
			//first_[i] = vec.first_[i];
			allocator_.construct(first_ + i, vec.first_[i]);
		}
		last_ = first_ + len;
		end_ = first_ + size_;
	}

	Vector<T>& operator=(const Vector<T>& vec) {
		if (this == &vec)
			return *this;
		//delete[] first_;
		for (T* p = first_; p != last_; ++p) {
			allocator_.destory(p);
		}
		allocator_.deallocate(first_);

		int size_ = vec.end_ - vec.first_;
		//first_ = new T[size_];
		first_ = allocator_.allocate(size_);

		int len = vec.last_ - vec.first_;
		for (int i = 0; i < len; ++i) {
			//first_[i] = vec.first_[i];
			allocator_.construct(first_ + i, vec.first_[i]);
		}

		last_ = first_ + len;
		end_ = first_ + size_;
		return *this;
	}

	void pushBack(const T& val) {
		if (full()) {
			expand();
		}
		//*last_++ = val;
		allocator_.construct(last_, val);
		last_++;
	}
	void popBack() {
		if (empty()) {
			return;
		}
		//last_--;
		last_--;
		allocator_.destory(last_);
	}

	T Back() {
		return *(last_ - 1);
	}

	bool empty() {
		return first_ == last_;
	}
	bool full() {
		return last_ == end_;
	}
private:
	T* first_;
	T* last_;
	T* end_;
	Alloc allocator_;

	void expand() {
		int size_ = end_ - first_;
		//T* newfirst_ = new T[size_ * 2];
		T* newfirst_ = allocator_.allocate(size_);
		int len = last_ - first_;
		for (int i = 0; i < len; ++i) {
			//newfirst_[i] = first_[i];
			allocator_.construct(newfirst_ + i, first_[i]);
		}
		//delete[] first_;
		for (int i = 0; i < len; ++i) {
			allocator_.destory(first_ + i);
		}
		allocator_.deallocate(first_);

		first_ = newfirst_;
		last_ = first_ + len;
		end_ = first_ + size_ * 2;
	}
};

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-06-08 18:52:05  更:2022-06-08 18:52:26 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 16:31:37-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码