接着日期类
void testDate6()
{
Date d1;
cin >> d1;
//>>是流提取,意为从流中提取,放到d1中。
cout << d1;
//<<是流插入,意为把对象插入到流中。
}
这两个运算符都是需要重载的。
具体如何重载需要引入一个概念。
已知:内置类型可以直接使用运算符,cout会自动识别内置类型。
cout如何自动识别类型的?cout和cin具体又是什么?
先从下面这张图开始说明
cin和cout其实是全局的对象,包含在头文件iostream中
cin是istream类型的对象,cout是ostream类型的对象,ostream类型的对象还有cerr,clog。istream和ostream是流的类型
cout能输出各种类型就是因为cout把各种类型都重载了
cin >> a;
cout << a;
变量a通过运算符重载找到对应的重载函数,返回一个ostream类型,由cout << a;可知,打印是通过cout对象实现的,所以写日期类<<运算符的重载需要ostream类型的变量;>>运算符的重载需要istream类型的变量。
void operator<<(std :: ostream& out)
//ostream在std中,没有展开std的话要声明所在域
{
out << _year << "-" << _mouth << "-" << _day << endl;
}
不过这样写cout << d1;会报错。因为位置不对。
因为cout << a;在调用是会转换成cout.operator<<(a);而cout << d1;会转换成cout.operator<<(d1);d1和out类型不一致,没有找到匹配的重载函数,报错。想要调用应该是d1.operator<<(cout);运算符<<重载是写在日期类中的,应该是日期类来调用。
不写成重载形式的调用方式是d1 << cout;
之前规定过,当一个运算符是双操作数时,第一个数是左操作数,第二个数是右操作数。但是流插入运算符的语法是日期类插入到流中去,重载变成了流插入到日期类对象中。
函数调用只能像d1<>(std :: istream& in, Date& d);
pubilc:
……
}
std :: ostream& operator<<(std :: ostream& out, const Date& d)
{
out << d._year << "-" << d._mouth << "-" << d._day << endl;
return out;
}
std :: istream& operator>>(std :: istream& in, Date& d)
{
in >> d._year >> d._mouth >> d._day;
//这里输入非法日期是不会报错的,所以应该再加上一个检查
return in;
}
流本质上是一个类型(istream/ostream)的对象,这个对象的作用是输入/输出。在这个对象中的数据都会输出到屏幕上/存储进内存中的变量里。是由系统控制的。
到这里,日期类才算有一个完整的框架,大概实现了日期类的80%。
剩下的就是比较简单的一些知识了
在默认构造函数部分,有人会有疑惑:没有默认构造函数编译器会报错吗?
答案是不会,默认构造函数不是必须提供的。重点是有没有匹配的构造函数,没有匹配的构造函数才会报错。正常情况下,最好提供一个默认构造函数。
正常初始化方式:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_mouth = month;
_day = day;
}
下面提供一个新的初始化方式:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_mouth(mouth)
,_day(day)
//必须是这种格式
{}
这两种初始化是分开的。
第一种称为函数体内初始化,第二种称为初始化列表初始化。
第二种初始化方式中的括号里面也可以写,两种初始化方式可以混着用。
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_mouth(mouth)
{
_day = day;
}
列表初始化方式的价值在于:初始化列表可以认为是对象成员变量定义的地方。而有些成员变量必须在定义的时候初始化。比如:const修饰的类型,引用类型。
对象创建是对象的整体的定义,对象的成员在初始化列表中定义。
int ret = 10;
class Date
{
public:
Date(int _year, int n)
:_n(n)
,_ret(ret)
{
_year = year;
}
private:
int _year;
const int _n;
int& _ret;
};
int main()
{
Date d1(2022, 1);
return 0;
}
还有一种情况必须在初始化时定义:
class AA
{
public:
AA(int a)
{
_aa = a;
}
private:
int _aa;
};
class Date
{
public:
Date(int _year, int n)
:_n(n)
{
_year = year;
}
private:
int _year;
const int _n;
AA _a;
};
int main()
{
Date d1(2022, 1);
return 0;
}
当Date类中出现一个自定义类型的成员变量,又没有默认构造函数时,就会报错。或者说我需要指定一个值来进行初始化。
解决方案有两个,一个是创建默认构造函数(这样就不能指定值初始化),一个是初始化列表(可以指定值来进行初始化)。
class AA
{
public:
AA(int a)
{
_aa = a;
}
private:
int _aa;
};
class Date
{
public:
Date(int _year, int n, int a)
:_n(n)
//列表初始化部分不支持调试
,_a(a)
//初步怀疑在这里会调用AA的构造函数,用a来初始化AA的对象_a
{
//如果没有初始化列表初始化,且存在默认构造函数,在这里按f11会进入AA中的默认构造函数
//即使Date中没有AA相关的步骤,编译器还是会完成AA的初始化。
_year = year;
}
private:
int _year;
const int _n;
AA _a;
};
int main()
{
Date d1(2022, 1, 10);
return 0;
}
综上所述
1. 每个成员变量在初始化列表中
只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量
const成员变量
自定义类型成员(该类没有默认构造函数)
其他变量既可以在函数体内初始化,也可以在初始化列表中初始化。引用类型和const修饰的类型不是内置类型。
不想考虑复杂情况就都在初始化列表初始化。
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
assert(_a);
_size = 0;
_capacity = capacity;
}
private:
int* a;
int size;
int capacity;
};
class myQueue
{
public:
myQueue()
//这里写不写结果都是一样的
{}
private:
Stack q1;
Stack q2;
size_t size;
//这里给缺省值,缺省值是给初始化列表用的
};
int main()
{
myQueue m1;
return 0;
}
已知默认构造函数对自定义类型调用它的默认构造函数,对内置类型不处理。这步操作发生在初始化列表中。在构造函数的初始化列表中,内置类型不处理,自定义类型调用对应的构造函数。因此在myQueue的类中,myQueue构造函数写不写是一样的结果,初始化实际发生在初始化列表中,每个构造函数都有初始化列表。
初始化列表不显式的写就用缺省值初始化,没有缺省值就随机值初始化。显式写了就用显式写的值初始化。
public:
myQueue(int size = 10)
:_size(size)
{}
所有成员都会在初始化列表中走一遍,没有显式写也会走一遍。
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print()
{
cout<<_a1<<" "<<_a2<>的运算符重载,流运算符如果写成成员函数第一个参数必是this指针,<<的主体是ostream类型的变量,所以要写成外部函数,而外部函数不能访问私有成员变量。所以需要声明友元函数来访问私有成员变量。
说明:
友元函数可访问类的私有和保护成员,但不是类的成员函数,所以不需要声明属于什么类域。
友元函数不能用const修饰,在成员函数后面加const,表示this指针被const修饰。
友元函数可以在类定义的任何地方声明,不受类访问限定符限制,一般情况下定义在前面。
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用和原理相同
友元类
友元类的所有成员函数都是另一个类的友元函数,所有成员函数都可以访问另一个类中的非公有成员。
class Date;
//前置声明,编译器只会向上查找
class Time
{
friend class Date;
//声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
//不声明友元的话时间类和日期类属于两个不同的类域,彼此没有访问权限。
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 = 1900, 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;
};
int main()
{
return 0;
}
友元关系是单向的,不具有交换性。比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
友元关系不能传递。如果B是A的友元,B可以使用A的私有成员变量,C是B的友元,C可以访问B的私有成员变量,则不能说明C时A的友元,C不能访问A的私有成员变量。
内部类——用的比较少
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元
class A
{
private:
static int k;
int h;
public:
//B是内部类
class B
//B天生是A的友元,可以使用A的私有成员变量,但A不是B的友元。A不能使用B的私有成员变量
{
public:
void foo(const A& a)
{
cout << k << endl;
cout << a.h << endl;
}
private:
int _b;
};
};
int A::k = 1;
int main()
{
A a1;
cout << sizeof(a1) << endl;
//a1大小为4,class B只是声明,A里面没有B对象的成员变量
A::B b1;
//B要受到访问限定符限制,即创建B类型的变量要指定属于A的类域。前提是B为公有。
return 0;
}
如果B类为私有,就只能在A类中使用B类,即在A类域中创建B类的对象。
类域存在向上查找逻辑,所以如果先声明私有成员变量且其中包含B类对象,再声明B类,会报错。
因为编译器编译的时候,先看到A类 ,然后看到B类 ,那在A类中没有办法用B类的对象作成员变量,因为还不知道B要开多大的空间。所以创建B类时,必须知道B类的定义。
编译器对类处理的过程:先识别的成员变量,再识别成员函数,最后对成员函数进行改写,主要就是还原this。
不使用内部类和使用内部类的比较,性能而言并没有什么优化。
在实际中编译器会对拷贝构造进行优化
先做一下铺垫,下列代码会进行几次拷贝构造函数的调用?
class Weight
{
Weight()
{
cout << "Weight()"<