「C++」类和对象(2)

欢迎大家来到小鸥的类和对象第二篇博客~

目录

类的默认成员函数

构造函数

构造函数的特点:

析构函数

析构函数的特点:

拷贝构造函数

拷贝构造的特点:

结语:


本篇会着重讲解类和对象中的难点:类的默认成员函数

由于篇幅原因,本篇只讲解构造,析构以及拷贝构造三个默认成员函数,运算符重载等内容将结合Date类在下一篇讲解

类的默认成员函数

默认成员函数就是指我们没有手动显示实现的函数,编译器会在实例化类的对象时自动生成这些默认成员函数。

一个类在我们不写的情况下,会自动生成6个默认成员函数(C++11以后还增加了两个默认成员函数:移动构造和移动赋值),默认成员函数重要且复杂,学习目标主要有两个:

  1. 我们不显示实现时,编译器默认生成的函数的行为是什么,确认是否满足需求;
  2. 当编译器生成的默认成员函数不满足需求时,学会显示实习这些函数。

构造函数

构造函数虽然叫构造,但它的作用并不是用来创建对象的,而当创建一个类类型的对象时,每个对象在创建完成后都要进行初始化,构造函数的作用就在于此,将创建好的实例化对象进行初始化。

构造函数的特点:

  1. 函数名和类名相同;

  2. 无返回值(区别于返回值为void的无返回值函数,构造函数连void都不用写出来,只有单独的函数名);

  3. 对象在实例化时会自动调用该类对应的构造函数;

  4. 构造函数可以重载;

  5. 若类中没有显示定义构造函数,那么C++编译器将会自动生成一个无参的构造函数,若显示定义构造函数则编译器不会生成;

  6. 无参构造函数、全缺省构造函数、编译器自动生成的构造函数都叫做默认函数,而不是单指编译器默认生成的构造函数为默认构造函数。

    三个默认 构造函数,在一个类类型中有且仅有一个,不能同时存在。且要注意,无参函数和全缺省函数调用时可能会产生歧义。

    总结起来就是:不传实参就可以调用的构造函数为默认构造函数;

  7. 编译器生成的默认构造函数为无参函数且对内置类型(说明: C++把类型分成内置类型(基本类型)和⾃定义类型。 内置类型就是语⾔提供的原⽣数据类型,如:int/char/double/指针等, ⾃定义类型就是我们使⽤class/struct等关键字⾃⼰定义的类型。)的成员变量没有硬性的初始化要求,即对内置类型是否初始化是不确定的,看编译器。而对于自定义类型的成员变量,会要求调用这个成员变量的默认构造函数来初始化。如果没有就会报错

#include <iostream>
using namespace std;
class hdmo
{
public:
	//1.无参数构造函数
	/*hdmo()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}*/
	//2.含参构造函数
	/*hdmo(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}*/
	//3.全缺省构造函数
	hdmo(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << '.' << _month << '.' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	hdmo h1;//不传递实参时不需要加()
	//hdmo h1();//(错误的写法)函数声明还是定义对象?
	h1.Print();

	hdmo h2(2024, 3, 4);//当给构造函数传递实参时,直接在定义对象时后面加上(实参)
	h2.Print();
	return 0;
}
  • 注意点:

    • 只有在要给构造函数传递实参时才在定义对象的后面使用 (实参) 用于传递;
    • 无参数传递的时候,也不能在定义对象处加(),因为这样会和函数声明分不开,产生歧义
  • 由于编译器自动生成的构造函数对于内置类型是否初始化不确定的,所以大多数时候构造函数都是要自己实现的

析构函数

我们知道构造函数是对成员变量的初始化,那么马上来到的析构函数则是完成对象中资源的清理释放⼯作(不是销毁),在对象的生命周期结束前,会自动调用析构函数来释放需要释放的空间(比如动态申请的空间);当对象不存在需要释放的变量时,则可以不用写。

需要注意的是:像局部对象或者函数都是存在栈帧的,当栈帧销毁时,(如内置类型)就自动释放了空间了,此时是不需要用到析构函数的;需要用到的场景一般都为我们自己申请空间的情况,即需要用到free的情况。

析构函数的特点:

