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

一. 类的 6 个默认成员函数

默认成员函数:我们不写,编译器会自己生成

C语言用栈时:1. 有时会忘记初始化、销毁(内存泄漏) 2. 有些地方写起来很繁琐

C++进化到可以自动初始化、销毁

以前的 C++ 栈:

cpp 复制代码
typedef int DataType;

class Stack
{
public:
    void Init(int capacity = 4)
    {
    	_a = (DataType*)malloc(sizeof(DataType) * capacity);
    	if (nullptr == _a)
    	{
    		perror("malloc申请空间失败!!!");
    		return;
    	}
    	_capacity = capacity;
    	_size = 0;
    }

	void Push(DataType data)
	{
		CheckCapacity();
		_a[_size] = data;
		_size++;
	}

	void Pop()
	{
		if (Empty())
			return;
		_size--;
	}

	DataType Top() { return _a[_size - 1]; }
	int Empty() { return 0 == _size; }
	int Size() { return _size; }

	void Destroy()
	{
		if (_a)
		{
			free(_a);
			_a = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}

private:
	void CheckCapacity()
	{
		if (_size == _capacity)
		{
			int newcapacity = _capacity * 2;
			DataType* temp = (DataType*)realloc(_a, newcapacity *
				sizeof(DataType));
			if (temp == nullptr)
			{
				perror("realloc申请空间失败!!!");
				return;
			}
			_a = temp;
			_capacity = newcapacity;
		}
	}

private:
	DataType* _a;
	int _capacity;
	int _size;
};

不 Init,不 Destroy

cpp 复制代码
int main()
{
	Stack s;
	// s.Init();

	s.Push(1);
	s.Push(2);
	s.Push(3);
	s.Push(4);

	printf("%d\n", s.Top());
	printf("%d\n", s.Size());
	s.Pop();
	s.Pop();
	printf("%d\n", s.Top());
	printf("%d\n", s.Size());

	// s.Destroy();

	return 0;
}

报错,程序异常退出

二. 构造函数

构造函数是特殊的成员函数。不是开空间创建对象,而是初始化对象,在对象整个生命周期内只调用一次

对象不需要某个函数创建。因为对象在栈里面,栈里面的变量是自动创建的(跟着栈帧走的)。函数调用,给局部变量开空间;函数结束,变量随栈帧销毁,空间也就销毁了
特征1. 函数名与类名相同

特征2. 无返回值,也不写 void

特征3.对象实例化时 编译器 自动调用对应的构造函数

现在的 C++ 栈:

cpp 复制代码
class Stack
{
public:
	Stack(int capacity = 4) // 构造函数,功能:替代Init
	{
        cout << "Stack(int capacity = 4)" << endl;
		_a = (DataType*)malloc(sizeof(DataType) * capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}

	/*void Init(int capacity = 4)
	{
		_a = (DataType*)malloc(sizeof(DataType) * capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}*/

	void Push(DataType data)
	{ }

	void Pop()
	{ }

	DataType Top() { return _a[_size - 1]; }
	int Empty() { return 0 == _size; }
	int Size() { return _size; }

	void Destroy()
	{
		if (_a)
		{
			free(_a);
			_a = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}

private:
	void CheckCapacity()
	{ }

private:
	DataType* _a;
	int _capacity;
	int _size;
};

不 Init,不 Destroy

C++祖师爷规定了,对象实例化的时候,自动调用构造函数。从此不需要 Init( )

现在程序还存在内存泄漏,因为我们没有调 Destroy。先看:三. 析构函数

特征 4:构造函数可以重载

为什么构造函数支持重载? 因为有多种初始化方式

eg:一上来有一组数据作为默认初始化

cpp 复制代码
class Stack
{
public:
	Stack(DataType* a, int n)
	{
		cout << "Stack(DataType* a, int n)" << endl;
		_a = (DataType*)malloc(sizeof(DataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		memcpy(_a, a, sizeof(DataType) * n);

		_capacity = n;
		_size = n;
	}

    Stack(int capacity = 4) // 构造函数,功能:替代Init
	{
        cout << "Stack(int capacity = 4)" << endl;
		_a = (DataType*)malloc(sizeof(DataType) * capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}

	......

	~Stack()
	{
		cout << "~Stack()" << endl;
		if (_a)
		{
			free(_a);
			_a = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}

private:
	void CheckCapacity()
	{ }

private:
	DataType* _a;
	int _capacity;
	int _size;
};

特征 5:自动生成

如果类中没有显式定义构造函数,则 C++ 编译器会 自动生成****一个无参的默认构造函数,一旦用户显式定义编译器将不再生成

cpp 复制代码
class Date
{
public:
	/* // 如果用户显式定义了构造函数,编译器将不再生成
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	} */
	
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year; // 内置类型成员
	int _month;
	int _day;
};

int main()
{
	Date d1;
	Date d2;
	d1.Print();

	return 0;
}

一堆随机值,自动生成的构造函数好像啥也没干(编译器会干,只是我们看不见)。而且我咋知道有没有生成构造函数?

这是祖师爷的失误,这里应该初始化为0 C++标准没有规定要初始化

特征 6:

C++ 把类型分为2类:
1. 内置 / 基本类型: 语言本身定义的基础类型 int / char / double / 指针 ......
2. 自定义类型: struct / class 等类型

我们不写,编译器默认生成的构造函数:

内置类型不做处理(有些编译器会处理,但我们当做不处理)、自定义类型会去调用他们的默认构造

结论:
1. 一般情况,要自己写构造函数
2. 用编译器自动生成的就可以:
a. 内置类型成员都有缺省值,且初始化符合我们的要求
b. 全是自定义类型的构造,且这些类型都定义了默认构造(用栈实现队列,定义2个栈)


2. a. 1

C++ 11发布时,打了补丁:成员声明时,可以给缺省值(主要针对内置类型)

cpp 复制代码
class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	// 内置类型
	// C++11支持,这里不是初始化,因为这里只是声明,没有开空间
	// 这里给的是默认的缺省值,给编译器生成的默认构造函数用
	int _year = 1;
	int _month = 2;
	int _day = 3;
};

int main()
{
	Date d1;
	d1.Print();
	return 0;
}
cpp 复制代码
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year = 1;
	int _month = 2;
	int _day = 3;
};

int main()
{
	Date d1(2025, 6, 1); // 在这显式初始化了,不会用缺省值
	d1.Print();

	return 0;
}

此时必须自己写构造函数,否则:

补充:构造函数的调用问题

构造函数的定义很特殊:同名、无返回值、自动调用 构造函数的调用也很特殊

cpp 复制代码
class Date
{
public: // 写2个构造函数,构造函数可以重载。有多种初始化方式
	Date() // 可以无参
	{
		_year = 2025;
		_month = 6;
		_day = 1;
	}

	Date(int year, int month, int day) // 可以带参
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	// 内置类型
	// C++11支持,这里不是初始化,因为这里只是声明,没有开空间
	// 这里给的是默认的缺省值,给编译器生成的默认构造函数用
	int _year = 1;
	int _month = 2;
	int _day = 3;
};
cpp 复制代码
int main()
{
	Date d1(2025, 6, 1); // 构造函数的调用1:对象 + 参数列表
	Date d2; // 构造函数的调用2:对象不加列表

	d1.Print(); // 普通函数的调用:函数名 + 参数列表
	d2.Print();

	Date d3(); // 报警告
	// 原因:与函数声明冲突,编译器不好识别。
	// 这么写编译器可以看做构造函数的调用;也可以看做一个函数的声明
	// Date d1(2025, 6, 1); 这一看就不是函数声明。函数声明()里不是变量对象,是类型

	return 0;
}
cpp 复制代码
// 有人认为祖师爷脑子不清楚,下面这样写就没上面那么多事:对象调函数,变成正常的函数调用
int main()
{
	Date d1;    // 对象调函数。对象.函数名
	d1.Date(); // 实事是:报错:类型名称"Data"不能出现在类成员访问表达式的右侧

	Date d2;
	d2.Date(2025, 6, 1); // 实事是:报错

	return 0;
}

// 能这么写,为什么不这样写?: 
int main()
{
	Date d1;
	d1.Init(); // 实事是:报错		叫 Init 不是更香吗,为什么还搞 Data出来?

	Date d2;
	d2.Init(2025, 6, 1); // 实事是:报错

	return 0;
} // 回去了。对象实例化时,自动调用怎么办?

2. a. 2

cpp 复制代码
struct TreeNode
{
	TreeNode* _left;
	TreeNode* _right;
	int _val;
};

class Tree
{
private:
	TreeNode* _root; // 定义一棵树,最开始要有根节点
    // 能不能不写它的构造函数?	可以!
    // 直接不写肯定不行,默认生成的构造函数对内置类型不初始化
};

int main()
{
	Tree t1;
	return 0;
}
cpp 复制代码
class Tree
{
private:
	TreeNode* _root = nullptr;
};

灵活应变:

cpp 复制代码
struct TreeNode
{
	TreeNode* _left;
	TreeNode* _right;
	int _val;

	TreeNode(int val = 0) // 这里推荐自己写默认构造
	{
		_left = nullptr;
		_right = nullptr;
		_val = val;
	}
};

class Tree
{
private:
	TreeNode* _root = nullptr; // 定义一棵树,最开始要有根节点
};

int main()
{
	Tree t1;

	TreeNode n0;
	TreeNode n1(1);
	TreeNode n2(8);

	return 0;
}

2. b.

cpp 复制代码
class Stack
{
public:
	Stack(int capacity = 4) // 这些类型(Stack)都定义了默认构造
	{
        cout << "Stack(int capacity = 4)" << endl;
		_a = (DataType*)malloc(sizeof(DataType) * capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
    }

	~Stack()
	{ }

private:
	DataType* _a = nullptr;
	int _capacity;
	int _size = 0;
};

class MyQueue
{
private: // 自定义类型(Stack)成员
	Stack _pushst;
	Stack _popst;
};

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

特征 7:默认构造函数

无参的构造函数 全缺省的构造函数 都称为默认构造函数,并且默认构造函数 只能有一个**。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是** 默认构造函数

总结:不传参就可以调用的就是默认构造函数


构造函数虽可重载,但写全缺省最香:

cpp 复制代码
class Date
{
public:
	Date() // 无参
	{
		_year = 2025;
		_month = 6;
		_day = 1;
	}

	Date(int year = 1, int month = 1, int day = 1) // 全缺省
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	// 内置类型
	// C++11支持,这里不是初始化,因为这里只是声明,没有开空间
	// 这里给的是默认的缺省值,给编译器生成的默认构造函数用
	int _year = 1;
	int _month = 2;
	int _day = 3;
};

无参、全缺省语法上可同时存在,因为构成函数重载 但无参调用存在歧义,所以现实中不会同时存在

cpp 复制代码
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	// 内置类型
	// C++11支持,这里不是初始化,因为这里只是声明,没有开空间
	// 这里给的是默认的缺省值,给编译器生成的默认构造函数用
	int _year = 1;
	int _month = 2;
	int _day = 3;
};

int main()
{
	Date d1(2022);
	d1.Print();
	
	Date d2(2025, 6);
	d2.Print();

	return 0;
}
cpp 复制代码
class Date
{
public:
	Date(int year, int month = 1, int day = 1) // 不写成全缺省(没有默认构造)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	// 内置类型
	// C++11支持,这里不是初始化,因为这里只是声明,没有开空间
	// 这里给的是默认的缺省值,给编译器生成的默认构造函数用
	int _year = 1;
	int _month = 2;
	int _day = 3;
};

int main()
{
	Date d1; // 不传参数:报错:"Data": 没有合适的默认构造函数可用
             // 可以不传参的是默认构造,这里必须传参数
	d1.Print(); 

	return 0;
}

三. 析构函数

析构函数是特殊的成员函数,不是完成对象本身的销毁(不是销空间)。局部对象(在栈帧里,由系统完成)销毁工作是由编译器完成的

特征 1:析构函数名:~类名

特征 2:无参数(析构函数不能重载),无返回值

特征 3:对象生命周期结束(销毁)时,自动调用完成对象中 资源清理 工作

cpp 复制代码
class Stack
{
public:
	Stack(int capacity = 4) // 构造函数
	{	}

	~Stack() // 析构函数
	{
		cout << "~Stack()" << endl;
		if (_a)
		{
			free(_a);
			_a = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}

	/*void Destroy()
	{
		if (_a)
		{
			free(_a);
			_a = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}*/

private:
	void CheckCapacity()
	{ }

private:
	DataType* _a;
	int _capacity;
	int _size;
};

Stack 有了构造、析构,就不怕忘记写 初始化、清理函数 了,也简化了

特征 4:自动生成

一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数

1、内置类型成员不做处理 2、自定义类型会去调用它的析构函数

所以,上面的代码,不写自己写析构,编译器默认生成的析构,不会释放 _a指向的空间,内存泄漏

cpp 复制代码
class Stack
{
public:
	Stack(int capacity = 4) // 构造函数,功能:替代Init
	{	}

private:
	DataType _a[100];
	int _capacity;
	int _size;
};

写的是静态的会不会释放?
析构函数是释放 动态申请(堆)的资源。这种 静态的资源(栈)不用手动释放,出了作用域会自动销毁
只有堆上的要手动释放

总结

1、一般情况下,有动态申请资源,就需要显式写析构函数,来释放资源
2、没有动态申请的资源,不需要写析构
3、需要释放资源的成员都是自定义类型,不需要写析构,前提:类型都定义了析构函数

cpp 复制代码
// 1、栈是经典的需要写析构
~Stack()
{
    cout << "~Stack()" << endl;
    free(_a);
    _a = nullptr;
    _capacity = _size = 0;
}

// 2、日期类没有析构可写,没有申请资源
class Data
{
private:
    int _year;
    int _month;
    int _day;
};

// 3、默认生成的析构会自动调用析构
class MyQueue
{
private:
    Stack _pushst;
    Stack _popst;
};

本篇的分享就到这里了,感谢观看 ,如果对你有帮助,别忘了点赞+收藏+关注

小编会以自己学习过程中遇到的问题为素材,持续为您推送文章

相关推荐
小白学大数据几秒前
基于Python的新闻爬虫:实时追踪行业动态
开发语言·爬虫·python
freed_Day2 分钟前
python面向对象编程详解
开发语言·python
刚入坑的新人编程9 分钟前
暑期算法训练.9
数据结构·c++·算法·leetcode·面试·排序算法
I'mSQL20 分钟前
C#与WPF使用mvvm简单案例点击按钮触发弹窗
开发语言·c#·wpf
love530love1 小时前
命令行创建 UV 环境及本地化实战演示—— 基于《Python 多版本与开发环境治理架构设计》的最佳实践
开发语言·人工智能·windows·python·conda·uv
陪我一起学编程1 小时前
MySQL创建普通用户并为其分配相关权限的操作步骤
开发语言·数据库·后端·mysql·oracle
麦子邪1 小时前
C语言中奇技淫巧04-仅对指定函数启用编译优化
linux·c语言·开发语言
破刺不会编程2 小时前
linux线程概念和控制
linux·运维·服务器·开发语言·c++
henreash2 小时前
NLua和C#交互
开发语言·c#·交互