目录
1.类的六个默认成员函数
空类
class Date
{};
空类中并不是什么都没有,编译器自动生成六个默认成员函数。
2.构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。
构造函数是特殊的成员函数,其特征如下:
- 函数名与类名相同
- 无返回值
- 对象构造(对象实例化)时,编译器自动调用对应的构造函数。
- 构造函数可以重载。
- 构造函数可以在类中定义,也可以在类外定义。
- 如果类中没有显示定义构造函数,则C++编译器自定生成一个无参的默认构造函数,一旦用户显示定义,编译器将不再生成。
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
默认生成的构造函数,默认生成的构造函数一定是不带参数的
基本类型:int/char/数组/指针.....对基本类型不做处理
自定义类型:class/struct定义的类型....调用默认构造初始化
class Date{
public:
//如果没有定义构造函数,对象也能定义成功,此时会调用编译器生成的默认构造函数,成员变量是随机值
//1.无参构造函数
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
//2.带参构造函数
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();
Date d2(2018,12,14);
d2.Print();
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
//Date d3();
return 0;
}
构造函数保证对象定义出来就被初始化,减少了随机值。
再看看下面的测试用例
public:
//1.无参构造函数
//Date()//此时无参构造函数不能存在,会跟后面缺省参数会反生歧义
//{
// _year = 1900;
// _month = 1;
// _day = 1;
// }
//2.带参构造函数,多数情况下是用下面的构造函数
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;
d1.Print();
Date d2(2018,12,14);
d2.Print();
Date d3(2018,12);
d3.Print();
return 0;
}
3.析构函数
析构函数:与构造函数功能相反,在对象被销毁时,由编译器自动调用,完成类 的一些资源清理和汕尾工
作。
一般不需要用到析构函数,如malloc的空间就需要用到析构函数:
typedef int DataType;
class SeqList
{
public :
SeqList (int capacity = 10)//构造函数
{
_pData = (DataType*)malloc(capacity * sizeof(DataType));
assert(_pData);
_size = 0;
_capacity = capacity;
}
~ SeqList()//析构函数
{
if (_pData)
{
free(_pData );//释放堆上的空间
_pData = NULL; //将指针置为空
_capacity = 0;
_size = 0;
}
}
private :
int* _pData ;
size_t _size;
size_t _capacity;
};
析构函数是特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值。
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
注意:析构函数体内不是删除对象,而是做一些对象删除前的相关清理工作。
析构的顺序:从后往前析构,因为数据都是在栈桢上,满足后进先出的原则。
默认析构函数:清理资源
基本类型:int/char/数组/指针.....对基本类型不做处理,没有资源需要清理
自定义类型:class/struct定义的类型....调用默认析构函数
4.拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
#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)//加上const是起保护作用
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
}
private:
int _year;
int _month;
int _day;
};
int main ()
{
Date d2(2018,12,14);
d2.Print();
Date d4(d2);
d4.Print();
return 0;
}
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
- 若未显示定义,系统会默认生成默认的拷贝构造函数。 默认的拷贝构造函数会按照成员的声明顺序依次拷贝类成员进行初始化。(默认构造拷贝是值拷贝,浅拷贝,依次按字节拷贝)。默认拷贝构造只能针对浅拷贝,空间申请一次只能释放一次。
深拷贝的情况:举例顺序表:
先创建s1,再由s1拷贝构造s2,销毁的时候,s2先销毁,把空间释放了;s1再销毁,再次把空间释放。如果手机浅拷贝就是以下这种情况,就会出错。
此时就需要给s2单独开辟一片空间,这种情况被称为深拷贝:
对于SeqList类,发现如果使用编译器生成的默认拷贝构造函数与赋值运算符重载,编译没有问题,代码运行
也没有任何问题,但是在离开函数作用域销毁对象时,代码崩溃。
崩溃的原因是:当用s1拷贝构造s2时,编译器将s1中的值原模原样的拷贝到s1中,s2给s3赋值也是类似,因
为编译器生成的默认拷贝构造与赋值的方式只是简单将一个对象中的内容原封不动的拷贝到另一个对象中,
结果导致s1、s2共用同一块资源,在对象销毁时同一块内存销毁多次引起程序崩溃。
将编译器生成的默认拷贝构造函数以及赋值运算符重载称之为浅拷贝方式,即值的拷贝。因此如果一个类中
涉及到资源管理,不能使用编译器生成的默认拷贝构造以及赋值运算符重载(该问题后续有一个章节专门解
决)。
5.赋值运算符重载
5.1运算符重载
运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字依旧参数列表,其返回值类型与参数列表与普通的函数类似,函数名字为:关键字operator需要重载的操作符符号。
返回值类型 operator 需要重载的操作符(参数列表)
#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 (Date& d)//拷贝构造
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
}
bool operator==(const Date& d2 )//两个操作数,第一个左操作数,第二个右操作数
{
return this->_year == d2._year
&& this->_month == d2._month
&& this->_day == d2._day ;
}
private:
int _year;
int _month;
int _day;
};
int main ()
{
Date d1;
//d1.Print();
Date d2(2018,12,14);
//d2.Print();
cout<<(d1==d2)<<endl;
cout<<d1.operator==(d2)<<endl;
return 0;
}
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员的重载函数,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
5.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 (Date& d)//拷贝构造
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
}
//d2 = d1
//d2.oprator=(d1)
//d2.oprator=(&d2,d1)
Date operator=(const Date& d)//设置返回值是为了连续赋值,void不支持连续赋值
{
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;
Date d2(2018,12,14);
Date d3(2014,10,9);
// d2 = d1;
d3 = d2 = d1;//d3 = (d2.oprator=(&d2,d1))从右往左赋值
d2.Print();
d3.Print();
return 0;
}
赋值运算符要注意四点:
- 参数类型
- 返回值
- 检测是否自己给自己赋值
- 返回*this
注意:一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成值的拷贝工作。(浅拷贝)
【面试题】
哪些运算符不能重载? .* :: sizeof ?: .
构造、析构:初始化,清理
考别构造、operator=:复制
拷贝构造:Date d3(d1);//用一个对象去拷贝构造创建一个对象
赋值运算符重载: d2 = d1//一个已经存在的对象赋值给一个已经存在的对象
Date d4 = d1;//拷贝构造,在这之前d4还不存在
运算符重载和函数重载之间没有关系。
拷贝构造和赋值运算符重载都是浅拷贝。所以如果只是浅拷贝操作的话,我们可以不写着两个函数,但是如SeqList这类的深拷贝就必须要写。
6.const成员
7.1const修饰普通变量
在C++中,const修饰的变量已经成为一个常量,具有宏的属性,即在编译期间,编译器会将const所修饰的常量进行替换。常量不能修改。
7.2const修饰类的成员函数
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
注意:const成员函数汇总一定要修改某个成员变量,可在该成员变量前加mutable关键字。
【面试题】
- const对象可以调用非const成员函数和const成员函数吗?
- 非const对象可以调用非const成员函数和const成员函数吗?
- const成员函数内可以调用其它的const成员函数和非const成员函数吗?
- 非const成员函数内可以调用其它的const成员函数和非const成员函数吗?
- const对象不可以调用非const成员函数,可以调用const成员函数
- 非const成员可以调用非const成员函数和const成员函数
- const成员函数内可以调用其他的const成员函数,不可调用非const成员函数
- 非const成员函数内可以调用其他的const成员函数和非const成员函数
- 简单来说:const值可以调用const,非const都可以调用
7.取地址重载及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器会默认生成。只有特殊情况才需要重载,比如像让别人获取到指定的内容。
取地址重载可以控制取地址时返回的地址。
const Date*operator()const
{
return NULL;
}