0
点赞
收藏
分享

微信扫一扫

【C++】类和对象(中)—— 构造函数 + 析构函数 + 拷贝构造函数

单调先生 2022-01-20 阅读 88

类和对象(中)

1. 类的默认六个成员函数

如果一个类中什么成员都没有,称为空类。空类中什么都没有吗?并不是的。任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。这就是C++比较复杂的初始化机制。

class Date{}

它们是特殊的成员函数,特殊的点非常多,后面一一展开。

2. 构造函数

2.1 构造函数概念

构造函数特殊的成员函数。注意,构造函数的虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象

在写数据结构时,我就吃过这样的亏,最后出了稀奇古怪的错误,调试然后发现忘记调用初始化函数了。忘记销毁了我好像还没有直观的感受,但我也看过别人有忘记释放资源把服务器搞挂了的故事。

🍓 那么构造函数就是,对象定义出来就自动调用保证对象一定是被初始化的了。

2.2 构造函数特征

🍓 特性 ——

  1. 函数名和类名相同
  2. 无返回值
  3. 对象实例化时,编译器自动调用对应的构造函数
  4. 构造函数可以重载

❄️来看日期类 ——

class Date
{
public:
	//1.无参构造函数
	Date()
	{
		_year = 0;
		_month = 1;
		_day = 1;
	}
	//2.带参构造函数 - 初始化成指定值
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	//对象实例化时,自动调用
	Date d1;//调用无参构造函数
	Date d2(2022, 1, 17);
	return 0;
}

上面这两个构造函数构成了函数重载,其实他们也可以合并成一个函数,实现同样功能 —— 那就是通过全缺省【推荐like this

	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

注:无参构造函数 Date();和全缺省函数Date(int year = 2002, int month = 2, int day = 19);构成函数重载,语法上可以同时存在,但是,若有 无参调用Date d1;,则有二义性会报错。

❄️5. 但是如果在类中我们没有写构造函数,则C++编译器会自动生成一个无参的默认构造函数,(一旦用户显式定义编译器将不再生成)。d1对象调用了编译器生成的默认构造函数,但是d对象的_year /_month/_day,依旧是随机值。那么这个默认生成的构造函数干了什么?

在C++中把类型分为了两类 ——

  • 内置类型(基本类型)—— C语言原生带类型int/char/double/指针/内置类型的数组
  • 自定义类型 —— struct/class定义的类型

🍓 我们啥也不写编译器会默认生成构造函数 ——

  • 对于内置类型的成员变量不做处理

  • 对于自定义类型的成员变量,会去调用它的默认构造函数(即不用传参就可以调)初始化

    注:如果没有构造函数,编译器就会报错。(比如我显式的写了一个带参的Date()

为此,写了一个自定义类型,来验证第二点 —— 对于自定义类型,会去调用它的默认构造函数

class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}
private:
	int _a;
};

class Date
{
public:
	
private:
	int _year;
	int _month;
	int _day;

	A _aa;
};

int main()
{
	//对象实例化时,自动调用
	Date d1;
	return 0;
}

可以看到,确实是调了_aa的默认构造函数,打印了 ——

再来解释一下 —— 所谓如果没有构造函数,编译器就会报错

如果我把上段代码中的class A做一点修改,就报错了——

在上面代码我们实例化d1时,在Date类中我们啥也没写,对于自定义类型变量_aa,会去调用它无参的默认构造函数。对于A这个类,我们没有写无参/全缺省的构造函数,然后还故意手欠写了一个带参的,那编译器也就没再生成。这就没有默认构造函数可调了,就报错咯。

❄️6. 任何一个类的默认构造函数(不用参数就可以调用),有三个 —— 无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数。无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个(语法上他们可以同时存在,但是如果有对象定义去调用就会报错)。

3. 析构函数

3.1 析构函数概念

与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象的一些资源清理工作。

3.2 析构函数的特征

析构函数是特殊的成员函数。

🍓 特征 ——

  1. 析构函数名是在类名前加上字符~
  2. 无参数无返回值。
  3. 一个类有且只有一个析构函数(无参数无法构成重载)。若未显式定义,系统会自动生成默认的析构函数。
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

Date类为例,通过打印/调试都能看到在d1生命周期结束时,编译器自动调用了析构函数 ——

class Date
{
public:
    Date(int year = 2002, int month = 2, int day = 19)
	{
		_year = year;
		_month = month;
		_day = day;
	}
     
	~Date()
	{
		cout << "~Date()" << endl;
	}
    
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	return 0;
}

