目录
一、类的默认成员函数
默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。
分别为构造函数、析构函数、拷贝构造、赋值重载和取地址重载
也就是说,当我们创建了一个类
class Date
{
};
实际上编译器已经生成了六个默认成员函数
class Date
{
public:
Date();//构造函数
~Date();//析构函数
Date(const Date& d);//拷贝构造函数
Date& operator=(const Date& d);//赋值运算符重载
Date* operator&();//取地址运算符重载(&)
const Date* operator&() const;//const修饰的取地址运算符重载(const &)
};
二、构造函数
概念
构造函数
对象实例化时初始化对象(不是开空间创建对象,我们常使⽤的局部对象是栈帧创建时,空间就开好了)
默认构造函数
无参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函 数。但是这三个函数有且只有⼀个存在,不能同时存在
总结⼀下就是不传实参就可以调用的构造就叫默认构造
注意:无参构造函数和全缺省构造函数同时存在会造成歧义
特点
1、函数名和类名相同
2、无返回值
3、对象实例化时系统自动调用
4、构造函数可重载
5、如果没有显式定义,系统会生成一个无参的构造函数(显示定义了就不会生成)
实例
1、对于内置类型
我们不写编译器自动生成
这里发现会给随机值,因为C++编译器在系统默认构造函数中初始化类型的时候,对于内置类型不做处理,对于自定义类型会调用它的默认构造
需要注意的是,当我们写两个构造函数的时候会报错
2、对于自定义类型
自定义类型需要调用这个成员变量的默认构造函数初始化
假设我们要用两个栈实现队列
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
// ...
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
private:
Stack pushst;
Stack popst;
};
int main()
{
MyQueue mq;
return 0;
}
对于Stack,如果没有显式写构造函数,编译器就会报错
对于MyQueue,因为编译器默认⽣成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始化(如果这个时候注释Stack的构造,MyQueue也初始化不了)
三、析构函数
概念
与构造函数相反,析构函数的功能是完成对对象中的资源的清理工作(不是完成对对象本⾝的销毁,⽐如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了)
特点
1、析构函数函数名是类名前加~
2、无参无返回值⼀个类只能有⼀个析构函数
3、对象⽣命周期结束时,系统会⾃动调⽤析构函数
4、对于内置类型和自定义类型,与构造函数类似
5、我们显示写析构函数,对于自定义类型成员也会调⽤他的析构(自定义类型成员无论什么情况都会自动调用析构函数)
6、有资源申请时,⼀定要 ⾃⼰写析构,否则会造成资源泄漏
7、C++规定后定义的先析构
实例
1、对于内置类型
如果没有资源的开辟,使用编译器生成的析构即可
2、对于自定义类型
还是以两个栈实现队列为例
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
// ...
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
private:
Stack pushst;
Stack popst;
};
int main()
{
Stack st;
MyQueue mq;
return 0;
}
这里程序结束时,Stack会调用它的析构,编译器默认生成 MyQueue 的析构函数调用了 Stack 的析构,释放的 Stack 内部的资源
四、拷贝构造
概念
如果⼀个构造函数的第⼀个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数 也叫做拷贝构造函数(拷贝构造是一个特殊的构造函数)
特点
1、第⼀个参数必须是类类型对象的引用
2、C++规定自定义类型对象进行拷贝行为必须调⽤拷贝构造(传值传参和传值返回都会调⽤拷贝构造)
3、若未显式定义,编译器会自动生成,自动生成的是浅拷贝/值拷贝(一个字节一个字节拷贝),对于自定义类型会调用它的拷贝构造
4、传值返回会产生⼀个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但如果返回一个当前函数局部域的局部对象,函数结束后销毁,使用引用返回就有问题(类似野指针)
实例
1、对于内置类型
需要注意拷贝构造和普通构造的区别,也要注意是否返回局部对象
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 编译报错:error C2652 : "Date":⾮法的复制构造函:第⼀个参数不应是"Date"
//Date(Date d)
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date(Date* d)
{
_year = d->_year;
_month = d->_month;
_day = d->_day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Func1(Date d)
{
cout << &d << endl;
d.Print();
}
Date& Func2()
{
Date tmp(2024, 7, 5);
tmp.Print();
return tmp;
}
int main()
{
Date d1(2024, 7, 5);
// 传值传参调用拷贝构造
Func1(d1);
cout << &d1 << endl;
// 这⾥可以完成拷⻉,但是不是拷⻉构造,只是⼀个普通的构造
Date d2(&d1);
d1.Print();
d2.Print();
//这样写才是拷⻉构造,通过同类型的对象初始化构造,⽽不是指针
Date d3(d1);
d2.Print();
// 也可以这样写,这⾥也是拷⻉构造
Date d4 = d1;
d2.Print();
// Func2返回了⼀个局部对象tmp的引⽤作为返回值
// Func2函数结束,tmp对象就销毁了,相当于了⼀个野引⽤
Date ret = Func2();
ret.Print();
return 0;
}
2、对于内置类型
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
Stack(const Stack& st)
{
// 需要对_a指向资源创建同样⼤的资源再拷⻉值
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
if (nullptr == _a)
{
perror("malloc申请空间失败!!!");
return;
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_top = st._top;
_capacity = st._capacity;
}
void Push(STDataType x)
{
if (_top == _capacity)
{
int newcapacity = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
// ...
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认⽣成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始化
// 显⽰写析构,也会⾃动调⽤Stack的析构
/*~MyQueue()
{}*/
private:
Stack pushst;
Stack popst;
};
int main()
{
//Stack st;
//MyQueue mq;
Stack st1;
st1.Push(1);
st1.Push(2);
Stack st2 = st1;
MyQueue mq1;
MyQueue mq2 = mq1;
return 0;
}
Stack不显示实现拷贝构造,会用自动生成的拷贝构造完成浅拷贝
导致st1和st2中的_a指向同一块资源,析构时西沟两次,程序崩溃
而MyQueue自动生成的拷贝构造,会调用Stack的拷贝构造
只要Stack拷贝构造自己实现了深拷贝就没有问题
五、赋值重载
运算符重载
概念
当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规 定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编 译报错
形式:operator符号名
特点
1、重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多,⼀元运算符有⼀个参数,⼆元 运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数(成员函数默认第一个为this,这点需要注意一下)
2、运算符重载后,优先级和结合性与之前一致
3、.* :: sizeof ?: . 这五个运算符不能重载
4、前置++和后置++区分(--也是),C++规定后置++重载时增加一个int形参
5、重载<<和>>,由于第一个是this指针,调用时就变成了对象<<cout,这时候重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象
//ostream& out = cout,out作为返回值,成为下一次左侧参数
ostream& operator<<(ostream& out, const Date& d);
//流提取
//提取后的值放d里面,所以不加const
istream& operator>>(istream& in, Date& d);
实例
以日期类Date为例,下面就是一些运算符重载的例子
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day);
// 日期-天数
Date operator-(int day);
// 日期-=天数
Date& operator-=(int day);
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
// >运算符重载
bool operator>(const Date& d);
// ==运算符重载
bool operator==(const Date& d);
// >=运算符重载
bool operator >= (const Date& d);
// <运算符重载
bool operator < (const Date& d);
// <=运算符重载
bool operator <= (const Date& d);
// !=运算符重载
bool operator != (const Date& d);
// 日期-日期 返回天数
int operator-(const Date& d);
赋值运算符重载
概念
用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于⼀个对象拷贝初始化给另⼀个要创建的对象
Date d1;
//拷贝构造
Date d2 = d1;
Date d3;
//赋值重载
d3 = d1;
特点
1、没有显示实现时,编译器默认生成的默认赋值运算符重载完成浅拷贝,对于自定义类型成员会调用它的赋值重载函数
2、有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋值场景
关于是否需要显示实现
总的来说,当我们有资源开辟/释放,我们就需要显示实现构造函数、析构函数和拷贝构造函数。如果⼀个类显⽰实现 了析构并释放资源,那么他就需要显示写赋值运算符重载
六、取地址运算符重载和const修饰的取地址运算符重载
这两个一般不需要重载,特殊情况,不想让别人获取到地址,将返回值改成别的
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}