目录
一、再探构造函数
1.基本定义以及用法
• 之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有一种方 式,就是初始化列表,初始化列表的使用方式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成 员列表,每个"成员变量"后面跟⼀个放在括号中的初始值或表达式。• 每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
// 初始化列表,成员变量定义初始化的位置
:_year(year)
,_month(month)
,_day(day + 1) //每个成员变量只能出现一次,括号里面可以放初始值或者表达式
{}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 成员变量的声明
int _year;
int _month;
int _day;
};
int main()
{
// 类对象定义初始化
Date d(2024, 7, 27);
return 0;
}
2.必须在初始化列表初始化的成员变量
• 引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始 化,否则会编译报错。
#include<iostream>
using namespace std;
class Time
{
public:
// Time类没有默认构造
Time(int hour)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int& x, int year = 1, int month = 1, int day = 1)
// 初始化列表,成员变量定义初始化的位置
:_year(year)
,_month(month)
,_day(day + 1) //每个成员变量只能出现一次,括号里面可以放初始值或者表达式
,_t(1)
,_ref(x)
,_n(1)
// 如果不初始化上面三个则会报错
// error C2512: “Time”: 没有合适的默认构造函数可用
// error C2530 : “Date::_ref” : 必须初始化引用
// error C2789 : “Date::_n” : 必须初始化常量限定类型的对象
{}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 成员变量的声明
int _year;
int _month;
int _day;
Time _t; // 没有默认构造的类类型对象
int& _ref; // 引用成员变量
const int _n; //const成员变量
};
int main()
{
int a = 1;
// 类对象定义初始化
Date d(a,2024, 7, 27);
return 0;
}
为什么会编译报错呢?
• 因为初始化列表是类对象内成员变量定义初始化的地方,就相当于主函数的int a = 1;,而引用和const变量只在定义时有一次(也必须)初始化的机会,后续将不能改变引用对象和const变量的值。
• 初始化列表在没有手动初始化类类型时,会自动调用类的默认构造来初始化,如果没有默认构造,当然就会报错
3.成员变量声明位置的缺省值(C++11)
• C++11支持在成员变量声明的位置给缺省值,这个缺省值可以是值也可以是一个表达式,主要是给没有显示在初始化列表初始化的成员使用的。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
// 成员变量定义初始化
:_year(year)
, _month(month)
, _day(day)
{
// ...
}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 成员变量的声明
// 这里没有初始化,只是单纯的给缺省值!!!!
int _year = 0;
int _month = 0;
int _day = 0;
};
int main()
{
// 类对象定义初始化
Date d;
d.Print();
return 0;
}
• 这里就要注意,这里成员变量声明位置缺省值的优先级是小于构造函数参数缺省值的优先级的,可以理解为,构造函数没有将成员变量初始化好(所以是优先使用参数位置的缺省值),就会利用到声明位置的缺省值来初始化。
• 尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。
4.成员变量初始化顺序
• 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持⼀致。
二、隐式类型转换
• C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数• 构造函数前面加explicit就不再支持隐式类型转换
#include<iostream>
using namespace std;
class A
{
public:
// explicit A(int a1 = 1) 不想让这样的隐式类型转换发生就在函数前面加上explicit这个关键字就行了
A(int a1 = 1)
:_a1(a1)
{
_a2 = _a1;
}
A(int a1, int a2)
:_a1(a1)
,_a2(a2)
{}
void Print()const
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1;
int _a2;
};
class Stack
{
public:
void Push(const A& aa)
{
// ...
}
private:
A arr[10];// 使用A的默认构造
int _top = 0; // 这样就不用写构造函数了哈哈
};
int main()
{
A aa1(0);// 正常定义初始化一个类对象
aa1.Print();
A aa2 = 2; // 这是啥???
// 隐式类型转换!!
// 这里编译器会用 2 去调用构造函数得到一个临时对象,再把临时对象拷贝构造给 aa2
// 其实编译器还会优化,编译器遇到连续构造+拷贝构造->优化为直接构造
// 有什么用呢?方便传参,就比如入栈
aa2.Print();
Stack s;
// 正常情况下(不用隐式类型转换)入栈
A aa3 = 3;
s.Push(aa3);
// 利用隐式类型转换入栈
s.Push(3);
// 3在括号里面调用构造函数生成一个临时对象,再把临时对象入栈,和上面那个入栈效果是相同的
// 提醒一下,临时对象具有常性,为了不把权限放大,Push函数里面A类型的参数一定要加const
// 本来C++是只支持单参数构造函数才能像上面那么写,多参数是默认不支持的
// 不过.........C++11支持了!
// 这里是使用的演示,要用到花括号{},大家注意用法就行
A aa4 = { 4,4 };
s.Push(aa4);
// 简化就是
s.Push({ 4,4 });
return 0;
}
大家用的时候注意写法就行了。
三、static成员
#include<iostream>
using namespace std;
class A
{
public:
// 在构造函数和析构函数里面调整_scount的值来记录类实例化出对象的个数
A()
{
++_scount;
}
A(const A& aa)
{
++_scount;
}
~A()
{
--_scount;
}
static int GetScount()// 用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针
{
// _a = 2; // 静态成员函数没有this指针,不能访问非静态的成员变量
return _scount;
}
int GetScount2()// 非静态的成员函数
{
int ret = GetScount();// 可以访问静态成员函数
return _scount; // 可以访问静态成员变量
}
private:
// 类里面声明,注意这里仅仅只是声明,这里不能给缺省值,静态成员变量不属于某一个对象,不走初始化列表
static int _scount;// 用static修饰的成员变量,称之为静态成员变量
int a = 1;
};
// 类外面初始化
int A::_scount = 0;
int main()
{
A aa1,aa2;
A aa3(aa2);
// 这里定义三个对象,_scount会变成3
// 静态成员变量为所有类对象所共享,可以通过类类型或者具体对象来调用
cout << A::GetScount() << " " << aa1.GetScount() << " " << aa2.GetScount() << " " << aa3.GetScount() << endl;
// 这里要注意非静态成员函数不能用A::GetScount2()来玩
cout << aa1.GetScount2() << " " << aa2.GetScount2() << " " << aa3.GetScount2() << endl;
// int x = A::_scount; // error,静态成员也是类的成员,受public、protected、private 访问限定符的限制。
return 0;
}
再总结一下:
1. static修饰成员变量,类内声明,类外初始化,不给缺省值,不属于某一个对象,为所有类对象所共享,也可以用类名访问。
2. 静态成员变量也是成员变量,仍然受访问限定符限制。
3. 静态成员函数由于没有this指针而不能访问非静态成员
那现在,来道题??
题目在牛客网:求1+2+3+...+n_牛客题霸_牛客网
大家可以尝试一下自己利用刚刚学过的static成员自己尝试一下,下面是我的过程,供大家参考
// 从1到n,要加n次,怎么计数?
// 可以建一个类,构造一个关于这个类容量为n的数组,会调用n次构造函数
// 用i记录下来构造函数调用的次数便能够计数
// 我们的任务就成了在构造函数里面实现求合
class Sum
{
public:
Sum()
{
_sum+=_i; //第一次构造sum+1,第二次+2........
_i++;
}
static int GetSum()
{
return _sum;
}
private:
static int _i;
static int _sum;
};
int Sum::_i = 1; // 这样_i就能从1开始,随构造到加到n
int Sum::_sum = 0;// sum来每次加上_i的值来求和
class Solution {
public:
int Sum_Solution(int n) {
Sum arr[n];//利用数组调用n次构造函数
return Sum::GetSum();
}
};
四、友元
(这里把友元关系当朋友关系,就会很好理解)
• 友元提供了⼀种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加friend,并且把友元声明放到⼀个类的里面。• 外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。• 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。• ⼀个函数可以是多个类的友元函数。
下面这个代码只有友元函数,友元类会放到后面几条的文字下面
#include<iostream>
using namespace std;
// 这里是前置声明,因为A类里面的友元声明里面用到了B,如果不声明编译器会不认识
class B;
class A
{
// 友元声明
friend void func(const A& aa, const B& bb);
// 友元函数声明仅仅是一种声明,它不是类的成员函数
// 友元函数可以在类定义的任何地方声明,不受类访问限定符限制,在哪里都可以,这里是为了美观,一般都在最上面
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
int _b2 = 4;
};
// func在A类和B类中都进行了友元声明(一个函数可以是多个类的友元函数)
// 就可以理解为func是AB两个类的好朋友,就可以访问它们的私有
void func(const A& aa, const B& bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
int main()
{
A aa;
B bb;
func(aa, bb);
return 0;
}
• 友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。• 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。• 友元类关系不能传递,如果A是B的友元, B是C的友元,但是A不是C的友元。
#include<iostream>
using namespace std;
class A
{
// 友元声明
friend class B;
//void func1(const B& bb)
//{
// cout << _a1 << endl;
// cout << bb._b1 << endl;// 友元关系是单向的,不具有交换性,B是A的友元类,A不能访问B
//
//}
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
// B是A类的友元类,就可以在B类里面访问A的私有
void func1(const A& aa)
{
cout << aa._a1 << endl;
cout << _b1 << endl;
}
// 友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。
void func2(const A& aa)
{
cout << aa._a2 << endl;
cout << _b2 << endl;
}
private:
int _b1 = 3;
int _b2 = 4;
};
int main()
{
A aa;
B bb;
bb.func1(aa);
bb.func2(aa);
return 0;
}
• 友元有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
五、内部类
• 如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。• 内部类默认是外部类的友元类。
#include<iostream>
using namespace std;
class A
{
public:
class B // B默认就是A的友元
{
public:
void foo(const A& a)
{
cout << _k << endl;
cout << a._h << endl; // 这里为什么可以直接使用_k而不能直接使用_h,博主偶然发现这个问题时也很疑惑
// 当外部类的成员变量不是类型名称、静态或枚举数时 内部类无法直接使用该成员变量
// 这是我找到的资料,以我们的水平暂时应该不用管,其实用a._k程序也可以跑
}
private:
int num = 1;
};
private:
static int _k;
int _h = 1;
};
int A::_k = 1;
int main()
{
cout << sizeof(A) << endl;
// 这里结果表明外部类定义的对象中不包含内部类,内部类是一个独立的类,只是受到外部类访问限定符限制和类域限制
A::B b; // 定义B类对象需要通过A
A aa;
b.foo(aa);
// B对A的成员成功访问,B是A的友元类
return 0;
}
• 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其其它地方都用不了。
还是我们上面static成员做过的那道题,就可以通过内部类来稍微改一下:
// 把_i和_sum作为了Solution的静态成员变量,Sum类作为了Solution的专属内部类
class Solution {
public:
int Sum_Solution(int n) {
Sum arr[n];
return Sum::GetSum();
}
class Sum
{
public:
Sum()
{
_sum+=_i;
_i++;
}
static int GetSum()
{
return _sum;
}
};
private:
static int _i;
static int _sum;
};
int Solution::_i = 1;
int Solution::_sum = 0;
六、匿名对象
• 用类型(实参) 定义出来的对象叫做匿名对象,相比之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象。• 匿名对象生命周期只在当前一行,⼀般临时定义⼀个对象当前用⼀下即可,就可以定义匿名对象。
匿名对象用起来很爽,可以让我们代码变简单,看一下下面的代码就明白了:
#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;
};
class Solution {
public:
int Sum_Solution(int n)
{
//...
return n;
}
};
int main()
{
A aa1;
//A aa1();// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
A(); // 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
A(1); // 但是它的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
// 麻烦
Solution st;
cout << st.Sum_Solution(10) << endl;
// 用匿名对象更方便
cout << Solution().Sum_Solution(10) << endl;
return 0;
}
解释一下运行结果:
七、日期类实现
我直接把实现代码放出来,用到的知识都是前面博主写过的,会有必要的注释,大家可以看一下
Date.h
// Date.h
#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:
// 检查日期是否合法
bool CheckDate() const;
// 构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 打印日期
void Print() const;
// 在类内实现的成员函数默认为内联
// 为什么在类内实现,因为这个函数频繁多次调用,且较为短小,作为内联是很好的选择
// 得到当月天数
int GetMonthDay(int year, int month) const
{
// 通过下标能得到平年月份相对的天数
// 用static修饰是为了不让这个数组频繁建立和销毁,因为要一直用
static int MonthDay[13] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };
// 这里有个小细节,month == 2放在前面优先判断,如果不是2月判断语句就会直接结束,就不用再判断年份,会提升一点效率
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
return 29;
}
return MonthDay[month];
}
// 日期的先后比较
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);
// d1++;
Date operator++(int);
// ++d1;
Date& operator++();
// d1--;
Date operator--(int);
// --d1;
Date& operator--();
// d1 - d2
int operator-(const Date& d) const;
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
Date.cpp
// Date.cpp
#include "Date.h"
// 检查日期是否合法
bool Date::CheckDate() const
{
if (_month < 1 || _month > 12 || _day > GetMonthDay(_year, _month))
{
return false;
}
return true;
}
// 构造函数 缺省值再头文件写了之后这里就不要再写了,会重定义
Date::Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
assert((*this).CheckDate());//保证日期的合法性
}
// 打印日期
void Date::Print() const
{
cout << *this << endl;// 这里的流运算符是重载过的,详细看这个文件的最下面
}
// 日期的先后比较->可以优先实现等于和小于然后复用!!
bool Date::operator<(const Date& d) const
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month < d._month)
{
return true;
}
else if (_month == d._month)
{
return _day < d._day;
}
}
return false;
}
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 || *this < d;
}
bool Date::operator>(const Date& d) const
{
return !(*this <= d);
}
bool Date::operator>=(const Date& d) const
{
return *this == d || *this > d;
}
bool Date::operator!=(const Date& d) const
{
return !(*this == d);
}
// 日期的前几天后几天的基本运算
Date Date::operator+(int day) const
{
Date tmp = *this;
tmp += day;
// 为什么是+复用+=而不是+=复用+,因为+=可以返回引用,有利于减少拷贝,而+=复用+则会增加拷贝,下面的-和-=同理
return tmp;
}
Date& Date::operator+=(int day)
{
// 考虑输入小于0的情况
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 tmp = *this;
tmp -= day;
return tmp;
}
Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += (-day);
}
_day -= day;
while (_day < 1)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);// 这行代码放到下面就不用检查_month了
}
return *this;
}
// d1++;
Date Date::operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
// ++d1;
Date& Date::operator++()
{
*this += 1;
return *this;
}
// d1--;
Date Date::operator--(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
// --d1;
Date& Date::operator--()
{
*this -= 1;
return *this;
}
// d1 - d2
int Date::operator-(const Date& d) const
{
// CPU的运行是很快的,一般每秒几十亿次,所以如果不是特殊需求,我们可以通过计数来算日期差值
int flag = 1;
Date max = *this;
Date min = d;
if (*this < d)
{
flag = -1;
max = d;
min = *this;
}
int count = 0;
while (max != min)
{
++min;
++count;
}
return count * flag;
}
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
while (1)
{
cout << "请依次输入年月日:>_"; //这里不写endl是为了不换行,直接在当行输入
in >> d._year >> d._month >> d._day;
if (!d.CheckDate())
{
cout << "输入日期不合法,请重新输入!" << endl;
}
else
{
break;
}
}
return in;
}
Test.cpp
// Test.cpp
#include "Date.h"
void DateTest1()
{
Date d1(2024, 7, 27);
Date d2 = d1 + 100;
d1.Print();
d2.Print();
d1 += 1000;
d2 = d1 + 1000;
d1.Print();
d2.Print();
}
void DateTest2()
{
Date d1(2024, 7, 27);
d1 -= 3000;
d1.Print();
Date ret1 = d1++;
ret1.Print();
d1.Print();
Date d2(2024, 7, 27);
Date ret2 = ++d2;
ret2.Print();
d2.Print();
}
void DateTest3()
{
Date d1(2024, 7, 27);
d1 += -100;
d1.Print();
d1 -= -100;
d1.Print();
}
void DateTest4()
{
Date d1(2039, 12, 1);
Date d2(2024, 7, 27);
cout << d1 - d2 << endl;
cout << d2 - d1 << endl;
}
void DateTest5()
{
Date d1, d2;
cin >> d1 >> d2;
cout << d1 << d2;
cout << d1 - d2 << endl;
}
int main()
{
//DateTest1();
//DateTest2();
//DateTest3();
//DateTest4();
//DateTest5();
return 0;
}
终于写完了......
类和对象,完结撒花!!!
ヾ(~□~;)ノ