第一坑,隐式声明的移动构造函数和移动赋值运算符
class A{
int * p;
public:
A(int value):
p(new int(value))
{}
~A(){
std::cout<<"delete pointer:"<<(void *)p<<std::endl;
//查看析够函数删除了哪个指针
delete p;
}
};
int main(){
A a(1);
A b(2);
std::swap(a,b);
/*首先我们来了解一下std::swap的一些特性
1.大概全貌如下:
template<class T>
void swap(T & a ,T & b);
2.要求T必须满足MoveAssignable和MoveConstructible
通俗讲就是有移动构造函数和移动赋值函数
*/
}
结果如下:
delete pointer:0x558527e3be70
delete pointer:0x558527e3be70
delete pointer:0x558527e3be90
你们会发现一二条发生了重复删除了两次,对于这种小范围new的来说,运行期间并没有报错,但到了大范围new数组和delete数组的时候,就会出现double free的现象。
没错,这是系统自动声明会带来的一些后果,如果你们在类里面new一个对象,我们要显示声明移动构造函数或者把移动构造函数删除掉。
第二坑,显式声明移动构造函数
你是不是和我一样把他们写成这样子?
A(const A && );
A & operator=(const A && );
如果你跟我一样把移动构造函数和复制构造函数混淆了形式,那你必然会写成这样:
A(const A && a):
p(a.p)
{}
A & operator=(const A && a){
p=a.p;
return *this;
}
你完了,你再次引发double free。必须要着重说一下,移动构造函数形式如下:
A (A &&);
A & operator=(A &&);
注意,并没有const,并没有const,并没有const!!!而且必须要把右值的内容更改掉!!!
A (A && a):
p(a.p)
{
a.p=nullptr;
}
A & operator=(A && a){
p=a.p;
a.p=nullptr;
return *this;
}
&&的特性,右值引用就是对一个右值进行引用的类型。因为右值不具名,所以我们只能通过引用的方式找到它。
无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去,通过右值引用来延长临时右值的生命周期。
第三坑,竟不调用成员的移动构造。
class A{
int * p;
public:
A(int value):
p(new int(value))
{}
A(A & a):
p(new int(*a.p))
{
std::cout<<"copy c"<<std::endl;
//如果调用A的复制构造函数,则打印 "copy c"
}
A(A && a):
p(a.p)
{
std::cout<<"move c"<<std::endl;
//如果调用A的移动构造函数,则打印 "move c"
a.p=nullptr;
}
A & operator=(A & a){
std::cout<<"copy"<<std::endl;
//如果调用A的复制赋值运算符,则打印 "copy "
p=new int(*a.p);
return *this;
}
A & operator=(A && a){
std::cout<<"move"<<std::endl;
//如果调用A的移动赋值运算符,则打印 "move
p=a.p;
a.p=nullptr;
return *this;
}
~A(){
delete p;
}
};
class B{
A a;
public:
B(int value):
a(value)
{}
B(B && b):
a(b.a)
//这里是一个标志1,标志着原来的我对c++移动构造函数的一个憧憬:
//我原本以为A有移动构造函数了,那么B的移动构造函数应该会自动
//调用A的移动构造函数,果然我还是太年轻了。
{}
B & operator=(B && b){
a=b.a;
//这又是一个标志2,标志着原来的我对c++移动赋值运算符的一个憧憬
return *this;
}
};
int main(){
B b1(1);
B b2(std::move(b1));
b1=std::move(b2);
}
结果如下:
copy c
copy
所以各位孩子们,如果你们还被移动构造函数给迷惑了,不要担心,记得在每个类的移动构造函数和移动赋值运算符加几个std::move()就行了.
//修改标志1的内容:
a(std::move(b.a))
//修改标志2的内容:
a=std::move(b.a);
需要注意的是如果是基本类型,或者不存在内存分配的类,可以不用调用std::move()因为他们都一样,都是复制构造,不存在移动构造。如果有调用内存分配的类,那你需要注意了。这三个坑,切忌。
第四坑,我自己移动我自己
比起前三个坑,第四个坑按理来说应该必须要知道的,因为这我们在复制赋值运算符就知道了,有一条语句叫做
if(& name == this)
return *this;
没错,我们移动赋值运算符也应该加上这一句。他的抉择比复制赋值运算符的抉择更重要,甚至可以说是刚需,但是如果我们没有或者忘了添加会有什么下场,我们继续上代码:
struct A{
//为了方便检查指针 p 的值,我把A声明为struct
int * p;
A(int value):
p(new int(value))
{}
A(A & a):
p(new int(*a.p))
{
std::cout<<"copy c"<<std::endl;
}
A(A && a):
p(a.p)
{
a.p=nullptr;
}
A & operator=(A & a){
p=new int(*a.p);
return *this;
}
A & operator=(A && a){
p=a.p;
a.p=nullptr;
return *this;
}
~A(){
delete p;
}
};
int main(){
A a1(10);
a1=std::move(a1);
std::cout<<(void *)a.p<<std::endl;
//打印指针p
}
结果如下:
0
没错,你会发现a1.p直接变成了nullptr,最关键是因为在移动函数中允许他们自己移动他们自己,间接造成了内存泄漏,比起复制函数,移动函数这部分非常不显眼!!!所以在移动函数中我们应该做到阻止他们复制他们自己。
//正确的移动函数写法
A & operator=(A && a){
if(&a == this)
return *this;
p=a.p;
a.p=nullptr;
return *this;
}
这段代码其实在复制复制运算符中并不是必要的,effective c++ 就讨论过这个问题。(想知道可以自己去看书)。但是对于移动构造函数,我觉得这段代码是刚需的,因为他修改了参数的值,如果参数是本身,那么就间接修改了自己,这是很烦恼的事,对于内存泄漏的排查,这些隐藏在构造函数的代码是最容易忽略和也是最难排查的。
本文深入探讨C++中移动语义的四大常见陷阱,包括隐式声明的移动构造函数和赋值运算符、显式声明移动构造函数的正确方式、不调用成员的移动构造及自我移动的问题。通过实例解析,帮助读者理解并避免这些潜在错误。
1万+

被折叠的 条评论
为什么被折叠?



