创作不易,多多支持!
前言
相信你对这几个知识点有点混淆,相信看完以后,你会对此有一个清晰的认识。
一 类的6个默认成员函数
如果我们写一个类,但是类里面什么都没有,我们称之为空类。
其实这个类也不完全为空,因为编译器会类中自动生成这6个成员函数。
所以这几个成员函数也叫作默认成员函数,我们不去实现,编译器会生成。
接下来我们一个一个说明
二 初始化和清理
2.1 构造函数
1 .我们知道构造函数是执行初始化的操作,要是我们像以前一样写一个初始化函数去初始化也是可以的,下面用一个日期类去演示
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)//初始化函数
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//类的实例化
d1.Init(2024, 7, 5);调用函数初始化
d1.Print();
Date d2;
d2.Init(2024, 7, 6);
d2.Print();
return 0;
}
从代码中我们可以看出 如果我们要初始化我们就需要每次都调用这个初始化函数,这就会显得非常的麻烦,那有没有更加便捷的方法呢?
构造函数是一种特殊的成员函数,函数名与类名相同,不需要返回值,在类实例化时自动调用,每个类只调用一次,我们可以用这个自动调用的特性去让我们的初始化变的非常方便
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:
Date()//无参构造函数
{
_year = 2024;
_month = 7;
_day = 5;
}
Date(int year, int month, int day)//有参
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用无参
Date d2(2024, 4, 24);调用有参
d1.Print();
d2.Print();
return 0;
}
在上面代码中我们并没有去调用里面的构造函数,我们可以看看输出的结果是什么
这里就体现了自动调用的特性。其中我们可以看出无参调用的时候有的人会这样写
Date d1();
这其实就错误了,因为编译器不知道你是调用函数还是类的实例化,所以不能这样写
2. 对于构造函数因为是编译器默认生成的,所以即使我们不写,那么编译器也会自动生成一个,但是如果我们写了,那么编译器就不会生成了
为了更好理解,下面给出相应代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
这串代码是不会报错的,因为编译器会自动生成一个默认的构造函数,这样在类实例化的时候会调用这个默认的构造函数
但是如果我们这样写
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
可以看出这样就不行了,因为我们写了一个带参的构造函数,但是我们要初始化无参的就没了,因为我们写了构造函数,那么编译器就不会再生成默认的构造函数了
但是不要想着编译器会给你那么方便,他虽然可以帮你自动调用,你没写也可以自动生成,但是它自动生成的默认构造函数是不会给你初始化的,这一点要尤其注意
我们可以看第一段代码, 是可以运行通过的,但是运行的结果却是
可以看出结果不是我们想的那样,它并没有完成初始化,所以我们可以得出默认的构造函数对于内置类型是不处理的,内置类型就是(int/double/char...)之类的类型。
3. 那这个默认的构造函数没用吗?并不是,对于自定义类型,这个默认构造函数会去调用这个自定义类型的默认构造函数
可能这段话看起来非常绕,那下面我们看一点代码就清楚了
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Time
{
public:
Time()//Time类的默认构造
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
//内置类型
int _year;
int _month;
int _day;
//自定义类型
Time _t;
};
int main()
{
Date d1;
return 0;
}
之所以有这样一个结果就是在Date中有一个默认构造函数,这个是由编译器自动生成的,这个函数对于内置类型是不处理的,但是对于自定义类型,会去调用它的默认构造函数,所以就会打印出这个 Time()
4. 其实这让我们也非常难以接受,所以在后面c++又对它进行了补丁,可以在成员变量声明的时候给缺省值
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
void Print()
{
cout << _year <<'-'<< _month <<'-' << _day << endl;
}
private:
//内置类型
int _year=2024;
int _month=7;
int _day=21;
//自定义类型
Time _t;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
注意这里虽然是在声明的时候给了值,但是这里并不代表是定义,也就是没有给空间,给空间还是要在实例化的时候给空间
5. 这里还有一个点要分清楚,默认成员函数只包括(全缺省构造函数,无参构造函数,我们没写编译器默认生成的构造函数)对于不是这些类型的都不能算是默认构造函数
所以如果我们写一个无参的构造函数,再写一个全缺省的构造函数,那么这个编译器就会报错,因为再类实例化的时候,它不知道调用哪一个,这一点也是要注意的
2.2 析构函数
析构函数也是特殊的成员函数 ,他和构造函数相反,他是负责清理的,但是两者的特性是差不多的 对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
析构函数特性
1. 析构函数名是在类名前加上字符 ~ 。2. 无参数无返回值类型。3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载4. 对象生命周期结束时, C++ 编译系统系统自动调用析构函数
和构造函数一样,对于内置类型不处理,对于内置类型去调用它的析构函数
如果是对于这么日期类,里面只包含内置类型,那么也可以不写析构函数,因为出了作用域,内置类型的变量会随着栈的销毁而销毁 ,但是如果涉及到申请资源那么就需要用到析构函数了
#include<iostream>
using namespace std;
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
这里和上面的构造函数一样,就不做太多的说明了。
三 拷贝复制
3.1 拷贝构造函数
拷贝构造函数只有单个形参,该形参是对本类类型对象的引用一般用const修饰。
拷贝构造函数特性:
1.拷贝构造函数是构造函数的一种重载
2.拷贝构造函数的参数只有一个,而且这个形参必须是该类的引用,如果用传值会导致无穷递归
#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(const Date& d) // 正确写法
Date(const Date d) // 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
在解释无穷递归的原因之前,我们得先来了解一下内置类型的拷贝和自定义类型的拷贝的区别
内置类型的拷贝就在底层就是一个字节一个字节的拷贝,也就是浅拷贝,那如果自定义类型也是浅拷贝的话,有可能会发生意想不到的后果
比如如果说自定义类型里面有申请空间开辟的数组,那么就会发生两次释放空间的问题
那如果避免这个问题了,这里我们就需要深拷贝,深拷贝就是再开辟一片空间,把他们分开,这样就不会导致重复释放了,所以这里我们就需要用拷贝构造函数去实现这一功能
所以我们对于自定义类型不管里面是不是有申请空间的变量,我们都去调用它的拷贝构造函数
那么回到无穷递归这个问题,如果我们要拷贝自定义类型,那么编译器会去调用拷贝构造函数,那么就会传参,那么我们传参又会去调用新的拷贝构造,调用新的拷贝构造又会传参,那么就陷入无穷递归了
所以我们需要用到引用,但是这个引用我们不能去改变这个值,所以用const
所以拷贝构造函数的形式就是
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
其他的形式都不是拷贝构造
拷贝构造和构造函数也有点相似的地方,比如我们没有写拷贝构造函数的时候,编译器会去调用默认生成的拷贝构造,不过这个拷贝构造不会完成深拷贝,只是简单的值拷贝,也就是浅拷贝
3.2 赋值运算符重载
赋值运算符重载是具有特殊函数名的函数
函数名为:operator后面接需要重载的运算符符号(+,-,*,[])
函数原型:返回值类型 operator操作符(参数列表)
但是要注意有几个符号是不能重载的
.* :: sizeof ?: .
还需要注意的是
1.不能通过连接其他符号来创建新的操作符:比如operator@2.重载操作符必须有一个类类型参数3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
1.运算符重载
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:
bool operator==(const Date& d2)//运算符重载,这里只有一个参数
//其实还有一个隐含的参数this
//如果把该函数放在外面就没有this,但是就确保不了封装性了
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout << (d1 == d2) << endl;
}
int main()
{
Test();
return 0;
}
上面的代码就是运算符重载的一个实例,那赋值运算符重载其实道理也大差不差
但是其中也有些许细节
参数类型 : const T& ,传递引用可以提高传参效率返回值类型 : T& ,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值返回 *this :要复合连续赋值的含义
按照上面的格式,我们可以写出一个相应代码
2. 赋值运算符重载
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
Date(int year = 1900, 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;
Date d2(2024, 7, 40);
d1 = d2;
d1.Print();
d2.Print();
return 0;
}
3.赋值运算符重载只能定义成成员函数,不能定义成全局函数
因为运算符重载也是默认的成员函数所以,编译器会自己生成一个,那么我们自己如果在全局再写一个就会导致冲突,所以运算符重载必须写成成员函数,不能写成全局函数
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员
4. 如果我们不写赋值运算符重载,那么编译器会自动生成一个,这个自动生成的对内置类型完成值拷贝,对于自定义类型会去调用对应类的赋值运算符重载
与拷贝构造函数类似,对于只有内置类型的类来说,写不写都可以,但是如果涉及到申请资源的变量那么就得自己写完成深拷贝的函数
5. 前置++与后置++重载
对于这两个直接看代码理解吧
class Date
{
public:
Date& operator++()//前置++
{
_day += 1;
return *this;
}
Date operator++(int)后置++
{
Date tmp = *this;
_day+=1;
return tmp;
}
Date(int year = 1900, 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形参去构成函数重载,这里只是构成重载没有任何作用
注意后置是先返回,后++,所以这得用一个临时对象保存,返回的时候不能用引用返回,因为返回的是临时对象,用引用返回会出现未定义行为
四 取地址重载
class Date
{
public :
Date* operator&()//不加const
{
return this;
}
const Date* operator&()const//加const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这里分为加const和不加const
编译器默认生成的是不加const,所以一般这种重载我们不写交给编译器,但是如果有特殊的要求则需要自己手动写,比如想人获取指定的内容
const 成员函数
用const修饰的成员函数称之为const成员函数,使用const修饰的成员函数不能修改类的成员变量,也不能调用非const成员函数
const修饰类成员函数,实际上修饰该成员函数的隐含指针this,表明在该成员函数中不能对类的任何成员进行修改
语法声明为:void fun() const;
class Date
{
public:
void fun()const
{
_year = 6;//尝试对成员变量进行赋值
}
Date(int year = 1900, 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;
};
对于const对象调用const成员函数时,会调用const版本函数,而使用非const对象调用const成员函数时,会调用非const版本函数
这也就是一一对应
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:
void fun()const
{
cout << "const" << endl;
}
void fun()
{
cout << "非const" << endl;
}
Date(int year = 1900, 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()
{
const Date d1;
Date d2;
d2.fun();
d1.fun();
return 0;
}
可以看出结果也是一一对应的。
在这里还有一个点就是
🎈const成员函数不能调用非const成员函数,因为这是 权限放大
在const成员函数里它承诺了不能修改成员变量,如果去调用非const,非const又可以修改,这就违法了const成员变量的约定
🎈非const成员函数可以调用const成员函数,权限缩小是可以的
在非const可以修改也可以不修改,那在里面调用const成员函数,const成员函数规定不能修改,在非const里面并不矛盾,可以包容,所以是合理的
🎈const对象不可以调用非const成员函数
const对象里面的成员函数被隐式的看成了const成员函数,因此就和上面的道理是差不多的了
🎈非const对象可以调用const成员函数
一句话就是,如果内部不涉及修改的,用const修饰,如果涉及修改的就不能加const
相信看到这里,你会对构造函数,析构函数,拷贝构造函数,赋值运算符重载有一个更深的认识