lesson03:类和对象(中)

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

2.构造函数

3.析构函数

4.拷贝构造函数

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

空类(类中一个成员都没没有)会有成员函数吗?

其实是有的!如果我们在类中什么都不写,编译器会自动生成6个默认成员函数(用户什么都不写,编译器自动生成的函数叫默认成员函数)。

它们分别是:

1.构造函数

2.析构函数

3.拷贝构造函数

4.赋值运算符重载函数

5.取地址操作符重载函数

6.const取地址操作符重载函数

2.构造函数

2.1定义

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
	void Init(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;
	d1.Init(2022, 7, 5);
	d1.Print();
	Date d2;
	d2.Init(2022, 7, 6);
	d2.Print();
	return 0;
}

构造函数是一个特殊的成员函数,是用来给对象的数据成员初始化的。(用处类似于上面的自定义成员函数Init,注意:这个Init不是构造函数!)

2.2特性

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
	// 1.无参构造函数
	Date()
	{}

	// 2.带参构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

void TestDate()
{
	Date d1; // 调用无参构造函数
	Date d2(2015, 1, 1); // 调用带参的构造函数
}

int main()
{
	TestDate();
	return 0;
}

构造函数是特殊的成员函数,构造函数虽然叫构造函数,但它不是用来开空间创建对象的,而是用来初始化对象的。

1.构造函数函数名和类名相同

2.构造函数无返回值,也不用在定义时写返回值类型(简单的来说,连void都不要写,返回值类型直接空着)

3.实例化对象时编译器自动调用构造函数。(这一步是一定进行的,实例化对象时一定会调用构造函数),在对象整个生命周期内只能调用一次,且构造函数不能显式调用

4.构造函数可以重载。

5.在自定义函数TestDate中:

5.1因为d1后面什么都没写,所以实例化(创建)对象d1时,编译器将调用无参构造函数或全缺省构造函数,也就是第一个构造函数。

那为什么只能调用这两种呢?

解答:因为没有传递参数,其他种类的构造函数都要传参数啊。

5.2因为d2后面在括号里写了三个参数(不要问为什么这么写,这是规定),这三个参数将传递给构造函数,所以将调用需传递三个参数的构造函数,也就是第二个函数。

那为什么不能调用给其他的构造函数呢?

解答:以无参构造函数为例,也就是第一个构造函数,他没有形参去接受这两个参数

那么一般的多参构造,以有4个参数的构造函数为例,它也是不可以的,传递的参数不够

5.3那么所有的多参构造都不可以吗?

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
	// 1.无参构造函数
	Date()
	{}

	// 2.带参构造函数
	Date(int year, int month, int day, int n = 5)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d(1, 2, 3);
	return 0;
}

解答:其实不是的,像上面这样的半缺省构造函数可以只传三个参数,那么它就是可以的。

注意:看第8行和第12行,构造函数函数名前什么都不写,不要写类型

	Date d3();

注意:如果实例化对象时不给构造函数传参,一定不能写小括号。

这个是函数的声明,如果实例化对象这样写,无法和函数声明区分!

	Date d3(1, 2, 3);

但是,像这样给构造函数传参的,是可以写小括号的,函数声明要写明类型,而上面的代码没有写类型,所以这里是可以和函数声明区分开的。

6.如果用户显示定义了构造函数(就是你自己写了构造函数),编译器将不会生成默认构造函数(默认构造也没有形参,属于一种无参构造)。

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
private:
	int _year;
	int _month;
	int _day;
};

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

前面说过,实例化(创建)对象时编译器一定会自动调用构造函数,但是这里我们没有定义构造函数,会不会报错呢?

答案是不会的,Date类中有一个构造函数,它由编译器生成,我们是看不见的。在实例化d3时调用的是这个由编译器生成的构造函数,这个默认生成的构造函数是无参构造函数,不需要传参。

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
private:
	int _year;
	int _month;
	int _day;
public:
	Date (int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
};

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

这段代码是编不过去的,为什么错了呢?编译器不是会生成默认的无参构造函数吗?

