第12章 动态内存
12.1 智能指针
shared_ptr<T>
make_shared<T>(args)
直接初始化 new int(10);
默认初始化 new int;
值初始化 new int();
由内置指针(而不是智能指针)管理的动态内存在被显示释放前一直都会存在。
最好坚持只使用智能指针;
delete之后重置指针值为nullptr;
unique_ptr
u = nullptr 释放u指向的对象,将u置为空
u.release() u放弃对指针的控制权,返回指针,将u置为空
u.reset() 释放u指向的对象
u.reset(q) 释放u指向的对象,如果q为内置指针,将u指向这个对象
u.reset(nullptr) 释放u指向的对象,将u置为空
例子:
unique_ptr<string>p1(new string(“sta”));
unique_ptr<string>p3(new string(“test”));
release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。
p2.reset(p3.release());
或者unique_ptr<string> p2(p1.release());
12.2 动态数组
int *pia = new int[10](); //10个值初始化为0的int
delete [] pia;
智能指针和动态数组
unique_ptr<int []> up(new int[10]); //up指向一个包含10个未初始化int的数组
up.release(); //自动用delete[]销毁其指针
allocator 类
allocator<T> a
a.allocate(n)
a.deallocate(p,n)
a.construct(p,args)
a.destroy(p)
第13章 拷贝控制
13.1 拷贝、赋值与销毁
在定义任何C++的类时,拷贝控制操作都是必要部分。
拷贝构造和移动构造定义了当用同类型的另一个对象初始化本对象时做什么。
拷贝赋值和移动赋值定义了将一个对象赋予另一个对象时做什么。
string sBook = “C++primer”; // 拷贝初始化 string b; //默认初始化 b = sBook; //拷贝赋值 |
13.1.1 拷贝构造函数
拷贝构造函数:如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数是拷贝构造函数。
合成拷贝构造函数:如果没有定义拷贝构造函数,编译器会为我们定义一个。
一般情况,合成的拷贝构造函数会将起参数的成员逐个拷贝到正在创建的对象中。
每个成员的类型决定它如何拷贝。数值成员逐个拷贝。
拷贝初始化 VS 直接初始化
参考:https://www.cnblogs.com/cposture/p/4925736.html
直接初始化:要求编译器使用普通的函数匹配来选择最匹配的构造函数。
拷贝初始化:要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要还要经行类型转换。
拷贝初始化何时发生?
1. 用=定义变量时 *********实验,临时变量
2. 将一个对象作为实参传递给一个非引用类型的形参
3. 从一个返回类型为非引用类型的函数返回的对象 ******实验,编译器优化
4. 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
5. 标准容器库调用insert或push成员 *********实验,emplace比push高效
ClassTest ct2 = "ab"; //复制初始化 实际。。。编译器的思想是能不用临时变量就不用临时变量 |
ClassTest ct6 = getOne();//复制初始化 实际。。。直接将ct6的地址带入函数经行了初始化 |
#include <map> #include <iostream> #include <unordered_map> #include <vector>
#include <cstring>
using std::cout; using std::endl; struct T_TEST { unsigned int mme = 13; int test = 5; T_TEST() = default; T_TEST(unsigned int _mme, int t) { mme = _mme; test = t; }; };
typedef struct viterbiNode { int attr = 2;
}T_VITERBINODE; unsigned char IP[] = "111.111.111.111";
struct T_CONSTRUCT { int a = 1; int b = 2;
T_CONSTRUCT() { std::cout<<"default construct"<<std::endl; }; T_CONSTRUCT(int _a, int _b) { a = _a; b = _b; std::cout<<"two param construct"<<std::endl; }; T_CONSTRUCT(const T_CONSTRUCT& other) { a = other.a; b = other.b; std::cout<<"copy construct"<<std::endl; };
};
class ClassTest { public: ClassTest() { c[0] = '\0'; cout<<"ClassTest()"<<endl; } ClassTest& operator=(const ClassTest &ct) { strcpy(c, ct.c); cout<<"ClassTest& operator=(const ClassTest &ct)"<<endl; return *this; } ClassTest(const char *pc) { strcpy(c, pc); cout<<"ClassTest (const char *pc)"<<endl; } // private: ClassTest(const ClassTest& ct) { strcpy(c, ct.c); cout<<"ClassTest(const ClassTest& ct)"<<endl; } private: char c[256]; };
ClassTest getOne() { cout<<"getOne begine"<<endl;
ClassTest a("a");
cout<<"getOne end"<<endl; return a; };
int main() {
std::vector<T_CONSTRUCT> v1; std::vector<T_CONSTRUCT> v2;
std::cout<<"push:"<<std::endl;
v1.push_back(T_CONSTRUCT(3,4));
std::cout<<"emplace:"<<std::endl;
v2.emplace_back(5,6);
cout<<"ct1: "; ClassTest ct1("ab");//直接初始化 cout<<"ct2: "; ClassTest ct2 = "ab";//复制初始化 cout<<"ct3: "; ClassTest ct3 = ct1;//复制初始化 cout<<"ct4: "; ClassTest ct4(ct1);//直接初始化 cout<<"ct5: "; ClassTest ct5 = ClassTest();//复制初始化 cout<<"ct6: "; ClassTest ct6 = getOne();//复制初始化 } |
延伸:聚合类
满足条件: 所有成员都是public 没有定义任何构造函数 没有类内初始值,如果有初始值,不能用{}进行赋值了,否则编译不过 没有基类,也没有virtual函数
不足: 所有成员public 将正确初始化的工作交给用户 增加或者删除一个成员后,所有的初始化语句都需要更新
初始化: 初始化列表,如果个数少于成员数量,靠后的成员被值初始化
默认初始化: 如果内置类型的变量未被显示初始化,它的值由定义的位置决定。 定义在任何函数体之外的变量被初始化为0; 定义在函数内部的类型变量将不被初始化。
为什么有这种区别? 1. 变量存储的位置; 2. C++为了兼容C。
|
POD类: 聚合类的一种 参考:https://www.cnblogs.com/DswCnblog/p/6371071.html POD的定义:极简的、属于标准布局的 KW检查:非POD类,不能用memset等内存式的拷贝 memset(pthandleInfo, 0, sizeof(T_HANDLEINFO));
为什么需要POD类型? 可以使用字节赋值,比如memset,memcpy操作 对C内存布局兼容。 保证了静态初始化的安全有效。
是否是POD类型? c++11 用std::is_pod<T>::value 判断 |
13.1.2 拷贝赋值运算符
Foo& operator=(const Foo&);
理解:拷贝初始化、赋值运算符的区别
string s = string(“2017”); // s拷贝初始化
string j,k;
j = k; // 使用string的拷贝赋值运算符
实验:修改拷贝赋值运算符,共几次拷贝赋值或构造?
cout<<"***********A=getOther(c)"<<endl; d = getOther(c);
cout<<"***********getOther(c)"<<endl; getOther(c);
cout<<"*********T_CONSTRUCT e = getOther(c)"<<endl; const T_CONSTRUCT e = getOther(c); |
13.1.3 析构函数
智能指针是类类型,具有析构函数。智能指针成员在析构阶段会自动销毁。隐式销毁一个内置的指针类型成员不会delete它所指向的对象。
什么时候调用析构函数?
1. 变量在离开作用域是
2. 当一个对象被销毁时
3. 容器被销毁时,其元素被销毁
4. 动态对象应用delete时
5. 临时对象,当创建它的完整表达式结束时
析构函数体自身并不直接销毁成员。成员是在析构函数体之后隐含的析构阶段中被销毁的。
实验:
cout<<"***********getOther(c)"<<endl; getOther(c); copy construct before return copy construct delete delete |
13.1.4 三五法则
1 需要析构函数的类也需要拷贝和赋值操作
2 需要拷贝操作的类也需要赋值操作,反之亦然
13.1.5 使用=default
显示的要求编译器生成合成的版本。
13.1.6 阻止拷贝
通过=delete
本质上,当不能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就是被定义为删除的。
合成的拷贝控制成员可能是删除的:
如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的。
通过private拷贝控制(新标准之前,现在不建议用)
拷贝控制成员声明为pirvate,但不定义它们。
13.2 拷贝控制和资源管理
两种策略:
1. 类的行为像值,通过构造函数和赋值函数控制;
类值拷贝赋值运算符:赋值运算符通常组合了析构函数和构造函数的操作。
类似析构函数,赋值操作会销毁左侧对象的资源;
类似拷贝构造函数,赋值操作会从右侧运算对象拷贝数据;
2. 类的行为像指针,通过shared_ptr的方案解决或者引用计算的控制;
引用计数的工作方式:
构造函数还需要创建一个引用计数,用来记录有多少对象与正在创建的对象共享状态,当我们创建一个对象时,只有一个对象共享状态,计数器初始化1
拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。拷贝构造函数递增计数器,指出给定对象的状态又被一个新用户。
析构函数递减计数器,如果计数器变为0,则析构函数释放状态。
拷贝赋值运算符先递增右侧运算对象的计数器,然后递减左侧运算对象的计数器。如果左侧运算对象的计数器变为0,则销毁状态。
赋值运算符:
当将一个对象赋值给它自身时,赋值运算必须正确;
一个好的方法是在销毁左侧运算对象资源之前拷贝右侧运算对象。
13.3 交换操作swap
对于分配了资源的类,定义swap可能是一种很重要的优化手段。(交换的可能只是某个成员的指针)
class HasPtr{
friend void swap(HasPtr&, HasPtr&);
};
inline
void swap(HasPtr &lhs, HasPtr &rhs)
{
using std::swap;
swap(lhs.ps, rhs.ps);
}
13.4 拷贝控制示例
13.5 动态内存管理类
很好的代码示例
实验:vecoter容量扩容及拷贝P317vector对象是如何增长的?
std::vector<T_CONSTRUCT> v1; std::vector<T_CONSTRUCT> v2; std::cout<<"push 1:"<<std::endl; v1.push_back(T_CONSTRUCT(3,4));
std::cout<<"push 2:"<<std::endl; v1.push_back(T_CONSTRUCT(2,3));
std::cout<<"emplace:"<<std::endl; v2.emplace_back(5,6);
push 1: two param construct copy construct delete
push 2: two param construct copy construct copy construct delete delete
emplace: two param construct |
如何提高性能?
1、 直接push_back()改成用emplace_back(); 2、 reserve(n) 预定n个空间,当然后续push_back()会增加,其中的值不确定; 3、 resize(n, x) 申请n个空间初始化为x。 reserve只是保持一个最小的空间大小,而resize则是对缓冲区进行重新分配, 里面涉及到的判断和内存处理比较多所以比reserve慢一些。 对于数据数目可以确定的时候,先预设空间大小是很有必要的。直接push_back数据频繁移动很是耗时 |
C++11在时空性能方面的改进:https://www.cnblogs.com/me115/p/4788322.html#h26
大小固定容器 array 前向列表 forward_list 哈希表[无序容器] unordered containers 常量表达式 constexpr 静态断言 static_assert move语义和右值引用 原地安置操作 Emplace operations |
13.6 对象移动
移动而非拷贝对象会大幅度提升性能。
标准库容器、string和shared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝。
13.6.1 右值引用
右值引用一个重要的性质:只能绑定到一个将要销毁的对象。
所以:所引用的对象将要被销毁;
该对象没有其他用户。
有哪些是生成右值?
返回非引用类型的函数
算数、关系、位、后置递增递减运算符
std::move 标准库move函数:显示地将一个左值转换为对应的右值引用类型。
注意: 除了对源对象赋值或销毁之外,我们将不再使用它。
调用move之后,我们不能对移后源对象的值做任何假设。
13.6.2 移动构造函数和移动赋值运算符
为了让我们自己的类型支持移动操作,需要为其定义移动构造函数和移动赋值运算符。
移动构造函数
StrVector::StrVec(StrVec &&s) noexcept // 必须是noexcept
:elements(s.elements)
{
s.elements = nullptr;
}
移动后,源对象要保证安全。销毁是无害的。
移动赋值函数
StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
if (this != &rhs)
{
free(); //释放已有元素
elements = rhs.elements;
rhs.elements = nullptr; //源对象可析构
}
return *this;
}
在移动操作之后,移后源对象必须保持有效的、可析构的状态,但用户不能对其值进行任何假设。
为什么是noexcept?
vector异常处理保护。
合成的移动操作:只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能够移动构造或移动赋值时。
移动右值,拷贝左值
StrVec v1, v2;
v1 = v2; // v2是左值,使用拷贝赋值
StrVec getVec(istream &) //getVec 返回一个右值
v2 = getVec(cin); // getVec(cin)是右值,使用移动赋值
左值、右值:
在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。举个例子,int a = b+c, a 就是左值,其有变量名为a,通过&a可以获取该变量的地址;表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它。
如果没有移动构造函数,右值也被拷贝。(没有定义移动构造函数,使用move操作实际是用拷贝构造函数实现)
定义了移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作,否则这些成员默认地被定义为删除的。
建议:不要随意使用移动操作 只有在确信算法在为一个元素赋值或者将其传递给一个用户定义的函数后不再访问它。 小心地使用move,可以大幅度提升性能。随意使用很可能造成错误。 |
13.6.3 右值引用和成员函数
区分移动和拷贝的重载函数通常一个版本接受一个const X&,另一个版本接受一个T&&