构造函数与析构函数(一)

构造函数

  • 构造函数是特殊的成员函数
  • 创建类类型的新对象,系统自动会调用构造函数
  • 构造函数是为了保证对象的每个数据成员都被正确初始化

构造函数如何定义的?

  • 函数名和类名完全相同
  • 不能定义构造函数的类型(返回类型),也不能使用 void
  • 通常情况下构造函数应声明为公有函数,否则它不能像其他成员函数那样被显示调用
  • 构造函数被声明为私有有特殊的用途
  • 构造函数可以有任意类型和任意个数的参数,一个类可以有多个构造函数(重载)

默认构造函数

  • 不带参数的构造参数称为默认构造函数
  • 如果程序中未声明,则系统自动产生出一个默认构造函数
  • 构造函数是可以被重载的
class Test
{
public:
//如果类不提供任何一个构造函数,系统将为我们提供一个不带参数的默认构造函数
Test();
Test(int num);
void Display();
private:
int num_;

};


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

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

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

全局对象的构造先于 main 函数。

析构函数

  • 函数名和类名相似,前面多了一个字符 ~
  • 没有返回类型,
  • 没有参数,
  • 析构函数不能被重载,
  • 如果没有定义析构函数,编译器会自动生成一个默认析构函数,默认的析构函数是一个空函数,
  • 先创建的对象,后被销毁 堆栈
  • 析构函数可以显示调用,但一般很少用

析构函数与 delete

  • 如果生成的数组,使用 delete 时必须配套使用 []
Test *t3 = new Test[2];
delete[] t3; //省略 [] 会发生运行时错误
  • 当使用 delete 的时候,不只是释放了内存,还调用了对象的析构函数
  • 析构函数可以显示调用
  • 在栈区中创建的对象,在生存期结束的时候会自动调用析构函数
  • 在堆上创建的对象,要有程序员显示的调用 delete 释放该对象,同时调用析构函数

构造函数与析构函数(二)

转换构造函数

  • 单个参数的构造函数称为转换构造函数
  • 可以将其他类型转换为类类型
  • 类的构造函数只有一个参数是非常危险的,因为编译器可以使用这种构造函数把参数的类型隐式转换为类类型

构造函数的作用:

  • 初始化类
  • 类型转换(转换构造函数)

带一个参数构造函数:

  • 普通的构造函数(初始化)
  • 转换构造函数(初始化,类型转化)
int main(void)
{
// Test t;
Test t(10); //带一个参数的构造函数,充当的是普通构造函数的功能

t = 20; // 将20这个整数赋值给 t 对象
// 1. 调用转换构造函数将 20 这个整数转换成类类型(生成一个临时对象)
// 2. 将临时对象赋值给 t 对象(调用的是 = 运算符),赋值成功后会释放临时对象,会调用析构函数
Test t2;


return 0;
}

赋值与初始化区别

  • 在初始化语句中的等号不是运算符。编译器对这种表示方法有特殊的解释,
  • Test t = 10; // 等价于Test t(10); 这里的 = 不是运算符,表示初始化,不是赋值操作
Test t = 10; // 等价于Test t(10); 这里的 = 不是运算符,表示初始化,不是赋值操作
t = 20; //赋值操作,需要转换构造函数生成临时对象,在赋值

赋值运算符重载

当为一个类对象赋值(注意:可以用本类对象为其赋值,也可以用其它类型(如内置类型)的值为其赋值)时,会由该对象调用该类的赋值运算符重载函数。

当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动生成这样一个赋值运算符重载函数。

当用一个非类A的值(如上面的int型值)为类A的对象赋值时

  • 如果匹配的构造函数和赋值运算符重载函数同时存在,会调用赋值运算符重载函数。
  • 如果只有匹配的构造函数存在,就会调用这个构造函数。

t = t2; //赋值操作 等价于 t.operator=(t2) 返回的是 t 对象本身

explicit

  • explicit 是避免构造函数的参数自动转换为类对象的标识符
  • 只提供给类的构造函数使用的关键字
  • 编译器不会把声明为 explicit 的构造函数用于隐式转换,它只能在程序代码中显示创建对象
  • 如果c++类的构造函数有一个参数,那么在编译的时候就会有一个缺省的转换操作:将该构造函数对应数据类型的数据转换为该类对象-这是隐式的转换

构造函数与析构函数(三)

构造函数初始化列表

  • 推荐在构造函数初始化列表中进行初始化
  • 构造函数的执行分为两个阶段
    • 初始化阶段
    • 普通计算阶段,函数体内部的属于普通计算阶段
  • 冒号后面是初始化列表
