C++ Primer 学习笔记_19_类与数据抽象(5)_初始化列表(const和引用成员)、拷贝构造函数

C++ Primer 学习笔记_19_类与数据抽象(5)_初始化列表(const和引用成员)、拷贝构造函数 

从概念上将,可以认为构造函数分为两个阶段执行:

1)初始化阶段;

2)普通的计算阶段。计算阶段由构造函数函数体中的所有语句组成。

一、构造函数初始化列表

推荐在构造函数初始化列表中进行初始化

1、对象成员及其初始化

<span style="font-size:14px;">#include <iostream>
using namespace std;

class Object
{
public:
    Object(int num) : num_(num)
    {
        cout << "Object " << num_ << " ..." << endl;
    }
    ~Object()
    {
        cout << "~Object " << num_ << " ..." << endl;
    }
private:
    int num_;
};

class Container
{
public:
    Container(int obj1 = 0, int obj2 = 0) : obj2_(obj2), obj1_(obj1)
    {
        cout << "Container ..." << endl;
    }
    ~Container()
    {
        cout << "~Container ..." << endl;
    }

private:
    Object obj1_;
    Object obj2_;
};

int main(void)
{
    Container c(10, 20);
    return 0;
}</span>

运行结果:

Object 10 ...

Object 20 ...

Container ...

~Container ...

~Object 20 ...

~Object 10 ...

解释:从输出可以看出几点,一是构造对象之前,必须先构造对象的成员;二是对象成员构造的顺序与定义时的顺序有关,跟初始化列表顺序无关;三是构造的顺序和析构的顺序相反;四是如果对象成员对应的类没有默认构造函数,那对象成员也只能在初始化列 表进行初始化。再提一点,如果类是继承而来,基类没有默认构造函数的时候,基类的构造函数要在派生类构造函数初始化列表中调用。

例子:

运行下面的C++代码,其输出结果是什么?

<span style="font-size:14px;">class A
{
private:
    int i;
    int j;
public:
    A() : j(0), i(j+2) {}
    void print() {
        cout << "i:" << i << ", j:" << j << endl;
    }
};
int main()
{
    A a;
    a.print();
    return 0;
}</span>

解答:i是一个内存中的垃圾值,而j为0。在C++中,成员变量的初始化顺序与变量在类型中的声明顺序相同,而与它们在构造函数的初始化列表的顺序无关。

2、const成员、引用成员的初始化

——const成员的初始化只能在构造函数初始化列表中进行

——引用成员的初始化也只能在构造函数初始化列表中进行

——对象成员(对象成员所对应的类没有默认构造函数)的初始化,也只能在构造函数初始化列表中进行

<span style="font-size:14px;">#include <iostream>
using namespace std;

// const成员的初始化只能在构造函数初始化列表中进行
// 引用成员的初始化也只能在构造函数初始化列表中进行
// 对象成员(对象成员所对应的类没有默认构造函数)的初始化,也只能在构造函数初始化列表中进行
class Object
{
public:
    enum E_TYPE
    {
        TYPE_A = 100,
        TYPE_B = 200
    };
public:
    Object(int num = 0) : num_(num), kNum_(num), refNum_(num_)
    {
        cout << "Object " << num_ << " ..." << endl;
    }
    ~Object()
    {
        cout << "~Object " << num_ << " ..." << endl;
    }

    void DisplayKNum()
    {
        cout << "kNum=" << kNum_ << endl;
    }
private:
    int num_;
    const int kNum_;
    int &refNum_;
};

int main(void)
{
    Object obj1(10);
    Object obj2(20);
    obj1.DisplayKNum();
    obj2.DisplayKNum();

    cout << obj1.TYPE_A << endl;
    cout << obj2.TYPE_A << endl;
    cout << Object::TYPE_A << endl;

    return 0;
}</span>

运行结果:

Object 10 ...

Object 20 ...

kNum=10

