C++ 类的复制控制

写了又删,删了又写,才发现这一章节不好描述。

那就假定个前提吧,假定已经知道:

① C++的类有构造函数。

② 如果不提供任何构造函数,那编译器会生成默认的无参构造函数--默认构造函数只会进行成员变量的初始化。

③ 如果提供了任何一个构造函数,那编译器就不会再生成默认的无参构造函数。

④ 函数的形参都是实参的副本(引用类型除外)。

⑤ 构造函数也是函数。

⑥ 直接初始化是在括号()中,复制初始化则使用=赋值操作符。

在此基础上,稍作推理:

一、如果构造函数是单形参、且形参类型为该类的类型的构造函数。

  以 class Person 为例,

string name="anonymous";
int age=18;
Person p1(name, age);
Person p2(p1); //note this

  根据上面前提的④,p2 需要 p1 的一个副本(复制一个),这时就出现问题了:该怎么复制?

  这就是复制构造函数的作用了--用于指明如何复制。

  再推理一下,如果不能使用副本,那还能使用什么?必须是引用啊(不要说指针,囧)!所以复制构造函数就是单形参、且形参类型为该类类型的引用 (常const修饰)的构造函数。

class Person{
    public:
        Person(const Person&); //copy-constructor
        ....
};

  需要说明的是,编译器会自动生成一个默认的复制构造函数,原则就是一一复制成员变量。这里面又存在一个问题,如果存在指针类型的成员变量,那么与其他成员变量不同的是,复制后的指针仍然指向同一个地址!这可能会导致很严重的bug。如下:

#include <iostream>
#include <string>

using namespace std;

//注意,有指针成员,但没有自定义复制构造函数。
//这时,其他成员复制时都是副本,而指针成员却指向同一个地址。
class HasPtr{
private:
    int *ptr;
    int val;

public:
    HasPtr(int *p, int i):ptr(p), val(i){}

    int *get_ptr() const { return ptr; }
    void set_ptr(int *p){ ptr=p; }

    int get_int() const { return val; }
    void set_int(int i){ val=i; }

    int get_ptr_val() const { return *ptr; }
    void set_ptr_val(int i) const { *ptr = i; } //why not take a ref?
};

int main(){
    int obj=10;

    HasPtr ptr1(&obj, 42);
    HasPtr ptr2(ptr1); // copy-constructor

    cout<<ptr1.get_ptr()<<"--"<<ptr1.get_ptr_val()<<endl;
    cout<<ptr2.get_ptr()<<"--"<<ptr2.get_ptr_val()<<endl;

    ptr1.set_int(3);
    ptr2.set_int(5);

    cout<<ptr1.get_int()<<"--"<<ptr2.get_int()<<endl;

    ptr1.set_ptr_val(7);
    cout<<ptr1.get_ptr()<<"--"<<ptr1.get_ptr_val()<<endl;
    cout<<ptr2.get_ptr()<<"--"<<ptr2.get_ptr_val()<<endl;

    cout<<"-----------"<<endl;

    int *ip=new int(42);
    HasPtr ptr(ip, 2);
    delete ip;
    ptr.set_ptr_val(3);//OOPS! error

    return 0;
}

  上面代码充分说明了有指针类型的成员变量时,类的对象的行为变得诡异。

  此外,还有一个小知识点,虽然数组不能支持复制,但是复制构造函数中却可以一一复制数组的元素,从而做到复制数组。

  

  推理一下,如果将复制构造函数设为private或者explicit会怎样?

二、赋值操作

  我们已经知道对于基本类型来说,赋值操作就是将一个副本传递给变量。副本怎么来的?当然是复制出来的,所以这种形式又叫复制初始化。(还是赋值好听)

  那么对于类类型来说,赋值又会怎样表现?

  这里还需要另外一个前提:“赋值”和“=”不是一回事!

  对于C++来说,所有的操作符都是定义出来的,就是说,具体的行为是被定义好的!

  同理,对类来说,这些操作符(+-*/等)也是被定义出来的。--定义操作符的函数叫做操作符函数,这个过程叫做重载操作符。

  例如:

