目录
一、构造函数——默认成员函数一
1.概念
2.特性
二、析构函数——默认构造函数二
1.概念
2.特性
三、拷贝构造函数——默认成员函数三
1.概念
2.特征
3.浅拷贝与深拷贝
四、运算符重载
1.概念
2.加减运算符的重载(“+”、“-”、“+=”、“-=”)
3.简单运算符的重载(“>”、“<”、“>=”、“<=”、“!=”、“==”)
4.前置++和--、后置++和--(“++”、“--”)
5.赋值运算符重载(“=”)——默认成员函数四
6.流插入与流提取的重载(">")
五、const成员
六、取地址重载——默认成员函数五、六
类中有六个默认成员函数,这六个函数包括:构造函数、析构函数、拷贝构造函数、赋值运算符重载、普通对象取地址重载、const修饰对象的取地址重载。
所有的默认成员函数都是可以由编译器自己生成的,编译器会自己生成一个默认的函数。同时我们也可以自己定义这些函数,那么编译器就会直接使用我们定义好的函数而不会自己再生成了。
我们之前学过数据结构中的栈,其中有一个函数initstack(ST* p)这个函数非常容易忘记,所以C++中引入了构造函数负责变量的初始化。
int main()
{
stack s;//创建变量
stack* p = &s;
initstack(p);//这个初始化非常容易忘记
stackpush(p, 1);
stackpush(p, 2);
stackpush(p, 3);
stackpush(p, 4);
printf("%d\n",stacktop(p));
stackpop(p);
printf("%d\n", stacktop(p));
stackpop(p);
stackdestory(p);
return 0;
}
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。也就是说我们不用再去调用initstack函数,在创建变量后这个变量就直接被初始化完成了。
对于一个日期类,可以直接使用编译器内部生成的构造函数
#include
using namespace std;
class Date
{
public:
void print()
{
cout << _year << ' ';
cout << _month << ' ';
cout << _date << endl;
}
private:
int _year;
int _month;
int _date;
};
int main()
{
Date a;//在定义完变量后,也初始化完毕了
a.print();
return 0;
}
//结果:-858993460 -858993460 -858993460
从这里我们看到,每一个成员变量都是随机值。
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,编译器生成默认的构造函数会对自定类型成员调用的它的默认构造函数。
那么什么是默认构造函数?
默认构造函数其中一个是编译器自己生成的构造函数,另一个是自己定义的无参数或全缺省的构造函数。
//Date ();编译器自己生成的,没有显式定义
Date ()//无参数
{
_year = 1990;
_month = 1;
_date = 1;
}
Date (int year = 1990, int _month = 1, int _date = 1)//全缺省
{
_year = year;
_month = month;
_date = date;
}
//上面的这些都是默认构造函数,如果我们自己显示定义的(自己写的)构造函数
//不满足无参或全缺省时,就不能在定义变量后直接自动对自定义类型初始化
比如说,我们定义一个队列,这个队列由两个栈组成。那么,当我们定义一个这样的类的变量时,编译器生成的构造函数就会调用原来初始化栈的构造函数来初始化每一个栈。
对数据类值类型不做处理的这种方式时C++设计的一个不好的地方,但语言需要向下兼容,只能继续打补丁。C++11中针对内置类型成员不初始化的缺陷,内置类型成员变量在类中声明时可以给缺省值。
#include
using namespace std;
class Date
{
public:
void print()
{
cout << _year << ' ';
cout << _month << ' ';
cout << _date << endl;
}
private:
int _year = 1900;//缺省值
int _month = 1;//缺省值
int _date = 1;//缺省值
};
int main()
{
Date a;
a.print();
return 0;
}
//结果:1900 1 1
也可以自己写构造函数,编译器就直接使用写好的构造函数而不会再次生成。无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
#include
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int date = 1)//构造函数,如果没有传参数,变量的值为缺省值
{
_year = year;
_month = month;
_date = date;
}
void print()
{
cout << _year << ' ';
cout << _month << ' ';
cout << _date << endl;
}
private:
int _year;
int _month;
int _date;
};
int main()
{
Date a(2022, 10, 1);//按传参的数值初始化
a.print();
return 0;
}
同样是数据结构中的栈,其中又有一个函数stackdestory(ST* p)这个函数也非常容易忘记,可能会导致内存泄漏的问题。所以C++中引入了析构函数负责变量的清理。
int main()
{
stack s;//创建变量
stack* p = &s;
initstack(p);
stackpush(p, 1);
stackpush(p, 2);
stackpush(p, 3);
stackpush(p, 4);
printf("%d\n",stacktop(p));
stackpop(p);
printf("%d\n", stacktop(p));
stackpop(p);
stackdestory(p);//这个清除数据也非常容易忘记
return 0;
}
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。最常见的就是堆区空间的释放。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType)* capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack s;
s.Push(1);
s.Push(2);//在testStack结束调用时,就直接调用~Stack函数,相当于原来的destory函数
return 0;
}
编译器生成的默认析构函数,对自定类型的成员变量也会调用该自定义类型的析构函数。
#include
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;
}
//输出:~Time()
在main函数中没有直接创建Time类的对象,Date类型中有三个内置类型_year, _month,_day和一个自定义类型Date d,对于内置类型直接回收就可以了,但是自定义类型就需要调用对应类的析构函数。Date类型的d就需要调用~Date销毁,创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数。
我们在创建对象时,一定会有需要创建另一个一模一样的变量。
那在创建对象时,怎么创建一个与已存在对象一某一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
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;
}
#include
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
void print()//内部成员函数,打印
{
printf("%d ", _year);
printf("%d ", _month);
printf("%d\n", _day);
}
private:
int _year = 1970;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1(2022, 10, 1);//
Date d2(d1);//d2是新创建的变量,d2相当于d1的赋值粘贴
d1.print();
d2.print();
return 0;
}
//结果:
//2022 10 1
//2022 10 1
浅拷贝是指,只拷贝对象的具体内容,完全复制粘贴。
深拷贝是指,拷贝对象的具体内容,深拷贝在计算机中开辟一块新的内存地址用于存放复制的对象。源数据改变不会影响复制的数据。
比如说,我有一个C++类实现的栈的变量s1,我想拷贝一份与原来存储数据相同的栈s2。
#include
using namespace std;
typedef int TYPE;
class Stack
{
public:
Stack(size_t capacity = 4)//构造函数
{
_arr = (TYPE*)malloc(sizeof(TYPE)* capacity);
if (NULL == _arr)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
Stack(const Stack& s)//拷贝构造
{
_arr = s._arr;
_capacity = s._capacity;
_size = s._size;
}
~Stack()
{
if (_arr)
{
free(_arr);
_arr = NULL;
_capacity = 0;
_size = 0;
}
}
void Push(TYPE data)
{
_arr[_size] = data;
_size++;
}
void print()
{
printf("0x%p ", _arr);
printf("%d ", _capacity);
printf("%d\n", _size);
}
private:
TYPE* _arr;
int _capacity;
int _size;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
Stack s2(s1);
s1.print();
s2.print();
return 0;
}
我们把断点设在return 0;处查看s1和s2的内部变量值
s1和s2的内容完全是一模一样的,相当于两个指针都指向了同一个位置,并没有做到再次新建一个一样的栈。当我们继续向下运行时,系统会调用两次析构函数,free函数对同一块空间进行了两次释放,报错为触发断点。
此时就需要深拷贝了,让不同变量的_arr指向不同的内存空间,变量的值可以直接赋值。
下面是改造后深拷贝的构造函数:
Stack(const Stack& s)
{
_arr = (TYPE*)malloc(sizeof(TYPE)* _capacity);
if (NULL == _arr)
{
perror("malloc申请空间失败!!!");
return;
}//我们再次向系统申请一块空间
_capacity = s._capacity;
_size = s._size;
}
简单来说,类中如果没有涉及资源申请(比如说动态内存管理,文件的打开关闭等)时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
这里只是简单介绍一下深拷贝,并不代表这就是深拷贝的所有内容,更多的内容还是需要以后学习。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
比如说:我们平常使用的加减只是普通的计算(1+1=2),如果我们想知道某一个日期过三天后或者往前倒三天的日期是几号(2022.10.1 - 3 = 2022.9.29,2022.10.1 + 3 = 2022.10.4),我想用+-的方式简单实现日期的前后查看,通过函数实现这个与我们常见四则运算的不同的+-也就是运算符重载。
函数名字为:关键字operator后面接需要重载的运算符符号。函数原型:返回值类型 operator操作符(参数列表)
class Date
{
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month)
{
static int arr[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
//static可以减少内存占用,对应坐标对应相应天数
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))//闰年的2月特殊
{
if (month == 2)
return arr[month] + 1;
}
return arr[month];
}
// 日期+=天数
Date& operator+=(int day)
{
_day += day;//先把day加到this的_day上
while (_day > GetMonthDay(_year, _month))//判断day是否大于本月的日期
{
_day -= GetMonthDay(_year, _month);//大于则剪掉这个月的天数
_month += 1;//转到下个月
if (_month == 13)//没有13月,到了13就转为1
{
_year++;//转为1月,年也要加一年
_month = 1;
}
}
return *this;//返回引用减少拷贝
}
// 日期-=天数
Date& operator-=(int day)
{
_day -= day;//先把day减到this的_day上
while (_day <= 0)//判断day是否减为负
{
_month--;//向前找月份
_day += GetMonthDay(_year, _month);//把这个月份的天数加上去
if (_month == 0)//没有0月,再次变为12月
{
_year--;//往前倒一年
_month = 12;
}
}
return *this;//返回引用减少拷贝
}
// 日期+天数
Date operator+(int day)
{
Date a(*this);//拷贝构造一份一模一样的a
a += day;//操作a
return a;//返回a,保证原来的数据不被改变
}
// 日期-天数
Date operator-(int day)
{
Date a(*this);//拷贝构造一份一模一样的a
a -= day;//操作a
return a;//返回a,保证原来的数据不被改变
}
};
对于一个类的大小比较运算符重载,我们只需要写大于与等于的具体实现,剩下的通过这两个运算符重载函数就可以实现。
class Date
{
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month)
{
static int arr[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
//static可以减少内存占用,对应坐标对应相应天数
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))//闰年的2月特殊
{
if (month == 2)
return arr[month] + 1;
}
return arr[month];
}
// >运算符重载
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;
}
// ==运算符重载
bool operator==(const Date& d)
{
if (_year == d._year && _month == d._month && _day == d._day)
{
return true;
}
return false;
}
// >=运算符重载
bool operator >= (const Date& d)
{
return !(*this < d);
}
// <运算符重载
bool operator < (const Date& d)
{
return !(*this > d);
}
// <=运算符重载
bool operator <= (const Date& d)
{
return !(*this > d);
}
// !=运算符重载
bool operator != (const Date& d)
{
return !(*this == d);
}
};
前置++(--)和后置++(--)在符号上是一样的,我们通常在后置++(--)的函数加上一个int参数,这个参数只是用于辨别,甚至不用起名,真正的函数也不会接收这个参数。
// 前置++
Date& operator++()
{
*this += 1;
return *this;
}
// 后置++
Date& operator++(int)
{
Date a(*this);
*this += 1;
return a;
}
// 后置--
Date& operator--(int)
{
Date a(*this);
*this -= 1;
return a;
}
// 前置--
Date& operator--()
{
*this -= 1;
return *this;
}
老生常谈,“=”和“==”并不一样,一个是赋值另一个是判断相等,这里讲解赋值的重载。编译器默认生成赋值运算符重载的函数可以做到类似memcpy的逐字节拷贝。
在实现这个函数时需要注意下面四点:
这个运算符的重载函数非常像拷贝构造,但是还是有所不同。
一般而言,拷贝构造函数用于一个新建的类变量的初始化,而赋值运算符重载函数用于两个已经创建的变量的赋值。
int main()
{
Date a(2018,6,28);//调用拷贝构造函数
Date b = a;//调用拷贝构造函数,b的初始化
Date c;
c = a;//调用赋值运算符重载
return 0;
}
我们当然也可以自己实现这个函数,最简单的实现方式就是每一个变量都各自赋值
void operator=(const Date& d)//传参和返回都用引用避免了两次对象的拷贝
{
_year = d._year;
_month = d._month;
_day = d._day;//把每一个内部的变量赋值
}
如果我们有两个Date类型的a和b变量,对于a = b;这个表达式,左侧的a一般为this指针指向的内容,右侧的b为参数。(也有反过来的情况,不过很少)
上面这个函数是我们写的第一版赋值运算符重载,对于这个函数中我们看看它有什么样的问题。
int main()
{
Date a(2018,6,28);
Date b;
Date c = b = a;
return 0;
}
我们实现的第一版赋值运算符重载是可以满足我们单次赋值的需要
但是连续赋值上就会出现很大问题
Date c = b = a;中b = a是一个表达式,这个表达式根据赋值运算符重载函数处理后,最后的返回类型为void,void的表达式结果与Date类型无法匹配,所以没有办法连续赋值,还是要进行改进。
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;//一般返回的都是表达式左侧的内容,也对应*this
}
上面是第二版,我们继续分析它问题
自己给自己赋值,你可能会想,这不纯纯有病吗。笔误也有可能,毕竟是情况就都需要考虑。
对于单纯的值拷贝即使你这里不做处理也无所谓,但是这种没有意义的赋值我们本来就没有必要做,所以可以加个if语句可以避免对这样的情况的处理。
Date& operator=(const Date& d)
{
if(&d != this)//避免自己赋值自己
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;//一般返回的都是表达式左侧的内容,也对应*this
}
到这里我们都是在自己实现进行值拷贝的赋值运算符重载,但是我们在最开始就已经知道了,C++编译器自己生成的赋值运算符重载就已经可以做到每一个字节的精确拷贝,所以这样的值拷贝我们也没有必要去自己实现这个函数,用编译器自己生成的就好了。
对于值拷贝就可以达到赋值的类确实没什么问题,但是一旦涉及到动态内存就会出现问题,比如下面的栈(只实现了部分功能):
#include
using namespace std;
#define int TYPE;
class Stack
{
public:
//构造函数
Stack(size_t capacity = 4)
{
_arr = (TYPE*)malloc(sizeof(TYPE)* capacity);
if (NULL == _arr)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
//拷贝构造函数
Stack(const Stack& s)
{
_arr = (TYPE*)malloc(sizeof(TYPE)* _capacity);
if (NULL == _arr)
{
perror("malloc fail");
return;
}
_capacity = s._capacity;
_size = s._size;
}
//析构函数
~Stack()
{
if (_arr)
{
free(_arr);
_arr = NULL;
_capacity = 0;
_size = 0;
}
}
//压栈
void Push(TYPE data)
{
_arr[_size] = data;
_size++;
}
//赋值运算符重载
Stack& operator=(const Stack& s2)
{
_arr = s2._arr;
_capacity = s2._capacity;
_size = s2._size;
return *this;
}
private:
TYPE* _arr;
int _capacity;
int _size;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
s2.Push(1);
s2.Push(2);
s2 = s1;
return 0;
}
别看就这两行代码,内部的毛病可大了去了。代码的问题不只是深拷贝的问题,还有内存泄漏。
这不试不知道,一试吓一跳啊。所以,这种涉及到动态内存之类的赋值运算符重载函数还是要自己实现的。
我们在实现这样有动态内存的拷贝时最好先释放左侧变量的空间,然后再重新开辟空间,其余的部分进行值拷贝。
Stack& operator=(const Stack& s)
{
free(this->arr);
_arr = (TYPE*)malloc(_capacity*sizeof(TYPE));
_capacity = s._capacity;
_size = s._size;
return *this;
}
先说流提取:
在C++中,我们在对内置类型向屏幕中输入时不需要C语言中对类型进行识别。其实这也并不是魔法,只是用了用函数重载的思想对流插入与流提取进行了不同内置类型的重载,相当于就是穷举法。
但是我们自己实现的自定义类型,并没有实现相应数据的运算符重载,所以需要我们自己写。而且这个函数一般作为公有函数定义在类外面。
根据Date类型内部的_year,_month,_day三个变量,我们可以规定输出格式。
这是我们实现的第一版函数
void operator<<(ostream& out)//out是cout的别称,cout是一个ostream类型的变量
{
out << _year << '/';
out << _month << '/';
out << _day << '/';
}
这个函数并不能实现我们需要的功能,由于this指针默认是函数的左参数,此刻就发生了这样的情况:
void operator<<(Date* this, ostream& out)
这是这个函数的声明,但是如果我们执行:cout<
经过改进,这是我们实现的第二版函数
//流插入
class Date
{
//……
friend inline ostream& operator<<(ostream& out, const Date& d);//友元声明
//……
};
inline ostream& operator<<(ostream& out, const Date& d)
//函数较小并调用次数多,可以设置为内联
{
out << d._year << "/";
out << d._month << "/";
out << d._day << endl;
return out;//返回值为cout,可以做到连续输出
}
对于流插入也是相似的,但是由于流插入的重载函数定义在类的外面,不能访问类内的私有变量。所以我们需要在相应的类内的任意地方加上一个友元声明(friend+函数声明),就可以访问私有对象了。
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
class Date
{
public:
//输出打印
void print()
{
std::cout << _year << ' ';
std::cout << _month << ' ';
std::cout << _day << std::endl;
}
private://封装
int _year;
int _month;
int _day;
};
int main()
{
const d1(2018,6,28);
cout<< d1 <
我们定义一个const修饰的d1变量,我们如果调用d1的print函数,print函数中有一个Date* this的指针,但是此时我们传的参数是const Date*类型的指针,属于权限的放大,会报错。(之前也说过权限的放大和缩小,这个说法只在引用和指针中有效)
可以加上在函数后加上const将Date* this用const修饰,就可以编过了。
void print() const == void print(const Date* this)
所以,对于不需要对对象的内部变量值进行改变的函数最好都要加上const修饰参数,比如说:“>”、“=”、“>=”、“<="、“!=”、“==”、“<<”等
取地址重载分为:普通对象取地址重载、const修饰对象的取地址重载。
class Date{
public:
Date* operator&() //普通对象取地址重载
{
return this;
}
const Date* operator&() const //const修饰对象的取地址重载
{
return this;
}
private:
int _year;
int _month;
int _day;
};
这个函数一般都不需要我们自己去写,用编译器默认生成的就可以了。只有一个十分小众的情况会需要自己实现,就是我不想让别人获取到某一个类的变量的地址,这个函数的实现很难会用到,了解一下就够了。
Date* operator&() //普通对象取地址重载
{
return nullptr;
}
const Date* operator&() const //const修饰对象的取地址重载
{
return nullptr;
}