C/C++:copy control (拷贝控制)

2023-07-29,,

前言:当定义一个类的时候,我们显示或者隐式地指定在此类型的对象拷贝,移动,赋值,销毁时做些什么,一个类通过定义五种特殊的成员函数来控制这些操作,包括拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符和析构函数, 拷贝和移动构造函数定义了同类型的另一个对象初始化本对象时做什么,拷贝和移动赋值运算符定义了将一个对象赋予另一个对象时做什么,析构函数则定义当此类型销毁时做什么,称这些操作为拷贝控制操作;

合成拷贝构造函数:如果我们没有定义拷贝构造函数,与合成默认构造函数不同(只要有其他构造函数定义,编译器不会帮你生成合成默认构造函数),即使我们定义了其他构造函数,编译器也会为我们生成合成拷贝构造函数,合成拷贝构造函数会将其参数逐个拷贝到正在创建的对象中,编译器从给定的对象一次将每个非static成员拷贝到正在创建的对象中,

拷贝规则:对类类型的成员调其拷贝构造函数,内置类型成员直接拷贝.

拷贝初始化和直接初始化区别:

直接初始化:实际上是要求编译器使用普通的函数匹配来选择与我们的参数最匹配的构造函数;

拷贝初始化:实际上要求编译器将右侧的对象拷贝到正在创建的对象中,如果需要还要进行类型转换(调"构造函数"匹配建立左侧对象->调 "拷贝构造函数/移动拷贝构造函数" 将右侧对象拷贝);

总结:直接初始化:一对小括号加参数。拷贝初始化:等号右侧对象拷贝到正在创建的对象中,如果需要还需进行类型转换;

注意:调用构造函数!=直接初始化,调用拷贝构造构造!=拷贝初始化

直接初始化:一般在 "()" 调用时发生

拷贝初始化不仅在我们使用 "=" 时发生,下列三种情况也会发生

1.将一个对象作为实参传递给非引用类型的形参时.

2.从一个返回类型为非引用类型的函数返回参数.

3.使用花括弧列表初始化一个数组中的元素或聚合类的成员

拷贝构造函数参数是 "const 类型&" 如果不是,实参传递过程中则需要拷贝,则需要循环拷贝,所以必须 "const 类型&" ;

拷贝初始化限制:其实只要都加上explicit来强制显示调用,就可以不用管拷贝初始化或者直接初始化了;

编译器可以绕过拷贝构造函数:在拷贝初始化过程中,编译器可以略过(但不是必须)拷贝/移动构造函数,直接创建对象

C/C++:编译器将把 std::string str="123sadw2-asd"; 改成这样 std::string str("123sadw2-asd"); 虽然这些拷贝构造略过了,但拷贝/移动构造必须是可以被访问的;

C/C++(constructor/copy constructor 表示打印调用):

 #include <iostream>
#include <string> class CopyClass
{
public:
std::string str_;
public:
CopyClass(const std::string &str = std::string())
: str_(str)
{
std::cout << str_ << " constuctor CopyClass" << std::endl;
} CopyClass(const CopyClass &rhs)
: str_(rhs.str_)
{
std::cout << str_ << " copy constructor CopyClass" << std::endl;
} }; int main(int argc, char *argv[])
{
CopyClass A("A"); //explicit constructor
CopyClass U{"D"}; //explicit constructor CopyClass E = CopyClass("D"); //explictt constructor
CopyClass B(A); //explicit copy constructor
CopyClass C = B; //imolicit copy constructor
CopyClass F = {"X"}; //implicit constructor
CopyClass D = {C}; //implicit copy constructor return ;
}

拷贝赋值运算符,其实就是一个名为 operator= 的函数(operator后加表示要定义的运算符的符号),重载运算符,有返回类型和参数,返回类型通常是左侧运算符的引用(为了和内置类型赋值返回本身保持一致),未定义拷贝赋值运算符的话编译器会帮你生成一个合成拷贝赋值运算符,内部实现也是把每个非static变量赋值给左侧对象

析构函数:由"~类型名()"组成,与构造函数执行相反顺序,由一个函数体和析构部分执行,函数体可以执行一些释放动态内存的操作,析构部分属于函数体制外执行的,逆序释放成员变量,内置类型没有析构函数,复合类型调用它们自己的析构函数,当然如果没有定义析构函数,编译器也会为你提供合成析构函数,但是不会为你释放动态内存=申请的内存;

注意点1:析构函数体自身并不直接销毁成员,是在析构函数体执行完毕之后隐式的析构阶段中被销毁的

注意点2:隐式销毁一个内置指针类型的成员不会delete它所指的对象

注意点3:当 指向一个对象的引用或指针离开作用域,析构函数不会执行

调用析构函数的情况:

1:变量离开作用域时被销毁

2:当对象被销毁,其成员被销毁

3:容器被销毁,成员被销毁

4:动态分配的对象,指针被delete时

5:临时对象,创建的完整表达式结束时

需要析构函数的类也需要拷贝和赋值操作,合成的析构函数不会delete一个指针数据成员,所以有时我们需要自己定义一个析构函数释放构造函数分配的内存,所以需要析构函数的类,也就需要拷贝构造函数和拷贝赋值运算符,而合成的拷贝构造函数和拷贝赋值运算符只能简单的拷贝指针成员,这就意味着多个对象指向同一个内存,释放多个对象时,造成多次delete

如果一个类需要自定义版本的析构函数,那么肯定是需要自定义的拷贝构造函数和拷贝赋值运算符

