默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。
六个默认成员函数:
在C++中,构造函数是专门用于初始化对象的方法。当创建类的新实例时,构造函数会自动被调用。通过构造函数,我们可以确保对象在创建时就被赋予合适的初始状态。下面将详细解释如何使用构造函数进行初始化操作,并以Date类为例进行说明。
// 创建一个Date类
class Date
{
public:
// 成员函数...
private:
int _year;
int _month;
int _day;
};
⽆参构造函数
、全缺省构造函数
、拷贝构造
这三个我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调⽤的构造就叫默认构造。请注意第8条特征
无参构造函数允许我们创建Date对象而不提供任何参数。但是,需要注意的是,如果我们不在无参构造函数中初始化成员变量,那么这些变量的初始值将是未定义的,这可能会导致程序出错。
Date d1; // 调用无参构造函数
class Date
{
public:
// 1. 无参构造函数
Date()
{
// 在这里可以添加一些初始化代码,例如设置默认日期
// 例如:_year = 2000; _month = 1; _day = 1;
}
// 其他成员函数...
private:
int _year;
int _month;
int _day;
};
带参构造可以和无参构造函数重载,因为在之后调用的时候不会受影响,可以与之后讲解的全缺省构造函数和无参构造函数之间的不能函数重载的进行区别。
带参构造函数可以在对对象进行初始化的时候进行传参,传参的数值会直接进行初始化对象中的成员变量。
Date date2(2023, 3, 15); // 调用带参构造函数创建对象,并初始化日期为2023年3月15日
class Date
{
public:
// 1. 无参构造函数
Date()
{
// ...
}
// 2. 带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// 其他成员函数...
private:
int _year;
int _month;
int _day;
};
在这个带参构造函数中,我们通过参数
year、month
和day来初始化_year、_month和_day
成员变量。这样,我们就可以在创建Date
对象时直接指定日期了。
注意区别创造对象的格式:
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数
C++11 😗*内置类型成员变量在类中声明时可以给默认值。 **
全缺省参数的构造函数结构类似于以下代码:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
特点:会在参数列表中进行类似于赋值的操作 这个构造函数接受三个参数,并且每个参数都有一个默认值。这意味着,在创建Date对象时,你可以选择性地提供这些参数。如果你没有为任何一个参数提供值,那么它们将使用默认值(即1900年1月1日)。
思考:以下代码是否可以编译通过?
class Date
{
public:
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
结论:无法通过。 原因:
Date d;
的时候编译器无法抉择选择构造函数。对象在销毁时(生命周期结束时)会自动调用析构函数,完成对象中资源的清理工作(如释放动态分配的内存、关闭文件等)。
~Stack() { }
;注意:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (nullptr == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
if (_size == _capacity)
{
// 扩展数组大小
_capacity *= 2;
_array = (DataType*)realloc(_array, sizeof(DataType) * _capacity);
if (nullptr == _array)
{
perror("realloc扩展空间失败!!!");
return;
}
}
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _capacity;
size_t _size;
};
void TestStack()
{
Stack s;
s.Push(1);
s.Push(2);
}
int main()
{
TestStack();
return 0;
}
当正确使用析构函数后就不用担心程序中有内存泄漏的情况了,因为在每次该对象生命周期结束后都会自动调用析构函数,流程如下:
如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值,则此构造函数 也叫做拷⻉构造函数,拷⻉构造是⼀个特殊的构造函数。
// 错误的写法
Date(const Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
以日期类举例:若使用Date(const Date d)
传参进行拷贝构造时,在传参的时候例如是以Date(d2)
来传参那么就相当于用d = d2
,这样的话由于是在构造一个新的对象d2
,所以会继续调用拷贝构造函数,如此下去就会造成无限循环的去调用拷贝构造函数而不会执行结束。
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
所以正确的写法应该如上代码所示。
在写的参数的时候用
const
是为了保证数据的安全性,防止被修改。
浅拷贝是指在创建对象的副本时,只复制对象本身,而不复制对象所持有的资源(如动态分配的内存)。浅拷贝可能导致的问题是,如果原始对象和副本对象都尝试释放相同的资源,就可能发生内存泄漏或双重释放错误。
深拷贝是指在创建对象的副本时,不仅复制对象本身,还复制对象所持有的所有资源。这意味着如果对象包含指针指向动态分配的内存,深拷贝会为副本对象分配新的内存,并复制原始内存中的数据。
在默认生成的拷贝构造函数和赋值运算符重载中使用的是浅拷贝还是深拷贝取决于自定义成员变量的拷贝构造函数,当没有空间申请的时候一般会使用浅拷贝,但是在有空间申请的时候会进行深拷贝,前提是自定义成员变量的拷贝构造函数有申请空间进行拷贝,这样上一级自动生成的默认构造函数才会进行正确调用。 例如:用两个栈实现队列
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(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()
{
MyQueue mq1;
MyQueue mq2 = mq1;
return 0;
}
MyQueue
⾃动⽣成的拷⻉构造,会⾃动调⽤Stack
拷⻉构造完成pushst/popst
的拷⻉,只要Stack
拷⻉构造⾃⼰实现了深拷⻉,当用MyQueue
自动生成的拷贝构造的时候就会进行深拷贝从而完成拷贝。
返回值为引用要注意返回的值为局部对象还是全局对象:
如下代码,返回值为引用,即会出现也引用的问题:
Date& Func2()
{
Date tmp(2024, 7, 5);
tmp.Print();
return tmp;
}
正确应该如下:
Date Func2()
{
Date tmp(2024, 7, 5);
tmp.Print();
return tmp;
}
当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。
运算符重载是具有特名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其他 函数⼀样,它也具有其返回类型和参数列表以及函数体 。
例如定义一个日期类的**+=**
运算符(一元运算符):
Date& Date::operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
使用方法:
d1 += 2;
流插入运算符重载(二元运算符):
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "⽉" << d._day << "⽇" << endl;
return out;
}
使用方法:
cout << d1;
重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。 C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分。
前置递增运算符直接修改对象本身,并返回修改后的对象的引用。返回引用的原因是为了提高性能和节省内存。由于前置递增运算符直接修改对象,返回引用避免了不必要的对象拷贝。具体代码如下:
Date& Date::operator++()
{
*this += 1; // 修改当前对象
return *this; // 返回当前对象的引用
}
后置递增运算符需要返回修改前的对象,因此需要创建一个临时对象来保存递增前的状态。由于返回的是临时对象,不能返回引用。具体代码如下 :
Date Date::operator++(int)
{
Date temp = *this; // 保存当前对象状态
*this += 1; // 修改当前对象
return temp; // 返回保存的临时对象
}
重载
<<
和>>
时,需要重载为全局函数,因为重载为成员函数,this
指针默认抢占了第⼀个形参位 置,第⼀个形参位置是左侧运算对象,调⽤时就变成了对象<<cout
,不符合使⽤习惯和可读性。 重载为全局函数把ostream/istream
放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对 象。友元函数没有this指针
// 流提取运算符重载
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "月" << d._month << "月" << d._day << "日" << endl;
return out;
}
// 流插入运算符重载
istream& operator>>(istream& in, Date& d)
{
cout << "请以此输入年月日 > ";
in >> d._year >> d._month >> d._day;
if (!d.CheckDate())
{
cout << "Date is error!" << endl;
}
return in;
}
// 在类中声明:
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
const
修饰成员函数的格式:void Print() const
。作用:
this
指针,加了const
后表示函数内不能对类内任何成员进行修改。const
,这样在使用函数的时候不论是const
类型还是普通类型的对象都可以正常使用,还可以保证数据的安全。取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载。
如需重载取地址运算符,逻辑大体如下:
Date* operator&()
{
return this;
// return nullptr;
}