(1)C++中的内存分配如下图:
栈用于函数参数以及局部变量值,编译器分配,每个函数有自己的一个堆栈帧,提供独立的内存空间;
堆中放置的是自己申请的内存,如malloc和new申请的;
全局静态区分配的内存分为两部分,初始化以及未初始化的,分开存放;
注意:
类似:p1 = (char *)malloc(10);int *p2=new int(10);
这种分配的空间实在堆中,但是p1,p2这两个指向内存的指针是在栈中;因为这两个指针是进行声明的;
栈分配:只要栈的剩余空间大于所需的空间,就为程序提供内存,否则栈溢出异常;
堆分配:系统中有一个记录空闲内存地址的链表,收到申请之后,遍历链表寻找第一个大于该链表的块;内存块的首地址会记录本次分配的大小,便于delete进行正确释放;分配多余之后的那一块会放回空闲链表中。容易产生碎片。
(2)指针也是堆区,记得初始化,未初始化的指针会指向内存中的随意位置(很危险),如果不想初始化,可以令其为空指针nullptr;
使用完指针之后,要用delete释放;释放之前先设置为nullptr,防止指针再次使用时出错;
(3)动态分配的数组:new[]给数组分配的内存,要用delete[]删除(申请/删除多个内存地址);
(4)智能指针:超出作用域的时候,会自动释放内存;
在C++11之前,有一种智能指针的简单实现:auto_ptr,只不过该指针在STL中使用的时候,会出现问题,所以,被
unique_ptr,shared_ptr,weak_ptr所代替。
(5)new会返回一个指向内存的指针,如果忽略返回值,这块内存也会孤立,内存泄漏;
在delete之前某段代码,程序运行异常退出(或者循环退出。。。),也会因为未调用delete而导致内存泄漏
new和malloc比较:
malloc和free是库函数(编译器不可控),new和delete是运算符;new不仅分配内存,还构建对象;delete会调用对象的析构函数,所以在类里面应用new和delete比较多。
(6)堆栈中声明数组:int array[5];
堆中声明数组:int* arrayptr=new int[5]; 可以动态指定大小,5替换成一个可变的参数
(7)堆栈中多维数组分配:
char board[3][3];
分配结果:在堆栈中分配一堆连续的内存,分别为[0][0],[0][1],[0][2],[1][0],,,,等;
堆中分配多维数组:
(char** board=new char[i][j];这种方式会分配错误!因为多维数组内存布局不连续)
正确的分配方式:先为多维数组的第一个下标分配连续的数组,每个元素是指向另一个数组的指针:
char** myarray = new char*[i];
for(m;m<i;m++) myarray[m]=new char[j];
释放时:先delete[]每一个myarray[i];再delete[] myarray;
注:可以使用vector<vector<char>>的形式!!
(8)将一个指针减去另一个同类型的指针,会得到两个指针指向的元素之间的元素个数;
(9)函数指针:指向函数的指针,存的是函数的地址。类型取决于返回类型;
方法的指针:指向类成员的某一个方法地址,必须要使用&取地址符。
(10)指针和数组:
指针存放的是一个地址;数组是一个连续内存,里面存放相同类型的数据,很多用到数组名的地方,会将其转换成指向首元素的指针;
智能指针:
避免内存泄漏建议采用的技术,原理:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。由于是一个模板,所以在声明的时候需要指定数据类型。
主要使用unique_ptr和shared_ptr #include<memory>
(1)unique_ptr
声明:unique_ptr<simple> simple_ptr(new simple()); 其中simple 为一个类或者是一个数据类型
没有make_unique函数,只能使用new返回指针这种绑定的形式
unique不支持拷贝或者是赋值的操作,但是可以调用release方法将一个unique指针指向对象的所有权转移给另一个unique指针:
unique_ptr<string> p1(new string("hello"));
unique_ptr<string> p2(p1);//错误,不支持拷贝
unique_ptr<string> p2(p1.release());//转移之后p1为空
unique_ptr<string> p3(new string("world"));
p3.reset(p2.release());//p2转移给p3
(2)shared_ptr
特点:引入了计数器(计数器是共享的),可以有多个shared_ptr指向同一块内存,只有在最后一个实例离开作用域的时候,才释放这片内存,故而,它是多线程安全的!!
每一个shared_ptr都知道有多少个相同的shared_ptr指向同一个对象,并进行更新。
两种声明方式:
①使用裸指针的形式:
shared_ptr<string> myPtr(new string("hello"));//会有两次内存分配
其实等价于:
string* p1 = new string("hello");
shared_ptr<string> myPtr(p1);//千万别myPtr = p1,会报错
但是:像上面这样分配不好,一个原因是会引起两次内存分配,第二个原因是如果p1还给另一个shared_ptr赋值,可能会引起两次内存释放。所以一般采用下面的方式:
②使用make_shared函数(c++14才有)make_shared和new一样都是进行创建
shared_ptr<string> myPtr = make_shared<string>("hello");
shared_ptr<string> myPtr2 = make_shared<string>(10,'a');//跟上对象创建的参数即可
也可以auto myPtr = make_shared<string>("hello");
shared_ptr的拷贝和赋值
每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。
一个例子如下:
int
main()
{
shared_ptr<Test> ptest(
new
Test(
"123"
));
shared_ptr<Test> ptest2(
new
Test(
"456"
));
cout<<ptest2->getStr()<<endl;
cout<<ptest2.use_count()<<endl;
ptest = ptest2;
//"456"引用次数加1,“123”销毁
ptest->print();
cout<<ptest2.use_count()<<endl;
//2
cout<<ptest.use_count()<<endl;
//2
ptest.reset();
ptest2.reset();
//此时“456”销毁
cout<<
"done !\n"
;
return
0;
}
|
在进行赋值的时候,右边的计数加一,左边的减一(若为0,则销毁),然后再将右边对象的计数给左边
智能指针的循环引用问题?
template<typename T>
struct ListNode{
T _value;
std::shared_ptr<ListNode> _prev;
std::shared_ptr<ListNode> _next;
ListNode(const T & value)
:_value(value)
,_prev(NULL)
,_next(NULL){}
~ListNode(){
std::cout<<"~ListNode()"<<std::endl;
}
};
void TestWeekPtr(){
std::shared_ptr<ListNode<int>> sp1(new ListNode<int>(10));
std::shared_ptr<ListNode<int>> sp2(new ListNode<int>(20));
sp1->_next = sp2;
sp2->_prev = sp1;
//构成死锁,出了函数作用域,也没有调用析构函数
std::cout<<sp1.use_count()<<std::endl; //sp1的引用计数
std::cout<<sp2.use_count()<<std::endl; //sp2的引用计数
}
循环引用结果是:两个指针指向的对象都等待对方先释放,最后谁也没有释放,造成了内存泄漏
解决方式:使用weak_ptr,weak_ptr对象引用资源时不会增加引用计数,但是它能够通过lock()方法来判断它所管理的资源是否被释放,从而避免访问非法内存。
weak_ptr必须从一个已经存在的shared_ptr(或者weak_ptr)转换而来。
(不能用shared_ptr管理旧版C语言的数组,但是unique_ptr可以)
shared_ptr<int> myPtr(new int[10]);//这种会出错,在回收的时候,只回收myPtr[0],其他的会内存泄漏
解决方法是进行封装,自己手动删除。
但是,unique可以管理。
unique_ptr<int[]> myPtr(new int[10]);
可以参考:使用智能指针时常见的错误
内存常见的陷阱:
1.字符串分配不足,尾部要有‘\0’哨兵字符;(C风格的char s[]出现)
解决方法:(1)使用C++风格字符串string
(2)将内存分配在堆上
2.访问内存越界
写入数组尾部的内存产生缓冲区溢出错误
解决方法:使用string和vector(自动管理内存)
3.内存泄漏————就是分配了没有正常释放(可能忘记了,或者是释放函数未正常执行)
解决方法:使用智能指针
4.双重删除和无效指针
双重删除:第二次在同一个指针上执行delete操作,可能会释放重新分配给另一对象的内存
解决办法:将指针设置为nullptr
野指针--未初始化,或者释放之后未制空,引起程序崩溃
关于allocator类(#include <memory>)
将内存分配和对象创建分离开来,使用者按需创建对象,用法如下:
allocator<string> alloc;
auto const p =alloc.allocate(n);//分配n个未构造的string
alloc.construct(q,args);//执行构造,q是首地址,args为构造函数
关于内存中的边界对齐
class类默认按照最大的那个基本类型大小进行对齐。
class c1
{
char c;
int a;
char d;
}
大小为12
class c2
{
int a;
char c;
char d;
}
大小为8
内存边界对齐的好处是可以增加访问速度;
例如一个0~3的int只需要取一次,一个1~4的int可能要取两次
对于一个结构体包含另外一个结构体的情况:-----取内部结构体中最长
如果一个结构里有某些结构体成员,则结构体成员要从其内部"最宽基本类型成员"的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.
关于类的大小还有两点需要注意:
1.空类的大小为一个字节(大小为0,那么就是啥也没有了)
2.如果有虚函数,那么还隐含包含了一个指向虚函数表的指针,不管有多少个虚函数,只需要一个指针即可;并且这个指针应当放在对象实例的最前面的位置。
RAII-resource acquisition is initialization
提供了一种资源自动管理的方式,当产生异常、回滚等现象时,RAII可以正确地释放掉资源。
原理:利用stack上的临时对象生命期是程序自动管理的这一特点,将我们的资源释放操作封装在一个临时对象中。
class Resource{};
class RAII{
public:
RAII(Resource* aResource):r_(aResource){} //获取资源
~RAII() {delete r_;} //释放资源
Resource* get() {return r_ ;} //访问资源
private:
Resource* r_;
};
其实,可以用智能指针~~~
注意:对RAII对象执行复制的时候,一定要复制管理的资源。
一般执行复制会出问题,常见的解决方法是,采用抑制复制或者引用计数的方式