👌个人主页: 起名字真南
👆个人专栏:【数据结构初阶】 【C语言】 【C++】
目录
- 1 类的默认成员函数
- 2 构造函数
- 3 析构函数
- 4 拷贝构造
- 5 赋值运算符重载
- 5.1 运算符重载
- 5.2 赋值运算符的重载
1 类的默认成员函数
默认成员函数就是用户没有显示实现,编译器自动生成的的成员函数称为默认成员函数
一个类我们不写的情况下会默认生成六个成员函数,分别是构造函数,析构函数,拷贝构造,赋值重载以及关于取地址对普通对象和const对象的重载。
2 构造函数
构造函数是特殊的成员函数,构造函数的主要任务是在对象实例化时对其进行初始化对象。他的本质就是Stack和Date类中Init函数的功能,而且构造函数可以自动调用的特点就完美的替代了Init函数。
构造函数的特点 :
- 函数名和类型名一致
- 无返回值(不需要写void )
- 对象实例化时系统会自动调用对应的构造函数。
- 构造函数可以重载
- 如果类中没有定义构造函数那么编译器会自动生成一个无参的构造函数,如果显示定义了就不会自己生成
- 无参构造函数,全缺省构造函数,以及我们不写编译器自动生成的构造函数都叫做默认构造函数,并且这三个函数不能同时存在,同时存在虽然无参和全缺省构成了函数的重载但是调用函数时会存在歧义。总结不传实参就可以调用的构造函数统称为默认构造。
- 我们不写编译器默认生成的构造函数对内置类型没有影响,具体怎么初始化取决于编译器怎么初始化,但是对于我们用class/struct来创建的自定义类型成员变量就需要调用他的构造函数进行初始化,如果没有构造函数就会报错,我们要初始化成员变量就需要用到初始化列表(后面会解释)
#include<iostream>
using namespace std;
class Date
{
public:
//无参构造函数
/*Date()
{
_year = 1;
_month = 1;
_day = 1;
}*/
//带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//全缺省构造函数
/*Date(int year = 1, 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(2024, 10, 6); //调用带参的构造函数
d2.Print();
return 0;
}
如果我们将第一个无参构造函数和第三个全缺省构造函数注释掉编译器就会报错显示没有可用的构造函数。
当我们使用无参的构造函数进行初始化时后面不需要加()否则编译器无法区分这里是函数声明还是实例化对象。
typedef int STLDateType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STLDateType*)malloc(sizeof(STLDateType) * n);
if (_a == nullptr)
{
perror("malloc fail!");
return;
}
_top = 0;
_capacity = n;
}
private:
STLDateType* _a;
size_t _top;
size_t _capacity;
};
//两个Stack实现队列
class MyQueue
{
public:
//编译器默认生成MyQueue的构造函数调用了Stack的构造函数,完成了两个成员的初始化
private:
Stack _STPush;
Stack _STPop;
};
int main()
{
MyQueue mq;
return 0;
}
当一个类类型的成员变量是自定义类型的时候例如上面的代码MyQueue类的成员变量是自定义类型Stack类,所以编译器不会生成MyQueue的默认构造函数需要我们自己来实现,但是为什么我们明明没有写但是还是可以运行,因为编译器默认生成MyQueue的构造函数调用了Stack的构造函数,完成了两个成员的初始化。如果我们将Stack的构造函数注释掉就会报错。出现下面这种情况
3 析构函数
析构函数与构造函数的功能相反,析构函数不是完成对对象本身的销毁比如局部对象创立是在栈帧,如果函数结束那么栈帧就会被销毁,不需要我们去管。C++规定对象在销毁时会自动调用析构函数完成对对象中资源的清理和释放工作。析构函数的功能就类似与我们Stack类中Destory函数的功能,而向Date类没有Destory就不需要析构函数。
析构函数的特点 :
- 析构函数的函数名是在类名的前面加上‘~’。
- 无参数无返回值和构造函数类似
- 一个类只能有一个析构函数,如果未显示定义编译器就会自动生成一个默认析构函数
- 函数声明周期结束会自动调用析构函数
- 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型的成员变量不做处理,自定义类型成员会调用他自己的析构函数
- 需要注意的是如果我们写了析构函数但是对于自定义类型还是会调用他自己的析构函数,也就是说自定义类型成员变量无论什么时候都会调用自己的析构函数
- 一个类中如果没有资源申请那么使用编译器自己生成的析构函数就可以比如Date类,如果有资源申请像Stack/MyQueue类都需要自己写析构函数,否则会造成资源泄露。
- 一个局部域有多个对象,C++规定后定义的先析构。
#include<iostream>
using namespace std;
typedef int STLDateType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STLDateType*)malloc(sizeof(STLDateType) * n);
if (_a == nullptr)
{
perror("malloc fail!");
return;
}
_top = 0;
_capacity = n;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STLDateType* _a;
size_t _top;
size_t _capacity;
};
//两个Stack实现队列
class MyQueue
{
public:
//编译器默认生成MyQueue的构造函数调用了Stack的构造函数,完成了两个成员的初始化
private:
Stack _STPush;
Stack _STPop;
};
int main()
{
Stack st;
MyQueue mq;
return 0;
}
从运行结果我们可以看到一共调用了三次析构函数,分别是st一次和mq内的两次
4 拷贝构造
如果说构造函数的第一个参数是自身类类型的引用,并且额外的参数都有默认值,那么这个构造函数可以叫做拷贝构造函数,拷贝构造也是一种特殊的构造函数。
拷贝构造的特点 :
- 拷贝构造函数是构造函数的重载
- 拷贝构造的函数参数有且必须只有一个就是自身类类型的引用,使用传值的方式编译器会报错,因为会引发无穷递归。
- C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造来完成
- 若未显示定义拷贝构造,编译器会自动生成拷贝构造函数,自动生成的内置类型成员变量会进行值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员会调用他自己的拷贝构造
- 像Date类型的成员变量都是内置类型可以不写拷贝构造直接使用编译器自己生成的拷贝构造函数,但是如果是Stack类型虽然内部的成员变量都是内置类型但是其中的_a涉及到了资源的指向一起开辟,如果使用浅拷贝会发生冲突,所以需要我们自己实现并且将指向的资源同样进行拷贝。像MyQueue类型的虽然他的成员变量都是自定义类型但是在调用拷贝构造的时候也会调用Stack的拷贝构造所以也不用自己去实现。
- 传值返回会产生一个临时对象调用拷贝构造,传引用返回,返回的是对象的别名(引用),没有产生拷贝。但是如果返回对象是当前局部域的临时对象当前函数结束时该局部对象也会销毁就会出现野引用的问题,类似于野指针,传引用返回可以减少拷贝,但是一定要确定返回的对象不会因为当前函数结束而消失才可以使用。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//编译报错 Date类的复制构造函数不能有Date类型的参数,但是可以有 Date& 以及 Date* 类型
//Date(Date d)
Date(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, 10, 01);
tmp.print();
return tmp;
}
int main()
{
//C++规定传值传参必须调用拷贝构造,所以这里调用了拷贝构造
Date d1(2024,10,07);
//传值传参给d会调用拷贝构造,使用传引用传参可以减少拷贝
func1(d1);
cout << &d1 << endl;
//传引用传参,这里不是拷贝构造只是单纯的拷贝
Date d2(&d1);
d1.print();
d2.print();
//这里也是拷贝构造
Date d3 = d1;
d3.print();
//拷贝构造用同类型的对象初始化而不是指针
Date d4(d1);
d4.print();
//传引用返回的tmp在函数结束的时候就已经被销毁了,所以返回的是野引用
Date ret = func2();
ret.print();
return 0;
}
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (_a == nullptr)
{
perror("malloc fail");
}
_capacity = n;
_top = 0;
}
Stack(const Stack& st)
{
//需要对_a指向的资源同样进行拷贝
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_capacity = st._capacity;
_top = st._top;
}
void Push(STDataType data)
{
if (_top == _capacity)
{
int newcapacity = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, sizeof(STDataType) * newcapacity);
if (tmp == nullptr)
{
perror("realloc fail");
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = data;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
//用两个栈实现队列
class MyQueue
{
private:
Stack push;
Stack pop;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
MyQueue m1;
MyQueue m2 = m1;
return 0;
}
5 赋值运算符重载
5.1 运算符重载
- 当运算符被用于类类型对象时,C++允许我们通过运算符重载的方式指定新的含义
- 重载运算符的参数个数和运算符本来的操作对象数量一样多一元操作符有一个参数,二元运算符有两个参数。
- 如果一个重载运算符函数是成员函数那么他的第一个参数是隐式指针this,因此运算符重载作为成员函数时会少一个参数。
- . :: ?: . sizeof* 这五个运算符不能重载
- 运算符重载是具有图书名字的函数,他的名字是由operator加上后面定义的运算符共同构成。和其他函数一样他也有返回类型 函数体和参数列表
- 重载++运算符要注意后置++多一个int类型的形参用来区分两个运算符
- 重载‘<<’ '>>'两个运算符的时候需要重载为全局函数,如果是成员函数他的第一个默认类型是隐式类型this作为他的左操作数调用时就变成了 ‘ 输出的对象<<cout ’ 不符合使用习惯和可读性,重载为全局函数只需要第一个参数类型时ostream/istream就可以了,第二个参数位置为该类类型的对象。
- 运算符重载后优先级和结合性要保持一致
- 重载操作符必须有一个类类型的参数,不能改变其原有的含义。
class A
{
public:
void func()
{
cout << "A::func()" << endl;
}
};
typedef void(A::* PF)(); //成员函数指针类型
int main()
{
//C++规定成员函数要加&才可以取到函数指针
PF pf = &A::func;
A obj;
(obj.*pf)();//对象调用成员函数指针使用.*运算符
return 0;
}
//重载为全局函数会面临的问题
// 无法访问私有变量
// 解决办法
// 1 用public
// 2 提供get函数
// 3 友元函数
// 4 重载为成员函数
//public成员变量
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1(2024, 10, 06);
Date d2(2024, 10, 07);
//显示调用
operator==(d1, d2);
//编译器会转换成operator==(d1, d2)
d1 == d2;
return 0;
}
5.2 赋值运算符的重载
赋值运算符重载是一个默认成员函数,用于完成两个已经存在的的对象直接拷贝赋值,但这里和拷贝构造的区分,拷贝构造用于一个对象拷贝初始化给另一个创建的对象。
赋值运算符重载的特点 :
- 赋值运算符重载是一个运算符重载,必须重载为成员函数,他的参数必须是当前的类类型建议加上const,否则传值传参会有拷贝。
- 有返回值,建议写成当前类类型的引用,引用返回可以提高效率,有返回值的母体是为了可以连续赋值
- 没有显式实现时,编译器会⾃动⽣成⼀个默认赋值运算符重载,默认赋值运算符重载⾏为跟默认构造函数类似,对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造
#include<iostream>
using namespace std;
typedef int STDataType;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//编译报错 Date类的复制构造函数不能有Date类型的参数,但是可以有 Date& 以及 Date* 类型
//Date(Date d)
Date(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;
}
Date& operator=(const Date& d1)
{
if (this != &d1)
{
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
return *this;
}
//private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 10, 06);
Date d2(2024, 10, 07);
//显示调用
operator==(d1, d2);
//编译器会转换成operator==(d1, d2)
d1 == d2;
d2.print();
d2 = d1;
d2.print();
//注意这里不是赋值重载,而是拷贝构造,赋值重载只用于两个已经存在的对象,
// 而拷贝构造用于一个对象初始化另一个对象
Date d3 = d1;
return 0;
}