C++类中拷贝构造函数详解

Wesley13
• 阅读 677

a. C++标准中提到“The default constructor, copy constructor and copy assignment operator, and destructor are special member functions.[Note: The implementation will implicitly declare these member functions for some class types when the program does not explicitly declare them. The implementation will implicitly define them if they are used.]”。即缺省构造函数、拷贝构造函数、拷贝赋值操作符和析构函数是特殊成员函数。

b. “Constructors do not have names. A special declarator syntax using an optional sequence of function- specifiers(inline, virtual and explicit) followed by the constructor’s class name followed by a parameter list is used to declare or define the constructor.” 构造函数没有名称。

c. 构造函数不能有返回类型,也不能由virtual, const, static 和 volatile来修饰。但可以由inline来修饰,事实上隐式构造函数就是用inline来修饰的。inline表示编译时展开,通常速度块;virtual表示运行时绑定,通常意味着灵活。

d. 类中存在虚函数或者有虚基类的情况下需要显式声明构造函数。拷贝构造函数也是如此。

#include <iostream>
using namespace std;

class A{
public:
    A(){ cout << "A" << endl;}
    //virtual ~A(){ cout << "~A" << endl;}
    ~A(){ cout << "~A" << endl;}
};
class B : public A{
public:
    B(){ cout << "B" << endl;}
    ~B(){ cout << "~B" << endl;}
};
int main()
{
    A *a = new B();
    delete a;
    return 0;
}

输出:

A
B
~A
请按任意键继续. . .

一般情况下类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。我想所有的C++程序员都知道这样的危险性。当然,如果在析构函数中做了其他工作的话,那你的所有努力也都是白费力气。
这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。
当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。

f. 构造函数是一种特殊函数,而拷贝构造函数是一种特殊的构造函数。类X的构造函数的第一个参数必须为X&,或者const X&;除了第一个参数外,构造函数要么不存在其他参数,如果存在其他参数,其他参数必须有默认值。一个类可以有多个拷贝构造函数。它的形式如下:

X::X(X& x)
X::X(const X& x)
X::X(X& x, int a = 0, int b = 1…)

g. 什么时候会调用拷贝构造函数?
    以下三种情况出现时,会调用一个类的拷贝构造函数:
    1) 用一个已经实例化了的该类对象,去实例化该类的另外一个对象;
    2) 用该类的对象传值的方式作为一个函数的参数;
    3) 一个函数返回值为该类的一个对象。

#include <iostream>
using namespace std;

class CA
{
public:
    int a;
    int b;
public:
    inline CA()
    {
        a = 1;
        b = 1;
    }

    inline CA(int A, int B)
    {
        a = A;
        b = B;
    }

    inline CA(CA& x)
    {
        a = x.a;
        b = x.b;
        cout << "copy constructor is called." << endl;
    }

    void printInfo()
    {
        cout << "a = " << a << ", b = " << b << endl;
    }
};

int someFun1(CA x)
{
    return x.a + x.b;
}

CA someFun2(int a, int b)
{
    CA ca(a, b);
    return ca;                                      
}

int main(void)
{
    CA a;
    // CA b();                                 // 不能用这种方式声明CA的对象b!
    CA c(10, 10);
    CA d(c);                                   // 情况1) -> 调用拷贝构造函数
    int anInt = someFun1(c);           // 情况2) -> 调用拷贝构造函数
    CA e = someFun2(11, 11);        // 情况3) -> 调用拷贝构造函数

    return 0;
}

运行结果:
copy constructor is called.
copy constructor is called.
copy constructor is called.

运行结果表明,上述结论是正确的。

