13.1 拷贝、赋值、销毁
知识点1:在定义一个类时,我们可以显式或隐式的定义在此类型的对象拷贝、赋值、移动、销毁是做什么,主要通过五种特殊的成员函数来完成这些操作:拷贝构造函数、拷贝复制运算符、移动构造函数、移动复制运算符。析构函数
知识点2:拷贝和移动构造函数定义了当用同类型的一个对象初始化本对象时做什么。拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。析构函数定义了当此类型对象销毁时的操作
知识点3:若一个类没有显式的定义这五个操作,编译器会自动为其定义缺失的操作,在定义一个类时,拷贝控制操作是非常的重要的
知识点4:拷贝构造函数:本身是一个构造函数,其参数是一个自身类类型的引用,且任何额外参数皆有默认值
知识点5:每个成员的类型决定了它的拷贝方式,对于类类型,将调用其拷贝构造函数进行拷贝,对于内置类型,则会直接拷贝,对于数组的拷贝是逐个元素的拷贝,若数组的元素是类类型,则使用拷贝构造函数来拷贝
知识点6:直接初始化:一对小括号加参数。拷贝初始化:等号右侧对象拷贝到正在创建的对象中,如果需要还需进行类型转换(拷贝初始化没有=号的情况:将一个对象作为实参传递给一个非引用类型的形参时、从一个返回类型非引用类型的函数返回一个对象、用花括号初始化列表初始化一个数组的元素或一个聚合类的成员)
知识点7:函数的调用中,非引用类型的参数都要进行拷贝初始化。非引用类型的返回值也会被用来初始化调用方的结果
练习13.1
如上
练习13.2
此为一个类的拷贝构造函数,作为函数其非引用类型的参数需要进行拷贝初始化,但拷贝初始化又要调用拷贝构造函数以拷贝实参,但为了拷贝实参又需要调用拷贝构造函数,无限循环。
练习13.3
StrBlob中元素复制,且智能指针计数加一。StrBlobStr中元素复制,弱指针复制不影响计数器
练习13.4
首先foo_bar函数的参数为非引用类型,需拷贝,使用拷贝构造函数、函数的返回类型非引用,也需要进行拷贝,使用拷贝构造函数。
在函数体中arg拷贝到local对象,global拷贝到heap对象,local、*heap拷贝到pa[4]中皆使用拷贝构造函数
local拷贝到*heap为拷贝赋值运算符
练习13.5
HasPtr(const Hasptr& HP):*ps(new string *HP.ps),i(HP.i){}
练习13.6
知识点1:拷贝复制运算符,其实就是一个名为 operator= 的函数(operator后加表示要定义的运算符的符号),重载运算符,有返回类型和参数,返回类型通常是左侧运算符的引用
知识点2:若在类内未显式定义,则编译器会自动生成合成拷贝赋值运算符,它主要是将运算符右侧的所有非static成员赋给左侧元算对象对应成员(或是用来禁止该类型对象的赋值)
练习13.7
所有成员的赋值会发生,两个StrBlob中智能指针所指对象内存相同,计数器加一,两个StrBlobPtr中弱指针所致对象内存相同,计数器不变。
练习13.8
HasPtr& operator= (const Hasptr& HP)
{
string *p = new string(*HP.ps);//new返回的是指向分配好内存、创建了对象的指针
delete ps;//首先删除原内存
ps = p; //赋值
i = HP.i;
return *this;//返回值
}
练习13.9
知识点1:构造函数初始化对象的非static数据成员,析构函数释放对象所使用资源,并销毁对象的非static数据成员
知识点2:析构函数不接受参数,因此它不能被重载,对于一个给定类,只会有唯一一个析构函数
知识点3:构造函数中,成员初始化是在函数体执行之前完成的,且按照他们在类内出现的顺序进行初始化,析构函数中,首先执行函数体,然后销毁成员,成员按照初始化顺序的逆序销毁,所以析构函数可以执行设计者想要的任何收尾工作,再销毁成员
知识点4:成员的销毁完全依赖于其本身的类型,类类型需要执行自身的析构函数,而内置类型则什么也不做(无析构函数)
知识点5:调用析构函数的情况:
1:变量离开作用域时被销毁
2:当对象被销毁,其成员被销毁
3:容器被销毁,成员被销毁
4:动态分配的对象,指针被delete时
5:临时对象,创建的完整表达式结束时
知识点6:合成析构函数—编译器自动生成的析构函数,基本上为空,或者被用来阻止对象被销毁
知识点7:析构函数体自身并不直接销毁成员,是在析构函数体执行完毕之后隐式的析构阶段中被销毁的
13.10
所有对象的数据成员被销毁,智能指针的计数减一,所有对象的数据成员被销毁,弱指针不影响计数器
13.11
~HasPtr() { delete ps; }//必须首先delete对象ps,ps指向一个动态分配的string
13.12
知识点:当指向一个对象的引用或者指针离开作用域时,析构函数并不会执行
析构三次,accum、item1和item2
13.13
#include<iostream>
#include<string>
#include<fstream>
#include<list>
#include<vector>
#include<map>
#include<set>
#include<cctype>//ctype无法打开,包含tolower()函数和ispunct函数
#include<algorithm>
#include<utility>//保存pair的头文件
#include<memory>
using namespace std;
class A
{
public:
A(int m):val(m)//默认构造函数
{
cout<<"默认构造函数"<<endl;
}
A& operator= (const A& a) //拷贝赋值运算符
{
val = a.val;
cout<<"拷贝复制运算符"<<endl;
return *this;
}
~A()//析构函数
{
cout<<"析构函数"<<endl;
}
int val;
};
void show1(A& a)
{
cout<<a.val<<endl;
}
void show2(A a)
{
cout<<a.val<<endl;
}
int main(int argc, char**argv)
{
//将A的对象当作引用或者非引用传递
A a(10);
A b(5);
A c(2);
c = a;
show1(a);
show2(b);
show2(c);
//存放于容器中
vector<A> m;
m.push_back(a);
//动态分配
A *d = new A(5);
show2(*d);
delete d;
return 0;
}
13.1.4
知识点1:如果一个类需要一个拷贝构造函数,那么它肯定需要一个拷贝赋值运算符,反之亦然。然而无论是两者的哪一个,都不必然需要析构函数。
练习13.14
会输出三个一样的数据
练习13.15
使用自定义的拷贝构造函数,生成的是三个不一样的数据
练习13.16
函数的参数变为引用版本的形参,无需拷贝,结果改变,但输出仍为三个不一样的数字
练习13.17
#include<iostream>
#include<string>
#include<fstream>
#include<list>
#include<vector>
#include<map>
#include<set>
#include<cctype>//ctype无法打开,包含tolower()函数和ispunct函数
#include<algorithm>
#include<utility>//保存pair的头文件
#include<memory>
using namespace std;
class A
{
public:
A()//默认构造函数,14题
{
static int val1 = 10;
val = val1++;
}
A(A &a)//自定义版本的拷贝构造函数
{
val = a.val+5;
}
int val;
};
void show2(A a)
{
cout<<a.val<<endl;
}
void show3(const A& a)
{
cout<<a.val<<endl;
}
int main(int argc, char**argv)
{
A a, b = a, c = b;
show2(a);//调用函数时需要拷贝一次
show2(b);
show2(c);
// show3(a);//这里的调用不需要拷贝构造运算符
// show3(b);
// show3(c);
return 0;
}
14题因为没有自定义拷贝构造函数,由编译器自动生成了一个合成拷贝构造函数,而合成拷贝构造函数只是逐个拷贝各个类成员,所以a、b、c中的val值都是10。15题自定义了拷贝构造函数,
A(A &a){val = a.val+5;}
所以a、b、c的val值分别是10、15、20。但实际输出却是15、20、25。 这是因为由于f的参数是非引用类型的,这里会调用一次拷贝构造函数。
所以每次调用f的时候,f的形参A a的val值实际上在实参的val值上又加了5。这样是16题的意图所在。
我觉得16题的意思是在15题基础上再把f的参数改成引用的,而不是单独的只改f不给类自定义拷贝构造函数。
这样因为f是引用传递参数的,所以不会调用拷贝构造函数,所以这一次输出的值就是正确的a、b、c中val的值了。