嗨~大家好,这里是春栀怡铃声的博客~

"做你害怕的事,然后发现,不过如此~"
哈喽呀,今天我们继续与C++智斗~
目录
类的默认成员函数
默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。
⼀个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可
这4个:构造函数、析构函数、拷贝构造函数、赋值运算符重载
我们学习默认成员函数时,第一我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。
第二要研究如果默认成员函数不符合我们的要求,我们怎么写成员函数去实现
const写在函数末尾的情况
加了 const 编译器就能拦住你修改变量?秘密在于 C++ 类的隐藏参数:this 指针。
当你调用一个成员函数时,编译器会偷偷把当前对象的地址作为参数传进去。
对于普通成员函数 void func():
隐藏的 this 指针类型是 Data* const this(指针本身不能变,但指针指向的对象可以被修改)。
对于常成员函数 void func() const:
隐藏的 this 指针类型变成了 const Data* const this(不仅指针本身不能变,指针指向的对象也被视为了常量)。
因为 this 指向的对象变成了 const,所以通过 this 访问到的所有成员变量也都被当成了常量,自然就无法赋值了。
在写 C++ 的类时,请养成一个极其良好的习惯:
只要一个成员函数不需要修改对象内部的数据(比如各种 get() 函数、打印函数),就立刻、马上、毫不犹豫地在它末尾加上 const!
构造函数
完成初始化工作
构造函数的特点:
-
函数名与类名相同。
-
无返回值。(返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
-
对象实例化时系统会自动调用 对应的构造函数。
-
构造函数可以重载。
-
如果类中没有显式定义构造函数,则C++编译器会自动生成⼀个无参的默认构造函数,⼀旦用户显式定义编译器将不再生成。
请看这段代码,类名为Date ,构造函数的函数名也是Date ,并且没有返回值,
cpp
class Date
{
public:
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(2024, 7, 5);
d1.Print();
Date d2(2024, 7, 6);
d2.Print();
return 0;
}
这里构造函数是以全缺省形式写的,还有无参和带参函数:
// 1.无参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
// 2.带参构造函数Date(int year, int month, int day)
{ _year = year;
_month = month;
_day = day;
}
相比于无参和带参形式,全缺省更具有优势,
全缺省在没有传参的时候 有自己初始值,如果此时执意打印也不会出错,传参后就保持传参的数值。
这个构造函数需要我们自己动手写才能达到这个效果,接下来我们看一个不需要自己动手写的构造函数
cpp
#include<iostream>
using namespace std;
class MyQueue
{
public:
//编译器默认⽣成 MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:
Stack pushst;
Stack popst;
};
int main()
{
MyQueue mq;
return 0;
}
析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比我们之前Stack实现的Destroy功能,而像Date没有 Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。
析构函数的特点:
1.析构函数名是在类名前加上字符 ~
-
无参数无返回值 。(这里跟构造类似,也不需要加void)
-
⼀个类只能有**⼀个析构函数** 。若未显式定义,系统会自动生成默认的析构函数 。
-
对象生命周期结束时 ,系统会自动调用析构函数 。
-
跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员
调用他的析构函数。 -
还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
7.对于我们在构造函数那节讲的Date 类型,由于只是有相关变量(_year 、_month 、_day)的处理,没有特别的,所以无需自己动手写析构函数
而对于Stack 类**,有资源申请时** ,⼀定要自己写析构, 否则会造成资源泄漏。有指针的释放,置为空指针,所以不能直接套用编译器默认那一套(换句话说,编译器那一套已经过时,满足不来我们新的需求,需要自己实现)
cpp
include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n =4) //构造函数
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
_capacity = n;
_top = 0;
}
~Stack() //自动调用析构函数,后定义的先析构
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
int main()
{
Stack st1;
Stack st2;
return 0;
}
注意!这里调用析构函数后,先析构st2 后析构st1
拷贝构造函数
如果⼀个构造函数的第⼀个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数。
拷贝构造函数的特点:
-
拷贝构造函数是构造函数的⼀个重载。
-
拷贝构造函数的第⼀个参数必须是类 类型对象的引用 ,使用传值方式编译器直接报错 ,因为语法逻辑上会引发无穷调用递归。拷贝构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引用,后面的参数必须有缺省值。
-
C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
-
若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型
员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构
造。
-
这里还有一个小技巧,如果⼀个类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则就不需要。
-
传值返回会产生⼀个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引用相当于⼀个野引用,类似⼀个野指针⼀样。传引用返回可以少
拷贝,但是⼀定要确保返回对象,在当前函数结束后还在,才能用引用返回。
关于Date 类的拷贝构造如下
cpp
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year,int month,int day) //构造函数 ,带参构造函数
{
_year = year;
_month = month;
_day = day;
}
Date(const Date &d) //拷贝构造 不加 & 会报错 必须是引用
//加const 保护d不被改变
//d是d1的别名 d2拥有_year等,接下来的步骤是把d1中的值拷贝到d2中
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2026, 4, 1);
d1.Print();
Date d2(d1); //拷贝d1的值给d2 ---拷贝构造
//Date d2=d1; 也是拷贝构造,这是另一种写法
d2.Print();
return 0;
}
关于Stack 类的拷贝构造
cpp
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n =4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
_capacity = n;
_top = 0;
}
~Stack() //自动调用析构函数,后定义的先析构
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
int _capacity;
int _top;
};
int main()
{
Stack st1;
Stack st2(st1);
Stack st3 =st1; //也是拷贝构造 ,规定
return 0;
}
1.如果我们不自己写拷贝构造函数,而是直接调用默认拷贝构造,程序会崩溃
Why?
我们想一想,在调用默认拷贝构造过程中,并没有申请新空间,所以我们st2 和st1 指向同一个空间,如下图:

