现在的位置: 首页 > 综合 > 正文

C++类中的4个特殊函数 && Object copy (Deep vs. Shallow vs. Lazy copy)

2018年05月12日 ⁄ 综合 ⁄ 共 17040字 ⁄ 字号 评论关闭

转载自 http://patmusing.blog.163.com/blog/static/1358349602009113061024796/ 

 

C++类中的4个特殊函数 C++类中的4个特殊函数 - 缺省构造函数、拷贝构造函数、拷贝赋值操作符和析构函数



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. 类中存在虚函数或者有虚基类的情况下需要显式声明构造函数。拷贝构造函数也是如此。

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 <iostream>
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

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

#include <iostream>
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++类中的4个特殊函数 - 玄机逸士 - 玄机逸士博客

可见,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++类中的4个特殊函数 - 玄机逸士 - 玄机逸士博客

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

3) 关于拷贝构造函数另外一个完整的例子,其中包含了copy constructor,destructor 和copy assignment operator。
#include <iostream>
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(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(!_point) delete _point;
}
 
// 需要增加的拷贝赋值函数的实现
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 = cb;                                              // 调用拷贝赋值函数
         cc.printCoordinates();
         cc.setPointValues(13, 13);
       
         ca.printCoordinates();
         cb.printCoordinates();
         cc.printCoordinates();
 
         return 0;
}

 

========================

 

Shallow Copy = Bitwise Copy

Deep Copy = Memberwise Copy
.

Long story short, a shallow copy only
copies the binary, in memory, print of a class. A deep copy “dives
into” the members, and copy their logical data. Usually, by default, it
is a Shallow/Bitwise copy.

 

对象(地址0x200000B0)被浅拷贝到另外一个对象(地址
0x200000C0),由于其中含有一个字符串对象,其地址为0x200000D0,在浅拷贝的时候,只是简单地将地址拷贝到新对象,因此原对象和新对
象所指向的内容是一样的,都是“Hello,
World”,如果新对象将该字符串进行了修改,那么原对象中对应的字符串也会发生同样的变化。而这在大部分情况下是不能接受的。

 

===============================

From Wikipedia, the free encyclopedia

 

One of the most common procedures that occurs in computer programs is the copying of data. An object
is a composite data type
in object-oriented programming
languages. Object copy thus describes the action wherein an object has its attributes
copied to another object of the same data type. An object may be copied
in order to reuse all or part of its data in a new context.

Contents

[hide
]

[edit
]

Deep vs. Shallow vs. Lazy copy

The design goal of most objects is to give the semblance of being
made out of one monolithic block even though most are not. As objects
are made up of several different parts, copying becomes non trivial.
Several strategies exist to attack this problem.

Consider two objects, A and B, which each refer
to two memory blocks xi
and yi
(i = 1, 2,...). Think of A and B as strings and of xi
and yi
(i = 1, 2,...) as the characters they contain.

Pre shallow deep copy.svg

The following paragraphs explain different strategies for copying A into B.

[edit
]

Shallow copy

One of them is the shallow copy. In this process B is attached to
the same memory block as A.This is otherwise known as address copy.

Shallow copy in progress.svg

Shallow copy done.svg

This results in a situation in which some data is shared between A
and B, thus modifying the one will alter the other. The original memory
block of B is now no longer referred to from anywhere. If the language
does not have automatic garbage collection
the original memory block of B has probably been leaked.

The advantage of shallow copies is that their execution speed is fast and does not depend on the size of the data.

Bitwise copies of objects which are not made up of a monolithic block are shallow copies.

[edit
]

Deep copy

An alternative is a deep copy. Here the data is actually copied over.

Deep copy in progress.svg

Deep copy done.svg

The result is different from the result a shallow copy gives. The
advantage is that A and B do not depend on each other but at the cost
of a slower more expensive copy.

[edit
]

Lazy copy

A lazy copy is combination of both strategies above. When initially
copying an object, a (fast) shallow copy is used. A counter is also
used to track how many objects share the data. When the program wants
to modify an object, it can determine if the data is shared (by
examining the counter) and can do a deep copy if necessary.

Lazy copy looks to the outside just as a deep copy but takes
advantage of the speed of a shallow copy whenever possible. The
downside are rather high but constant base costs because of the
counter. Also, in certain situations, circular references
can also cause problems.

