1.构造函数,代替Init()函数
构造函数的特点
1.函数名与类名相同
2.无返回值,void也不需要写
3.对象实例化时,系统会自动调用构造函数
4.构造函数可以重载
class Date
{
public:
//函数名与类名相同,无返回值
Date()//函数重载,无参
{
_year = 1;
//..
}
Date(int year)//函数重载,带参
{
_year = year;
//...
}
Date(int year = 2024,...)//函数重载,缺省,各种缺省的重载都可以
{
}
//全缺省和无参不可以同时存在,因为虽然构成了重载,但调用函数会出现歧义
void Dateprint()
{
//...
}
private:
int _year;
//...
};
int main()
{
Date A;//调用无参的构造函数,A后面不跟括号!!!
Date B(2023);//调用带参的
//如果写成Date A();//与函数声明混淆,规定不可以这么写
//这里这么实例化之后系统就会自动调用构造函数
return 0;
}
5.如果类中没有显式地定义构造函数,C++编译器会自动生成一个无参地默认构造函数,如果显式定义则编译器不再生成
6.默认构造函数包括:显式定义的无参构造函数,全缺省的构造函数,编译器自动生成的构造函数,总结就是不需要传实参调用的构造函数就是默认构造函数
我们不写,编译器生成的默认构造函数对于内置类型成员变量是否初始化不确定,看编译器,VS没有初始化
类中的自定义类型的成员变量会调用这个自定义类型的默认构造函数初始化,比如两个栈实现自己的队列这种题,如果这个成员变量没有默认构造,那么就会报错,需要使用初始化列表解决
#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:
//编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:
Stack pushst;//自定义类型
Stack popst;
};
int main()
{
MyQueue mq;
return 0;
}
总结:大多数情况下,构造函数都需要我们自己写,少数情况比如MyQueue 通过 Stack的默认构造函数自动实现
2.析构函数,代替Destroy()函数完成 对象中资源清理工作
析构函数特点:
1.类名前加"~"
2.无参数无返回值,同样不需要加void
3.一个类只能有一个析构函数,若未显式定义,编译器会自动生成一个默认的析构函数
4.对象生命周期结束时,系统会自动调用析构函数
#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:
//编译器默认⽣成MyQueue的析构函数调⽤了Stack的析构,释放的Stack内部的资源
// 显⽰写析构,也会⾃动调⽤Stack的析构
/*~MyQueue()
{}*/
private:
Stack pushst;
Stack popst;
};
int main()
{
Stack st;
MyQueue mq;
return 0;
}
Date这种没有资源申请的类不需要写析构函数,因为就没有资源需要清理
析构函数的调用顺序:
规定:如果有多个对象,后定义的先析构
5.如果不显式地写析构函数,编译器会自动生成默认的析构函数,默认的析构函数对内置类型成员不做处理,对自定义类型成员会调用它的析构函数
6.如果显式地写了析构函数,对于自定义类型成员也会自动调用它的析构函数,也就是不论什么情况,自定义类型的成员一定会调用自己的析构函数
7.如果没有资源申请,可以自己不用写析构函数如Date;如果有资源申请,那就一定要自己写析构函数,否则会内存泄漏
3.拷贝构造函数
拷贝构造函数是特殊的构造函数,也就是构造函数的一个重载.作用是通过初始化定义的方式拷贝一个已经实例化的对象
特点:
1.如果构造函数的第一个参数是自身类型的引用,且任何额外的参数都有默认值,则这个构造函数就是拷贝构造函数
2.C++规定自定义类型的(类)函数传值传参(拷贝行为)要调用拷贝构造,因此建议自定义类型的函数建议就用传引用传参,因为传值也需要调用一次拷贝构造,不如就传引用,高效
拷贝构造函数的第一个参数必须是自身类类型的引用,如果是传值就会引发无限递归报错.
如果拷贝构造函数的第一个参数是传值就会发生下图这样的无限递归
3.未显式定义的拷贝构造,编译器会自动生一个拷贝构造函数,这个自动生成的拷贝构造函数对内置类型成员变量会进行简单的值拷贝/浅拷贝(一个字节一个字节的拷贝)
#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& Func2()
{
Date tmp(2024, 7, 5);
tmp.Print();
return tmp;
}
int main()
{
Date d1(2024, 7, 5);
// C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥传值传参要调⽤拷⻉
构造
// 所以这⾥的d1传值传参给d要调⽤拷⻉构造完成拷⻉,传引⽤传参可以较少这⾥的拷⻉
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;
}
4.像Date这样的类,全是内置类型且没有指向任何资源,编译器自动生成的拷贝构造就可以满足我们的需求,他会把变量值拷贝,这就达到了目的.而像栈stack这种虽然也全是内置类型,但是_a指向了资源,仅仅是编译器默认生成的拷贝构造的浅拷贝就不满足我们的要求了,这需要自己实现一个深拷贝
简单的说就是,浅拷贝非常的死板,完完全全就是一个字节一个字节地拷贝,如果像stack这种指向了了资源的类,拷贝的时候也会死板地把被拷贝对象的所有涉及地址拷贝地一模一样,那么这个拷贝出来的对象和原来的对象就会有"交汇"的东西,总不能修改这个对象,另一个对象也改,同时也会带来一个问题,最后析构的时候,释放的空间也是同一块,这回导致程序崩溃!
两个stack实现Myqueue:
对于类里面自定义类型的成员变量(类里面还有个类对象),如果该类没有自己实现拷贝构造,Myqueue自动调用的拷贝构造会自动调用stack实现的拷贝构造,那么Myqueue不用自己写拷贝构造也可以实现深拷贝了,因为stack都写好了.
5.自定义类型的传值返回会产生一个临时对象带调用拷贝构造,
传引用返回会返回对象的别名(没有产生拷贝)
但是返回对象是一个函数局部作用域的局部对象,函数结束就销毁了,那么返回它的引用是有问题的
传引用返回可以减少拷贝,但是要确保一定要返回对象,在当前函数结束后,对象还在.
4.赋值运算符重载
运算符重载:
1.C++规定类类型的对象使用运算符时必须转换成调用对应运算符重载,原有的运算符对于自定义的类没有意义,会报错.
2.运算符重载是具有特殊名字的函数 比如operater+ operater-…
3.参数个数和该运算符的操作数个数一样多,一元运算符有一个参数,二元有两个,而且顺序从左至右
4.如果重载成成员函数(定义在类内部),则默认的第一个参数就是this指针,参数会看起来少一个
5.重载以后的运算符作用优先级结合性不变
6. .(指针进行成员函数回调时会使用) ::(域作用) sizeof(计算数据类型大小) ? :(三目操作符) . 这五个运算符不能重载
对于 . C++规定 普通函数的函数名就是函数指针,但是需要使用&才能取到函数指针
7.重载操作符必须要有一个类类型的参数
比如 int operater+(int x,int y) 不能运算符重载实现内置类型对象操作,这样会让+ 变成- ,变成其他含义
赋值运算符重载
1.赋值运算符重载是一个默认的成员函数,用于完成两个已经存在的对象的拷贝赋值,区别拷贝构造的是一个对象拷贝初始化给另一个要创建的对象
2.是运算符重载,规定必须为成员函数,参数建议写成const当前类类型引用,否则会传值调用调用拷贝构造
3.没有显式实现时,编译器会默认生成一个赋值运算符重载,默认赋值运算符重载的行为和默认拷贝构造类似,对内置类型浅拷贝/值拷贝,对自定义类型会调用它的赋值重载.
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
cout << " Date(const Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
// 传引⽤返回减少拷⻉
// d1 = d2;
Date& operator=(const Date& d)
{
// 不要检查⾃⼰给⾃⼰赋值的情况
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// d1 = d2表达式的返回对象应该为d1,也就是*this
return *this;//实现d1 = d2 = d3
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 5);
Date d2(d1);
Date d3(2024, 7, 6);
d1 = d3;
// 需要注意这⾥是拷⻉构造,不是赋值重载
// 请牢牢记住赋值重载完成两个已经存在的对象直接的拷⻉赋值
// ⽽拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象
Date d4 = d1;
return 0;
}
4.同拷贝构造,Date不需要自己写赋值重载,Stack有指向资源,需要自己写.
Myqueue这种里面是自定义类型的,不需要自己写复制重载,对于自定义类型会自动调用自定义类型(Stack)的赋值重载