kNum=20

100

100

100

~Object 20 ...

~Object 10 ...

解释:因为const 变量或者引用都得在定义的时候初始化,所以const 成员和引用成员必须在初始化列表中初始化。另外,可以使用定义枚举类型来得到类作用域共有的常量。

二、复制构造函数

——只有单个形参,而且该形参是对本类类型对象的引用(常用const修饰),这样的构造函数称为复制构造函数。与默认构造函数一样,复制构造函数可由编译器隐式调用。

——复制构造函数、赋值操作符和析构函数总称为复制控制。

——如果类需要析构函数,则它也需要赋值操作符和复制构造函数,这是一个有用的经验法则,这个规则常称为三法则。

1、两种复制构造函数情况

(1)类类型初始化

<span style="font-size:14px;">string book1("9-999-99");  //直接初始化,不调用复制构造函数
string book2 = book1; //复制初始化
string book3(book1); //复制初始化
string book4 = "9-999-99"; //复制初始化</span>

(2)当形参或返回值为类类型时,将由复制构造函数进行复制。

<span style="font-size:14px;">#include <iostream>

using namespace std;

class Myclass
{
public:
        Myclass(int n) { number = n;}
        Myclass(const Myclass &other)
        {
                number = other.number;
                cout << "a ";
        }
private:
        int number;
};

void fun(Myclass p)
{
        Myclass temp(p);
}

int main()
{
        Myclass obj1(10), obj2(0);
        Myclass obj3(obj1);
        fun(obj3);
        return 0;
}</span>

运行结果:

a a a

解释:调用了三次拷贝构造函数,第一次时main中的Myclass obj3(obj1),第二次是实参obj3到fun形参p,第三次时函数fun中的Myclass temp(p)语句。

2、复制构造函数——形参和返回值为非引用时调用

功能:使用一个已经存在的对象来初始化一个新的同一类型的对象

声明:只有一个参数并且参数为该类对象的引用 Test::Test(const Test &other)
;

如果类中没有定义复制构造函数,则系统自动生成一个缺省复制构造函数,作为该类的公有成员,所做的事情也是简单的成员复制

<span style="font-size:14px;">#include <iostream>
using namespace std;

class Test
{
public:
    // 如果类不提供任何一个构造函数,系统将为我们提供一个不带参数的
    // 默认的构造函数
    Test();
    explicit Test(int num);
    Test(const Test &other);
    void Display();
    Test &operator=(const Test &other);
    ~Test();
private:
    int num_;
};

// 不带参数的构造函数称为默认构造函数
Test::Test() : num_(0)
{
    cout << "Initializing Default" << endl;
}

Test::Test(int num) : num_(num)
{
    cout << "Initializing " << num_ << endl;
}

Test::Test(const Test &other) : num_(other.num_)
{
    cout << "Initializing with other " << num_ << endl;
}

Test::~Test()
{
    cout << "Destroy " << num_ << endl;
}

void Test::Display()
{
    cout << "num=" << num_ << endl;
}

Test &Test::operator=(const Test &other)
{
    cout << "Test::operator=" << endl;
    if (this == &other)
        return *this;

    num_ = other.num_;
    return *this;
}

int main(void)
{
    Test t(10);
    //Test t2(t);       // 调用拷贝构造函数
    Test t2 = t;        // 等价于Test t2(t);
    cout << "........." << endl;
    return 0;
}</span>

运行结果:

Initializing 10

Initializing with other 10

.........

Destroy 10

Destroy 10

解释:即调用了拷贝构造函数,destroy 的两个分别是t 和 t2。

3、拷贝构造函数调用的几种情况

当形参为非引用类类型的时候,将复制实参的值,类似的,以非引用类类型作返回值时,将返回return语句中值的副本。

