文章目录
再谈构造函数
构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;//只能是说是赋初始值,不能说初始化
_month = month;
_day = day;
_year = 100;//再次赋值
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。于是便有了接下来的初始化列表。
初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟
一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
//,_day(100)err,已经初始化
{}
private:
int _year;
int _month;
int _day;
};
【注意】
1: 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2: 类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量(引用在定义时必须初始化),
- const成员变量(const变量具有常性,无法修改,必须在定义时初始化),
class Date
{
public:
// Date(int year, int month, int day)//err,无法初始化引用和const对象,必须使用初始化列表
// {
// _year = year;
// _month = month;
// _day = day;
// _year = 100;
// _rday = _day;
// _x = _day;
// }
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
,_rday(_day)
,_x(_day)
{}
private:
int _year;
int _month;
int _day;
int& _rday;
const int _x;
};
- 自定义类型成员(且该类没有默认构造函数时)
- 默认构造函数是不需要传参的,如无参的或者全缺省的构造函数才叫做默认构造函数。
typedef int DataType;
class Stack
{
public:
//Stack(size_t capacity = 10)//默认构造函数不需要传参
Stack(size_t capacity)//不提供默认构造函数
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
class My_Queue
{
public:
My_Queue(int n)//必须显示去写构造函数
:_pushst(n)
,_pophst(n)
{}
//My_Queue(int n)//err,不允许这样,只能走初始化列表
//{
// _pushst(n);
// _pophst(n);
//}
private:
Stack _pushst;
Stack _pophst;
};
int main()
{
My_Queue q(10);
return 0;
}
如上面的代码,有两个类,一个Stack类和My_Queue类,Stack类作为My_Queue类的成员,而Stack类是没有默认构造函数的,只有一个需要传参的构造函数,所以Stack的对象需要传参才能进行构造,这也就导致无法构造My_Queue的对象。
解决方法:
必须手写一个My_Queue的构造函数,并且将Stack类需要的参数由My_Queue的对象传给Stack的构造函数。My_Queue类的构造函数必须写成初始化列表的方式。
3:所以建议以后写构造函数时尽量写成初始化列表,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
4: 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
看看下面_a1,_a2是多少
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main() {
A aa(1);
aa.Print();
}
先声明_a2,再声明_a1,所以初始的顺序为_a2,再到_a1。所以_a1是1,_a2为随机值。
explicit关键字
函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值
的构造函数,还具有类型转换的作用。
以除第一个参数无默认值其余均有默认值的构造函数为例。
class Date
{
public:
Date(int year, int month = 4, int day = 21)
: _year(year)
, _month(month)
, _day(day)
{}
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1 = 2024;
d1.Print();
}
int main()
{
Test();
return 0;
}
在早期的编译器中,当编译器遇到Date d1 = 2024这句代码时,会先构造一个临时对象,再用临时对象拷贝构造d1;但是现在的编译器已经做了优化,当遇到Date d1 = 2024这句代码时,会按照Date d1(2024)这句代码处理,这就叫做隐式类型转换。
在语法上,代码中Date d1 = 2024等价于以下两句代码:
Date tmp(2024); //先构造
Date d1(tmp); //再拷贝构造
实际上,类型转化也是隐式类型转化
int a = 1;
double b = a; //隐式类型转换
在这个过程中,编译器会先构建一个double类型的临时变量接收a的值,然后再将该临时变量的值赋值给b。这就是为什么函数可以返回局部变量的值,因为当函数被销毁后,虽然作为返回值的变量也被销毁了,但是隐式类型转换过程中所产生的临时变量并没有被销毁,所以该值仍然存在。
但是,对于单参数的自定义类型来说,Date d1 = 2021这种代码的可读性不是很好,我们若是想禁止单参数构造函数的隐式转换,可以用关键字explicit来修饰构造函数。
static成员
概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用
static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
特性
1.静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。
class Test
{
private:
static int _n;
};
int Test::_n = 0;
int main()
{
Test t;
cout << sizeof(t) << endl;
return 0;
}
我们可以计算一下Test对象的大小来验证一下 静态成员存放在静态区。
2.静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明。
注意:这里静态成员变量_n虽然是私有,但是我们在类外突破类域直接对其进行了访问。这是一个特例,不受访问限定符的限制,否则就没办法对静态成员变量进行定义和初始化了。
3.类静态成员即可用 类名::静态成员或者对象静态成员来访问。
class Test
{
public:
static int GetN()
{
return _n;
}
private:
static int _n;
};
int Test:: _n = 1;
int main()
{
Test t;
cout << sizeof(t) << endl;
t.GetN(); //通过对象调用成员函数进行访问
Test().GetN();//通过匿名对象调用成员函数进行访问
Test::GetN();//通过类名调用静态成员函数进行访问
return 0;
}
当成员属性为私有时,提供一个获取_n的函数GetN来获取,若是访问属性为公共时,则无需提供获取函数,直接按上面的方法实现就可以。
4.静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
- 但是非静态成员函数是可以访问静态成员函数的
5.静态成员也是类的成员,受public、protected、private 访问限定符的限制。
友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以
友元不宜多用。
友元分为:友元函数和友元类
友元函数
我们都知道C++的<<,>>可以自动识别类型,不再像C语言那样需要指明类型,看起来很智能很神奇。其实这里是重载了运算符<<,(>>同理)所以我们只需要去查看一下<<相关文档就可以揭开他的庐山真面目。流提取<<
之所以能够自动识别类型,是因为库里面已经写好了运算符<<对内置类型的重载,所以遇到对应的内置类型时就会调用对应的opeator<<(),那么我们能不能直接用cout<<输出一个自定义类型的对象呢?当然可以
ostream& operator<<(ostream& out)
{
out << _year << '/' << _month << '/' << _day << endl;
return out;
}
- 这里的out是cout的别名,在这个函数里随便取(理想情况下out为左操作数的别名:cout<<d1)。
- 这里返回的out是为了能连续输出。
Date d1;
//cout << d1;err
d1 << cout;
我们确实做到了自定义类型对象的直接输出,但好像形式与我们平时写的有点不一样
- 由于我们在类内实现的该函数,该函数的第一个参数被this指针抢占了,ostream类型的对象只能作为第二个参数,所以运算符<<的左侧必须是this指针类型的(也就是日期类(Date*this)),ostream的对象就只能在<<的右侧。也就是d1<<cout的形式了。
那么有什么办法解决这个问题吗?
友元函数就能很好解决这个问题:
重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问私有成员,此时就需要友元来解决。operator>>同理。
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在
类的内部声明,声明时需要加friend关键字。
class Date
{
// 友元函数的声明
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 2024, int month = 4, int day = 22)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
// <<运算符重载
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day<< endl;
return out;
}
// >>运算符重载
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
写成全局函数就没有this指针了, operator>>的第一个参数就可以是ostream的对象了。
总结:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。 - 友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C时A的友元。 - 友元关系不能继承。
class A
{
// 声明B是A的友元类
friend class B;
public:
A(int n = 0)
:_n(n)
{}
void Print()
{
cout << "test" << endl;
}
private:
int _n;
};
class B
{
public:
void Test(A& a)
{
// B类可以直接访问A类中的私有成员变量
cout << a._n << endl;
a.Print();
}
};
int main()
{
B b;
A a;
b.Test(a);
return 0;
}
内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void test(const A& a)
{
cout << k << endl;//直接访问,无需指定类名
cout << a.h << endl;//可以访问私有
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.test(A());
A a;
cout << sizeof(a) << endl;//a对象的大小不包括b
return 0;
}
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访
问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
匿名对象
匿名对象即创建没有具体名字的对象,他的生命周期只在匿名对象所在行数
class A
{
public:
A(int a=0,int b=0)
:_a(a)
,_b(b)
{
cout << "A(int a=0,int b=0)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
void Print() const
{
cout << _a << " " << _b << endl;
}
private:
int _a;
int _b;
};
int main()
{
A();
int a = int();
cout << a << endl;
return 0;
}
如上面的A(), 我们可以这么定义匿名对象,匿名对象的特点不用取名字,但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数。
当然,也有延长匿名对象生命周期的办法,那就是引用。
const A& ra = A();
ra.Print();
ra是A()匿名对象的别名,将匿名对象的生命周期变为引用ra的生命周期 。可以看到直到程序运行结束才会去调用析构函数。
我们看到匿名对象对内置类型也是支持匿名对象的,而且会把a初始化为0,所以我们就可以这样写默认构造函数。
class A
{
public:
A(int a= int(),int b= int())
:_a(a)
,_b(b)
{
//cout << "A(int a= int(),int b= int()" << endl;
}
~A()
{
//cout << "~A()" << endl;
}
void Print() const
{
cout << _a << " " << _b << endl;
}
private:
int _a;
int _b;
};
int main()
{
int a = int();
cout << a << endl;
A b;
b.Print();
return 0;
}
在默认构造函数里,a,b的初始值我们直接用匿名对象来进行初始化。现在看着有点鸡肋,但在容器中会频繁使用。
再次理解类和对象
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现
实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创
建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:
- 用户先要对现实中洗衣机实体进行抽象==—即在人为思想层面对洗衣机进行认识,洗衣机有什
么属性,有那些功能,即对洗衣机进行抽象认知的一个过程。 - 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清
楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、
Java、Python等)将洗衣机用类来进行描述,并输入到计算机中。 - 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣
机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才
能洗衣机是什么东西。 - 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。
日期类的实现
进行了三轮的类和对象的学习,来敲个日期类来巩固一下吧。该日期类的实现我们采用声明定义分离的方式。
获取月份天数
对于日期类而言,获取月份天数是很重要的,类中其他函数的实现也需要用到该函数,所以先实现一个获取月份天数的函数
int Date::GetMonthsOfDay(int year, int month)const
{
//assert(year > 0 && month < 13);不能断言,跨年份时会报错
int days[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if ((month == 2) && (year % 100 != 0 && year % 4 == 0) || year % 400 == 0)//闰年
{
return days[month] + 1;
}
return days[month];
}
判断月有多少天只需知道两点,什么月份(可知有多少天),哪一年(判断是否是闰年,是即二月为29天,否则为28天)。
- 采用一个数组存放天数(最好第一个位置放0,数组下标从0开始),再通过月份获取天数。
- 闰年判断:四年一闰并且百年不闰:四百年一闰。
构造函数
我们最好写一个默认构造函数(传不传参都可以)。对于日期类而言自己实现拷贝构造是没有意义的,因为浅拷贝就可以,但还是可以写来熟练一下语法。
Date::Date(int year, int month, int day)
{
if (day>0 && day <= GetMonthsOfDay(year, month))
{
_year = year;
_month = month;
_day = day;
}
else
{
cout << "非法日期" << endl;
exit;
}
}
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
默认构造函数需要借助GetMonthsOfDay来判断一下日期是否合法。
运算符重载——日期比较
对于日期类,日期比较是必不可少的。而且d1==d2这样表述也更为直观,但自定义类型是不能直接使用运算符的,所以会涉及大量运算符重载。
- 该日期类是在类里面实现的,多以第一个参数为this指针(也是日期类),第二个参数采用引用接受要比较的日期类就好了,减少拷贝。
- 以下复用指调用其他功能相似或相反的函数来实现该函数。
- 以下的运算符重载只需要比较即可,所以加上const来修饰*this更为合适。
- 返回类型为bool
==
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);
}
!=是不是== 的反面啊,直接复用==不就好了,多香啊。
- 如d1!=d2,*this就是d1,d就是d2.
>
bool Date:: operator>(const Date& d)const
{
if (_year > d._year)
{
return true;
}
if (_year == d._year && _month > d._month)
{
return true;
}
if (_year == d._year && _month == d._month && _day > d._day)
{
return true;
}
return false;
}
- 大于:我们只需要判断是否大于就好了,否则为假(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,所以参数只需要传要加减的天数即可。
- 为支持连续使用重载的运算符,返回类型为该类。
+
Date Date:: operator+(int day)
{
if (day < 0)
{
day = -day;
*this -= day;//复用-=
}
else
{
Date tmp(*this);
tmp._day += day;
while (tmp._day > GetMonthsOfDay(tmp._year, tmp._month))
{
tmp._day -= GetMonthsOfDay(tmp._year, tmp._month);
++tmp._month;
if (tmp._month > 12)
{
tmp._year += 1;
tmp._month = 1;
}
}
return tmp;
}
}
- + 是不改变自身的,所以需要用this对象构造一个临时对象tmp,通过tmp去进行+操作,最后返回tmp(传值返回,tmp为临时对象,不能传引用)。
- 主体逻辑为:先让tmp的——day+=day,然后通过循环检查天数是否符合本月天数进行调整
- 天数超了加月份,月份过十二了归1加年数
- if条件是由于+=是复用+的,当+=一个负数使等于-=负数的相反数。
+=
Date& Date:: operator+=(int day)
{
*this=*this+ day;
return *this;
}
- +=是直接改变自身,不需要临时变量,所以可以传引用返回。
- +=是直接复用+。
-
Date Date:: operator-(int day)
{
if (day < 0)
{
day = -day;
*this += day;
}
else
{
Date tmp(*this);
tmp._day -= day;
while (tmp._day <= 0)
{
//err,向前借,先调整月
//tmp._day += GetMonthsOfDay(tmp._year,tmp. _month);
//--tmp._month;
//if (tmp._month == 0)
//{
// --tmp._year;
// tmp._month = 12;
//}
--tmp._month;
if (tmp._month == 0)
{
--tmp._year;
tmp._month = 12;
}
tmp._day += GetMonthsOfDay(tmp._year, tmp._month);
}
return tmp;
}
}
- 整体逻辑和+差不多,不过要先调整月份再调天数。
- if条件的原因也是和==+==的差不多。
-=
Date& Date:: operator-=(int day)
{
*this = *this - day;
return *this;
}
- 复用-。
运算符重载——前后++,–
前置++
Date& Date:: operator++()
{
*this += 1;
return *this;
}
- 前置++,返回++后的值,也就是直接改变this指向的对象即可。
- 前面已经重载了+(int)了,所以直接this+1即可。直接引用返回。
后置++
Date Date:: operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
- 返回++前的值,所以需要临时变量tmp,所以只能传值返回。
前置–
Date& Date:: operator--()
{
*this -= 1;
return *this;
}
- 与前置++同理。
后置–
Date Date:: operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
- 与后置++同理
重载运算符——日期相减
int Date::operator-(Date& d)
{
//假设
Date max = *this;
Date min = d;
int flag = 1;
//假设错误,调整
if (min > max)
{
max = d;
min = *this;
flag = -1;
}
int count = 0;//计数
while (max != min)
{
min++;
count++;
}
return count * flag;
}
- 思路:采用计数的办法,让小的日期++直到与大的日期相等。
- flag用来最后检测两日期相减为正数还是负数
运算符重载——流输入输出
输出
//全局实现
ostream& operator<<(ostream& out, const Date& d)
{
out <<d._year << '/' <<d._month << '/' <<d._day << endl;
return out;
}
- 为了和库里的类似,使用友元函数,在全局实现。
输入
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
- 流输出原理在友元函数已进行介绍,可翻阅查看。
- 记得在日期类里面友元声明
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
篇幅有点长,错误难免,希望佬们指出。