copy initialization
14.13 - 拷贝初始化
Key Takeaway -
考虑下面代码:
该语句使用拷贝初始化的方式将一个新创建的整型变量 x 的值设置为 5。
而对于类来说,问题则会变得稍微复杂一些,因为类在初始化时需要使用构造函数。本节课我们会介绍与类的拷贝初始化相关的问题。
类的拷贝初始化
考虑下面的 Fraction
类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 #include <cassert>
#include <iostream>
class Fraction
{
private :
int m_numerator ;
int m_denominator ;
public :
// Default constructor
Fraction ( int numerator = 0 , int denominator = 1 )
: m_numerator ( numerator ), m_denominator ( denominator )
{
assert ( denominator != 0 );
}
friend std :: ostream & operator << ( std :: ostream & out , const Fraction & f1 );
};
std :: ostream & operator << ( std :: ostream & out , const Fraction & f1 )
{
out << f1 . m_numerator << '/' << f1 . m_denominator ;
return out ;
}
考虑下下面代码:
int main ()
{
Fraction six = Fraction ( 6 );
std :: cout << six ;
return 0 ;
}
编译运行代码,结果如你所以想的那样:
这种形式的拷贝构造,其求值方式和下面的代码是一样的:
Fraction six ( Fraction ( 6 ));
在上一节课中(14.12 - 拷贝构造函数 )我们学到,上面的代码可能会调用 Fraction(int, int)
以及 Fraction
的拷贝构造函数(可能会被优化掉)。不过,由于省略不被保证一定发生,所以最好避免使用拷贝初始化来初始化类,而改用统一初始化 。
拷贝初始化的其他使用场景
还有其他一些地方使用了拷贝初始化,其中有两个地方值得一提。当按值传递 或按值返回 类时,会使用拷贝初始化。
考虑下面代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49 #include <cassert>
#include <iostream>
class Fraction
{
private :
int m_numerator ;
int m_denominator ;
public :
// Default constructor
Fraction ( int numerator = 0 , int denominator = 1 )
: m_numerator ( numerator ), m_denominator ( denominator )
{
assert ( denominator != 0 );
}
// Copy constructor
Fraction ( const Fraction & copy ) :
m_numerator ( copy . m_numerator ), m_denominator ( copy . m_denominator )
{
// no need to check for a denominator of 0 here since copy must already be a valid Fraction
std :: cout << "Copy constructor called \n " ; // just to prove it works
}
friend std :: ostream & operator << ( std :: ostream & out , const Fraction & f1 );
int getNumerator () { return m_numerator ; }
void setNumerator ( int numerator ) { m_numerator = numerator ; }
};
std :: ostream & operator << ( std :: ostream & out , const Fraction & f1 )
{
out << f1 . m_numerator << '/' << f1 . m_denominator ;
return out ;
}
Fraction makeNegative ( Fraction f ) // ideally we should do this by const reference
{
f . setNumerator ( - f . getNumerator ());
return f ;
}
int main ()
{
Fraction fiveThirds ( 5 , 3 );
std :: cout << makeNegative ( fiveThirds );
return 0 ;
}
在上面的例子中,函数 makeNegative
将 Fraction
类型的对象作为参数,同时返回按值返回 Fraction
对象。程序执行后,输出结果如下:
Copy constructor called
Copy constructor called
-5/3
第一次调用拷贝构造函数是在 fiveThirds
被传入 makeNegative()
作为形参 f
的时候。第二次调用是在 makeNegative()
按值返回时。
在上面的例子中,实参和返回值都是按值 传递或返回的,所以拷贝构造不会被省略。但是,在其他的一些场合,如果实参和返回值满足某些条件,编译器仍然可能省略拷贝构造函数。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 #include <iostream>
class Something
{
public :
Something () = default ;
Something ( const Something & )
{
std :: cout << "Copy constructor called \n " ;
}
};
Something foo ()
{
return Something (); // copy constructor normally called here
}
Something goo ()
{
Something s ;
return s ; // copy constructor normally called here
}
int main ()
{
std :: cout << "Initializing s1 \n " ;
Something s1 = foo (); // copy constructor normally called here
std :: cout << "Initializing s2 \n " ;
Something s2 = goo (); // copy constructor normally called here
}
The above program would normally call the copy constructor 4 times -- however, due to copy elision, it’s likely that your compiler will elide most or all of the cases. Visual Studio 2019 elides 3 (it doesn’t elide the case where goo() is returned), and GCC elides all 4
上面的程序通常会调用拷贝构造函数4次——然而,由于省略,编译器很可能会省略大部分或所有调用。Visual Studio 2019省略了3个(它没有省略返回goo()
的情况),而GCC省略了所有4个。