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

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

一、构造函数初始化列表
1、 推荐在构造函数初始化列表中进行初始化
(1)示例:更改之前的Clock代码
//Clock.h
//#pragma once
#ifndef _CLOCK_H_
#define _CLOCK_H_
class Clock
{
public:
 Clock(int hour=0, int minute=0, int second=0);
 ~Clock();
 void Display();
 void Update();
 int GetHour();
 int GetMinute();
 int GetSecond();
 void SetHour(int hour);
 void SetMinute(int minute);
 void SetSecond(int second);
private:
 int hour_;
 int minute_;
 int second_;
};
#endif // _CLOCK_H_
//Clock.cpp
#include "Clock.h"
#include <iostream>
using namespace std;
void Clock::Display()
{
 cout<<hour_<<":"<<minute_<<":"<<second_<<endl;
}
Clock::Clock(int hour/* =0 */, int minute/* =0 */, int second/* =0 */) : hour_(hour),
 minute_(minute), second_(second)
{
 //hour_ = hour;
 //minute_ = minute;
 //second_ = second;
 cout<<"Clock::Clock"<<endl;
}
Clock::~Clock()
{
 cout<<"Clock::~Clock"<<endl;
}
void Clock::Update()
{
 second_++;
 if (second_ == 60)
 {
  minute_++;
  second_ = 0;
 }
 if (minute_ == 60)
 {
  hour_++;
  minute_ = 0;
 }
 if (hour_ == 24)
 {
  hour_ = 0;
 }
}
int Clock::GetHour()
{
 return hour_;
}
int Clock::GetMinute()
{
 return minute_;
}
int Clock::GetSecond()
{
 return second_;
}
void Clock::SetHour(int hour)
{
 hour_ = hour;
}
void Clock::SetMinute(int minute)
{
 minute_ = minute;
}
void Clock::SetSecond(int second)
{
 second_ = second;
}
//01.cpp
#include "Clock.h"
int main(void)
{
 Clock c(10, 10, 10);
 c.Display();
 return 0;
}


2、构造函数的执行分为两个阶段
(1)初始化段
(2)普通计算段

(3)示例

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

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;
}

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



二、构造函数初始化列表——对象成员及其初始

1、示例
//02.cpp
#include <iostream>
using namespace std;
class Object
{
public:
 Object()
 {
  cout<<"Object..."<<endl;
 }
 ~Object()
 {
  cout<<"~Object..."<<endl;
 }
private:
};
class Container
{
public:
 Container()
 {
  cout<<"Container ..."<<endl;
 }
 ~Container()
 {
  cout<<"~Container ..."<<endl;
 }
private:
 Object obj_;
};
int main(void)
{
 Container c;
 return 0;
}
运行结果:
Object...
Container ...
~Container ...
~Object...
解释:构造对象之前,必须先构造对象的成员;析构时,先析构后构造的对象。


2、示例
//03.cpp
#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;
}
运行结果:

Object 10 ...

Object 20 ...

Container ...

~Container ...

~Object 20 ...

~Object 10 ...

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


3、再修改上述示例
int main(void)
{
    Container c;
    return 0;
}
运行结果:

Object 0 ...

Object 0 ...

Container ...

~Container ...

~Object 0 ...

~Object 0 ...



三、const成员、引用成员的初始化
1、const成员的初始化只能在构造函数初始化列表中进行
2、引用成员的初始化也只能在构造函数初始化列表中进行
3、对象成员(对象成员所对应的类没有默认构造函数“03.cpp”)的初始化,也只能在构造函数初始化列表中进行
4、示例

//04.cpp
#include <iostream>
using namespace std;

class Object
{
public:
    enum E_TYPE  //用枚举实现,对于所有类的对象都一样
    {
        TYPE_A = 100,
        TYPE_B = 200
    };
public:
    Object(int num = 0) : num_(num), kNum_(num), refNum_(num_)
    {
        //kNum_ = 100;  //不能初始化
        //refNum_ = num_;  //不能初始化
        cout << "Object " << num_ << " ..." << endl;
    }
    ~Object()
    {
        cout << "~Object " << num_ << " ..." << endl;
    }
    void DisplayKNum()
    {
        cout << "kNum=" << kNum_ << endl;
    }
private:
    int num_;
    const int kNum_; //const成员
    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;
}

运行结果:

Object 10 ...

Object 20 ...

kNum=10

kNum=20

100

100

100

~Object 20 ...

~Object 10 ...

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



