构造函数,拷贝构造函数,移动构造函数,拷贝赋值运算符,移动赋值运算符应用场景
#include <iostream>
using namespace std;
class ConstructTest{
public:
ConstructTest(){
cout<<"default Constructor\n";
dim_=0;
base_= nullptr;
};
~ConstructTest(){
cout<<"Destructor:base "<<base_<<endl;
if (base_ != nullptr){
delete base_;
}
}
ConstructTest(int dim){
cout<<"Constructor with param"<<endl;
dim_=dim;
base_ = new int [dim];
for (int i = 0; i < dim_; ++i) {
*(base_ + i) = 0;
}
}
ConstructTest (const ConstructTest & a){
cout<<"copy Constructor"<<endl;
dim_= a.dim_;
base_ = new int [dim_];
for (int i = 0; i < dim_; ++i) {
*(base_ + i) = *(a.base_+i);
}
}
ConstructTest& operator =(const ConstructTest & a){
cout<<"copy assign "<<endl;
dim_= a.dim_;
base_ = new int [dim_];
for (int i = 0; i < dim_; ++i) {
*(base_ + i) = *(a.base_+i);
}
return *this;
}
ConstructTest& operator =( ConstructTest && a)noexcept{
cout<<"moving copy assign "<<endl;
//避免自己移动自己
if ( this == &a )
return *this;
delete base_;
dim_ = a.dim_;
base_ = a.base_;
a.base_ = nullptr;
return *this;
}
ConstructTest (ConstructTest && a) noexcept{
cout<<"moving copy Constructor"<<endl;
dim_ = a.dim_;
base_ = a.base_;
a.base_ = nullptr;
}
public:
int dim_;
int * base_;
};
ConstructTest square(ConstructTest para){
ConstructTest ret(para.dim_);
ret.base_ = new int [para.dim_];
for (int i = 0; i < para.dim_; ++i) {
*(ret.base_+i) = *(para.base_+i) * (*(para.base_+i));
}
return ret;
}
int main(){
ConstructTest c1(3);
ConstructTest c2(c1);
ConstructTest c4 ;
c4=c2;
cout<<"------------------------\n";
ConstructTest c5 ;
c5=square(c4);
ConstructTest c6 = std::move(c5);
cout<<"------------------------\n";
ConstructTest c7;
c7=ConstructTest();
ConstructTest c8 = square(c7);
ConstructTest c9 = ConstructTest();
cout<<"<<<<<<finish >>>>>>>>\n";
}
C++ 和JAVA不一样的是,C++ 区分了值类型和引用类型,不像JAVA一样全是引用类型。创建对象 JAVA 用 <类名> 对象名= new ...... 而C++ 用 <类名> 对象名 就行 . 对于普通对象 用值传递的方式传到形参,有一个隐式赋值的过程,此时调用的拷贝构造函数.以下的构造函数使用场景:
构造函数 : 创建对象时,给对象初始化时调用。
拷贝(复制)构造函数: 利用相同的类对象给新对象初始化.时调用.
拷贝赋值运算符 : 两个旧对象之间的赋值。
所谓“移动”就是把a的内存资源挪为自用。
移动构造函数: 在创建对象时,用临时对象初始化时调用。在返回值传给返回值的副本时也会调用。
移动赋值运算符: 用临时对象给旧对象赋值时调用。
我用的是CLION,在CMakeList.txt 加入如下代码,来取消编译器优化
add_compile_options(-fno-elide-constructors)
在无编译器优化的情况下,输出结果:
Constructor with param
copy Constructor
default Constructor
copy assign
------------------------
default Constructor
copy Constructor
Constructor with param
moving copy Constructor
Destructor:base 0
moving copy assign
Destructor:base 0
Destructor:base 0x3e1da8
------------------------
moving copy Constructor
------------------------
default Constructor
default Constructor
moving copy assign
Destructor:base 0
copy Constructor
Constructor with param
moving copy Constructor
Destructor:base 0
moving copy Constructor
Destructor:base 0
Destructor:base 0x3e1da8
default Constructor
moving copy Constructor
Destructor:base 0
<<<<<<finish >>>>>>>>
Destructor:base 0
Destructor:base 0x3e1918
Destructor:base 0
Destructor:base 0x3e1dd8
Destructor:base 0
Destructor:base 0x3e1d90
Destructor:base 0x3e1d78
Destructor:base 0x3e1d60
Process finished with exit code 0
在main函数第一到第四行中,展示了构造函数,拷贝构造,和拷贝赋值。
从第5行起:
cout<<"------------------------\n";
ConstructTest c5 ;
c5=square(c4);
ConstructTest c6 = std::move(c5);
cout<<"------------------------\n";
首先用 默认构造函数创建对象c5,
在c5 = square(c4) 中, 首先把实参c4赋值给square的形参param,此时用的是拷贝构造函数。
在square函数体中,创建了局部对象ret,此时调用构造函数,
然后为返回值ret创建副本,此时调用移动构造函数,接着函数体结束,把形参param析构,
然后把原来ret的副本赋值给c5 ,对过程是对已经存在的对象c5进行赋值,所以调用移动赋值。
接着把ret 和ret的副本给析构。
利用 std::move() 手动把c5的内容移动给某个临时引用,把临时引用初始化给c6,调用移动构造函数.
cout<<"------------------------\n";
ConstructTest c7;
c7=ConstructTest();
ConstructTest c8 = square(c7);
ConstructTest c9 = ConstructTest();
cout<<"<<<<<<finish >>>>>>>>\n";
先默认构造函数创建c7对象。
用默认构造函数创建临时对象,把临时对象"移动"给c7;移动完后把临时对象析构
在c8 = square(c7) 中, 首先把实参c7赋值给square的形参param,此时用的是拷贝构造函数。
在square函数体中,创建了局部对象ret,此时调用构造函数,
然后为返回值ret创建副本,此时调用移动构造函数,接着函数体结束,把形参param析构,
然后把原来ret的副本赋值给c8 ,对还未创建的对象的对象c8进行初始化,所以调用移动构造。
接着把ret 和ret的副本给析构。
创建临时对象,并用给c9进行初始化,最后把临时对象析构掉;
程序结束,析构所有变量。
编程思路
在类成员变量带有指针的情况下,会面临如何施放资源的难题。应为默认拷贝构造,拷贝赋值,移动构造、移动赋值只是单单的浅拷贝。即值复制 指针指向内存的地址。在浅拷贝后,就会有两个对象中的成员指针指向同一处内存空间,如果在析构函数中对成员指针delete,最后就会面临对同一处内存空间施放两次。在C++中,如果访问不属于本程序中的内存就会出现“段错误”,即指针越界.
在编程时,可以利用 boost::shared_ptr 来管理动态分配的内存,此时可以不用理睬资源施放问题。因为智能指针能够对动态内存的引用计数归零时自动清理内存空间。
如果要用 普通指针来管理动态内存,那么就要考虑施放资源,浅拷贝与深拷贝的问题。如果想用深拷贝那么就要,写好构造函数,拷贝构造函数,拷贝赋值运算符,移动构造,移动赋值运算符;
在构造函数中初始化所有成员变量包括指针;在拷贝构造和拷贝赋值中重新为指针分配内存空间,并把对应的内存值进行赋值;在移动构造和移动赋值中,把返回内容复制完后要把临时对象指针内容重新赋空,达到不拷贝内存又不会施放两次内存的目的,即所谓的"移动“.