C++学习:初始化列表、对象的构造顺序、对象析构

一、初始化列表

在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释放堆空间的时候就紫东阁调用析构函数。

析构函数的调用顺序

单个对象:

调用成员变量的构造函数,调用顺序与声明顺序相同;

调用类自身的构造函数,析构函数与对一个构造函数的调用顺序相反;

多个对象析构:析构顺序与构造顺序相反

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值