当函数的形参是类的对象,调用函数时,进行形参与实参结合时使用。这时要在内存新建立一个局部对象,并把实参拷贝到新的对象中。理所当然也调用拷贝构造函数。还有一点,为什么拷贝构造函数的参数需要是引用? 这是因为如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class),那么就相当于采用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。

当函数的返回值是类对象,函数执行完成返回调用者时使用。也是要建立一个临时对象,再返回调用者。为什么不直接用要返回的局部对象呢?

因为局部对象在离开建立它的函数时就消亡了,不可能在返回调用函数后继续生存,所以在处理这种情况时,编译系统会在调用函数的表达式中创建一个无名临时对象,该临时对象的生存周期只在函数调用处的表达式中。所谓return 对象,实际上是调用拷贝构造函数把该对象的值拷入临时对象。如果返回的是变量,处理过程类似,只是不调用构造函数。

<span style="font-size:14px;">#include <iostream>
using namespace std;

class Test
{
public:
    // 如果类不提供任何一个构造函数,系统将为我们提供一个不带参数的
    // 默认的构造函数
    Test();
    explicit Test(int num);
    Test(const Test &other);
    ~Test();
private:
    int num_;
};

// 不带参数的构造函数称为默认构造函数
Test::Test() : num_(0)
{
    cout << "Initializing Default" << endl;
}

Test::Test(int num) : num_(num)
{
    cout << "Initializing " << num_ << endl;
}

Test::Test(const Test &other) : num_(other.num_)
{
    cout << "Initializing with other " << num_ << endl;
}

Test::~Test()
{
    cout << "Destroy " << num_ << endl;
}

void TestFun(const Test t1)
{

}

void TestFun2(const Test &t1)
{

}

Test TestFun3(const Test &t1)
{
    return t1;
}

const Test &TestFun4(const Test &t1)
{
    //return const_cast<Test&>(t1);
    return t1;
}

int main(void)
{
    Test t(10);
    TestFun(t);
    cout << "........" << endl;
    return 0;
}</span>

(1)运行结果:

Initializing 10

Initializing with other 10

Destroy 10

.........

Destroy 10

解释:即在传参的时候调用了拷贝构造函数,函数返回时TestFun 的形参t 1生存期到了,在分割线输出之前销毁t1,最后destroy 的是 t。

(2)将TestFun(t); 换成 TestFun2(t);

运行结果:

Initializing 10

.........

Destroy 10

解释:参数为引用,即没有调用拷贝构造函数。

(3)将TestFun(t); 换成 t = TestFun3(t);

运行结果:

Initializing 10

Initializing with other 10

Test::operator=

Destroy 10

.........

Destroy 10

解释:从右到左的顺序,函数返回时会调用拷贝构造函数,接着调用赋值运算符,释放临时对象,最后释放t。如果没有用t 接收,不会调用operator= 而且临时对象也会马上释放。

(4)将TestFun(t); 换成 Test t2 = TestFun3(t);

运行结果:

Initializing 10

Initializing with other 10

.........

Destroy 10

Destroy 10

解释:函数返回调用拷贝构造函数,但没有再次调用拷贝构造函数,而且没有释放临时对象,可以理解成临时对象改名为t2
了。

(5)将TestFun(t);
换成const Test& t2 = TestFun3(t);

运行结果:

Initializing 10

Initializing with other 10

.........

Destroy 10

Destroy 10

解释:函数返回时调用拷贝构造函数,因为t2
引用着临时对象,故没有马上释放。

(6)将TestFun(t);
换成 Test t2 = TestFun4(t);

运行结果:

Initializing
10

Initializing with other 10

.........

Destroy 10

Destroy 10

解释:函数传参和返回都没有调用拷贝构造函数,初始化t2 时会调用拷贝构造函数。

(7)将TestFun(t);
换成 const Test&  t2 = TestFun4(t);

运行结果:

Initializing 10

.........

Destroy 10

解释:函数传参和返回都没有调用构造函数,t2 是引用故也不会调用拷贝构造函数。

