发布时间:2023-01-27 20:30
内存分四区:代码区、全局区、栈区、堆区。前两者编译时划分,后两者运行时划分。全局区存放全局变量和静态变量以及常量,其中常量区里含有字符串常量和其他常量(const修饰的全局变量)且常量区的数据不可更改。栈区包含形参、局部变量等。堆区使用malloc、new等函数开辟的空间。(内存分区详情以及new的使用见《C++笔记二》第一节内存四区。)
示例:
const int t1 = 10;
int t2;
void test21(int t21)
{
cout<<"栈区——函数形参的地址:"<<&t21<<endl;
}
void test2()
{
int *a;
cout<<sizeof(a)<<endl; //测试指针所占内存
a = new int;
cout<<"堆区——堆区开辟的地址:"<<a<<endl;
delete a;
a = NULL;
const char *cPtr1 = "abcde";
const char *cPtr2 = "abcde";
cout<<"全局区——字符串常量abcde的地址:"<<&"abcde"<<endl;
cout<<"栈区——指针cPtr1的地址"<<&cPtr1<<endl;
cout<<"栈区——指针cPtr2的地址"<<&cPtr2<<endl;
int t;
cout<<"栈区——局部变量的地址:"<<&t<<endl;
test21(10); //函数形参_栈区
cout<<"全局区——常量区的其他常量即const修饰的全局变量的地址:"<<&t1<<endl;
cout<<"全局区——全局变量地址:"<<&t2<<endl;
}
//结果:
//8
//堆区——堆区开辟的地址:0xb31510
//全局区——字符串常量abcde的地址:0x488000
//栈区——指针cPtr1的地址0x6ffde0
//栈区——指针cPtr2的地址0x6ffdd8
//栈区——局部变量的地址:0x6ffdd4
//栈区——函数形参的地址:0x6ffdb0
//全局区——常量区的其他常量即const修饰的全局变量的地址:0x4880fc
//全局区——全局变量地址:0x4a8030
其一:指针占内存空间为8字节,这与操作系统的位数有关(见《C++笔记一》第七节指针)。指针存放的是地址,64位系统显然可以有64个“位置”给0、1来排列组合以表示地址,所以地址长度就是64比特也就是8字节。内存最大也就是2的64次方比特。
其二:指针cPtr1的地址与指针cPtr2的地址相差8,原因就是一个地址对应一个存储单元,一个存储单元可以存储1个字节的内容,而指针变量大小占8个字节(64位系统),所以相邻俩指针地址相差8。
1> 动态分配的对象(运行时分配的对象,其生存周期由程序控制)存储在堆中。
2> 动态内存:程序员根据需求在堆中开辟的一块内存。
3> 动态内存的管理由new\delete完成的。
1> 程序不知道自己需要使用多少对象。(比如我们使用容器类,当空间不够时可以动态扩展。又比如看直播,有时10人,有时10000人,使用动态内存可以来一个人创建一个对象)
2> 程序不知道所需对象的准确类型。(因为有多态的存在。比如商品基类,子类为锅碗瓢盆等,有个商品上架的操作需要商品的指针,具体是什么商品不确定)
3> 程序需要在多个对象间共享数据。(为了更容易更安全的使用动态内存,C++里提供了指针指针,像shared_ptr可以让多个指针指向同一对象,实现共享。问题是static也可以实现共享,为什么不多用static?因为动态内存空间更大,使用更加灵活,可以由程序员自己创建、销毁。但static创建后就位于全局区,使用不够灵活,且占用内存太大)
1> 确保在正确的时间释放动态内存是及其关键的,如果我们忘记释放,就会造成内存泄漏。
2> 有时在还有指针指向动态内存时,我们就释放了,这就会出现引用非法内存的指针的问题。
使用new分配空间,使用delete释放空间。
1>默认初始化,内置类型的对象的值未定义,类类型则调用默认构造函数。
type * p = new type;
2>直接初始化,使用圆括号(或者列表初始化使用花括号),调用相应的构造函数
type * p = new type(value);
type * p = new type {v1,v2……};
vector< int > * p= new vector< int >{1,2,3,4,5};
3> 值初始化,类型名后跟空括号
type * p = new type();
注意:对于类类型来说默认初始化与值初始化效果完全一样,都是调用默认构造函数。对于内置类型来说使用默认初始化,其值未定义;使用值初始化,有良好定义的值。
4> 使用auto初始化
auto p1 = new auto(value);
注意,括号中只能有单一初始化器才能使用auto!!!auto *p = new auto(value1, value2);是错误的。
5> 允许动态分配const对象
分配const对象,返回的是常量指针。动态分配const对象,必须进行初始化,若没有默认构造函数,则只能显示初始化;若有默认构造函数可以隐式初始化。
void test()
{
const int *p = new const int(1024);
cout<< *p<<endl;
delete p;
const string *stp = new const string;
delete stp;
}
1> 内存耗尽,new表达式就会失效,抛出一个bad_alloc异常。同时也可以不让new抛异常,在new与数据类型之间加(nothrow),如int * p = new (nothrow) int;
2> 释放动态内存
使用delete释放动态内存,delete释放的必须是动态内存或者空指针。非new分配的内存或者相同的指针值释放多次是未定义的。释放const动态内存与一般的一样。delete之后记得将nullptr赋予指针,但是此种保护有限,倘若有两个指针同时指向一块动态内存,释放后,置空一个不影响另一个指针,所以要都置空,但在实际应用中找指向相同内存的指针还是很困难的。
1> 忘记delete内存,造成内存泄漏。
2> 使用已经释放的对象,如果是置空了,还有可能发现这种错误(访问空指针,会报读写访问权限冲突的错误)。
3> 同一块内存释放两次。
解决办法:使用智能指针,这些都不是问题!!!
为了更加容易更加安全的使用动态内存,C++提供了智能指针去管理动态对象,shared_ptr、unique_ptr、weak_ptr。智能指针都是类模板,其中shared_ptr,允许多个指针指向同一个对象;unique_ptr只允许一个指针指向一个对象;weak_ptr指向shared_ptr所指对象,但不会影响shared_ptr指针的计数。智能指针的使用方式与普通指针类似,且这三种智能指针都在头文件memory中。
shared_ptr支持的操作(《C++primer P401》):
1> make_shared函数(shared_ptr的构造)
最安全的分配和使用动态内存的方法就是调用make_shared函数。
make_shared函数是在动态内存中分配一个对象并初始化它,返回一个指向该对象的shared_ptr。make_shared也在头文件memory中。在调用该函数时,make_shared< T >(参数);参数与类型T的构造函数匹配即可!而且可以通过auto来定义一个对象去保存shared_ptr。
class MyPrint
{
public:
void operator()(int v)
{
cout << v << "\t";
}
};
void test()
{
shared_ptr<vector<int>> vp = make_shared<vector<int>>();
for (int i = 0; i < 5; i++)
{
(*vp).push_back(i);
}
for_each((*vp).begin(), (*vp).end(), MyPrint()); //0 1 2 3 4
cout << endl;
shared_ptr<int> inp = make_shared<int>(10);
cout << *inp << endl;
auto sp = make_shared<string>("hello!");
cout << *sp << endl;
}
2> shared_ptr的拷贝与赋值
每个shared_ptr都有一个关联的计数器,通常称为引用计数。执行拷贝或者赋值时,引用计数就会加1。
void test()
{
shared_ptr<int> inp = make_shared<int>(12);
cout << inp.use_count() << endl; //1
shared_ptr<int> inp1 = inp; //赋值
cout << inp.use_count() << endl; //2
auto inp2(inp); //拷贝
cout << inp.use_count() << endl; //3
}
3> shared_ptr自动销毁所管理的对象
当最后一个shared_ptr被销毁时(计数为0),shared_ptr会调用析构函数,销毁对象,释放空间。
注意:当shared_ptr没有使用时一定要销毁,不然会浪费内存。特别是当shared_ptr存储在容器中时,如果不再使用,记得及时删除那些不需要的元素。
4>shared_ptr与new结合起来使用(表12.3)
第一:指针指针的构造函数是explicit的(《C++primer》P265),所以不支持隐式转换。所以内置指针无法隐式转换成智能指针。
void test()
{
//shared_ptr p1 = new int(10); //错误
shared_ptr<int> p2(new int(20)); //正确
int * p = new int(30);
shared_ptr<int> p3(p); //不推荐智能指针与内置指针混合使用
cout << "p2:" << *p2 << " p3:" << *p3 << endl;
}
所以,当函数返回类型是智能指针时,return new int(value); 也是有问题的!!!必须转换成return shared_ptr< int >(new int(value));
第二:一个用于初始化智能指针的内置指针必须是指向动态内存的,简单说内置指针必须是通过new得到的。因为智能指针默认通过delete释放内存。(虽然智能指针并不是一定指向动态内存,如表12.3中第三第四两个方法,但是这里使用内置指针初始化,如表12.3中的第一个,没有删除器,然而智能指针默认的是进行delete,所以只能是通过new获得的内置指针)
第三:不要混合使用智能指针与内置指针
shared_ptr可以协调对象的析构,这仅限于其指针的拷贝。所以,最好使用make_shared,避免了new,也避免了既出现智能指针又出现内置指针的情况(如果智能指针销毁了,内存被释放,内置指针依然指向被释放的内存;如果内置指针delete了,智能指针只要销毁就会出错,销毁时又释放一次内存),还避免了出现多个指向同一块内存但相互独立的智能指针(shared_ptr可以协调析构仅仅是发生了拷贝,倘如因为new产生内置指针,然而又想让多个智能指针指向这块内存,便用new产生的内置指针直接初始化多个智能指针,虽然这些智能指针指向同一块内存,但相互独立,析构互不影响)。
错误示例:
//错误示例
void test()
{
int * p = new int(10);
shared_ptr<int> p1(p);
shared_ptr<int> p2(p);
}
第四:不要用get去初始化另一个智能指针或者为智能指针赋值
使用get返回一个指向智能指针管理的对象的内置指针,目的是为了给那些不能使用智能指针的代码传递一个内置指针。而且还要确保该内置指针不被delete掉。显然不能通过get得到的内置指针去给智能指针赋值,因为内置指针无法隐式转换成智能指针。但也不能去初始化一个另一个智能指针,因为效果与上面不要混合使用智能指针与内置指针讨论的一样。简单说,此用法比较特殊,少用。
第五:其他shared_ptr操作(reset)
reset可以将新的内置指针赋予一个shared_ptr(与赋值类似,所以还是会出现多个相同指向但又相互独立的智能指针的情况),通常可以与unique()一起使用,修改智能指针的指向。
5>智能指针的一个用法(可能不够完善,等后期实际遇到了再做补充)
在确保发生异常后资源能够正确的释放,可以使用智能指针。比如:某个函数由于发生异常过早结束,如果使用了智能指针,由于是局部变量,当函数结束时,智能指针也会被销毁,然后就会检测引用计数,来判断是否释放资源。当然这里不一定是动态内存,但如果是其他资源,需要传递一个删除器,像表12.3中第三第四个方法以及最后一个reset。删除器就是为那些不是通过new获取的资源准备的,而智能指针的作用就是确保资源能够更加安全、方便的使用。
6>智能指针的陷阱
1> 不要用相同的内置指针去初始化多个智能指针
2> 不delete通过get获得的内置指针
3> 不使用get去初始化或者reset另一个智能指针
4> 如果使用了get,需要记住当最后一个智能指针销毁时,该指针就无效了
5> 如果智能指针管理的资源不是通过new获取的内存,那么需要传个删除器进去
某个时刻只能有一个unique_ptr指向给定对象,一旦unique_ptr销毁,所指对象就被销毁。unique_ptr的一些操作(《C++primer》P418),与shared_ptr相同的操作见上面的表12.1
1> 初始化
定义unique_ptr时需要绑定一个new返回的指针,而且也只能直接初始化。区别是unique_ptr不支持拷贝与赋值操作。
2> unique_ptr不允许拷贝与赋值但有特例
虽然不能够拷贝与赋值,但是可以通过release与reset将指针的所有权从一个非cosnt的unique_ptr转移到另一个unique_ptr。
而且不能拷贝unique_ptr有特例:我们可以拷贝或者赋值一个将要被销毁的unique_ptr。比如从函数返回一个unique_ptr。
3> 向unique_ptr传递删除器
与shared_ptr一样,智能指针被销毁时会默认使用delete释放对象。而且unique_ptr也可以传递一个删除器,unique_ptr传递删除器需要在尖括号中给出删除器类型。
weak_ptr是一种不控制所指对象生存期的智能指针,它指向shared_ptr所指对象,但不增加shared_ptr的引用计数。而且,最后一个shared_ptr被销毁时,不管有没有weak_ptr指向该对象,该对象都会被销毁。所以,weak_ptr的目的就是既想“了解”所指资源的信息又不像与其有所牵连。
weak_ptr的一些操作见《C++primer》P420
1> 创建weak_ptr
创建weak_ptr需要使用shared_ptr进行初始化。由于对象可能不存在,所以不能直接使用weak_ptr访问对象,要用lock获取是否有指向对象的shared_ptr。
void test()
{
shared_ptr<int> p = make_shared<int>(10);
weak_ptr<int> wp(p);
if (wp.use_count()!=0)
{
cout << *(wp.lock()) << endl; //返回10
}
}
2> 示例:
class A
{
public:
shared_ptr<A> m_sp;
};
void test()
{
A a1;
A a2;
//sp1指向a1,a1的成员变量指向a2;
//sp2指向a2,a2的成员变量指向a1;
shared_ptr<A> sp1 = make_shared<A>(a1);
shared_ptr<A> sp2 = make_shared<A>(a2);
a1.m_sp = make_shared<A>(a2);
a2.m_sp = make_shared<A>(a1);
//这样构成循环,可以通过weak_ptr解除循环
weak_ptr<A> wp(sp1);
if (wp.use_count() == 1)
{
wp.lock()->m_sp.reset(); //将sp1所指对象a1的成员变量置空
}
//循环解除
}
虽然使用new、delete可以一次分配、释放一个对象,但有时需要一次给很多对象分配内存的功能,这里就要用动态数组。C++可以使用new来分配动态数组,但还可以使用allocator类u,该类可以把分配与初始化分离,而且拥有更好的性能与更加灵活的内存管理能力。
1> 定义动态数组与释放动态数组
分配数组可以得到一个元素类型的而且指向首元素的指针。因为得到的是指针,所以动态数组不是数组类型,显然无法使用begin()与end()!!!
void test()
{
int *a = new int [5] {1, 2, 3, 4, 5};
string *as = new string[5];
delete[]a;
delete[]as;
}
注意:必须给出数组大小,倘若初始化器数目大于元素数目,则new失败,抛出bad_array_new_length异常。动态分配空数组也是合法的!
初始化动态数组与释放动态数组示例如上,只是多了对中括号。而且和上面的一样也有值初始化([ ]后加个空括号)。
int *a = new int[10]; //十个元素都没有初始化
int *b = new int[10](); //十个元素都初始化为0
2>动态数组与智能指针
C++提供了一个可以管理new分配的动态数组的unique_ptr。同上只是加了个中括号。注意:shared_ptr不能直接管理动态数组,如果想用,那就要自己定义删除器。不然,只delete第一个元素,就像delete没加[ ]的效果一样。同时shared_ptr没有重载[ ],所以只能通过get获取内置指针,再去操作。
void test()
{
unique_ptr<int[]> ap(new int[10]{ 1,2,3,4,5,6,7,8,9,0 });
for (int i = 0; i < 10; i++)
{
cout << ap[i] << endl;
}
ap = nullptr;
}
指向数组的unique_ptr的一些操作《C++primer》P426
1>使用new\delete的弊端
new、delete把内存分配与对象构造、对象析构与内存释放组合在一起了,所以有时new的时候会分配很大块内存,却用的不多,可能导致不必要的浪费,有局限性。而且更重要的是,那些没有默认构造函数的类不能动态分配数组。
class A
{
public:
A(int v) :m_A(v) {}
int m_A;
};
void test()
{
A *p = new A[10]; //报错:A没有默认构造函数
int *b = new int[100]{1,2,3,4,5};
}
2>allocator
allocator是类模板,在头文件memory中。allocator可以将内存分配与对象构造分开。
allocator类及其算法《C++ primer》P428
allocator< T > a; //确定类型
a.allocate(n); //分配内存,是未构造的内存,返回类型为T的指针
a.construct(p, args) //p是类型T的指针,args是构造函数,构造1个对象
a.destroy§; //析构p所指对象
a. deallocate(p,n); //释放从p所指空间开始的n个对象,而且一定要保证释放空间前,对象都析构了(不管是类类型还是内置类型)。p必须是allocate返回的指针,n必须是allocate里的n。
class A
{
public:
A(int v) :m_A(v) {}
int m_A;
};
void test()
{
allocator<A> a; //A 类型
auto p = a.allocate(5); //5个A对象
auto p1 = p;
for (int i = 1; i < 6; i++)
{
a.construct(p1++, A(i)); //构造对象
}
cout << (p+2)->m_A << endl; //返回3
for (int i = 1; i < 6; i++)
{
a.destroy(--p1); //析构对象,这里p1最开始指向最后一个元素的后一位
}
a.deallocate(p, 5); //释放内存
}
allocator还有两个伴随算法,在头文件memor中。《C++ primer》P429
举个例子:我们像把int型vector容器存储到动态内存里,内存是之前两倍,拷贝放到前半部分,后半部分用给定值填充。
void test()
{
vector<int> v;
for (int i = 0; i < 10; i++)
{
v.push_back(i);
}
allocator<int> a;
auto p = a.allocate(2 * (v.size())); //申请内存
auto p1 = p;
auto p2 = uninitialized_copy(v.begin(), v.end(), p1); //p2指向第11个元素
uninitialized_fill_n(p2, 10, 6);
for (int i = 0; i < 20; i++)
{
cout << *(p++) << " ";
}
//0 1 2 3 4 5 6 7 8 9 6 6 6 6 6 6 6 6 6 6
cout << endl;
auto pl = p;
for (int i = 0; i < 20; i++)
{
a.destroy(--p);
}
a.deallocate(p, 2 * (v.size()));
}