【C++】类和对象(中)

目录

一、类的六个默认函数

注意点:

二、构造函数 

定义:

注意事项:

特征:

那么,构造函数有什么作用呢?

具体实现例子:

 补充:

补充一:

补充二:

 补充三:

补充四: 

三、析构函数

定义:

特征:

  补充: 

四、拷贝构造函数

场景导入:

拷贝构造函数定义:

特征:

具体例子:

例一:

以上这段代码无法跑过。

解决办法:

例二:

传值传参的注意事项:

小补充:

补充1:

补充2: 

实现一个函数,获取任意天后的日期。

补充:


一、类的六个默认函数

以下这个思维导图就是今天这篇博客的大纲了。

注意点:

以上6个默认的成员函数,假如我们不写的话,编译器会自动生成一个默认的。

但是我们实现了,其编译器就会调用我们实现的,就不会使用默认生成的了。


二、构造函数 

定义:

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。

注意事项:

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

特征:

1.函数名与类名相同。
2.无返回值。
3.对象实例化时编译器自动调用对应的构造函数。
4.构造函数可以重载。(说明一个类可以有多个构造函数


那么,构造函数有什么作用呢?

其实,它的主要作用就是补充了C语言中,我们在实例化一个对象的时候,总是容易忘记对其进行初始化的操作了。

具体实现例子:

#include<iostream>

using namespace std;



class Bag {

public:
	//构造函数,无参
	Bag() {
		_pocket = nullptr;
		_book = _pencil = 0;
	}

	//构造函数,有参
	Bag(int n) {
		_pocket = (int*)malloc(sizeof(int) * n);
		if (nullptr == _pocket) {
			
			perror("malloc失败");
			exit(-1);
		}

		_book = n;
		_pencil = 1;//至少得有一支笔吧

	}



	void Init(int book ,int pencil) {

		this->_book = book;//这里的this是默认存在的,我们可以手动加
		this->_pencil = pencil;
		
	}

	void Destory() {

		//这里我们不实现Destory的具体功能,只是拿来做个参照。
		cout << "Destory()启动了!" << endl;
	}

	void Add(int x,int y) {
		_book += x;
		_pencil += y;
	}


private:
	int* _pocket;//包的袋子的个数,用数组来存储

	int _book;//声明,口头上的,没有开辟存储空间
	int _pencil ;


};



int main() {

	//构造函数的有参形式
	Bag b1(4);

	
	Bag b1;//正确构造函数的无参形式

       //Bag b1();构造函数的无参形式不能这么写,编译器无法识别这是一个普通函数还是默认的构造函数。


	b1.Add(3, 4);
	b1.Add(2, 1);

	b1.Destory();


	return 0;
}

 补充:

补充一:

class Bag {

public:

	Bag() {
		_pocket = nullptr;
		_book = _pencil = 0;
	}

	Bag(int n = 4) {
		_pocket = (int*)malloc(sizeof(int) * n);
		if (nullptr == _pocket) {

			perror("malloc失败");
			exit(-1);
		}

		_book = n;
		_pencil = 1;//至少得有一支笔吧

	}

private:
	int* _pocket;//包的袋子的个数,用数组来存储

	int _book;//声明,口头上的,没有开辟存储空间
	int _pencil;


};


int main() {

	//构造函数的有参形式
	Bag b1(4);


	Bag b1;//正确构造函数的无参形式

	


	return 0;
}

如上,假如我们在定义构造函数的时候,

一个构造函数是无参的。

一个是有参的,但是它的参数是全缺省的。

这样,我们在main函数要调用无参的构造函数时,会发生什么呢?

运行结果:

根据这里的结果,我们了解到,假如在定义一个有参的构造函数的时候,要注意全缺省,容易和无参的构造函数混淆。


补充二:

#include<iostream>

using namespace std;


class Bag {

public:

    //默认生成的构造函数,对内置类型不处理。
	void Print() {
		cout << "书包里有" << _pencil << "支笔" << endl;
	}

private:

    //基本类型/内置类型
	int* _pocket;//包的袋子的个数,用数组来存储

	int _book;//声明,口头上的,没有开辟存储空间
	int _pencil;


};

int main() {

	Bag b1;//正确构造函数的无参形式
	b1.Print();	

	return 0;
}

运行结果:

这里我们就有疑问了?

我们这里没有自己来实现构造函数

根据以上总结我们可知,编译器会自动生成一个默认的构造函数。但是看起来其好像没啥作用啊!其结果由上面这一段代码及输出结果可知其仍然是随机值

这其实是C++设计的一个没有考虑到的点,的确在实际使用中存在一点问题

其解决办法在C11给出了,即:内置类型成员变量在类中声明时可以给默认值。

C++把类型分为内置类型(基本类型)和自定义类型

1.内置类型就是语言提供的数据类型,如:int,char,float....

2.自定义数据类型就是我们自己使用的struct/class/union等

3.且C++早期语法说默认生成的构造函数对:

   3.1内置类型成员不做处理

   3.2  而对自定义类型的成员,会去调用它的默认构造(不用传参数的构造)

解答:

至此,我们总算了解了为啥上面的代码 _pencil 最终运行出来的结果为那么一大串数字了。

原因就是其类型是内置成员类型int,编译器对其不做处理的。


 补充三:

对自定义类型的成员,会去调用它的默认构造(不用传参数的构造)

其上具体实现的实例:

#include<iostream>

using namespace std;


class Bag {

public:

	/*Bag() {
		_pocket = nullptr;
		_book = 10;
		_pencil = 5;
	}*/

	void Print() {
		cout << "书包里有" << _pencil << "支笔" << endl;
	}

private:
	int* _pocket;//包的袋子的个数,用数组来存储

	int _book;//声明,口头上的,没有开辟存储空间
	int _pencil;


};

class Table {

public:
	//默认生成的构造函数,对自定义类型,会调用其默认构造函数
	void Lay() {

	}

	Bag _B1;
	Bag _B2;


};

int main() {

	Bag b1;//正确构造函数的无参形式
	b1.Print();	

	Table T;
	T.Lay();


	return 0;
}

当我们调试到Table类中的时候,通过监视可以看到:

其不再是一个随机数了,而是-1.

补充四: 

以上代码无法编译通过。

因为对象在实例化的时候一定会调用构造函数,如果我们自己不定义,编译器就是自动生成一个默认的。但如果我们自己定义了,就会调用我们自己定义的。

以上由于我们自己定义了构造函数,而且是带参数的,所以我们在实例化对象的时候,必须要带参数。 


三、析构函数

定义:

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

特征:

1.析构函数名是在类名前加上字符 ~ (小波浪号)


2.无参数无返回值类型。


3.一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载


4.对象生命周期结束时,C++编译系统系统自动调用析构函数。

具体例子:

#include<iostream>

using namespace std;



class Bag {

public:
	//构造函数,无参
	Bag() {
		_pocket = nullptr;
		_book = _pencil = 0;
	}

	//构造函数,有参
	Bag(int n) {
		_pocket = (int*)malloc(sizeof(int) * n);
		if (nullptr == _pocket) {
			
			perror("malloc失败");
			exit(-1);
		}

		_book = n;
		_pencil = 1;//至少得有一支笔吧

	}



	void Init(int book ,int pencil) {

		this->_book = book;//这里的this是默认存在的,我们可以手动加
		this->_pencil = pencil;
		
	}

	void Destory() {

		//这里我们不实现Destory的具体功能,只是拿来做个参照。
		cout << "Destory()启动了!" << endl;
	}

	void Add(int x,int y) {
		_book += x;
		_pencil += y;
	}

	//析构函数
	~Bag() {

		free(_pocket);
		_pocket = nullptr;
		_pencil = _book = 0;
	}


private:
	int* _pocket;//包的袋子的个数,用数组来存储

	int _book;//声明,口头上的,没有开辟存储空间
	int _pencil ;


};



int main() {

	//构造函数的有参形式
	Bag b1(4);

	
	Bag b1;//正确构造函数的无参形式

       //Bag b1();构造函数的无参形式不能这么写,编译器无法识别这是一个普通函数还是默认的构造函数。


	b1.Add(3, 4);
	b1.Add(2, 1);

	//b1.Destory();//由于我们这里已经定义好了析构函数,所以就不用调用Destory函数了
	             //编译器在对象销毁时会自动调用析构函数了


	return 0;
}

  补充: 

一起来看看下面这段代码:

#include<iostream>

using namespace std;


class Bag {

public:

	~Bag() {
		cout << "书包已经放好了!" << endl;
	}


private:
	int* _pocket;//包的袋子的个数,用数组来存储

	int _book;//声明,口头上的,没有开辟存储空间
	int _pencil;


};

class Table {

public:

	int _cup;
	int _fruit;

	Bag _B1;


};

int main() {

	Table T;

	return 0;
}

最终输出结果为书包已经放好了!

在main方法中根被没有直接创建Bag类的对象,为什么最后会调用Bag类的析构函数呢?

因为:main方法中创建了Table对象T,而T中包含三个成员变量,其中_cup 、_fruit两个是内置成员类型,销毁时不需要资源清理,最后系统直接将其内存回收即可。而_B1是Bag类对象,所以在T销毁时,要将其内部包含的Bag类的_B1对象销毁,所以要调用Bag类的析构函数。

但,注意,main函数中不能直接调用Bag类的析构函数,因实际要释放的是Table类对象,所以main函数会调用Table类的编译器默认提供的析构函数,用来保证在Table对象销毁时,其内部的自定义对象可以正确的销毁掉。

小总结:

如果类中没有申请资源时,析构函数可以不写。但要是有申请,则必须写上。


四、拷贝构造函数

场景导入:

在实际应用中,我们可能会需要要创建一个与已存在对象一样的新对象。那么有没有什么简单的方法呢?

拷贝构造函数定义:

只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

特征:

1.拷贝构造函数是构造函数的一个重载形式。


2. 楼贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错因为会引发无穷递归调用。
 

具体例子:

例一:

class Bag {
	
public:
	Bag(int book = 10, int pencil = 2) {
		_book = book;
		_pencil = pencil;
	}
	
	Bag(Bag B) {

		_book = B._book;
		_pencil = B._pencil;
	}

	void Print() {
		cout << "书包已经放好了!" << endl;
	}
	
private:
	int* _pocket;//包的袋子的个数,用数组来存储
	
	int _book;//声明,口头上的,没有开辟存储空间
	int _pencil;
	
	
};
int main() {
	Bag B1(1,2);
	Bag B2(B1);
	
	return 0;
}
以上这段代码无法跑过。

会导致无穷递归调用。

其实也比较容易理解为什么会产生无穷递归调用了,

举个例子,当然不一定能编译出来,但是用于辅助理解还行。

就是假如有一个函数Func(),这时,假如在main函数中写了Func(Func());

这一段代码,是不是就会产生无穷递归了。

解决办法:


例二:

传值传参的注意事项:
class Bag {
	
public:
	Bag(int book = 10, int pencil = 2) {
		_book = book;
		_pencil = pencil;
	}
	
	Bag(Bag& B){

		_book = B._book;
		_pencil = B._pencil;
	}

	void Print() {
		cout << "书包已经放好了!" << endl;
	}
	
private:
	
	int _book;//声明,口头上的,没有开辟存储空间
	int _pencil;
	
	
};


//传值传参
//内置类型,编译器可以直接拷贝
//自定义类型的拷贝,需要调用拷贝构造
void Fuc1(Bag b) {

}

//传引用传参
void Fuc2(Bag& b) {

}
int main() {
	Bag B1(1,2);
	Fuc1(B1);
	Fuc2(B1);
	
	return 0;
}

以上Fuc1传值传参的代码运行没问题。

主要是因为我去掉了Bag类中原有的int*成员变量,这样就可以实现浅拷贝了 。

可要是我没去掉Bag类中的int*成员变量,则需要使用深拷贝。不过这里使用拷贝构造也能解决,因为引用的话就是直接公用一块内存空间了,避免了深拷贝。

也可以直接使用引用传参来解决,更为方便。


小补充:

补充1:

 int main() {


    Bag B1(1,2);
    Bag B2 = B1;//这样也可以实现对象拷贝了。
    
    return 0;
}

补充2: 

注意,在实现拷贝构造时,注意参数要设置为const。

不然,可能会产生赋值错误的情况。


实现一个函数,获取任意天后的日期。

#include<iostream>
#include <assert.h>

using namespace std;


class Data {

public:
	Data (int year,int month,int day) {
		_year = year;
		_month = month;
		_day = day;
	}

	int GetMonthDay(int year, int month) {
		assert(month > 0 && month < 13);

		int monthArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };

		if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400) == 0) {
			return 29;
		}
		else {
			return monthArray[month];
		}

	}


	Data GetAfterDay(int x) {

		Data tmp = *this;
		tmp._day += x;

		while (tmp._day > GetMonthDay(tmp._year, tmp._month)) {

			//进位
			tmp._day -= GetMonthDay(tmp._year, tmp._month);
			++tmp._month;

			if (tmp._month == 13) {
				tmp._year++;
				tmp._month = 1;
			}
		}

		return tmp;//this指的就是本身,就是这个Data类
	}

	void Print() {
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

private:
	int _year;
	int _month;
	int _day;

};

