浅浅的说明了C++中的一些基本的语法概念,主要是类的概念已经关于this指针的引入,那么在本篇博客中我将会继续完善相关语法帮助我们的C++学习,关于六个默认成员函数的分析
1.默认成员函数
首先是什么是默认成员函数?如果我们创建了一个空类,里面就真的什么都不存在吗?
默认成员函数就是用户没有显示实现,但是编译器会自动生成的成员称为默认成员函数
我们一共有六个默认成员函数:
1.构造函数
在之前我们写链表时都知道我们每一次都要刚开始自己写一个Init函数对链表进行初始化,并且每一次都需要调用它一遍,非常的麻烦,那么在C++中对这一操作进行了优化,就是我们不再需要自己去调用了,在创建类类型对象时编译器会自己调用自己的一个初始化函数,保证每一个成员在创建的那一刻就已经被初始化好了,并且在对象整个生命周期内只会调用一次。
1.特性
1.函数名与类名相同
class Date {
public:
Date() { //我们用户也可以自己显示的写构造函数
} //无参构造函数
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day; ///带参的构造函数
}
2.没有返回值
3.在对象实例化时编译器自动调用该函数(用户不需要自己写调用操作)
4.构造函数可以重载
从以下这个代码我们就可以看出我们提供了两种不同的构造函数带参和不带参的,在实例化对象时可以调用不同的构造函数,从而也体现出来了构造函数可以重载这一性质
class Date {
public:
Date() {
cout << "Date()" << endl; //显示打印出来方便观察
}
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
cout << "Date(int year,int month,int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1;//调无参的构造函数
cout << endl;
Date d2(2023,1,3);//调有参的构造函数
return 0;
}
5.如果类中并没有由用户显示定义构造函数,则C++会自动生成一个无参的构造函数,如果用户显示定义那么就不会再生成了
编译器自己生成的默认构造的特点:
1.我们不写就生成,任意写一个编译器都不会再生成了
2.内置类型不会进行处理(例如int,char等)但C++11声明支持给缺省值
3.自定义类型的成员才会去处理,回去调用这个函数的构造函数
内置类型就是语言提供的数据类型类似于int char这些,而自定义类型就是类似于class union struct这些自己定义的类型
private:
int _year=1;//C++支持在声明中给缺省值
int _month=1;
int _day=1;
};
class Date {
public:
Date(int year, int month, int day) {//只有带参的构造函数
_year = year;
_month = month;
_day = day;
cout << "Date(int year,int month,int day)" << endl;
}
private:
int _year=1;
int _month=1;
int _day=1;
};
int main() {
Date d1;
return 0;}
由此可以说明当用户写了之后编译器就不会自己再生成了,那么如果自己写了一个带参的构造函数但是我们又需要一个无参的,如果不再写一个无参构造函数的办法就是给带参构造函数给缺省值(C++11支持),否则我们就只能选择用后面我们即将要学习的初始化列表来解决
Date(int year=1, int month=1, int day=1) {//给缺省值的带参构造函数就能够将上述编译通过
_year = year;
_month = month;
_day = day;
cout << "Date(int year,int month,int day)" << endl;
}
6.无参构造函数,全缺省构造函数以及我们没有写编译器默认生成的构造函数都可以认为是默认构造函数,以上提到这三种默认构造函数只能出现一个,否则会产生歧义
2.析构函数
一个对象能够被我们初始化创建,那么它又是如何消失的?
1.概念
析构函数恰好与构造销毁,一个是初始化工作,另一个则是清理工作,需要注意,它本身不是对对象本身进行销毁,局部销毁工作由编译器完成,只是对象在销毁时会自动调用析构函数,完成对对象的清理工作
2.特性
1.析构函数是在类名前加上~
2.与构造函数相同时无参数无返回类型的
3.一个类只有一个析构函数,如果用户自己不写,系统会自己自动生成一个默认析构函数(注意析构函数不能重载!)
4.对象声明周期结束时C++系统会自动调用析构函数(先定义的后析构)
5.同样内置类型成员不做处理,自定义类型成员会自动调用析构(如下)
class Date {
public:
Date(int year=1, int month=1, int day=1) {
_year = year;
_month = month;
_day = day;
cout << "Date(int year,int month,int day)" << endl;
}
~Date() {
cout << "~Date()" << endl;
}
private:
int _year=1;
int _month=1;
int _day=1;
};
int main() {
Date d1;
return 0;
}
那么我们在看这样一个代码:
class A {
public:
~A() {
cout << "~A()" << endl;
}
};
class Date {
public:
Date(int year=1, int month=1, int day=1) {
_year = year;
_month = month;
_day = day;
cout << "Date(int year,int month,int day)" << endl;
}
~Date() {
cout << "~Date()" << endl;
}
private:
int _year=1;
int _month=1;
int _day=1;
A a;//注意这是一个自定义类型
};
int main() {
Date d1;
return 0;
}
运行结果如下:
为什么没有定义A类的对象,在最后却调用了其析构函数?
首先A类是一个自定义类型,我们在Date类型中声明了一个A的类型的a,那么我们知道在销毁时内置类型不做处理,最后系统回收即可但是a作为一个自定义类型成员,在d1销毁时其内部包含的a也要销毁那就必须要调用A类的析构函数,但是main函数中不能直接去调用A类的析构函数,因为我们要销毁的是d1对象,所以要先调用Date的析构函数,目的就是调用A类的析构函数
所以析构函数保证了每一个自定义对象都能够正确的被销毁
6.如果类中没有申请资源时则可以不写析构函数,直接使用编译器默认生成的,比如上面所显示的Date类,但是有资源申请的我们一定要自己写一个否则就会造成资源泄露这种问题,比如stack类这种,那么是为什么会出现这样的问题呢?
我们首先要明白在动态申请资源时在哪个地方申请呢?
答案:堆
我们先给出关于stack类的相关代码:
typedef int STDataType ;
class stack {
public:
stack(int capacity=4) { //可以选择在构造函数初始化时提前开出一部分的空间
_array = (STDataType*)malloc(sizeof(STDataType) * _capacity);
if (NULL == _array) {
perror("malloc error!");
}
else {
_capacity = capacity;
_size = 0;
}
}
void Push(STDataType data) {
if (_size == _capacity) {
int newcapacity = _capacity * 2;
STDataType*tmp = (STDataType*)realloc(_array,sizeof(STDataType) * newcapacity);
if (NULL == tmp) {
perror("malloc error!");
}
else {
_capacity = newcapacity;
_array = tmp;
_size++;
_array[_size] = data;
}
}
}
//.......
~stack() { //自己写的析构函数
if (_array) {
free(_array); //需要自己手动区销毁这部分空间
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
STDataType* _array;
int _capacity;
int _size;
};
int main() {
stack s;
return 0;
}
我们通过一张图来分析
3. 拷贝构造函数
1.概念
在创建对象时,能否创建一个与与已存在对象一某一样的新对象呢?
拷贝构造函数:只有单个形参,该 形参是对本类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建新的拷贝对象时由编译器自动调用。(也就是用当前对象去拷贝和初始化另一个对象
具体表现为:以Date类作为表示
Date(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
2.特征
1.拷贝构造函数是构造函数的一种重载形式
从函数名就可以看出,函数重载可以是函数名相同,但是参数类型不同,这两者刚好满足这一特点
2.拷贝构造函数参数只有一个且必须为类类型对象的引用,使用传值方式会直接报错,因为会引发无穷递归(本来应该是以无穷递归的形式存在,编译器做了优化直接报错)
那么为什么会引发无穷递归呢?
首先我们要知道一点:那就是在进行值拷贝时会先去调拷贝构造函数!(为什么呢会在下面讲)
我们先记住这一点,然后来看:
因此造成了无穷递归,那么为什么在传值传参时会先去调拷贝构造函数:
我们来看这样一个例子:
void test1(int n) {//直接进行值拷贝
cout << "test1(int n)"<<endl;
}
int main() {
int a;
test1(a);//直接传值过去是没有问题的
return 0;
}
在C语言中,我们直接进行传值传参时是不是按字节依次拷贝将a拷贝给了临时对象n,这就是值拷贝也是浅拷贝,对于内置类型int没有什么影响,但是对于stack类型如果用值拷贝的话就大坑特坑了。因为stack类型涉及到了在堆上的空间开辟,会造成两次析构,所以只是浅拷贝的话是远远不够的,那么在C++语言中引入拷贝构造函数,在进行值拷贝时就会先去调用拷贝构造函数就是为了解决像stack这样的问题。
解释完为什么会在值拷贝时会先去调用拷贝构造的问题之后,我们再回头看,也便解释出了为什么会不断的无穷递归,就是因为要传值传参就会先调用拷贝构造函数,然后又要因为是传值传参所以继续调用 ,然后造成了无限递归。那么加了引用为什么就能够解决呢?、
因为引用是取别名,相当于给d1取了一个别名d,引用不存在拷贝(因为 引用的底层本质上是指针),两者指向同一块空间,所以不会引发因为值拷贝而去递归调用拷贝构造
3.若未显示定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做值拷贝,也叫做浅拷贝。
注意:在编译器生成的默认拷贝构造函数中,对于内置类型是按字节方式依次拷贝的,自定义类型是调用其拷贝构造函数完成拷贝
但是对于Date这种类型不需要自己实现编译器默认的就能用,因为都是值拷贝,但是 stack就会出问题,它必须要用深拷贝来解决,用一张图来解释以下:
实现stack深拷贝的拷贝构造函数如下:
stack(const stack& s) {
_array= (STDataType*)realloc(_array, sizeof(STDataType) *s. _capacity);
if (_array == NULL) {
perror("malloc error");
}
else {
memcpy(_array, s._array, sizeof(STDataType) * s._size);
_size = s._size;
_capacity = s._capacity;
}
}
4.拷贝构造函数需要注意的调用场景
1.用已经存在的对象去创建新的对象时
2.函数参数类型为自定义类的类型的对象
3.函数返回值类型为自定义类的类型的对象(例如返回为Date 类型时也会自动调用拷贝构造函数)
Date test() {
Date tmp;//第二次调用构造函数
return tmp;//返回值为自定义类类型调用拷贝构造
}
int main() {
Date d1;//第一次调用构造函数
d1.test();
return 0; //析构三次,第一次是销毁tmp;
//第二次是销毁返回时调用拷贝构造函数时产生的临时对象d
//第三次是d1
}
所以在传参时能使用引用类型尽量使用引用类型,返回时也如此,毕竟能够提高效率
4.赋值运算符重载
1.运算符重载
1.概念
运算符重载是具有特殊函数名的函数,,同样具有返回类型,通俗来说就是比如:
int main() {
int a;
int b;
a + b;//int +int 存在
Date d1, d2;
d1 + d2;//那如果日期类加日期类呢?比如2023/10/27 加上三天呢该怎么直接用+来实现呢?
return 0;
}
那么运算符重载就是帮助我们实现这个目的的
函数名: 关键字operator后面接需要重载的运算符号(关键字operator就是用于对运算符的重新定义来使用的)
注意:
1.不能通过链接其他符号创建新的操作符,比如operator@(本身就不存在这个操作符)
2.重载时必须有一个自定义类型参数
3.用于内置类型的操作符,不能使用operator使其改变(int+就不可能重新定义成int-)
4.作为类成员进行函数重载时,其参数看起来比操作数少1,因为成员函数的第一个参数为隐藏的this
5.不能重载的运算符 .* :: sizeof ?: . 这五种不能重载
6.不能改变操作符的操作个数,一个操作符本身需要几个操作数,重载就仍然需要几个操作数
2.赋值运算符重载
以Date为例:
Date& operator=(const Date&d){//引用传参一般都要加const防止其改变
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
注意:
1.我们使用Date&返回的前提是:*this出了作用域还在,加引用会提高效率(因为在之前学过不加引用返回都会调拷贝构造函数)
2.禁止自己给自己赋值
3.赋值运算符重载只能重载成类的成员函数,不能重载为全局函数
因为如果赋值运算符不显示实现,编译器会生成一个默认的,如果我们再在类外自己实现一个的话,会和编译器生成的冲突了,所以赋值运算符必须定义为类的成员函数
4.用户没有显示实现,编译器会自己生一个一个默认运算符重载,以值的方式逐个字节拷贝。
那么既然编译器已经可以自己生成一个默认的我们还要不要自己写,答案是必须,对于Date类不受影响,但是对于之前提到过的stack类就会拥有相同的问题,析构两次,一块空间释放两次而崩溃,原理和之前在拷贝构造函数时很相像 :
所以在赋值运算符重载这个默认成员函数中,一旦涉及到在堆上开辟资源和资源管理必须要自己实现,否则很容易造成资源泄漏
我们会发现它和之前的拷贝构造函数有几分相像:
Date(const Date& d) { //这是拷贝构造函数
_year = d._year;
_month = d._month;
_day = d._day;
}
那么他们的区别在哪里呢?
首先拷贝构造函数传参如果不用引用会引发无穷递归但是它不会,因为赋值操作是已经存在的对象进行拷贝,而拷贝构造是一个已经存在的对象去初始化创建另一个即将要创建的对象
Date d1(2023, 10, 27);
Date d2 = d1;//这是拷贝构造
Date d3(2023, 10, 28);
d3 = d1;//这是运算符重载
3.前置++和后置++重载
1.前置++是返回+1之后的结果
我们直接先展示出+=的结果,其余都可以复用,因为++其实就是+=1的结果
int GetMonthDay(int year, int month) {
int days[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
int day = days[month];
if (month == 2 && ((year % 4 == 0 && year % 400 != 0) || (year % 400 == 0))) {
day = day + 1;
}
return day;
}
Date& operator+=(int day) {
if (day < 0) {
return *this -= (-day);
}
_day = _day + day;
while (_day > GetMonthDay(_year, _month)) {
_month = _month + 1;
_day = _day - GetMonthDay(_year, _month);
if (_month > 12) {
_month = 1;
_year = _year + 1;
}
}
return *this;//本身发生了改变,并且 *this还在
}
那么前置++和后置++的区别就在于前置++是先++在使用,而后置++是先使用然后再++,那么我偶们就需要对这两者进行区别;
// 前置++
Date& operator++() {
*this += 1;//对代码进行复用
return *this;
}
// 后置++
Date operator++(int) {
Date tmp(*this);
*this += 1;
return tmp;
}
下面就是一个完整的日期类实现:
class Date
{
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month) {
int days[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
int day = days[month];
if (month == 2 && ((year % 4 == 0 && year % 400 != 0) || (year % 400 == 0))) {
day = day + 1;
}
return day;
}
void Print() {
if (_month > 12 || _day > GetMonthDay(_year, _month)) {
printf("非法日期\n");
}
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
// 全缺省的构造函数,大多数如此
Date(int year = 1900, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
// d2(d1)
Date(const Date& d) {//必须要引用
_day = d._day;
_month = d._month;
_year = d._year;
}
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d) {//应该是有返回值的,因为存在连续赋值比如i=j=10,j=10的返回值是10
_day = d._day;
_year = d._year;
_month = d._month;
return *this;
}
// 析构函数
~Date() {
_day = 0;
_month = 0;
_year = 0;
}
// 日期+天数
Date operator+(int day) {//出了作用域对象不存在不能用引用
Date tmp(*this);//拷贝构造
return tmp += day;//复用
}
// 日期+=天数
Date& operator+=(int day) {
if (day < 0) {
return *this -= (-day);
}
_day = _day + day;
while (_day > GetMonthDay(_year, _month)) {
_month = _month + 1;
_day = _day - GetMonthDay(_year, _month);
if (_month > 12) {
_month = 1;
_year = _year + 1;
}
}
return *this;//本身发生了改变,并且 *this还在
}
// 日期-=天数
Date& operator-=(int day) {
if (day < 0) {
return *this += (-day);
}
_day -= day;
while (_day <=0) {//小于等于日期都不合法
_month = _month - 1;
if (_month ==0) {
_month = 12;
_year = _year -1;
}
_day = GetMonthDay(_year, _month) + _day;
}
return *this;
}
// 日期-天数
Date operator-(int day) {
Date tmp(*this);
tmp = *this -= day;
return tmp;
}
// 前置++
Date& operator++() {
*this += 1;
return *this;
}
// 后置++
Date operator++(int) {
Date tmp(*this);
*this += 1;
return tmp;
}
// 后置--
Date operator--(int) {
Date tmp(*this);
*this -= 1;
return tmp;
}
// 前置--
Date& operator--() {
*this -= 1;
return*this;
}
// >运算符重载
bool operator>(const Date& d) {
return !(*this < d);
}
// ==运算符重载
bool operator==(const Date& d) {
if (_year == d._year && _month == d._month && _day == d._day) {
return true;
}
else {
return false;
}
}
// >=运算符重载
bool operator >= (const Date& d) {
return !(*this <= d);
}
// <运算符重载
bool operator < (const Date& d) {
if (_year < d._year) {
return true;
}if (_year == d._year && _month < d._month) {
return true;
}if (_year == d._year && _month == d._month && _day < d._day) {
return true;
}
else {
return false;
}
}
// <=运算符重载
bool operator <= (const Date& d) {
return *this < d || *this == d;
}
// !=运算符重载
bool operator != (const Date& d) {
return !(*this == d);
}
// 日期-日期 返回天数
int operator-(const Date& d) {
Date max = *this;
Date min = d;
int flag = 1;
int day = 0;
if (*this < d) {
max = d;
min = *this;
flag = -1;
}
while (max!=min) {
day++;
min++;
}
return day * flag;
}
5.const成员
概念:将const修饰的成员函数称之为const成员函数,const修饰类成员函数,实际修饰该函数做隐藏的this指针,表明在该成员函数中不能对类的任何成员进行修改。
void print() const { const成员函数
cout << _year << "/" << _month << "/" << _day << endl;
}
//两者是一个意思
void print(const Date*this) {
cout << _year << "/" << _month << "/" << _day << endl;
}
const成员函数特点:
1.const成员函数内只能读取类的成员,无法修改类的成员
2.const成员函数内,不能调用其他非const成员函数
3.非const成员函数可以调用其他的const成员函数
4.const对象不能调用非const成员函数
5.非const成员对象可以调const成员函数
总的来说就是权限可以缩小,可以平移,但不能放大
6.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器默认生成,并且这两个运算符一般不需要重载,使用编译器生成的默认取地址重载即可。
Date* operator&()const {
return this;
}
const Date* operator&()const {
return this;
}
(将会继续更新,欢迎各位大佬批评指正)