而且需要拷贝操作也需要复制操作,反之亦然;

我们可以通过将拷贝控制成员定义为 =default 来显式的要求编译器生成合成的版本(只能对有合成版本的函数使用),在此之后,合成的函数将隐式的声明为内联

iostream类阻止了拷贝,避免多个对象同时写入,或读取相同的IO缓冲,我们可以将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝,虽然声明了他们,但不能以任何的方式使用他们,在参数列表之后加上 =delete 来指出我们希望其是被删除的,这是为了通知编译器,我们不希望这些函数被定义

可以对任何类内函数(析构函数除外)声明 =delete ,且必须出现在函数第一次声明的时候,如果析构函数被声明=delete ,析构函数被删除,就无法销毁此类型的对象

C++11之前,是将拷贝构造函数和拷贝赋值运算符定义为private来阻止拷贝的(旧标准)判断一个类是否需要拷贝控制函数成员,首先判断其是否需要自定义版本的析构函数,如果需要,则拷贝控制成员函数都需要

以上内容主要来自C++primer

定义行为像值的类:为了提供类值行为,对于类管理的资源,每个对象都应该拥有一份自己的拷贝

类值拷贝赋值运算符:赋值运算符通常组合了析构函数和构造函数,类似析构函数,赋值操作会销毁左侧对象,类似拷贝构造函数,赋值操作会从右侧对象拷贝数据,但是非常重要的一点这些操作顺序是以正确的顺序执行的,即使将一个对象赋予它本身也是正确的(保证C++异常安全),当异常发生时能将左侧运算对象置于一个有意义的状态

步骤:先拷贝右侧对象(此时异常也能保证源对象安全),然后释放左侧对象,把资源指针变量重新指向或赋值;

当你编写赋值运算符的时,记住两点:

1.如果将一个对象它本身赋予自身,赋值运算符也要保证必须能正确工作

2.大多数赋值运算符组合了析构函数和拷贝函数的工作

C/C++:

 //类值行为
class HasPtr
{
private:
int value;
std::string *str;
public:
HasPtr(const std::string &str_ = std::string(), const int &value_ = int())
: str(new std::string(str_)),
value(value_)
{ //构造对象,先构造完对象在执行构造函数体,所以函数体内一般属于赋值而不是初始化,并根据
//声明顺序初始化而不是形参或列表初始化顺序; } ~HasPtr()
{
//动态内存对象需自己手动delete释放(智能指针除外)
delete str;
//析构对象,先执行析构函数体后执行析构成员部分(隐式的,分离的);
//析构成员按照声明顺序的逆序析构,内置类型无析构函数,类对象执行析构函数
} //拷贝构造(以值方式传递,副本与原版互不影响)
HasPtr(const HasPtr &other)
: str(new std::string(*(other.str))),
value(other.value)
{ } HasPtr &operator=(const HasPtr &other)
{
std::string *ptr = new std::string(*(other.str));
delete str;
str = ptr;
value = other.value; return *this;
}
};

定义行为像指针的类:我们需要更改拷贝构造函数/拷贝复制运算符来改变指针的指向而是改变内存,而析构函数则根据内存指针是否是左后一个拥有者来释放动态内存,类似shared_ptr<T>,利用引用计数来判别;

C/C++

//类指针行为(共享指针)
class HasPtr_
{ friend void swap(HasPtr_ &lhs, HasPtr_ &rhs); friend bool operator<(const HasPtr_ &lhs, const HasPtr_ &rhs); private:
int value;
std::string *str;
std::size_t *use;
public:
HasPtr_(const std::string &str_ = std::string())
: value(),
str(new std::string(str_)),
use(new std::size_t())
{ } HasPtr_(const HasPtr_ &rhs)
: value(rhs.value),
str(rhs.str),
use(rhs.use)
{
++*use;
} //对象退出作用域时会判断引用计数
~HasPtr_()
{
if (*use == )
{
delete str;
delete use;
} //然后退出函数体执行其他成员变量析构
} HasPtr_ &operator=(const HasPtr_ &rhs)
{
++*(rhs.use); //先递增右对象引用计数;
if (--*use == ) //判断递减左对象是否是唯一拥有则释放指针
{
delete str;
delete use;
} //然后赋值操作
value = rhs.value;
str = rhs.str;
use = rhs.use; return *this; } void display()
{
std::cout << *str << std::endl;
} };

交换操作:当我们调用stl一些算法时类似sort这样的,如果编译器没有找到类自己提供的交换函数swap,则会调用标准库的std::swap,而标准库的swap操作过程会出现一次拷贝,两次赋值操作(类似值

交换),但这样的操作对于有内存分配的类来说资源浪费,因为交换两对象的资源只要互换指针就行,而不用重复申请内存这样的.

C/C++:

 void swap(HasPtr_ &lhs, HasPtr_ &rhs)
{
std::cout << "HasPtr swap()" << std::endl;
//声明为friend,需要使用私有变量
//声明使用外部swap,防止递归
using std::swap;
swap(lhs.str, rhs.str);
swap(lhs.use, rhs.use);
swap(lhs.value, rhs.value); } bool operator<(const HasPtr_ &lhs, const HasPtr_ &rhs)
{
return lhs.str->length() < rhs.str->length();
}

总结:看完一章总结有点乱,暂且作为记录.....

C/C++:copy control (拷贝控制)的相关教程结束。

《C/C++:copy control (拷贝控制).doc》

下载本文的Word格式文档,以方便收藏与打印。