Lazy copy is related to copy-on-write
.

[edit
]

How to copy objects

Nearly all object oriented
programming languages
provide some way to copy objects. As most objects are not provided by
the languages themselves, the programmer has to define how an object
should be copied, just as he or she has to define if two objects are
identical or even comparable in the first place. Many languages provide
some default behavior.

How copying is solved varies from language to language and what
concept of an object it has. The following presents examples for two of
the most widely used object-oriented languages, C++
and Java
, which should cover nearly every way that an object-oriented language can attack this problem.

[edit
]

Copying in C++

C++ is designed so that user-defined types should be able to function exactly like native types such as char
and int
. One way in which C++ achieves this is by allowing any class to define a copy constructor
and an assignment operator. C++ provides default versions of these
which perform a memberwise copy, but any class is free to disable
copying or define its own copy behaviour.

class



Vector



{






public


:






Vector(


)


{


x =


0


;


y =


0


;


}


// Standard constructor










Vector(


const


Vector&


other)


// Copy constructor






{






x =


other.x


;






y =


other.y


;






}










Vector&


operator=


(


const


Vector&


other)


// Assignment operator






{






x =


other.x


;






y =


other.y


;










return


*


this


;






}










// [Other member functions needed to make class useful]










private


:






int


x, y;






}


;










void


foo(


Vector v)






{






// [Some code here]






}










void


bar(


)






{






Vector v1;


// v1 will be constructed by the standard constructor










foo(


v1)


;


// The copy constructor will be used to create a copy of v1










Vector v2 =


v1;


// v2 will typically be constructed by the copy constructor










Vector v3;






v3 =


v1;


// The assignment operator will be used here






}






[edit
]

Another example of deep and shallow copying in C++

//DEEP and SHALLOW copy concept example:







#include <iostream>










class


base



{






public


:






int


i;










base(


)






{






i=


0


;






}














base(


int


j)






{






i=


j;






}






}


;










int


main(


)






{






using


namespace


std;










base *


p1=


new


base(


23)


;






base *


p2;






//Shallow copy






p2=


p1;


// address is copied






cout


<<


"/n


address of P1:"



<<


p1;






cout


<<


"/n


value at p1:"



<<


p1-


>


i;






cout


<<


"/n


address of P2:"



<<


p2;






cout


<<


"/n


value at p2:"



<<


p2-


>


i;






delete


p2;






cout


<<


"/n


address of P1 after delete:"



<<


p1;






cout


<<


"/n


value in P2 after delete:"



<<


p2-


>


i;


// behaviour undefined, will cause a run-time fault on many systems










//DEEP copy






base o1(


67)


;






base o2;






o2=


o1;


//contents are copied. But, the addresses remained different






cout


<<


"/n


value in o1:"



<<


o1.i


;






cout


<<


"/n


value in o2 after copy:"



<<


o2.i


<<


endl;






return


0


;






}






[edit
]

Output

address of P1:



0x00323C88



value at p1:


23



address of P2:


0x00323C88



value at p2:


23



address of P1 after delete


:


0x00323C88



value in P2 after delete


:


-


572662307






value in o1:


67






value in o2 after copy:


67






[edit
]

Copying in Java

Unlike in C++, objects in Java are always accessed indirectly through references
. Objects are never created implicitly but instead are always passed or assigned by reference. The virtual machine
takes care of garbage collection
so that objects are cleaned up after they are no longer reachable. There is no automatic way to copy any given object in Java.

