【c++类和对象一】——构造函数与析构函数

说到构造函数和析构函数,你知道他们分别是什么,用来干什么的吗?首先我们用oop思想来模拟实现一个顺序栈。代码实现如下:

class SeqStack
{
public:
	//对象成员变量的初始化操作
	void init(int size = 10)
	{
		_pstack = new int[size];
		_top = -1;
		_size = size;
	} 

	//释放对象成员变量占用的外部堆内存(外部资源)
	void release()
	{
		delete[] _pstack;
		_pstack = nullptr;
	}
	void push(int val)
	{
		if (full())
			resize();
		_pstack[++_top] = val;
	}
	void pop()
	{
		if (empty())
			return;
		--_top;
	}
	int top()
	{
		return _pstack[_top];
	}
	bool empty() { return _top == -1; }
	bool full() { return _top == _size - 1; }
private:
	int* _pstack;//动态开辟数组,存储顺序栈的元素
	int _top;//指向栈顶元素的位置
	int _size;//数组扩容的总大小
	void resize()
	{
		int* ptmp = new int[_size * 2];
		for (int i = 0; i < _size; ++i)
		{
			ptmp[i] = _pstack[i];
		}
		delete[]_pstack;
		_pstack = ptmp;
		_size *= 2;
	}
};
int main()
{
	SeqStack s;
	s.init(5);
	for (int i = 0; i < 15; ++i)
	{
		s.push(rand() % 100);
	}
	while (!s.empty())
	{
		cout << s.top() << " ";
		s.pop();
	}
	return 0;
}

运行结果如下:
在这里插入图片描述
但是看似完美的代码过后却有很多的问题。面对众多的成员方法可能连你也会忘记自己到底写了哪些吧~例如,release方法就被我们全然忘记了。仔细想一想,我们堆上开辟的内存还没有释放,这样很容易造成程序崩溃。所以每次都需要手动初始化对象以及对对象资源进行释放,所有有什么好的方法解决这个问题呢,由此,我们引出了构造函数以及析构函数的概念。

一、什么是构造函数和析构函数

构造函数和析构函数他们的函数的名字和类名不一样,没有返回值。

1、定义

构造函数:主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。
可以带参数的,因此可以提供多个构造函数,可重载和init的功能一样,构造函数一调用完对象就生成了

SeqStack(int size = 10)
	{
		cout << this << "SeqStack()" << endl;
		_pstack = new int[size];
		_top = -1;
		_size = size;
	}

析构函数:与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数。**析构函数不能重载。不带参数,**所以析构函数只能有一个出作用域的时候自动调用,对象即不存在了

~SeqStack()
	{
		cout << this << "~SeqStack()" << endl;
		delete[] _pstack;
		_pstack = nullptr;
	}

2、有关注意点

1、构造函数的调用可以不用对象,根据传入参数进行匹配

2、先构造的后析构,后构造的先析构。
例如:还是上述代码,我们在main函数里面再再加入一个对象,代码实现如下:

int main()
{
	SeqStack s;
	for (int i = 0; i < 15; ++i)
	{
		s.push(rand() % 100);
	}
	while (!s.empty())
	{
		cout << s.top() << " ";
		s.pop();
	}
	SeqStack s1(50);
	return 0;
}

运行结果如下:
在这里插入图片描述
我们可以发现先构造的s对象是最后才析构的。

3、系统会默认产生不带参数的构造函数和析构函数,当所使用的的类不占外部空间的时候,不用自己定义析构函数。

4、析构函数的调用可以有对象,因为在没有调用析构之前对象还没有消失

5、但是如果根据对象调用了析构函数,此时再用该对象的方法会存在堆内存的非法访问,所以不建议自己调用析构函数。
如执行这两句代码s1.~SeqStack();s1.push(30);
分析如下:
在这里插入图片描述

二、构造函数的初始化列表

1、实现一个商品类

当我们想定义一个商品类,代码如下:

