0
点赞
收藏
分享

微信扫一扫

C++:10---再议拷贝构造函数

一、概念

  • 使用一个已经存在的对象,去构造(初始化)另一个对象

二、格式 

  • 参数加上const&,因为拷贝构造函数在几种情况下都会被隐式地使用,因此拷贝构造函数不应该是explict的
  • const:防止函数内部修改值
  • &:防止无限循环拷贝

类名(类名 const& 参数名)

{

函数体

}

三、拷贝构造函数的分类

  • 浅拷贝:成员变量无动态内存(指针等)变量时,在拷贝构造函数内对成员变量只做简单的赋值,不做内存申请
  • 深拷贝:成员变量有动态内存(指针等)变量时,在拷贝构造函数内对成员变量先进行内存申请,然后进行内容拷贝
  • 默认拷贝构造:没有写拷贝构造时,系统默认给出(默认的为浅拷贝)

浅拷贝:

//此类情况使用浅拷贝 class Cperson { private: int a; public: Cperson(Cperson const& other);//拷贝构造 } Cperson::Cperson(Cperson const& other) { this->a=other.a; }

深拷贝:

//此类含有指针的情况使用深拷贝 class Cperson { private: int age; char* name; public: Cperson(Cperson const& other); } Cperson::Cperson(Cperson const& other) { this->age=other->age; if (other.name)//深拷贝 { int len = strlen(other.name); this->name = new char[len+1]; strcpy(this->name, other.name); } else this->name = NULL; } int main() { Cperson person1; Cperson person2(person1);//隐式调用拷贝构造 Cperson person2=person1;//显示调用拷贝构造 }

四、默认拷贝构造函数(合成拷贝构造函数)

  • 规则:如果没有主动给出拷贝构造,编译器会自动添加一个拷贝构造(做的是浅拷贝),主动给出后默认的拷贝构造消失
  • 如果类中有动态内存变量出现,必须重写拷贝构造,且使用深拷贝。如果没有动态内存变量出现,可不重写拷贝构造,用默认的即可

五、成员的数据类型决定其拷贝的规则

每个成员的类型绝对了它如何被拷贝:

  • 对类类型的成员,会使用其拷贝构造函数来拷贝
  • 内置类型的成员则直接拷贝
  • 虽然不能直接拷贝一个数组,但是合成拷贝构造函数会逐元素地拷贝一个数组类型的成员。如果数组元素是类类型,则使用元素的拷贝构造函数来进行拷贝

六、直接初始化和拷贝初始化

  • 直接初始化:实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数
  • 拷贝初始化:要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换

string dots(10,'.');             //直接初始化

string s(dots); //直接初始化

string s2=dots; //直接初始化

string null_book="9-999-99999-9";//拷贝初始化

string nines=string(100,'9'); //拷贝初始化

附加:

  • 移动构造函数:拷贝初始化通常使用拷贝构造函数来完成。但是,如果一个类有一个移动构造函数,则拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成。但现在,我们只需了解拷贝初始化何时发生,以及拷贝初始化是依靠拷贝构造函数或移动构造函数来完成的就可以了

七、拷贝构造出现的情景

拷贝初始化不仅在我们使用=定义变量时会发生,在下列情况下也会发生:

●将一个对象作为实参传递给- -个非引用类型的形参

●从一个返回类型为非引用类型的函数返回一个对象

●用花括号列表初始化-一个数组中的元素或--个聚合类中的成员


八、使用=default

  • 与构造函数使用=default一样,拷贝构造函数也可以使用=default
  • =default的拷贝构造就相当于系统默认的拷贝构造
  • 当在类内使用=default时,函数将隐式地声明为内联,如果不希望是内联函数,就将函数在类外定义

class Sales_data{

public:

Sales_data(const Sales_data&)=default;

}

我们只能对具有合成版本的成员函数使用=default(即,默认构造函数或拷

贝控制成员)。

九、绕过拷贝构造函数

  • 在拷贝初始化过程中,编译器可以(但不是必须)跳过拷贝/移动构造函数,直接创建对象
  • 但是,即使编译器库绕过了拷贝/移动构造函数,但在这个程序点上,拷贝/移动构造函数必须是存在且可访问的(例如,不能是private的)

例如:

string null_book="9-999-9999-9"; //拷贝初始化

改写为:

string null_book("9-999-9999-9"); //编译器略过了拷贝构造函数

十、拷贝赋值运算符(=)

  • 可参考之前的构造函数篇或者运算符重载

拷贝构造函数与拷贝赋值运算符的关系

  • 拷贝构造函数是用另一个对象来初始化一块内存区域这块内存就是新对象的内存区
  • 赋值函数是对于一个已经被初始化的对象来进行operator=操作。例如:

class A; A a; A b = a; // 调用拷贝构造函数, 因为b是第一次初始化 A c(a); // 调用拷贝构造函数, 因为c是第一次初始化 b = c; // 调用赋值运算符, 因为b已经初始化过了

十一、需要析构函数的类也需要拷贝和赋值操作

  • 原则:通常,如果一个类需要一个析构函数,我们几乎可以肯定这个类也需要一个拷贝构造函数和一个拷贝赋值运算符

案例:

  • 下面有一个类和一个函数
  • 函数是传值参数,因此参数要拷贝

class HasPtr{ public: HasPtr(const std::string &s=std:: string()):ps(new std::string(s)),i(0){} ~HasPtr(){delete ps;} private: std::string *ps; int i; } HasPtr f(HasPtr hp) { HasPtr ret=hp; //拷贝给定的HasPtr return ret; //ret和hp被销毁 }

当f返回时,hp和ret都被销毁,在两个对象上都会调用HasPtr的析构函数。此析构函数会delete ret和hp中的指针成员。但这两个对象包含相同的指针值。此代码会导致此指针被delete两次,这显然是一一个错误(参见12.1.2 节,第411页)。将要发生什么是未定义的。

此外,f的调用者还会使用传递给f的对象:

HasPtr p ("some values") ;

f(p) ;//当f结束时,p.ps指向的内存被释放

HasPtr q(p);//现在p和q都指向无效内存!

P (以及q)指向的内存不再有效,在hp (或ret!) 销毁时它就被归还给系统了。


十二、需要拷贝操作的类也需要赋值操作,反之亦然

虽然很多类需要定义所有(或是不需要定义任何)拷贝控制成员,但某些类所要完成的工作,只需要拷贝或赋值操作,不需要析构函数。

作为一个例子,考虑一个类为每个对象分配一个独有的、 唯一的序号。这个类需要一个拷贝构造函数为每个新创建的对象生成一个新的、独一无二的序号。除此之外,这个拷贝构造函数从给定对象拷贝所有其他数据成员。这个类还需要自定义拷贝赋值运算符来避免将序号赋予目的对象。但是,这个类不需要自定义析构函数。

这个例子引出了第二个基本原则:如果一个类需要一个拷 贝构造函数,几乎可以肯定它也需要一个拷 贝赋值运算符。反之亦然一如果一个类需要一个拷贝赋值运算符,几乎可以肯定它也需要一个拷贝构造函数。然而,无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着也需要析构函数。


十三、浅拷贝错误演示

  • 原因:下列代码中,other->name指向一块内存,直接把other->name赋值给this->name,则两个变量都指向同一块内存,虽然不会出错,但是对不同的指针操作,会改变另一个指针的内容,非常的危险,建议使用上面的深拷贝

class Cperson

{

private:

int age;

char* name;

public:

Cperson(Cperson const& other);

}

Cperson::Cperson(Cperson const& other)

{

this->age=other.age;

this->name=other.name;//错误做法

}



举报

相关推荐

0 条评论