C++基础教程面向对象(学习笔记(29))

拷贝构造函数

重新初始化类型

由于我们将在接下来的几节课中谈论很多初始化,让我们首先回顾一下C ++支持的初始化类型:直接初始化,统一初始化或拷贝初始化。

以下是使用我们的Fraction类的所有示例:

#include <cassert>
#include <iostream>
 
class Fraction
{
private:
    int m_numerator;
    int m_denominator;
 
public:
    //默认构造函数 
    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 x(5); // 用一个整数直接初始化
Fraction fiveThirds(5, 3); // 直接初始化一个Fraction, 调用Fraction(int, int) 构造函数

在C ++ 11中,我们可以进行统一初始化:

int x { 5 }; //用一个整数统一初始化
Fraction fiveThirds {5, 3}; // 统一初始化一个Fraction, 调用Fraction(int, int) 构造函数

最后,我们可以进行拷贝初始化:

int x = 6; // 用一个整数复制初始化
Fraction six = Fraction(6); // 复制初始化一个Fraction,将调用Fraction(6,1)
Fraction seven = 7; //复制初始化Fraction。编译器将尝试找到将7转换为Fraction的方法,该方法将调用Fraction(7,1)构造函数。

通过直接和统一的初始化,直接初始化正在创建的对象。但是,复制初始化稍微复杂一些。我们将在下一课中更详细地探讨复制初始化。但为了有效地做到这一点,我们需要先放下这一点。

拷贝构造函数

现在考虑以下程序:

#include <cassert>
#include <iostream>
 
class Fraction
{
private:
    int m_numerator;
    int m_denominator;
 
public:
    // 默认构造函数
    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 fiveThirds(5, 3); // 直接初始化一个Fraction, 调用Fraction(int, int) 构造函数
	Fraction fCopy(fiveThirds); // 直接初始化 -- 调用什么构造函数呢?
	std::cout << fCopy << '\n';
}

如果你编译这个程序,你会看到它编译得很好,并产生结果:
5/3
让我们仔细看看这个程序是如何工作的。
变量fiveThirds的初始化只是一个标准的直接初始化,它调用Fraction(int,int)构造函数。非常正常。但下一行怎么样?变量fCopy的初始化显然也是直接初始化,你知道构造函数用于初始化类。那么这行调用的构造函数是什么?

答案是这行是调用Fraction的拷贝构造函数。一个拷贝构造函数是用来创建一个新的对象,为现有对象的拷贝构造函数的一种特殊类型。与默认构造函 数非常相似,如果您没有为类提供拷贝构造函数,C ++将为您创建一个公共复制构造函数。由于编译器对您的类知之甚少,因此默认情况下,创建的拷贝构造函数使用称为成员初始化的初始化方法。 成员初始化只是意味着副本的每个成员都直接从被拷贝的类的成员初始化。在上面的例子中,fCopy.m_numerator将从fiveThirds.m_numerator等初始化…

就像我们可以显式定义默认构造函数一样,我们也可以显式定义拷贝构造函数。拷贝构造函数看起来就像你期望的那样:

#include <cassert>
#include <iostream>
 
class Fraction
{
private:
    int m_numerator;
    int m_denominator;
 
public:
    // 默认构造函数
    Fraction(int numerator=0, int denominator=1) :
        m_numerator(numerator), m_denominator(denominator)
    {
        assert(denominator != 0);
    }
 
