前面我们有提到"空类"这一说法,但是"空类"真的是什么都没有吗?其实并不是,在类中什么也不写,编译器会自动生成六个默认成员函数,下面来了解了解!
目录
一、构造函数
1.概念
构造函数是一个特殊的成员函数,名字与类名相同,创建对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个周期内只调用一次。
可以看出,构造函数的主要任务就是在创建对象的同时直接赋初始值,这就省去了我们在类中再写一个初始化函数!
2.特性
- 函数名与类名相同
- 无返回值(不是void)
- 对象实例化时编译器自动调用对应的构造函数(注意有参和无参的写法)
- 构造函数也是可以重载的(比如上图的代码,有参与无参可以同时在)
- 如果我们没有显示定义出来,那么编译器会自动生成一个无参的默认构造函数,一旦我们写了,编译器就不会在生成了
- 由编译器生成的默认构造,对内置类型(int、double、char……)不做处理,而自定义类型会去调用他自己的默认构造
其实大家可以在想想,自定义类型里面是不是还会有内置类型呢?是的,编译器还是会对内置类型不做处理,除非你提供了缺省值,否则就是随机值。所以可以明白,自定义类型的尽头还是内置类型,这只是个套娃!!
- 为了弥补对内置类型不做处理的缺陷,C++允许内置类型成员变量在类中声明时可以给默认值(缺省值)
- 构造函数也支持缺省参数,但是要注意,无参构造、全缺省构造、编译器自动生成的构造,都可以认为是默认构造函数(也就是说他们是一样的,三者只能由一个)
不需要传参就可以调用的构造函数,都可以叫默认构造函数
通过上面可以得出,一般情况下,最好提供全缺省构造函数!!!!!!!!!!!!d
二、析构函数
为什么会有析构呢?当然除了因为它是六个默认成员函数之一以外,对象的清理工作就是由析构函数去完成的!注意:只是完成资源的清理工作!
1.概念
- 与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
2.格式
~类名() { //函数体 }
3.特性
- 类名前面加上字符 "~"
- 无返回值类型
- 对象生命周期结束时,C++编译系统自动调用析构函数
- 析构函数不能重载
- 一个类只能有一个析构函数。若未显示定义,系统会自动生成默认析构函数,这个生成的析构函数跟构造函数类似,内置类型不做处理,自定义类型去调用它的析构
注意:创建哪个类的对象则调用该类的析构函数,销毁哪个类的对象则调用该类的析构函数
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器默认的即可,比如上面的Date类;有申请资源时,一定要写,否则会造成内存资源的泄漏,比如栈这种需要申请的就一定要写
- 对于对象析构的顺序,一般为:局部对象(先定义后析构)>>局部静态(先定义后析构)>>全局(先定义后析构)
其实原因就是函数的调用会创建栈帧,里面的局部变量是在栈区的,所以满足栈先进后出的规则
三、拷贝构造函数
1.概念
- 只有单个形参,该形参是对本类型对象的引用(一般用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
- 说白了就是用一个同类型已经存在的对象去初始化一个正在创建的对象
class Date { public: Date(int year = 2024, int month = 2, int day = 24)//全缺省,也叫默认构造 { year_ = year; month_ = month; day_ = day; } Date(const Date& d)//拷贝构造,实际上这里还有个this指针,只是不能显示写 { 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 d1(2024, 2, 24); Date d2(d1);//用已经存在d1去创建对象d2 d1.print(); d2.print(); return 0; }
注意:加上const的原因就是因为害怕出现下面的情况
我就是你的拷贝对象,你反而把我给改了!!!
2.格式
类名(const &参数)//实际上是有两个,还有一个隐藏的this指针 { //函数体 }
3.特性
- 它是构造函数的一种重载形式(因为函数名一样,参数不一样嘛)
- 拷贝构造函数的参数只有一个并且必须是类类型对象的引用,因为传值的方式会引发无穷递归调用
①调用拷贝构造,要先传参,但是这里的传值传参,是对对象的拷贝,会去调用拷贝构造
②C++规定对自定义类型都会调用拷贝构造
- 若未显示定义,编译器会生成默认的拷贝构造函数
class Date { public: Date(int year = 2024, int month = 2, int day = 24)//全缺省,也叫默认构造 { year_ = year; month_ = month; day_ = day; } void print() { cout << year_ << "-" << month_ << "-" << day_ << endl; } private: int year_; int month_; int day_; }; int main() { Date d1(2024, 2, 24); Date d2(d1);//用已经存在d1去创建对象d2 d1.print(); d2.print(); return 0; }
来看看结果
可以看到,虽然我们没有显示写,但是编译器默认生成的拷贝构造函数还是完成了拷贝!
其实默认生成的拷贝构造函数有两点机制:
①内置类型是按照字节方式直接拷贝的,这种拷贝叫做浅拷贝或者值拷贝
②对于自定义类型是调用其拷贝构造函数完成拷贝的!
- 编译器默认生成的拷贝构造不能实现深拷贝(换句话说就是对于需要申请资源的,需要自己写)
class A { public: A(int capacity = 3) { _a = (int*)malloc(sizeof(int) * capacity); if (_a == NULL) { perror("falie"); return; } _capacity = capacity; _size = 0; } ~A() { if (_a) { free(_a); _a = NULL; _size = _capacity = 0; } } void print() { cout << _a << endl; } private: int* _a; int _size; int _capacity; }; int main() { A a1(4); A a2(a1); a1.print(); a2.print(); return 0; }
我们打印两个对象空间的地址看看结果:
发现结果是一样的!!!但是这个程序已经崩溃掉了!!说明编译器默认生成的拷贝构造无法构造这一场景!!
因为默认生成的拷贝构造只是值的拷贝,那这样必然会引发一个问题就是-----值拷贝,那必然就是将内容原封不动的照搬,两个对象指向同一块内存空间,那么当程序退出时,a2会先释放空间资源,可是a1还是指向这块空间,那么结果就是野指针,a1再释放,使同一块空间连续释放两次!!!这必然会导致程序崩溃掉!
可以这样修改
class A
{
public:
A(int capacity = 3)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == NULL)
{
perror("falie");
return;
}
_capacity = capacity;
_size = 0;
}
A(const A& a)
{
//开个一样大小的临时空间
int* tmp = (int*)malloc(sizeof(int) * a._capacity);
if (tmp == nullptr)
{
perror("file");
return;
}
//在把要拷贝的内容,放入临时空间中
memcpy(tmp, a._a, sizeof(int) * a._size);
//使当前对象指向这个空间
_a = tmp;
_size = a._size;
_capacity = a._capacity;
}
~A()
{
if (_a)
{
free(_a);
_a = NULL;
_size = _capacity = 0;
}
}
void print()
{
cout << _a << endl;
}
private:
int* _a; //需要深拷贝
int _size; //浅拷贝
int _capacity; // 浅拷贝
};
int main()
{
A a1(4);
A a2(a1);
a1.print();
a2.print();
return 0;
}
所以说,当出现上面的这种情况的时候,就必须自己写拷贝构造了,不能使用编译器默认生成的!但是像Date类这样的,我们可写可不写!因为没有向内存申请资源!!
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用 尽量使用引用。
4.调用拷贝构造函数典型场景
- 使用已存在对象创建新对象
A a2(a1);
- 函数参数类型为类类型对象
int tets(A a)
- 函数返回值为类类型对象
A test()
四、运算符重载
Ⅰ、运算符重载
1.概述
- C++为了增强代码的可读性引入了运算符重载,比如对于我们所写的什么func1、func2这些函数,可读性就不是很好
- 运算符重载是具有特殊函数名的函数,也具有其返回值类型,和普通的函数类似
2.语法格式
返回值类型 operator 操作符(参数列表)
{
//……函数体
}
3.特性
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个自定义类型的参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐 藏的this
- 以下5中运算符不能重载
点星(.*) 域作用限定符(::) sizeof ?: .
4.以重载==运算符为例
//重载==判断两个日期是否相等
class Date
{
public:
Date(int year = 2024, int month = 2, int day = 24)//全缺省,也叫默认构造
{
year_ = year;
month_ = month;
day_ = day;
}
bool operator==(const Date& d)//实际上这里有隐藏一个this指针,d为d2
{
return year_ == d.year_
&& month_ == d.month_
&& day_ == d.day_;
}
void print()
{
cout << year_ << "-" << month_ << "-" << day_ << endl;
}
private:
int year_;
int month_;
int day_;
};
当然,我们也可以将重载函数放在类外面,但是呢,要注意需要把类成员变量的权限设置为公有的,才能访问(当然也可以使用友元)。同时,参数部分就要显示两个,因为类外没有this指针
//类外写法
class Date
{
public:
Date(int year = 2024, int month = 2, int day = 24)//全缺省,也叫默认构造
{
year_ = year;
month_ = month;
day_ = day;
}
void print()
{
cout << year_ << "-" << month_ << "-" << day_ << endl;
}
//private:
int year_;
int month_;
int day_;
};
bool operator==(const Date &d1,const Date& d2)
{
return d1.year_ == d2.year_
&& d1.month_ == d2.month_
&& d1.day_ == d2.day_;
}
可见放在类外不是那么的方便!!
Ⅱ、赋值运算符重载
1.特点
- 已经存在的两个对象,一个给一个赋值
- 与拷贝构造不同,拷贝构造是一个已经存在了,另外一个没有!
2.格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值(A=B=C)
- 检测是否自己给自己赋值(防止d1=d1这种出现)
- 返回*this :要复合连续赋值的含义
class Date
{
public:
Date(int year = 2024, int month = 2, int day = 24)//全缺省,也叫默认构造
{
year_ = year;
month_ = month;
day_ = day;
}
Date& operator=(const Date& d)
{
//检查是否自己给自己赋值
if (this != &d)
{
year_ = d.year_;
month_ = d.month_;
day_ = d.day_;
}
return *this;//返回*this(对象)
}
void print()
{
cout << year_ << "-" << month_ << "-" << day_ << endl;
}
private:
int year_;
int month_;
int day_;
};
int main() { Date d1(2024, 2, 25); Date d2(d1);//拷贝构造,一个已经存在 Date d3(2024, 2, 26); Date d4(2024, 2, 27); d3 = d4;//赋值运算符重载,两个对象都已经存在 //d3.operator=(d4) 这样写也是可以的 return 0; }
3.注意事项
- 如果我们没有显示写,编译器会生成一个默认赋值运算符重载,但是呢只是简单的值拷贝,这一点和拷贝构造类似(内置类型值拷贝,对于需要开空间的深拷贝就需要去自己写)
- 赋值运算符只能重载成类的成员函数不能重载成全局函数(这是规定)
原因就是:赋值运算符如果不显式实现,编译器会生成一个默认的。在定义一个全局的就会
和类中默认的产生冲突!!
Ⅲ、前置++和后置++重载
1.前置++
Date& operator++() { _day += 1; return *this; }
前置++:返回加1以后的结果
注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
2.后置++
Date operator++(int) { Date temp(*this); _day += 1; return temp; }
- 为了区分前置和后置,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器 自动传递
- 后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存 一份,然后给this+1,而temp是临时对象,因此只能以值的方式返回,不能返回引用
五、取地址及const取地址操作符重载
- 这两个默认成员函数一般不用重新定义 ,编译器默认会生成。我们只需要使用编译器默认生成的重载即可,只有特殊情况才需要写,比如想让别人获取到指定的内容!
class Date
{
public:
Date* operator&() //取地址重载
{
return this;
}
const Date* operator&()const //const取地址操作符重载
{
return this;
}
private:
int _year;
int _month;
int _day;
};
总结:
函数 | 功能 |
构造函数 | 完成初始值工作 |
析构函数 | 完成对象中资源的清理工作 |
拷贝构造函数 | 使用同类对象初始化创建对象 |
赋值重载函数 | 一个对象赋值给一个对象(两个都存在) |
取地址及const取地址操作符重载 | 只要是普通对象和const对象取地址,这两个很少自己写 |