int main() {

	Data d1(2023, 10, 30);
	Data d2 = d1.GetAfterDay(100);

	d1.Print();
	d2.Print();

}

运行结果:

补充:

    //相当于  "+" 的形式,不会改变自身的值
	//使用的是传值返回。
	//且注意返回的不是tmp,因为传值返回时作用域会被销毁,其内部数据都会被清理
	//所以这里return tmp的下一步会跑到Data类的拷贝构造中去产生一个临时对象了来使用了

    //这里不会改变d1的原来数值,但会增加空间的开辟
	Data GetAfterDay01(int x) {

		Data tmp = *this;
		tmp._day += x;

		while (tmp._day > GetMonthDay(tmp._year, tmp._month)) {

			//进位
			tmp._day -= GetMonthDay(tmp._year, tmp._month);
			++tmp._month;

			if (tmp._month == 13) {
				tmp._year++;
				tmp._month = 1;
			}
		}

		return tmp;
	}




//是+=的形式的。
//并且这里可以使用引用返回,因为其中的*this在函数掉用完后不会被销毁,节省了拷贝构造的空间

//但这里仍然会改变d1的数值,但减少了空间的开辟。
 Data& GetAfterDay02(int x) {

		_day += x;

		while (_day > GetMonthDay(_year, _month)) {

			//进位
			_day -= GetMonthDay(_year, _month);
			++_month;

			if (_month == 13) {
				_year++;
				_month = 1;
			}
		}

		return *this;
	}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值