一、类的默认成员函数
在C++中,类的默认成员函数是那些当开发者没有显式定义时,编译器会自动为类生成的特殊成员函数。⼀个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可。默认成员函数很重要,也比较复杂,我们要从两个方面去学习:
第⼀:我们不写时,编译器默认生成的函数是什么,是否满足我们的需求。
第⼆:编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?
二、默认构造函数
1.构造函数定义
在C++中,构造函数是一种特殊的成员函数,用于在创建对象时初始化对象。相当于以前所写的Init函数作用类似。
2.构造函数的核心作用
2.1初始化成员变量:当使用关键字创建对象,声明对象时,构造函数会自动被调用,用于设置对象的初始状态。这是通过给成员变量赋初值来实现的。
2.2替代初始化函数:构造函数的出现替代了传统上在类中手动编写的初始化函数(如Init方法)。它使对象的初始化过程更加自然和统一,因为对象的创建和初始化几乎是同时发生的。
2.3构造函数的自动调用:构造函数的调用是自动的,不需要(也不应该)在代码中显式调用。这一特性确保了对象在生命周期开始时就被正确地初始化,减少了出错的可能性。
3.构造函数的特点
3.1. 函数名与类名相同。
3.2. 无返回值。
3. 3对象实例化时系统会自动调用对应的构造函数。
3.4.构造函数可以重载。
3.5. 如果类中没有显式定义构造函数,则C++编译器会自动生成⼀个无参的默认构造函数。
3.6. 无参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在,因为可能会存在调用歧义。总结就是不传参就能调用的函数就是默认构造。
#include <iostream>
class MyClass {
private:
int myNumber;
public:
// 默认构造函数(无参数)
MyClass() : myNumber(0) {
std::cout << "默认构造函数被调用" << std::endl;
}
// 带参数的构造函数
MyClass(int number) : myNumber(number) {
std::cout << "带参数的构造函数被调用,数字为: " << number << std::endl;
}
// 其他成员函数...
void display() const {
std::cout << "myNumber 的值为: " << myNumber << std::endl;
}
};
int main() {
MyClass obj1; // 调用默认构造函数
MyClass obj2(10); // 调用带参数的构造函数
obj1.display(); // 输出: myNumber 的值为: 0
obj2.display(); // 输出: myNumber 的值为: 10
return 0;
}
三、析构函数
1.析构函数的定义
析构函数是类的一个成员函数,用于在对象生命周期结束时自动调用,执行清理工作,如释放分配的内存、关闭打开的文件等。类似与Destory()函数。
2.析构函数的特点
1. 析构函数名是在类名前加上字符~。
2. 无参数无返回值,不用加void。
3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时,系统会自动调用。
5.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date;如 果默认生成的析构就可以用,也就不需要显示写析构,如MyQueue;但是有资源申请时,⼀定要自己写析构,否则会造成资源泄漏,如Stack。
例如:
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass() {
cout << "Constructor called" << endl;
// 初始化代码...
}
~MyClass() {
cout << "Destructor called" << endl;
// 清理代码...
}
};
int main() {
MyClass obj; // 调用构造函数
// ... 对象使用过程 ...
// 当main函数结束时,对象obj的生命周期结束,自动调用析构函数
return 0;
}
注意 :如果有多个类对象定义了,先调用构造函数的类,后调用析构函数。
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass() {
cout << "Constructor called" << endl;
// 初始化代码...
}
~MyClass() {
cout << "Destructor called" << endl;
// 清理代码...
}
};
int main() {
MyClass obj1; // x先调用构造函数构造obj1,后面才是obj2。
MyClass obj2;
// ... 对象使用过程 ...
// 当main函数结束时,先对obj2调用析构函数,后面才对obj1调用析构函数。
return 0;
}
四、拷贝构造函数
1.拷贝构造函数的定义
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数。
2.拷贝构造函数特点
2.1 拷贝构造函数时构造函数的重载。
2.2第一个参数必须时自身类类型的引用,不可以是传值引用,因为语法上会引发无穷递归调用。
如图:
2.3 C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
2.4 若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成 员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。如图:浅拷贝就是这样完完全全一模一样,地址一样,这样子当它析构类时会造成两次对同一对象析构,导致程序崩溃。
2.5 类的拷贝构造函数的实现取决于其成员变量的性质。如果类仅包含内置类型成员,则编译器自动生成的拷贝构造函数(浅拷贝)通常足够就如Date类。然而,如果类包含指向动态分配资源的指针(如Stack类)或需要特殊处理的自定义类型成员(MyQueue),且这些自定义类型的拷贝行为不足以满足当前类的需求,则通常需要自定义拷贝构造函数来实现深拷贝或确保正确的拷贝行为。
6. 传值返回时,C++会创建临时对象并调用拷贝构造函数,而传引用返回则直接返回对象的引用(别名),避免了拷贝的开销。然而,如果引用返回的是局部对象的引用,则该对象在函数返回后会被销毁,这样返回的引用就有问题,类似于野指针,指向不再存在的内存。因此,在使用引用返回时,必须确保返回的对象在函数执行完毕后仍然有效,以避免使用引用导致的未定义行为。简而言之,引用返回可以减少拷贝,但需保证返回对象的生命周期。
#include<iostream>
using namespace std;
typedef int STDataType;
class Date
{
public:
Date(int year=1949, int month=10, int day=1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
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:
private:
Stack pushst;
Stack popst;
};
int main()
{
Date d1;
Date d2 = d1;
Stack st1;
st1.Push(1);
st1.Push(2);
// Stack不显⽰实现拷⻉构造,⽤⾃动⽣成的拷⻉构造完成浅拷⻉
// 会导致st1和st2⾥⾯的_a指针指向同⼀块资源,析构时会析构两次,程序崩溃
Stack st2 = st1;
MyQueue mq1;
// MyQueue⾃动⽣成的拷⻉构造,会⾃动调⽤Stack拷⻉构造完成pushst/popst
// 的拷⻉,只要Stack拷⻉构造⾃⼰实现了深拷⻉,他就没问题
MyQueue mq2 = mq1;
return 0;
}
五、赋值运算符重载。
1.运算符重载
1.1定义
当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
1.2特点
(1)运算符重载的函数名是由operator加上运算符,和其他函数一样有返回类型、参数和函数体。
(2)运算符重载与正常运算符作用对象的数量一样多。一元运算符重载就有一个参数,二元运算符重载就有两个参数,左侧运算对象传给第一个参数,右侧传给第二个参数。如果二元运算符重载是成员函数,则默认第一个对象传给this指针,因此只要有一个参数即可。
(3)运算符重载后其优先级和结合性不变。
(4)不能通过连接语法中没有的符号来创建新的操作符:比如operator@。
(5)重载操作符至少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int operator+(int x, int y)。
(6)⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如日期Date类重载operator-就有意义,但是重载operator+就没有意义,因为日期加日期没有意义。
(7)重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分。
(8)注意 .* :: sizeof ?: . 这五个运算符不可以函数重载。
(9)重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调用时就变成了 对象<<cout,不符合使用习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。
#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);//友元函数
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int GetMonthDay(int year, int month) const
{
assert(month > 0 && month < 13);
static int monthDayArray[13] = { -1, 31, 28, 31, 30, 31, 30,31, 31, 30, 31, 30, 31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
return 29;
return monthDayArray[month];
}
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
Date& operator++()
{
cout << "前置++" << endl;
//...
return *this;
}
Date operator++(int)
{
Date tmp;
cout << "后置++" << endl;
//...
return tmp;
}
bool CheckDate() const;
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
bool Date::CheckDate() const
{
if (_month < 1 || _month > 12
|| _day < 1 || _day > GetMonthDay(_year, _month))
{
return false;
}
else
{
return true;
}
}
// 11:45
istream& operator>>(istream& in, Date& d)
{
while (1)
{
cout << "请依次输入年月日:>";
in >> d._year >> d._month >> d._day;
if (!d.CheckDate())
{
cout << "输入日期非法:";
cout << "请重新输入!!!" << endl;
}
else
{
break;
}
}
return in;
}
int main()
{
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
// 运算符重载函数可以显⽰调⽤
d1.operator==(d2);
// 编译器会转换成 d1.operator==(d2);
d1 == d2;
// 编译器会转换成 d1.operator++();
++d1;
// 编译器会转换成 d1.operator++(0);
d1++;
return 0;
}
2.赋值运算符重载
2.1定义
赋值运算符重载是⼀个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于⼀个对象拷贝初始化给另⼀个要创建的对象。
2.2特点:
-
必须作为成员函数实现:赋值运算符
=
必须被重载为类的成员函数,不能是友元函数或普通函数。 -
参数类型:参数通常被设计为
const 当前类类型&
(即常量引用),这样可以避免不必要的对象拷贝,提高效率。如果采用传值方式,将会导致额外的拷贝操作。 -
返回类型:赋值运算符重载通常返回当前对象的引用(
当前类类型&
),这支持了连续赋值操作(如a = b = c
),并且允许赋值表达式被用作更大的表达式的一部分。 -
默认行为:如果开发者没有显式地实现赋值运算符重载,编译器会提供一个默认的赋值运算符实现。这个默认实现的行为类似于默认拷贝构造函数,即逐字节地拷贝内置类型成员(浅拷贝),并调用自定义类型成员的拷贝赋值运算符。
-
资源管理类:对于像
Stack
这样管理动态分配资源的类,默认的赋值运算符重载(浅拷贝)通常是不够的,因为它不会复制指向的资源,而是仅仅复制了资源指针。这会导致两个对象共享同一份资源,一旦其中一个对象释放了资源,另一个对象就会拥有一个悬空指针。因此,这类类通常需要显式地实现深拷贝的赋值运算符重载。 -
包含自定义类型成员的类:如果类(如
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(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;
}
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;
}
六、取地址运算符重载
1.const成员函数
将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。
const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
例如:const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// void Print(const Date* const this) const
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 这⾥⾮const对象也可以调⽤const成员函数是⼀种权限的缩⼩
Date d1(2024, 7, 5);
d1.Print();
const Date d2(2024, 8, 5);
d2.Print();
return 0;
}
2.取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。除非一些很特殊的场景,比如我们不想让别人取到当前类对象的地址,就可以自己实现⼀份,胡乱返回⼀个地址。
class Date
{
public:
Date* operator&()
{
return this;
// return nullptr;
}
const Date* operator&()const
{
return this;
// return nullptr;
}
private:
int _year; // 年
int _month; // ⽉
int _day; // ⽇
};