4、析构函数和复制函数综合实践

【例子1】

<span style="font-size:14px;">#include <iostream>

using namespace std;

class A
{
public:
        A() {cout << "A";}
        ~A() {cout << "~A";}
};

class B
{
public:
        B(A &a): _a(a) //_a(a)调用了拷贝构造函数
        {
                cout << "B" ;
        }
        ~B() {cout << "~B";}
private:
        A _a;
};

int main()
{
        A a; //当你定义对象的时候,自动调用构造函数,输出A
        B b(a); //输出B之前调用了A的拷贝构造函数
        return 0;
}  //当定义的对象的声明周期结束,系统自动调用析构函数进行释放,顺序按照构造函数的顺序逆序进行</span>

运行结果:

AB~B~A~A

【例子2】

<span style="font-size:14px;">#include <iostream>
using namespace std;

class A
{
public:
        A() {cout << "A";}
        ~A() {cout << "~A";}
};

class B: public A   //B以公有的方法继承类A
{
public:
        B(A &a): _a(a) //_a(a)调用了拷贝构造函数
        {
                cout << "B" ;
        }
        ~B() {cout << "~B";}
private:
        A _a;
};

int main()
{
        A a; //当你定义对象的时候,自动调用构造函数,输出A
        B b(a); //输出B之前调用了A的拷贝构造函数
        return 0;
}  //当定义的对象的声明周期结束,系统自动调用析构函数进行释放,顺序按照构造函数的顺序逆序进行</span>

运行结果:

AAB~B~A~A~A。

解释:首先语句1构造一个A对象,输出A。然后语句2调用其父类的构造函数,输出A,然后B的构造函数执行如上。

参考:

C++ primer 第四版

Effective C++ 3rd

http://blog.csdn.net/jnu_simba/article/details/9173151

http://blog.csdn.net/zjf280441589/article/details/24862135

C++编程规范

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-12 22:46:01

C++ Primer 学习笔记_19_类与数据抽象(5)_初始化列表(const和引用成员)、拷贝构造函数的相关文章

C++ Primer 学习笔记_16_类与数据抽象(2)_隐含的this指针

C++ Primer 学习笔记_16_类与数据抽象(2)_隐含的this指针 1.引言 在前面提到过,成员函数具有一个附加的隐含形参,即指向该类对象的一个指针.这个隐含形参命名为this. 2.返回*this 成员函数有一个隐含的附加形参,即指向该对象的指针,这个隐含的形参叫做this指针(编译器自动传递)使用this指针保证了每个对象可以拥有不同数值的数据成员,但处理这些成员的代码可以被所有对象共享.成员函数是只读的代码,由所有对象共享,并不占对象的存储空间,因为this指针指向当前对象,所以

C++ Primer 学习笔记_17_类与数据抽象(3)_类作用域

C++ Primer 学习笔记_17_类与数据抽象(3)_类作用域 引言: 每个类都定义了自己的新作用域与唯一的类型.即使两个类具有完全相同的成员列表,它们也是不同的类型.每个类的成员不同与任何其他类(或任何其他作用域)的成员. 一.类作用域中的名字查找 1)首先,在使用该名字的块中查找名字的声明.只考虑在该项使用之前声明的名字. 2)如果在1)中找不到,则在包围的作用域中查找. 如果找不到任何声明,则出错. 类的定义实际上是在两个阶段中处理: 1)首先,编译器声明: 2)只有在所有成员出现之后

C++ Primer 学习笔记_15_类与数据抽象(1)_类的定义和声明

