C++ Primer 学习笔记_22_类与数据抽象(4)_默认构造函数、重载构造函数、全局对象的构造先于main、构造函数与new、析构函数与delete、转换构造函数、赋值与初始化、explicit

一、构造函数
1、构造函数是特殊的成员函数
2、创建类类型的新对象,系统自动会调用构造函数
3、构造函数是为了保证对象的每个数据成员都被正确初始化 (构造函数的作用)
4、函数名和类名完全相同
5、 不能定义构造函数的类型(返回类型),也不能使用void
6、 通常情况下构造函数应声明为公有函数 ,否则它不能像其他成员函数那样被显式地调用
(1)示例
#include <iostream>
using namespace std;
class Test
{
public:
    // 如果类不提供任何一个构造函数,系统将为我们提供一个不带参数的  
    Test();  //不带参数的构造函数称为默认构造函数
private:
    int num_;
};

Test::Test()
{
    num_ = 0;
    cout << "Initializing Default" << endl;
}

int main(void)
{
    Test t;
    return 0;
}
7、构造函数被声明为私有有特殊的用途。
8、 构造函数可以有任意类型和任意个数的参数,一个类可以有多个构造函数 (重载)


二、默认构造函数
1、 不带参数的构造函数
2、如果程序中未声明,则系统自动产生出一个默认构造函数
3、一个类哪怕只是定义了一个构造函数(包括拷贝构造函数),编译器也不会再生成默认构造函数。

4、回顾

在C++中类型大致可以分为三种

第一种、内置类型

如int, char, float, unsigned等。内置类型是最基本的类型。

第二种、复合类型

复合类型:使用其它类型定义的类型。有三种复合类型:引用,指针,数组。

第三种、类类型

就是类。比如string以及自己定义的类。


5、若使用编译器自动生成的默认构造函数(或自己定义一个未进行任何操作的默认构造函数),则类中的每个成员,使用与初始化变量相同的规则来进行初始化。

——类类型成员:运行该类型的默认构造函数来初始化

——内置或符合类型的成员:依赖于对象的作用域,在局部作用域中这些成员不被初始化,而在全局作用域中他们被初始化为0。

例子:

下面代码中a,b的各个成员变量值时多少?

class Student
{
public:
    Student(){}
    void show();
private:
    string name;
    int number;
    int score;
};
Student a;
int main()
{
    Student b;
}

解答:a与b的name都调用string类的默认构造函数初始化,a是全局对象,故a的number与score初始化为0;而b是局部对象,故b的number与score不被初始化,为垃圾值。



三、重载构造函数

1、构造函数可以被重载

    一般而言,不同的构造函数允许用户指定不同的方式来初始化数据成员。构造函数可以有任意类型和任意个数的参数,一个类可以有多个构造函数(重载)。

2、实参决定使用哪个构造函数

Sales_item();      //Sales_item empty;
Sales_item(const string &);     //Sales_item Primer_3td_Ed("0-201-82470-1");
Sales_item(std::istream &);     //Sales_item Primer_4th_Ed(cin);

3、构造函数自动执行

创建类类型的新对象,编译器自动会调用构造函数



四、全局对象的构造先于main函数
示例:
//Test.h
#ifndef _TEST_H_
#define _TEST_H_
class Test
{
public:
 // 如果类不提供任何一个构造函数,系统将为我们提供一个不带参数的
 // 默认的构造函数
 Test();
 Test(int num);
 void Display();
 ~Test();
private:
 int num_;
};
#endif // _TEST_H_

//Test.cpp
#include "Test.h"
#include <iostream>
using namespace std;
// 不带参数的构造函数称为默认构造函数
Test::Test()
{
 num_ = 0;
 cout<<"Initializing Default"<<endl;
}
Test::Test(int num)
{
 num_ = num;
 cout<<"Initializing "<<num_<<endl;
}
Test::~Test()
{
 cout<<"Destroy "<<num_<<endl;
}
void Test::Display()
{
 cout<<"num="<<num_<<endl;
}

//01.cpp
#include "Test.h"
#include <iostream>
using namespace std;
Test t(10);
int main(void)
{
 cout<<"Entering main ..."<<endl;
 cout<<"Exiting main ..."<<endl;
 return 0;
}
运行结果:

Initializing 10
Entering main ...
Exiting main ...
Destroy 10

解释:在return 0 时全局变量的生存期也到了,故也会自动调用析构函数。


五、构造函数与new
    new operator 做了两件事,一个是创建了对象内存,一个是调用构造函数;堆上的内存需要delete 释放,做了两件事,一是调用析构函数,二是释放内存。
1、示例(Test.cpp和Test.h 同上)
//02.cpp
#include "Test.h"
int main(void)
{
 Test t;
 t.Display();
 Test t2(10);
 t2.Display();
 Test* t3 = new Test(20);	// new operator
 t3->Display();
 delete t3;
 return 0;
}
运行结果:

Initializing Default
num=0
Initializing 10
num=10
Initializing 20
num=20
Destroy 20
Destroy 10
Destroy 0

解释:可以看到构造函数是被自动调用的,且构造函数可以被重载调用;栈上的对象生存期到了会自动调用析构函数;而new operator 做了两件事,一个是创建了对象内存,一个是调用构造函数;堆上的内存需要delete 释放,做了两件事,一是调用析构函数,二是释放内存。


六、析构造函数
1、析构函数
    在构造函数中分配了资源之后,需要一个对应操作自动回收或释放资源。析构函数就是这样的一个特殊函数,它可以完成所需的资源回收,作为类构造函数的补充。
