C++学习笔记(拷贝、赋值、销毁)
文章目录
在对类进行定义时,除了对类对象可执行操作等定义
还会显示或隐式地指定在此类型的对象拷贝、移动、赋值和销毁的具体操作
这些操作具体通过拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数
拷贝:
拷贝构造函数:
C++中,如果我们没有设置自定义的拷贝构造函数,编译器会自动定义一个合成拷贝构造函数(即将对象中非static成员拷贝到正在创建的对象中)
#include<iostream>
#include<vector>
using namespace std;
class initclass
{
private:
int pri;
public:
int pub;
int getpri()
{
return pri;
}
initclass()
{
pri =1;
pub = 1;
};
//initclass(const initclass& fa)
//{
// this->pri = fa.pri;
// this->pub =fa.pub+1;
//};//与合成拷贝构造函数等价
initclass(const initclass& fa)
{
this->pri = fa.pri;//当有对象成员没有拷贝时,未拷贝的成员就是未定义值
this->pub =fa.pub*2;
}//自定义功能拷贝构造函数
};
int main()
{
initclass fa;
initclass ch=fa;//拷贝初始化
initclass ch1(ch);//直接初始化
cout << fa.pub<<" "<<fa.getpri()<<endl;
cout << ch.pub << " " << ch.getpri() << endl;
cout << ch1.pub << " " << ch1.getpri() << endl;
return 0;
}
pri未定义:
1 1
2 -858993460
4 -858993460
所有对象都有拷贝,但pub不是直接赋值,需要*2:
1 1
2 1
4 1
不定义拷贝构造(使用合成构造函数):
1 1
1 1
1 1
直接初始化与拷贝初始化:
直接初始化:直接调用与实参匹配的构造函数;
拷贝初始化:首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将临时对象复制到创建的对象中。
一般来说,初始化尽量使用直接初始化,拷贝初始化效率会低一些。
下面看看这两种初始化方法具体流程效果:
设置类a,采用两种初始化,查看构造和拷贝构造函数调用情况
class a
{
public:
a()
{
val = pp;
++pp;
cout << "调用构造函数1" << endl;
};
a(int x)
{
++pp;
val = pp +10;
cout << "调用构造函数2" << endl;
}
a(const a& fb)
{
val = pp;
++pp;
cout << " 调用拷贝构造函数" << endl;
}
int val;
static int pp;
};
int a::pp = 0;
a aa;//直接初始化
bb = aa;//拷贝初始化
a c(bb);//直接初始化
cout << aa.val << " " << bb.val << " " << c.val << endl;
a d = a(10);//拷贝初始化
cout << d.pp << " " << d.val << endl;
结果显示:
直接初始化:aa:直接调用构造函数1;但是cc:显示调用了拷贝构造函数;
所以直接初始化不是只能调用构造函数的,拷贝构造函数也符合其与实参匹配的构造函数条件。
拷贝构造化:bb:只调用了拷贝构造函数
dd:只调用了拷贝构造函数。????不对呀,怎么只调用了构造函数呢,根据定义右侧对象不是要临时构造吗?一看,哦,大意了,编译器已经对这块进行了优化,可以直接跳过拷贝构造函数,直接使用构造函数来构造对象。
1 1
2 1
4 1
调用构造函数1
调用拷贝构造函数
调用拷贝构造函数
///
0 1 2
调用构造函数2
4 14
那两者区别在哪里呢?
1.有的文献和博客里面会说:使用“=”初始化的,一定是拷贝初始化,这也不一定,如果存在“=”(拷贝赋值运算符重载),就不会调用这个拷贝构造函数
2.直接初始化可以调用拷贝构造函数、或者构造函数;
拷贝初始化一定要调用拷贝构造函数。
这里会有疑问,上例中dd不是因为编译器优化,不调用拷贝构造函数吗?
咳咳,这块我也是挺疑惑,看了几个牛人博客,自己也将拷贝构造函数放入private中,再次运行时,就出现以下错误。
修改:
private:
a(const a& fb)
{
val = pp;
++pp;
cout << " 调用拷贝构造函数" << endl;
}
结果:
/
严重性 代码 说明 项目 文件 行 禁止显示状态
错误 C2248 “a::a”: 无法访问 private 成员(在“a”类中声明) ProjectC++_Base d:\code_study\projectc++_base\projectc++_base\c++拷贝控制.cpp 84
原因结论
主要是因为复制构造函数是可以由编译默认合成的,而且是公有的(public),编译器就是根据这个特性来对代码进行优化的。
如果你自己定义这个复制构造函数,编译则不会自动生成,虽然编译不会自动生成,但是如果你自己定义的复制构造函数仍是公有的话,编译还是会为你做同样的优化。然而当它是私有成员时,编译器就会有很不同的举动,因为你明确地告诉了编译器,你明确地拒绝了对象之间的复制操作,所以它也就不会帮你做之前所做的优化(from http://t.csdn.cn/i0k8J)
所以说拷贝初始化,编译器还是会使用到拷贝构造函数,再这个上面再做优化,如果拷贝构造不可用,拷贝初始化就不可用了。
拷贝构造函数使用场景:
拷贝构造函数除了“=定义变量”,构造函数调用情况还有:
1.使用同一类型显式或隐式初始化一个对象;
2.对象作为实参传递给一个非引用对象
比如:使用函数FUN(a fa);
a aa;
FUN(aa);//此处会使用拷贝构造函数
3.标准库容器初始化,使用push、insert等操作时,也会调用拷贝构造函数
4.返回类型,返回一个非引用类对象;
5.花括号列表初始化一个数组元素或一个聚合类中的成员
vector<string> ss={"123454","234565"};
explict关键字:
作用:防止单参数构造函数的隐式自动转换;(不允许所修饰的构造方法,来隐式初始化对象)
class a
{
public:
explicit a()
{
val = pp;
++pp;
cout << "调用构造函数1" << endl;
};
explicit a(int x)
{
++pp;
val = pp +10;
cout << "调用构造函数2" << endl;
}
int val;
static int pp;
explicit a(const a& fb)
{
val = pp;
++pp;
cout << " 调用拷贝构造函数" << endl;
}
};
a aa;//不报错
bb = aa;//报错
a c(aa);//不报错
a d = a(10);//报错
原因:aa:显示调用构造函数;bb隐式调用拷贝构造函数,编译报错
cc:显示调用拷贝构造函数;dd 隐式调用拷贝构造函数,编译报错
明显,explicit关键字修饰单参数构造函数,主要作用,禁止隐式转换、拷贝初始化(注意不是禁止拷贝构造函数!!,例子中c调用拷贝构造函数没有报错)
显式初始化:程序给定了初始化参数类型(比如例子中 c)
隐式初始化:没有给定初始化参数类型,编译器还需要隐式转换类型。
拷贝赋值运算符重载:
直接看代码
class qt
{
public:
qt()
{
val1 = 5;
}
qt(qt&fq)
{
this->val1 = fq.val1;
cout << "拷贝构造函数" << endl;
}
qt& operator=(const qt& fq)
{
this->val1 = fq.val1;
cout << "拷贝构造运算符" << endl;
return *this;
}
int val1;
};
//结果:拷贝构造函数
qt q1;
qt q2=q1;
//结果:拷贝构造运算符
qt q1;
qt q2;
q2 = q1;
结果分析:当使用“=”时,若左右值已经初始化了,将调用拷贝运算符,而不是拷贝构造函数
(这个好理解,都没有初始化,怎么去调用运算符重载函数)
析构函数:
析构函数时不能重载的,对于一个类来说是唯一的,主要作用:释放对象使用的资源。
编译器默认的析构函数是不能删除new运算符在堆中分配的对象或者对象成员,这需要程序员自定义析构函数来实现
比如:
class sa
{
public:
sa()
{
n1 = new int(5);
n2 = 5;
}
~sa() {
delete n1;//必须手动去删除
};
static int b;
int* n1;
int n2;
};
int sa::b = 5;
当析构函数如果使用自定义的析构函数,或者说我们需要在析构时析构动态分配的对象时,如果使用默认的合成拷贝赋值函数与运算符,将会导致系统中断!
sa copysa(sa f)
{
sa cf = f;
return cf;
}
sa a1;
copysa(a1);
当运行该语句和函数时,会报错;
原因分析:使用copysa函数时,cf与f在函数作用域结束后会调用自己的析构函数;
而默认的拷贝函数是采用浅拷贝(不会给指针类型的数据分配新内存),这就导致 cf与f 的n1指针都指向了同一个对象,而在析构函数中,cf、f先后调用两次delete,这就会使得第二次的delete,已销毁的对象,导致报错。
措施:采用自定义的拷贝构造函数、运算符,对于指针类型数据进行深拷贝
sa(sa&fa)
{
int *cpn1 = new int(*fa.n1);
this->n1 = cpn1;
this->n2 = fa.n2;
}
加上自定义拷贝构造函数,这样就不会报错了,@-@;这也是自定义拷贝构造函数主要的使用原因。
阻止拷贝:
即使我们没有定义拷贝构造,编译器也会自动生成。其使用方法
CLASSA (const CLASSA&)=delete;//禁止拷贝
CLASSA &operator= (const CLASSA&)=delete;//禁止赋值
CLASSA()=default;//使用默认构造函数
~CLASSA()=default;//使用默认析构函数,注意析构函数不能删除
一般这个东西,平时使用情况很少的。可以了解的是,iostream类就使用了阻止拷贝,主要就是避免多个对象使用同一个IO缓冲
对象移动:
对于不能共享的资源、或者对象拷贝后很快就要销毁的情况,使用移动而非拷贝,可以提高程序的性能
左值引用与右值引用
C++为了支持移动操作,加入了一种新的引用类型:右值引用;
先看看左值与右值是啥:
特点总结:左值持久、右值短暂
左值:左值在内存一定有实体(有定义,有具体地址、名字)
右值:右值存储位置可以是寄存器也可以是在内存,但即将销毁,或者是临时值(不能取地址、没有具体名字)
左值引用: &
右值引用: &&
//左值引用
int i = 4;
int &j = i;
const int &p = i * 5;
const int &p1 = 5;
//右值引用
int &&na = i*5;
int &&nb = 66;
sa &nbb = sa();//报错,此处是右值,不能使用左值引用
sa &&nc=sa();//正确,使用右值引用
sa &&nd = nc;//报错,nc是左值,不能使用右值引用
sa &&nd =move(nc);//move函数转换nc为右值;
标准库move函数:作用:显式将左值转换为一个右值;
nc.n1=5;//报错,在使用move后,n1就是右值,不能在进行修改,但可以使用
cout<<nc.n1;//不报错
移动构造函数与移动构造运算符
移动构造函数:与拷贝构造函数不同,其不分配任何内存,它直接接管给定右值对象的内存;将内存接管后,会将所给定的对象的指针置为nullptr,右值对象在销毁时不会释放所“窃取”的内存。
移动构造函数的第一个参数必须是右值引用,其余参数都必须有默认实参。
合成移动构造函数:只有在没有定义拷贝函数,所有成员都支持移动拷贝时,编译器才会自动生成移动拷贝函数。
class moveclass
{
public:
moveclass()
{
n1 =new int(1);
n2 = new int(2);
cout << "构造"<<endl;
}
moveclass(moveclass &&)noexcept; //引用参数必须是右值,noexcept 表示不抛出任何异常
moveclass(moveclass &fm)
{
int*cn1 = new int(*fm.n1);
const int *cn2 = new int(*fm.n2);
this->n1 = cn1;
this->n2 = cn2;
cout << "拷贝" << endl;
}
moveclass& operator= (moveclass &&fm) noexcept
{
if (this == &fm)
return *this;
n1 = fm.n1;
n2 = fm.n2;
fm.n1 = nullptr;
fm.n2 = nullptr;
cout << " 移动构造运算符" << endl;
return *this;
}
~moveclass()
{
delete n1;
delete n2;
cout << "析构函数" << endl;
}
int* n1;
const int* n2;
static int n3;//静态变量不参与拷贝、构造
};
int moveclass::n3 = 3;
moveclass::moveclass(moveclass &&fm) noexcept :n1(fm.n1), n2(fm.n2)
{
//delete fm.n1;
//delete fm.n2;
fm.n1 = nullptr;
fm.n2 = nullptr;
cout << " 移动构造函数" << endl;
}
moveclass getmc()
{
moveclass a;
return a;
}
moveclass getmc2()
{
return moveclass();
}
///
cout << "移动拷贝例子"<<endl;
moveclass ma;//直接初始化
ma=moveclass();
cout << "b" << endl;
moveclass mb= moveclass();//如果没有定义移动构造函数,会报错
cout << "c" << endl;
moveclass mc= getmc();
//moveclass mc= getmc2();
结果:
移动拷贝例子
构造
构造
移动构造运算符
析构函数
b
构造
c
构造
移动构造函数
析构函数
析构函数
析构函数
析构函数
///使用getmc2
c
构造
析构函数
析构函数
析构函数
结果分析:ma直接初始化,ma=右值,右值调用一次构造,=调用ma的移动构造运算符重载,右值作为临时值进行析构;
mb:可能是编译器优化,只调用一次构造,但是不能缺少移动构造函数定义;
mc:调用函数getmc中使用,构造一个临时 对象a,返回a时,调用移动构造函数对mc初始化,并析构返回值a;
调用getmc2时,值调用了一次构造,emmmm,又是编译器优化的结果
最后对ma、mb、mc析构
此外**:当一个类存在拷贝构造函数,未定义移动构造函数时,编译器不会合成移动构造函数,此时就会拷贝右值。**
三五法则:
三:定义一个类时,我们显式地或隐式地指定了此类型的对象在拷贝、赋值和销毁时做什么。一个类通过定义三种特殊的成员函数来控制这些操作:拷贝构造函数、拷贝赋值运算符、析构函数。
五:在C++11标准中,为了支持移动语义,加入移动构造函数与移动赋值运算符,又被称为C++五法则,相比于三法则,移动在一些方面比拷贝优化;
主要特点:
析构函数不能删除;
需要析构函数的类也就需要拷贝、赋值操作
需要拷贝操作的类也需要赋值操作,反之亦然。
如果有一个类有删除的或不可访问的析构函数,其默认和拷贝构造函数就会被定义为删除的
将五个拷贝控制成员看成一个整体,一般来说只有一个类定义了其中任何一个的拷贝操作,就应该把所有的5个操作都定义。