✈️类的6个默认成员函数包括:构造函数、析构函数、拷贝构造函数、赋值运算符重载函数、取地址操作符重载、const修饰的取地址操作符重载。
class Date {};//空类
如果一个类什么成员变量以及函数都没有,那么这就是一个空类。但是空类中并不是什么都没有,任何一个类在我们不写的情况下,都会自动生6个默认成员函数。
在c++中,有一种特殊的成员函数,它的名字和类名相同,没有返回值,不需要用户显式调用(用户也不能调用),而是在创建对象时自动执行。这种特殊的成员函数就是构造函数(Constructor)。
以下代码是一个Date类:
class Date
{
public:
void SetDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2;
d1.SetDate(2021, 12, 10);
d1.Display();
Date d2;
d2.SetDate(2022, 7, 7);
d2.Display();
return 0;
}
❔对于这个Date类,可以通过SetDate的方法给对象设置内容,但是如果每一次创建对象都用该方法去设置初始化信息,不仅比较麻烦,而且我们自己写程序时经常忘记调用初始化函数,那么能否在对象创建时,就将信息设置进去呢?
对于解决这个问题,c++引入了构造函数,构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。
class Date
{
public:
可能我们会忘记调用它,c++为了解决这个问题,引入了构造函数来进行初始化
//void SetDate(int year, int month, int day)
//{
// _year = year;
// _month = month;
// _day = day;
//}
//构造函数 -> 对象实例化时自动调用,这样就可以保证对象一定初始化
//一般情况下,对象初始化都分两种,默认值初始化,给定值初始化,那么我们给一个全缺省的构造函数就解决了这两个问题。
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名字叫做构造,但是构造函数的主要任务并不是开辟空间创建对象,而是对对象进行初始化。
1️⃣函数名与类名相同。
2️⃣没有返回值。
3️⃣对象实例化时编译器自动调用对应的构造函数。
4️⃣构造函数支持重载
//一般我们都把这两个无参和带参的构造函数写成一个全缺省的函数
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;
};
int main()
{
Date d1;//调用无参构造函数
Date d2(2020, 2, 21);//调用带参数的构造函数
//注意:如果通过无参构造函数创建对象时,对象后面不能跟括号,否则编译器就识别为函数声明
Date d3;//声明了d3函数,该函数无参,返回一个日期类型的对象
return 0;
}
5️⃣如果类中没有显式定义构造函数,则c++编译器会自动生成一个无参的默认构造函数,如果用户显式定义了编译器将不再生成。
6️⃣无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
总结:构造函数的细节很多,实际上大多数情况下需要我们自己写构造函数完成初始化,并且建议一般情况都是写一个全缺省的构造函数,这种方式能够适应各自场景。
✈️析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
1️⃣析构函数是特殊的成员函数。
2️⃣析构函数名是在类名前面加上字符~。
3️⃣无参数无返回值。
4️⃣一个类只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
5️⃣对象生命周期结束时,c++编译器会自动调用析构函数完成清理工作。
typedef int DataType;
class SeqList
{
public:
SeqList(int capacity = 4)
{
_a = (DataType*)malloc(sizeof(DataType) * capacity);
assert(_a);
_size = 0;
_capacity = capacity;
}
void Push(DataType x)
{}
~SeqList()
{
if (_a)
{
free(_a); //释放堆上的空间
_a == NULL; //将指针置为空
_size = _capacity = 0;
}
}
private:
DataType* _a;
size_t _size;
size_t _capacity;
};
int main()
{
SeqList s1;
s1.Push(1);
SeqList s2;
s2.Push(2);
//因为对象是定义在函数中的,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合先进后出
//s1先构造,s2后构造,s2先析构,s1后析构
return 0;
}
//析构函数对于内置类型成员不处理。而对于自定义成员,它会去调用它的析构函数。
关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,会对自定义类型成员调用它们的析构函数。
对于计算机来说,拷贝是指用一份原有的、已经存在的数据创建出一份新的数据。在c++中,拷贝并没有脱离它原本的含义,只是c++中指用一个已经存在的对象创建出一个新的对象。从本质上说,对象也是一份数据,因为它会占用内存。严格来说,对象的创建包括两个阶段,首先分配空间,再进行初始化。分配内存就是在堆区、栈区或者静态区留足够多的字节。此时内存中的内容一般都是随机值,没有什么实际的意义。初始化就是首次对内存赋值,让它的数据有意义。
构造函数:只有单个新参,该形参是对本类类型对象的引用(一般常用const修饰),在用已经存在的类类型对象创建新对象时由编译器自动调用。
class Date
{
public:
Date(int year = 0,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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
//这里d2调用的默认拷贝构造函数完成拷贝,d2和d1的值是一样的
Date d2(d1);
return 0;
}
拷贝构造函数也是特殊的成员函数,它有以下特点:
1️⃣拷贝构造函数是构造函数的一个重载形式。
2️⃣拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
3️⃣若未显式定义,系统生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我么叫做浅拷贝,或者值拷贝。
❔那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,那么我们还需要自己实现吗?当然像日期这样的类是没有这个必要的,那么像下面的这个类呢?
//这里我们会发现下面的程序会崩溃掉?这里就需要用深拷贝去解决。
class String
{
public:
String(const char* str = "earth")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
}
private:
char* _str;
};
int main()
{
String s1("xyz");
String s2(s1);
return 0;
}
c++为了增强代码的可读性引入了运算符重载,运算符重载就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。运算符重载也是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型operator操作符(参数列表)。
//全局的operator==
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//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;
}
int main()
{
Date d1(2021, 10, 26);
Date d2(2021, 10, 27);
cout << (d1 == d2) << endl;
return 0;
}
对于运算符重载,需要注意以下几点:
1.不能通过连接其它符号来创建新的操作符:比如operator@、operator$等。
2.重载操作符必须有一个类类型或者枚举类型的操作数。
3.用于内置类型的操作符,其含义不能改变,例如:内置的整形+或者-等,不能改变其含义。
4.作为类成员的重载函数时,其形参看起来比操作数数目少一个,因为成员函数的操作符有一个默认的形参this,限定为第一个形参。
5. :: 、.* 、sizeof 、?: 、. 这几个运算符不能重载。
如下为将运算符重载定义在类中的例子:
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//bool operator==(Date* this, const Date& d);
//这里需要注意的是,左操作数是this指向的调用函数的对象
bool operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
//当运算符是两个操作数时,第一个参数是左操作数,第二个参数是右操作数
//d1 < d2
bool operator<(Date x)
{
if (_year < x._year)
{
return true;
}
else if (_year == x._year)
{
if (_month < x._month)
{
return true;
}
else if (_month == x._month)
{
if (_day < x._day)
{
return true;
}
}
}
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 10, 26);
Date d2(2021, 10, 27);
//内置类型,语言层面就支持运算符
//自定义类型,默认不支持,c++可以用运算符重载来让类对象支持某个运算符
d1.operator==(d2);//d1.opertor(&d1,d2);
cout << (d1 == d2) << endl;//d1.operator==(&d1,d2)
cout << (d1 < d2) << endl;
return 0;
}
一般地,赋值运算符重载函数的参数是函数所在类的const类型的引用。
加上const是因为:
我们不希望在这个函数中对用来赋值的"原版"做任何的修改
加上const,对于const和非const的实参,函数都能够接受,如果不加,就只能接受非const的实参。
使用引用传参和引用返回是因为:
这样可以避免在函数调用时对实参的一次拷贝,从而可以提高效率。
class Date
{
public:
Date(int year = 0,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;
}
}
private:
int _year;
int _month;
int _day;
};
☑️赋值运算符主要注意以下4点:
1.参数类型
2.返回值
3.检测是否自己给自己赋值
4.返回*this
5.一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝(浅拷贝)。
❔那么编译器生成的默认赋值重载函数已经可以完成字节序的值拷贝了,那么我们还需呀自己实现吗?显然,像日期这样的类就没有必要自己实现了,它可以通过浅拷贝完成赋值运算符重载。但是像下面这样的类编译器生成的就不能够完成拷贝,需要深拷贝。
下面代码总结一下:
//赋值运算符重载 -- 也是拷贝行为,但是不一样的是,拷贝构造是创建一个对象时,拿同类对象初始化的拷贝
//这里的赋值拷贝是两个对象都已经存在,现在把一个变量赋值拷贝给另外一个对象
class Date
{
public:
Date(int year = 0,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;
}
//赋值运算符的重载也是一个默认成员函数,也就是说我们不写编译器会自动生成一个
//编译器默认生成的 赋值运算符跟拷贝构造的特性是一致的
//a:针对内置类型,会完成浅拷贝,也就是说像Date这样的类不需要我们自己写赋值运算符重载,Stack这样的类需要我们自己写。
//b:针对自定义类型也是一样的,它会调用它的赋值运算符重载完成拷贝
Date& operator=(const Date& d)
{
if (this != &d)//自己给自己赋值没有意义,不需要进行
{
_year = d._year;
_month = d._month;
_day = d._day;
}
}
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2020, 5, 5);
Date d3(2022, 5, 8);
//这里d1调用的编译器生成的operator=完成拷贝,d2和d1的值也是一样的
d1 = d2;
d1 = d2 = d2;//连续赋值
Date d4(d1);//拷贝构造 - 拿一个已经存在的对象去初始化另外一个要创建的对象
d1 = d2;//赋值重载 - 两个已经存在的对象
Date d6 = d1;//这个是拷贝构造,用d1去初始化创建d6
return 0;
}
//针对我们不写编译器默认生成的总结一下:
//构造和析构的特性是类似的,我们不写编译器对内置类型不处理,自定义类型调用它的构造和析构处理
//拷贝构造和赋值运算符的特性是类似的,内置类型会完成浅拷贝,自定义类型会调用它们的拷贝构造和赋值运算符重载
const修饰类的成员函数:将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明该成员函数中不能对类的任何成员进行修改。
看一看下面的案例,了解某些函数加上const的好处:
看以下代码思考几个问题:
class Date
{
public:
void Display()
{
cout << "Display ()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void Display() const
{
cout << "Display () const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1;
d1.Display();
const Date d2;
d2.Display();
}
以上问题可以自行去编译器验证,这里就不多说了。
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有在特殊的情况下,才需要进行重载,比如想让别人获取到指定的内容。
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
"&"运算符是一个单目运算符,其中只有一个参数,而这个参数就是一个对象,所以这个对象是不需要传参的,定义为类成员函数就应该少一个参数,第一个函数参数就被this指针替代。所以这里不需要传参。
一般情况下,这两个函数是不需要我们写的,实际也没有太大作用。如果不写这两个函数,编译器会默认生成,若没有其它特殊操作,那么编译器生成的就已经够用了。除非,你想返回别的地址,比如返回一个病毒的地址,返回一个很深的调用链等,那么需要按照需求进行重载实现,否则就不需要自己实现了。