默认成员函数是用户没有显示的写而编译器自动生成的函数,一个类,我们不写编译器会默认生成6
个默认成员函数。
构造函数是特殊的成员函数。虽然名叫构造函数,但是它的主任务并不是开空间创建对象,而是对象实例化时初始化对象。 相当于Stack中的Init
函数.
📌构造函数的特点: (1)函数名与类名相同 (2)没有返回值 (3)对象实例化时系统会自动调用对应的构造函数 (4)构造函数可以重载 (5)如果没有显示定义构造函数,编译器会自动生成一个无参的构造函数,如果显式写了,编译器将不再自动生成 (6)无参构造函数,全缺省构造函数,编译器自动生成的构造函数都叫做默认构造函数。需要注意的是有且只有一个存在,不能同时存在。✏️总结:不传实参调用的构造就叫默认构造 (7)编译器默认生成的构造函数对内置类型成员变量的初始化没有要求。对自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。若没有默认构造函数编译器就会报错。要初始化这个成员变量就要用初始化列表来初始化。初始化列表在后面的章节我们会讲
🔖内置类型: 语言提供的原生数据类型,如int /char/double/指针
等
🔖 自定义类型: 我们使用的class/struct
等关键字自己定义的类型
class Date
{
public:
//1.无参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
//2.带参构造函数
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
//3.全缺省构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
析构函数并不是完成对对象本身的销毁,C++中规定对象在销毁时会自动调用析构函数,完成对象中资源的清理和释放工作。析构函数就类似Stack中的Destroy功能。
📌析构函数的特点:
(1)析构函数的函数名是在类名前**+** ~
(2)无参数无返回值(和构造函数类似)
(3)对象生命周期结束时会自动调用对应的析构函数
(4)跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,对自定义类型成员对调用它的析构函数
(5)自定义类型成员无论什么时候都对调用它的析构函数
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
//构造函数
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
_capacity = n;
_top = 0;
}
//析构函数
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
//两个队列实现栈
class MyQueue
{
//编译器默认生成MyQueue的析构函数调用了Stack的析构,释放Stack内部的资源
/*~MyQueue()
{
}*/
private:
Stack pushst;
Stack popst;
};
int main()
{
Stack st;
MyQueue mq;
return 0;
}
若一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值(缺省值),则此函数叫做拷贝构造函数。也就是说拷贝构造是一个特殊的构造函数
📌拷贝构造函数的特点:
(1)拷贝构造函数是构造函数的重载
(2)拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值的方式就会陷入无穷递归中,编译器就会报错。拷贝构造允许有多个参数,但第一个参数必须是类类型的引用,后面的参数必须有缺省值。
(3)C++规定自定义类型对象进行传值传参时必须调用拷贝构造,所以自定义类型进行传值传参和传值返回都会调用拷贝构造来完成。
(4)如果未显式写拷贝构造函数,编译器会自动生成拷贝构造函数。自动生成的拷贝构造函数对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节拷贝),对自定义成员变量会调用它的拷贝构造函数。
(5)像Date这样的成员变量都是内置类型并且没有指向什么资源,编译器自动生成的拷贝构造函数就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。像Stack
这样的类,_a
指向了资源,编译器自动生成的拷贝构造是浅拷贝不符合我们的需求,所以需要我们自己完成深拷贝(对指向的资源也进行拷贝)。像MyQueue
这样的的类型内部也是Stack
的成员,编译器自动生成的拷贝构造会调用Stack
的拷贝构造,不需要我们实现MyQueue
的拷贝构造。✏️总结:如果一个类显示实现了析构并释放了资源,那么它就需要写拷贝构造,否则不需要。
(6)传值返回会产生一个临时对象调用拷贝构造,但传引用返回,返回的是引用对象的别名,没有产生拷贝。但是如果传引用返回返回的是一个当前函数局部域的局部对象,函数结束返回值就销毁了,这时的引用相当于一个野指针。✏️所以:传引用返回一定要确保返回对象在函数结束后还在,才能用引用返回。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1,int month = 1,int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//虽然可以传参但不是拷贝构造
/*Date(Date* d)
{
_year = d->_year;
_month = d->_month;
_day = d->_day;
}*/
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024,9,27); //C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,这里传值传参要调用拷贝构造
Date d2(d1);
Date d2 = d1;//这两种写法都是可以的,也都是拷贝构造
return 0;
}
🌵当运算符被用到类类型对象时,C++允许我们用运算符进行重载的形式指定新的定义。C++规定当类类型使用运算符时必须对运算符进行重载。若没有运算符重载,编译器就会报错。
🌵运算符重载是具有特殊名字的函数,由operator
和后面要定义的运算符共同构成。
🌵重载运算符函数的参数的个数和该运算符作用的对象个数一样多(比如a+b,+的作用对象是a和b所以+运算符重载函数有两个参数)
🌵一个重载运算符函数如果是成员函数,那么它的第一个运算对象传给隐式地this
指针,因此运算符重载作为成员函数时,参数比运算对象少一个。
🌵运算符重载后,其优先性和结合性与内置类型一样。
🌵.*
,::
,sizeof
,?:
,.
这几个运算符不能重载。
🌵operator++
默认为前置++,后置++给operator++
增加一个int
的形参,与前置++构成函数重载。
🌵重载<<
和>>
时,需要重载为全局函数,因为重载为成员函数,this
指针抢了第一个形参位置,而第一个形参调用的是左侧对象,调用时就成了对象<<cout,不符合使用习惯和可读性。重载为全局函数把ostream/istream
放到第一个形参位置就行了。
#include<iostream>
using namespace std;
class Date
{
public:
//成员函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//<的重载函数
bool operator<(const Date& d)
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year && _month < d._month)
{
return true;
}
else if (_year == d._year && _month == d._month && _day < d._day)
{
return true;
}
return false;
}
private:
//成员变量
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 9, 27);
Date d2(2024, 10, 1);
cout << d1.operator<(d2) << endl;
return 0;
}
运行结果:
赋值运算符重载是一个默认成员函数,用于两个已经存在的对象的直接拷贝赋值。而拷贝构造是一个对象拷贝初始化给另一个要创建的对象。
📌赋值运算符重载的特点:
🌵赋值运算符重载是运算符重载,并且规定重载为成员函数。赋值运算符重载的参数建议写成const
+类类型的引用,否则会传值传参会有拷贝
🌵有返回值,并且建议写成当前类类型的引用,引用返回可以提高效率,有返回值是为了支持连续赋值。
🌵没有显示实现时,编译器会默认生成一个默认赋值运算符重载,默认赋值运算符对内置类型完成值拷贝/浅拷贝对自定义类型成员会调用它的赋值重载函数
🌵像Date
这样的类成员变量全是内置类型没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成我们需要的拷贝,所以不需要我们显示实现赋值运算符重载。而像Stack
这样的类,_a
指向了内部资源,编译器自动生成的赋值运算符重载完成的是值拷贝/浅拷贝不符合我们的要求,所以需要我们自己实现**深拷贝。**像MyQueue
这样的自定义类型,内部时Stack
的成员,编译器自动生成的赋值运算符重载会调用Stack
的赋值运算符重载。✏️总结:如果一个类显示实现了析构并释放了资源,那么它就需要显示写赋值运算符重载。
#include<iostream>
using namespace std;
class Date
{
public:
//成员函数
Date(int year = 1900, int month = 1, int day = 1)
{
_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;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
//成员变量
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 9, 27);
Date d2;
d2.operator=(d1);
d1.Print();
d2.Print();
return 0;
}
用const
修饰的成员函数称为const
成员函数。const
修饰成员函数const
放到成员函数的后面。
const
修饰成员函数其实修饰的是成员函数的this
指针,表示该成员函数中不能对类的成员进行修改。
const
修饰Date
类的Print
函数时,Print
隐含的this
指针由Date* const this
变成了const Date* const this
#include<iostream>
using namespace std;
class Date
{
public:
//成员函数
//构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//void Print(const Date* const this)
void Print() const //const放在成员函数的后面。const修饰的不是this本身,而是this指向的内容
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
//成员变量
int _year;
int _month;
int _day;
};
int main()
{
//非const对象也可以调用const成员函数是一种权限的缩小
Date d1(2024, 9, 27);
d1.Print();
Date d2;
d2.Print();
return 0;
}
取地址运算符重载分为普通的取地址运算符重载和被const
修饰的取地址运算符重载。一般编译器自动生成的就够我们用了,不需要去显示实现。除非我们不想让别人知道当前类对象的地址。
class Date
{
public:
Date* operator&()//普通对象调用普通版本
{
return this;
//return 0x222232 不想让别人取到当前类的地址,胡乱返回一个
}
const Date* operator&()const //const对象调用const版本
{
return this;
//return nullptr;
}
private:
int _year;
int _month;
int _day;
};
如果不显示写这样也可以取到地址,原因是取地址运算符重载是默认成员函数,不写编译器会默认生成。