C++构造函数总结

一.构造函数基本概念(本部分源自C++ 类构造函数 & 析构函数)

1.1 类的构造函数

1.1.1 概念

类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。

构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。

下面的实例有助于更好地理解构造函数的概念:

#include 
 
using namespace std;
 
class Line
{
   public:
      void setLength( double len );
      double getLength( void );
      Line();  // 这是构造函数
 
   private:
      double length;
};
 
// 成员函数定义,包括构造函数
Line::Line(void)
{
    cout << "Object is being created" << endl;
}
 
void Line::setLength( double len )
{
    length = len;
}
 
double Line::getLength( void )
{
    return length;
}
// 程序的主函数
int main( )
{
   Line line;
 
   // 设置长度
   line.setLength(6.0); 
   cout << "Length of line : " << line.getLength() <

当上面的代码被编译和执行时,它会产生下列结果:

Object is being created
Length of line : 6

1.2 带参数的构造函数

默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值,如下面的例子所示:

#include 
 
using namespace std;
 
class Line
{
   public:
      void setLength( double len );
      double getLength( void );
      Line(double len);  // 这是构造函数
 
   private:
      double length;
};
 
// 成员函数定义,包括构造函数
Line::Line( double len)
{
    cout << "Object is being created, length = " << len << endl;
    length = len;
}
 
void Line::setLength( double len )
{
    length = len;
}
 
double Line::getLength( void )
{
    return length;
}
// 程序的主函数
int main( )
{
   Line line(10.0);
 
   // 获取默认设置的长度
   cout << "Length of line : " << line.getLength() <

当上面的代码被编译和执行时,它会产生下列结果:

Object is being created, length = 10
Length of line : 10
Length of line : 6

1.3 使用初始化列表来初始化字段

使用初始化列表来初始化字段:

Line::Line( double len): length(len)
{
    cout << "Object is being created, length = " << len << endl;
}

上面的语法等同于如下语法:

Line::Line( double len)
{
    length = len;
    cout << "Object is being created, length = " << len << endl;
}

 假设有一个类 C,具有多个字段 X、Y、Z 等需要进行初始化,同理地,您可以使用上面的语法,只需要在不同的字段使用逗号进行分隔,如下所示:

C::C( double a, double b, double c): X(a), Y(b), Z(c)
{
  ....
}

1.4 类的析构函数

类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。

析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。

下面的实例有助于更好地理解析构函数的概念:

#include 
 
using namespace std;
 
class Line
{
   public:
      void setLength( double len );
      double getLength( void );
      Line();   // 这是构造函数声明
      ~Line();  // 这是析构函数声明
 
   private:
      double length;
};
 
// 成员函数定义,包括构造函数
Line::Line(void)
{
    cout << "Object is being created" << endl;
}
Line::~Line(void)
{
    cout << "Object is being deleted" << endl;
}
 
void Line::setLength( double len )
{
    length = len;
}
 
double Line::getLength( void )
{
    return length;
}
// 程序的主函数
int main( )
{
   Line line;
 
   // 设置长度
   line.setLength(6.0); 
   cout << "Length of line : " << line.getLength() <

当上面的代码被编译和执行时,它会产生下列结果:

Object is being created
Length of line : 6
Object is being deleted

二、拷贝构造函数(本部分源自C++拷贝构造函数详解)

2.1. 什么是拷贝构造函数

首先对于普通类型的对象来说,它们之间的复制是很简单的,例如:

int a = 100;
int b = a; 

而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。
下面看一个类对象拷贝的简单例子。

#include 
using namespace std;

class CExample {
private:
     int a;
public:
      //构造函数
     CExample(int b)
     { a = b;}

      //一般函数
     void Show ()
     {
        cout<

运行程序,屏幕输出100。从以上代码的运行结果可以看出,系统为对象 B 分配了内存并完成了与对象 A 的复制过程。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。

下面举例说明拷贝构造函数的工作过程。

#include 
using namespace std;

class CExample {
private:
    int a;
public:
    //构造函数
    CExample(int b)
    { a = b;}
    
    //拷贝构造函数
    CExample(const CExample& C)
    {
        a = C.a;
    }

    //一般函数
    void Show ()
    {
        cout<

CExample(const CExample& C) 就是我们自定义的拷贝构造函数。可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量

2.2 . 拷贝构造函数的调用时机

在C++中,下面三种对象需要调用拷贝构造函数!


2.2.1. 对象以值传递的方式传入函数参数

class CExample 
{
private:
 int a;

public:
 //构造函数
 CExample(int b)
 { 
  a = b;
  cout<<"creat: "<

调用g_Fun()时,会产生以下几个重要步骤:
(1).test对象传入形参时,会先会产生一个临时变量,就叫 C 吧。
(2).然后调用拷贝构造函数把test的值给C。 整个这两个步骤有点像:CExample C(test);
(3).等g_Fun()执行完后, 析构掉 C 对象。

2.2.2. 对象以值传递的方式从函数返回

class CExample 
{
private:
 int a;

public:
 //构造函数
 CExample(int b)
 { 
  a = b;
 }

 //拷贝构造
 CExample(const CExample& C)
 {
  a = C.a;
  cout<<"copy"<

 当g_Fun()函数执行到return时,会产生以下几个重要步骤:
(1). 先会产生一个临时变量,就叫XXXX吧。
(2). 然后调用拷贝构造函数把temp的值给XXXX。整个这两个步骤有点像:CExample XXXX(temp);
(3). 在函数执行到最后先析构temp局部变量。
(4). 等g_Fun()执行完后再析构掉XXXX对象。

2.2.3. 对象需要通过另一个对象进行初始化;

CExample A(100);
CExample B = A; 
// CExample B(A);

后两句都会调用拷贝构造函数。

2.3. 浅拷贝和深拷贝

2.3.1. 默认拷贝构造函数

    很多时候在我们都不知道拷贝构造函数的情况下,传递对象给函数参数或者函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值,它一般具有以下形式:

Rect::Rect(const Rect& r)
{
    width = r.width;
    height = r.height;
}

 当然,以上代码不用我们编写,编译器会为我们自动生成。但是如果认为这样就可以解决对象的复制问题,那就错了,让我们来考虑以下一段代码:

class Rect
{
public:
    Rect()      // 构造函数,计数器加1
    {
        count++;
    }
    ~Rect()     // 析构函数,计数器减1
    {
        count--;
    }
    static int getCount()       // 返回计数器的值
    {
        return count;
    }
private:
    int width;
    int height;
    static int count;       // 一静态成员做为计数器
};

int Rect::count = 0;        // 初始化计数器

int main()
{
    Rect rect1;
    cout<<"The count of Rect: "<

 这段代码对前面的类,加入了一个静态成员,目的是进行计数。在主函数中,首先创建对象rect1,输出此时的对象个数,然后使用rect1复制出对象rect2,再输出此时的对象个数,按照理解,此时应该有两个对象存在,但实际程序运行时,输出的都是1,反应出只有1个对象。此外,在销毁对象时,由于会调用销毁两个对象,类的析构函数会调用两次,此时的计数器将变为负数。

说白了,就是拷贝构造函数没有处理静态数据成员。

出现这些问题最根本就在于在复制对象时,计数器没有递增,我们重新编写拷贝构造函数,如下:

class Rect
{
public:
    Rect()      // 构造函数,计数器加1
    {
        count++;
    }
    Rect(const Rect& r)   // 拷贝构造函数
    {
        width = r.width;
        height = r.height;
        count++;          // 计数器加1
    }
    ~Rect()     // 析构函数,计数器减1
    {
        count--;
    }
    static int getCount()   // 返回计数器的值
    {
        return count;
    }
private:
    int width;
    int height;
    static int count;       // 一静态成员做为计数器
};

2.3.2. 浅拷贝

    所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了,让我们考虑如下一段代码:

class Rect
{
public:
    Rect()      // 构造函数,p指向堆中分配的一空间
    {
        p = new int(100);
    }
    ~Rect()     // 析构函数,释放动态分配的空间
    {
        if(p != NULL)
        {
            delete p;
        }
    }
private:
    int width;
    int height;
    int *p;     // 一指针成员
};

int main()
{
    Rect rect1;
    Rect rect2(rect1);   // 复制对象
    return 0;
}

在这段代码运行结束之前,会出现一个运行错误。原因就在于在进行对象复制时,对于动态分配的内容没有进行正确的操作。我们来分析一下:

    在运行定义rect1对象后,由于在构造函数中有一个动态分配的语句,因此执行后的内存情况大致如下:

C++构造函数总结_第1张图片

 在使用rect1复制rect2时,由于执行的是浅拷贝,只是将成员的值进行赋值,这时 rect1.p = rect2.p,也即这两个指针指向了堆里的同一个空间,如下图所示:

C++构造函数总结_第2张图片

当然,这不是我们所期望的结果,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因。我们需要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。

2.3.3. 深拷贝

    在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间,如上面的例子就应该按照如下的方式进行处理:

class Rect
{
public:
    Rect()      // 构造函数,p指向堆中分配的一空间
    {
        p = new int(100);
    }
    Rect(const Rect& r)
    {
        width = r.width;
        height = r.height;
        p = new int;    // 为新对象重新动态分配空间
        *p = *(r.p);
    }
    ~Rect()     // 析构函数,释放动态分配的空间
    {
        if(p != NULL)
        {
            delete p;
        }
    }
private:
    int width;
    int height;
    int *p;     // 一指针成员
};

此时,在完成对象的复制后,内存的一个大致情况如下:

C++构造函数总结_第3张图片

此时rect1的p和rect2的p各自指向一段内存空间,但它们指向的空间具有相同的内容,这就是所谓的“深拷贝”。

2.3.4. 防止默认拷贝发生

    通过对对象复制的分析,我们发现对象的复制大多在进行“值传递”时发生,这里有一个小技巧可以防止按值传递——声明一个私有拷贝构造函数。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。

// 防止按值传递
class CExample 
{
private:
	int a;

public:
	//构造函数
	CExample(int b)
	{ 
		a = b;
		cout<<"creat: "<

2.4. 拷贝构造函数的几个细节

1. 拷贝构造函数里能调用private成员变量吗?
解答:
这个问题是在网上见的,当时一下子有点晕。其时从名子我们就知道拷贝构造函数其时就是一个特殊的构造函数,操作的还是自己类的成员变量,所以不受private的限制。

2. 以下函数哪个是拷贝构造函数,为什么?

X::X(const X&);    
X::X(X);    
X::X(X&, int a=1);    
X::X(X&, int a=1, int b=2);

解答:对于一个类X, 如果一个构造函数的第一个参数是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数.

X::X(const X&);  //是拷贝构造函数    
X::X(X&, int=1); //是拷贝构造函数   
X::X(X&, int a=1, int b=2); //当然也是拷贝构造函数

 3. 一个类中可以存在多于一个的拷贝构造函数吗?
解答:
类中可以存在超过一个拷贝构造函数。

class X { 
public:       
  X(const X&);      // const 的拷贝构造
  X(X&);            // 非const的拷贝构造
};

注意,如果一个类中只存在一个参数为 X& 的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化.

class X {    
public:
  X();    
  X(X&);
};    

const X cx;    
X x = cx;    // error

如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数。
这个默认的参数可能为 X::X(const X&)或 X::X(X&),由编译器根据上下文决定选择哪一个。

三、禁用拷贝构造函数(源自为什么很多人禁用拷贝(复制)构造函数)

关于C++的拷贝构造函数,很多的建议是直接禁用。为什么大家会这么建议呢?没有拷贝构 造函数会有什么限制呢?如何禁用拷贝构造呢?这篇文章对这些问题做一个简单的总结。

这里讨论的问题以拷贝构造函数为例子,但是通常赋值操作符是通过拷贝构造函数来实现 的( copy-and-swap 技术,详见《Exceptional C++》一书),所以这里讨论也适用于赋 值操作符,通常来说禁用拷贝构造函数的同时也会禁用赋值操作符。


3.1 为什么禁用拷贝构造函数

关于拷贝构造函数的禁用原因,我目前了解的主要是两个原因。第一是浅拷贝问题,第二 个则是基类拷贝问题。

3.1.1 浅拷贝问题

编译器默认生成的构造函数,是memberwise拷贝^1,也就是逐个拷贝成员变量,对于 下面这个类的定义

class Widget {
public:
Widget(const std::string &name) : name_(name), buf_(new char[10]) {}
~Widget() { delete buf_; }
private:
std::string name_;
char *buf_;
};

默认生成的拷贝构造函数,会直接拷贝buf_的值,导致两个Widget对象指向同一个缓 冲区,这会导致析构的时候两次删除同一片区域的问题(这个问题又叫双杀问题)。

解决这个问题的方式有很多:

  1. 自己编写拷贝构造函数,然后在拷贝构造函数中创建新的buf_,不过拷贝构造函数的 编写需要考虑异常安全的问题,所以编写起来有一定的难度。

  2. 使用 shared_ptr 这样的智能指针,让所有的 Widget 对象共享一片 buf_,并 让 shared_ptr 的引用计数机制帮你智能的处理删除问题。

  3. 禁用拷贝构造函数和赋值操作符。如果你根本没有打算让Widget支持拷贝,你完全可 以直接禁用这两操作,这样一来,前面提到的这些问题就都不是问题了。

3.1.2 基类拷贝构造问题

如果我们不去自己编写拷贝构造函数,编译器默认生成的版本会自动调用基类的拷贝构造 函数完成基类的拷贝:

class Base {
public:
Base() { cout << "Base Default Constructor" << endl; }
Base(const Base &) { cout << "Base Copy Constructor" << endl; }
};
class Drived : public Base {
public:
Drived() { cout << "Drived Default Constructor" << endl; }
};
int main(void) {
Drived d1;
Drived d2(d1);
}

上面这段代码的输出如下:

Base Default Constructor
Drived Default Constructor
Base Copy Constructor // 自动调用了基类的拷贝构造函数

但是如果我们出于某种原因编写了,自己编写了拷贝构造函数(比如因为上文中提到的浅 拷贝问题),编译器不会帮我们安插基类的拷贝构造函数,它只会在必要的时候帮我们安 插基类的默认构造函数:

class Base {
public:
Base() { cout << "Base Default Constructor" << endl; }
Base(const Base &) { cout << "Base Copy Constructor" << endl; }
};
class Drived : public Base {
public:
Drived() { cout << "Drived Default Constructor" << endl; }
Drived(const Drived& d) {
cout << "Drived Copy Constructor" << endl;
}
};
int main(void) {
Drived d1;
Drived d2(d1);
}

上面这段代码的输出如下:

Base Default Constructor
Drived Default Constructor
Base Default Constructor // 调用了基类的默认构造函数
Drived Copy Constructor

这当然不是我们想要看到的结果,为了能够得到正确的结果,我们需要自己手动调用基类 的对应版本拷贝基类对象。

Drived(const Drived& d) : Base(d) {
cout << "Drived Copy Constructor" << endl;
}

这本来不是什么问题,只不过有些人编写拷贝构造函数的时候会忘记这一点,所以导致基 类子对象没有正常复制,造成很难察觉的BUG。所以为了一劳永逸的解决这些蛋疼的问题, 干脆就直接禁用拷贝构造和赋值操作符。


3.2 没有拷贝构造的限制

在C++11之前对象必须有正常的拷贝语义才能放入容器中,禁用拷贝构造的对象无法直接放 入容器中,当然你可以使用指针来规避这一点,但是你又落入了自己管理指针的困境之中 (或许使用智能指针可以缓解这一问题)。

C++11中存在移动语义,你可以通过移动而不是拷贝把数据放入容器中。

拷贝构造函数的另一个应用在于设计模式中的原型模式,在C++中没有拷贝构造函数,这 个模式实现可能比较困难。


3.3 如何禁用拷贝构造

  1. 如果你的编译器支持 C++11,直接使用 delete

  2. 否则你可以把拷贝构造函数和赋值操作符声明成private同时不提供实现。

  3. 你可以通过一个基类来封装第二步,因为默认生成的拷贝构造函数会自动调用基类的拷 贝构造函数,如果基类的拷贝构造函数是 private,那么它无法访问,也就无法正常 生成拷贝构造函数。

class NonCopyable {
protected:
~NonCopyable() {} // 关于为什么声明成为 protected,参考
// 《Exceptional C++ Style》
private:
NonCopyable(const NonCopyable&);
}
class Widget : private NonCopyable { // 关于为什么使用 private 继承
// 参考《Effective C++》第三版
}
Widget widget(Widget()); // 错误

上不会生成memberwise的拷贝构造函数,详细内容可以参考《深度探索C++对象模型》一 书

3.4  禁用拷贝


禁用原因主要是两个:
1. 浅拷贝问题,也就是上面提到的二次析构。
2. 自定义了基类和派生类的拷贝构造函数,但派生类对象拷贝时,调用了派生类的拷贝,没有调用自定义的基类拷贝而是调用默认的基类拷贝。这样可能造成不安全,比如出现二次析构问题时,因为不会调用我们自定义的基类深拷贝,还是默认的浅拷贝。

Effective C++条款6规定,如果不想用编译器自动生成的函数,就应该明确拒绝。方法一般有三种:
1. C++11对函数声明加delete关键字:Base(const Base& obj) = delete;,不必有函数体,这时再调用拷贝构造会报错尝试引用已删除的函数。
2. 最简单的方法是将拷贝构造函数声明为private
3. 条款6给出了更好的处理方法:创建一个基类,声明拷贝构造函数,但访问权限是private,使用的类都继承自这个基类。默认拷贝构造函数会自动调用基类的拷贝构造函数,而基类的拷贝构造函数是private,那么它无法访问,也就无法正常生成拷贝构造函数。

Qt就是这样做的,QObject定义中有这样一段,三条都利用了:

第一种方法:最简单的方法是将拷贝构造函数声明为private

private:
    Q_DISABLE_COPY(QMainWindow)

#define Q_DISABLE_COPY(Class) \
    Class(const Class &) Q_DECL_EQ_DELETE;\
    Class &operator=(const Class &) Q_DECL_EQ_DELETE;


类的不可拷贝特性是可以继承的,例如凡是继承自QObject的类都不能使用拷贝构造函数和赋值运算符。
 

(2)第二种方法 继承一个uncopyable类
C++的编译在链接之前,如果我们能在编译期解决这个问题,会节省不少的时间,要想在编译期解决问题,就需要人为制造一些bug。我们声明一个专门阻止拷贝的基类uncopyable。

class uncopyable{
protected:
    uncopyable(){}
    ~uncopyable(){}
private:
    uncopyable(const uncopyable&);
    uncopyable& operator=(const uncopyable&);
}


接下来,我们的类只要继承uncopyable,如果要发生拷贝,编译器都会尝试调用基类的拷贝构造函数或者赋值运算符,但是因为这两者是私有的,会出现编译错误。

四、C++类对象的赋值与复制(源自C++类对象的赋值与复制)

本文主要介绍C++中类对象的赋值操作、复制操作,以及两者之间的区别,另外还会讲到“深拷贝”与“浅拷贝”的相关内容。

本系列内容会分为三篇文章进行讲解。

4.1 对象的赋值


4.1.1 what

如同基本类型的赋值语句一样,同一个类的对象之间也是可以进行赋值操作的,即将一个对象的值赋给另一个对象。

对于类对象的赋值,只会对类中的数据成员进行赋值,而不对成员函数赋值。

例如:obj1 和 obj2 是同一类 ClassA 的两个对象,那么对象赋值语句“obj2 = obj1;” 就会把对象 obj1 的数据成员的值逐位赋给对象 obj2。


4.1.2 代码示例

下面展示一个对象赋值的代码示例(object_assign_and_copy_test1.cpp),如下:

#include 
 
using namespace std;
 
class ClassA
{
public:
    // 设置成员变量的值
    void SetValue(int i, int j)
    {
        m_nValue1 = i;
        m_nValue2 = j;
    }
    // 打印成员变量的值
    void ShowValue()
    {
        cout << "m_nValue1 is: " << m_nValue1 << ", m_nValue2 is: " << m_nValue2 << endl;
    }
private:
    int m_nValue1;
    int m_nValue2;
};
 
int main()
{
    // 声明对象obj1和obj2
    ClassA obj1;
    ClassA obj2;
 
    obj1.SetValue(1, 2);
    // 对象赋值场景 —— 将obj1的值赋给obj2
    obj2 = obj1;
    cout << "obj1 info as followed: " << endl;
    obj1.ShowValue();
    cout << "obj2 info as followed: " << endl;
    obj2.ShowValue();
 
    return 0;
}
 

编译并运行上述代码,结果如下:

上面的执行结果表明,通过对象赋值语句,我们将obj1的值成功地赋给了obj2。

4.1.3 几点说明

对于对象赋值,进行以下几点说明:

  • 进行对象赋值时,两个对象的必须属于同一个类,如对象所述的类不同,在编译时将会报错;
  • 两个对象之间的赋值,只会让这两个对象中数据成员相同,而两个对象仍然是独立的。例如在上面的示例代码中,进行对象赋值后,再调用 obj1.set() 设置 obj1 的值,并不会影响到 obj2 的值;
  • 对象赋值是通过赋值运算函数实现的。每一个类都有默认的赋值运算符,我们也可以根据需要,对赋值运算符进行重载。一般来说,需要手动编写析构函数的类,都需要重载赋值运算符(具体原因下文会介绍);
  • 在对象声明之后,进行的对象赋值运算,才属于“真正的”对象赋值运算,即使用了赋值运算符“=”;而在对象初始化时,针对对象进行的赋值操作,其实是属于对象的复制。示例如下:

    // 声明obj1和obj2
    ClassA obj1;
    ClassA obj2;
    obj2 = obj1; // 此语句为对象的赋值
 
    // 声明obj1
    ClassA obj1;
    // 声明并初始化obj2
    ClassA obj2 = obj1; // 此语句属于对象的复制

4.1.4 进一步研究

下面从内存分配的角度分析一下对象的赋值操作。


4.1.4.1 C++中对象的内存分配方式

在C++中,只要声明了对象,对象实例在编译的时候,系统就需要为其分配内存了。一段代码示例如下:

class ClassA
{
public:
    ClassA(int id, char* name)
    {
        m_nId = id;
        m_pszName = new char[strlen(name) + 1];
        strcpy(m_pszName, name);
    }
private:
    char* m_pszName;
    int m_nId;
};
 
int main()
{
    ClassA obj1(1, "liitdar");
    ClassA obj2;
 
    return 0;
}

在上述代码编译之后,系统为 obj1 和 obj2 都分配相应大小的内存空间(只不过对象 obj1 的内存域被初始化了,而 obj2 的内存域的值为随机值)。两者的内存分配效果如下:


4.1.4.2 默认的赋值运算符

延续上面的示例代码,我们执行“obj2 = obj1;”,即利用默认的赋值运算符将对象 obj1 的值赋给 obj2。使用类中默认的赋值运算符,会将对象中的所有位于 stack 中的域进行相应的复制操作;同时,如果对象有位于 heap 上的域,则不会为目标对象分配 heap 上的空间,而只是让目标对象指向源对象 heap 上的同一个地址。

执行了“obj2 = obj1;”默认的赋值运算后,两个对象的内存分配效果如下:

因此,对于类中默认的赋值运算,如果源对象域内没有 heap 上的空间,其不会产生任何问题。但是,如果源对象域内需要申请 heap 上的空间,那么由于源对象和目标对象都指向 heap 的同一段内容,所以在析构对象的时候,就会连续两次释放 heap 上的那一块内存区域,从而导致程序异常。

    ~ClassA()
    {
        delete m_pszName;
    }

1.4.3 解决方案

为了解决上面的问题,如果对象会在 heap 上存在内存域,则我们必须重载赋值运算符,从而在进行对象的赋值操作时,使不同对象的成员域指向不同的 heap 地址。

重载赋值运算符的代码如下:

    // 赋值运算符重载需要返回对象的引用,否则返回后其值立即消失
    ClassA& operator=(ClassA& obj)
    {
        // 释放heap内存
        if (m_pszName != NULL)
        {
            delete m_pszName;
        }
        // 赋值stack内存的值
        this->m_nId = obj.m_nId;
        // 赋值heap内存的值
        int nLength = strlen(obj.m_pszName);
        m_pszName = new char[nLength + 1];
        strcpy(m_pszName, obj.m_pszName);
        
        return *this;
    }

使用上面重载后的赋值运算符对对象进行赋值时,两个对象的内存分配效果如下:

这样,在对象 obj1、obj2 退出其的作用域,调用相应的析构函数时,就会释放不同 heap 空间的内存,也就不会出现程序异常了。

4.2 对象的复制(源自C++类对象的赋值与复制(二))

4.2.1 what

相对于“对已声明的对象使用赋值运算符进行的对象赋值”操作,使用拷贝构造函数操作对象的方式,称为“对象的复制”。

类的拷贝构造函数是一种特殊的构造函数,其形参是本类对象的引用。拷贝构造函数的作用为:在创建一个新对象时,使用一个已经存在的对象去初始化这个新对象。例如语句“ClassA obj2(obj1);”就使用了拷贝构造函数,该语句在创建新对象 obj2 时,利用已经存在的对象 obj1 去初始化对象 obj2。

对象的赋值与对象的复制,貌似都是只对类的成员变量进行拷贝,而不会对类的成员函数进行操作。—— 待进一步确认。

4.2.2 拷贝构造函数的特点

拷贝构造函数有以下特点:

  • 拷贝构造函数也是一种构造函数,所以其函数名与类名相同,并且该函数也没有返回值类型;
  • 拷贝构造函数只有一个参数,并且该参数是其所属类对象的引用;
  • 每一个类都必须有一个拷贝构造函数,我们可以根据需要重载默认的拷贝构造函数(自定义拷贝构造函数),如果没有重载默认的拷贝构造函数,系统就会生成产生一个默认的拷贝构造函数,默认的拷贝构造函数将会复制出一个数据成员完全相同的新对象;

4.2.3 自定义拷贝构造函数

这里展示一个自定义拷贝构造函数的代码示例(object_assign_and_copy_test2.cpp),如下:

#include 
 
using namespace std;
 
class ClassA
{
public:
    // 普通构造函数
    ClassA(int i, int j)
    {
        m_nValue1 = i;
        m_nValue2 = j;
    }
    // 自定义的拷贝构造函数
    ClassA(const ClassA& obj)
    {
        m_nValue1 = obj.m_nValue1 * 2;
        m_nValue2 = obj.m_nValue2 * 2;
    }
    // 打印成员变量的值
    void ShowValue()
    {
        cout << "m_nValue1 is: " << m_nValue1 << ", m_nValue2 is: " << m_nValue2 << endl;
    }
private:
    int m_nValue1;
    int m_nValue2;
};
 
 
int main()
{
    // 创建并初始化对象obj1,此处调用了普通构造函数
    ClassA obj1(1, 2);
    // 创建并初始化对象obj2,此处调用了自定义的拷贝构造函数
    ClassA obj2(obj1);
 
    obj1.ShowValue();
    obj2.ShowValue();
    
    return 0;
}
 

编译并执行上述代码,结果如下:

上述执行结果表明,通过调用自定义的拷贝构造函数,我们在创建对象 obj2 时,结合对象 obj1 的成员变量的值,完成了我们自定义的初始化过程。

4.2.4 调用形式上的区别

我们可以从调用形式上,对“对象的赋值”和“对象的复制”进行区分。在此,我们列出一些对应关系:

  • 对象的赋值:指的是调用了类的赋值运算符,进行的对象的拷贝操作;
  • 对象的复制:指的是调用了类的拷贝构造函数,进行的对象的拷贝操作。

上面的对应关系是不严谨的,因为有些情况下,即使使用了赋值运算符“=”,但其实最终使用的仍然是类的拷贝构造函数,这就引出了拷贝构造函数的两种调用形式。

拷贝构造函数的调用语法分为两种:

  • 类名 对象2(对象1)。例如:“ClassA obj2(obj1);”,这种调用拷贝构造函数的方法称为“代入法”;
  • 类名 对象2 = 对象1。例如:“ClassA obj2 = obj1;”,这种调用拷贝构造函数的方法称为“赋值法”。

拷贝构造函数的“赋值法”就很容易与“对象的赋值”场景混淆,其二者之间的区别是:对象的赋值场景必须是建立在源对象与目标对象均已声明的基础上;而拷贝构造函数函数的赋值法,必须是针对新创建对象的场景。代码如下:

【对象的赋值】:

    // 声明对象obj1和obj2
    ClassA obj1;
    ClassA obj2;
 
    obj1.SetValue(1, 2);
    // 对象赋值场景 —— 将obj1的值赋给obj2
    obj2 = obj1;

【拷贝构造函数的“赋值法”】:

    // 创建并初始化对象obj1,此处调用了普通构造函数
    ClassA obj1(1, 2);
    // 创建并初始化对象obj2,此处调用了自定义的拷贝构造函数
    ClassA obj2 = obj1;

当然,为了代码的清晰化,建议使用拷贝构造函数的“代入法”,更可以让人一眼就看出调用的是拷贝构造函数。

4.2.5 调用拷贝构造函数的三个场景

4.2.5.1 类对象初始化

当使用类的一个对象去初始化另一个对象时,会调用拷贝构造函数(包括“代入法”和“赋值法”)。示例代码如下:

    // 创建并初始化对象obj1,此处调用了普通构造函数
    ClassA obj1(1, 2);
    // 创建并初始化对象obj2,此处调用了自定义的拷贝构造函数
    ClassA obj2 = obj1;    // 代入法
    ClassA obj3 = obj1;    // 赋值法

4.2.5.2 类对象作为函数参数

当类对象作为函数形参时,在调用函数进行形参和实参转换时,会调用拷贝构造函数。示例代码如下:

// 形参是类ClassA的对象obj
void funA(ClassA obj)
{
    obj.ShowValue();
}
    
int main()
{
    ClassA obj1(1, 2);
 
    // 调用函数funA时,实参obj1是类ClassA的对象
    // 这里会调用拷贝构造函数,使用实参obj1初始化形参对象obj
    funA(obj1);
    
    return 0; 
}

说明:在上面的main函数内,语句“funA(obj1);”就会调用拷贝构造函数。


4.2.5.3 类对象作为函数返回值

当函数的返回值是类的对象、在函数调用完毕将返回值(对象)带回函数调用处,此时会调用拷贝构造函数,将函数返回的对象赋值给一个临时对象,并传到函数的调用处。示例代码如下:

// 函数funB()的返回值类型是ClassA类类型
ClassA funB()
{
    ClassA obj1(1, 2);
    // 函数的返回值是ClassA类的对象
    return obj1;
}
 
int main()
{
    // 定义类ClassA的对象obj2
    ClassA obj2;
    // funB()函数执行完成、返回调用处时,会调用拷贝构造函数
    // 使用obj1初始化obj2
    obj2 = funB();
    
    return 0;
}

说明:在上面的main函数内,语句“obj2 = funB();”就会调用拷贝构造函数。由于对象obj1是函数funB中定义的,在函数funB结束时,obj1的生命周期就结束了,因此在函数funB结束之前,执行语句"return obj1"时,会调用拷贝构造函数将obj1的值拷贝到一个
临时对象中,这个临时对象是系统在主程序中临时创建的。funB函数结束时,对象obj1消失,但是临时对象将会通过语句“obj2 = funB()”赋值给对象obj2,执行完这条语句后,临时对象也自动消失了。 

4.3 浅拷贝(源自C++类对象的赋值与复制(三))

4.3.1 what

浅拷贝:就是只拷贝类中位于 stack 域中的内容,而不会拷贝 heap 域中的内容。

例如,使用类的默认的赋值运算符“=”,或默认的拷贝构造函数时,进行的对象拷贝都属于浅拷贝。这也说明,“浅拷贝”与使用哪种方式(赋值运算符或是拷贝构造函数)进行对象拷贝无关。

4.3.2 问题

浅拷贝会有一个问题,当类中存在指针成员变量时,进行浅拷贝后,目标对象与源对象的该指针成员变量将会指向同一块 heap 内存(而非每个对象单独一块内存),这就会导致由于共用该段内存而产生的内存覆盖、重复释放内存等等问题。详情可参考本系列第一章内容。

所以,对于带有指针的类对象的拷贝操作,正确的做法应当使两个对象的指针指向各自不同的内存,即在拷贝时不是简单地拷贝指针,而是将指针指向的内存中的每一个元素都进行拷贝。由此也就引出了“深拷贝”的概念。

4.4 深拷贝

深拷贝:当进行对象拷贝时,将对象位于 stack 域和 heap 域中的数据都进行拷贝。

前面也提到了,类默认提供的赋值运算符或拷贝构造函数,进行的都是浅拷贝,所以,为了实现对象的深拷贝,我们需要对赋值运算符或拷贝构造函数进行重载,以达到深拷贝的目的。

4.4.1 赋值运算符的重载

这里展示一段重载赋值运算符的示例代码,如下:

    // 重载赋值运算符
    ClassA& operaton= (ClassA& obj)
    {
        // 拷贝 stack 域的值
        m_nId = obj.m_nId;
        
        // 适应自赋值(obj = obj)操作
        if (this == &a)
        {
            return *this;
        }
        // 释放掉已有的 heap 空间
        if (m_pszName != NULL)
        {
            delete m_pszName;
        }
        // 新建 heap 空间
        m_pszName = new char[strlen(obj.m_pszName) + 1];
        // 拷贝 heap 空间的内容
        if (m_pszName != NULL)
        {
            strcpy(m_pszName, obj.m_pszName);
        }
        
        return *this;
    }
    
private:
    int m_nId;
    char* m_pszName;

4.4.2 拷贝构造函数的重载

这里展示一段重载拷贝构造函数的示例代码,如下:

    // 重载拷贝构造函数,重载后的拷贝构造函数支持深拷贝
    ClassA(ClassA &obj)
    {
        // 拷贝 stack 域的值
        m_nId = obj.m_nId;
        // 新建 heap 空间
        m_pszName = new char[strlen(obj.m_pszName) + 1];
        // 拷贝 heap 空间的内容
        if (m_pszName != NULL)
        {
            strcpy(m_pszName, obj.m_pszName);
        }
    }
    
private:
    int m_nId;
    char* m_pszName;

4.4.3 总结

从上述两个示例代码可以看出,支持深拷贝的重载赋值运算符和重载拷贝构造函数相似,但两者也存在以下区别:

  • 重载赋值运算符最好有返回值,以方便进行链式赋值(obj3=obj2=obj1),返回值类型也最好是对象的引用;而重载拷贝构造函数因为属于构造函数的一种,所以不需要返回值;
  • 重载赋值运算符首先要释放掉对象自身的 heap 空间(如果存在的话),然后再进行 heap 内容的拷贝操作;而重载拷贝构造函数无需如此,因为拷贝构造函数函数是在创建(并初始化)对象时调用的,对象此时还没有分配 heap 空间呢。
  • 如果在重载赋值运算符和重载拷贝构造函数都可以解决问题时,建议选择重载拷贝构造函数,因为貌似坑少一些^-^。

五、C++中构造函数,拷贝构造函数和赋值函数的区别和实现(源自C++中构造函数,拷贝构造函数和赋值函数的区别和实现)

C++中一般创建对象,拷贝或赋值的方式有构造函数,拷贝构造函数,赋值函数这三种方法。下面就详细比较下三者之间的区别以及它们的具体实现

5.1.构造函数

构造函数是一种特殊的类成员函数,是当创建一个类的对象时,它被调用来对类的数据成员进行初始化和分配内存。(构造函数的命名必须和类名完全相同)

首先说一下一个C++的空类,编译器会加入哪些默认的成员函数

·默认构造函数和拷贝构造函数

·析构函数

·赋值函数(赋值运算符)

·取值函数

**即使程序没定义任何成员,编译器也会插入以上的函数!

注意:构造函数可以被重载,可以多个,可以带参数;

析构函数只有一个,不能被重载,不带参数

而默认构造函数没有参数,它什么也不做。当没有重载无参构造函数时,

  A a就是通过默认构造函数来创建一个对象

下面代码为构造函数重载的实现

class A
{
int m_i;
Public:
  A() 
{
 Cout<<”无参构造函数”<

5.2.拷贝构造函数

拷贝构造函数是C++独有的,它是一种特殊的构造函数,用基于同一类的一个对象构造和初始化另一个对象。

当没有重载拷贝构造函数时,通过默认拷贝构造函数来创建一个对象

A a;

A b(a);

A b=a;  都是拷贝构造函数来创建对象b

强调:这里b对象是不存在的,是用a 对象来构造和初始化b的!!

先说下什么时候拷贝构造函数会被调用:

在C++中,3种对象需要复制,此时拷贝构造函数会被调用

1)一个对象以值传递的方式传入函数体

2)一个对象以值传递的方式从函数返回

3)一个对象需要通过另一个对象进行初始化

什么时候编译器会生成默认的拷贝构造函数:

1)如果用户没有自定义拷贝构造函数,并且在代码中使用到了拷贝构造函数,编译器就会生成默认的拷贝构造函数。但如果用户定义了拷贝构造函数,编译器就不在生成。

2)如果用户定义了一个构造函数,但不是拷贝构造函数,而此时代码中又用到了拷贝构造函数,那编译器也会生成默认的拷贝构造函数。

因为系统提供的默认拷贝构造函数工作方式是内存拷贝,也就是浅拷贝。如果对象中用到了需要手动释放的对象,则会出现问题,这时就要手动重载拷贝构造函数,实现深拷贝。

下面说说深拷贝与浅拷贝:

浅拷贝:如果复制的对象中引用了一个外部内容(例如分配在堆上的数据),那么在复制这个对象的时候,让新旧两个对象指向同一个外部内容,就是浅拷贝。(指针虽然复制了,但所指向的空间内容并没有复制,而是由两个对象共用,两个对象不独立,删除空间存在)

深拷贝:如果在复制这个对象的时候为新对象制作了外部对象的独立复制,就是深拷贝。

拷贝构造函数重载声明如下:

A (const A&other)

下面为拷贝构造函数的实现:

class A
{
  int m_i
  A(const A& other):m_i(other.m_i)
{
  Cout<<”拷贝构造函数”<

5.3.赋值函数

当一个类的对象向该类的另一个对象赋值时,就会用到该类的赋值函数。

当没有重载赋值函数(赋值运算符)时,通过默认赋值函数来进行赋值操作

A a;

A b;

b=a; 

强调:这里a,b对象是已经存在的,是用a 对象来赋值给b的!!

赋值运算的重载声明如下:

 A& operator = (const A& other)

通常大家会对拷贝构造函数和赋值函数混淆,这儿仔细比较两者的区别:

1)拷贝构造函数是一个对象初始化一块内存区域,这块内存就是新对象的内存区,而赋值函数是对于一个已经被初始化的对象来进行赋值操作。

class  A;
A a;
A b=a;   //调用拷贝构造函数(b不存在)
A c(a) ;   //调用拷贝构造函数
 
/****/
 
class  A;
A a;
A b;   
b = a ;   //调用赋值函数(b存在)

2)一般来说在数据成员包含指针对象的时候,需要考虑两种不同的处理需求:一种是复制指针对象,另一种是引用指针对象。拷贝构造函数大多数情况下是复制,而赋值函数是引用对象

3)实现不一样。拷贝构造函数首先是一个构造函数,它调用时候是通过参数的对象初始化产生一个对象。赋值函数则是把一个新的对象赋值给一个原有的对象,所以如果原来的对象中有内存分配要先把内存释放掉,而且还要检察一下两个对象是不是同一个对象,如果是,不做任何操作,直接返回。(这些要点会在下面的String实现代码中体现)

 

!!!如果不想写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,最简单的办法是将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。如:

class A
{
 private:
 A(const A& a); //私有拷贝构造函数
 A& operate=(const A& a); //私有赋值函数
}

如果程序这样写就会出错:

A a;
A b(a); //调用了私有拷贝构造函数,编译出错
 
A b;
b=a; //调用了私有赋值函数,编译出错

所以如果类定义中有指针或引用变量或对象,为了避免潜在错误,最好重载拷贝构造函数和赋值函数。

下面以string类的实现为例,完整的写了普通构造函数,拷贝构造函数,赋值函数的实现。String类的基本实现见我另一篇博文。

String::String(const char* str)    //普通构造函数
 
{
 
 cout<

一句话记住三者:对象不存在,且没用别的对象来初始化,就是调用了构造函数;

                对象不存在,且用别的对象来初始化,就是拷贝构造函数(上面说了三种用它的情况!)

                 对象存在,用别的对象来给它赋值,就是赋值函数。

以上为本人结合很多资料和图书整理出来的,将核心的点都系统的理出来,全自己按条理写的,现在大家对普通构造函数,拷贝构造函数,赋值函数的区别和实现应该都清楚了。

六、C++类禁止copy构造函数和copy assign操作符(C++类禁止copy构造函数和copy assign操作符 - 十|里 - 博客园)

在C++类中,编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。注意,这些编译器产生出来的函数都是public的,为了阻止这些函数被创建出来,我们可以把它们声明为private,这样就阻止了编译器暗自创建其对应版本函数。

class Node  
{  
public:  
    Node(int _data = 0) : data(_data) {}  
    int get() const { return data; }  
    void set(int _data) { data = _data; }  
  
private:  
    Node(const Node &);  
    Node &operator=(const Node &);  
  
    int data;  
};  

在上面的class定义中,当程序企图拷贝Node对象时,编译器就会阻止该操作。这样的话,只要将copy构造函数和copy assign操作符声明为private就可以了,还有另外一种方式,我们可以专门定义一个阻止copying动作的base class。这个base class如下所示:

class Uncopyable  
{  
protected:  
    Uncopyable() {} // 允许derived对象构造和析构  
    ~Uncopyable() {}  
private:  
    Uncopyable(const Uncopyable &); // 阻止copying  
    Uncopyable &operator=(const Uncopyable &);  
};  
  
class Node : private Uncopyable  
{  
public:  
    Node(int _data = 0) : data(_data) {}  
    int get() const { return data; }  
    void set(int _data) { data = _data; }  
  
private:  
    int data;  
};  

这样的话,在程序中,甚至在member函数或friend函数中,尝试拷贝Node对象,编译器就会试着生成一个copy构造函数或copy assign操作符,这些函数的“默认版本”会尝试调用其base class的对应函数,但是这些调用会被阻止,因为它们是private的,即阻止了该类对象的copy操作。

 

 

 

你可能感兴趣的:(#,C/C++编程语言学习之路,c++,开发语言,后端)