指向同一个空间,意味着析构函数起作用时,这个空间被析构了2次!所以程序会崩溃!
那我们怎么解决呢?自己写一个拷贝构造函数,需要申请空间~
cpp
Stack(const Stack &st) //正确的拷贝构造函数 深拷贝
{
_a = (STDataType*)malloc(sizeof(STDataType)*st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_capacity = st._capacity;
_top = st._top;
}
野引用
如果说 C++ 中的"引用是给一个变量起了个贴心的"小名",那么野引用就像是你试图拨打前任已经注销的电话号码。
想象一下:刚谈恋爱时,你存了对方的号码并备注为"亲爱的"(这叫建立引用)。后来你们分手了,对方把号码注销了(这叫变量被销毁、内存被释放)。几个月后,这个号码被运营商重新分配给了一个卖保险的大哥(内存被分配给了其他程序)。
这时,如果你习惯性地再拨打"亲爱的"找人借钱:
情况 A: 提示空号,直接挂断(程序直接 Crash 崩溃)。
情况 B: 电话接通了,但对面传来一句"大哥买保险吗?"(程序没崩,但读写了完全错乱的垃圾数据)。
在 C++ 中,这种情况被称为未定义行为(Undefined Behavior),俗称"薛定谔的代码"------它有时好用,有时崩溃,全看运气。
赋值运算符重载
运算符重载
当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规 定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编 译报错。
换一种说法:现在有2个钱包,一个钱包里面有200元,另一个则有50元,现在我想让编译器算算这2个钱包一共有多少元,可惜编译器只能对它认识的类型做加法!我们的钱包相加是自定义类型,这时候,运算符重载就排上用场啦
请看下面代码示意:
cpp
#include <iostream>
using namespace std;
class Wallet {
public:
int money;
Wallet(int m)
{
money==m;
}
// 重载 '+' 运算符
// 函数名就是 operator+
Wallet operator+(const Wallet& other) {
// 创建一个新钱包,里面的钱等于两个钱包的钱之和
return Wallet(this->money + other.money);
}
};
int main() {
Wallet a(200); // 钱包A有200块
Wallet b(50); // 钱包B有50块
// 现在编译器认识这个加号了!
// 实际上它在底层偷偷调用了:a.operator+(b)
Wallet c = a + b;
cout << "新钱包里有: " << c.money << " 块钱" << endl;
// 输出: 新钱包里有: 250 块钱
return 0;
}
运算符重载是具有特殊名字的函数,他的名字是由 operator 和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体。
重载运算符函数的参数个数和该运算符作用的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元 运算符有两个参数,二元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算 符重载作为成员函数时,参数⽐运算对象少⼀个。
运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。(就是说原来的运算符优先级不改变)
不能通过连接语法中没有的符号来创建新的操作符:比如operator@
.* :: sizeof ?: . 注意以上5个运算符不能重载。
重载操作符至少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int
operator+(int x, int y)
⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义
重载++运算符时,有前置++和后置++ ,运算符重载函数名都是operator++, 无法很好的区分。
C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分。
请看代码演示:
cpp
class Date
{
public:
Date(int year=2026, int month=4, int day=18)
{
_year = year;
_month = month;
_day = day;
}
void Date::Print() const
{
cout << _year << "/" << _month << "/" << _day << "/"<<endl;
}
//d1++
Date operator++(int)
{
Date tmp = *this;
tmp += 1;
return tmp;
}
//++d1
Date& operator++()
{
*this += 1;
return *this;
}
private:
int _year;
int _month;
int _day;
};
注意看前置++我们实现的运算符重载返回的是引用,而后置++并没有返回引用
这是为什么呢?
前置++:先完成++后使用,*this本身会发生改变,传结果时可以直接传*this
后置++:先完成使用后++,*this 本身不会改变,传结果时必须要有一个新值在原来*this基础上+1,最后返回新值,新值的生命周期从调用完这个函数后就结束了,所以如果这里依旧选择传引用返回的话,会造成野引用的出现(野引用)
重载<<和>>时,需要重载为全局函数 ,因为重载为成员函数,this指针默认抢占了第⼀个形参位
置,第⼀个形参位置是左侧运算对象,调用时就变成了对象<<cout,不符合使用习惯和可读性。
重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对
象。
cpp
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d); //为什么都有引用
//in 和out前面加 & 是规定
//为什么const 不全部都加?
//因为输入的返回值 需要改变,不能加const
public:
Date(int year=2026, int month=4, int day=18)
{
_year = year;
_month = month;
_day = day;
}
void Date::Print() const
{
cout << _year << "/" << _month << "/" << _day << "/"<<endl;
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream &out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream &in, Date &d)
{
while (1)
{
cout << "请输入年月日>";
in >> d._year >> d._month >> d._day;
if (!d.Check()) //加d.
{
cout << "输入错误";
d.Print();
cout << "重新输入";
}
else
{
break;
}
}
return in;
}
以日期类为例,介绍<< >> 如何重载
重点:
1.设计成全局函数
2.使用友元函数调用类的私有
3.为什么const 不加到输入重载函数中?因为输入的返回值 需要改变,不能加const
4.in 和out前面加 & 是规定
赋值运算符重载的特点
赋值运算符重载是⼀个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值 ,这里要注意跟拷贝构造区分,拷贝构造用于**⼀个对象拷贝初始化给另⼀个要创建的对象。**
赋值运算符重载的特点:
- 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成
const 当前类类型引用,否则会传值传参会有拷贝
-
有返回值,且建议写成当前类类型引用 ,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
-
没有显式实现时,编译器会自动生成⼀个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。