C++标准ISO-IEC-14882-2003:第12章:类的特殊成员函数-第8节:拷贝对象

12.8 拷贝对象

1.      一个对象有两种拷贝的途径:初始化(包括函数参数传递、返回值)和赋值。概念上,这两种操作都是通过拷贝构造函数和赋值操作符实现的。

2.      如果类 X 非模板构造函数的第一个参数类型是 X& const X& volatile X& const volatile X& ,且没有其他参数或其他参数有默认值,那么该构造函数就是个拷贝构造函数。 【例如: X::X(const X&) X::X(X&, int=1) 都是拷贝构造函数。

class X {

// ...

public:

X(int);

X(const X&, int = 1);

};

X a(1); // 调用 X(int);

X b(a, 0); // 调用 X(const X&, int);

X c = b; // 调用 X(const X&, int);

】【注:可以同时声明各种各样的拷贝构造函数。】【例

class X {

// ...

public:

X(const X&);

    X(X&); // OK

}; 】【注:如果一个类 X 仅有一个参数为 X& 的拷贝构造函数, const X volatile X 类型的初始化器不能用来初始化该类 X 的对象(不论是否 cv 限定)。【例:

struct X {

X(); // 默认构造函数

X(X&); // const 类型参数的拷贝构造函数

};

const X cx;

X x = cx; // 错误 X::X(X&) 不能把 cx 拷贝给 x

】】

【按:因为这种构造函数没有任何存在的意义;如果是内置类型的这种重载,倒是还有存在的意义】

3.         X 的构造函数的第一个参数如果是 X 类型并且没有其他参数,或其他参数都有默认值,那么这个声明是非法的。永远不能实例化试图拷贝自己所在类的对象的模板成员函数。【例:

struct S {

template<typename T> S(T);

};

S f();

void g() {

S a( f() ); // 不能实例化成员模板

}

4.         如果用户没有显式声明拷贝构造函数,编译器会隐式地声明一个。这样,如下形式的定义:

struct X {

X(const X&, int);

};

会隐式地声明一个拷贝构造函数。如果用户声明的构造函数后来定义为:

X::X(const X& x, int i =0) { /* ... */ }

    那么,在使用拷贝构造函数的地方就会出现非法的二义性;这种情况不需要诊断信息

5.         X 的隐式拷贝构造函数是 X::X(const X&) 这种形式,如果它满足以下条件:

a)         X 的每个直接基类或虚基类 B 都有一个第一个参数是 const B& const volatile B& 的拷贝构造函数,并且

b)        对于 X 的每个类型为 M 类(或对象数组)的非静态数据成员,每个这样的类都有一个第一个参数是 const M& const volatile M& 的拷贝构造函数。

否则, X 的隐式拷贝构造函数会是 X::X(X&) 这种形式。

隐式拷贝构造函数是 inline public 属性的。

6.         X 隐式声明的拷贝构造函数满足如下条件就被称为是 trivial 的:

a)         X 没有虚函数,没有虚基类,并且

b)        每个直接基类都有一个 trivial 的拷贝构造函数,并且

c)        对于 X 的每个类型为类(或对象数组)的非静态数据成员都有一个 trivial 的拷贝构造函数;

否则,拷贝构造函数就不是 trivial 的。

7.         如果使用隐式声明的拷贝构造函数来初始化该类或其继承类的一个对象,它会被隐式地定义。【注:在某些情况下编译器会做优化而不使用拷贝构造函数,但它仍然被隐式定义。】隐式定义拷贝构造函数的类如果有以下情况之一,程序就是非法的:

a)         类的非静态类成员(或对象数组)的拷贝构造函数无法访问或有二义性

b)        基类的拷贝构造函数无法访问或有二义性

在拷贝构造函数隐式定义之前,该类的所有直接基类和虚基类的拷贝构造函数都必须已经隐式定义。【注:隐式拷贝构造函数有一个异常说明。】

8.         隐式定义的拷贝构造函数对其子对象执行按成员拷贝。拷贝的顺序与用户自定义的构造函数中的初始化顺序一致。每个子对象都按照适合它们的类型来拷贝:

a)         如果子对象是类,就使用该类的拷贝构造函数;

b)        如果子对象是数组,每个元素按照其合适的类型拷贝;

c)        如果子对象是标量,就使用内建的赋值操作符。

隐式定义的拷贝构造函数对虚基类子对象只能拷贝一次。

9.         用户声明的拷贝赋值操作符 X::operator= X 的非静态、非模板成员函数,只有一个参数,为 X X& const X volatile X& 、或者 const volatile X& 。【注:赋值操作符只能重载为一个参数的函数,参加 13.5.3 】【注:可以声明多个拷贝赋值操作符】【注:如果用户声明的拷贝赋值操作符的参数类型为 X& const X 类型的表达式不能赋值给 X 类型的对象。【例:

struct X {

X();

X& operator=(X&);

};

const X cx;

X x;

