目录
1.explicit关键字
1.1 强制类型转化和隐式类型转化
在C语言中,其实我们早已经遇到过类似的概念
比如说下面的代码,就是隐式类型转化的典型例子.
j是浮点double类型,如果要赋值给i,编译器会创建一个临时的int类型变量,将j的值赋给该临时变
量,再赋值给i.
#include <iostream>
using namespace std;
int main()
{
//隐式类型转化
int i = 1;
double j = 2.0;
i = j;
cout << i << endl;
return 0;
}
又或者说我们当时C语言用malloc开辟空间,返回的是void*类型,此时一般需要强制类型转化.
#include <iostream>
using namespace std;
int main()
{
//强制类型转化
int* tmp = (int*)malloc(sizeof(int) * 4);
return 0;
}
总的来说,隐式类型转化和强制类型转化都有其运用的场景,合理使用,可以带来奇效,当然也可能带来巨大的隐患.
1.2 explicit应用
构造函数不仅可以初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值
的构造函数,还具有类型转换的作用
比如说下面的代码,我们先用2018作为单参数构造出d1对象.
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
class Date
{
public:
Date(int year)
:_year(year)
{}
void Print()
{
std::cout << _year <<std::endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2018);
//隐式类型转化
d1 = 2019;
d1.Print();
return 0;
}
对于2019,是一个int类型的变量,编译器会先构造出一个临时变量,是Date自定义类型的,然后
再用这个临时变量调用赋值函数,更新我们的d1对象.
还有下面这种情况的单参数构造函数也属于隐式类型转化
编译器会先用1构造出Date类型的临时对象(隐式类型转化),再拷贝构造给aa2对象.
// 单参数的构造函数
class A
{
public:
A(int a)
:_a1(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a1(aa._a1)
{
cout << "A(const A& aa)" << endl;
}
private:
int _a1;
};
int main()
{
// 单参数构造函数 C++98
A aa1(1); // 构造函数
// 隐式类型转换 构造+拷贝+优化->构造
A aa2 = 1;
return 0;
}
但是从结果上来看,并没有调用拷贝函数,这是为什么呢?
因为现在的编译器已经非常智能,能够识别出来,直接用1调用拷贝函数,构造对象aa2.
那如何验证的的确确是存在隐式类型转化这种现象呢?
我们可以用下面这段代码验证,由于是引用,就不需要再赋值,而是直接创建一个临时变量,ref
是它的别名.
而用10直接构造的这个临时变量,具有常性,权限可以缩小但不能扩大,如果要引用,就需要const修饰.
// 单参数的构造函数
class A
{
public:
A(int a)
:_a1(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a1(aa._a1)
{
cout << "A(const A& aa)" << endl;
}
private:
int _a1;
};
int main()
{
//不加const修饰,程序报错
//A& ref = 10;
//加了const修饰后,程序正常运行
const A& ref = 10;
return 0;
}
上面这样的代码,在可读性上大大比较低,因此C++引入了explicit关键字,只要在函数前面加上,就可以禁止隐式转化这种方式.
// 单参数的构造函数
class A
{
public:
explicit A(int a)
:_a1(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a1(aa._a1)
{
cout << "A(const A& aa)" << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
// 单参数构造函数 C++98
A aa1(1); // 构造函数
// 隐式类型转换 构造+拷贝+优化->构造
A aa2 = 1;
return 0;
}
1.3 C++11多参数构造
C++11现在也支持多参数的隐式类型转化,编写的方式和结构体初始化的方式类似.
// 多参数的构造函数
class A
{
public:
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{}
A(const A& aa)
:_a1(aa._a1)
,_a2(aa._a2)
{
cout << "A(const A& aa)" << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
// 多参数构造函数 C++11
//多参数构造
A aa2(1, 1);
//多参数隐式类型转化
A aa3 = { 2, 2 };
const A &ref = { 2, 2 };
return 0;
}
2.Static成员
我们先来看一道题,实现一个类,计算程序中创建出了多少个类对象
一个简单的想法,就是创建一个全局变量count,在每个构造成员函数里面,每次调用成员函数的时候,就count++.
PS:由于std类中含有count命名的函数,所以直接命名空间全部展开,会发生程序报错,这里采取部分展开.
int count = 0;
using std::cout;
using std::endl;
class A
{
public:
A(int a = 0)
{
++count;
}
A(const A& aa)
{
++count;
}
};
void func(A a)
{}
int main()
{
A aa1;
A aa2(aa1);
func(aa1);
A aa3 = 1;
cout << count << endl;
return 0;
}
构造对象aa1,拷贝构造aa2,调用func函数,拷贝构造临时变量a,优化构造对象aa3.
总计构造4个类对象.
但是这样的实现方式很挫,如果在函数外,又直接对count操作,那输出的结果可能就有问题.
这时候,C++就引入了一个叫做静态成员的概念.
静态成员是属于整个类的,是属于所有的对象.
(存放在静态区)
就像小区里面的运动设施,是小区成员所共有的,能够一起使用的.
访问静态成员有两种方式,一种是类名::静态成员 或者 对象.静态成员
这个也很好理解,由于静态成员是属于类,属于所有对象,所以只要能够突破类域,告诉编译器去哪里找该成员,都可以实现访问.
换言之,用类A的指针,也是可以的.
代码可以修改为这样
using std::cout;
using std::endl;
class A
{
public:
A(int a = 0)
{
count++;
}
A(const A& aa)
{
count++;
}
//类里面声明
static int count;
};
//类外定义
int A::count = 0;
int main()
{
A aa1;
A aa2(aa1);
A* ptr = nullptr;
cout << A::count << endl;
cout << aa1.count << endl;
cout << ptr->count << endl;
return 0;
}
但是静态成员并不是私有的,我们将其放开了,如果加上private进行修饰,那程序会发生报错.
换言之,静态成员也是类的成员,是受public、protected、private 访问限定符的限制
为了解决这个问题,配套给出了静态成员函数的概念.
在成员函数前面加上static修饰,就能够成为静态成员函数,它没有隐藏的this指针,不
能访问任何非静态成员,只为静态成员服务.
并且使用上,不需要创建对象,只需要类,就可以访问.
class A
{
public:
A(int a = 0)
{
count++;
}
A(const A& aa)
{
count++;
}
static int GetCount()
{
return count;
}
private:
//类里面声明
static int count;
};
//类外定义
int A::count = 0;
void f1()
{
A aa1;
A aa2(aa1);
A aa3 = 1;
A aa4[10];
}
int main()
{
f1();
cout << A::GetCount() << endl;
return 0;
}
3.友元
在类外部编写的函数,是无法访问类里面私有的成员变量.
那是否有方法是可以解决这个问题的呢?
C++提供了友元的概念,它能够突破封装.(但友元不能多用,能少用就少用,毕竟它会增加耦合度,破坏封装)
3.1友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在
类的内部声明,并且声明时需要加friend关键字
PS:可以在类里面任何一个地方声明.
比如说我们需要实现Date类对象,也能调用流插入和流输出的函数.
假如在类内部直接编写的话,会出现一个问题,就是类内部成员函数默认第一个参数是this指针,并且我们无法对其直接进行修改.
我们对流插入和流输出进行重载的话,默认左边的参数就是*this,所以使用上会非常怪异.
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day;
return _cout;
}
istream& operator>>(istream& _cin)
{
_cin >> _year;
_cin >> _month;
_cin >> _day;
return _cin;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
//d需要放置在左边
d >> cin;
d << cout << endl;
return 0;
}
但我们实际上常规使用,并不是这样,我们想要的是cout << d,d >> cin这种形式.
所以我们想到把函数实现放在外面,但又需要访问类里面的私有成员,因此我们用友元函数进行实现.
即在函数前面加friend关键字修饰,就可以在类外部,访问类里的所有成员,包括私有和保护成员.
PS:友元函数参数中没有this指针,更谈不上this指针解引用等说法.
#include <iostream>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
补充:
1.友元函数的作用就是使其可以突破封装,在类外也能访问私有和保护成员,因此不可以用const进行修饰
2.一个函数可以是多个类的友元
3.2友元类
既然我们可以在类里面声明友元函数,自然而然的一个想法,就是可不可以在类里面声明友元类呢?
答案是可以的.
比如说下面这段程序,Date类在Time类里声明为其友元类,因此我们可以在Date类里面访问Time类内私有的成员,比如hour,minute,second.
class Time {
//Date类是Time的友元,因此在Date类内部可以访问Time类的私有和保护成员
friend class Date;
public:
Time(int hour = 1,int minute = 1,int second = 1)
:_hour(hour)
,_minute(minute)
,_second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date {
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_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;
};
PS:
1.友元类不具有传递性
A是B的友元,也是C的友元,B和C不是友元关系
2.友元类是单向的关系
类似于我们关注明星,我们可以获取明星的相关信息,但他不能够获取我们的相关信息
同样地,友元类可以访问你的私有成员变量,但是反过来则不行.
4.内部类
在类里面定义的类,我们称为内部类.
比如说我们在A类里面定义B类,B类就是内部类,A类就是外部类.
#include <iostream>
using namespace std;
class A
{
public:
class B //B是A的内部类
{
public:
void f1(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
private:
static int k;
int h;
};
int A::k = 1;
它主要带来下面几个特性:
1.B天生就是A的友元,即B可以访问A的私有和保护成员(还包括A类的static修饰的成员)
2.sizeof(A)计算的是类A的大小,和B无关
3.两者相互独立,只是内部类受外部类的类域限制,比如创建B类对象,需要A::B b1.
5.匿名对象
在构造对象的时候,我们曾经说过,下面这种构造方式是不被允许的.
因为我们无法区分这究竟是调用函数还是构造A类型的对象.
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
//error
A a();
return 0;
}
但是下面这种方式是可以的,也就是说编译器允许我们构造一个没有名字的对象.
int main()
{
//correct
A();
return 0;
}
它的特点就一个,它的生命周期只有这一行,一旦到下一行,这个对象就会调用析构函数被销毁.
看起来好像没什么用,实际上在某些场景上还是很实用的.
比如说下面的例子,我们调用Solution类里面的函数输出结果,其实不需要单独构造一个对象,直接用匿名对象输出就可以了.
class Solution {
public:
int Sum_Solution(int n) {
//...
return n;
}
};
int main()
{
//Solution s;
//cout << s.Sum_Solution(10) << endl;
cout << Solution().Sum_Solution(10) << endl;
return 0;
}
6.拷贝对象时编译器所做的优化
现在的编译器比较智能,会对我们编写的程序做一定优化,我们可以用代码简单验证一下.
我们先构造一个A类,并自己实现它的构造,析构,拷贝构造等函数,并且在每次调用的时候,输出相应的提示,以便我们的观察.
class A
{
public:
//构造函数
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
//拷贝构造
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
//赋值重载
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
//析构函数
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
我们先来看第一组测试案例
void func1(A a)
{
}
int main()
{
A aa1 = 1; //1
func1(2); //2
func1(A(3)); //3
return 0;
}
按照我们预期的想法
1,2,3组都应该是用1先构造A类型的临时对象,再拷贝构造
但按照结果输出来看,全部都是直接调用了构造函数
1.隐式类型,连续构造+拷贝构造->优化为直接构造(A aa1 = 1;)
2.一个表达式中,连续构造+拷贝构造->优化为一个构造 (func1(A(3));)
我们再来看第二组案例
A func3()
{
A aa;
//构建对象返回
return aa;
}
A func4()
{
//匿名对象返回
return A();
}
int main()
{
A aa1 = func3();
cout << "---------------------------" << endl;
A aa2 = func4();
return 0;
}
按照我们预期的想法,两者都应该是先构造一个对象,然后再拷贝构造给一个临时变量,然后再通过这个临时变量,拷贝给对象aa1和aa2.
但按照结果输出来看,我们可以发现,它会被优化为先构造,然后一次拷贝构造.
对于匿名对象直接返回,它甚至直接调用构造函数.
3.一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造(A aa1 = func3();)
4.匿名对象返回,构造+连续拷贝构造->优化为直接构造(A aa2 = func4();)
对象返回总结:
1.接受返回值对象,尽量拷贝构造方式接收,不要赋值接受
2.函数中返回对象时,尽量返回匿名对象.
7.时间类的实现
在了解前面有关类和对象的知识后,我们可以简单实现日期类.
日期类可以支持日期与整数相加减,日期与日期相减等操作.
具体代码如下:
//Date.h
#pragma once
#include <iostream>
#include <assert.h>
using namespace std;
class Date {
public:
//全缺省构造函数
Date(int year = 2003, int month = 1, int day = 1);
void Print()const;
//根据年和月,得到对应的天数
int GetMonth(int year, int month) 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);
Date operator+(int day)const;
Date& operator-=(int day);
// d1 - 100
Date operator-(int day)const;
// d1 - d2;
int operator-(const Date& d)const;
// ++d1
Date& operator++();
// d1++
// int参数 仅仅是为了占位,跟前置重载区分
Date operator++(int);
//--d1
Date& operator--();
//d1--
Date operator--(int);
private:
int _year;
int _month;
int _day;
};
看上去需要实现的代码非常多,但实际上,我们可以换一个思路
假如我们实现了==, <的运算符重载,实际上<=,>,>=,!=等运算符都可以赋用==,<的运算符重载实现.
//Date.c
#include "Date.h"
int Date::GetMonth(int year, int month) const
{
assert(month > 0 && month < 13);
int MonthArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
//是闰年的2月,返回29
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
return 29;
else
return MonthArray[month];
}
//声明时已经给了缺省参数,则定义不需要再给
Date::Date(int year, int month, int day)
{
//月份必须在1到12之间,天数必须符合1到该月的最大天数
if ((month > 0 && month < 13) && (day > 0 && day <= GetMonth(year,month)))
{
_year = year;
_month = month;
_day = day;
}
else
{
perror("Create fail.");
exit(-1);
}
}
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)
//{
// 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
{
return (_year < d._year)
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day);
}
//实现判断d1 <= d2
bool Date::operator<=(const Date& d)const
{
return *this == d || *this < d;
}
//实现判断d1 > d2
bool Date::operator>(const Date& d)const
{
return !(*this <= d);
}
//实现判断d1 >= d2
bool Date::operator>=(const Date& d)const
{
return !(*this < d);
}
//实现判断d1 != d2
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 > GetMonth(_year, _month))
{
_day -= GetMonth(_year, _month);
_month++;
//判断是否需要跨年
if (_month == 13)
{
_month = 1;
++_year;
}
}
return *this;
}
//实现新日期与天数相加
Date Date::operator+(int day)const
{
//拷贝构造新日期
Date tmp(*this);
tmp += day;
return tmp;
}
//++d1
Date& Date::operator++()
{
*this += 1;
return *this;
}
// d1++
// int参数 仅仅是为了占位,跟前置重载区分
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
//实现原日期和天数相减
Date& Date::operator-=(int day)
{
//如果天数是负数,本质上是日期和天数相加
if (day < 0)
{
*this += (-day);
return *this;
}
else
{
_day -= day;
while (_day < 0)
{
//要先判断是否需要跨年
_month--;
//等于0的时候,就需要调整
if (_month == 0)
{
_month = 12;
_year--;
}
//由于前面已经调整好相应的月份,所以这里的_month就不需要再--
_day += GetMonth(_year, _month);
}
}
return *this;
}
//实现新日期和天数相减
Date Date::operator-(int day)const
{
Date tmp(*this);
tmp -= day;
return tmp;
}
//实现两个日期相差天数
int Date::operator-(const Date& d)const
{
//常规套路,由于无法确定哪个天数更大,先假设
Date max = *this;
Date min = d;
//设立哨兵位,初始值为1
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;
}
Date& Date::operator--()
{
*this -= 1;
return *this;
}
// d1-- -> d1.operator--(1)
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}