兰州市建设工程质量监督站网站加热器网站怎么做的
兰州市建设工程质量监督站网站,加热器网站怎么做的,广告平面设计工作内容,纯静态网站seo目录
一、运算符重载 1#xff09;比较、赋值运算符重载 2#xff09; 流插入留提取运算符重载
二、剩下的默认成员函数 1#xff09;赋值运算符重载 2#xff09;const成员函数 3#xff09;取地址及const取地址操作符重载
三、再谈构造函数 1#xff09;初始化列表 …目录
一、运算符重载 1比较、赋值运算符重载 2 流插入留提取运算符重载
二、剩下的默认成员函数 1赋值运算符重载 2const成员函数 3取地址及const取地址操作符重载
三、再谈构造函数 1初始化列表
编辑 2explicit关键字 3静态成员
四、友元 1友元函数 2友元类 五、内部类 前言 上回我们主要谈及了C里面的类和对象以及类内部的部分成员函数今天我们继续学习剩下的两个成员函数以及类的一些其它的应用场景。 一、运算符重载 1比较、赋值运算符重载 以往在进行比较的时候一定是基于像int、double..类型来进行比较的这些也被称为内置类型如果我们需要自定义类型进行比较那些运算符还会不会有用呢还是以日期类为例
#includeiostreamusing namespace std;class Date{
public:Date(int year 2023, int month 11, int day 1){_year year;_month month;_day day;}void Print(){cout year: _year month: _month day: _day endl;}private:int _year;int _month;int _day;
};void Test()
{Date d1;Date d2(2023, 11, 7);d1 d2;d1 d2;d1 d2;//..return;
}int main()
{Test();return 0;} 通过编译我们可以看到这些运算符对自定义类型是不能识别的当然这也符合我们的常理也在意料之中但是很多时候我们就是需要对内置类型进行比较该如何实现 这个问题很关键解决这个关键的问题就在于问题的关键...其实很简单我们自己实现一个符合需求的比较函数不就行了吗传的参数是内置类型参数返回值按照需求进行设置
bool Equal(Date d1, Date d2)
{return d1._year d2._year d1._month d2._monthd1._day d2._day;
} 这样就可以对日期类内置类型进行比较了啊没错这样就可以对日期类进行比较了但是这里要注意的是这种函数需要放在类内部因为 私有成员变量不能被外界访问。 此时我们再次进行测试 这个时候我们发现这样还是不对啊大家可千万别忘记了在类内是存在this指针的成员函数第一个参数为this指针这是隐藏参数所以在类内我们要这样写
bool Equal(Date d2)
{return _year d2._year month d2._month_day d2._day;
} 照此我们趁热打铁来试试日期类大于操作该怎么写最好多思考一下在往下看我们其实可以这么实现我们把大于的情况全部写出来剩下的就是false了。
bool Greater(Date y)
{if(_year y._year){return true;}else if(_year y._year month y._month){return true;}else if(_year y._year month y._month day y._day){return true;}return false;
} 现在实现对内置类型的比较运算已经不算大问题了可是在我们都知道运算符是我们经常使用的东西在与别人合作的过程中命名叒会引发一系列问题不同国家不同的文化对命名使用是不一样的这样可能别人就不知道你这个是干嘛用的于是祖师爷规定了一个特殊的名字来做函数名————operator运算符 例如
//Equal
bool operator(Date d1, Date d2)
{return d1._year d2._year d1._month d2._monthd1._day d2._day;
}//Greater
bool operator(Date x, Date y)
{if(x._year y._year){return true;}else if(x._year y._year x._month y._month){return true;}else if(x._year y._year x._month y._month x.day y._day){return true;}return false;
} 虽然这样方便了很多但是祖师爷觉得还是不方便于是祖师爷让编译器可以给我们像比较自定义类型一样直接比较
Date s1(2022,10,1);
Date s2(2023,10,1);int ret1 s1 s2;
int ret2 s1 s2; 这种以operator进行内置类型比较的函数叫做——运算符重载实际上 C为了增强代码的可读性引入了运算符重载运算符重载是具有 特殊函数名 的函数也具有其返回值类型函数名字以及参数列表其返回值类型与参数列表与普通函数类似。
注意事项 1、不能通过其他符号来创建新的操作符比如operator 2、重载操作符必须有一个为类的类型参数 3、用于内置类型的运算符其含义不能改变比如operator但是实现却是小于 4、作为类成员函数重载时其形参看起来比操作数数目少1因为成员函数第一个参数为隐藏的this 5、.* :: sizeof ?: . 以上五个运算符是不能重载的 函数重载与运算符重载并没有什么关系函数重载是可以允许参数不同的同名函数而运算符重载是自定义类型可以使用运算符。 其实我们上面的运算符重载还是有些问题运算符重载函数传参使用的是类的类型所以在调用运算符重载函数的时候会先调用拷贝构造。所以我们需要在类型前面使用引用类型而且我们只是进行比较加上const更加保险
bool operator(Const Date d2)
{return _year d2._year _month d2._month_day d2._day;
}bool operator(Const Date y)
{if(_year y._year){return true;}else if(_year y._year _month y._month){return true;}else if(_year y._year _month y._month day y._day){return true;}return false;
} 我们需要重载哪些运算符是根据这个运算符重载是否有意义。有意义就可以实现没意义就不用实现比如日期类相加没有什么太大的意义就不用实现。像是两个日期相减表示差了多少天就是有意义的。 我们可以实现过了多少天之后的日期等等..我们不妨来实现一下过了多少天之后的日期。 过了多少天也就是加了多少天返回值应该是加上这些天之后的日期所以返回类型应为类的类型的引用避免拷贝构造
Date operator (int day)
{//...具体实现过程
} 我们想要知道加了多少天加满了需要对月份进行操作那我们就必须要知道这个月到底有多少天我们不妨设置一个GetMonthDay函数
int GetMonthday(int year, int month)
{int Monthday[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;}return Monthday[month];
} 这样每个月的天数就能得出来了 那么我们如何将得到的天数转换成年和月呢其实很简单 首先需要先将天数累加到当前天数上再调用本类的GetMonthday来获得当前月的天数如果本类的天数大于本月的天数那我们就减去本月的天数再将月份1如果此时月份超过12变成13我们直接将将月份重置为1年份1就行了。 但是可能会出现减完一次天数还要大于当前月份所以我们在外层设置一个while循环只要当前天数大于当月天数就一直重复上述操作所以我们可以得到
Date operator(int day)
{ _day day;while(_day GetMonthday(_year, _month)){_day - GetMonthday(_year, _month);_month;if(_month 13){_year;_month 1;}}return *this;
} 我们既然要返回日期类的类型那么我们的返回值其实就是看不见的this指针所指向的对象返回本类对象只需要返回this指针的解引用就行了让我们来测试一下实际的效果
void Test()
{Date d1(2023, 10, 1);d1.Print();d1 100;d1.Print(); d1 50;d1.Print();
} 如果你照着日历来查看的话会发现是完全符合日期的这也就说明了我们写的是没有问题的。 上面我们实现的是对本类对象的改变我们每次想要看到多少天之后是什么日期的时候我们都是直接将这个对象内容改变如果我们仅仅想看看n天之后的日期而不改变原类对象呢这个时候其实就是单纯的加法而不是加等。 如果想实现这个操作我们有必要在实现一个operator的运算符重载首先我们需要保证返回的日期类是不会影响调用者的对象的其实我们只需要在原有的operator的函数内部创建一个临时对象对本对象进行拷贝用临时对象进行操作这样就不会对本类对象进行改变了。 这里要注意的是我们返回值类型不应该再用引用返回做处理因为这是个临时对象出了作用域就会销毁所以返回类型应该为本类类型返回值为临时对象。
Date operator(int day)
{Date tmp(*this);//创建临时对象对本对象进行拷贝//用临时对象实现操作这样就不会对本类对象进行改变了tmp._day day;while(tmp._day GetMonthDay(tmp._year, tmp._month)){tmp._day - GetMonthDay(tmp._year, tmp._month);tmp._month;if(tmp._month 13){tmp._year;tmp._month 1;}}return tmp;//返回临时对象临时对象销毁本类对象不改变
}//测试信息
/*void Test()
{Date d1(2023, 10, 1);Date s d1 100;s.Print();d1.Print();return;
}*/ 由测试信息可以看到我们确实并没有对本类对象进行改变但是operator这个操作其实是有些冗余的为什么这样说呢我们来看下面代码
Date operator(int day)
{Date tmp(*this);tmp day;return tmp;
} 直接创建临时对象对本类对象进行拷贝直接复用operator的操作实际上是和上面写的代码完全一致所以我才会说上面的代码比较冗余因为没有必要在将给实现一遍了你不信来看一下完整的过程以及运行结果
#includeiostreamusing namespace std;class Date{
public:Date(int year 1, int month 1, int day 1){_year year;_month month;_day day;}int GetMonthDay(int year, int month){int Monthday[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;}return Monthday[month];}Date operator(int day){_day day;while(_day GetMonthDay(_year, _month)){_day - GetMonthDay(_year, _month);_month;if(_month 13){_year;_month 1;}}return *this;}Date operator(int day){Date tmp(*this);tmp day;return tmp;}void Print(){cout year: _year month: _month day: _day endl; return;}private:int _year;int _month;int _day;
};void Test()
{Date d1(2023, 10, 1);Date s d1 100;s.Print();d1.Print();return;
}int main()
{Test();return 0;} 所以我们在写代码的时候可以复用的尽量来复用尽量避免冗余的代码可以减少很多的工作量。 于此类似我们可以进行其他的有意义的运算符重载例如实现...其实这些运算符都可以由来进行复用你可以尝试着自己复用一下如何进行实现这些重载吧下面为具体的复用代码
bool operator!(const Date d)
{return !(*this d);
}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) || (*this d);
} 除了比较操作其实我们还有常用的运算符——自增自减运算符这些运算符实现其实也有复用
Date operator()
{_day 1;return *this;
} 与前面相同我们天数自增只需要复用前面的再返回本类的类型就可以了。但是不知道你发现没有我们的自增自减运算符还细分为前置自增自减与后置自增自减。那么我们如何区分前置自增还是后置自增呢难道operator吗并不存在这种写法。 我们的祖师爷开始并没有想到这个问题但是后面他又补了自己挖的坑operator()就表示前置operator(int)就表示后置 运算符只能说祖师爷这招太妙了我们在参数列表位置放置一个类型也不需要具体形参因为用不到这种方法表示后置再好不过。 注意这里后置的形参列表只能是int类型这是祖师爷规定死的。
Date operator(int)
{Date tmp(*this);_day 1;return tmp;
} 后置改变本类对象但是返回值却是改变之前的对象所以这里也是可以用一个临时对象拷贝本类对象本类对象自增返回之前的临时对象这样就完成了后置的作用了。类似的还有前置后置自减运算符的重载方法类似你可以尝试自己实现结果在下面
Date operator--()
{_day - 1;return *this;
}Date operator--(int)
{Date tmp(*this);_day - 1;return tmp;
} 我们已经实现了大部分的运算符重载但是我们相差的天数还没有实现也就是operator-和operator-这两个运算符重载这两个重载和前面的和实现的不太一样尤其是operator-它并不是返回类的类型而是返回天数因为日期的相减的意义是差几天。 我们可以根据年月日进位来进行相差天数的计算但是这样实现会变得很麻烦如果有想尝试的小伙伴可以自己尝试下面我介绍一种更加简单的方法 我们直接省去年和月的进位我们用两个临时对象max,min记录本对象日期和传参对象日期比较他俩的大小将max和min进行调整创建一个计数器count记录天数只要小的那个日期类min不等于大的日期类max那就将min自增count自增。循环结束时count就是所差的天数
int operator-(const Date d)
{int flag 1;Date max *this;Date min d;if(*this d){flag -1;max d;min *this;}int count 0;while(min ! max){min;count;}return count * flag;
} 这里注意我使用了flag是为了保证结果一定为正数防止类拷贝不对的情况而造成返回值为负数所以我们在函数内对flag值进行调整防止有负数的情况出现。 operator-我们知道是相差了几天而operator-的意义和前面的类似表示减去n天之后的日期所以在实现的层面也和差不多只有操作是相反的你可以先尝试自己实现一下。
Date operator-(int day)
{ _day - day;while(_day 0){_day GetMonthday(_year, _month);--_month;if(_month 0){--_year;_month 12;}} return *this;
} operator-这个运算符重载以我们实现的意义来看是要传入要减去的天数从而得到减完之后的日期所以传的天数默认为正数但是不能保证有负数不会被传进来这样日期就乱套了安全性就变得很差所以我们需要在操作之前先判断一下天数day是否小于0如果小于0还要进行-操作那么这个意义就变成了加上day之后返回的日期。 所以我们遇到负数不必直接终止退出函数这个负数可以直接转换为加上的天数直接复用operator的操作
if(day 0)
{return *this (-day);
} 同样在实现的运算符重载里我们也要判断天数day是否小于0如果小于0表示的含义就不再是而应该是-的含义所以也要在的前面加上同样的判断
if(day 0)
{return *this - (-day);
} 安全问题在日常中尤为重要还有哪里是可能会发生安全问题在日期类的最开始构造函数那里可能会传入非法的日期所以我们也要在那里进行判断
if(year 0 || month 0 ||
month 12 || day 0 ||
day GetMonthday(year, month))
{cout 非法输入 :;Print();cout endl;exit(-1);
} 以下为日期类的运算符重载实现的完整代码
namespace MyDate{class Date{public:Date(const int year 2023, const int month 10, const int day 1){if(year 0 || month 0 ||month 12 || day 0 ||day GetMonthday(year, month)){cout 非法输入 :;Print();cout endl;exit(-1);}_year year;_month month;_day day;}Date(const Date d){_year d._year;_month d._month;_day d._day;}int GetMonthday(int year, int month){int Monthday[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;}return Monthday[month];}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){if(_year d._year _month d._month _day d._day){return true;}return false;}Date operator(int day){Date tmp(*this);_day day;return tmp;}Date operator(int day){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;}bool operator!(const Date d){return !(*this d);}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) || (*this d);}Date operator(){_day 1;return *this;}Date operator(int){Date tmp(*this);_day 1;return tmp;}Date operator--(){_day - 1;return *this;}Date operator--(int){Date tmp(*this);_day - 1;return tmp;}int operator-(const Date d){int flag 1;Date max *this;Date min d;if(*this d){flag -1;max d;min *this;}int count 0;while(min ! max){min;count;}return count * flag;}Date operator-(int day){if(day 0){return *this (-day);}_day - day;while(_day 0){_day GetMonthday(_year, _month);--_month;if(_month 0){--_year;_month 12;}}return *this;}void Print(){cout _year / _month / _day endl;}~Date(){_year 2000;_month 1;_day 1; }private:int _year;int _month;int _day;};
} 2 流插入留提取运算符重载 说到运算符在C里面就不得不提到我们在接触第一个C程序时如何打印出hello world的使用了流插入运算符与之相对的还有流提取运算符它们也是运算符是不是也可以重载 我们先来思考通常我们如何使用流插入流提取运算符的有哪些注意事项还记得在【上】我们说的吗流插入与流提取运算符是自动识别类型的其实这与我们的头文件iostream有关iostream表示输出输出流分为 我们用红圈圈出来的部分以外我们目前不需要了解很多cout、cerr、clog这些也不需要过多了解这与后面多态继承有关我们具体看看istream里面到底包含了什么 我们可以看到哪里存在什么自动识别类型全部都是运算符重载罢了“哪里有什么岁月静好不过是有人替你负重前行”我们仔细看一下这些全都是内置类型对于自定义类型是需要自己实现流插入与流提取运算符的。 那么我们该如何实现operator 上面我们看到流插入的 类型是ostream除了ostream还需要类的类型比如实现日期类
void operator (Date const d, ostream out) 我们把运算符重载放在类内部会有隐藏的this指针传入所以我们不需要第一个参数
void operator (ostream out);//这里的out就是cout名字不同但是都是同一个ostream类 将声明和定义分离
class Date{
public:Date(int year 2023, int month 11, int day 11){_year year;_month month;_day day;}void operator (ostream out);~Date(){_year 1;_month 1;_day 1;}
private:int _year;int _month;int _day;
};void Date::operator (ostream out)
{out _year 年 _month 月 _day 日 endl;
}int main()
{Date d1;cout d1;return 0;
} 这样却报错了我们的逻辑看起来也没问题啊。这时候如果我们将cout d1反过来
d1 cout; 这样居然可以运行这不是乱套了吗很奇怪的写法。其实在C中双操作数的运算符第一个参数是左操作数第二个参数时右操作数。
//其实可以这样看d1 cout —————— d1.operator(d1, cout); 如果在类里面写的话一定要有本类的this指针Date对象会默认占据第一个参数列表 的位置operator实现成成员函数就是不好的写法所以我们考虑在全局范围写流插入重载
class Date{
public:Date(int year 2023, int month 11, int day 11){_year year;_month month;_day day;}private:int _year;int _month;int _day;
};void operator (ostream out, Date const d)
{out d._year 年 d._month 月 d._day 日 endl;
} 我们再次编译发现还是编译不通过看报错信息哦原来是类的成员是私有的那我们先将类私有成员开放成公有的试试 这个时候就能正常运行起来了 但是我们能得到结果的前提是将私有成员变量给开放为公有得不偿失啊老铁有没有什么办法来让我能访问到类的私有成员变量呢 我们可以使用在本章稍后面要说的——友元我们只需要在类的内部加上
friend void operator(ostream out, Date const d); friend声明这个operator函数是这个类的友元那么作为你的朋友我就可以访问这个类的私有成员变量具体可以往后翻到友元那一节这样就不需要担心私有成员不能访问的问题了。 我们通常在使用流插入时会经常用到连续插入
int a 1, b 2;
cout a b endl;//多次流插入操作 而我们上面实现的流插入操作只能执行一次我们仔细分析这里流插入的特点为什么能进行连续流插入其实我们在流插入的时候从右往左是依次将返回值传给左值的endl返回给bb在返回给a最后打印出来所以我们只需要将返回类型改为流插入类型的引用就行了
#includeiostreamusing namespace std;class Date{
public:Date(int year 2023, int month 11, int day 11){_year year;_month month;_day day;}friend ostream operator(ostream out, Date const d);
private:int _year;int _month;int _day;
};ostream operator (ostream out, Date const d)
{out d._year 年 d._month 月 d._day 日 endl;return out;
}int main()
{Date d1;Date d2(2023, 1, 1);cout d1 d2 endl;return 0;
} 流插入我们已经实现完成了接下来就是流提取运算符了实现的过程和流插入几乎没什么区别可以自己动手实现一下实在懒得写就看下面现成的吧
class Date{
public:Date(int year 2023, int month 11, int day 11){_year year;_month month;_day day;}friend ostream operator(ostream out, Date const d);friend istream operator(istream in, Date const d);private:int _year;int _month;int _day;
};ostream operator (ostream out, Date const d)
{out d._year 年 d._month 月 d._day 日 endl;return out;
}istream operator (istream in, Date const d)
{in d._year d._month d._day;return in;
} 总结 其他的运算符重载一般是实现成成员函数而 必须实现在全局这样才能让流对象做第一个参数才符合可读性。 流的本质是为了解决C语言中不能支持自定义类型输入输出问题使用 面向对象运算符重载 解决这类问题。 以上就是运算符重载的相关知识了理解或许很简单但是多加练习才能打牢根基。所以要勤加练习很多问题是自己在写的时候才会注意到的。 二、剩下的默认成员函数 1赋值运算符重载 我们前面学习了拷贝构造函数拷贝构造实际上是一个已经存在的对象去拷贝初始化另一个对象。如果是两个都已经存在的对象呢我们来看下面代码
#includeiostreamusing namespace std;class Date{
public:Date(int year 2023, int month 11, int day 1){_year year;_month month;_day day;}void Print(){cout year: _year month: _month day: _day endl;}private:int _year;int _month;int _day;
};void Test()
{Date s1(2023, 11, 6);Date s2(2023, 10, 24);Date s3(s1);Date s4 s2;s3.Print();s4.Print();return;
}int main()
{Test();return 0;} 在Test函数里毫无疑问s3是会调用拷贝构造来初始化自己的但是下面s4呢按我们前面的学习来说内置类型运算符是不会对自定义类型处理的啊那这里是拷贝构造吗其实这里调用的是六大默认成员函数之一————赋值运算符重载
赋值运算符重载的实际效果是这样的
Date operator (const Date d)
{if(this ! d){_year d._year;_month d._month;_day d._day}return *this;
} 这里的 返回类型是Date因为我们可能要进行一个表达式 多次赋值 运算返回类的类型可以让左边被赋值对象连续接收参数。这里的使用if是为了防止自己对自己的拷贝。 可能你会问赋值运算符重载有什么用我们直接用拷贝构造函数不就行了其实我们很多时候调用的并不是拷贝构造而是运算符重载。
两个角度来理解本质还是一回事 1、我们上面也说了拷贝构造的应用场景是一个已存在的对象去拷贝初始化另一个对象而赋值运算符的应用场景是两个已存在的对象进行拷贝。 2、初始化和赋值是两个概念。初始化是指在定义的时候进行赋值而在定义完成之后再进行赋值的操作叫做赋值。程序以拷贝的方式初始化时是会调用拷贝构造的。程序给一个对象赋值时是会调用赋值运算符的。 既然编译器能帮助我们实现我们不写不也行吗在日期类中当然可以但是如果是在栈stack、二叉树BinaryTree或者打开文件等需要申请资源的类当中使用编译器默认生成的赋值重载则会造成跟拷贝构造相同的错误 所以像栈类似的需要申请资源的类拷贝构造与赋值运算符都要自己来写。
总结 1、参数类型为const引用对象可以避免调用拷贝构造同时可接收const类型与非const类型的对象权限高。 2、返回值类型为类的引用返回同样避免拷贝构造的问题同时还能保证连续赋值s3 s2 s1; 3、检查是否进行了自我赋值。 4、如果没有将赋值重载显示地写出来编译器会默认生成一个赋值重载而且对申请资源的类无用。 2const成员函数 我们先来看这样一段代码
#includeiostreamusing namespace std;class Date{
public:Date(int year 1, int month 1, int day 1){_year year;_month month;_day day;}void Display(){cout year: _year month: _month day: endl;return;}private:int _year;int _month;int _day;
};void Test()
{const Date s(2023, 10, 1);s.Display();
}int main()
{Test();return 0;
} 这里因为某些原因我需要将创建的对象s加上const 这个时候我再想调用类内部函数会发生什么 居然不让访问这是为什么 还记得我们前面说的this指针吗实际上this指针在Display这个函数里是这样 this指针的完整写法是 const Date* this 这个const作用的是指针而不是指针指向的内容 而在Test函数里我们给s对象加了const也就是说我们权限只能进行读取操作不可修改对象。 在C中对const修饰的成员取地址是非法的因为这样可能会改变修饰内容。而对象在调用的时候实际上会对该对象取地址传入this指针也就是之前说的权限放大问题所以造成程序错误。 为了使被const修饰的对象可以访问成员函数C规定了可以用const来修饰类的成员函数。在类中这些const成员函数本质上是修饰成员函数隐藏的this指针的这样就保证了this指针在该成员函数内部不可修改只可读取。 我们这在成员函数后面加上const测试一下 这下就能操作类的成员函数了。 由此我们来思考下面四个问题 1、const 对象可以调用非const成员函数吗 2、非const 对象可以调用const成员函数吗 3、const 成员函数内可以调用其他的非const成员函数吗 4、非const 成员函数内可以调用其他的const成员函数吗 我们上面的例子也算是解释了第1个问题const对象不能调用非const成员函数因为权限不能被放大调用只能平移或者缩小。 第二个问题非const对象调用const成员函数明显是权限缩小是可以调用的。 第三个问题const 成员函数不可调用非const成员函数权限被放大了当然不行。 第四个问题非const成员函数调用const成员函数也是一种权限缩小的情况可以调用。 建议在不改变类内容的成员函数后面都加上const把权限降到最低那么任何权限参数就都能接收同时又保证安全性以及代码的健壮性。 3取地址及const取地址操作符重载 取地址也算是运算符所以就一定有取地址运算符重载这里分为const和非const取地址运算符重载
class Date
{
public://两个取地址的权限不同Date* operator()//this指针为Date *const this{return this;}const Date* operator()const //this指针为const Date *const this{return this;}
private:int _year; // 年int _month; // 月int _day; // 日
}; 一般这两个运算符重载不需要自己实现除非你不想让别人得到你的地址返回个假地址糊弄过去...这两个的区别就是取const修饰变量的地址与取非const修饰变量的地址编译器默认生成的和我们上面写的没什么区别。 三、再谈构造函数 还记得我们的构造函数吗我们前面说构造函数是为了进行初始化给各个对象中各个成员变量一个合适的初始值。
class Date{
public:Date(int year, int month, int day){_year year;_month month;_day day;}private:int _year;int _month;int _day;
}; 但实际上这严格来说并不能叫初始化虽然在调用构造的时候会给一个初始值但不能将其称为类对象成员的初始化构造函数体中的语句只能将其称为赋初值而不能称为初始化因为 初始化只能初始化一次而构造函数体内 可以多次赋值。 我们来看一看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;
}; 其实在类中私有private部分是对变量的声明并不是定义我们之前所有的定义全都是在类的实例化的时候对 对象整体定义。 上面是类的整体定义如果想要在细一点对类的成员变量分开定义该如何做呢这就是初始化列表的用处了当然更大的用处是为了给引用变量常变量来初始化的例如
#includeiostreamusing namespace std;int main()
{const int i;//声明常变量int p i;//声明引用return 0;} 我们之前也说过const修饰的变量需要再声明的同时初始化引用也必须要在声明的时候初始化不能将声明与定义分离如果这两个变量出现在类内仅仅用构造函数势必会报错的也就是说也用类型与const修饰类型必须用初始化列表来初始化。
class Date{
public:Date(int year, int month, int day):_year(year),_month(month),_day(day),_a(1),_aa(year){}private:int _year;int _month;int _day;const int _a;int _aa;
};
当然也可以这么写
class Date{
public:Date(int year, int month, int day):_a(1),_aa(year){_year year;_month month;_day day;}private:int _year;int _month;int _day;const int _a;int _aa;
}; 初始化列表可以与函数体内赋值一起使用。每个成员变量在初始化列表只能初始化一次多次初始化会报错
Date(int year, int month, int day):_year(year),_month(month),_year(year)//_year第二次出现在初始化列表,_day(day),_a(1),_aa(year)
{} 初始化列表还有一个妙用给没有默认构造的自定义成员变量进行初始化。我们前面提到过内置类型如果没初始化编译器会使用默认构造初始化随机值自定义类型会调用它的默认构造。
#includeiostreamusing namespace std;class A{
public:A(int a):_a(a){}private:int _a;
};class Date{
public:Date(int year, int month, int day):_year(year),_month(month),_day(day),_aa(1){}private:int _year;int _month;A _aa;int _day;
};int main()
{Date a(2023, 11, 11);return 0;
} 总结 1、每个成员变量在初始化列表中只能出现一次初始化只能初始化一次 2、类中包含以下成员必须放在初始化列表进行初始化 1引用成员变量 2const成员变量 3自定义类型成员该类没有默认构造 我们能使用初始化列表来初始化就尽量用它因为你不管是否使用初始化列表对于自定义类型成员变量一定会先使用初始化列表初始化。
#includeiostreamusing namespace std;class Time{
public:Time(int hour 0):_hour(hour){cout _hour endl;}private:int _hour;
};class Date{
public:Date(int day)//没有对_t这个内置类型初始化调用Time类的构造函数{}private:int _day;Time _t;
};int main()
{Date d(1);return 0;
} 可以看到main函数中Test的初始化是不成功的如果不使用初始化列表仅仅用构造函数进行赋值操作。 那么在Date类内想要对_t这个属于Time类的成员函数进行初始化就必须要调用Time没构造函数所以调用拷贝构造类的拷贝构造然后再调用赋值运算符对自定义成员变量赋值。
#includeiostreamusing namespace std;class Time{
public:Time(int hour 0):_hour(hour){cout _hour endl;}private:int _hour;
};class Date{
public:Date(int day, int h){_day day;Time t(h);_t t;}private:int _day;Time _t;
};void Test()
{Date d(1, 1);return;
}int main()
{Test();return 0;
} 这里虽然能成功初始化但是却调用了一次构造一次拷贝构造与一次赋值运算符重载第一次构造是d进行实例化的时候Time _t同时会被定义从而调用Time的构造函数。对于数据量大的类来说这样的开销实在不小如果直接用初始化列表
class Date{
public:Date(int day, int h):_day(day),_t(h){}private:int _day;Time _t;
}; 这样初始化的开销就会小很多。对于初始化列表还有要注意的一点是初始化列表的初始化顺序是成员变量在类中声明的次序与其在初始化列表的先后顺序无关
#includeiostreamusing namespace std;class Date{
public:Date(int year, int month, int day):_day(day),_month(month),_year(year){}private:int _year;int _month;int _day;
};int main()
{Date d(2023, 11, 11);return 0;
} 可以看到这里我是把_day和_month放在初始化列表最前面但是_year却先被初始化。 初始化列表的成员变量可以直接使用缺省值在成员变量的后面直接使用缺省值并不是初始化而是给参数列表的初值
class Date{
public:Date(int year, int month, int day):_day(day),_month(month),_year(year){}private:int _year 1;//在成员变量给缺省值表示给参数列表的值int _month 1;int _day 1;
};
注意这里在声明处给缺省值是C11之后才有的规定。 2explicit关键字 不知道你了不了解C/C中的隐式类型转换比如:
int a 1;
double b a; 这里double和int不匹配那么就会发生隐式类型转换 可以看到发生隐式转换之后a的类型并没有被改变实际上a对b进行赋值时发生隐式类型转换是生成一个临时变量b改变的是临时变量的类型而不是a的类型。 这里有一种特殊情况当b为引用类型的时候必须在引用前加上const因为临时变量具有常性不加const就会造成权限放大的问题所以要加上const让b成为常变量使得权限平移。
int a 1;
const double b a;//临时变量具有常性要加const 在C类单参数的类中是支持隐式类型转换的
#includeiostreamusing namespace std;class A{
public:A(int a):_a(a){}private:int _a;
};int main()
{A a 1;//先调用构造函数A tmp(1)//在调拷贝构造A a(tmp)return 0;
} 这就是类的隐式类型转换中途也会生成临时对象在调用拷贝构造赋值。这是一件开销很大的事情那我们如何避免这种隐式类型转换呢C提供了一个关键字 explicit关键字 可以很好解决这个问题
class A{
public:explicit A(int a):_a(a){}private:int _a;
}; 用explicit 关键字修饰构造函数就将隐式类型变成显示类型这样就可以避免不小心发生隐式类型转换了。 3静态成员 在类的成员里还有这样一类特殊的成员——static成员 声明为static的类成员称为类的静态成员用static修饰的成员变量称之为静态成员变量用static修饰的 成员函数称之为静态成员函数。静态的成员变量一定要在类外进行初始化。 现在我想实现一个类来计算调用这个类会创建了多少个对象。其中有一个·问题是我们如何将创建了几次对象给记录下来如果用普通成员变量计数器来记录创建了多少个对象肯定是不可行的因为在创建多次对象的时候每个对象都会给自己的计数器自增。 既然我们不能使用普通成员变量那我们不妨使用全局变量count来对类创建对象进行计数创建对象在类中无非是调用构造函数与拷贝构造我们只需要在构造和拷贝构造函数内将count1就行了。
#includeiostreamusing namespace std;namespace byte {int count 0;
};class A {
public:A() { byte::count; }A(const A t) { byte::count; }~A() {}private:};A func()
{A aa;return aa;
}void Test()
{byte::count;A aa;func();cout byte::count endl;return;
}int main()
{Test();return 0;
} 因为使用count会与std库里的的函数有冲突所以这里使用了命名空间使用func函数是为了测试返回临时对象会不会创建对象在Test函数里我们自己创建了一个对象所以count要1接着调用func函数最后打印出来记录着的count值。 由此可见我们出了在Test函数里创建的对象与func里创建的对象func返回对象时也会创建一个临时对象。 虽然说用全局变量可以记录函数创建对象的次数但是如果在之前就有人调用或者中途有人恶意1这里也不太好辨别用全局变量终究是不安全的有没有别的办法来获取创建对象的次数呢 这个时候我们的静态成员变量就出场了我们知道静态成员只能初始化一次因为存在于静态区所以不论创建多少个对象他们都会共用同一个静态成员变量。
class A{
public:A() { count; }A(const A t) { count; }~A() {}private:static int count;
};int A::count 0; 你肯定注意到了为什么我们静态成员要在类外边初始化我们为什么不能再类中声明的时候给个缺省值 注意我们前面成员变量可以给缺省值的条件是这个成员变量必须属于这个类静态成员变量是所有类都可访问的属于静态变量所以不能在声明的时候给缺省值。这也就解释了为什么静态成员变量一定要在类外初始化。 但是这个时候我们就能直接使用这个成员函数了吗
void Test()
{A::count;A a;func();cout a.count endl;return;
} 我们可以看到并不能直接使用静态成员变量因为目前静态成员变量是私有的那么我们将私有成员变量暂时变为公有 这个时候我们就可以看到静态成员就可以使用了不知道你注意到没有这里我用了两种使用方式
A::count;
A.count; 在类成员公有的情况下这两种调用方式都是正确的。 但是将私有成员给改为公有这样的开销似乎很大我们有没有什么别的办法来获取count的值呢当然有我们可以再类内部编写一个GetCount的函数
class A{
public:A() { count; }A(const A t) { count; }~A() {}int GetCount()//返回count的值{return count;}private:static int count;
};这样我们直接调用成员函数就可以获取count的值了这个函数并不会被改写
#includeiostreamusing namespace std;class A
{
public:A() {count;}A(const A t) { count;}~A() {}int GetCount(){return count;}private:// 声明static int count;
};// 定义
int A::count 0;A func()
{A aa;return aa;
}int main()
{A aa;func();func();func();cout aa.GetCount() - 1 endl;return 0;
} 还有另外一种写的方式
cout A().GetCount - 1 endl;//匿名对象生命周期仅仅为这条语句语句结束调用析构销毁 若不想创建对象可以使用匿名对象来调用函数只不过匿名对象调用的时候也是会发生构造的所以还是要-1匿名对象顾名思义就是没有名字的对象写法就是上面的写法。 其实上面的写法都不能算是好的写法好的写法是将GetCount函数加上static成为静态成员函数这样就不需要再创建一个对象来调用GetCount函数了直接使用 A::GetCount();//静态成员函数在类的域里所以要使用域作用限定符 问题 1、静态成员函数可以调用非静态成员函数吗 2、非静态成员函数可以调用类的静态成员函数吗 其实我们前面也算是解释了第二个问题我们甚至都能通过类域直接访问到类的静态成员函数那么类内部的非静态成员函数也是能访问类的静态成员函数静态成员有全局性。 那我们来看第一个问题
class A
{
public:void Print(){cout you can see me endl; }static int GetCount(){Print();return count;}private:static int count;
}; 这里我想要用类的静态成员函数来调用类的非静态成员函数我们编译会发现 可以发现类的静态成员函数是不能调用类的非静态成员函数的。其实这是因为静态成员函数、静态成员变量本质上和全局变量没区别在全局范围内访问类的成员函数是非法的所以会报错。 总结 1. 静态成员 为所有 类对象所共享 不属于某个具体的实例 2. 静态成员变量必须在 类外定义 定义时不添加 static 关键字 3. 类静态成员即可用类名 :: 静态成员或者对象 . 静态成员来访问 4. 静态成员函数 没有 隐藏的 this 指针 不能访问任何非静态成员 5. 静态成员和类的普通成员一样也有 public 、 protected 、 private3 种访问级别也可以具有返回值 四、友元 1友元函数 我们在前面学习的时候有时候有函数需要用到类的私有成员变量但是我们又不希望这个函数出现在类内部那样我们就只能用Get方法来返回类的私有成员变量但是我们在C中并不经常使用Get和Set方法而是使用另一个东西来访问类的私有成员变量/函数——友元 友元分为友元类 和 友元函数 友元提供了一种突破封装的方式有时提供了便利。但是友元会增加耦合度破坏了封装所以友元不宜多用。 我们前面在流插入与流提取就用到了友元函数声明流操作符函数为类的友元函数则这个函数就可以访问这个类的私有成员变量
friend void operator(ostream out, Date const d); 友元函数可以 直接访问 类的 私有 成员它是 定义在类外部 的 普通函数 不属于任何类但需要在类的内部声 明声明时需要加friend 关键字 注意 1、友元函数可访问类的私有和保护成员但不是类的成员函数 2、友元函数可以在类定义的任何地方声明不受类访问限定符限制 3、友元函数不能用const修饰 4、一个函数可以是多个类的友元函数 5、友元函数的调用与普通函数的调用和原理相同 2友元类 友元类的所有成员函数都可以是另一个类的友元函数都可以访问另一个类中的非公有成员。 友元类就好比你跟你的好友小王你家比较有钱有个篮球场对外是不开放的但是你认为小王是你的亲密好友所以他可以来你家的篮球场打篮球就像下面的代码
#includeiostreamusing namespace std;class A{
public:friend class B;//声明B是A的友元类则在B中就可以直接访问到A的私有成员 A(int a):_a(a){}private:int _a;B _bb;
};class B{
public:private:int _s;double _p;A _aa;
}; B类是A类的友元所以在B类当中可以访问A类的私有成员变量但是反过来A类不可以访问B类的私有成员变量。真是哥们跟你心连心你跟哥们玩脑筋。 除此之外友元类并没有传递性如果类B是类A的友元类C是类B的友元但是并不能说明类C是类A的友元
#includeiostreamusing namespace std;class A{
public:friend class B;
private:int _a;
};class B{
public:friend class C;
private:int _b;
};class C{
public:private:int _c;A _aa;
};int main()
{C c;c._aa._a 1;return 0;
} 这说明了友元类不具有传递性。 总结 1、友元关系是单向的不具有交换性。 2、友元关系不能传递如果B是A的友元C是B的友元则不能说明C时A的友元。 五、内部类 我们经常使用类来进行封装但是有时候单纯的类并不能满足我们的需要类的内部还需要再进行封装成一个小的类 概念如果一个类定义在另一个类的内部这个内部类就叫做内部类。注意此时这个内部类是一个独立的类它不属于外部类更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。 注意 内部类就是外部类的友元类。注意友元类的定义内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。 我们来看下面的代码 #includeiostreamusing namespace std; class A{
private:static int k;int h;public:class B{public:void foo(const A a){cout k endl;//OKcout a.h endl;//OK}};
};int A::k 1;int main()
{A::B b;b.foo(A()); return 0;
} 这里B类就是A类的内部类B不仅仅可以在public可以在A类的任意位置。执行结果 这里说明了内部类是可以访问外部类的静态成员变量的而且不需要外部类的对象/类名。那么你可能会问内部类和外部类很关系一定很密切了。 我们不妨对外部类sizeof
sizeof(A); 可以看到与内部类其实并没有什么关系这个4是外部类的成员变量。
总结 1. 内部类可以定义在外部类的public、protected、private都是可以的。 2. 注意内部类可以直接访问外部类中的static、枚举成员不需要外部类的对象/类名。 3. sizeof(外部类)外部类和内部类没有任何关系 呼~~大概24000字真的很累如果觉得这篇文章对你有帮助的话希望可以三连来支持一下下~
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/90211.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!