h. 什么时候必须要显式声明拷贝构造函数?
    拷贝构造函数的作用就是用一个已经实例化了的该类对象,去实例化该类的另外一个对象。

  1. 下面的代码并没有显式声明一个构造函数,编译器会自动为类CExample1生成一个缺省的隐式拷贝构造函数:

    #include using namespace std;

    class CExample1 { private: int a; public: CExample1(int b){a = b;} void SetValue(int a){this->a = a;} void Show(){cout << a << endl;} };

    int main(void) { CExample1 A(100); CExample1 B = A; // 调用了缺省的隐式拷贝构造函数 CExample1 C(B); // 调用了缺省的隐式拷贝构造函数 B.Show(); // 输出应该是100 B.SetValue(90); B.Show(); // 输出应该是90 A.Show(); // 输出应该是100 C.Show(); // 输出应该是100 return 0; }

输出为:

100
90
100
100

  1. 如果有成员变量以指针形式存在,涉及动态内存分配等情况下,一定要显式声明拷贝构造函数。要注意到,如果需要显式定义拷贝构造函数,那么通常都是需要同时定义析构函数(因为通常涉及了动态内存分配),至于是否必须重载操作符“=”,要视情况而定。

    #include using namespace std;

    class CSomething { public: int a; int b; public: CSomething(int a, int b) {this->a = a; this->b = b;} };

    class CA { private: CSomething* sth; // 以指针形式存在的成员变量 public: CA(CSomething* sth){this->sth = new CSomething(sth->a, sth->b);} ~CA() { cout << "In the destructor of class CA..." << endl; if (NULL != sth) delete sth; } void Show(){cout << "(" << sth->a << ", " << sth->b << ")" << endl;} void setValue(int a, int b){sth->a = a; sth->b = b;} void getSthAddress() { cout << sth << endl; } };

    int main(void) { CSomething sth(1, 2); CA ca(&sth); ca.Show(); CA cb(ca); // 调用缺省的隐式拷贝构造函数 cb.Show(); cb.setValue(2, 3); ca.Show(); cb.Show(); ca.getSthAddress(); cb.getSthAddress(); return 0; }

上面的程序没有显式声明拷贝构造函数,运行结果如下:
C++类中拷贝构造函数详解

可见,ca和cb中的指针成员变量sth指向的是同一个内存地址(Console输出的第5、6行),这就是为什么在cb.setValue(2, 3)后,ca对应的内容也发生了改变(Console输出的第3、4行),而这不是我们所期望的;其次,我们生成了两个对象ca和cb,因此对两次调用析构函数,第一次调用析构函数的时候没有问题,因为此时sth里面有内容,第二次调用析构函数时,sth里面的内容由于在第一次调用析构函数的时候已经被delete了,所以会出现如上的错误提示。

保持其他代码不变,现在我们增加一个拷贝构造函数如下:

CA(CA& obj)
{
         sth = new CSomething((obj.sth)->a, (obj.sth)->b);
}
再运行上面的程序,所得到的结果如下:

C++类中拷贝构造函数详解

这次,ca和cb中的指针成员变量sth指向的不是同一个内存地址(Console输出的第5、6行)了,这就是为什么在cb.setValue(2, 3)后,ca对应的内容保持不变,而cb的内容该如愿地改为(2, 3)(Console输出的第3、4行);其次,析构函数也不会报告错误了。

  1. 关于拷贝构造函数另外一个完整的例子,其中包含了copy constructor,destructor 和copy assignment operator。

    #include using namespace std;

    class Point { public: int _x; int _y; public: Point(); Point(int, int); };

    Point::Point() { _x = 0; _y = 0; }

    Point::Point(int x, int y) { _x = x; _y = y; }

    class CA { public: Point* _point; public: CA() { _point = NULL; } CA(const Point*); void setPointValues(int, int); void printCoordinates(); // 需要增加的拷贝构造函数 CA(const CA&); // 需要增加的析构函数 virtual ~CA(); // 需要增加的拷贝赋值函数 CA& operator = (const CA&); };

    CA::CA(const Point* point) { _point = new Point(); // 发生了动态内存分配!因此不能缺少析构函数。 _point->_x = point->_x; _point->_y = point->_y; }

    // 需要增加的拷贝构造函数的实现 CA::CA(const CA& ca) { _point = new Point(); _point->_x = (ca._point)->_x; _point->_y = (ca._point)->_y; }

    // 需要增加的析构函数的实现 CA::~CA() { if(NULL != _point) delete _point; _point = NULL; }

    // 需要增加的拷贝赋值函数的实现 CA& CA::operator = (const CA& ca) { _point = new Point(); _point->_x = (ca._point)->_x; _point->_y = (ca._point)->_y; return *this; }

    void CA::setPointValues(int x, int y) { _point->_x = x; _point->_y = y; }

    void CA::printCoordinates() { cout << "Coordinates = (" << _point->_x << ", " << _point->_y << ")" << endl; }

    int main(void) { Point apoint(1, 2); CA ca(&apoint); ca.printCoordinates(); CA cb(ca); // 调用拷贝构造函数 cb.printCoordinates(); cb.setPointValues(12, 12); cb.printCoordinates(); ca.printCoordinates(); CA cc; cc = cb; // 调用拷贝赋值函数 cc.printCoordinates(); cc.setPointValues(13, 13);
    ca.printCoordinates(); cb.printCoordinates(); cc.printCoordinates(); return 0; }

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解2016年09月02日00:00:36 \牧野(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fme.csdn.net%2Fdcrmg) 阅读数:59593
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这