void f() {

x = cx; // 错误 : X::operator=(X&) 不能将 cx 赋值给 x

}

10.     如果类定义没有显式声明一个拷贝赋值操作符,编译器会隐式声明一个。形式如下为: X::operator=(const X&) ,如果满足以下条件:

a)         X 的每个直接基类或虚基类 B 都有一个第一个参数是 const B& const volatile B& 的拷贝赋值操作符,并且

b)        对于 X 的每个类型为 M 类(或对象数组)的非静态数据成员,每个这样的类都有一个第一个参数是 const M& const volatile M& 的拷贝赋值操作符。

否则, X 的隐式拷贝赋值操作符会是 X::operator=(X&) 这种形式。

X 的隐式拷贝赋值操作符返回类型是 X& ,返回调用者那个对象,即用来给别人赋值的对象。隐式拷贝赋值操作符的属性是 inline public 。如果用户不声明的话,拷贝赋值操作符总是被隐式声明,所以基类的拷贝赋值操作符总是被派生类的隐藏。使用 using-declaration 从基类介绍进来的带一个参数的赋值操作符,看起来可以作为派生类的拷贝赋值操作符,却不会被认为是拷贝赋值操作符的显式声明,因而也不会阻止隐式拷贝赋值操作符的声明; using-declaration 介绍进来赋值操作符会被派生类的隐式拷贝赋值操作符隐藏掉。

11.     X 的隐式拷贝赋值操作符如果满足以下条件就被认为是 trivial 的:

a)         X 没有虚函数,没有虚基类,并且

b)        每个直接基类都有一个 trivial 的拷贝赋值操作符,并且

c)        对于 X 的每个类型为类(或对象数组)的非静态数据成员都有一个 trivial 的拷贝赋值操作符;

否则,拷贝赋值操作符就不是 trivial 的。

12.     如果使用隐式声明的拷贝赋值操作符来初始化该类或其继承类的一个对象,它会被隐式地定义。隐式定义拷贝赋值操作符的类如果有以下情况之一,程序就是非法的:

a)         有一个非静态的 const 数据成员

b)        有一个非静态的引用类型的数据成员

c)        类的非静态类成员(或对象数组)的拷贝赋值操作符无法访问或有二义性

d)        基类的拷贝赋值操作符无法访问

在拷贝赋值操作符隐式定义之前,该类的所有直接基类和虚基类的拷贝赋值操作符都必须已经隐式定义。【注:隐式拷贝赋值操作符有一个异常说明。】

13.     隐式定义的拷贝构造函数对其子对象执行按成员拷贝。直接基类子对象以它们在继承列表中的出现顺序先拷贝,接着是非静态数据成员以他们声明的顺序拷贝。每个子对象都按照适合它们的类型来拷贝:

a)         如果子对象是类,就使用该类的拷贝赋值操作符(就好像被显式地限定了一样,即忽略任何可能的来自更继承类的虚函数覆盖);

b)        如果子对象是数组,每个元素按照其合适的类型拷贝;

c)        如果子对象是标量,就使用内建的赋值操作符。

虚基类的子对象被隐式的赋值操作符赋值多少次是没有定义的。【例:

struct V { };

struct A : virtual V { };

struct B : virtual V { };

struct C : B, A { };

V 的子对象是否被 C 的赋值操作符赋值赋值了 2 次是没有定义的。】

14.     对象的拷贝构造函数、拷贝赋值函数被隐式调用但是这些特殊函数却没有访问权限,那么程序是非法的。【注:使用拷贝构造函数、拷贝赋值函数将一个对象拷贝到另一个对象是不会改变任何对象的内存布局的。】

15.     当某些条件满足时,允许编译器省略拷贝构造函数,即使该对象的拷贝构造函数 / 析构函数有副作用。这种情况下,编译器将省略了拷贝操作的源和目的对象看做是访问同一个对象的不同方法;对象的销毁时机是在:如果不做优化,最后一个对象销毁的时候。在下述情况中,拷贝构造函数是允许省略的(为了消除多次拷贝):

a)         函数的返回类的 return 语句,当表达式是 non-volatile 的自动对象的名字,且返回类型的 cv 限定性与自动对象相同时,拷贝构造函数可以省略,此时自动对象被直接在返回值处构造

b)        当一个还没被引用绑定的临时对象要被拷贝到一个同样 cv 限性定的对象时,拷贝构造函数可以省略,此时临时对象被直接在目标对象处构造

【例:

       class Thing {

public:

Thing();

˜Thing();

Thing(const Thing&);

};

Thing f() {

Thing t;

return t;

}

Thing t2 = f();

这里,省略的条件可以被合并,这样就可以避免 2 次调用 Thing 类的拷贝构造函数:本地自动对象 t 拷贝到 return 的临时对象;临时对象拷贝到 t2 中。其实站在效率的角度来说,本地自动对象 t 可以看做是用来直接初始化全局对象 t2 的,然后 t 在程序退出时销毁。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值