类和对象
- 一、初始化列表
-
- [1. 构造函数体赋值](#1. 构造函数体赋值)
- [2. 初始化列表](#2. 初始化列表)
- [二、explicit 关键字](#二、explicit 关键字)
-
- [1. 单参数构造函数的隐式类型转换](#1. 单参数构造函数的隐式类型转换)
- [2. explicit 关键字](#2. explicit 关键字)
- [3. 多参数的隐式类型转换(C++11)](#3. 多参数的隐式类型转换(C++11))
- [三、static 成员](#三、static 成员)
- 四、友元
-
- [1. 友元函数](#1. 友元函数)
- [2. 友元类](#2. 友元类)
- 五、内部类
- 六、匿名对象
- 七、日期类
在前两期的 类和对象(上篇) 和 类和对象(中篇) 我们学习了有关类和对象的大部分知识,这一篇我将会带大家完善这方面的有关知识,并完成我们日期类的完整实现。
一、初始化列表
1. 构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化 ,构造函数体中的语句只能将其称为赋初值 ,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
2. 初始化列表
所以我们引入一个概念:初始化列表 ,初始化列表是每个对象的成员定义的地方,不管我们写不写,每个成员都要走初始化列表。
其用法是,以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个 "成员变量" 后面跟一个放在括号中的初始值或表达式。
例如以下日期类:
// 初始化列表
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
使用初始化列表需要注意的:
-
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。
-
C++11 支持在成员声明处给缺省值,这个缺省值也会给初始化列表,如果初始化列表没有显示给值,就用这个缺省值;如果显示给值了,就不用这个缺省值。
// 初始化列表 class Date { public: Date(int year , int month , int day) : _year(year) , _month(month) ,_day(day) {} void Print() { cout << _year << "年" << _month << "月" << _day << "日" << endl; cout << _x << endl; } private: int _year ; int _month; int _day ; int _x = 10; };
例如以上代码,_x 没有显式给值,但是它在声明处给了缺省值,这个缺省值最终也会给初始化列表定义 _x。
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const 成员变量
- 自定义类型成员(且该类没有默认构造函数时)
例如以下这段代码:
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class Date
{
public:
Date(int year, int month, int day, int& i)
: _year(year)
, _month(month)
,_aa(1)
,_refi(i)
{
// 赋值,并不是初始化
_day = day;
}
private:
// 每个成员声明
int _year;
int _month;
int _day;
// 必须定义时初始化
const int _x = 10; //const成员变量;此处是给缺省值,也可以在初始化列表中初始化
int& _refi; // 引用成员变量
A _aa; //自定义成员变量(没有默认构造)
};
int main()
{
int n = 0;
Date d1(2023, 7, 28, n);
return 0;
}
const 成员变量必须定义时初始化是因为一旦初始化就不能被修改;引用成员变量 也一样,引用成员变量是一个变量的别名,需要定义的时候就初始化;因为 _aa 是个自定义成员变量,而且它没有默认的构造函数(因为它的构造函数中没有给缺省值,所以无法调到),所以也要在定义的时候初始化;
所以以上三种类型必须在定义的时候初始化,而初始化列表就是每个成员定义的地方,所以我们要在初始化列表给它们初始化的值,也可以在声明处给缺省值(C++11支持),例如以上代码中 const 成员变量 _x 就是给了缺省值,但缺省值最终也会给初始化列表初始化。
- 能用初始化列表就用初始化初始化列,有些场景还是需要初始化列表和函数体混着用。
例如 Stack 类,以下场景:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 4)
:_array((DataType*)malloc(sizeof(DataType)* capacity))
, _size(0)
, _capacity(capacity)
{
if (_array == NULL)
{
perror("malloc申请空间失败!!!");
return;
}
memset(_array, 0, sizeof(DataType) * _capacity);
}
private:
DataType* _array;
int _size;
int _capacity;
};
- 对于自定义类型成员变量,一定会先使用初始化列表初始化。
例如以下代码,时间类显式写了构造函数并用初始化列表初始化;
class Time
{
public:
Time(int hour = 0)
:_hour(hour)
{}
private:
int _hour;
};
class Date
{
public:
Date(int day)
:_day(day)
{}
private:
int _day;
Time _t;
};
int main()
{
Date d(1);
}
我们在日期类中声明时间类的自定义类型,但是不在日期类的初始化列表初始化它,我们观察 _t 对象中的成员变量的值被初始化为什么:
通过调试窗口可以观察到,它会调它的构造函数并走它的初始化列表,并使用缺省值 0 初始化;
那么我们在日期类的初始化列表给它初始化呢?我们也不妨试试,结果如下:
如上图,我们在日期类的初始化列表中给它初始化,它还是会走时间类的初始化列表,但是没有用缺省值进行初始化,而是用我们给定的值进行初始化。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
例如以下的日期类,我们观察 _a1 和 _a2 的结果会是什么呢?
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
private:
int _a2;
int _a1;
};
int main()
{
A aa(1);
}
我们对它进行打印观察:
结果是 1 和 随机值 ,就是因为初始化列表是按照声明的顺序进行初始化,先对 _a2 进行初始化,此时 _a1 还没有被初始化,所以用 _a1 对 _a2 进行初始化是随机值;然后再对 _a1 进行初始化,此时 _a1 被初始化为 1.
二、explicit 关键字
1. 单参数构造函数的隐式类型转换
构造函数不仅可以构造与初始化对象,对于单个参数 或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
我们在以前也学过隐式类型转换,例如一个 int 类型的值赋给 double 类型,中间会发生隐式类型转换;同样道理,对象的构造函数也会完成隐式类型的转换。
单个参数构造函数的隐式类型转换,例如以下 A 类:
class A
{
public:
A(int i)
:_a(i)
{
cout << "A(int i)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
我们可以对对象进行这样的实例化:A aa1(1);
这是我们已经知道的,但是我们还可以这样对对象实例化:A aa2 = 2;
,这就是单参数构造函数的隐式类型转换。
对于我们的理解,A aa2 = 2;
应该是用 2 调用 A 构造函数生成一个临时对象,再用这个对象去拷贝构造 aa2,但是编译器会优化,优化用 2 直接构造对象 aa2 ,例如以下代码,我们对对象实例化观察对象调用了哪些函数:
int main()
{
A aa1(1);
cout << "-----------------------------------" << endl;
A aa2 = 2;
cout << "-----------------------------------" << endl;
return 0;
}
结果如下图:
我们观察可以看出,它们两个是等价的,所以说明了编译器对 aa2 对象的实例化进行了优化。
对于除第一个参数无默认值其余均有默认值,例如以下日期类:
class Date
{
public:
// 虽然有多个参数,但是创建对象时后两个参数可以不传递
Date(int year, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month = 1, int day = 1)" << endl;
}
Date& operator=(const Date& d)
{
cout << "Date& operator=(const Date& d)" << endl;
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
我们对对象进行这样的实例化:Date d1(2022);
这对我们来说也没问题,但是我们这样 d1 = 2023;
呢,编译器又会做什么优化处理呢?我们执行观察一下:
我们可以观察到,在 d1 = 2023;
的时候,我们用一个整型变量给日期类型对象赋值, 实际编译器背后会用 2023 构造一个无名对象,最后用无名对象给 d1 对象进行赋值,这也是编译器的单参数构造函数的隐式类型转换。
2. explicit 关键字
对于上述代码可读性不是很好,所以C++中可以用 explicit 修饰构造函数,将会禁止构造函数的隐式转换。
例如上述的日期类中,我们在构造函数前用 explicit 关键字修饰,那么d1 = 2023;
这段代码就不会发生单参数构造函数的隐式类型转换,例如:
explicit Date(int year, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
编译器会报以下错误:
同理,我们对上面的 A 类的构造函数也用 explicit 关键字修饰,如:
explicit A(int i)
:_a(i)
{
cout << "explicit A(int i)" << endl;
}
我们对对象进行 A aa2 = 2;
的实例化时,编译器也会报类似的错误:
因为 explicit 修饰构造函数,禁止了单参构造函数类型转换的作用。
3. 多参数的隐式类型转换(C++11)
在 C++11 中,C++11 支持多参数的隐式类型转换,例如以下的 B 类:
class B
{
public:
B(int b1, int b2)
:_b1(b1)
, _b2(b2)
{
cout << "B(int b1, int b2)" << endl;
}
B(const B& b)
:_b1(b._b1)
,_b2(b._b2)
{
cout << "B(const B& b)" << endl;
}
private:
int _b1;
int _b2;
};
我们有以下的实例化对象:
int main()
{
B bb1(1, 1);
B bb2 = { 2, 2 };
const B& ref2 = { 3,3 };
return 0;
}
其中 B bb1(1, 1);
是正常的实例化对象,没有进行隐式类型的转换;而 B bb2 = { 2, 2 };
和 const B& ref2 = { 3,3 };
则进行了多参数的隐式类型转换,我们执行程序观察结果:
如上图,三个实例化都是只是调用了构造函数,说明编译器对其进行了优化。
当我们对构造函数加上 explicit 关键字后,编译器就无法对 B bb2 = { 2, 2 };
和 const B& ref2 = { 3,3 };
进行多参数的隐式类型转换了。
三、static 成员
概念:用 static 修饰类的成员变量,称之为静态成员变量 ;用 static 修饰的成员函数,称之为静态成员函数 。静态成员变量 一定要在类外进行初始化。
例如我们需要统计一个类中创建出了多少对象,并计算正在使用的还有多少个对象;我们可能会想到以下的思路:
// 累积创建了多少个对象
int n = 0;
// 正在使用的还有多少个对象
int m = 0;
class A
{
public:
A()
{
++n;
++m;
}
A(const A& t)
{
++n;
++m;
}
~A()
{
--m;
}
private:
};
// A& Func(A& aa)
A Func(A aa)
{
return aa;
}
我们创建了两个全局变量,m 和 n 分别统计正在使用的还有多少个对象和累积创建了多少个对象 ;并在构造函数、拷贝构造函数和析构函数体内做相应统计;
这种方法虽然能统计出来,但是 m 和 n 是全局变量,说明我们可以随意修改数据,变得不安全,所以这种方法不好。
所以就有静态成员变量这个概念,我们在类中定义静态成员变量,静态成员变量属于所有类的对象,属于整个类。 例如我们将上面的 m 和 n 声明为静态成员变量:
class A
{
public:
A()
{
++n;
++m;
}
A(const A& t)
{
++n;
++m;
}
~A()
{
--m;
}
// 静态成员函数的特点:没有this指针
static int GetM()
{
// x++; // 不能访问非静态,因为没有this
return m;
}
static int GetN()
{
return n;
}
private:
// 静态成员变量属于所有 A 类的对象,属于整个类
// 声明
// 累积创建了多少个对象
static int n;
// 正在使用的还有多少个对象
static int m;
int _x = 0;
};
// 定义
int A::n = 0;
int A::m = 0;
如上代码,静态成员的变量需要在类外定义 ;我们在类中也定义了静态成员函数 ,静态成员函数的特点是没有 this 指针,所以它不能访问非静态成员变量 ,假设我们声明了一个 _x 成员变量,GetM
函数是无法访问 _x 的;但是它可以访问静态成员的变量;例如如果我们想要拿到 m 和 n 的值,可以通过函数 GetM
和 GetN
拿到,而这两个静态成员函数都是返回静态成员变量。
那么我们为什么不将这两个函数直接定义成成员函数呢?因为我们需要统计的是累计创建了多少对象,而需要访问成员函数就必须得实例化一个对象出来,假如我们没有实例化对象,就不能得到 m 和 n 的值;而定义成静态成员函数只需要指定类域即可得到 m 和 n 的值,例如以下代码:
int main()
{
// 匿名对象
A();
A();
// error
//cout << aa1.GetM() << ' ' << aa1.GetN() << endl;
// 可以访问
cout << A::GetM() << ' ' << A::GetN() << endl;
// 实例化对象
A aa1;
cout << aa1.GetM() << ' ' << aa1.GetN() << endl;
cout << A::GetM() << ' ' << A::GetN() << endl;
return 0;
}
以上代码中,我们实例化了两个匿名对象(下面的内容会讲到匿名对象),匿名对象也会调用构造函数,但是我们如果不将 GetM
和 GetN
函数定义成静态成员函数,就无法得到 m 和 n 的值,因为没有实例化的对象,匿名对象的生命周期只有一行;只有我们实例化 aa1 对象才可以得到 m 和 n 的值,但是这样又调用了一次构造函数,并不是我们想要的结果。
四、友元
1. 友元函数
我们在运算符重载中,还有两个运算符没有重载:流插入和流提取。
假设我们在类内部实现流插入和流提取运算符重载:
// 流插入重载
void operator<<(ostream& out)
{
out << _year << '.' << _month << '.' << _day << endl;
}
// 流提取重载
void operator>>(istream& in)
{
in >> _year >> _month >> _day;
}
这时候我们要注意,this 指针抢占了成员函数的第一个参数的位置,导致 cout 参数变成在第二个参数位置,参数的顺序不一样,所以我们在使用中应该是 d << cout
, 虽然可以使用,但是不符合我们使用的习惯和价值。
所以要将 operator<< 重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。
我们先看如何使用:
class Date
{
// 友元函数
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
//日期类的构造函数
Date(int year = 2023, int month = 7, int day = 29)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
// 流插入重载
inline ostream& operator<<(ostream& out, const Date& d)
{
cout << 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;
}
如上代码,流插入和流提取重载是放在全局域中,此时我们需要在类的内部声明友元函数,可以在任意位置,此处我们在最上面声明两个重载的友元,此时两个重载函数就可以正常访问类的成员变量,从而不受访问限定符的影响,cout 的参数又可以在第一个形参位置,这才是符合我们的要求。这里使用 ostream& 和 istream& 类型返回是为了支持连续插入和提取。
最后总结,友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加 friend 关键字。
2. 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
例如,这里有个时间类,在时间类中声明日期类为友元类:
class Time
{
// 声明日期类为时间类的友元类,则在日期类中就直接访问 Time 类中的私有成员变量
friend class Date;
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
在日期类中定义时间类的自定义类型,访问时间类的成员变量:
class Date
{
public:
Date(int year = 2023, int month = 7, int day = 29)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
上述代码是没有问题的,在日期类中可以直接访问时间类的成员变量,因为日期类是时间类的友元 ,但是在时间类中却无法访问日期类的成员变量,因为时间类不是日期类的友元。
最后对友元进行总结,友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
五、内部类
概念: 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。 内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意: 内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
例如以下两个类,A类和B类,B类是A类的内部类:
class A
{
public:
class B
{
public:
void FuncB()
{
A aa;
aa._a = 1;
}
private:
int _b;
};
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
A类受B类域和访问限定符的限制,B类可以访问A类的成员变量,A类却不能访问B类的成员变量;其实他们是两个独立的类;内部类默认就是外部类的友元类
如果计算上面代码中A类的大小,会是多少呢?
int main()
{
cout << sizeof(A) << endl;
// 实例化 aa
A aa;
// 实例化 bb1
A::B bb1;
return 0;
}
结果如下,虽然B类是A类的内部类,但是实际上它们是两个独立的类,所以没有计算B类的大小,A类的大小只包括成员变量 _a 的大小;
六、匿名对象
以前我们定义的对象大多数都是有名对象,有名对象的特点是生命周期在当前局部域 ;而匿名对象的特点是生命周期只在定义的那一行。
例如有一个A类,匿名对象的生命周期只有这一行,下一行它就会自动调用析构函数:
// 有名对象
A aa(6);
// 匿名对象
A(7);
但是我们不可以这样定义对象:
A aa1();
因为编译器无法识别这是一个函数声明,还是对象定义;
七、日期类
下面我们用前面所学的知识完善我们的日期类,我们将声明和定义分开写在两个文件中,声明写在 .h 文件,定义写在 .cpp 文件中:
声明:
#pragma once
#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:
// 获取月份的天数
int GetMonthDay(int year, int month)
{
static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int day = days[month];
// 二月并且是闰年
if (month == 2
&& ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
day += 1;
}
return day;
}
//检查日期是否合法
bool CheakDate()
{
if (_year >= 1
&& _month > 0 && _month < 13
&& _day > 0 && _day <= GetMonthDay(_year, _month))
{
return true;
}
return false;
}
//日期类的构造函数
Date(int year = 2023, int month = 7, int day = 29)
:_year(year)
, _month(month)
, _day(day)
{
assert(CheakDate());
}
void Print() 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;
bool operator<=(const Date& d) const;
Date operator+(int day) const;
Date& operator+=(int day);
Date operator-(int day) const;
Date& operator-=(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;
};
// 流插入重载
inline ostream& operator<<(ostream& out, const Date& d)
{
cout << d._year << '.' << d._month << '.' << d._day << endl;
return out;
}
// 流提取重载
inline istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
assert(d.CheakDate());
return in;
}
函数的实现:
#include "ClassAndObj_3.h"
//打印日期
void Date::Print() const
{
cout << _year << '.' << _month << '.' << _day << endl;
}
// == 运算符重载
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);
}
// > 运算符重载
bool Date::operator>(const Date& d) const
{
if (_year > d._year
|| (_year == d._year && _month > d._month)
|| (_year == d._year && _month == d._month && _day > d._day))
{
return true;
}
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) || (*this == d);
}
// + 运算符重载
Date Date::operator+(int day) const
{
Date ret(*this);
ret += day;
return ret;
}
// += 运算符重载
Date& Date::operator+=(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-(int day) const
{
Date ret(*this);
ret -= day;
return ret;
}
// -= 运算符重载
Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += -day;
}
_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_month, _year);
}
return *this;
}
// 前置++ 重载
Date& Date::operator++()
{
return *this += 1;
}
// 后置++ 重载
Date Date::operator++(int)
{
Date ret(*this);
*this += 1;
return ret;
}
// 前置-- 重载
Date& Date::operator--()
{
return *this -= 1;
}
// 后置-- 重载
Date Date::operator--(int)
{
Date ret(*this);
*this -= 1;
return ret;
}
//计算两个日期之间相差的天数
int Date::operator-(const Date& d) const
{
int flag = 1;
Date max = *this;
Date min = d;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
min++;
n++;
}
return n * flag;
}
如上我们的日期类就基本完善了。这也意味着我们的类和对象的知识也就学完啦,感觉有帮助的小伙伴点个赞吧~