调试发现,这个析构函数好像什么都没有做。事实上,这个Date类也没有资源需要清理,不是所有的类都要析构函数。所以对于它不实现析构函数都是可以的。

那对于我们之前实现的栈这个类 ——

class Stack
{
public:
	//构造函数
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int)* capacity);
		if (_a == nullptr)
		{
			cout << "malloc failed" << endl;
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}

	//析构函数
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

int main()
{
	Stack st1;
	Stack st2(20);
	return 0;
}

这样就保证了,栈定义出来,就一定被初始化了;出作用域,在堆上申请的空间一定被回收了。就不会再忘记手动InitDestroy

注:析构顺序?st2先清理,st1后清理(调试可看)

  1. 如果我们不写,编译器自动生成的析构函数,会做一些什么呢?

🍓与构造函数类似,它——

  • 对内置类型的成员变量不做处理
  • 对于自定义类型的成员变量会回去调它的析构函数

我们以用两个栈实现队列为例,(在这儿不谈题目思路,思路看我题解),主要看默认生成的作用 ——

class Stack
{
public:
	//构造函数
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int)* capacity);
		if (_a == nullptr)
		{
			cout << "malloc failed" << endl;
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}

	//析构函数
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQueue {
public:
	// 我们不需要写,构造函数和析构函数
    // 默认生成的很有用
	// 对于自定义类型,会自动调用它的默认构造函数和析构函数
	/*MyQueue() {} */
    
	void push(int x) {}
	int pop() {}
	int peek() {}
	bool empty() {}
    
private:
	Stack _pushST;
	Stack _popST;
};

int main()
{
	MyQueue mq;
	return 0;
}

而之前的C语言实现,哎,要手动调用 ——

MyQueue* myQueueCreate() {
    MyQueue* q = (MyQueue*)malloc(sizeof(MyQueue));
    StackInit(&q->pushST);
    StackInit(&q->popST);
    return q;
}

void myQueueFree(MyQueue* obj) {
    StackDestroy(&obj->pushST);
    StackDestroy(&obj->popST);
    free(obj);
}

4. 拷贝构造函数

4.1 拷贝构造函数特征

拷贝构造函数也是特殊的成员函数。

🍓其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式。它的函数名就是类名,无返回值。

  2. 拷贝构造函数的参数只有一个必须使用引用传参,使用传值方式会引发无穷递归调用

class Date
{
public:
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	//构造函数
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2002, 3, 7);
	Date d2(d1);
	return 0;
}

注:这里const Date&常引用,最明显的原因是防止误写,当然还有很多原因,后续学习。

🍓下面来解释一下,为什么拷贝构造函数的必须使用引用传参,因为使用传值方式会引发无穷递归调用

如果我们传值传参 ——

这里可能比较有疑惑的是,传值传参为什么是拷贝构造 ——

传值传参,就是把实参的值拷贝赋给形参,用同类型的来初始化你,其实就是一个拷贝构造。下面这段代码,调试可以观察到,先进入了拷贝构造函数,再进入了f(Date d)函数 ——

但是引用传参,d就是d1的一个别名。

  1. 如果没有显式定义,系统生成默认的拷贝构造函数

🍓这块儿和之前的构造函数和析构函数有点差别 ——

  • 内置类型成员,会完成字节序拷贝(浅拷贝)
  • 自定义类型成员,会去调用它的拷贝构造

我们来验证一下:

可以看到,编译器生成的默认构造函数,对于内置类型成员,确实完成了字节序的拷贝。也就是说像日期类这样的我们完全可以不写。

而对于栈呢 ?我们还啥也不写 ——

class Stack
{
public:
	//构造函数
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int)* capacity);
		if (_a == nullptr)
		{
			cout << "malloc failed" << endl;
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}

	//析构函数
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

int main()
{
	Stack st1(10);
	Stack st2(st1);
}

就崩了💩 ——

这是因为 ——

像这种类,就不能用默认的了,要我们自己实现。

对于自定义类型变量,确实会调用它的拷贝构造函数 ——

class A
{
public:
	A(const A& a)
	{
		cout << "A(const A&)" << endl;
	}

	A()
	{

	}
};

class Date
{
public:
	//构造函数
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
private:
	int _year;
	int _month;
	int _day;

	A _a;
};

int main()
{
	Date d1(2002, 3, 7);
	Date d2(d1);
	return 0;
}

5. 总结

上文描述了太多细节了,确实容易晕,在此汇总一下,就非常非常清晰了 ——

5.1 构造函数

5.2 析构函数

5.3 拷贝构造函数

本文完@边通书

举报

相关推荐

0 条评论