@TOC
C++类和对象(二)
这节才是类和对象的精华,其中有非常多的细节要去进行处理,虽然这块骨头比较难啃,但是还是要硬着头皮去搞懂,迈过类和对象这一关,就为后面的C++学习打下了非常坚实的基础。迈不过,可以说后面的C++基本没得玩了。
类的六个默认成员函数
如果一个类中什么成员都没有,简称为空类。
class Date {};
但是空类真的是什么都没有吗?答案其实是否定的,一个类在什么都不写时,默认会生成6个默认成员函数。
构造函数:
概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次
那么为什么要有默认成员函数呢?
假设有一个日期类,我们创建一个日期类的对象,第一步必须是进行初始化,后面才可以使用,否则在很多情况下会出错。但是初始化函数每次都需要写,而且每次创建一个对象想要去使用第一步都得初始化,第一是比较麻烦,第二很容易忘记,所以为了解决这样的难题,类中引入了构造函数的概念。
class Date
{
public:
void Init(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2023, 4, 27);
//...使用
Date d2;
d2.Init(2022, 2, 22);
//...
return 0;
}
C语言中这种方式使用起来既麻烦又容易忘记。下面看构造函数怎么使用。
特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
- 函数名和类名相同
- 无返回值
- 对象实例化时编译器自动调用构造函数
- 构造函数可以重载
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
- 编译器默认生成的成员函数只对自定义类型初始化,内置类型成员不作处理,C++11中规定,内置类型成员变量在声明时可以给缺省值。
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数
这几个特征必须要理解透彻,一个都不能少,才可以说是理解了构造函数。
首先函数名和类名相同,无返回值,编译器自动调用这些都是规定,主要是将下面几个理解并加以验证。
实践验证
- 无参构造函数
我们先写一个无参构造函数看一下是否会自动调用:
class Date
{
public:
Date()
{
cout << "hello structure funtion!!!" << endl;
}
private:
int _year;
int _month;
int day;
};
int main()
{
Date d1;
return 0;
}
可以看到,答案是肯定的,确实是自动调用。所以我们就可以在这个函数里面进行初始化的操作。
- 编译器自动生成的无参默认构造函数
如果类中我们没有定义显式构造函数,那么编译器会自动生成一个无参的默认构造函数。那么该函数又有什么不同呢?我们可以在一个类中同时定义内置类型成员和自定义类型成员,看一下此函数的作用。
struct Stack
{
int* _a;
int _top;
int _capacity;
};
class Date
{
public:
void Print()
{
cout << _year << ' ' << _month << ' ' << _day << endl;
cout << _s1._a << ' ' << _s1._capacity << ' ' << _s1._top << endl;
}
private:
int _year;
int _month;
int _day;
Stack _s1;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
看到这个结果我相信你可能很迷惑,这个编译器默认生成的函数似乎没有什么作用,它也没有完成初始化的工作嘛。
但是事实真的是这样吗?当然不是,否则还生成这个函数做什么。
C++规定:对于内置类型成员变量不做初始化,对于自定义类型成员一定要做初始化。
为什么我们看到的是这个现象呢?答案当然是编译器的问题,不同的编译器,不同的版本实验起来现象可能不同,下面是我的编译器当前版本:
我们编译器实验起来是这样的情况,当然大家也可以动手去尝试一下。
- C++补丁的作用
正是因为默认成员函数不做初始化这一条规定,埋下了很多的坑,所以后来C++11中又打了补丁来解决这一问题。
C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给缺省值。
所以在仅有内置类型成员的情况下,自己实现一个无参构造函数和直接给缺省值效果是一样的。
第一种情况:
class Stack
{
int* _a = nullptr;
int _top = 0;
int _capacity = 4;
};
//等价于
class Stack
{
public:
Stack()
{
_a = nullptr;
_top = 0;
_capacity = 4;
}
private:
int* _a;
int _top;
int _capacity;
};
第二种情况:
在全部都是自定义类型成员的情况下,也不需要我们自己写构造函数。
- 带参的构造函数
除了上述两种特殊情况,在绝大多数情况下,都是需要我们自己写的,我们通常要写的是带参的构造函数,而且最好给缺省值
class TreeNode
{
public:
TreeNode(int val = 0)
{
_left = nullptr;
_right = nullptr;
_val = val;
}
private:
TreeNode* _left;
TreeNode* _right;
int _val;
};
int main()
{
//创建一个树节点,很灵活的创建
TreeNode tr(5);
TreeNode tr(6);
TreeNode tr(7);
TreeNode tr(8);
return 0;
}
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
析构函数
概念
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
特性
析构函数也是特殊的成员函数,特征:
- 析构函数是类名前加字符
~
- 无参数,无返回值类型
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,编译器自动调用析构函数
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_a = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _a)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_top = 0;
}
void Push(DataType data)
{
//CheckCapacity()
_a[_top++] = data;
}
~Stack()
{
cout << "you can see me!" << endl;
if (_a)
{
free(_a);
_a = NULL;
_capacity = 0;
_top = 0;
}
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack s1;
return 0;
}
析构函数比起构造就简单的多了,大体上可以分为三种情况:
- 有自定义类型成员,进行了动态申请资源,需要析构函数来进行资源的清理
- 全是内置类型成员,没有动态申请的资源,不需要析构函数
- 需要释放资源的成员都是自定义类型成员,本身就能够释放资源,不需要析构函数
例如以下三种情况:
//有自定义类型成员,进行了动态申请资源,需要析构函数来进行资源的清理
class Stack
{
private:
int* _a;
int _top;
int _capacity;
};
//全是内置类型成员,没有动态申请的资源,不需要析构函数
class Date
{
int _year;
int _month;
int _day;
};
//需要释放资源的成员都是自定义类型成员,本身就能够释放资源,不需要析构函数
class Queue
{
private:
Stack pushst;
Stack popst;
};
当然了,不是只有这三种情况,只有理解了析构函数的使用场景,灵活使用不是难事。
拷贝构造函数
概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。注意参数要用const修饰,否则会引发一些不必要的麻烦。
设计拷贝构造函数的原因就是我们有时会有场景,需要创建一个和已存在对象一模一样的对象。但是对象通常内部数据比较复杂,所以有些细节需要特别注意,所以不能像内置类型那样简单的浅拷贝。
特性
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
- 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。ps:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
- 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用
- 在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的
class Stack
{
private:
int* _a;
int _top;
int _capacity;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
Stack st1;
};
int main()
{
Date d1;
// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
Date d2(d1);
return 0;
}
- 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝
如果是使用默认生成的拷贝构造函数则仅仅是浅拷贝,此时如果类中申请了资源,并且将指针或者引用直接浅拷贝过去,在最终析构函数时,就会将申请资源的空间释放两次,此时就会出问题。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
//注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;
//一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型参数,用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
.* :: sizeof ?: .
注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
先来简单看一下什么是运算符重载,举个简单的例子:
// 全局的operator
// 比较日期大小
class Date
{
public:
Date(int year = 0,int month=0,int day=0)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
bool operator < (const Date& d1,const Date& d2)
{
if (d1._year < d2._year)
return true;
else if (d1._year == d2._year && d1._month < d2._month)
return true;
else if (d1._year == d2._year && d1._month == d2._month && d1._day < d2._day)
return true;
else
return false;
}
int main()
{
Date d1(2023, 12, 10);
Date d2(2022, 12, 10);
cout << (d1 < d2) << endl;
cout << (d2 < d1) << endl;
return 0;
}
但是这样写出的代码无疑看起来怪怪的,在用opeartor
运算符重载的时候,访问了类里面的成员,实际上我们的成员变量本应该是私有的,这里为了演示才用了公有,所以这样我们当然不会这样写,那么怎么解决呢?自然而然地就可以想到将重载函数写到类里面。但是要写到类里面又有一些其他的细节,我们看下面:
直接复制粘贴到类里面就会发现编译器报错了,
编译器告诉我们参数太多,是否有点莫名其妙,但是仔细想想,参数有几个呢?真的是两个吗?类的内部成员函数的this
指针可不要忘了,没错,此时参数确实多了,那么怎么改呢?
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
{
_year = year;
_month = month;
_day = day;
}
bool 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;
else
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 12, 10);
Date d2(2022, 12, 10);
cout << (d1 < d2) << endl;
cout << d1.operator<(d2);
return 0;
}
赋值运算符重载
赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
赋值运算符只能重载成类的成员函数不能重载成全局函数,原因:用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝
ps:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现
- 有返回值是为了支持连续赋值
class Date
{
public:
Date(int year = 0, int month=0, int day=0)
{
_year = year;
_month = month;
_day = day;
}
/*注意这段代码中的细节*/
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 5, 3);
Date d2(2020, 3, 3);
Date d3(2022, 6, 6);
d1 = d2 = d3;
return 0;
}
- 默认赋值运算符重载
内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值
class Time
{
public:
Time()
{
_hour = 10;
_minute = 10;
_second = 10;
}
Time& operator=(const Time& t)
{
if (this != &t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
return *this;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 0, int month=0, int day=0)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date d1(2020, 2, 2);
Date d2(2021, 3, 3);
d1 = d2;
return 0;
}
- 如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现
例如日期这样的只需要浅拷贝就可以解决的问题,使用默认赋值运算符重载是没有任何问题的,但是一旦涉及到资源的申请,例如栈这种类,就必须自己动手写,通过深拷贝来解决问题。
前置++和后置++重载
前置++和后置++怎么重载呢,这两个其实都是比较常用的,而且用它的特性的地方还是非常多的。直接写肯定是不行,都是operator++
怎么区分呢,最后没有办法只能做一点操作用来区分,前置++不变,后置++要都写个参数类型int
,来区分两者。
C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//前置++
Date& operator++()
{
_day += 1;
return *this;
}
//后置++
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
private:
int _year ;
int _month ;
int _day ;
};
其实从效率也可以看出来,后置++需要额外创建变量,效率显然是不及前置++的,所以选择了让后置++做出一点牺牲。
const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
可以用下面代码来验证一下一些const和非const修饰的函数的问题,其实无非就是一个权限的问题,权限只能缩小或者平移,但是绝对不能被放大。
const对象可以调用非const成员函数吗?
用const修饰的对象是不可被修改的,所以不能调用非const修饰的函数。
- 非const对象可以调用const成员函数吗?
可以。
const成员函数内可以调用其它的非const成员函数吗?
不可以。
非const成员函数内可以调用其它的const成员函数吗?
可以。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void Print() const
{
cout << "Print()const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
void Test()
{
Date d1(2022, 1, 13);
d1.Print();
const Date d2(2022, 1, 13);
d2.Print();
}
int main()
{
Test();
return 0;
}
可以用一段代码来验证一下。
当然,其实用const
的好处并不在于这些,否则我们直接全部都不加const
不就好了,直接给最大权限,能读能写。其实有时候const
还是非常必要的,权限给的大了,有时候未必是好事,例如下面情况:
所以那些不需要改变变量的函数,const能加就尽量加上比较好,const修饰和非const修饰就都可以调用。
取地址操作符重载
通常分两种对对象的取地址,const
对象的取地址,非const
对象的取地址
class Date
{
public:
Date& operator& ()
{
return *this;
}
const Date& operator&() const
{
return *this;
}
private:
int _year;
int _month;
int _day;
};
这两个一般不需要自己写,编译器会自动生成,除非某种非常特殊的情况,例如想让别获得指定内容。
日期类的实现
到这里,类和对象二就算暂告一段落了,当然其实还有类和对象三,(狗头),到这里已经差不多85%的内容了吧,类和对象这块确实是个大杂烩,很多很杂,但是走过一段基础打好,后面的学习就会轻松不少,最后就将日期类的完整代码放在这里,有需要的可以自行复制。
Date.cpp
:
#include "Date.h"
Date::Date(int year = 0, int month = 0, int day = 0)
{
_year = year;
_month = month;
_day = day;
}
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date& Date::operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
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;
else
return false;
}
bool Date::operator==(const Date& d) const
{
if (_year == d._year && _month == d._month && _day == d._day)
return true;
else
return false;
}
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);
}
bool Date::operator!=(const Date& d) const
{
return !(*this == d);
}
//两处优化
int Date::GetMonthDay(const int year, const int month)
{
static int date[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 date[month];
}
Date& Date::operator+=(const int 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+(const int day) const
{
Date temp(*this);
temp += day;
return temp;
}
Date& Date::operator++()
{
*this += 1;
return *this;
}
Date Date::operator++(int)
{
Date temp(*this);
*this += 1;
return temp;
}
Date& Date::operator--()
{
*this -= 1;
return *this;
}
Date Date::operator--(int)
{
Date temp(*this);
*this -= 1;
return temp;
}
//Date& Date::operator--()
//{
// _day -= 1;
// if (_day < 1)
// {
// _month -= 1;
// _day += GetMonthDay(_year, _month);
// if (_month < 1)
// {
// _year -= 1;
// _month = 12;
// }
// }
// return *this;
//}
Date& Date::operator-=(const int day)
{
if (day > 0)
{
return *this += day;
}
_day -= day;
while (_day < 1)
{
--_month;
if (_month < 1)
{
_month = 12;
--_year;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(const int day) const
{
Date temp(*this);
temp -= day;
return temp;
}
int Date::operator-(const Date& d) const
{
Date max = *this;
Date min = d;
int flag = 1;
if (max < min)
{
max = d;
min = *this;
flag = -1;
}
int count = 0;
while (min != max)
{
++min;
++count;
}
return count * flag;
}
ostream& operator<<(ostream& out, Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
cout << "Please Enter date:>" << endl;
int year, month, day;
in >> year >> month >> day;
if (month > 0 && month < 13
&& day > 0 && day <= d.GetMonthDay(year, month))
{
d._year = year;
d._month = month;
d._day = day;
}
else
{
cout << "非法日期" << endl;
assert(false);
}
return in;
/*in >> d._year >> d._month >> d._day;
return in;*/
}
Date.h
:
#pragma once
#include <assert.h>
#include <iostream>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
//构造函数
Date(int year, int month, int day);
//拷贝构造
Date(const Date& d);
//赋值运算符重载
Date& operator=(const Date& d);
//析构
~Date() {};
//打印
void Show()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
int GetMonthDay(const int year, const int month);
// < 运算符重载
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;
// != 运算符重载
bool operator!=(const Date& d) const;
//日期+=天数
Date& operator+=(const int day);
//日期+天数
Date operator+(const int day)const;
//日期-天数
Date operator-(const int day)const;
//日期-=天数
Date& operator-=(const int day);
//前置++
Date& operator++();
//后置++
Date operator++(int);
//前置--
Date& operator--();
//后置--
Date operator--(int);
//日期-日期,返回天数
int operator-(const Date& d)const;
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, Date& d);
istream& operator>>(istream& in, Date& d);