C++ Primer 学习笔记_15_类与数据抽象(1)_类的定义和声明 在C++中,用类来定义自己的抽象数据类型.通过定义类型来对应所要解决的问题中的各种概念,可以使我们更容易编写.调试和修改程序.可以使得自己定义的数据类型用起来与内置类型一样容易和直观. 看一下Sales_item类: class Sales_item { private: std::string isbn; unsigned units_sold; double revenue; public: double ave_pr

C++ Primer 学习笔记_20_类与数据抽象(6)_深拷贝与浅拷贝、空类与空数组

C++ Primer 学习笔记_20_类与数据抽象(6)_深拷贝与浅拷贝.空类与空数组 一.深拷贝与浅拷贝 浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象.换而言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象. 深拷贝:被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量.那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象.换而言之,深拷贝把要复制的对象所引用的对象都复制了一遍. 浅拷贝可

C++ Primer 学习笔记_18_类与数据抽象(4)_构造函数、析构函数、explicit关键字、赋值与初始化、类成员的显式初始化

引言: 构造函数确保每个对象在创建时自动调用,以确保每个对象的数据成员都有合适的初始值. 一.构造函数.默认构造函数 1.构造函数 --构造函数是特殊的成员函数 --构造函数是为了保证对象的每个数据成员都被正确初始化 --函数名和类名完全相同 --不能定义构造函数的类型(返回类型),也不能使用void --通常情况下构造函数应声明为公有函数,一般被隐式地调用. --构造函数被声明为私有有特殊的用途,比如单例模式. (1).构造函数可以被重载 一般而言,不同的构造函数允许用户指定不同的方式来初始化

C++ Primer 学习笔记_56_类与数据抽象 --消息处理示例

复制控制 --消息处理示例 说明: 有些类为了做一些工作需要对复制进行控制.为了给出这样的例子,我们将概略定义两个类,这两个类可用于邮件处理应用程序.Message类和 Folder类分别表示电子邮件(或其他)消息和消息所出现的目录,一个给定消息可以出现在多个目录中.Message上有 save和 remove操作,用于在指定Folder中保存或删除该消息. 数据结构: 对每个Message,我们并不是在每个Folder中都存放一个副本,而是使每个Message保存一个指针集(set),set中

C++ Primer 学习笔记_57_类与数据抽象 --管理指针成员

复制控制 --管理指针成员 引言: 包含指针的类需要特别注意复制控制,原因是复制指针时只是复制了指针中的地址,而不会复制指针指向的对象! 将一个指针复制到另一个指针时,两个指针指向同一对象.当两个指针指向同一对象时,可能使用任一指针改变基础对象.类似地,很可能一个指针删除了一对象时,另一指针的用户还认为基础对象仍然存在.指针成员默认具有与指针对象同样的行为. 大多数C++类采用以下三种方法之一管理指针成员: 1)指针成员采取常规指针型行为:这样的类具有指针的所有缺陷但无需特殊的复制控制! 2)类

C++ Primer 学习笔记_55_类与数据抽象 --析构函数

复制控制 --析构函数 引言: 在构造函数中分配了资源之后,需要一个对应操作自动回收或释放资源.析构函数就是这样的一个特殊函数,它可以完成所需的资源回收,作为类构造函数的补充. 1.何时调用析构函数 撤销类对象时会自动调用析构函数: Sales_item *p = new Sales_item; { Sales_item item(*p); //调用复制构造函数 delete p; //调用指针p的析构函数 } //调用对象item的析构函数 动态分配的对象只有在指向该对象的指针被删除时才撤销,

C++ Primer 学习笔记_53_类与数据抽象 --友元、static成员

类 --友元.static成员 一.友元 友元机制允许一个类将对其非公有成员的访问权授予指定的函数或类(对未被授权的函数或类,则阻止其访问):友元的声明以关键字friend开始,但是它只能出现在类定义的内部.友元声明可以出现在类中的任何地方:友元不是授予友元关系的那个类的成员,所以它们不受其声明出现部分的访问控制影响. [最佳实践] 通常,将友元声明成组的放在类定义的开始或结尾是个好主意! 1.友元关系:一个例子 假设一个窗口管理类Window_Mgr可能需要访问由其管理的Screen对象的内部