一、初始化列表
在C语言中是没有初始化列表这个概念的,在C++中材引入初始化列表这个概念。
类中是否能存在const成员,答案是肯定的,类中是可以定义const成员的,如下面代码所示,编译是正常的。
#include <iostream>
using namespace std;
//定义一个类,没有定义构造函数
class Test
{
private:
const int ci; //在类中定义const成员
public:
int getCI() { return ci; }
};
int main()
{
return 0;
}
但是上面的代码值定义类了一个类,没有使用类,那没什么意义,那就使用吧,但是刚定义一个类对象,编译器就提示错误,没有合适的构造函数能使用,连聪明的C++编译器都没办法把这个问题代码圆过去。
#include <iostream>
using namespace std;
//定义一个类,没有定义构造函数
class Test
{
private:
const int ci; //在类中定义const成员
public:
int getCI() { return ci; }
};
int main()
{
Test t; //聪明点的编译器测试就提示这里出错了
return 0;
}
说明上面的代码犯了原则性错误:如果类中有const成员,那const成员必须先初始化,那怎么初始化,用构造函数初始化ci?不可能,ci是const属性,不能作为左值,无法用常规的方式初始化。,那只能用非常规手段了,初始化列表。
C++初始化列表对成员变量进行初始化的语法规则
//v1对m1进行初始化,v1,v2对m2进行初始化,v3对m3进行初始化
ClassName::ClassName():m1(v1), m2(v1,v2), m3(v3)
{
//
}
初始化列表用在构造函数处,在调用构造函数时,先完成初始化列表的内容,再来完成构造函数函数体里面的内容。初始化列表里面初始化的是类对象的成员变量,不仅可以初始化const变量,也可以初始化常规变量。
程序实例1:const成员变量初始化
#include <iostream>
using namespace std;
//定义一个类,没有定义构造函数
class Test
{
private:
const int ci; //在类中定义const成员
public:
int getCI() { return ci; }
//在构造函数中用初始化列表将ci的值初始化为10
Test():ci(10)
{
}
};
int main()
{
Test t;
cout << "t.ci = " << t.getCI() << endl;
return 0;
}
输出结果:
t.ci = 10
结果分析:从结果中可以看出初始化列表式生效的,因为除了在初始化列表中给ci初始化了值,其他的地方没有赋值。
类成员的初始化
成员的初始化顺序与成员的声明顺序相同,与初始化列表中的位置无关,初始化列表先于构造函数的函数体执行,可以通过下面的程序来证明。
程序实例2:初始化列表的使用
#include <iostream>
using namespace std;
//创建一个value类
class Value
{
private:
int mi;
public:
//构造函数
Value(int i)
{
cout << "i = " << i << endl;
mi = i;
}
//成员函数用于返回成员变量mi的值
int getI()
{
return mi;
}
};
//创建一个Test类
class Test
{
private:
//在Test类中成员变量中,声明3个Value对象,顺序是m2、m3、m1
Value m2;
Value m3;
Value m1;
public:
//在构造函数的初始化列表中初始化3个Value对象,
Test() :m1(1),m2(2),m3(3) //初始化列表
{
cout << "Test::Test()\n";
}
};
int main()
{
Test t;
return 0;
}
输出结果:
i = 2
i = 3
i = 1
Test::Test()
结果分析:
在Test类中声明Value对象的顺序是m2、m3、m1,即使初始化列表中的顺序是m1、m2、m3,初始化的顺序还是按照声明中的顺序来初始化。聪明一点的编译器在Test() :m1(1),m2(2),m3(3)构造函数这块会给出警告说m1的初始化顺序在m2之后。结果中依次输出i = 2 、i =3、 i = 1、之后,再输出Test::Test()说明了舒适化列表的内容比构造函数函数体的内容先执行。
二、对象的构造顺序
类可以定义多个对象,对象创建完成的标志就是对象的构造函数执行完成。对象可以看成一种变量,C++中可以在代码的任何位置定义变量,也就是可以在不同的地方定义对象。
局部对象的构造顺序:当程序执行流到达对象的定义语句时进行构造;
堆对象的构造顺序:当程序执行流到达new语句时创建对象;
全局对象的构造顺序是不确定的;
下面的代码展示了局部对象的构造顺序,也就是看程序的执行流,执行到哪个局部对象,就构造哪个对象。
程序实例3:局部对象的构造顺序
#include <iostream>
using namespace std;
//创建一个Test类
class Test
{
private:
int mi;
public:
//构造函数
Test(int i)
{
mi = i;
cout << "Test(int i):" << mi << endl;
}
//因为涉及到对象的赋值,需要拷贝构造函数
Test(const Test& obj)
{
mi = obj.mi;
cout << "Test(const Test& obj):" << mi << endl;
}
};
int main()
{
int i = 0;
Test a1 = i; //1、首先构造,打印 Test(int i):0
while (i < 3)
{
Test a2 = ++i; //2、反复构造3次,打印Test(int i):1 Test(int i):2 Test(int i):3
}
if (i < 4)
{
Test a = a1; //3、对象赋值,使用拷贝构造函数,打印Test(const Test& obj):0
}
else
{
Test a(100); //执行不到这里,不构造
}
return 0;
}
输出结果
Test(int i):0
Test(int i):1
Test(int i):2
Test(int i):3
Test(const Test& obj):0
结果分析:
代码的注释已经写得比较清晰了,局部对象的构造就时看程序的执行流,执行到哪里就构造到哪里,没什么特别的地方。
堆对象的构造顺序
当程序执行流到达new语句时创建对象,使用new创建对象将自动触发构造函数的调用,这与局部对象的构造没有什么区别,也是执行到哪里就构造到哪里。
程序实例4:堆对象的构造顺序
#include <iostream>
using namespace std;
class Test
{
private:
int mi;
public:
//构造函数
Test(int i)
{
mi = i;
cout << "Test(int i): " << mi << endl;
}
//拷贝构造函数
Test(const Test& obj)
{
mi = obj.mi;
cout << "Test(const Test& obj): " << mi << endl;
}
};
int main()
{
int i = 0;
Test* a1 = new Test(i);
while (++i < 10)
{
if (i % 2) //奇数的时候就创建对象空间
{
new Test(i);
}
}
if (i < 4)
{
new Test(*a1);
}
else
{
new Test(100); //根据前面的代码,这个语句会被执行,所以这里要构造对象空间
}
return 0;
}
输出结果:
Test(int i):0
Test(int i):1
Test(int i):3
Test(int i):5
Test(int i):7
Test(int i):9
Test(int i):100
全局对象的构造顺序
不同的编译器使用不同规则确定的构造顺序;
既然这样就没必要写代码了。
三、对象的销毁(析构函数)
对象被初始化之后才能使用,对象不需要了就销毁,销毁前需要做一些清理工作。
解决方案:
为每个类都提供一个public的free函数;
对象不再需要时立即调用free函数进行清理;
但是这种方案存在明显的问题:free只是一个普通的函数,必须显式的调用,对象销毁前如果没有做清理,就可能造成资源泄露。
class Test
{
private:
int* p;
public:
//构造函数
Test(int i) { p = new int;}
void free() {delete p;}
};
C++编译器能否自动调用某个特殊的函数进行对象的清理?答案是肯定的,那就是析构函数。
析构函数
C++中可以定义一个特殊的清理函数叫做析构函数,析构函数的功能与构造函数相反。
析构函数定义:~ClassName();
析构函数没有参数,也没有返回值类型声明;
析构函数在对象被销毁时自动被调用。
程序实例5:析构函数的调用
#include <iostream>
using namespace std;
class Test
{
public:
//构造函数
Test()
{
printf("调用Test()\n");
}
//析构函数
~Test()
{
printf("调用~Test()\n");
}
};
int main()
{
Test t;
return 0;
}
输出结果:
调用Test()
调用~Test()
结果分析
当定义了t时,构造函数就被调用,t只是个局部变量,用完之后是要被清理的,在return 之前就自动调用析构函数。
程序实例6:堆变量的销毁
#include <iostream>
using namespace std;
class Test
{
public:
Test()
{
cout << "using Test().\n";
}
~Test()
{
cout << "using ~Test().\n";
}
};
int main()
{
Test* pt = new Test(); //在堆中申请一个类对象空间
delete pt; //释放堆空间
return 0;
}
输出结果:
using Test().
using ~Test().
结果分析:在堆空间中创建对象也是用new,在使用delete释放堆空间的时候就紫东阁调用析构函数。
析构函数的调用顺序
单个对象:
调用成员变量的构造函数,调用顺序与声明顺序相同;
调用类自身的构造函数,析构函数与对一个构造函数的调用顺序相反;
多个对象析构:析构顺序与构造顺序相反