int val = 3;
int val2 = val; // val2接收的是val的副本

  事实上,你完全可以定义自己的操作符函数,例如混乱一点的,将+作为赋值操作~ 只是没有必要罢了。

  这里不再介绍重载操作符了,有兴趣的新人可以查一下相关的资料。

  

  下面重点说一下赋值操作符。

  编译器也会生成默认的赋值操作符,默认执行一一赋值,并返回对左操作数的引用。

  一般来说,可以使用默认复制构造函数的类,也可以使用默认赋值操作符。

  对于类类型对象来说,直接初始化直接调用相应的构造函数;而复制初始化则先调用相应构造函数构造一个临时对象,再调用复制构造函数将临时对象复制到正在创建的对象。

  如下:

string s1="abc"; //先是string tmp(const char*); 再s1(&tmp);
string s2=string(); //string()已经是string对象,所以直接调用s2(&string())

  注意上面的代码,tmp 是临时对象,s1(&tmp) 则是调用复制构造函数!

  

三、所有对象都是有生命周期的,一般生命周期仅限于其作用域范围内---手动释放的除外。

  C++能够指定当一个对象走向死亡的时候所进行的操作,这就是析构函数。--这个名字如果看英文就知道,和构造函数是相反的,constructor - destructor,一个创建,一个拆毁。

  所以,顾名思义,析构函数应该是定义了如何销毁成员变量,虽然还可以定义其他的操作(如释放资源),但那不是我们关注的重点。

  

  编译器总是会生成一个默认的析构函数,以成员变量声明顺序的逆序进行销毁,销毁每一个非static成员。

  且,总是最后调用默认的析构函数。哪怕已经调用过自定义的析构函数!

  需要注意的是,对于类的对象来说,①当超出作用域时,系统会自动调用析构函数。②当主动释放内存时,也会自动调用析构函数。

  这点对于Javaer来说就是一个坑,极容易被带入坑中。例如 :

Person *p=new Person; // no ()
{
    Person p2(*p);
    delete p;
}

  上面,超出花括号时,系统就会自动调用p2的析构函数,销毁p2! -- 注意,引用和指针不会。

  另外,撤销一个容器(包括数组)时,也会一一调用其元素的析构函数,从最后一个开始撤销。

  默认的析构函数并不会删除指针类型成员所指向的对象!

一个规则:如果类需要析构函数,则它也需要赋值操作符和复制构造函数!

时间: 2024-10-06 20:55:31

C++ 类的复制控制的相关文章

类的复制控制

新建一个类对象时,类的构造函数会对其初始化,许多时候需要使用一个已经存在的对象去复制出同类的一个或多个新对象,这个时候就需要使用到类的复制构造函数.有些情况需要同类对象之间互相赋值,就像A=B一样,这就是赋值函数的工作.当对象的作用域结束或动态分配的对象被删除时,就应自动调用析构函数释放对象获取的所有资源.复制构造函数是一种特殊的构造函数.具有单个形参,这个参数采用对象的引用的形式.赋值函数其实就是赋值操作符=.析构函数是构造函数的互补. 复制构造函数.赋值函数和析构函数总称为复制控制.当类没有

复制控制(下)

消息处理示例 有些类为了做一些工作需要对复制进行控制.为了给出这样的例子,我们将概略定义两个类,这两个类可用于邮件处理应用程序.Message 类和 Folder 类分别表示电子邮件(或其他)消息和消息所出现的目录,一个给定消息可以出现在多个目录中.Message 上有 save 和 remove 操作,用于在指定 Folder 中保存或删除该消息.对每个 Message,我们并不是在每个 Folder 中都存放一个副本,而是使每个 Message 保存一个指针集(set),set 中的指针指向

15.4——构造函数与复制控制

构造函数与继承: (1)构造函数和复制控制成员不能继承,每个类定义自己的,要是没定义就用合成的. 对于基类来说: (1)基本没啥影响,若是只希望派生类来使用,可以定义为protected. 对于派生类来说: (1)派生类构造函数除了初始化自己的成员外,还要初始化基类(这个先进行). (2)若是合成的派生类默认构造函数,它将先调用基类的合成默认构造函数,再接着根据常规变量初始化自己的. (3)定义默认构造函数时的初始化列表可以只写派生类的,会自动隐含的调用基类的. (4)不能直接初始化继承类的成员

