C++ Primer 学习笔记_23_类与数据抽象(5)_初始化列表(const和引用成员)、拷贝构造函数
//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; }
(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++中,成员变量的初始化顺序与变量在类型中的声明顺序相同,而与它们在构造函数的初始化列表的顺序无关。
二、构造函数初始化列表——对象成员及其初始化
//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; }
//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 ...
解释:从输出可以看出几点,一是构造对象之前,必须先构造对象的成员;二是对象成员构造的顺序与定义时的顺序有关,跟初始化列表顺序无关;三是构造的顺序和析构的顺序相反;四是如果对象成员对应的类没有默认构造函数,那对象成员也只能在初始化列 表进行初始化。再提一点,如果类是继承而来,基类没有默认构造函数的时候,基类的构造函数要在派生类构造函数初始化列表中调用。
Object 0 ...
Object 0 ...
Container ...
~Container ...
~Object 0 ...
~Object 0 ...
//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成员和引用成员必须在初始化列表中初始化。另外,可以使用定义枚举类型来得到类作用域共有的常量。
四、 拷贝构造函数
//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; }
——如果类需要析构函数,则它也需要赋值操作符和复制构造函数,这是一个有用的经验法则,这个规则常称为三法则。
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 是引用故也不会调用拷贝构造函数。
#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 第四版