Clock::Clock(int hour, int minute, int second) : hour_(hour), minute_(minute), second_(second)  //采用初始化列表来初始化
{
// hour_ = hour;
// minute_ = minute;
// second_ = second;
cout << "Clock::CLock" << endl; // 属于普通计算阶段
}

对象成员及其初始化

  • 一个类可以包含普通的对象成员,也可以包含其他类类型的对象成员,子对象
  • 构造次序与对象定义的顺序有关,与初始化列表无关
  • 对数据成员的初始化推荐放在初始化列表中,包括普通的对象成员和对象数据成员
  • 对象成员所对应的类没有默认的构造函数,初始化一定要放在初始化列表,如果有默认的构造函数,可以省略掉初始化列表

const 成员、引用成员初始化

  • const 成员的初始化只能在构造函数的初始化列表中进行,在构造函数体内初始化本质是赋值,相当于对常量赋值,非法
  • 引用成员的初始化也只能在构造函数初始化列表中进行
  • 对象成员(对象所对应的类没有默认构造函数),对象成员的初始化也只能在构造函数初始化列表中进行

枚举

  • 枚举常量对所有对象都是常量,而 const 常量只是对当前对象是常量
public:
enum E_TYPE
{
TYPE_A = 100, // TYPE_A TYPE_B 对所有对象来说都是常量
TYPE_B = 200
};

构造函数与析构函数(四)

拷贝构造函数

  • 功能:使用一个已经存在的对象来初始化一个新的同一类型的对象,即由一个对象初始化另一个对象
  • 声明:只有一个参数并且参数为该类对象的引用
  • 如果类中没有说明拷贝构造函数,则系统自动生成一个缺省复制构造函数,作为该类的公有成员
 Test(const Test &other); // 拷贝构造函数 声明

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

}
Test t(10);
Test t2(t); //调用拷贝构造函数(如果没有写自己的拷贝构造函数,则系统默认提供的)
Test t2 = t; //与 Test t2(t) 是等价的,相当于用 t 对象初始化 t2 对象

拷贝构造函数的参数是对象的引用?本质是一个对象 t 初始化同一种对象 other,根据拷贝构造函数的定义,一个对象初始化同一种对象需要调用拷贝构造函数,如果不是引用,则实参到形参是值传递,other 会开辟新的内存,此时实参会初始化形参,此时又会调用拷贝构造函数,产生递归的调用。所以使用引用传递,不会产生新的内存,不会构造出一个新的对象。引用传递可以减少对象的复制,内存的拷贝,提高效率

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

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

构造函数与析构函数(五)

深拷贝与浅拷贝

String s1("AAA");
s1.Display();
String s2 = s1; // 调用默认的拷贝构造函数
// 系统提供的默认拷贝构造函数实施的是浅拷贝 s2.str_ = s1.str_

  • 系统提供的默认拷贝构造函数实施的是浅拷贝 s2.str_ = s1.str_,仅仅将指针简单的指向 s2,没有重新分配内存。相当于两个对象指向同一个内存。当两个对象的生存周期结束是,都会调用各自的析构函数,会导致同一块内存被销毁 delete 两次,出现运行时错误。
  • 解决方法是采用深拷贝,自己提供一个拷贝构造函数来实施深拷贝
  • 如果涉及到动态内存分配,通常情况下实施的是深拷贝

赋值操作

String s3;
s3.Display();
s3 = s2; // 会发生运行错误,本质是调用默认等号运算符
// 系统提供的默认等号运算符实施的是浅拷贝,s3.str_ = s2.str_
  • 系统提供的默认等号运算符实施的是浅拷贝,会导致同一块内存被销毁 delete 两次,出现运行时错误。
  • 解决方法是自己提供一个等号运算符来实施深拷贝
  • 采用自己的等号运算符相当于 s3.operator=(s2)

禁止拷贝

  • 要让对象是五一无二的,我们要禁止拷贝
  • 方法是将拷贝构造函数声明为私有,将等号运算符声明为私有,并且不提供实现

空类默认产生成员

  • class Empty{}
  • Empty(); // 默认构造函数
  • Empty(const Empty&); // 默认拷贝构造函数
  • ~Empty() // 默认析构函数
  • Empty& operatpr=(const Empty&); // 默认赋值运算符
  • Empty* operator&(); // 默认取址运算符
  • const Empty* operator&() const // 默认取址运算符 const

Empty* p = &e; // 等价于 e.operator&();