第十九篇:复制控制( 下 ) --- 自定义析构函数

前言 经过前两篇随笔( 上  中 )的分析我们已经解决了具有指针成员的同类对象“ 干涉 ”问题.可惜,前面给出的解决方案代码还是不完整.还有什么问题呢?观察发现,构造函数里面有new的关键字出现,也就是说开辟了新的内存空间,我们也知道new必须也只能对应一个delete,而不应该让系统自己处理( 还不熟练new和delete用法的点这里 ),但这里和new对应的delete去哪里了? 解决思路 应该何时delete?显然,应该在对象销毁的时候delete.C++中定义了这样一种函数 --- 析构

稍微深入点理解C++复制控制【转】

通过一个实例稍微深入理解C++复制控制过程,参考资料<C++ primer>,介绍点基本知识: 1.在C++中类通过特殊的成员函数:复制构造函数.赋值操作符和析构函数来控制复制.赋值和撤销该类的对象时会发生什么. 2.复制构造函数(copy constructor)是一种特殊的构造函数,具有单个形参,该形参(常用const)是对该类类型的引用: 当定义一个新对象并用一个同类型的对象对它进行初始化时,将显示使用复制构造函数: 当将该类的对象传递给函数或从函数返回该类型的对象时,将隐式使用复制构造

C++拾遗(六)——复制控制

年前忙了几天,到现在才算是有空休息下来.先祝大家新年快乐,心想事成:)我也会发笑脸o.o 这篇博文主要介绍定义一个类型的对象时的复制控制方式,这部分内容之前有一定的了解但又浅尝辄止,始终感觉没能找到要点.年前又拿起书细细品读,算是有了一点新的了解.几天前就想动笔了,一直没时间,拖到现在. 每种类型定义了创建该类型的对象时会发生什么——构造函数定义了该类类型对象的初始化.类型还能控制复制.赋值或撤销该类型的对象时会发生什么——类通过特殊的成员函数:复制构造函数.赋值操作符和析构函数来控制这些行为.

C++之重载String ----- 构造函数、复制控制、重载操作符

本博文 我们通过 重新实现String类 来说明构造函数,复制控制,重载操作符. 一.构造函数(包括析构函数): 1:默认构造函数: 2:用户自己定义的构造函数 注意:当用户自己定义时,也要明确显示默认构造函数,这是因为,当我们没有定义自己的构造函数时,编译器会为我们自动合成一个,而我们定义了构造函数时,编译器默认构造函数改为我们自己定义的.这时就有可能出现错误: 3:析构函数: 具体声明与实现,代码如下: 1 声明部分: 2 String(); 3 String(const char*s);

C++复制控制

1.复制构造函数可用于: (1)根据另一个同类型的对象显示或隐式初始化一个对象 string str1="test";   //隐式 string str2=str1; //显示 str1为先调用string的字符串形参的构造函数,创建一个临时对象,然后,使用string复制构造函数将str1初始化为那个临时对象的副本. (2)复制一个对象,将它作为实参传给一个函数 (3)从函数返回时复制一个对象 当形参为非引用类型的时候,将复制实参的值.类似地,以非引用类型作返回值时,将返回retu

c++OOP之复制控制 ------复制构造函数、赋值重载、析构

本博文我们讨论OOP复制控制的一些内容: 首先考虑对象复制的时机: 非引用类型 1):根据一个类去显式或者隐式初始化一个对象: 2):复制一个对象,将它作为实参传给一个函数: 3):从函数返回时复制一个对象.(string tolittle(string word)) 一个空类,编译器提供默认无参数构造函数.拷贝构造函数.赋值运算符以及析构函数,一共四个函数.(面试) 11.复制构造函数.赋值运算符以及析构函数,称为三法则,一旦提供了其中一个,务必提供其余两个.以String为例: a) 涉及到