🏆个人主页:企鹅不叫的博客
? 🌈专栏
?? 博主码云gitee链接:代码仓库地址
?若有帮助可以【关注+点赞+收藏】,大家一起进步!
💙系列文章💙
【初阶与进阶C++详解】第一篇:C++入门知识必备
【初阶与进阶C++详解】第二篇:C&&C++互相调用(创建静态库)并保护加密源文件
【初阶与进阶C++详解】第三篇:类和对象上(类和this指针)
💎一、类的6个默认成员函数
💎二、构造函数
🏆2.1概念
作用是初始化对象
🏆2.2特性
-
函数名与类名相同 -
无返回值,不用写void -
构造函数是私有的,对象实例化时编译器自动调用对应的构造函数。 -
构造函数可以函数重载 构造函数定义 class Date
{
public :
Date ()
{}
Date (int year, int month , int day )
{
_year = year ;
_month = month ;
_day = day ;
}
private :
int _year ;
int _month ;
int _day ;
};
构造函数调用(参数在对象后面,实例化后自动调用)
Date d2(2022,15,15);
d2.Print();
Date d2(2022);
d2.Print();
Date d2;
d2.Print();
-
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成 。内置类型/基本类型:int/char/double/指针,自定义类型:class/struct去定义类型对象。默认生成构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量才会处理(默认生成的构造函数去调用自定义类型的构造函数来初始化)。 **总结:**如果一个类中的成员全是自定义类型,我们就可以用默认生成的函数,如果有内置类型的成员,或者需要显示传参初始化,那么都要自己实现构造函数。 -
无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数 ,总之,不传参就可以调用的都叫默认构造函数,上面三种默认构造函数特点是不用传参就可以调用的。**我们一般用全缺省。**我们不写编译器会自动生成构造函数,对于内置类型成员不做处理,对于自定义类型成员回去调用它的默认构造函数。 下面这种情况就不会报错,d2满足无参,可以运行。 class Date
{
public :
Date ()
{}
Date (int year, int month , int day )
{
_year = year ;
_month = month ;
_day = day ;
}
private :
int _year ;
int _month ;
int _day ;
};
Date d2;
针对编译器自己生成默认成员函数不初始化的问题。 给的是缺省值,编译器自己生成默认构造函数用,不是初始化,这里是声明,声明没有空间。 private:
int _size = 0;
总结:一般情况一个C++类,都要自己写构造函数,一般只有少数情况可以让编译器默认生成。
? 1.类里面成员都是自定义型成员,并且这些成员都提供了默认构造函数。
? 2.如果还有内置类型成员,在声明时给了缺省参数。
💎三、析构函数
🏆3.1概念
对象在销毁时会自动调用析构函数,完成对象的一些资源清理工作,类默认生成析构函数,内置类型不做处理,自定义类型成员回去调用它的析构函数。
🏆3.2特性
1.析构函数名是在类名前加上字符 ~。
2.无参数无返回值,不能函数重载。
3.一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。(内置类型不会处理,自定义类型会去调用它的析构函数)
4.对象生命周期结束时(就是他所在的域),C++编译系统系统自动调用析构函数。
栈里面定义对象,析构顺序和构造顺序是相反的,后定义的先析构
💎四、拷贝构造函数
🏆4.1概念
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
使用:
Date d1;
Date d2(d1);
定义:
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
🏆4.2特性
1.拷贝构造函数时构造函数的一个重载形式
2.拷贝构造函数的参数只有一个且必须使用引用传参(用引用来化简,用引用不改变变量的话,记得加const),使用传值会引发无穷调用(构造一个对象需要同类型对象初始化,同类型的对象初始化需要调用拷贝构造)(内置类型是直接拷贝,自定义类型对象,拷贝初始化规定要调用拷贝构造完成)
下面对于第二个Func()函数不是将d1的值拷贝给d,而是调用拷贝构造,第一个由于要传参,所以先调用拷贝构造然后再调用Func()
void Func(Data d){
}
void Func(Data& d){
}
int main (){
Data d1(2022.5.15);
Func(d1);
}
3.若未显示定义,系统生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫浅拷贝或者值拷贝,栈这种类不能调用这种浅拷贝,两个栈会指向同一个空间,这块空间析构时会释放两次,程序会崩溃。
4.那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了(浅拷贝)
5.类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象。
练习题:
以下代码共调用多少次拷贝构造函数: ( )
class weight
{
public:
Widget f(Widget u)
{
Widget v(u);
Widget w=v;
return w;
}
}
main(){
Widget x;
Widget y=f(f(x));
Widget x;
weight ret = f(x);
}
- 整个程序分别在,x初始化u、u初始化v、v初始化w、w返回时,注意w返回时初始u不在调用拷贝构造函数,第二次调用 f()函数时,拷贝构造和构造函数直接变成拷贝构造,其他不变,所以总体次数为4+3=7次.
- 同理这里就是四次,return的时候,构造一个临时对象,临时对象再去拷贝构造
weight x;
f(x);
solution st;
st.func(10);
Date(2022, 5, 28)
Date d1(2022,5,26);
Date d2(2022,5,27);
Date d3(d1);
d2 = d1;
A a1= 2;
总结:一个表达式中,连续步骤的构造+拷贝构造,或者拷贝构造+拷贝构造,一般编译器都会优化,合二为一
设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?( )
C c;
int main()
{
A a;
B b;
static D d;
return 0;
}
分析:1、类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象
2、全局对象先于局部对象进行构造
3、局部对象按照出现的顺序进行构造,无论是否为static
4、所以构造的顺序为 c a b d
5、析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生存作用域之后,会放在局部对象之后进行析构
6、因此析构顺序为B A D C
💎五、赋值运算符重载
🏆5.1运算符重载
内置类型可以直接用运算符,自定义类型不能直接用各种运算符,所以要运算符重载
函数原型:返回值类型 operator操作符(运算符操作数)
定义:(由于访问的参数都是私有的,一般我们将此函数放到类里面,并且成员函数有一个隐含的this指针,所以将此函数放到类里面就要少些一个参数,但是其实没必要拷贝呀,所以用引用,记得引用加上const)
bool operator==(const Date& d)
{
return _year == d._year;
&& _month == d._month
&& _day == d._day;
}
调用:(如果有两个函数一个是类外面的运算符重载,一个是类里面的运算符重载,则优先调用类里面的运算符重载)
if (d1.operator == (d2)){
cout << "==" << endl;
}
if(d1 == d2){
cout << "==" << endl;
}
- 不能通过连接其他符号来创建新的操作符,没有这个运算符就不能用:比如operator@
- 不能对内置类型使用,只能用自定义类型
- 用于内置类型,不能修改原来的操作符原有的含义,不能把+改成-
- 作为形参时比操作数目少1的成员函数,因为默认操作符有this
- :: sizeof ?: . .* 这五个运算符不能重载
🏆5.1赋值运算符重载
Date d1(2022,5,20);
Date d2(2022,5,21);
Date d3(d1);
d3 = d2 = d1;
为了能连续赋值,将上面d2的值赋值给d3,下面函数得到的是d2的地址,所以返回*this(d2指向的对象),d2类型是Date,所以返回的是Date,由于返回会产生拷贝所以我们用==Date&==作为返回减少拷贝构造,返回不能加const,因为会导致不能再给此返回值赋值(不能再作为左操作数了),为了防止自己给自己赋值,我们用this(d2)判断&d(d1)是否相等(地址比较)
但是如果我们不写赋值,他会按照拷贝构造赋值,拷贝构造是默认的,所以对于日期类,我们不用谢拷贝构造和赋值运算符重载
d2 = d1;
Date& operator=(const Date& d){
if(this != &d){
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
赋值运算符特点
- 参数类型
- 返回值
- 检测是否自己给自己赋值
- 返回*this
- 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝
💎六、日期类的实现
构造,拷贝构造,天数获取
前面是计算当月的天数,注意的是定义和声明分开要注意函数作用域,频繁调用数组可以放到static里面,构造函数在定义的时候不要缺省值,声明的时候可以给,拷贝构造,函数记得有一个隐藏参数this,第二个参数必须用const Date&类型,记得引用不然会一直循环
bool Date::IsLeapYear(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
int Date::GetMonthDay(int year, int month) {
assert(year >= 0 && month < 13 && month > 0);
static int MonthDayArrray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (month == 2 && IsLeapYear(year)) {
return 29;
}
else {
return MonthDayArrray[month];
}
}
Date::Date(int year, int month, int day) {
if (year >= 1
&& month >= 1
&& month <= 12
&& day >= 1
&& day <= GetMonthDay(year, month))
{
_year = year;
_month = month;
_day = day;
}
else {
cout << "日期非法!" << endl;
}
}
Date::Date(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
比较 = < > <= >=
下面是一些条件运算符,
Date& Date::operator=(const Date& d) {
if (this != &d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
bool Date::operator<(const Date& d) {
if ((_year < d._year)
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day))
{
return true;
}
else {
return false;
}
}
bool Date::operator>(const Date& d) {
return !(*this <= d);
}
bool Date::operator>=(const Date& d) {
return !(*this < d);
}
bool Date::operator!=(const Date& d) {
return !(*this == d);
}
bool Date::operator<=(const Date& d) {
return *this < d || *this == d;
}
特别说明以下复用,不用~,因为 ~ 是按位取反, !是是逻辑取反
bool Date::operator>(const Date& d) {
return !(*this <= d);
}
并且不可以写成以下函数,因为此函数被调用时,是d.operator>(*this);,第二个函数调用时,原本d是只读的,调用第一个函数时,变成可读可写的了,造成权限的放大,解决方法是给两个函数后面都加上const。 原则:成员函数内只要不改变成员变量,建议都加const
bool Date::operator>(const Date& d) {
return true;
}
bool Date::operator<(const Date& d) {
return d > *this;
}
关于this指针说明
特别说明,关于this指针访问
bool Date::operator==(const Date& d) {
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
+ += - -=
判断下面两种方法,一种是+=复用+,另外一种是+复用+=,哪一种方式更优?
首先第一种+=复用+,我们知道在+函数里面有两次拷贝构造(返回ret和拷贝*this),每次调用+=都会调用到+
第二种+复用+=,+函数固定有两次拷贝构造,但是+=函数没有调用拷贝构造
综上,第二种方式更优
重点:下面函数this是形参,出了作用域会销毁,但是我们没有返回this,我们返回的是*this
Date& Date::operator+=(int day) {
*this = *this + day;
return *this;
}
Date Date::operator+(int day) {
Date ret(*this);
ret._day += day;
while (ret._day > GetMonthDay(ret._year, ret._month)) {
++ret._month;
ret._day -= GetMonthDay(ret._year, ret._month);
if (ret._month == 13) {
++ret._year;
ret._month = 1;
}
}
return ret;
}
Date& Date::operator+=(int day) {
_day += day;
while (_day > GetMonthDay(_year, _month)) {
++_month;
_day -= GetMonthDay(_year, _month);
if (_month == 13) {
++_year;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day) {
Date ret(*this);
ret += day;
reutrn ret;
}
计算两个日期相差多少天
int Date::operator-(const Date& d) {
int flag = 1;
Date min = *this;
Date max = d;
if (min > max) {
min = d;
max = *this;
flag = -1;
}
int n = 0;
while (min != max) {
++min;
++n;
}
return n * flag;
}
前置和后置++,–
Date& operator++() {
*this += 1;
return *this;
}
Date operator++(int) {
Date ret(*this);
*this += 1;
return ret;
}
Date operator--(int) {
Date ret(*this);
*this -= 1;
return ret;
}
Date& operator--() {
*this -= 1;
return *this;
}
💎七、const修饰类的成员函数
下面代码,d1.Print();可以整成运行,但是Func中的函数不能正常运行,Print函数原型是void Pinrt(Date* const this),this指针不能被修改,但是this指针指向的对象可以被修改,
void Func(const Date& d) {
d.Pirnt();
}
void Test() {
Date d1(2002.2.3);
d1.Print();
Func(d1);
}
Print(){
cout << year <<endl;
}
在函数名后面加 const 用法
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
- const对象可以调用非const成员函数吗? 不可以,权限放大
- 非const对象可以调用const成员函数吗?可以,权限缩小
- const成员函数内可以调用其它的非const成员函数吗?不可以,权限放大
- 非const成员函数内可以调用其它的const成员函数吗? 可以,权限缩小
💎八、取地址及const取地址操作符重载
下面两个是默认成员函数,编译器自动生成
Date* operator&()
{
return this ;
return nullpter;
}
const Date* operator&()const
{
return this ;
return nullpter;
}
💎九、<< 与 >>
<<:流插入, >>: 流提取,内置类型库里面都函数重载好了
友元函数在类里面声明,在public上面,加上friend就好了,
class Date {
friend std::ostream& operator << (std::ostream& out, const Date& d);
friend std::istream& operator >> (std::istream& in, Date& d);
public:
};
不能将定义部分放到头文件里面,会在main源文件和调用的源文件展开,会生成两个.o文件,main源文件和调用的源文件里面都有符号表的定义,链接的时候就会冲突。所以全局函数不能在头文件中包含,不然可能会在多个源文件中定义,所以要定义在源文
定义部分:
std::ostream& operator << (std::ostream& out, const Date& d) {
out << d._year << "-" << d._month << "-" << d._day << endl;
return cout;
}
std::istream& operator >> (std::istream& in, Date& d) {
in >> d._year >> d._month >> d._day;
return in;
}
|