类的六大默认成员函数
1.构造函数
1.1定义理解
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
类似于我们C语言写的init初始化函数。
1.2语法特性
1.函数名和类名相同
2.无返回值
3.可以进行函数重载
4.对象实例化时自动调用
5.5如果类中没有显式定义构造函数,编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
class Date
{
public:
//无参构造
Date()
{
}
//有参构造 是无参构造的函数重载
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, 2, 28);
return 0;
}
那么编译器生成的默认构造函数会做什么事呢?
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型。
编译器生成的默认构造函数对于内置类型不做处理,对于自定义类型调用它的默认构造函数。
class A
{
private:
int a;
public:
A()
{
cout << "A()" << endl;
}
};
class B
{
private:
A a;
public:
};
int main()
{
B b;
}
运行结果:
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
1.3默认构造函数
注意:要区分默认构造函数和默认成员函数
默认成员函数:
用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
三大默认构造函数:
无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数
特点:默认构造函数只能有一个
//...
Date()
{
}
Date(int year = 2000, int month = 2, int day = 29)
{
_year = year;
_month = month;
_day = day;
}
//...
int main()
{
Date d1;
}
运行结果:报错
1.4初始化列表
1.4.1初始化列表和构造函数
Date(int year = 2024, int month = 3, int day = 4)
{
_year = year;
_month = month;
_day = day;
}
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
1.4.2语法
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
以日期类为例:
Date(int year, int month, int day):
_year(year),_month(month),_day(day)
{
}
1.4.3注意事项
1.每一个变量只能在初始化列表出现一次(初始化只能初始化一次)
2.必须使用初始化列表的情况:
const成员变量
引用成员变量
自定义类型成员(且该类没有默认构造函数时)
3..成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
4.在类的成员变量声明的时候可以给缺省值,当初始化列表没有出现该成员变量时使用缺省值初始化
5.不管是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化
注意事项3验证:
class A
{
public:
A(int a)
:_a2(a),_a1(_a2)
{
}
void Print()
{
cout << "_a1:" << _a1 << " _a2:" << _a2 << endl;
}
privat:
int _a1;
int _a2;
};
int main()
{
A a(2);
a.Print();
return 0;
}
结果:
1.4.4缺省值和初始化列表
在调用构造函数时无论写没写初始化列表最先走的是初始化列表,如果当初始化列表没有出现该成员变量时,使用声明处的缺省值。
class Date
{
public:
//情景1
Date(int year = 2027, int month = 2, int day = 2)
{
}
//情景2
Date(int year = 2027, int month = 2, int day = 2)
:_year(year), _month(month), _day(day)
{
}
//情景3
Date(int year = 2027, int month = 2, int day = 2)
:_year(year), _month(month)
{
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year = 2024;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
情景1结果:
情景2结果:
情景3结果:
1.5explicit关键字
1.5.1隐式类型转换
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
1.单参数形
//单参数形
Date(int year)
{
}
int main()
{
Date d1 = 2024;
return 0;
}
2.多参数形
虽然有多个参数,但是创建对象时后两个参数可以不传递(C++11以后可以支持)
//多参数形
Date(int year = 2022, int month = 2, int day = 2)
:_year(year), _month(month), _day(day)
{
}
int main()
{
Date d1 = 2024;
//Date d1 = 2024, 1; //错误
Date d2 = {2024, 1}; //C++11支持
return 0;
}
1.5.2expilcit关键字
explicit修饰构造函数,禁止类型转换,如果再有以上的写法编译器会报错
explicit Date(int year = 2022, int month = 2, int day = 2)
:_year(year), _month(month), _day(day)
{
}
2.析构函数
2.1定义理解
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
2.2语法特性
1.函数名在类名前面加上~
2.无返回值
3.不能重载
4.对象生命周期结束时自动调用
5.如果没有显式写析构函数,编译器会自己生成一个
那么系统自动生成的析构函数会做什么事情呢?
示例:
class A
{
public:
A()
{
}
~A()
{
cout << "~A()" << endl;
}
private:
int i;
};
class B
{
public:
B()
{
}
private:
A a;
};
int main()
{
B b;
}
输出结果:
结论:
对于内置类型不做处理,对于自定义类型调用它的析构函数。 因此如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date(日期)类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
2.3对象销毁的顺序
下面通过打印结果来研究对象销毁的顺序:
class Date
{
public:
//有参构造
Date(int year = 2000, int month = 1, int day = 1)
{
//cout << "Date(int year, int month, int day)" << endl;
cout << this << "->Date()" << endl;
_year = year;
_month = month;
_day = day;
}
//析构函数
~Date()
{
cout << this << "->~Date()" << endl;
}
private:
int _day;
int _month;
int _year;
};
int main()
{
Date d1(1);
Date d2(2);
Date d3(3);
cout << "------------------------" << endl;
}
对于局部变量:
结论:
对于局部对象,先定义的先构造,后定义的先析构。
那么如果加上全局变量和静态变量后结果又是如何呢?
我们将析构函数的打印信息调整为打印_year再来观察。
static Date d6(6);
Date d7(7);
void func()
{
Date d4(4);
static Date d5(5);
}
int main()
{
static Date d3(3);
Date d1(1);
Date d2(2);
func();
}
对于d6和d7调整顺序后:
Date d7(7);
static Date d6(6);
void func()
{
Date d4(4);
static Date d5(5);
}
int main()
{
static Date d3(3);
Date d1(1);
Date d2(2);
func();
}
结论:
对于对象的销毁顺序:
先销毁局部对象,再销毁局部的静态对象,最后销毁全局对象和全局的静态对象(全局对象按照先构造后析构的顺序)
3.拷贝构造函数
3.1定义理解
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
//日期类的拷贝构造
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
3.2语法特性
1.拷贝构造也是构造函数的一种重载形式,因此 大部分特性与基本构造函数类似
2.只有一个形参,是本类类型对象的引用。
3.如果没有显式写拷贝构造,编译器会生成一个默认拷贝构造函数(共有成员函数,否则无法进行默认的拷贝构造)。其中内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
4.系统生成的默认构造只会按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
3.3为什么需要传引用
使用传值方式编译器直接报错,因为会引发无穷递归调用。
如果是传值方式。在调用拷贝构造函数时,需要形成一个形参,而生成形参时也是拷贝,又会继续调用拷贝构造,此时的拷贝构造又需要传形参,继续无限调用。
3.4深拷贝和浅拷贝
浅拷贝是对于字节序进行拷贝,也就是只对于值的大小进行拷贝,那么对于成员变量需要动态内存开辟的对象,如果只是将地址按值拷贝过去,那么两个对象内的需要动态开辟的成员变量会指向同一块空间,在析构时造成同一块空间的多次释放引发报错。而深拷贝就是再对这个成员变量重新申请一块空间,而不是和拷贝对象共用一块空间。
场景:栈的拷贝构造
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
总结:
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
3.5典型调用场景
1.使用已存在对象创建新对象
2.函数参数类型为类类型对象
3.函数返回值类型为类类型对象
4.赋值运算符重载
4.1运算符重载
4.1.1语法
返回值类型 operator操作符(参数列表)
4.1.2注意事项
1.不能连接其他符号来创建新的操作符:比如operator@ 只能重载已经存在的运算符
2.重载后运算符的操作数数量不能改变
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
3.重载操作符必须有一个类类型参数
4.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
5. :: .* sizeof() . ?:这五个运算符不能重载
4.2赋值运算符重载
4.2.1语法
以Date类赋值运算符为例:
Date& operator=(const Date& t)
4.2.2注意事项
1.必须为类的成员函数
2.返回值为该类类型的引用,从而可以支持连续赋值-->返回*this
3.参数类型为const 类型&-->传引用为了提高效率,加上const防止传入的参数被修改
4.检查自己给自己赋值的情况-->即 this==&t 是否成立
4.2.3为什么不能重载为全局函数
赋值重载是类的默认成员函数,如果没有显式写,那么类会默认生成一个。如果我们将赋值运算符重载为全局函数,那么在调用时就有歧义,无法判断调用默认生成的还是全局的函数。
4.3流运算符重载
4.3.1注意事项
1.返回值为流对象,即istream或者ostream的引用,从而支持连续
2.必须重载为全局函数
4.3.2为什么要重载为全局函数
如果我们重载为类的成员函数,那么第一个参数就是默认的this指针。在调用的时候,会出现形式的不规范。
以日期类为例:
在类内重载<<操作符:
ostream& operator<<(ostream& out)
{
out << _year << "年" << _month << "月" << _day << "日" << endl;
return out;
}
在调用时:
Date d1(2022, 1, 1);
d1.operator<<(cout);
d1 << cout;
//cout << d1; //这种形式会报错
为了使形式统一 ,在重载流运算符时应将流对象作为第一个参数来实现调用,而类的成员函数默认第一个参数为隐含的this指针,因此我们只能重载为全局函数。但是在类外不能访问类的私有变量,因此我们需要将该函数在类内声明友元函数。
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
4.4赋值运算符与拷贝构造区别
Date d1(2022, 1, 1);
Date d2(2023, 2, 2);
Date d3(d2); //拷贝构造
d1 = d3; //赋值构造
Date d4 = d3; //拷贝构造 //这里虽然使用了 = 但是是构造没有存在的对象 所以为拷贝构造
注意:
拷贝构造是用一个已经存在的对象来构造一个没有存在的对象。
赋值重载是用一个已经存在的对象来构造另外一个已经存在的对象。
5.取地址以及const取地址重载
5.1const成员函数
5.1.1定义
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
5.1.2语法格式
以Date类Display打印函数为例:
void Display() const { }
5.1.3调用规则
请思考以下问题:
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其它的非const成员函数吗?
- 非const成员函数内可以调用其它的const成员函数吗?
解决以上问题的关键是权限是否放大。权限可以缩小,平移,但是不可以放大。
1.const对象是只读权限,非const成员函数是可读可写权限,权限放大,不可以调用。
2.非const对象是可读可写权限,const成员函数内是只读权限,权限缩小,可以调用。
3.const成员函数是只读权限,非const成员函数是可读可写权限,权限放大,不可以调用。
4.非const成员函数是可读可写权限,const成员函数是只读权限,权限缩小,可以调用。
5.2语法形式
Date* operator&()
{
return this;
}
//const对象取地址重载
const Date* operator&() const
{
return this;
}
5.3注意事项
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容。
以上是本次所有内容,谢谢观看。