class CGoods
{
public:
	CGoods(const char* n, int a, double p)
	{
		strcpy(_name, n);
		_amount = a;
		_price = p;
	}
	void show()
	{
		cout << "name: " << _name << endl;
		cout << "amount:" << _amount << endl;
		cout << "price:" << _price << endl;
		_data.show();
	}
private:
	char _name[20];
	int _amount;
	double _price;
};

但是在此基础上还想增加一个商品日期的信息如下:

class CDate
{
public:
	CDate(int y, int m, int d)
	{
		_year = y;
		_month = m;
		_day = d;
	}
	void show()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

日期是商品信息的一部分 a part of…组合的关系,怎样将两个部分组合起来呢?

我们采用成员对象的方式如下:
在CGoods类private里面加入CDate _data;成员对象。

但是这样还不够。编译会报错,说CDate没有合适的默认构造函数可用

因为我们都知道对象的生成分为分配内存和调用构造函数两个部分,当我们没有指定构造函数的时候编译器调用的是默认的构造函数但是因为我们已经有了一个构造函数,就不会产生默认的构造函数了。因此,我们要对上述构造函数加以改进。改进代码如下:

CGoods(const char* n, int a, double p,int y,int m,int d)
		:_data(y,m,d)
	{
		//当前类类型构造函数体
		strcpy(_name, n);
		_amount = a;
		_price = p;
	}

以这种方式指定data上的构造方式,也叫做构造函数的初始化列表:可以指定当前对象成员变量的初始化方式(尤其是对于成员对象而言)

2、构造函数的初始化列表和当前类类型构造函数体的区别

通俗一点,就拿_amout这个成员变量来说

  • 构造函数的初始化列表相当于在定义变量的时候直接做了初始化 类似于int _amount =a;
  • 类类型构造函数体相当于,int _amount;int _amount =a;

但是对于反汇编如果是一般类型没有区别,但是对于类类型就必须写到初始化列表里面
所以商品类的构造函数还可以改成以下方式

CGoods(char* n, int a, double p, int y, int m, int d)
	:_data(y, m, d)//CData _data(y,m,d);
	,_amount(a)
	,_price(p)
{
	strcpy(_name, n);
}

3、构造函数的初始化列表练习

如下代码,请问运行结果是多少?

class Test
{
public:
	Test(int data = 10):mb(data),ma(mb){}
	void show()
	{
		cout << "ma:" << ma << " mb:" << mb<<endl;
	}
private:
	int ma;
	int mb;
};
int main()
{
	Test t;
	t.show();
	return 0;
}

运行结果如下:
在这里插入图片描述
原因:在vs编译器里面,在栈上开辟都初始化为了0xcccccccc =-858993460,当生成一个对象时,对象的成员变量的初始化方式和他们定义的顺序有关,和构造函数初始化列表中出现的先后顺序无关

三、不同对象的生命周期

1.栈上局部对象:定义的时候调用构造,出函数作用域调用析构。例如上述代码实现。

2.栈上全局对象(.data段):定义时候调用构造,程序结束时候才析构。
例如上述代码原型,加入一个全局对象gs.他的构造析构调用如下:
在这里插入图片描述

3.堆上对象:定义时:①.先malloc开辟内存②.调用构造;释放时:①.调用析构 ②.再释放内存。
例如上述代码原型,加入一个堆上对象ps.他的构造析构调用如下:

SeqStack* ps = new SeqStack(60);//开辟在(.heap)段里面malloc内存开辟+SeqStack(60)对象构造操作
	ps->push(70);
	ps->push(80);
	ps->pop();
	cout << ps->top() << endl;
	delete ps;

根据运行结果,我们可以发现他只有构造没有析构
因为堆上的对象要自己手动释放 delete(先调用ps->~SeqStack()然后free(ps))

发布了98 篇原创文章 · 获赞 9 · 访问量 3673

猜你喜欢

转载自blog.csdn.net/qq_43412060/article/details/105065123