解答:要注意,由于我们自己定义了构造函数,编译器就不再生成默认构造函数了!现在只有一个要传三个参数的构造函数,必须传三个参数。 

7.默认构造函数能干什么呢?

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
	void print()
	{
		cout << _year << ' ' << _month << ' ' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d3;
	d3.print();
	return 0;
}

在visual studio 2022社区版上,上面的代码运行后,输出的是随机值,所以默认构造函数貌似没有初始化的功能,那么这个默认构造函数有什么用呢?

解答:c++把类型分为了内置类型(编译器自带的,如int,char等)和自定义类型(自己定义的,如类类型,结构体类型等,注意:凡是指针类型都是内置类型,如类指针类型,结构体指针类型也是内置类型),默认构造函数会自动调用自定义类型成员的构造函数

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

这里打印了“Time()”,说明自定义类型成员_t的无参构造函数确实被调用了。

总结:在实例化(创建)对象时,构造函数对内置类型成员不作处理,对自定义类型成员调用它的无参构造函数。

补充:由于标准未说明不允许对内置类型作处理,所以有些编译器会对内置类型作处理;但是标准也未说明一定要对内置类型作处理,有些编译器是不会对内置类型作处理的。

8.考虑到函数重载和调用歧义问题,无参构造和全缺省构造的总数最好不要超过1个

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
	Date()
	{
		_year = 1900;
		_month = 1;
		_day = 1;
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
// 以下测试函数能通过编译吗?
void Test()
{
	Date d1;
}
int main()
{
	Test();
	return 0;
}

如这里在实例化d1时,因为d1后面什么都没写,所以编译器要调用d1的无参构造函数或全缺省构造函数,由于这样的函数有两个,编译器不知道用哪个,所以这里就报错了。

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
	Date()
	{
		_year = 1900;
		_month = 1;
		_day = 1;
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
// 以下测试函数能通过编译吗?
void Test()
{
	Date d1(1);
}
int main()
{
	Test();
	return 0;
}

但是如果我们在实例化对象d1时传一个参数,就可以躲过调用歧义,就可以编过去了,但是极度不建议这么写!!!

9.如果实例化(创建)对象时,当中有自定义类型成员,且该成员所对应的类没有无参构造或全缺省构造,编译器会直接报错。

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Time
{
public:
	Time(int a)
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

 但是,自定义类型成员并不是必须是含有无参构造函数或全缺省构造函数的对象,后面会讲到初始化列表,可以解决这个问题。

可能很多小伙伴已经想到办法了,像下面这样写不就行了?

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Time
{
public:
	Time(int a)
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t(1);
};
int main()
{
	Date d;
	return 0;
}

 要注意啊,Time _t(1);这种写法是定义对象,类的声明中是不能定义数据对象或变量的,不过在类的成员函数的定义中是可以定义对象或变量的。

3析构函数

3.1概念

析构函数与构造函数的功能相反,构造函数不是创建对象的,析构函数也不是销毁对象的,析构函数的作用是完成对象中资源的清理工作,在对象生命周期结束时由函数自动调用

3.2特性

1.析构函数名就是在类名前加上~

2.无参数,无返回值类型(这里和构造一样,连void也不要写)

3.一个类只能有一个析构函数(因为析构函数没有参数,无法实现重载),若未显式定义,编译器会自动生成默认析构函数

4.对象生命周期结束时编译器自动调用析构函数。

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
	Date()
	{
		cout << "Date()" << endl;
		_year = 1900;
		_month = 1;
		_day = 1;
	}
	~Date()
	{
		cout << "~Date()" << endl;
		_year = 0;
		_month = 0;
		_day = 0;
	}
private:
	int _year;
	int _month;
	int _day;
};
void test()
{
	Date d;
}
int main()
{
	test();
	return 0;
}

对象d是在test函数中定义的局部变量,出函数后,自动调用析构函数;在这里,主函数并未结束,但已经调用了对象d的析构函数,可见,对象的析构函数不是一定在主函数结束时调用的。

5.默认的析构函数和默认的构造函数类似,对内置类型不做处理,对自定义类型自动调用它的析构函数。

补充:构造函数都会对自定义类型成员调用它的构造函数;析构函数都会对自定义类型成员调用它的析构函数。这样的特性并不是默认构造和默认析构所特有的,自定义的也有。

6.如果类中没有申请资源(如申请空间,打开文件等),析构函数没必要写,直接使用编译器生成的默认析构函数;有资源申请时一定要写,否则会造成内存泄漏等问题。

4.拷贝构造函数

4.1概念

拷贝构造函数时一个特殊的构造函数,主要体现在形参不同,它的作用也是初始化对象,它也拥有构造函数的特性(它也能对自定义类型成员自动调用它的构造,但这里调用的构造比较特殊,是它的拷贝构造)。

4.2特性

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

2.拷贝构造函数只有一个形参,且一定是本类类型的引用,使用传值传参编译器会报错,因为会引发无穷递归。

3.拷贝构造函数不仅可以用普通构造函数的方式使用,而且可以用初始化普通变量的方式使用(如int型变量,char型变量等),如下图。

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//Date(const Date d)//错误写法:编译报错,会引发无穷递归
	Date(const Date& d)// 正确写法
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void print()
	{
		cout << _year << ' ' << _month << ' ' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d2(2024, 4, 19);
	//Date d1(d2);与Date d1 = d2;是等价的,一般情况下写第二种,意思更加明确
	//Date d1(d2);
	Date d1 = d2;
	d1.print();
	return 0;
}

注意:

Date d1;
Date d2;
d2 = d1;

向这样分开写用的就不是拷贝构造函数了,而是使用了默认赋值运算符重载函数(后面会讲)。

3.若未显式定义拷贝构造函数,编译器会生成默认的拷贝构造函数

那默认的拷贝构造也和默认的构造一样吗?也是对内置类型不做处理吗?

解答:默认的拷贝构造函数会将参数按内存存储按字节序完成拷贝,这种拷贝叫浅拷贝,也叫值拷贝。顾名思义,就是将数据成员的值拷贝过去。

4.编译器生成的默认拷贝构造已经能完成拷贝了,还要显式实现吗?

解答:看情况,例如以下的就需要。

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class arr
{
private:
	int* a;
public:
	arr()
	{
		a = (int*)malloc(10 * sizeof(int));
	}
};
int main()
{
	arr a1;
	arr a2 = a1;
	return 0;
}

可以看到,对象a1和对象a2中数据成员,也就是指针变量a是一样的,两个指针指向了同一块空间,这不是我们想要的,我们希望的是让编译器再开一块空间的。

有人就要问了,这也拷贝成功了,有什么不妥吗?

解答:这两个对象的指针变量a指向的是同一块空间,它们是共用这块空间的,会相互影响的。

所以这里需要我们显式写拷贝构造函数,手动开空间,完成深拷贝。

5.拷贝构造函数典型调用场景:

场景一:使用已存在对象创建新对象

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
private:
	int _year;
	int _month;
	int _day;
public:
	Date(int year = 0, int month = 0, int day = 0)
	{
		cout << "Date()" << endl;
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
		_year = d._day;
		_month = d._month;
		_day = d._day;
	}
};
void test(Date d)
{

}
int main()
{
	Date d;
	Date d1 = d; //使用已存在对象创建新对象
	return 0;
}

场景二:函数传参时传递对象

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
private:
	int _year;
	int _month;
	int _day;
public:
	Date(int year = 0, int month = 0, int day = 0)
	{
		cout << "Date()" << endl;
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
		_year = d._day;
		_month = d._month;
		_day = d._day;
	}
};
void test(Date d)
{

}
int main()
{
	Date d;
	test(d); //使用已存在对象创建新对象
	return 0;
}

场景三:函数返回值返回对象

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
private:
	int _year;
	int _month;
	int _day;
public:
	Date(int year = 0, int month = 0, int day = 0)
	{
		cout << "Date()" << endl;
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
		_year = d._day;
		_month = d._month;
		_day = d._day;
	}
};
Date test(Date d)
{
	return d;//函数返回值返回对象
}
int main()
{
	Date d;
	test(d); //使用已存在对象创建新对象
	return 0;
}

  • 31
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

INUYACHA

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值