(1)函数名和类名相似(前面多了一个字符“~”
(2)没有返回类型
(3)没有参数
(4)析构函数不能被重载
(5)如果没有定义析构函数,编译器会自动生成一个默认析构函数,其格式如下:

  类名::~默认析构函数名( )
  {
  }

(6)默认析构函数是一个空函数。

(7)何时调用析构函数

    撤销类对象时会自动调用析构函数:动态分配的对象只有在指向该对象的指针被删除时才撤销,如果没有删除指向动态对象的指针,则不会运行该对象的析构函数,对象就会一直存在,从而导致内存泄漏,而且,对象内部使用的任何资源也不会释放!


2、 析构函数与数组
3、析构函数与 delete 运算符
4、示例 (Test.cpp和Test.h 同上)
//03.cpp
#include "Test.h"
int main(void)
{
 Test t[2] = {10, 20};
 Test* t2 = new Test(2);
 delete t2;
 Test* t3 = new Test[2];
 delete[] t3;
 return 0;
}
运行结果:

Initializing 10
Initializing 20
Initializing 2
Destroy 2
Initializing Default
Initializing Default
Destroy 0
Destroy 0
Destroy 20
Destroy 10

解释:注意  Test t[2] = {1020}; 中10,20是当作参数传递给每个对象的构造函数的,如果没有对应的构造函数,比如只有2个参数的构造函数,那么编译是失败的。



九、析构函数可以显式调用
     析构函数并不仅限于用来释放资源,一般而言,析构函数还可以执行任意操作,该操作是类设计者希望在该类对象的使用完毕之后执行的!

    实际上,构造函数和析构函数都是可以被显式调用的,只是很少这样做。

1、示例(Test.cpp和Test.h 同上)

#include "Test.h"
int main(void)
{
    Test t;
    t.~Test();	 // 析构函数可以显式调用,但一般很少用。
    return 0;
}

运行结果:
Initializing Default
Destroy 0
Destroy 0



十、转换构造函数

1、转换构造函数是单个参数的构造函数,但是单个参数的构造函数不一定是转换构造函数(单个参数的构造函数两个功能:普通构造函数、转换构造函数
2、作用:将其它类型转换为类类型(同时也初始化)
3、类的构造函数只有一个参数是非常危险的,因为编译器可以使用这种构造函数把参数的类型隐式转换为类类型

4、示例(Test.cpp和Test.h 同上)

//05.cpp
#include "Test.h"
int main(void)
{
 Test t(10);	 // 带一个参数的构造函数,充当的是普通构造函数的功能
 t = 20;	 // 将20这个整数赋值给t对象(调用转换构造函数)
         // 1、调用转换构造函数将20这个整数转换成类类型 (生成一个临时对象)
         // 2、将临时对象赋值给t对象(调用的是=运算符)
         // 3、最后将临时对象20释放
 Test t2; //为方便观察临时对象20释放的现象
 return 0;
}

运行结果:

Initializing 10
Initializing 20
Destroy 20
Initializing Default
Destroy 0
Destroy 20

解释:可以看到初始化了一个临时对象,传递参数20,然后调用赋值运算符operator=,接着释放临时对象,最后释放的对象是已经被更改过的t 。赋值运算符的格式为:Test& Test::operator=(const Test& other);事实上如果没有自己实现,编译器也会实现一个默认的赋值运算符。



十一、赋值与初始化的区别
1、在初始化语句中的等号不是运算符 因此也没有调用转换构造函数 编译器对这种表示方法有特殊的解释

2、赋值操作同时会调用转换构造函数

3、重载“=”运算符

Test& Test::operator=(const Test& other);


4、示例(Test.cpp和Test.h 添加了operator=函数)

//Test.h
#ifndef _TEST_H_
#define _TEST_H_
class Test
{
public:
 // 如果类不提供任何一个构造函数,系统将为我们提供一个不带参数的
 // 默认的构造函数
 Test();
 /*explicit */Test(int num);
 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;
 cout<<"Initializing Default"<<endl;
}
Test::Test(int num)
{
 num_ = num;
 cout<<"Initializing "<<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)  //当t = t时,就不用再赋值了
  return *this;
 num_ = other.num_; //一般编译器也是类似的生成
 return *this;
}

//06.cpp
#include "Test.h"
int main(void)
{
 Test t = 10;	 // 等价于Test t(10); 这里的=不是运算符,表示初始化。因此也没有调用转换构造函数 
 t = 20;	 // 赋值操作,会调用转换构造函数
 Test t2; //方便观察
 t = t2;	 // 赋值操作 t.operator=(t2);也没有调用转换构造函数
 return 0;
}

运行结果:

Initializing 10
Initializing 20
Test::operator=
Destroy 20
Initializing Default
Test::operator=
Destroy 0
Destroy 0

解释:第一条语句是初始化,后面是赋值操作,参照上面临时对象的创建销毁,赋值运算符的调用可以理解输出。



十二、explicit 关键字(抑制由构造函数定义的隐式转换)

1、只提供给类的构造函数使用的关键字,来防止在需要隐式转换的上下文中使用构造函数。
2、编译器不会把声明为explicit的构造函数用于隐式转换,它只能在程序代码中显示创建对象。

3、示例(Test.cpp 同上)

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

//07.cpp
#include "Test.h"
int main(void)
{
 Test t = 10; 
 Test t2;
 t2 = 20; 
 return 0;
}

运行结果:

编译不通过

    由于在构造函数Test(int num); 前面加上explicit 关键字。那么Test t = 10;做初始化的时候是不等价于Test t(10)的;另外的,t2 = 20; 这种语句都是编译不通过的;因为不允许隐式转换。

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