目录
类的六个默认成员函数
如果一个类中什么成员都没有,我们简称其为空类,。但是空类中真的什么都没有么?其实不然,任何一个类,即使我们什么都不写,类中也会自动生成6个默认成员函数。
默认成员函数:用户没有显示实现,编译器会自动生成的成员函数称为默认成员函数。
class Data{};//空类
构造函数
构造函数的概念
构造函数:名字与类名相同,创建类的类型对象时由编译器自动调用。
class Data
{
public:
//无参构造函数
Data()
{}
//带参构造函数
Data(int year,int month,int day)
{
_year=year;
_month=month;
_day=day;
}
private:
{
int _year;
int _month;
int _day;
}
};
void TestData()
{
Data d1;//调用无参构造函数
Data d2(2023,9,15);
}
注意:构造函数的主要任务不是开辟空间创建对象,而是初始化对象。
构造函数的特性
一.构造函数的函数名与类名相同
二.构造函数没有返回值。(并不是说返回值为void)
三.对象实例化时编译器自动调用对应的构造函数
当你用类创建一个对象时,编译器会自动调用该类的构造函数对新创建的变量进行初始化
四.构造函数支持重载
这意味着你可以用多种方式初始化函数,编译器会根据你所传递的参数去调用对应的构造函数。
五.无参的构造函数,全缺省的构造函数以及我们不编写时编译器自动生成的构造函数都是默认构造函数,并且默认构造函数只能有一个。默认构造参数有以下几种:
1.自己不编写,编译器默认生成的构造参数。
2.自己编写的无参的构造函数。
3.自己编写的全缺省的构造函数。
六.如果类中没有显示定义构造函数,则编译器会默认生成一个无参的构造函数,若用户自己定义则编译器不会自动生成。说到这里你可能会认为那我们自己就不用编写构造函数了,其实不然。
查看以下代码:
#include<iostream>
using namespace std;
class Data
{
public:
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1;
d1.Print();
return 0;
}
打印结果:
这里为什么会打印随机值?
answer:C++把类型分为内置类型和自定义类型。内置类型就是int,char,指针类型;自定义类型就是struct,union,class。而编译生成的构造函数不对内置类型进行处理。
运行结果:
会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员 函数。而内置类型没有完成初始化。
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
析构函数
析构函数的概念
析构函数:与构造函数功能相反,析构函数负责完成对对象的销毁,对象在销毁是会自动调用析构函数,完成类的一些资源的清理工作。
析构函数的特性
一:析构函数实在类名前加~。
二:无参数无返回类型。
三:一个类只能有一个析构函数。若未显示定义系统会自动生成析构函数。(析构函数不能重载)
编译器自动生成的析构函数机制:
1、编译器自动生成的析构函数对内置类型不做处理。
2、对于自定义类型,编译器会再去调用它们自己的默认析构函数。
四:对象生命周期结束时会自动调用析构函数。
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Data
{
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Data d;
return 0;
}
执行结果:
在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数? 因为main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对 象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date 类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁,main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析 构函数 注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数。
五:先构造的后析构,后构造的先析构。
因为对象是定义在函数中的,函数调用会建立栈帧,所以栈帧中的构造和析构也要符合栈的规则。
拷贝构造函数
拷贝构造函数的概念
在现实生活中,可能存在一个与你一模一样的你,我们称之为双胞胎。在创建对象时,能否创建一个与已经存在的对象一模一样的对象?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),再用已经存在的类类型对象创建新对象时由编译器自动调用。
#include<iostream>
using namespace std;
class Data
{
public:
//构造函数
Data(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Data(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2023, 9, 16);//实例化
Data d2(d1);//用已存在的对象创建对象
return 0;
}
拷贝构造函数的特性
一:拷贝构造函数是构造函数的一个重载形式
因为拷贝构造函数的函数名与类名相同
二:拷贝构造函数的参数有且只有一个,且必须是类类型对象的引用,如果使用传值方式编译器会报错,因为会引发无限递归。
调用拷贝构造函数就要先传参,传参又要拷贝构造,拷贝构造又要传值传参,所以就会引发无限递归。
三:若未显示定义,编译器会自动生成拷贝构造函数,默认的拷贝构造函数对象按照内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝。
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 0, 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()
{
Date d1(2021, 5, 30);
Date d2(d1); // 用已存在的对象d1创建对象d2
d1.Print();
d2.Print();
return 0;
}
打印结果:
编译器自动生成的拷贝构造函数机制:
1.编译器自动生成的拷贝构造函数对内置类型会完成浅拷贝。
2.对于自定义类型,编译器会再去调用他们自己的默认拷贝构造函数。
四:对于编译器自动生成的默认拷贝构造不能完成深拷贝。
#include<iostream>
#include<assert.h>
using namespace std;
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 4)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
assert(_array);
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_size = 0;
_capacity = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
int x = 1;
s1.Push(x);
Stack s2(s1);//拷贝构造
return 0;
}
这段代码为什么会崩掉?
answer:在拷贝构造时,我们没有显示定义,所以编译器会生成一份默认拷贝构造函数,默认拷贝构造函数是按值拷贝的,就是将s1中的值原封不到的拷贝到s2中,因此s1 s2指向了同一块空间。当程序退出时,s2 s1要销毁,按照栈的规则要先销毁s2,在销毁s1,s2销毁时调用析构函数,已经讲_array的空间释放了,但是s1并不知道,所以s1又将_array释放一次,而多次释放同一块空间就会造成程序崩溃。
五:拷贝构造函数典型调用场景
1.使用已存在对象创建新对象
2.函数参数类型为类的类型对象
3.函数返回值类型为类的类型对象
总结:内置类型可以浅拷贝,让编译器自动生成就可以。像自定义类型或者涉及到资源申请时,拷贝构造是一定要自己写的。
赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,其目的就是让自定义类型可以像内置类型一样可以直接使用运算符进行操作。
运算符重载函数也具有自己的返回值类型,函数名字以及参数列表。其返回值类型和参数列表与普通函数类似。
运算符重载函数名为:关键字operator后面接需要重载的操作符符号。
函数原型:返回值 operator运算符(参数列表)
注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@。
2.重载操作符必须有一个类类型或枚举类型的操作数。
3.用于内置类型的操作符,重载后其含义不能改变。
4.作为类成员的重载函数时,函数有一个默认的形参this,限定为第一个形参。
5.sizeof 、:: 、.* 、?: 、. 这5个运算符不能重载
class Data
{
public:
Data(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
// 这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?
// 这里其实可以用友元解决,或者干脆重载成成员函数。
bool operator==(const Data& d1, const Data& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Data d1(2023, 9, 17);
Data d2(2023, 9, 18);
cout << (d1 == d2) << endl;
return 0;
}
所以我们可以将该运算符重载函数作为类的一个成员函数,此时该函数的第一个形参默认为this指针。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date& d2)
{
return _year == d2._year;
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
赋值运算符重载
这里以重载 = 运算符作为例子:
class Date
{
public :
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date (const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date& operator=(const Date& d)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
重载赋值运算需要注意以下几点:
一、参数类型设置为引用,并用const进行修饰
赋值运算符重载函数的第一个形参默认是this指针,第二个形参是我们赋值运算符的右操作数。
由于是自定义类型传参,我们若是使用传值传参,会额外调用一次拷贝构造函数,所以函数的第二个参数最好使用引用传参。
二、函数的返回值使用引用返回
实际上,我们若是只以d2 = d1这种方式使用赋值运算符,赋值运算符重载函数就没必要有返回值,因为在函数体内已经通过this指针对d2进行了修改。但是为了支持连续赋值,即d3 = d2 = d1,我们就需要为函数设置一个返回值了,而且很明显,返回值应该是赋值运算符的左操作数,即this指针指向的对象。
和使用引用传参的道理一样,为了避免不必要的拷贝,我们最好还是使用引用返回,因为此时出了函数作用域this指针指向的对象并没有被销毁,所以可以使用引用返回。
三、赋值前检查是否是给自己赋值
若是出现d1 = d1,我们不必进行赋值操作,因为自己赋值给自己是没有必要进行的。所以在进行赋值操作前可以先判断是否是给自己赋值,避免不必要的赋值操作。
四、引用返回的是*this
赋值操作进行完毕时,我们应该返回赋值运算符的左操作数,而在函数体内我们只能通过this指针访问到左操作数,所以要返回左操作数就只能返回*this。
五、一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值拷贝
没错,赋值运算符重载编译器也可以自动生成,并且也是支持连续赋值的。但是编译器自动生成的赋值运算符重载完成的是对象按字节序的值拷贝,例如d2 = d1,编译器会将d1所占内存空间的值完完全全地拷贝到d2的内存空间中去,类似于memcpy。
对于日期类,编译器自动生成的赋值运算符重载函数就可以满足我们的需求,我们可以不用自己写。但是这也不意味着所有的类都不用我们自己写赋值运算符重载函数,而自定义类型成员变量需要调用对应类的赋值运算符 重载完成赋值。
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time& operator=(const Time& t)
{
if (this != &t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
return *this;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
Date d2;
d1 = d2;
return 0;
}
注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必 须要实现。
Date d1(2021, 6, 1);
Date d2(d1);
Date d3 = d1;
这里一个三句代码,我们现在都知道第二句代码调用的是拷贝构造函数,那么第三句代码呢?调用的是哪一个函数?是赋值运算符重载函数吗?
其实第三句代码调用的也是拷贝构造函数,注意区分拷贝构造函数和赋值运算符重载函数的使用场景:
拷贝构造函数:用一个已经存在的对象去构造初始化另一个即将创建的对象。
赋值运算符重载函数:在两个对象都已经存在的情况下,将一个对象赋值给另一个对象。