前言:
假如一个类中既没有成员变量也没有成员函数,那么这个类就是空类,空类并不是什么都没有,因为所有类都会生成如下6个默认成员函数:
一、构造函数
1、构造函数的定义及其特性
对于日期类对象,我们可能会忘记调用Init函数进行初始化,C++为了解决这个问题,引入构造函数进行初始化。
#include<iostream>
using namespace std;
class Date
{
private:
int _year;
int _month;
int _day;
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
};
int main()
{
Date d1;
d1.Init(2024, 2, 11);
d1.Print();
return 0;
}
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次
特性:
1.构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
2.其特征如下:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义了编译器将不再生成!
3.默认构造函数是我们不传参就可以调用的函数
- 我们什么都没写,编译器自动生成的
- 我们自己写的:无参的构造函数
- 我们自己写的:全缺省构造函数
这三类只能存在一个,注意后两个:不能同时存在的原因:当定义一个不带参数的类对象时,编译器不能确定到底要调用我们写的无参默认构造函数还是要调用我们写的带参全缺省默认构造函数,会报“对重载函数的调用不明确错误”。
(1) 我们什么都没写,编译器自动生成的
#include<iostream>
using namespace std;
class Date
{
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用编译器自动生成的默认构造函数
return 0;
}
(2)我们自己写的:无参的构造函数
#include<iostream>
using namespace std;
class Date
{
public:
//1.无参默认构造函数:初始化对象
Date()
{
_year = 2024;
_month = 2;
_day = 12;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
(3)我们自己写的:全缺省构造函数
#include<iostream>
using namespace std;
class Date
{
public:
//2.带参全缺省默认构造函数:初始化对象
Date(int year = 2024, int month = 2, int day= 12)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用带参默认构造函数
return 0;
}
2、编译器自动生成的默认构造函数
关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,
编译器生成默认的构造函数对内置类型不做处理
而对自定义类型成员会调用的它的默认成员函数
看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数:
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;
}
二、析构函数
1、析构函数的定义及其特性
析构函数用来完成类的资源清理工作,编译器在销毁对象时,会自动调用析构函数。
特性:
(1)析构函数名是在类名前加上字符 ~。
(2)无参数无返回值。(析构函数不能重载,一个类有且仅有一个析构函数)
(3)一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
(4)对象生命周期结束时,C++编译系统系统自动调用析构函数。
2、多对象的析构顺序
假如这个类有多个对象,那么析构的先后顺序是什么?
多对象的析构顺序 :局部对象(后定义先析构) -> 局部的静态(后定义先析构) ->全局对象(后定义先析构)
// 局部对象(后定义先析构) -> 局部的静态(后定义先析构) ->全局对象(后定义先析构)
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date(int year)
{
_year = year;
}
~Date()
{
// 调用一次 析构函数 我就打印一次
cout << "~Date()->" << _year << endl;
}
};
void func()
{
// 局部域
Date d4(4);
static Date d5(5);
static Date d10(10);
}
// 全局域
Date d6(6);
static Date d7(7);
Date d8(8);
static Date d9(9);
int main()
{
// 局部域
Date d1(1);
Date d2(2);
static Date d3(3);
func();
return 0;
}
3、编译器自动生成的默认析构函数
当不写析构函数时,编译器会自动生成默认的析构函数,不过这个默认的析构函数什么也不做,不需要清理资源。那么编译器自动生成的默认析构函数到底有什么用呢?
同析构函数
(1)对于内置类型,不会处理
(2)对于自定义类型,会调用它的析构函数
三、拷贝构造函数
1、拷贝构造函数定义及特性
1.拷贝构造函数:只有单个形参,该形参是对同类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
2.特征:
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
思考:为什么使用传值方式会引发无穷递归调用?
C++规定:对自定义类型的函数传值传参时,都会调用拷贝构造函数!!!
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <iostream>
using namespace std;
class Date
{
private:
int _year;
int _month;
int _day;
public:
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
// 构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
//Date(Date d)// 错误写法 使用传值方式编译器直接报错,因为会引发无穷递归调用
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 析构函数
~Date()
{
}
};
int main()
{
Date d1(2024,2,12);
Date d2(d1);
d1.Print();
d2.Print();
return 0;
}
拷贝构造函数也是构造函数,函数名和类型名相同,参数是同类型对象的引用,由编译器自动调用。
因此,对于自定义类型的对象,一般推荐使用传引用传参,虽然传值传参也可以,但是要调用拷贝构造
(1)对其拷贝构造函数的理解:
#include <iostream>
using namespace std;
class Date
{
private:
int _year;
int _month;
int _day;
public:
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
Date(int year = 2024, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 析构函数
~Date()
{
cout << "~Date()" << endl;//在析构函数内打印,调用一次就打印一次
}
//拷贝构造函数
Date(const Date& d) // Date (Date* this ,Date &d)
// 因为d2调用拷贝构造函数,所以&d2=this, (d是d1的别名)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
};
int main()
{
Date d1(2024,2,16);// 调用构造函数
Date d2(d1);// 调用拷贝构造函数,就是在定义对象的时候直接用拷贝构造函数
// Date(&d2,Date& d1)
d2.Print();
return 0;
}
2、深浅拷贝
1.传值传参:是浅拷贝
浅拷贝的缺点:
两个数组的指针指向同一块空间,当进行free时只能 free一个,那么另外一个就会变成“野指针”!
动态开辟资源的,浅拷贝都不行,因为浅拷贝只会仅仅copy数据。
2.传引用传参:是深拷贝:深拷贝还需要我们自己去写,编译器不会自动生成。
在判断一个类里面需不要写拷贝构造函数,根据具体情况而定。如果没有动态开辟的程序,就不需要写深拷贝。
3、编译器自动生成的拷贝构造函数
若未显式定义,系统会生成默认拷贝构造函数。 同构造函数和析构函数不同:
(1)拷贝构造函数对内置类型依次按照字节序完成拷贝,即浅拷贝或值拷贝。
假如不写拷贝构造函数,照样正常打印:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <iostream>
using namespace std;
class Date
{
private:
int _year;
int _month;
int _day;
public:
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
// 构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
拷贝构造函数
Date(Date d)// 错误写法 使用传值方式编译器直接报错,因为会引发无穷递归调用
//Date(const Date& d)
//{
// _year = d._year;
// _month = d._month;
// _day = d._day;
//}
// 析构函数
~Date()
{
}
};
int main()
{
Date d1(2024,2,12);
Date d2(d1);
d1.Print();
d2.Print();
return 0;
}
四、赋值运算符重载函数
1、运算符重载
1.定义:对于内置类型来说,语言层面本身就支持已经定义好的运算符,但对于自定义类型来说不行, C++中规定运算符重载的原因是:让自定义类型可以像内置类型一样使用运算符,想重载哪个运算符就重载哪个运算符。
运算符重载的语法:
函数原型:返回值类型 operator操作符(参数列表)
2.特性:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针
- (.*) (::) (sizeof) (?;) (.) 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
运算符重载实例:
(1)==运算符重载
#include<iostream>
using namespace std;
class Date
{
public:
//构造函数
Date(int year = 2024, int month = 2, int day = 16)
{
_year = year;
_month = month;
_day = day;
}
//成员变量公有
public:
int _year;
int _month;
int _day;
};
//operator==运算符重载
bool operator==(Date x1, Date x2)
{
return x1._year == x2._year
&& x1._month == x2._month
&& x1._day == x2._day;
}
int main()
{
Date d1(2024, 2, 15);
Date d2(2024, 2, 16);
//两种调用方式:
//1.可读性不强
operator==(d1, d2);
//2.当编译器看到==自定义类型,会去检查日期类有没有==的重载运算符,如果有重载会转换成operator==(d1, d2)去调用operator==函数
d1 == d2;
return 0;
}
2、运算符重载和函数重载的区别:
运算符重载和函数重载,虽然都使用了重载,但是两者之间没有关联:
(1)函数重载时支持定义同名函数
(2)运算符重载是为了让自定义类型可以像内置类型一样去使用运算符。
3、赋值运算符重载
1.赋值运算符重载语法格式:
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
赋值运算符重载和拷贝构造的区别:
(1)拷贝构造函数:对即将要创建的新对象进行初始化(不过初始化的内容是将一个已经存在的对象拷贝给他)
(2)赋值运算符重载:两个已经存在的对象进行赋值
2.赋值运算符重载代码 以及如何调用!
class Date
{
private:
int _year;
int _month;
int _day;
public:
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
Date(int year = 2024, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 析构函数
~Date()
{
cout << "~Date()" << endl;//在析构函数内打印,调用一次就打印一次
}
//拷贝构造函数
Date(const Date& d) // Date (Date* this ,Date &d)
// 因为d2调用拷贝构造函数,所以&d2=this, (d是d1的别名)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//赋值运算符重载
// d1=d2 //d1.operator=(&d1,d2)
Date& operator=(const Date& d) // void Date& operator=(&d1,const Date& d)
{
if (this != &d) // 对d取地址,判断this的值和d的地址是否相同,如果不是自己给自己赋值,才需要拷
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
};
int main()
{
//Date d1(2024,2,16);// 调用构造函数
//Date d2(d1);// 调用拷贝构造函数,就是在定义对象的时候直接用拷贝构造函数
Date(&d2,Date& d1)
//d2.Print();
Date d1(2024, 2, 16);
d1.Print();
Date d2;
d2.Print();
d2 = d1;
d2.Print();
return 0;
}
4、const修饰类的成员函数
定义:将const修饰的类成员函数称为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
比如有如下场景:假如把Date类的operator==运算符重载函数写错了
bool operator==(const Date& d)
{
return (_year == d._year)
&& (_month == d._month)
&& (_day == d._day);
}
将其中的一个"==“错写成”=" :
bool operator==(const Date& d) //bool operator==(Date* this,const Date& d)
{
return (_year == d._year)
&& (_month = d._month)
&& (_day == d._day);
}
虽然编译没有问题,但是这会导致this的值被修改了,并且执行结果也错误:
int main()
{
Date d1(2024, 2, 16);
Date d2(2024, 3, 16);
cout << (d1 == d2) << endl;
d1.Print();
d2.Print();
return 0;
}
这不符合要求,仅仅是比较而已,但是被比较对象的值却被修改了。const最大的作用是保护对象和变量,d2传给了d,d是d2的别名,const已经保护了d,那d1如何保护呢?由于this是隐含的,那么const为了保护this,应该如何加?把const加在成员函数的后面,叫做const修饰成员函数
bool operator==(const Date& d) const
{
return (_year == d._year)
&& (_month = d._month)
&& (_day == d._day);
}
总结:
1.const引用:我是你的别名,但我不能修改你(是之前:权限的缩小)
比如:const int& c = a; // c是a的别名,但由于const修饰c所以说不能通过c来修a的值。
2.如果有const的修饰,那么说明这个变量是只能读的!如果没有const修饰,普通的变量它是可读可写的!
五、总结:
1.构造函数和析构函数
如果我们不写,编译器对内置类型不做处理,自定义类型会调用它的构造函数和析构函数进行处理
2.拷贝构造和赋值运算符重载
如果我们不写,内置类型会完成浅拷贝,自定义类型会调用它的拷贝构造函数和赋值运算符重载函数。
3.取地址操作符重载和const取地址操作符重载
一般不需要重载,编译器默认生成的已经够用,重载没有价值。
好了,今天的分享就到这里了
如果对你有帮助,记得点赞👍+关注哦!
我的主页还有其他文章,欢迎学习指点。关注我,让我们一起学习,一起成长吧!