Copying is usually performed by a clone() method
of a class. This method usually, in turn, calls the clone() method of
its parent class to obtain a copy, and then does any custom copying
procedures. Eventually this gets to the clone() method of Object
(the uppermost class), which creates a new instance of the same class
as the object and copies all the fields to the new instance (a "shallow
copy"). If this method is used, the class must implement the Cloneable

marker interface, or else it will throw
a CloneNotSupportedException. After obtaining a copy from the parent
class, a class's own clone() method may then provide custom cloning
capability, like deep copying (i.e. duplicate some of the structures
referred to by the object) or giving the new instance a new unique ID.

One disadvantage is that the return type of clone() is Object
, and needs to be explicitly cast back
into the appropriate type (technically a custom clone() method could
return another type of object; but that is generally inadvisable). One
advantage of using clone() is that since it is an overridable method
,
we can call clone() on any object, and it will use the clone() method
of its actual class, without the calling code needing to know what that
class is (which would be necessary with a copy constructor).

Another disadvantage is that one often cannot access the clone()
method on an abstract type. Most interfaces and abstract classes in
Java do not specify a public clone() method. As a result, often the
only way to use the clone() method is if the actual class of an object
is known, which is contrary to the abstraction principle of using the
most generic type possible. For example, if one has a List reference in
Java, one cannot invoke clone() on that reference because List
specifies no public clone() method. Actual implementations of List like
ArrayList and LinkedList all generally have clone() methods themselves,
but it is inconvenient and bad abstraction to carry around the actual
class type of an object.

Another way to copy objects in Java is to serialize
them through the Serializable

interface. This is typically used for persistence
and wire protocol
purposes, but it does create copies of objects and, unlike clone, a
deep copy that gracefully handles cycled graphs of objects is readily
available with minimal effort from the programmer.

Both of these methods suffer from a notable problem: the constructor
is not used for objects copied with clone or serialization. This can
lead to bugs with improperly initialized data, prevents the use of final

member fields, and makes maintenance challenging.

[edit
]

Copying in Eiffel

Runtime objects in Eiffel
are accessible either indirectly through references
or as expanded
objects whose fields are embedded within the objects that use them. That is, fields of an object are stored either externally or internally
.

The Eiffel class ANY
contains features for shallow and deep copying and cloning of objects. All Eiffel classes inherit from ANY
, so these features are available within all classes, and are applicable both to reference and expanded objects.

The copy
feature is shown below being applied to x
with an argument y
.

            x.copy



(


y)






This effects a shallow, field-by-field copy from the object associated with y
to the object associated with x
.

In this case no new object is created. The same objects referenced by y
before the application of copy
, will also be referenced by x
after the copy
feature completes, as in the explanation of shallow copy
above.

To effect the creation of a new object which is a shallow duplicate of y
, the feature twin
is used. Below, a new object identical to y
is created and assigned to x
.

            x :=



y.twin






The single new object is created with its fields identical to those of y
.

The feature twin
relies on the feature copy
, which can be redefined in descendants of ANY
, if necessary. The result of twin
is of the anchored type like Current
, as shown in the contract for twin
below, excerpted from class ANY
.

    frozen



twin:


like


Current






-- New object equal to `Current'






-- twin calls copy; to change copying/twining semantics, redefine copy.






ensure






twin_not_void:


Result


/=


Void






is_equal:


Result


~ Current






The anchored type declares that the type of the result of twin
will be the same as the type of the target of the call ( Current
).

Deep copying and the creation of deep twins can be done by using the features deep_copy
and deep_twin
, again inherited from class ANY
.
These features have the potential to create many new objects, because
they duplicate all the objects in an entire object structure. Because
new duplicate objects are created instead of simply copying references
to existing objects, deep operations will become a source of
performance issues more readily than shallow operations.

[edit
]

Copying in other languages

The Python
library's copy
module provides shallow copy and deep copy of objects through the copy()
and deepcopy()
functions, respectively. Programmers may define special methods __copy__()
and __deepcopy__()
in an object to provide custom copying implementation.

In PHP
, the clone
keyword performs shallow copying of an object.[1]
Programmers may define a special method __clone()
in an object to provide custom copying implementation.

In Ruby
, all objects inherit two methods for performing shallow copies, clone
and dup
. The two methods differ in that clone
copies an object's tainted state, frozen state, and any Singleton
methods it may have, whereas dup
copies only its tainted state. Deep copies may be achieved by dumping
and loading an object's byte stream or YAML serialization.[2]

In OCaml
, the library function Oo.copy
performs shallow copying of an object.

In Objective-C
, the methods copy
and mutableCopy
are inherited by all objects and intended for performing copies; the
latter is for creating a mutable type of the original object. These
methods in turn call the copyWithZone
and mutableCopyWithZone
methods, respectively, to perform the copying. An object must implement the corresponding copyWithZone
method in order for it to be copyable. To perform the actual copying, the copying function may call the NSCopyObject()
function on the object to get a byte-for-byte shallow copy.

[edit
]

See also

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

相关链接:


C++中的虚函数(virtual function)

C++虚函数表解析

C++类中的4个特殊函数 && Object copy (Deep vs. Shallow
vs. Lazy copy)

抱歉!评论已关闭.