theme: condensed-night-purple
一:构造函数
在之前的文章中介绍过C++中的 六个默认成员函数 关于构造函数并没有完全介绍完 这篇文章继续研究c++的构造函数
简单回顾一下之前介绍过构造函数的特征
1:构造函数体赋值
在创建一个对象时 编译器通过调用构造函数 对对象中的每一个成员变量一个合适的初始值
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1,1,1);
return 0;
}
上面例子中调用构造函数之后 对象中已经有了一个初始值。 但不能将其称为对对象中的成员变量的初始化 构造函数体中的语句只能称之为赋初值 不能称为初始化。因为初始化只能初始化一次 而构造函数体内可以多次赋值(取最后一个值) 例如
Date(int year, int month, int day)
{
_year = year;
_year = 2023;
_month = month;
_day = day;
}
所以c++在这引入了初始化列表
2:初始化列表
初始化列表格式:以一个冒号开始,接着以逗号分隔成员变量列表 每个成员变量后面跟一个放在括号中的初值或者表达式
例如
Date(int year = 1,int month = 1,int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
1.每个成员变量在初始化列表中只能出现一次
因为初始化只能初始化一次 所以一个变量不能在初始化列表中多次出现
Date(int year = 1,int month = 1,int day = 1)
:_year(year)
,_year(1)//错误写法 不能多次进行初始化
,_month(month)
,_day(day)
{}
2.类中包含一下成员必须使用初始化列表初始化
- 引用成员变量
- const成员变量
- 自定义类型成员变量(该类没有默认构造函数)
class A
{
public:
A(int a)
{
_a = a;
}
private:
int _a;
};
class Date
{
public:
Date(int year = 1, int month = 2, int day = 3)
:_alias(day)
, _d(1)
, a1(1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
int& _alias;//引用类型
const int _d;//cosnt类型
A a1;//自定义类型
};
在定义时就必须初始化的类型 那么就必须使用初始化列表进行初始化
3.尽量使用初始化列表初始化
对于内置类型来说 几乎没有差别
但是对于自定义类型 使用初始化列可以提高代码的效率
举个栗子
class Time
{
public:
Time(int hour = 1)
{
_hour = hour;
}
private:
int _hour;
};
class Date
{
public:
Date(int hour)
:_t1(hour)//使用初始化列表调用Time的构造函数即可
{}
private:
Time _t1;
};
如果不使用初始化列表 就得写成这样
class Time
{
public:
Time(int hour = 1)
{
_hour = hour;
}
private:
int _hour;
};
class Date
{
public:
Date(int hour)
{
Time t2(hour);//创建一个临时变量去调用Time类的构造函数
_t1 = t2;//调用默认赋值运算符重载函数
}
private:
Time _t1;
};
上面两种写法的区别显而易见 第一种写法只用调用一次构造函数即可完成初始化 第二种写法既要生成临时变量 还要调用赋值运算符重装函数
4.成员变量在类中声明的次序就是在初始化列表中初始化顺序 和其在初始化列表中的顺序无关
class Date
{
public:
Date(int year = 1, int month = 2, int day = 3)
:_alias(day)
,_year(year)
,_day(day)
,_month(month)
{}
private:
int _year;
int _month;
int _day;
int& _alias;
};
上面例子初始化顺序 _year->_month->_day -> _alias;
3:explicit关键字
构造函数不仅可以构造和初始化对象 对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数 还具有类型转换的作用
class Date
{
public:
Date(int year)
:_year(year)
{}
private:
int _year;
};
int main()
{
Date d1 = 1;
return 0;
}
上面的代码 将一个整形类型1赋值给自定义类型d1 这其中存在隐式的类型转换
Date d1 = 1
等价于 Date tmp(1); -> Date d1(tmp);
编译器会先构造一个临时对象 再用临时对象拷贝构造d1
如果不想让构造函数支持隐式类型转换 在构造函数前加上关键字explicit即可
explicit Date(int year)
:_year(year)
{}
注意:不是所有的构造函数都支持类型转换
只有单参数的构造函数和第一个参数无缺省值,其余参数均有默认值的构造函数才支持类型转换
比如下面这两种构造函数都不支持类型转换
Date(int year ,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{}
Date(int year ,int month,int day = 1 )
:_year(year)
,_month(month)
,_day(day)
{}
二:Static成员
1:概念
声明为stati的类成员称之为类的静态成员(分为静态成员函数和静态成员变量)
用static修饰的成员函数称之为静态成员函数
用static修饰的成员变量称为静态成员变量 静态成员变量一定要在类外面进行定义
例如
class Date
{
public:
static int GetYear()//静态成员函数
{
return _year;
}
private:
static int _year;//声明静态成员变量
};
int Date::_year = 2023;//静态成员变量定义
2:静态成员的特性
1、静态成员变量必须在类外面定义(不能通过构造函数初始化静态类成员变量) 定义时不添加static关键字 类中仅仅只是声明
2、静态类成员可以用类名::静态成员或者对象.静态成员来访问
class Date
{
public:
static int GetYear()//静态成员函数
{
return _year;
}
private:
static int _year;//声明静态成员变量
};
int Date::_year = 0;//静态成员变量初始化
int main()
{
cout << Date::GetYear() << endl;//类名::静态成员
Date d1;
cout << d1.GetYear() << endl; //对象.静态成员
return 0;
}
3、静态成员函数没有this指针 不能访问任何的非静态成员
错误示例
class Date
{
public:
static int GetYear()//静态成员函数
{
return _newyear;//error不能访问
}
private:
int _newyear;//非静态成员变量
};
4、静态成员也是类的成员 也受访问限定符的限制
错误示例
class Date
{
private:
static int _year;
};
int Date::_year = 0;//静态成员变量初始化
int main()
{
cout << Date::_year << endl;//非法访问 成员变量为私有成员 不能进行访问
Date d1;
cout << d1._year << endl;//非法访问 成员变量为私有成员 不能进行访问
return 0;
}
正确示例
class Date
{
//private:
public:
static int _year;
};
int Date::_year = 0;//静态成员变量初始化
int main()
{
cout << Date::_year << endl;//可以进行访问 成员变量为public
Date d1;
cout << d1._year << endl;//可以进行访问 成员变量为public
return 0;
}
5、静态成员为所有类对象共享 不属于某个具体的对象 存放在静态区
class Date
{
public:
Date()//构造函数
{
++_year;
}
Date(const Date& dd)//拷贝构造函数
{
++_year;
}
static int GetYear()//静态成员函数
{
return _year;
}
private:
static int _year;//声明静态成员变量
};
int Date::_year = 0;//静态成员变量初始化
int main()
{
cout << Date::GetYear() << endl;//类名::静态成员
Date d1, d2;
Date d3(d1);
cout << d1.GetYear() << endl; //对象.静态成员
cout << d2.GetYear() << endl;
cout << d3.GetYear() << endl;
return 0;
}
对象d1 d2 d3去调用静态成员 拿到的都是一个值都是3 特征和全局变量基本一样
6、非静态成员函数可以调用类的静态成员函数
class Date
{
public:
void Print()
{
cout << GetYear() << endl;
}
static int GetYear()//静态成员函数
{
return _year;
}
private:
static int _year;
};
int Date::_year = 0;//静态成员变量初始化
在前面的文章 类的默认成员函数中简单的介绍了一下赋值运算符重载
这篇文章用一个日期类的例子更加详细的介绍一下赋值运算符重载和运算符重载
实现一个日期类主要包含以下内容
三:日期类的实现
1: 日期与日期的比较
==运算符重载
当两个日期的年月日都相等时 那么两个日期就是相等的
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
!= 运算符重载
bool operator != (const Date& d)
{
return !(*this == d);
}
等于取反 就是不等于 这里==会去调用运算符重载函数
>运算符重载
依次判断年月日是否相等即可
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;
}
return false;
}
<=运算符重载
bool operator <= (const Date& d)
{
return (*this < d) || (*this == d);
}
<= 满足其一即可 这里的< 和 == 会去调用< 或== 的重载函数
>运算符重载
bool operator>(const Date& d)
{
return !(*this <= d);
}
<= 的反面就是>
>= 运算符重载
bool operator >= (const Date& d)
{
return !(*this < d);
}
这里也可以写return (*this > d) || (*this == d);
2:日期加减天数
+=赋值运算符重载
对于+=而言 会改变原来日期的值 函数返回值可以使用类的引用类型
Date& operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)//更新year和month
{
++_year;
_month = 1;
}
}
return *this;
}
先将天数相加 若天数已满 那么用天数减去当月天数 月加一 月满 年加一 月更新为一月 知道日期合法为止
GetMonthDay函数实现 返回本月天数
int GetMonthDay(int year, int month)// 获取某年某月的天数
{
assert(year > 0 && (month > 1 || month < 12));
int arr[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 arr[month];
}
}
+运算符重载
+运算符不会改变本身 拷贝一份日期 用拷贝日期调用+=函数重载即可
Date operator+(int day)
{
Date tmp(*this);
tmp += day;
return tmp;
}
-=运算符重载
Date& operator-=(int day)
{
_day -= day;
//非法日期
while (_day <= 0)
{
--_month;
if (_month == 0)//更新年和月份
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
天数相减 不合法就进行调整 月份减一 到零时更新年和月 然后天数加上当月天数 直到合法为止
-运算符重载
思想与+ 一致
Date operator-(int day)
{
Date tmp(*this);
tmp -= day;
return tmp;
}
3: 日期自增和自减
前置++重载
自增1 即+=1;
Date& operator++()
{
return *this += 1;
}
后置++重载
为了区分前置和后置运算符 c++规定 后置++/--重载时 参数必须为int 这里的int不需要传实参 仅仅只是为了区分前置和后置的区别
Date operator++(int)//后置++参数是int 固定得
{
Date tmp(*this);
*this += 1;
return tmp;
}
后置++的规则是先使用 后自增 所有先拷贝一份 然后自增 返回拷贝的值 这里的tmp不能使用引用类型返回 tmp出了函数的作用域就销毁了 只能传值返回
前置–重载
Date& operator--()
{
return *this -= 1;
}
后置–重载
Date operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
4:日期减日期
即两个日期相减 得到相差的天数(即函数返回值是int)
这里的-号是两个日期类型的操作数 前面的的-号是日期类与整型相减 函数的参数不同 构成重载 调用时会自动调用自己匹配的
思路:
假设一个为较大者 另一个为较小者 然后进行判断
若相反则交换位置 然后定义一个计数器
让较小的自增 直到和较大的相等时
结束返回计数器的结果
int operator-(const Date& d)
{
//左大右小
int flag = 1;
Date max(*this);
Date min(d);
//左小右大
if (max < min)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n*flag;
}
四:友元
友元为了便利 提供了一种突破封装的方式。友元会增加耦合度 破坏封装的特性 所以友元不宜多用
友元分为友元函数和友元类
1:友元函数
对于上面实现的日期类 要去重载<<
发现没办法将operator重载成成员函数 因为cout的输出流和隐含的this指针 抢占在第一个参数位置 this指针默认是第一个参数也就是左操作数了 但是实际中cout需要的是第一个参数对象 才能正常使用
//cout << d1 ===> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
要将operator<<重载成全局函数 但是全局函数在类外面 又会导致类外没法访问私有成员的问题
这时候就需要用友元来解决
友元函数可以直接访问类的私有成员 他是定义在类外部的普通函数 不属于任何类但需要在类的内部声明 声明时需要加friend关键字
class Date
{
//声明友元函数
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
//测试是否调用此函数
cout << "ostream& operator<<(ostream& _cout, const Date& d)" << endl;
_cout << d._year << "-" << d._month << "-" << d._day << endl;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
cout << "istream& operator>>(istream& _cin, const Date& d)" << endl;
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
运行结果
自定义类型d调用 >> 和 << 时会去调用>>和<<重载函数
友元函数特征
- 友元函数可以访问类的私有和保护成员 但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类的任意地方声明 不受类的访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
2:友元类
友元类的所以成员函数都可以访问另一个类的友元函数 都可以访问另一个类的私有和保护成员
class Time
{
//声明Date为Time的友元类 在Datez中就可以直接访问Time的私有成员
friend class Date;
public:
Time(int hour = 0)
{
_hour = hour;
}
private:
int _hour;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)// 构造函数
{
_year = year;
_month = month;
_day = day;
}
void Test()
{
//直接访问Time类的私有成员
_t._hour = 1;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
友元类的特征
- 友元的关系是单向的 不具有交换性
上面的Time和Date类 在Time中声明Date为其友元类 那么可以在Date中直接访问Time的私有成员 但是在Time类中访问Date中的私有成员是做不到的
- 友元关系不能传递
若C是B的友元,B是A的友元 不能说明C是A的友元
- 友元关系不继承
五:内部类
1:概念
如果一个类定义在另一个类的内部 这个内部类就叫做内部类(类的嵌套定义)。内部类是一个独立的类 它不属于外部类 更不能通过外部类的对象去访问内部类的成员 外部类对内部类没有任何优越的访问权限
class Date
{
class Time//Time就叫做内部类
{
//....
};
};
2:特征
1、内部类就是外部类的友元类 内部类可以通过外部类的对象参数来访问外部类中所有的成员。
2、外部类不是内部类的友元类(外部类不能访问内部类的私有和保护成员)
3、内部类可以定义在外部类的piblic private protected都是可以的
4、内部类可以直接访问外部类中的staic 枚举成员 不需要外部类的对象/类名
5、sizeof(外部类)=外部类 和内部类没有任何关系
class Date
{
public:
class Time//Time 天生就是 Date的友元类
{
public:
void fun(const Date& d)
{
cout << _year << endl;//可以直接访问外部类的static成员
cout << d._month << endl;//通过外部类对象参数访问外部类中的所有成员
}
private:
int _hour;
};
private:
static int _year;
int _month;
};
int Date::_year = 1;
int main()
{
Date::Time t;
t.fun(Date());
cout << "sizeof(Date)= " << sizeof(Date) << endl;// 结果为4 sizeof(外部类)=外部类
return 0;
}
六:匿名对象
匿名对象在定义时不用取名 他的生命周期只有自己所在的这一行 到下一行时他就会自动调用析构函数
class Date
{
public:
Date(int year = 1)
{
_year = year;
cout << "Date(int year = 1)" << endl;
}
~Date()//析构函数
{
cout << "~Date()" << endl;
}
private:
int _year;
};
int main()
{
//Date d();//error 错误的定义对象方式 编译器无法识别这是函数声明还是对象定义
Date d1;
Date();//匿名对象 生命周期只有这一行 下一行就会调用析构函数
Date d2(2023);
return 0;
}
运行结果