一.构造函数
构造函数的特征:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。(这个会在后面讲new时讲到)
class Date {
private:
int _year;
int _month;
int _day;
public:
Date(int year=2024, int month=2, int day=18) {//全缺省
_year = year;
_month = month;
_day = day;
}
};
int main() {
Date d1(2005, 12, 18);//提供初始化的值
return 0;
}
这个是我们自己实现的构造函数,如果我们不写的话,编译器就会默认生成一个构造函数,要不然怎么叫默认成员函数呢。
而对于编译器生成的默认构造函数,会对内置类型(int/char/double/指针)不做处理,对自定义类型(class/struct)调用它的构造函数。
所以如果自定义类型有自定义类型的变量会再调用它的构造函数,直到全部是内置类型为止,这就是一个俄罗斯套娃。
class A {
public:
A() {
cout << "A()" << endl;//观察是否调用构造函数
}
private:
int _a;
};
class Date {
private:
int _year;
int _month;
int _day;
A _aa;
public:
Date(int year=2024, int month=2, int day=18) {
_year = year;
_month = month;
_day = day;
cout << "Date()" << endl;//观察是否调用该构造函数
}
};
int main() {
Date d1(2005, 12, 18);
return 0;
}
可以看到,在创建对象时调用了A的构造函数和Date的构造函数,至于为什么先调用A的构造函数再调用Date的构造函数后面讲初始化列表时会讲到。
分析一个类型成员和初始化需求,如果需要构造函数就我们自己写,如果不需要就靠编译器默认生成,但是大多数情况下都需要我们自己实现构造函数。
无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。总结一下,不需要传参就可以调用构造函数,都可以叫默认构造函数。所以如果不传参时写了构造函数却不写缺省值编译器就会报错。
二.析构函数
特征:
- 析构函数名是在类名前加上字符~.
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的折构函数。注意:析构函数不能重载
- 对象生命周期结束时,c++编译系统自动调用析构函数。
构造函数相当于链表实现里面的初始化Init,析构函数就相当于销毁链表Destroy。编译器在处理析构函数时也是内置类型不做处理,自定义类型调用它的默认析构函数。
~Date()
{
cout << "~Date()" << endl;
}
c++相对于c语言的好处在这里就可以体现出来:我们c语言实现链表时,时常忘记销毁链表,把申请的堆区空间释放掉,而c++就轻松了,自动调用。但是什么时候调用呢?
所谓的生命周期就是在函数内部定义的对象函数结束后调用析构函数,静态和全局的对象数据存储在数据段,在main函数结束后调用析构函数,如果是mallloc或new来的,需要手动free或者delete再或者把它封装成一个类自动调用它的析构函数。
调用析构函数的顺序是后进先出,类似于栈
Date d1(1);
static Date d2(2);
int main() {
Date d3(3);
static Date d4(4);
Date d5(5);
return 0;
}
三.拷贝构造函数
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
- 若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
Date(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
int main() {
Date d1(2024, 2, 18);
Date d2(d1);//创建d2对象,然后把d1的值拷贝给d2
return 0;
}
为什么是传引用传参?c++规定:自定义类型做函数参数时,传值引用会调用它的拷贝构造函数创建临时对象,传引用就不需要。
Date(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
cout << "Date(const Date& d)——>";
}
void func1(Date d) {
cout << "func1()——>";
}
void func2(Date& d) {
cout << "func2()——>";
}
int main() {
Date d1(2024, 2, 18);
func1(d1);
func2(d1);
return 0;
}
可以看到func1传值传参调用了拷贝构造函数,func2传引用传参就直接引用。同理如果拷贝构造函数是传值传参那就会无限调用拷贝构造函数,会造成无限递归。
在参数前加一个const可以防止内容被修改,但是为什么可以访问其他对象的私有成员呢?因为私有是相对于类外,不是不同之间对象不可以互相访问,只要在类里面都可以访问对象的私有成员。此外,要注意拷贝构造的顺序,是把形参传给this指针,不能这样,如果顺序写反了const也能很好地保护。
没有写拷贝构造函数时,编译器会自动生成默认拷贝构造函数,但是这个拷贝构造就是浅拷贝,按内置类型成员按内存存储字节序拷贝。如果成员要动态开辟空间,显然浅拷贝不行,还是需要我们自己去实现深拷贝,手动malloc或者new。
如果构造一个顺序表对象后,用它来拷贝构造,会造成野指针解引用。,如下:因为是浅拷贝,两个array都指向同一片开辟的空间,调用一次析构函数后就变成野指针了,再次析构就会报错。
SequeList(const SequeLsit& SL) {
SequeList* temp = (SequeList*)malloc(SL._capacity * sizeof(SequeList));
if (temp == NULL) {
perror("malloc fail");
return;
}
memcpy(temp, SL._array, sizeof(SequeList) * SL.size);
_array = temp;
_size = SL.size;
_capacity = SL.capacity;
}
四.赋值重载
运算符重载
如何比较日期的大小?肯定不可能直接用'<'、'>',这里需要引入c++新玩法:运算符重载,就是赋予运算符新的含义。但是要封装到类里面去,不能写成全局的,不然会影响运算符原本的含义的使用。
重载函数的写法:就是函数名是operator+运算符(如果把函数写在类里面要比实际参数少一个,因为还有隐藏的参数this指针)。关于操作符重载还有以下几点规定:
- 不能通过连接其他符号来创造新的运算符
- 必须至少有一个自定义类型参数(不能重载运算符改变内置类型的行为)
- '.*'、'::'、sizeof、'?:'、'.'注意以上五个操作符不能重载
比如实现日期的比较
bool operator<(const Date& d) {
if (_year < d._year) {
if (_month < d._month) {
if (_day < d._day) {
return true;
}
}
}
else
return false;
}
Date d1(2024, 2, 20), d2(2005, 12, 18);
cout << d1.operator<(d2) << endl;
cout << (d1 < d2) << endl;//编译器自动调用重载函数
结果正确
赋值重载函数
每个类都要有一个默认赋值重载函数,同样内置类型直接赋值,自定义类型调用它的赋值重载函数。用户没有显示实现时,编译器会生成一个默认运算符重载,以值的方式逐字节拷贝。
Date& operator=(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
或许有人会有疑问:为什么要返回该对象的引用呢?如果我们写成这样
void operator=(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
那么如何解释d1=d2=d3?显然,要实现完整的功能,连续赋值,就要返回被赋值的对象。
那再想想,能不能传值返回?显然对于d1=d2=d3是可以的,但是如果遇上(d1=d2)=d3呢?要知道传值返回是返回创建的临时对象,而临时变量是不能做左值的(不能被修改),所以必须返回引用,使其能做被被修改的左值。
五.普通对象和const对象取地址重载
const成员函数
将const修饰的"成员函数"称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
注意看,为什么不能对i取别名,也不能用非const指针来接收?因为这都是一种权限的放大。本身加了const变量就不能修改,如果取别名或者取地址就有可能修改,这是不允许的。同样,如果一个类里面的成员函数其功能不会修改成员变量,就可以加上const修饰,提高代码安全性;如果需要对成员变量进行修改就不能加const。
- const成员函数不能调用非const成员函数(权限放大)
- 非const成员函数能调用cosnt成员函数(权限缩小)
或者这样理解:读写函数可以调用只读函数,只读函数不能调用读写函数。
取地址重载
注意取地址重载是两个函数const和非const,算上前面的四个默认成员函数就一共六个默认成员函数。
这也是一个运算符重载,operator&。这个默认成员函数有什么用?没什么用。就是方便我们不用每次给自定义类型取地址重载,直接默认生成。或者说实现一些小部分奇怪的要求,比如只让const对象拿到地址,普通对象拿不到地址
A* operator&() {
return NULL;
}
Date d1;
const Date d2;
cout << &d1 << endl;
cout << &d2 << endl;
const A* operator&() {
//return NULL;
/*int a = 10;
return (const A*)&a;*/
return (const A*)0xffffffff;
}
这个很简单,就是凭自己心意返回地址。这是非常可怕的,如果是恶意的编程,返回一个野指针,程序报错很难找出原因。