四、 拷贝构造函数
1、定义
(1)功能:使用一个已经存在的对象来初始化一个新的同一类型的对象
(2)声明: 只有一个参数并且参数为该类对象的引用 ( 常用 const 修饰 )
(3)如果类中没有说明拷贝构造函数,则 系统自动生成一个缺省复制构造函数,作为该类的公有成员
(4)示例

//Test.h
#ifndef _TEST_H_
#define _TEST_H_
class Test
{
public:
 // 如果类不提供任何一个构造函数,系统将为我们提供一个不带参数的
 // 默认的构造函数
 Test();
 explicit Test(int num);
 Test(const Test& other);
 void Display();
 Test& operator=(const Test& other);
 ~Test();
private:
 int num_;
};
#endif // _TEST_H_
//Test.cpp
#include "Test.h"
#include <iostream>
using namespace std;
// 不带参数的构造函数称为默认构造函数
Test::Test() : num_(0)
{
 //num_ = 0;
 cout<<"Initializing Default"<<endl;
}
Test::Test(int num) : num_(num)
{
 //num_ = num;
 cout<<"Initializing "<<num_<<endl;
}
Test::Test(const Test& other) : num_(other.num_)
{
 //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;
}
//05.cpp
#include "Test.h"
int main(void)
{
 Test t(10);
 //Test t2(t);	 // 调用拷贝构造函数
 Test t2 = t;	 // 等价于Test t2(t);
 cout << "........" << endl;
 return 0;
}
//05.cpp
#include "Test.h"
int main(void)
{
 Test t(10);
 //Test t2(t);	 // 调用拷贝构造函数
 Test t2 = t;	 // 等价于Test t2(t);
 cout << "........" << endl;
 return 0;
}
运行结果:
Initializing 10
Initializing with other 10
........
Destroy 10
Destroy 10
解释:即调用了拷贝构造函数,destroy 的两个分别是t 和 t2。


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

2、两种复制构造函数情况(一个对象初始化另一个对象的时候,会调用拷贝构造函数

(1)类类型初始化

string book1("9-999-99");  //直接初始化,不调用复制构造函数
string book2 = book1; //复制初始化
string book3(book1); //复制初始化
string book4 = "9-999-99"; //复制初始化

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

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;
}

运行结果:

a a a

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


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

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

【1】示例

上述的Test.h和Test.cpp不变,修改05.cpp为

//06.cpp
#include "Test.h"
#include <iostream>
using namespace std;
void TestFun(const Test t)
{
}
void TestFun2(const Test& t)
{
}
Test TestFun3(const Test& t)
{
 return t;
}
const Test& TestFun4(const Test& t)
{
 //return const_cast<Test&>(t);
 return t;
}
int main(void)
{
 Test t(10);
 TestFun(t);
 cout<<"........"<<endl;
 return 0;
}

运行结果:

Initializing 10

Initializing with other 10

Destroy 10

.........

Destroy 10

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


(2)当函数的形参是类的对象的引用,不调用拷贝构造函数

【1】示例

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

运行结果:

Initializing 10
.........
Destroy 10

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


(3)当函数的形参是类的对象的引用,函数的返回值是类对象,函数执行完成返回调用者时使用。理由也是要建立一个临时对象中,再返回调用者。为什么不直接用要返回的局部对象呢?因为局部对象在离开建立它的函数时就消亡了,不可能在返回调用函数后继续生存,所以在处理这种情况时,编译系统会在调用函数的表达式中创建一个无名临时对象,该临时对象的生存周期只在函数调用处的表达式中。所谓return 对象,实际上是调用拷贝构造函数把该对象的值拷入临时对象。如果返回的是变量,处理过程类似,只是不调用构造函数。

【1】示例

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

运行结果:

Initializing 10
Initializing with other 10
Test::operator=
Destroy 10
.........
Destroy 10

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


(4)当函数的形参是类的对象的引用,函数的返回值是类对象,但是被Test t2接管

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

运行结果:

Initializing 10
Initializing with other 10
.........
Destroy 10
Destroy 10

解释:首先是没有调用赋值运算符了,函数返回调用拷贝构造函数,但没有再次调用拷贝构造函数,而且没有释放临时对象,可以理解成临时对象改名为t2了。因此临时对象不会马上销毁,被t2接管,知道return才销毁


(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】

#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;
}  //当定义的对象的声明周期结束,系统自动调用析构函数进行释放,顺序按照构造函数的顺序逆序进行

运行结果:

AB~B~A~A


【例子2】

#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;
}  //当定义的对象的声明周期结束,系统自动调用析构函数进行释放,顺序按照构造函数的顺序逆序进行

运行结果:

AAB~B~A~A~A。

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


参考:

C++ primer 第四版

你可能感兴趣的:(C++,C++,Primer,类与数据抽象)