文章目录
类的默认的六个成员函数
当我们在类中什么都不写的时候,这个类里面真的是什么都没有吗?其实并不是,当我们没有写成员函数的时候,编译器会在类内默认生成六个成员函数。
这六个是类的默认成员函数
构造函数
构造函数是负责对象的初始化工作的,并不是构建对象。
我们以前在写C语言的数据结构的时候,经常会忘记调用Init初始化数据结构,这里的构造函数就是为了解决对象没有初始化的问题,因为构造函数会在对象实例化的时候自动调用进行初始化。
构造函数的特性:函数名与类名相同,无返回值,创建对象的时候由编译器自动调用,可以保证每个对象都有一个初始值,在对象的生命周期内只会调用一次。构造函数式可以重载的。
下面来看代码
这里可以看到我们写了一个Date()的构造函数,创建对象后并不需要我们主动调用这个对象就已经初始化好了。
注意:void并不是无返回值而是空返回值,这里无返回值我们就直接不写。
当然如果我们想要主动传参也是可以的。就如下面这样:
class Date
{
public:
Date()
{
_year = 2022;
_month = 6;
_day = 18;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 6, 23);
return 0;
}
这里要注意写法,是直接将参数跟在对象后面,这样就可以给构造函数传参了,可以看到我们写的两个构造函数构成了函数重载(这就使得我们可以有多种初始化对象的方式)可以使用默认值也可以使用给定值初始化。但是这样写两个函数略显复杂,我们可以想一想,是不是可以使用全缺省函数呢?
class Date
{
public:
Date(int year= 2022, int month = 6, int day = 18)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
就像上面这个样子,我们不传参的时候自动调用就是使用默认值。
但是要注意下面这种写法,是错误的
class Date
{
public:
Date()
{
_year = 2022;
_month = 6;
_day = 18;
}
Date(int year= 2022, int month = 6, int day = 30)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
这里既写了无参的构造函数,又写了全缺省的,当不传参的时候,编译器就混乱了,因为它不知道改调用无参的构造函数,还是调用全缺省的构造函数使用默认值初始化,这个错误叫做调用不明确所以我们写的时候就写一个全缺省即可(这是大多数情况,还有的情况不写)
我们上面提到过如果不写构造函数,那编译器会自动生成一个构造函数,那么这个构造函数对于对象的初始化是什么样子呢?
class Date
{
public:
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
答案是,对于内置类型(就是int等这些)不会进行初始化,就是还是随机值,但是对于自定义类型,比如我们再次定义一个类,这时候就会调用这个类的构造函数对这个类的对象进行初始化。
class A
{
public:
A()
{
_a = 10;
_b = 20;
}
private:
int _a;
int _b;
};
class Date
{
public:
private:
int _year;
int _month;
int _day;
A _a;
};
int main()
{
Date d1;
return 0;
}
就如同上面的代码,在Date类里面包含了一个A的类的对象,在调用构造函数的时候对于int这些内置类型,编译器不会进行处理,对于A这样的类对象,会去调用他们的构造函数进行初始化。
这个会在数据结构能用到,比如我们用链表实现栈的时候,那这个栈里面的成员变量还包含着一个链表的类的对象。
如上图就是类似的意思,如果stack里面包含了链表类的节点对象,那就不需要调用链表的构造函数进行初始化了。
最后注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
析构函数
既然创建了对象,那最后就要销毁对象,构造函数不是创建对象而是负责初始化对象,析构函数也不是负责销毁对象,而是在对象销毁了之后调用用来清理资源。
特性
1,析构函数名是类名,但是前面要加~
2,无返回值无参数
3,一个类只有一个析构函数,如果不写系统自动生成。
4,析构函数会在对象销毁(生命周期结束)的时候由C++编译系统自动调用。
C++的析构函数本质上对于内置类型是没有什么用的,只有用malloc申请过空间的才有用,用来防止内存泄漏,我们知道在服务器端方面,内存泄漏是很严重的问题而且相比于不初始化,忘记释放内存是不会报错的,这就是隐藏的利刃,当服务器因为内存泄漏崩溃之后,你才能发现内存泄漏的问题。
C++不像java还有垃圾回收器,C++只能靠析构函数和后面会说的的智能指针来防止内存泄漏。
使用
class SList
{
public:
SList(int size = 10)
{
_arr = (int*)malloc(sizeof(int) * size);
_size = 0;
_capacity = size;
}
~SList()
{
if (_arr)
{
free(_arr);
_arr = nullptr;
_size = 0;
_capacity = 0;
}
}
private:
int* _arr;
int _size;
int _capacity;
};
int main()
{
SList _new;
return 0;
}
这里我们就是自己定义的析构函数,在_new这个对象生命周期结束后,就会自动释放对象内的_arr,malloc出来的空间,又叫清理资源。
这里还有一个小问题:比如我们定义了多个对象,那么在最后析构的时候的顺序是怎样的呢?
int main()
{
SList n1;
SList n2;
SList n3;
return 0;
}
这里可以看到我们定义了三个对象,在定义的时候,构造顺序是n1,n2,n3,在析构的时候是n3,n2,n1;因为我们的对象创建在栈上,栈区的性质也是类似操作系统的后进先出,比如最后入栈的n3会是最先出栈的,因此也是n3先析构的。
提到这个栈,有一个点,就是内存分段中的栈区和堆区,实际上和数据结构的栈,堆,没有任何直接练习,因为这两个东西属于两个不同的学科,唯一可以说相似的就是栈区和栈都是后进先出的。堆是个完全二叉树,内存的堆区是负责动态分配内存的,这两,没有一点联系。
既然是析构函数也是默认成员函数,那不写的时候也是会默认生成一个的,他的处理方式和构造函数也是一样的。对于内置类型不起作用,对于自定义类型会去调用它的析构函数。
因此,如果我们malloc申请了空间,那么就需要自己写出析构函数进行内存释放,方便的是不需要我们手动去调用。
class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
class Person
{
private:
String _name;
int _age;
};
int main()
{
Person p;
return 0;
}
这段代码可以看出,编译器默认生成的析构函数对于自定义类型是去调用它自身的析构函数。
如果对象内没有资源需要清理,不需要写,系统自动生成就够用,而且在release版本下,编译器会优化没了把这个析构函数,因为没有资源需要清理。
总结构造函数和析构函数
对于内置类型,构造函数和析构函数都是不起作用的,对于自定义类型回去调用这个对象的类的构造和析构函数。
拷贝构造函数
拷贝构造函数的应用场景是:创建一个对象用另一个同类的对象对他进行初始化。
就像是内置类型中的int a = b;用b给a初始化值。
拷贝构造的代码就像下面这样
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
{
_year = year;
_month = month;
_day = day;
}
//这就是拷贝构造函数
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);
Date d3 = d1;
return 0;
}
从上面代码可以看出来拷贝构造函数其实是构造函数的重载。下面定义d2,d3对象的时候初始化用的都是d1调用了拷贝构造函数,这里两种形式都可以。第二种实际上是重载了赋值操作符然后调用了拷贝构造函数,这个后面会讲的。
由此我们引出来了拷贝构造函数的特性
特性
1,拷贝构造是构造函数的重载函数
2,拷贝构造函数的参数只有一个而且只能是类的引用类型的(隐含的类型*this指针不算)如果参数不是引用会引发无限递归。
原因:如果使用传值,参数是Date d,那么值传参的时候就要将实参的值赋值给形参,这时候就需要哦调用拷贝构造,然后就无限递归了。
最后当然要讨论的是不写的话编译器自动生成的是什么样子啦
先说结论:当我们不写的时候,编译器自动生成的拷贝构造函数也可以完成浅拷贝,也就是值拷贝,在我们这个Date类上面是没有错误的,但是一旦到了成员对象有指针,指针指向的空间是malloc出来的时候就会出错了。来看代码:
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
{
_year = year;
_month = month;
_day = day;
}
void SetDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.SetDate(2022, 6, 18);
Date d2(d1);
Date d3 = d1;
return 0;
从这个调试界面我们可以看出来,不写的话编译器默认自动生成的拷贝构造函数对于我们的Date类是完全没有问题的。浅拷贝完全能满足我们Date类对象的需求。
那么现在来看下面这段代码
class stack
{
public:
stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
_size = 0;
_capacity = capacity;
}
~stack()
{
if (_a != nullptr)
{
free(_a);
_a = nullptr;
_size = 0;
_capacity;
cout << "free is ok" << endl;
}
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
stack s1;
stack s2(s1);
return 0;
}
可以看到这段代码我们手动实现了构造和析构,然后调用编译器默认的拷贝构造函数给对象s2初始化成与s1相同的对象,但是我们程序运行到最后的时候却崩溃了。
上面崩溃的图中我们可以看出,我们完成了一次s2对象的析构,然后第s1对象还没有析构,程序挂了,这是什么原因呢?看下面这个调试图,可以看到两个对象的成员对象_a指向的是同一块空间,原因出现了就是我们对同一块空间释放了两次所以程序崩溃了。这也说明了系统默认生成的拷贝构造函数进行了浅拷贝,把s1对象_a指针的地址拷贝给了s2中的_a.
因此对于这种类,我们就需要手动实现深拷贝构造函数,比较复杂后面会单独讲
至于为什么释放两次就会崩溃,比如:我把钱借给你,你不用的时候还给我,但是你不能还两次把,系统方面的问题就是,这块空间收回来之后,有可能又给了别人用,别人还没用完你给他释放了这是不是会出问题的。所以不能两次释放同一块空间。
总结
对于date这种类,没有涉及到malloc或者指针之间的关系的时候是可以不写的,系统默认生成的拷贝构造函数实现的浅拷贝完全够用,但是到了stack这类的时候就不可用默认拷贝构造函数了。
赋值运算符的重载
运算符重载
我们在写内置类型的时候想要比较大小或者加减乘除赋值,都可以用运算符来实现,±*/,但是我们的自定义类型要是只能通过函数来实现的话,不仅麻烦而且可读性也不高,因为不是每个人对这些函数的取名都很规范。所以我们的C++引入了运算符重载,让自定义类型也可以通过运算符重载后使用运算符。
先来了解一下运算符重载的特性:
1,定义函数的方式函数名是operator后面接上要重载的符号。
2,函数的返回值与普通函数类似,可以根据情况自定义。但是重载函数的参数必须有一个类的类型的参数或者枚举类型的参数。
3,重载的操作符不能改变其原来的含义,比如+不能重载成减法。
4,如果运算符重载函数定义为全局的函数,是有两个操作数的,定义成成员函数就会少一个,有一个参数是this指针。(this指针一定是第一个形参)
5,“.*”,“::”,“:?”,“sizeof”,“.” 这五个运算符,点星,域作用访问限定符,三目运算符,sizeof,和点操作符,是不可以重载的。
下面来简单看一下运算符重载的使用。
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造和析构都可以使用默认。
bool operator==(const Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
if (d1 == d2)
cout << "yes" << endl;
return 0;
}
这段代码我们重载了==来判断自定义的类是不是相等。
赋值运算符重载
这里我们来看一下赋值运算符的重载
特性:
1,参数类型(参数类型是引用,可以减少传值的时候拷贝构造的性能消耗)
2,返回值(为了可以使用连续赋值,返回值要是第一个对象参数的引用)
3,检测一下是不是在给自己赋值,如果是直接返回即可。
4,返回的是第一个对象的引用,因为第一个对象出函数也不会销毁所以返回值是*this
5,如果这个类没有手动写,那编译器会自动生成一个赋值运算符的重载函数。
下面来看代码
class Date
{
public:
Date(int year = 0, 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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022,6,19);
Date d2;
d2 = d1;
Date d3;
d3 = d2 = d1;
return 0;
}
这里我们通过这个返回值返回类的引用就可以实现连续赋值的情况。
我们上面也提到了赋值运算符是默认成员函数,就算我们不写,编译器也会自动生成。先说出结论就是:这个默认生成的赋值运算符重载函数,与默认生成的拷贝构造函数是类似的,只能实现按照字节序拷贝也就是浅拷贝,如果遇到Stack那种存在malloc空间的类就会出现和拷贝构造函数类似的错误,对同一块空间释放了两次。
来看一下代码:
可以看到对于日期类这种浅拷贝是没有问题的。
总结拷贝构造函数和赋值运算符重载
1,拷贝构造函数实际上是和构造函数形成函数重载,有一个类的对象的引用这个参数,无返回值
2,赋值运算符重载是一个operator=为函数名的函数,参数是一个对象的类的引用,返回值也是一个一个对象的引用,因为这个对象出了函数并不会销毁。
3,拷贝构造函数和赋值运算符重载函数,如果不写,系统默认生成的都是实现的按照字节序拷贝,也就是浅拷贝或者说是值拷贝,对于日期类这种,没有问题,但是对于Stack就会出现问题。
日期类的实现
//Date.h
#pragma once
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
class Date
{
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month)
{
static 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 % 100 != 0) || (year % 400 == 0)))
{
day += 1;
}
return day;
}
//打印日期
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
if (month > 12 || day<0 || day>GetMonthDay(year, month))
{
cout << "非法日期" << endl;
}
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
// d2(d1)
Date(const Date& d)
{
*this = d;//复用赋值运算符重载
}
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
// 析构函数
~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
// 日期+=天数
Date& operator+=(int day)
{
_day += day;
while (_day> GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month > 12)
{
_year++;
_month = 1;
}
}
return *this;
}
// 日期+天数
Date operator+(int day)
{
Date ret(*this);
ret += day;
return ret;
}
// 日期-天数
Date operator-(int day)
{
Date ans(*this);
ans -= day;
return ans;
}
// 日期-=天数
Date& operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
// 前置++
Date& operator++()
{
*this += 1;
return *this;
}
// 后置++
Date operator++(int)
{
Date ret = *this;
*this += 1;
return ret;
}
// 后置--
Date operator--(int)
{
Date ret = *this;
*this -= 1;
return ret;
}
// 前置--
Date& operator--()
{
*this -= 1;
return *this;
}
// >运算符重载
bool operator>(const Date& d)
{
if (_year > d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month > d._month)
{
return true;
}
else if (_month == d._month)
{
if (_day > d._day)
{
return true;
}
}
}
return false;
}
// ==运算符重载
bool operator==(const Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
// >=运算符重载
inline bool operator >= (const Date& d)
{
if (_year < d._year)
{
return false;
}
else if (_year == d._year)
{
if (_month < d._month)
{
return false;
}
else if (_month == d._month)
{
if (_day < d._day)
{
return false;
}
}
}
return true;
}
// <运算符重载
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 operator-(const Date& d)
{
int count = 0;
int flag = 1;
Date max = *this;
Date min = d;
if (max < min)
{
max = d;
min = *this;
flag = -1;
}
while (max != min)
{
min++;
count++;
}
return count * flag;
}
private:
int _year;
int _month;
int _day;
};
//test.cpp测试文件
#include"Date.h"
void Test1()
{
Date d1;
d1.Print();
Date d2(2022, 5, 3);
d2.Print();
Date d4(2022, 7, 31);
d4.Print();
Date sum = d4 + 1000;
sum.Print();
d4.Print();
}
void Test2()
{
Date d1(2022, 1, 10);
Date d2 = d1 - 20;
d2.Print();
d1.Print();
Date d3(2022, 12, 31);
Date ret = d3++;
ret.Print();
d3.Print();
}
void Test3()
{
Date d1(2022, 2, 28);
Date d2 = --d1;
d2.Print();
d1.Print();
}
void Test4()
{
Date d1(2022, 2, 28);
Date d2(2022, 2, 28);
cout << (d1 != d2) << endl;
}
void Test5()
{
Date d1(2022, 6, 19);
Date d2 = d1;
Date d3(2022, 7, 18);
cout << (d3 - d1) << endl;
}
int main()
{
Test5();
return 0;
}
可以看到在上面代码的实现中,很多函数是可以复用的。比如拷贝构造函数可以复用赋值运算符重载。比大小的我们只需要实现一个>and >=的实现,其他的都可以复用这两个函数加上取反符号来实现
注意:最后的日期减日期的实现可以使用找出较大的那个日期和较小的那个日期,然后让小的那个不断自增等和较大的那个相等了这时候计数器里面的值就是他们之间的天数差。
留一个问题,int占位符如何是保证后置++能调用到正确地重载函数呢?
答案是:后置++编译器在调用的时候会默认传一个0过去,所以我们参数加上一个int来占位,与前置++的函数构成重载。
const 修饰的成员函数
在我们写成员函数的时候,有没有时候想要这个this指针指向的对象不可以被修改,比如我们在Print的时候,或者是>这类比较重载的时候,我们都是不需要改变this指针指向的对象的。这时候为了防止我们不小心写错改变了this指针指向的对象,于是我们可以用const把这个对象保护起来。
bool operator==(const Date& d)const
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
//bool operator==(const Date* this,const Date& d)
比如这样,就是直接在函数后面加上const即可,这时候函数就变成了,注释掉的样子,加了const就表示类的对象不可以被修改了。
下面来思考这几个问题
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其它的非const成员函数吗?
- 非const成员函数内可以调用其它的const成员函数吗?
答案是:
1,不可以,const对象不可以调用非const对象,const对象只读,如果变成了普通对象就是可读可写的,权限放大了。
2,可以,非const的普通对象是可读可写的,调用const成员函数被修饰成只读的,权限缩小了。
3,不可以,const成员函数的this指针是const属性的,调用非const会变成普通属性的const,权限放大了。
4,可以,非const调用const类的成员函数,普通的this指针,传给了const的this指针,权限缩小。
取地址以及const取地址重载
最开始我们提到了六个默认成员函数,就是我们不写,编译器也会默认生成的那些,在这里列举一下,构造函数,析构函数,拷贝构造函数,赋值运算符重载函数,取地址重载函数和const取地址重载函数,就是这六种,但是其实最后这两种并没有什么作用,甚至可能用不到,所以了解一下即可。
//&运算符重载
const Date* operator&()const
{
return this;
}
Date* operator&()
{
return this;
}
就是这两种,其实取地址并不需要什么重载函数,我们取这个对象的地址的时候直接取就可以了,我们在创建这个对象的时候,肯定给他了一块内存空间,不管是自定义类型还是内置类型,只要返回他的第一个字节的地址就可以了。所以上面的了解一下,绝大多数时候都是不会写的。
唯一可能的用法就是下面这种。
//&运算符重载
const Date* operator&()const
{
return nullptr;
}
Date* operator&()
{
return nullptr;
}
那就是我不想让你取到我这个类的对象,可以像上面这样子写。