    // 拷贝构造函数
    Fraction(const Fraction &fraction) :
        m_numerator(fraction.m_numerator), m_denominator(fraction.m_denominator)
        // 注意:我们可以直接访问参数fraction的成员,因为我们在Fraction类中
    {
        //这里不需要检查0的分母,因为分数必须已经是有效分数
        std::cout << "Copy constructor called\n"; // just to prove it works
    }
 
    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 fiveThirds(5, 3); // 直接初始化一个Fraction, 调用Fraction(int, int) 构造函数
	Fraction fCopy(fiveThirds); // 直接初始化 -- 用Fraction拷贝构造函数?
	std::cout << fCopy << '\n';
}

运行此程序时,您将获得:
Copy constructor called
5/3
我们在上面的例子中定义的拷贝构造函数使用成员初始化,并且在功能上等同于我们默认获得的拷贝构造函数,除了我们添加了一个输出语句来说明正在调用拷贝构造函数。
与默认构造函数(您应该始终提供自己的默认构造函数)不同,如果满足您的需要,可以使用默认的拷贝构造函数。
一个有趣的注意事项:您已经看到了一些重载operator <<的例子,我们可以访问参数f1的私有成员,因为该函数是Fraction类的友元函数。类似地,类的成员函数可以访问相同类类型的参数的私有成员。由于我们的Fraction复制构造函数接受类类型的参数(以复制),我们能够直接访问参数fraction的成员,即使它不是隐式对象。

防止复制

我们可以通过将拷贝构造函数设为私有来防止产生的类的副本:

#include <cassert>
#include <iostream>
 
class Fraction
{
private:
    int m_numerator;
    int m_denominator;
 
    // 拷贝构造函数(私有)
    Fraction(const Fraction &fraction) :
        m_numerator(fraction.m_numerator), m_denominator(fraction.m_denominator)
    {
        // 这里不需要检查0的分母,因为副本必须已经是有效的分数
        std::cout << "Copy constructor called\n"; // 仅仅证明它在工作
    }
 
public:
    // 默认构造函数
    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 fiveThirds(5, 3); // 直接初始化一个Fraction,调用Fraction(int,int)构造函数
	Fraction fCopy(fiveThirds); // 拷贝构造函数是私有的,在这一行编译错误
	std::cout << fCopy << '\n';
}

现在,当我们尝试编译程序时,我们将得到编译错误,因为fCopy需要使用拷贝构造函数,但由于拷贝构造函数已声明为私有,因此无法看到它。

可以省略拷贝构造函数

现在考虑以下示例:

#include <cassert>
#include <iostream>
 
class Fraction
{
private:
	int m_numerator;
	int m_denominator;
 
public:
    // 默认构造函数
    Fraction(int numerator=0, int denominator=1) :
        m_numerator(numerator), m_denominator(denominator)
    {
        assert(denominator != 0);
    }
 
        // 拷贝构造函数
	Fraction(const Fraction &fraction) :
		m_numerator(fraction.m_numerator), m_denominator(fraction.m_denominator)
	{
		// 这里不需要检查0的分母,因为副本必须已经是有效的分数
		std::cout << "Copy constructor called\n"; // just to prove it works
	}
 
	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 fiveThirds(Fraction(5, 3));
	std::cout << fiveThirds;
	return 0;
}

考虑一下该程序的工作原理 首先,我们使用Fraction(int,int)构造函数直接初始化一个匿名Fraction对象。然后我们使用该匿名Fraction对象作为Fraction fiveThirds的初始化器。由于匿名对象是一个Fraction,这应该调用拷贝构造函数,对吗?

运行它并自己编译。你可能希望得到这个结果(你可以):

Copy constructor called
5/3
但实际上,你更有可能(但不能保证)得到这个结果:
5/3
为什么我们的复制构造函数没有被调用?

请注意,初始化匿名对象然后使用该对象来指导初始化我们定义的对象需要两个步骤(一个用于创建匿名对象,一个用于调用拷贝构造函数)。但是,最终结果基本上与仅进行直接初始化相同,只需要一步。

因此,在这种情况下,允许编译器选择不调用拷贝构造函数,而只是进行直接初始化。这个过程叫做elision。

所以尽管你写道:

Fraction fiveThirds(Fraction(5, 3));

编译器可能会将其更改为:

Fraction fiveThirds(5, 3);

它只需要一个构造函数调用(到Fraction(int,int))。请注意,在使用elision的情况下,拷贝构造函数体中的任何语句都不会执行,即使它们会产生其他作用作用(如打印到屏幕上)!

最后,请注意,如果您将拷贝构造函数设为private,那么使用拷贝构造函数的任何初始化都将导致编译错误,即使拷贝构造函数被省略了!

猜你喜欢

转载自blog.csdn.net/qq_41879485/article/details/83243179