类和对象(中)
前言.类的六个默认成员函数
在空类中,并不是什么都没有。任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。
一.构造函数
构造函数并不会开空间创建对象。它主要是用来完成对象的初始化的工作。对象的创建不需要某个函数去创建,因为对象是在栈中,成员变量随着栈帧的创建而自动创建的,同时随着栈帧的销毁。
1.1构造函数特性
- 函数名与类名相同。
- 无返回值(不用写void)。
- 对象实例化时编译器自动调用对应的构造函数。
- 显示定义的构造函数不能定义为私有(否则无法调用)
- 构造函数可以重载(支持多种初始化)。
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
- 默认构造函数对内置类型不做初始化(C++11中打了补丁,即:内置类型成员变量在类中声明时可以给缺省值),对自定义类型会做初始化。
- 不传参就可以调用的就是默认构造函数(空参,全缺省,默认构造函数),但是只能有一个。
1.2实现对象的初始化
-
- 调用函数来进行初始化
class Date
{
public:
void Init(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;
d1.Init(2023, 5, 4);
d1.Print();
Date d2;
d2.Init();
d2.Print();
}
缺点:在很多时候,我们会经常忘记初始化,从而导致野指针,随机值等问题的出现。为了避免这些问题的出现我们可以使用构造函数来自动完成对对象的初始化的工作。
- 2.显示实现构造函数
class Date
{
public:
//1.带参构造函数
/*Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}*/
//(建议)2.带参全缺省构造函数
Date(int year = 2, int month = 2, int day = 2)
{
cout << "Date(int year = 2, int month = 2, int day = 2)" << endl;
_year = year;
_month = month;
_day = day;
}
//3.空参构造函数
/*Date()
{
_year = 1;
_month = 1;
_day = 1;
}*/
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 5, 4); //调用带参构造函数
d1.Print();
Date d2; // 调用空参/全缺省/默认构造函数
d2.Print();
}
注意:
- 上面三种构造函数只能存在一个,1和2不构成重载(形参类型相同)。2和3构成重载,但是在你用无参构造函数创建对象的时候,会报错调用不明确。
- 如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明。
int main()
{
//报错
//会被编译器当作函数声明,函数名d3,返回值为Date类型。
Date d3();
return 0;
}
1.3默认构造函数
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数。
注意:默认成员函数都不能重载为全局,否则会和编译器默认生成的发生冲突。
- 默认构造将函数
class Date
{
public:
//使用编译器默认的构造函数
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
这里我们发现此时虽然调用了编译器默认生成的构造函数,但是并没有对内置类型做初始化。
- 若在成员变量中自定义类型时。
此时VS编译器对内置类型和自定义类型都做了初始化。
注意:
- 若成员变量中只有内置类型时,编译器默认生成的构造函数不会对内置类型进行初始化。
- 若成员变量既有内置类型和自定义类型,自定义类型会去调用它的默认构造,对于内置类型是否进行初始化取决于编译器。
- C++11中的改进
在C++11中打了一个补丁,在我们使用默认构造函数时可以给内置类型缺省值。
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
//这里给的是默认的缺省值
//给编译器默认生成的构造函数使用的
//如果显示定义了构造函数,缺省值就没用了
int _year = 1;
int _month = 1;
int _day = 1;
};
建议:
- 当成员变量中有内置类型的时候我们就需要自己写构造函数。
- 当成员变量中只有自定义类型时可以考虑使用编译器自动生成的构造函数。
二.析构函数
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁是随着栈帧的销毁而销毁的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
2.1析构函数的特性
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。(不能重载)
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数
- 如果类中没有动态申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数。有动态资源申请时,一定要写,否则会造成资源泄漏。
2.2显式定义析构函数
class stack
{
public:
stack(int defaultecapacity = 4)
{
cout << "stack(int defaultecapacity = 4)" << endl;
_a = (int*)malloc(sizeof(int) * defaultecapacity);
if (_a == nullptr)
{
perror("malloc failed:");
return;
}
_top = 0;
_capacity = defaultecapacity;
}
void Push(int x)
{
//不考虑扩容
_a[_top++] = x;
}
//析构函数
~stack()
{
cout << "~stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
stack st1;
st1.Push(1);
st1.Push(2);
return 0;
}
若我们写的是销毁函数,那么在很多地方需要我们注意是否程序会提前结束,然后及时释放。如果有析构函数那么我们就不用自己去考虑,在生命周期结束的时候,析构函数会被自动调用,进行资源的清理工作。
2.3默认析构函数
若没有显式定义析构函数,编译器会自动生成析构函数。编译器默认生成的析构函数和构造函数有着相同的特点。
-
- 对内置类型不做处理。(销毁时不需要资源清理,随着栈帧的销毁,系统直接将其内存回收)
-
- 自定义类型会去调用它的析构函数。
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour ;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
此时,Date类中并没有显式定义析构函数。当需要销毁对象d的时候,编译器会自动生成一个析构函数,完成对Date类中的动态资源的清理,如果有自定义类型,则会去调用自定义类型自己的析构函数。因此:创建哪个类的对象则调用该类的构造函数,销毁那个类的对象则调用该类的析构函数
注意:
- 一般情况下,没有动态申请资源时和需要释放的资源都是内置类型的时候,不需要写析构函数。
- 若有动态申请资源,就需要我们自己显式写析构函数释放资源。
三.拷贝构造函数
在我们想要将一个对象的数据拷贝给另一个对象时,可以使用拷贝构造函数,同时拷贝构造函数也是一种构造函数。
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
class Date
{
public:
Date(int year = 1, int month = 1, 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;
};
int main()
{
Date d1(2023, 5, 9);
Date d2(d1);
return 0;
}
3.1拷贝构造函数的特性
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
-
C++规定:内置类型直接拷贝,自定义类型(传参和赋值)都必须要调用拷贝构造函数去完成。
-
- 定义一个函数func
void func(Date d)
{
}
那为什么使用传值方式来拷贝会造成无穷递归呢?
答: 因为在使用传值调用的时候,需要先传参,传参会形成一个拷贝构造函数,在传参,在调用,无穷递归。
解决方法:
- (推荐)用引用(建议加const)
- 使用指针接收。
- 若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数按对象内存存储的字节序完成拷贝。这种拷贝叫做浅拷贝或者值拷贝。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 5, 9);
Date d2(d1);
return 0;
}
特殊情况:当栈中指针指向一块空间的时候,不能使用浅拷贝。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack st1;
//不能使用默认生成的拷贝构造函数
Stack st2(st1);
return 0;
}
注意:
- 若类中没有资源申请的时候,拷贝函数写不写都可以,但是如果有资源申请的时候,则拷贝构造函数是一定要写(进行深拷贝)。
- 对内置类型,直接拷贝,对自定义类型则会去调用它的拷贝构造函数。
3.2拷贝构造函数典型调用场景
-
- 使用已存在对象创建新对象
-
- 函数参数类型为类类型对象
-
- 函数返回值类型为类类型对象
class Date
{
public:
Date(int year, int minute, int day)
{
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d):" << this << endl;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d)
{
Date temp(d);
return temp;
}
int main()
{
Date d1(2022, 1, 13);
Test(d1);
return 0;
}
- 若Test函数使用值传递,和值返回。
Date Test(Date d)
{
Date temp(d);
return temp;
}
- 若使用引用
Date& Test(const Date& d)
{
//这须调用拷贝构造函数
Date temp(d);
return temp;
}
四.赋值运算符重载
4.1运算符重载
为了能使自定义类型能像内置类型一样使用运算符,同时C++为了增强代码的可读性引入了运算符重载。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数。(用于内置类型不会改变其含义)
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . 注意以上5个运算符不能重载(这个经常在笔试选择题出现)
.*(点星) ::域操作符 sizeof ?: .(点)
4.2运算符重载实现
对于内置类型使用运算符可以直接使用,对于自定义类型使用运算符都需要重载。
若我们要比较两个日期类对象的大小该如何写呢?
-
- 使用比较比较函数
class Date
{
public:
Date(int year, int month, int day)
{
}
~Date()
{
}
int _year;
int _month;
int _day;
};
bool d_less(const Date& x1, const Date& x2)
{
if (x1._year < x2._year)
{
return true;
}
else if (x1._year == x2._year && x1._month < x2._month)
{
return true;
}
else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
{
return true;
}
return false;
}
int main()
{
Date d1(2023, 5, 9);
Date d2(2023, 5, 10);
//此时看见函数名,可能导致使用的人不明白此函数的功能
cout << d_less(d1, d2) << endl;
return 0;
}
缺点:使用者看见函数名,可能不清楚此函数的功能。
- 2. 全局的operator<
bool operator<(const Date& x1, const Date& x2)
{
if (x1._year < x2._year)
{
return true;
}
else if (x1._year == x2._year && x1._month < x2._month)
{
return true;
}
else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
{
return true;
}
return false;
}
int main()
{
Date d1(2023, 5, 9);
Date d2(2023, 5, 10);
//此时使用者,一看就能知道此函数的功能。
d1 < d2;
operator<(d1, d2); //二者等价
return 0;
}
此时还有一个问题,此时类中成员变量是公有的。封装性如何保证?
解决方法:
- 重载成成员函数
- .友元(后面讲)
- 重载为类成员函数
class Date
{
public:
Date(int year, int month, int day)
{
}
//重载为类成员函数时,少一个参数
//因为调用此函数的对象的地址,传给了隐含的this指针
bool operator<(const Date& x2)
{
if (_year < x2._year)
{
return true;
}
else if (_year == x2._year && _month < x2._month)
{
return true;
}
else if (_year == x2._year && _month == x2._month && _day < x2._day)
{
return true;
}
return false;
}
~Date()
{
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 5, 9);
Date d2(2023, 5, 10);
d1 < d2;
d1.operator<(d2); //二者等价. d1的地址传给了this
return 0;
}
注意:运算符重载成全局函数需要传两个参数(没有this指针), 重载为类成员函数只需要一个参数(其中一个参数的地址传给了this)
4.3赋值运算符重载
赋值运算符只能重载成类的成员函数不能重载成全局函数。
因为:赋值运算符重载是默认成员函数(全局可能会冲突)。
void operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
int main()
{
Date d1(2023, 5, 9);
Date d2(2023, 10, 10);
d1 = d2;
return 0;
}
- 问题1:此时的赋值运算符重载不能实现连续赋值
1.内置类型
int main()
{
int i, j, k;
// 1先赋值给k,k在作为返回值赋值给j,以此往复
i = j = k = 1;
return 0;
}
- 自定义类型
此时定义的运算符重载函数的返回值不能为void。
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
int main()
{
Date d1(2023, 5, 5);
Date d2(2023, 10, 10);
Date d3(2023, 15, 15);
d1 = d2 = d3;
return 0;
}
此时,使用引用返回,不仅解决了连续赋值的问题,还可以提高程序效率。
- 问题2:d1 = d1的问题
int main()
{
Date d1(2023, 5, 5);
d1 = d1; //可通过
return 0;
}
能否允许这样赋值,是由编写程序的人决定。若不允许这种赋值:
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
- 区别赋值运算符重载和拷贝构造函数。
int main()
{
Date d1(2023, 5, 5);
Date d2(2023, 10, 10);
//已经存在的两个对象之间的拷贝构造 —— 运算符重载函数
d1 = d2;
//用一个已经存在的对象初始化另一个对象 —— 拷贝构造函数
Date d3(d2);
return 0;
}
4.4编译器默认生成的赋值运算符
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝(浅拷贝)。(注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。)
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
~Date()
{
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 5, 5);
Date d2(2023, 10, 10);
d1 = d2;
return 0;
}
注意:
- 编译其默认生成的赋值运算符重,对内置类型进行浅拷贝,自定义类型则需要调用它的赋值运算符重载。
- 若类中有涉及到资源管理则必须要自己实现运算符重载。
- 这里依然是栈的例子:
4.5运算符重载和函数重载
- 运算符重载:让自定义类型可以使用内置类型的运算符。
- 函数重载:函数名相同,参数不同。
5.前置++和后置++的重载
在C语言阶段,我们对内置类型使用前置++或者后置++对程序的效率几乎没有影响,但是在C++阶段前置++和后置++的重载的实现有很大的区别。
注意:
- 前置++和后置++重载的函数名相同。为了构成函数重载,并以示区分,后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
- 后置++效率没有前置++高,建议自定义类型多使用前置++
- 同理:前置- -和后置- -的重载也是如此
5.1区别
- 前置++:返回++之后值
Date& Date::operator+=(int day)
{
//day出现负数的情况
if (day < 0)
{
return *this -= -day;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
//前置++是返回++之后的值
Date& Date::operator++()
{
*this += 1;
return *this;
}
- 后置++:返回++之前的值
Date Date::operator++(int)
{
Date temp(*this);
temp += 1;
return temp;
}
此时,我们发现对于自定义类型来说,后置++效率没有前置++高。因为后置++比前置++多调用了两次拷贝构造函数。
6.const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,修饰*this,表明在该成员函数中不能对类的任何成员进行修改。
- 情景1
void Print()
{
cout << _year << '-' << _month << '-' << _day << endl;
}
int mian()
{
Date d1(2023, 12, 2);
d1.Print(); //可以调用
const Date d2(2023, 12, 12);
//d2.Print(); //不可调用
return 0;
}
此时d2去调用打印函数却不能调用。
因为:
解决方法:在函数名后加const, 此时this指针的类型为const Date*
void Print() const
{
cout << _year << '-' << _month << '-' << _day << endl;
}
- 情景2
bool Date::operator<(const Date& 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;
}
int main()
{
Date d1(2023, 12, 2);
const Date d2(2023, 12, 12);
d1 < d2; //可以调用
//d2 < d1; //不可以调用
return 0;
}
同理:
解决方法:在函数名后加const
bool Date::operator<(const Date& d) const
结论:
- 需要修改成员变量的函数不能加const
- 不需要修改成员变量的函数都应该加const
- 加const的好处:成员函数加了const之后,普通对象和const对象都可以调用
6.1思考题
-
const对象可以调用非const成员函数吗?
答案:不可以(权限放大) -
非const对象可以调用const成员函数吗?
答案:可以(权限缩小) -
const成员函数内可以调用其它的非const成员函数吗?
答案:不可以(权限放大) -
非const成员函数内可以调用其它的const成员函数吗?
答案:可以(权限缩小)
五.取地址及const取地址操作符重载
class Date2
{
public:
Date2* operator&()
{
return this;
}
const Date2* operator&() const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
一般情况下,我们不用自己实现取地址及const取地址操作符重载,编译器自动生成的就够用了。但是如果要写,就要写两个。
5.1使用的场景
当我们不想要别人取到可修改对象的地址的时候
class Date2
{
public:
Date2* operator&()
{
return nullptr; //返回空
}
const Date2* operator&()const
{
return this;
}
private:
int _year = 1; // 年
int _month = 1; // 月
int _day = 1; // 日
};
六.流插入和流提取运算符重载
在C++库中只实现了对内置类型的流插入和流提取运算符重载,如果需要对自定义类型进行流插入和流提取,需要自己定义运算符重载
int main()
{
Date d1(2023, 5, 5);
Date d2(2000, 1, 1);
d1.Print();
d2.Print();
return 0;
}
此时我们若想使用C++的方式来对对象进行打印和输入的话需要自己定义运算符重载。
6.1实现
- 若定义为成员函数
ostream& operator<<(ostream& out);
int main()
{
d1 << cout;
//cout << d1; //报错
return 0;
}
问题:若将流插入运算符重载为成员函数时,因为Date对象默认占用了第一个参数,就做了左操作数, 因此写为d1 << cout,这种方式和我们平时的使用习惯不符。
- 定义为全局函数
ostream& operator<<(ostream& out, Date& d)
问题:若定义为全局函数时无法访问类中的成员变量
解决方法:
- 在类中写一个公有的成员函数,返回成员变量,代替流插入时的变量
int GetMember_year()
{
return _year;
}
int GetMember_month()
{
return _month;
}
int GetMember_day()
{
return _day;
}
void operator<<(ostream& out, Date& d)
{
out << d.GetMember_year() << '-' << d.GetMember_month() << '-' << d.GetMember_day() << endl;
}
- 定义为友元函数
友元函数是声明,不考虑访问限定符,在类的任意位置声明都行将函数定义为友元函数之后,将会被类认为是"朋友",可以访问成员变量。
friend ostream& operator<<(ostream& out, Date& d);
friend istream& operator>>(istream& in, Date& d);
ostream& operator<<(ostream& out, Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
int year = 0, month = 0, day = 0;
in >> year >> month >> day;
//防止输入非法日期
//调用GetMonthDay 1.使用对象调用 2.没有对象,设为静态函数,直接调用(推荐)
if (month > 0 && month < 13 && day > 0 && day <= Date::GetMonthDay(year, month))
{
d._year = year;
d._month = month;
d._day = day;
}
else
{
cout << "非法日期" << endl;
assert(false);
}
return in;
}
6.3为什么流插入和流提取运算符重载返回值是该类型的引用?
答案:为了支持连续的流插入,返回值该流的引用。