类的默认成员函数:
在C++中,如果你没有显式地定义某些特定的成员函数(如构造函数、析构函数、拷贝构造函数、拷贝赋值运算符和移动构造函数),那么编译器会自动生成这些函数。这些由编译器自动生成的函数被称为默认成员函数。
构造函数
构造函数的概念:
构造函数(Constructor)是一种 特殊的成员函数 ,用于在创建对象时初始化对象的状态。它的名称与类名相同,没有返回类型(包括void),可以带有参数。构造函数在对象被创建时自动调用,负责确保对象在被使用之前具有合理的初始状态。构造函数的特点
-
初始化对象:构造函数主要作用是初始化新创建的对象。它负责为对象的数据成员赋予初始值,确保对象在被使用之前是可靠的。
-
与类名相同:构造函数的名称必须与类名完全相同,包括大小写。
-
没有返回类型:构造函数没有返回类型,包括void。这是与普通函数(成员函数)的重要区别。
typedef int STDataType;
class Stack
{
public:
// 成员函数
//构造函数
Stack(int capaticy=4)
{
std::cout << "Sack" << std::endl;
_a = (STDataType*)malloc(sizeof(STDataType) * capaticy);
if (!_a)
{
perror("malloc申请空间失败");
return;
}
_capacity = capaticy;
_top = 0;
}
void Init(int capaticy=4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * capaticy);
if (!_a)
{
perror("malloc申请空间失败");
return;
}
_capacity = capaticy;
_top = 0;
}
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;
}
void Pop()
{
assert(_top > 0);
--_top;
}
bool Empty()
{
return _top == 0;
}
int Top()
{
assert(_top > 0);
return _a[_top - 1];
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
从上图代码可以看到,创建了一个栈的构造函数,可以看到此构造函数与类名完全相同并且没有返回值。
在主函数里,仅仅只是创建了Stack类的s1对象,而在调用的时候可以看到,编译器在执行的时候会自动调用构造函数,并完成s1的初始化操作。这种操作与Init类似,但比Init方便。避免有时候忘记初始化,自动构造函数就能帮我们解决不需要我们手动操作。
4.构造函数可以重载
可以看到上图创建了两个构造函数,但参数并不相同。此时编译器运行并没报错也说明了构造函数支持重载。
无参构造:
通过上图代码可以看到小编创建了一个日期的类,在构造函数里并没用传参数,而直接在构造函数里直接与成员变量进行赋值操作。可以看到此时再输出d1也发现完成了初始化成员变量的操作。
全缺省构造:
全缺省构造函数是指所有参数都有默认值的构造函数。如下图构造函数里的参数进行了全缺省操作,这样能更方便的进行初始化
自动生成的默认构造:
编译器默认⽣成的构造,C++规定对内置类型成员变量的初始化没有要求。如果没有用户定义的构造函数,编译器会生成一个默认构造函数,这个构造函数不会对内置类型进行初始化 如上图,此时将自己编写的构造函数进行了注释,那么根据C++的规定如果没有构造函数,编译器会调用自身的构造函数,但对于内置类型(如int char short)并没有硬性要求初始化。所有如上图编译器并没用对d1对象里的成员进行初始化操作,输出的是随机值。 对于⾃定义类型(如class struct)成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,使用编译器自动生成的在使用时候则会报错。如上图,创建了一个Queue的类,类里有两个Stack类的成员变量push与pop。在Queue类里并没有写构造函数而是编译器自动调用构造函数。通过调试过程中看到,对于q对象里的Stack成员会去调用Stack自身的默认构造函数,从而完成初始化操作。
如果此时将Stack里的默认构造进行注释,调用编译器自身的默认构造函数
可以看到如果使用编译器自身的默认构造,会直接将push对象的a指针,与pop对象的a指针直接初始化为nullptr,如果此时再对栈进行插入与删除则会直接报错。
对于无参,全缺省,自动生成的默认构造的使用注意事项:
⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函 数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义。
析构函数
析构函数的概念:
析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁,⽐如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会 ⾃动调⽤ 析构函数,完成对象中资源的清理释放⼯作。功能类似于Stack栈里的Destroy函数的功能 。析构函数的特点:
析构函数名是在类名前加上字符 ~。 ⽆参数⽆返回值。 (这⾥跟构造类似,也不需要加void) ⼀个类只能有⼀个析构函数(也就是析构函数不支持函数重载)。若未显式定义,系统会⾃动⽣成默认的析构函数。 //析构函数
~Stack()
{
std::cout << "~Sack" << std::endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
从上图可以看到创建d1对象后,并没有对d1进行初始化以及销毁。当代码运行起来后,编译器自动调用了默认构造函数以及析构函数。
析构函数很好的解决了程序有有时候会忘记进行内存释放的问题,从一定程度上减小了内存泄漏的问题。
一般情况下,有动态申请资源,就需要显示写析构函数释放资源。没用动态申请的资源,不需要写析构函数。需要释放的资源成员都是自定义类型,不需要写析构,前提是自定义类型里写了析构函数,编译器会自动调用自定义类型成员的析构函数,这点与上面的构造函数类似。
拷贝构造函数
构造函数的概念:
如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值,则此构造函数也叫做拷⻉构造函数,也就是说拷⻉构造是⼀个特殊的构造函数。构造函数的特点:
1.拷⻉构造函数是构造函数的⼀个重载。 2.拷⻉构造函数的第⼀个参数必须是 类类型对象的引用 ,使⽤传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调⽤。 拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值。class Data
{
public:
//默认构造函数
Data(int year=1,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Data(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
//内置类型
//C++11支持,这里不是初始化,而是给缺省值,用于给编译器默认的构造函数用
int _year;
int _month;
int _day;
};
拷贝构造的第一个参数为什么必须是引用呢?首先要知道函数的传参本质上是对形参对实参的拷贝。C++规定对于自定义类型的对象进行拷贝行为必须调用拷贝构造。
从上图代码的调试过程我们可以看到,在调用fun函数时,并不会直接进入fun函数,而是会先跳到拷贝构造函数,将实参d1的值赋给形参d。当赋值完才会进入fun函数。
那么为什么拷贝构造传的是别名而不是形参呢?
从上图可以看到,如果使用传值,那么就会先进行拷贝构造。而d1是实参,d是形参,又会对自身进行拷贝构造,就形成了无穷的递归。所以为了防止这种事情发生,编译器会强制检查拷贝构造是否用的是引用。
3.若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉)。 可以看到,当把自己写的拷贝构造函数注释后编译器会调用自身的拷贝构。 对⾃定义类型成员变量会调⽤他自身的拷贝构造。 可以看到上图的Queue类里并没有拷贝构造函数,而也能成功的将q1拷贝给q2,这是因为Queue类里的成员函数属于Stack类,编译器会自动调用Stack类的拷贝构造函数。 那么如果将Stack类里的拷贝构造函数进行注释使用编译器自身的拷贝构造会发生什么呢? 从上图可以明显的看出,如果使用编译器自带的拷贝构造函数时,会将s1的成员变量a与s2的成员变量a的指针指向同一块空间。当s1与s2的声明周期结束时,会先析构s2,此时s2的成员a指向的那块空间已经被释放了,但s1的a就会变成野指针。而当析构s1时候就会对已经释放的空间再次进行释放,程序就崩溃结束。 所以类似栈这种数据结构拷贝构造需要自己实现(深拷贝)。运算符重载
C++ 中的运算符重载允许你重新定义内置运算符(+,-,*,/,>,<等)对自定义类型的对象之间的操作符行为。使得自定义类型可以像内置类型一样使用运算符进行操作。
运算符重载的概念:
运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。运算符重载的特点:
1.重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。 2.如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。 3.运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。
class Data
{
private:
int _year;
int _month;
int _day;
public:
// 构造函数
Data(int year = 2024, int month = 8, int day = 8);
//拷贝构造
Data(const Data& d);
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
bool operator<(const Data& d)
{
if (_year < d._year)
return true;
else if (_year == d._year && _month < d._month)
return true;
else if (_year == d._year && _month == d._month && _day < d._day)
return true;
return false;
}
bool operator==(const Data& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
通过上图代码可以看到,创建了一个Data的类,并创建了 Data类的d1与d2对象同时进行了初始化构造。使用 operator<对两个自定义类型的对象进行比较大小,并且返回1(真)。
那么 d1.operator<(d2)是否会感觉到很别扭,如果能将 d1.operator<(d2)变成d1<d2将更加方便理解。 从上图代码调试可以看到d1 < d2与d1.operator<(d2),在汇编指令上都调用了operator<函数,也就是说如果写成d1<d2编译器也会自动调用。所以这两种写法是等价的,写成d1<d2会更方便进行理解。 4.不能通过连接语法中没有的符号来创建新的操作符:比如operator@ C++规定只能使用自身携带的运算符进行重载,不允许自身去创建新的运算符。 5.⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,⽐如Date类重载operator-就有意义,但是重载operator+就没有意义。 因为日期相减可以计算天数差,日期相加并没有意义。 6. 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分。 可以看到前置operator++()函数的返回值为Data&,而后置operator++(int)返回值是Data呢? 比如 int i=10;int j=++i。可以知道j的值是i+1后的值,所以并不需要额外的开空间,直接使用i+1后的值。 而j=i++,是先将i的值赋给j,再让i+1。这里就需要额外创建一个对象,先保存d1的值,再让d1+1,并且因为这个额外的对象是一个局部对象,如果传引用返回就会造成野指针现象,必须穿值返回。赋值运算符重载
赋值运算符重载的概念:
赋值运算符重载是⼀个 默认成员函数 ,⽤于完成两个已经存在的对象直接的拷⻉赋值。拷贝构造:用一个已经存在的对象初始化另一个对象,这里要进行区分。赋值运算符重载的特点:
1.赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const当前类类型引⽤,否则会传值传参会有拷贝,大大降低效率。 使用cosnt是因为如果实参是const修饰的类,那么如果形参别名不是const修饰的,那么此时编译会报错,因为实参是const类型,形参引用却不是,权限就被放大了。 2.有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋值场景。类似i=j=k=0; 那么看到这里可能有小伙伴有疑惑,拷贝构造与赋值拷贝的区别到底在哪里呢? 上图先创建了d1与d2对象,并将他们进行默认构造。当创建d3进行初始化的时候将d3=d1.此时通过调试可以看见,这里函数调用的是拷贝构造的代码。而在下一行d2=d1时的函数调用是用赋值拷贝的代码。 所以可以得知虽然d3与d2都用了'='符号,但真正调用的却是不一样的函数。d3调用拷贝构造是因为d3是刚创建的对象,调用的是拷贝构造。而d2是一个已经存在的对象(已经初始化过),调用的是赋值拷贝。编译器在这里会自动的进行检查处理. 4.赋值运算符重载函数属于默认成员函数,所以跟之前的默认成员函数相同,如果自己不写编译器会调用自身的赋值运算符重载。 4.1 对于内置类型成员--值拷贝/浅拷贝。 4.2对于自定义类型成员会去调用他的赋值重载。取地址运算符重载
const成员函数:
const成员函数是 C++ 中的一种特殊成员函数,用于确保在函数调用时不会修改类的成员变量。其声明形式是在成员函数的声明末尾加上cosnt关键字
从上图可以看见当创建的对象被const修饰时,再去调用成员函数Print时,会发生报错。是因为当d2对Print成员函数进行传值时,用于接受的this指针修饰的值并没有被const修饰,此时d2的权限被放大。权限只能平移以及缩小,不允许放大,所以编译器在这产生了报错。
C++规定成员函数的声明末尾加上const关键字,表示const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。
在成员函数后加上const就讲*this修饰成const*this,那么此时传d2进成员函数Print就不会发生报错,权限也只是平移,那么在传d1的时候也同样不会报错,权限可以被缩小这种机制有助于增强代码的安全性和可读性,确保对象在被标记为const时保持不变。