类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
默认成员函数:特殊的成员函数,我们不写,编译器会自己生成一个,我们写了,编译器就不会生成。
隐含的意思:对于有些类,需要我们自己写。
对于另外一些类,编译器默认生成的就可以用。
1.构造函数
主要完成初始化功能,C语言中经常忘记Init和Destroy,C++就是利用构造函数在对象定义的时候自动初始化。
其特征如下:
1.函数名与类名相同。
2.无返回值。
3.对象实例化时编译器自动调用对应的构造函数。
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(2023, 3, 15);
Date d2(2021, 3, 12);
//不支持以下写法,编译器无法判断是定义一个对象还是函数声明
//Date d3();
//Date func();
return 0;
}
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证
每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
4.构造函数可以重载。
5.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_capacity = capacity;
_top = 0;
}
void push(int x)
{
//扩容...
_a[_top++] = x;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
//普通对象的生命周期:出了函数作用域就不存在了
//手动开辟的对象,手动释放的时候就不存在了
Stack st; //生命周期结束后,会自动调用析构函数
st.push(1);
st.push(2);
st.push(3);
return 0;
}
6.关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,默认的构造函数会对内置类型不处理,对自定类型成员调用的它的默认构造函数。(这一点非常重要)
class MyQueue
{
public:
//这种情况需要用初始化列表解决,后续会说明
/*MyQueue(size_t capacity = 8)
:_popST(capacity)
,_pushST(capacity)
,_size(0)
{}*/
void push(int x)
{
//...
}
private:
//自定义类型,不需要自己写构造函数,默认生成的构造函数会调用Stack自己的构造函数
Stack _pushST;
Stack _popST;
//既有自定义类型也有内置类型要如何处理???
//size_t _size = 0; //这里不是初始化,给的是缺省值
};
class A
{
public:
A()
{
_a = 1;
cout << "A()构造函数" << endl;
}
private:
int _a;
};
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
//自定义类型
int _year; // 年
int _month; // 月
int _day; // 日
//内置类型
A _aa;
};
int main()
{
Date d;
d.Print();
return 0;
}
7.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。(调用的时候会存在二义性)
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
不用传参数就可以调用的构造函数就叫默认构造函数!!!
构造函数可以重载意味着可以提供多个构造函数!!!
总结:什么时候要自己写构造函数???
面向需求,如果编译器默认生成的就可以满足,就不用自己写,不满足需求就要自己写。
Data Stack的构造函数需要自己写
MyQueue就不需要自己写,默认生成的构造函数就可以用
总而言之,看自己的需求,具体来看
补充!!!
class Stack
{
public:
/*Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_capacity = capacity;
_top = 0;
}*/
void push(int x)
{
//扩容...
_a[_top++] = x;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
//给了缺省值,就可以不写默认构造函数了
int* _a = nullptr;
int _top = 0;
int _capacity = 0;
//但这样非常不好
};
class MyQueue
{
public:
void push(int x)
{
//...
}
private:
Stack _pushST;
Stack _popST;
size_t _size = 0; //这里不是初始化,给的是缺省值
};
2.析构函数
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。(与之前在C中写的Destroy函数类似)
析构函数是特殊的成员函数,其特征如下:
1.析构函数名是在类名前加上字符 ~。
2.无参数无返回值类型。
3.一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4.对象生命周期结束时,C++编译系统系统自动调用析构函数。
5.关于编译器自动生成的析构函数,是否会完成一些事情呢?编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
总结:
什么时候需要自己写析构函数
Stack的析构函数,需要我们自己写。
MyQueue Data的析构函数不需要自己写,默认生成的就可以用。
3.拷贝构造函数
拷贝构造函数也是特殊的成员函数,其特征如下:
1.拷贝构造函数是构造函数的一个重载形式。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
传值过程本身也是需要调用拷贝构造函数的,所以用传值的方式会引发无穷递归。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//d2(d1)
//最好加一个const 防止d1被修改
Date(const Date& d1)
{
_year = d1._year;
_month = d1._month;
_day = d1._day;
//形参加const,防止写反,加const下面的问题就可以被检查出来
//d1._day = _day;
cout << "Date 的拷贝构造" << endl;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
//形参是实参的拷贝
void Func1(Date d)
{
cout << "Func1(Date d)" << endl;
}
//形参是实参的别名
void Func2(Date& d)
{
cout << "Func2(Date& d)" << endl;
}
int main()
{
Date d1(2023, 3, 18); //构造是初始化的意思
//拷贝一份d1
Date d2(d1); //拷贝构造--拷贝初始化
Func1(d1);
Func2(d1);
return 0;
}
3.若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
//对于Stack类默认生成的拷贝构造(浅拷贝)不能满足要求
//此时我们需要写一个深拷贝
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_capacity = capacity;
_top = 0;
}
Stack(const Stack& st)
{
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(_a, st._a, sizeof(int) * st._top);
_capacity = st._capacity;
}
void push(int x)
{
//扩容...
_a[_top++] = x;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1;
st1.push(1);
st1.push(2);
Stack st2(st1);
st2.push(10);
return 0;
}
原理
4.赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
运算符的真正意义是使代码更具有可读性!!!
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . 注意以上5个运算符不能重载。
日期类很好的体现了运算符重载的特性
赋值运算符重载
- 赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
- 赋值运算符只能重载成类的成员函数不能重载成全局函数
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现
一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值
运算符重载只能是类的成员函数。- 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注
意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值。
//Date.h
#pragma once
#include <iostream>
using namespace std;
class Date
{
public:
//友元声明的时候可以在任意位置
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
int GetMonthDay(int year, int month)
{
//只用生成一次数据
static int MonthDay[13] = { 0,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;
}
else
{
return MonthDay[month];
}
}
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
//检查日期的合法性
if (!(year >= 1 &&
(month >= 1 && month <= 12)
&& (day >= 1 && day <= GetMonthDay(year, month))))
{
cout << "非法日期" << endl;
}
}
//赋值运算符重载
//d1 = d2;
//一般这里的返回值不用加const,返回值一般可以修改
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
//return d; //权限的放大
//出了作用域返回对象还在,就可以用引用返回
return *this;
}
//d2(d1)
//最好加一个const 防止d1被修改,一定得传引用
Date(const Date& d1)
{
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
void Print() const //修饰*this
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//成员函数的第一个参数为隐藏的this指针
bool operator==(const Date& d) const;
//d1 > d2
bool operator>(const Date& d) const;
bool operator>=(const Date& d) const;
bool operator<(const Date& d) const;
bool operator<=(const Date& d) const;
bool operator!=(const Date& d) const;
//d1+=100
Date& operator+=(int day);
//d2 = d1+100
Date operator+(int day) const;
Date& operator-=(int day);
Date operator-(int day) const;
Date& operator++();
Date operator++(int); //C++规定后置加一个参数,但是不用接收这个实参
//多一个参数主要是为了跟前置进行区分,构成重载
Date& operator--();
Date operator--(int);
//d1-d2
int operator-(const Date& d) const;
//取地址重载(默认成员函数),会默认生成,自己不需要实现
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
//要求某个类的对象不让取地址
Date* operator&()
{
return nullptr;
}
const Date* operator&() const
{
return nullptr;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
//写成有友元函数,就可以访问类中的成员变量了
//static void operator<<(ostream& out, const Date& d)
//{
// out << d._year << "-" << d._month << "-" << d._day << endl;
//}
//1.改成static进行,前面加一个static是改变链接属性,只在当前文件可见,不会进入符号表
//2.声明和定义分离,只在.cpp中有定义,只会在.cpp中才会进入符号表
//ostream& operator<<(ostream& out, const Date& d);
//3.加一个内联,同样不会进入符号表,直接再用的地方展开
//cout<<d1 因为传参的顺序,这个必须要写成全局函数
inline ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
inline istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
//Date.cpp
#include "Date.h"
bool Date::operator>(const Date & d) const
{
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 Date::operator==(const Date& d) const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool Date::operator>=(const Date& d) const
{
return *this > d || *this == d;
}
bool Date::operator<(const Date& d) const
{
return !(*this >= d);
}
bool Date::operator<=(const Date& d) const
{
return *this < d || *this == d;
}
bool Date::operator!=(const Date& d) const
{
return !(*this == d);
}
Date& Date::operator+=(int day)
{
if (day < 0)
{
*this -= (-day);
return *this;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month > 12)
{
_year++;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day) const
{
Date ret(*this);
ret += day;
return ret;
}
Date& Date::operator-=(int day)
{
if (day < 0)
{
*this += (-day);
return *this;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day) const
{
Date ret(*this);
ret -= day;
return ret;
}
//对于自定义类型来说,尽量用前置,后置会多两层拷贝
Date& Date::operator++()
{
*this += 1;
return *this;
}
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
//它们既是函数重载,也分别是运算符重载
Date& Date::operator--()
{
*this -= 1;
return *this;
}
Date Date::operator--(int)
{
Date tmp(*this); //拷贝构造
*this -= 1;
return tmp;
}
//日期-日期(复用)
int Date::operator-(const Date& d) const
{
Date max = *this; //拷贝构造
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++n;
++min;
}
return n * flag;
}
//ostream& operator<<(ostream& out, const Date& d)
//{
// out << d._year << "-" << d._month << "-" << d._day;
// return out;
//}
//test.cpp
#include "Date.h"
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_capacity = capacity;
_top = 0;
}
//st1 = st2
Stack& operator=(const Stack& st)
{
cout << "Stack& operator=(const Stack& st)" << endl;
if (this != &st)
{
free(_a);
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(_a, st._a, sizeof(int) * st._top);
_capacity = st._capacity;
}
return *this;
}
Stack(const Stack& st)
{
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(_a, st._a, sizeof(int) * st._top);
_capacity = st._capacity;
}
void push(int x)
{
//扩容...
_a[_top++] = x;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
public:
void push(int x)
{
//...
}
private:
Stack _pushST;
Stack _popST;
};
void TestDate1()
{
Date d1(2022, 2, 30);
d1.Print();
d1 + 100; //转换成函数调用
}
//bool operator+(int a,int b)
//{}
void TestDate2()
{
Date d0;
Date d1;
Date d2(2022, 10, 8);
d1.Print();
Date d3(d2); // 拷贝构造(初始化)
d1 = d2; //赋值重载(复制拷贝),已经存在的两个对象之间的拷贝
d0 = d1 = d2; //链式赋值,一般是返回左操作数
d1.Print();
Date d4 = d2;//这里是拷贝构造还是赋值重载呢???
//此处是拷贝构造
int i, j, k;
i = j = k =10;//整形会有链式赋值
i++; //i可以修改
}
void TestStack()
{
Stack st1;
st1.push(1);
st1.push(1);
st1.push(1);
Stack st2;
st2.push(10);
st2.push(20);
//st1 = st2; //崩溃,析构两次+内存泄露
MyQueue q1;
MyQueue q2;
q2 = q1;
//st1 = st1;//错误
}
void TestDate3()
{
Date d1(2022, 10, 8);
d1 -= 10000;
d1.Print();
Date d2(d1);
(d2 - 10000).Print();
d1 -= -10000;
d1.Print();
(++d1).Print();//d1.opeartor++();
(d1++).Print();//d1.opeartor++(int);
(--d1).Print();
(--d1).Print();
/*Date d1(2023, 3, 19);
Date d2(2023, 9, 1);
cout << d2 - d1 << endl;*/
}
void TestDate4()
{
//为什么能自动识别类型
//本质原因是函数重载+运算符重载
Date d1, d2;
cin >> d1 >> d2;
//链式调用
cout << d1 << endl << d2 << endl;;
}
void TestDate5()
{
const Date d1(2023, 3, 19);
d1.Print();
}
int main()
{
TestDate5();
return 0;
}
MyQueue这样的类也不需要自己写赋值重载,自定义类型会调用它自己赋值重载函数!!!
函数定义写在.h里面的函数在多个链接包,会出现链接冲突,解决办法见代码!!!
会在两个文件中包含,进入在符号表中两次,会链接报错
const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数
隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
取地址重载
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如想让别人获取到指定的内容!