  1. 析构函数就是相对于构造函数在类名前面加上~
  2. 没有参数和返回值(和构造函数一样,void也不需要加)。
  3. 每个类只有一个析构函数,若未显示定义,则系统自动生成默认析构函数。
  4. 对象的生命周期结束时,自动调用
  5. 和构造函数相似,编译器自动生成的析构函数对内置类型不做处理,当存在自定义类型时,则会调用对应的析构函数。
  6. 不论是否显示定义析构函数,都会自动调用自定义类型的析构函数,没有则会报错;即便显示定义的析构函数中不包含调用自定义类型析构函数的语句,系统也会自动去调用自定义类型的析构函数防止内存泄漏,即自定义类型的析构函数,无论如何都会调用
  7. 当类中没有申请空间时,析构函数就可以不写,直接使用编译器自动生成的;但当存在空间的申请时,则必须显示定义析构函数,否则将导致内存泄漏。
  8. 当一个局部域存在多个需要析构的对象时,C++规定后定义的先析构(和数据结构中栈的先进后出相似)。
//初始化栈
#include <iostream>
using namespace std;
class stack
{
public:
	//析构函数(释放空间)
	~stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
	//构造函数(变量初始化)
	stack(int n = 4)
	{
		_a = (int*)malloc(sizeof(int) * n);
		if (_a == nullptr)
		{
			perror("malloc fail!");
			return;
		}
		_capacity = n;
		_top = 0;
	}
private:
	int* _a;
	size_t _top;
	size_t _capacity;
};

class MyQueue
{
public:
	//自动生成构造函数,两个stack变量的初始化就会自动调用stack类中的默认构造函数,即stack(int n = 4);
	//所以MyQueue类中的构造函数就不需要手动实现了

	//自动生成的析构函数会自动调用stack类中的析构函数来释放两个stack类的变量
	~MyQueue()
	{
		;//即便显式定义时不调用stack的析构函数也不会出错,因为编译器也会自动去调用。
	}
private:
	stack pushstack;
	stack popstack;
};

拷贝构造函数

定义:如果一个构造函数的第一个参数是自身类类型的引用,且其他的所有参数都有默认值(缺省参数),则这个构造函数也叫做拷贝构造函数,即拷贝构造函数是一个特殊的构造函数。

拷贝构造的作用就是在初始化时,解决想要使用相同类型对象来初始化新对象的情况

拷贝构造的特点:

  1. 拷贝构造函数也是构造函数的一个重载函数;
  2. 拷贝构造函数的第一个参数必须为类类型对象的引用,如果使用传值调用编译器会直接报错,因为语法逻辑将导致引发无穷递归(C++中包含类对象的传值调用,拷贝数据时会先调用对象对应的拷贝构造函数)。
  3. C++规定自定义类型对象进行拷贝行为时必须调用拷贝构造函数,所以自定义类型传值传参和传值返回都会调用拷贝构造。
  4. 当没有显示定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝会对内置类型进行值拷贝(浅拷贝),自定义类型则会调用它的拷贝构造。
  5. 当一个类中只包含了内置类型,且没有指向资源时(动态内存申请),就可以不显示写出拷贝构造,编译器自动生成的拷贝构造就可以满足需求;但如果类中有自定义类型或者指向资源的内容,就需要我们显示的写出拷贝构造函数。若一个类中显示写出了析构函数并释放了资源,则它就需要显示的写出拷贝构造函数。
  6. 函数中的传值返回会产生一个临时对象调用拷贝构造,传引用返回返回的是返回对象的别名,不会产生拷贝。若返回的对象是一个当前函数局部域的一个局部对象,那么函数结束时该对象就销毁了,此时传引用返回就会出错,因为该引用所代表的对象已经销毁,此时就类似一个野指针的错误。虽然传引用返回可以减少拷贝所消耗的资源,但是前提要搞清楚返回对象的作用域。
#include <iostream>
using namespace std;
typedef int STDataType;

class stack
{
public:
	//构造函数(初始化栈)
	stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(int) * n);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		_top = 0;
		_capacity = n;
	}
	//析构函数(释放动态空间)
	~stack()
	{
		free(_a);
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}
	//拷贝构造
	stack(const stack& st)
	{
		//创建一个大小相同的新栈,将原来栈的内容复制到新栈
		_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
		if (_a == nullptr)
		{
			perror("malloc fail!");
			return;
		}
		memcpy(_a, st._a, sizeof(STDataType) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}
	void Push(STDataType x)
	{
		//判断空间是否足够
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));
			if (tmp == nullptr)
			{
				perror("realloc fail!");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}
private:
	STDataType* _a;
	int _top;
	int _capacity;
};
//两个栈实现队列
class MyQueue
{
public:
	//
private:
	stack pushst;
	stack popst;
};

int main()
{
	stack st1;
	st1.Push(1);
	st1.Push(2);
	//stack st2 = st1;
	stack st2(st1);//调用拷贝构造

	MyQueue mq1;
	//MyQueue中自动生成的拷贝构造会自动调用stack的拷贝构造
	MyQueue mq2 = mq1;

	return 0;
}

结语:

本篇的讲解就到这里,有不足的地方请大家指正,互相成长,下一篇将结合Date类实现运算符重载内容,欢迎再次到来~

个人主页:海盗猫鸥-CSDN博客

本期专栏:C++_海盗猫鸥的博客-CSDN博客

感谢大家关注~

  • 17
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值