【C++深入浅出】类和对象中篇(六种默认成员函数、运算符重载)


目录

一. 前言 

二. 默认成员函数

三. 构造函数

3.1 概念

3.2 特性

四. 析构函数

4.1 概念

4.2 特性

五. 拷贝构造函数

5.1 概念

5.2 特性

六. 运算符重载

6.1 引入

6.2 概念

6.3 注意事项

6.4 重载示例

6.5 赋值运算符重载

6.6 前置++和后置++运算符重载

七. const成员函数

7.1 问题引入

7.2 定义方式

7.3 使用细则

 八. 取地址运算符重载


一. 前言 

        上期我们介绍了一些关于类的基础知识,学会了如何定义一个类,体会到了面向对象中封装的特征。本期我们将继续类和对象的学习,重点讨论C++类中的成员函数,并在下期我们将自己动手实现一个类----日期类

        话不多说,上菜咯!!!

二. 默认成员函数

        如果一个类中什么成员都没有,我们将其称之为空类

//空类
class Date
{};

         但是空类中真的什么都没有吗?实则不然。任何类在什么都不写时,编译器会自动生成6个默认成员函数。默认成员函数:用户没有显式实现,编译器会自动生成的成员函数。如下所示:

        接下来的内容,我们就对这6个默认成员函数进行逐一分析

三. 构造函数

3.1 概念

        我们来看看下面的日期类:

class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1;d1.Init(2022, 7, 5);d1.Print();Date d2;d2.Init(2022, 7, 6);d2.Print();return 0;
}

        对于上面Date类,我们发现我们每次创建一个对象后,都要通过Init 方法给对象设置日期,这未免显得过于麻烦,那能否在对象创建时,就同步将信息设置进去呢?

        使用构造函数就能很好的进行解决。构造函数是一个特殊的成员函数,函数名与类名相同,创建类对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。其形式如下:

class Date
{
public://Date的构造函数Date(){//进行初始化//...}
};

3.2 特性

        构造函数是特殊的成员函数,需要注意的是,构造函数虽然名为构造,但是其主要任务并不是创建对象开辟空间,而是初始化对象

        构造函数有如下特征

  1. 函数名与类名相同
  2. 没有返回值,void也不能有:
    class Date
    {
    public://Date的构造函数Date(){}void Date(){} //错误写法,没有返回值
    };
  3. 对象实例化时编译器会自动调用对应的构造函数
  4. 构造函数支持重载,可以匹配不同的初始化信息。从参数来看,主要分为无参构造函数带参构造函数
    class Date
    {
    public:// 1.无参构造函数Date(){}// 2.带参构造函数Date(int year, int month, int day){_year = year; _month = month;_day = day;}
    private:int _year;int _month;int _day;
    };int main()
    {//1.创建对象//2.调用相应的构造函数Date d1; //调用无参构造函数Date d2(2023, 8, 22); //调用带参构造函数
    }

    需要注意的是,调用无参的构造函数时,对象后面无需带(),否则会变成函数声明:

    Date d1; //调用无参构造函数Date d3(); //声明一个没有形参的函数d3,它的返回值类型为Date
  5. 构造函数是默认成员函数。如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成

    class Date
    {
    public:private:int _year;int _month;int _day;
    };int main()
    {Date d1; //调用编译器自动生成的默认构造函数,默认构造是无参的,相匹配Date d2(2023, 8, 22); //该行代码会报错,没有匹配的带参构造函数
    }

    而如果我们显式地定义了构造函数,编译器就不会自动生成无参的默认构造函数,如下:

    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;  //该行代码会报错,没有匹配的默认构造函数Date d2(2023, 8, 22); //调用带参的构造函数
    }

  6. 编译器自动生成的默认构造函数对内置类型不会进行初始化,如:int,char,double等等;而对于自定义类型,会去调用该自定义类型的默认构造函数。

    class Time
    {
    public:Time() //Time类的默认构造函数{_hours = 0;_minute = 0;_second = 0;}
    private:int _hours;int _minute;int _second;
    };
    class Date
    {
    public:private://内置类型int _year;int _month;//自定义类型Time _day;
    };int main()
    {Date d; //调用编译器自动生成的默认构造函数return 0;
    }

    我们发现Date的默认构造函数对_year和_month没有进行初始化,依然是随机值,而对_day则去调用了Time类的默认构造函数,将其成员变量初始化为0。我们可以通过调试进一步进行验证:

    默认构造函数调试

  7. 值得一提的是:在C++11中,针对默认构造函数对内置类型不进行初始化的缺陷进行了改进,支持内置类型的成员变量在类中声明时给默认值。如下:

    class Date
    {
    public:void Print(){cout << _year << '-' << _month << '-' << _day << endl;}
    private:int _year = 0; //声明时给默认值int _month = 0;int _day = 0;
    };int main()
    {Date d;d.Print();return 0;
    }

  8.  构造函数也支持给缺省值无参的构造函数和全缺省的构造函数都称作默认构造函数。而默认构造函数只能有一个,故二者不能同时存在。举例如下

    class Date
    {
    public:Date() //无参的构造函数{_year = 2023;_month = 8;_day = 22;}Date(int year = 2023, int month = 8, int day = 22) //全缺省的构造函数{_year = year;_month = month;_day = day;}
    private:int _year;int _month;int _day;
    };int main()
    {Date d1(2024); //编译通过,调用全缺省的构造函数Date d2; //这里编译会报错,d2调用默认构造函数,但存在两个默认构造函数,编译器不知道调用哪个return 0;
    }

    小贴士:一般我们显式定义构造函数时,习惯将构造函数写成全缺省的,以提高代码的健壮性

四. 析构函数

4.1 概念

        构造函数是在对象创建时对其进行初始化,有初始化便有销毁,析构函数的作用就是在对象生命周期结束时,完成对象中资源的清理和释放。和构造函数一样,析构函数由编译器自动调用。下面是Stack类的构造函数和析构函数的实现

class Stack
{
public:Stack(size_t capacity = 4) //构造函数,初始化一个栈,写成全缺省的形式{_array = (int*)malloc(sizeof(int) * capacity);if (nullptr == _array){perror("malloc申请空间失败!!!");return;}_capacity = capacity;_top = 0;}~Stack() //析构函数,在类名前加~号{free(_array); //堆上动态申请的空间需要由用户自行释放//下面的代码也可以不写,栈上的空间操作系统会自动释放_array = nullptr;_capacity = _top = 0;}
private:int* _array;int _capacity;int _top;
};

4.2 特性

        析构函数也是特殊的成员函数,其特征如下:

  1. 析构函数的函数名是在类名前加上字符 ~
  2. 析构函数既没有返回值,也没有参数
  3. 与构造函数相反,析构函数不能支持重载,一个类中有且只能有一个析构函数。参考构造函数的特征,当用户没有显式地定义析构函数,编译器会自动生成一个默认的析构函数。
  4. 当对象的生命周期结束时,C++编译器会自动调用析构函数。
    class Stack
    {
    public:Stack(){cout << "Stack()" << endl;}~Stack(){cout << "~Stack()" << endl;}
    private:int* _array;int _capacity;int _top;
    };int main()
    {Stack s;return 0;
    }

    当s对象创建时编译器自动调用构造函数,当s对象生命周期结束时编译器自动调用析构函数,效果如下:

  5. 和构造函数类似,编译器默认生成的析构函数不会对内置类型成员进行清理,最终由操作系统自动进行回收即可;而对于自定义类型成员,默认析构函数会去调用它的析构函数,保证其内部每个自定义类型成员都可以正确销毁。

     回到我们之前的日期类

    class Time
    {
    public:Time() //Time类的默认构造函数{cout << "Time()" << endl;}~Time() //Time类的析构函数{cout << "~Time()" << endl;}
    private:int _hours;int _minute;int _second;
    };
    class Date
    {
    public://没有显式写出构造函数和析构函数,使用编译器自动生成的private:int _year;int _month;Time _day;
    };int main()
    {Date d; //调用编译器自动生成的默认构造函数return 0;
    }

    尽管我们没有直接创建Time类的对象,但依然调用了Time类的构造函数和析构函数。这是因为Date类中的_day成员是Time类的对象,在Date类的默认构造函数和默认析构函数中,会去调用Time类这个自定义类型的构造函数和析构函数,对_day成员进行初始化清理工作。

  6. 如果类中没有动态申请内存时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成内存泄漏,比如Stack类

  7. 类的析构函数一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放static对象。

         Q:假设已经有A,B,C,D 4个类的定义,则程序中A,B,C,D析构函数调用顺序为?

C c;
int main()
{A a;B b;static D d;return 0;}

答案是BADC。解析如下:

1、全局变量优先于局部变量进行构造,因此构造的顺序为cabd

2、析构的顺序和构造的顺序相反

3、static和全局对象需在程序结束才进行析构,故会放在局部对象之后进行析构

综上:析构的顺序即为BADC。


五. 拷贝构造函数

5.1 概念

        在现实生活中,可能存在一个与你长相,我们称其为双胞胎

         那我们在创建类对象时,能不能创建一个和已有对象一模一样的对象呢?Ctrl+CCtrl+V想必没有人不喜欢吧嘿嘿这就要谈到我们的拷贝构造函数惹。

         拷贝构造函数:只有单个形参,该形参是对本类型对象的引用(一般常用const修饰),在用已存在的类对象创建新对象时编译器会自动调用拷贝构造函数。

class Date
{
public:Date() {};Date(const Date& d) //Date的拷贝构造函数{_day = d._day;_month = d._month;_day = d._day;}
private:int _year;int _month;int _day;
};int main()
{Date d1; Date d2(d1); //用d1拷贝构造d2return 0;
}

5.2 特性

         拷贝构造函数也是属于特殊的成员函数,其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个且必须是本类对象的引用使用传值方式编译器会直接报错
    因为会引发无穷递归调用
    //拷贝构造函数的写法
    Date(const Date d) // 错误写法:编译报错,会引发无穷递归
    {_year = d._year;_month = d._month;_day = d._day;
    }Date(const Date& d) // 正确写法
    {_year = d._year;_month = d._month;_day = d._day;
    }
  3. 未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对内置类型成员会按照其在内存存储的字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝;而对于自定义类型成员,会去调用它的拷贝构造函数,完成拷贝。
    class Time
    {
    public:Time(){_hour = 1;_minute = 1;_second = 1;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}
    private:int _hour;int _minute;int _second;
    };
    class Date
    {
    private:// 内置类型int _year = 2023;int _month = 1;int _day = 1;// 自定义类型Time _t;
    };
    int main()
    {Date d1; //d1调用默认的构造函数进行初始化// 用已经存在的d1拷贝构造d2,此时会调用Date类的拷贝构造函数// 但Date类并没有显式定义拷贝构造函数,因此编译器会给Date类生成一个默认的拷贝构造函数Date d2(d1);return 0;
    }
    我们可以通过监视窗口来查看d2对象的拷贝情况可以看出,编译器默认生成的拷贝构造函数不仅会对自定义类型成员进行拷贝(通过调用相应的拷贝构造函数),也会对内置类型成员进行拷贝(按字节序的浅拷贝)。

    默认拷贝构造函数调试

  4. 咦?既然编译器默认生成的拷贝构造函数已经可以很好的完成拷贝了,那我们还需要显式实现拷贝构造函数吗对于上面的日期类确实已经足够了,让我们再回到之前实现的Stack类
    class Stack
    {
    public:Stack(size_t capacity = 4){_array = (int*)malloc(capacity * sizeof(int));if (nullptr == _array){perror("malloc申请空间失败");return;}_size = 0;_capacity = capacity;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
    private:int* _array;size_t _size;size_t _capacity;
    };
    int main()
    {Stack s1;Stack s2(s1);return 0;
    }

    当我们兴冲冲地运行代码时,诶,程序居然崩溃

     为什么呢?这就涉及到了浅拷贝按字节序拷贝的缺陷,如下图所示

     那这种浅拷贝的问题要如何解决呢?

    一般有两种解决方案:深拷贝或者引用计数


    所谓深拷贝,就是手动再申请一段空间,然后将原空间的内容依次拷贝到新空间中,最后让s2的_array指针指向这个新空间。这种方法避免了一块空间被多个对象指向的问题。

    引用计数,就是在类中额外增加一个变量count记录堆空间被引用的次数,只有当引用次数变为1时,我们才对这段空间进行释放。这种方法避免了一块空间被多次释放的问题。


    现在我们对这两种方式有个初步的印象即可,后续我们会详细讲解。不过无论是深拷贝还是引用计数,都是编译器默认生成的拷贝构造函数无法做到的,需要我们显式地实现拷贝构造函数。

  5. 拷贝构造函数有三个典型的调用场景:使用已存在的对象创建新对象函数形参为类对象函数返回值为类对象

    class Date
    {
    public:Date(int year, int minute, int day){cout << "Date(int,int,int):" << this << endl;}Date(const Date& d){cout << "Date(const Date& d):" << this << endl;}~Date(){cout << "~Date():" << this << endl;}
    private:int _year;int _month;int _day;
    };
    Date Test(Date d)
    {Date temp(d);return temp;
    }
    int main()
    {Date d1(2022, 1, 13);Test(d1);return 0;
    }

    结论:为了提高效率,减少拷贝构造的次数,一般对象传参时,我们尽量使用引用传参,函数返回时也是根据实际场景,能用引用返回尽量使用引用返回


六. 运算符重载

6.1 引入

        对于内置类型,我们可以使用==、>号运算符判断它们的大小关系,可以使用+,-号运算符对其进行加减......如下所示

int main()
{int a = 10;int b = 20;a = a + 10;b = b - 10;cout << (a == b);cout << (a > b);//还可以使用许许多多的运算符进行操作,这里就不一一挪列了//...return 0;
}

        但对于自定义类型来说,也就是我们的类,这些运算符仿佛都失效

class Date
{
public:Date(int year = 2000, int month = 1, int day = 1){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};
int main()
{Date d1(2023, 8, 24);Date d2(2023, 8, 25);d1 = d1 + 10; //d1类对象使用+号运算符d1 == d2; //d2类对象使用==号运算符
}

        很明显编译器报错了,这是因为对于几个固定的内置类型,编译器知道它们的运算规则,而对于我们自定义的类型,编译器并不知道它的运算规则,例如d1+10究竟是年份+10还是月份+10呢?编译器无法进行确定,故报错。

        有一种很简单的解决方法就是给类定义成员函数,通过调用成员函数来实现我们想要的运算,如下所示:

class Date
{
public:Date(int year = 2000, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void AddYear(int val) {_year += val;}bool isSame(const Date& d){return _year == d._year && _month == d._month && _day == d._day;}
private:int _year;int _month;int _day;
};
int main()
{Date d1(2022, 8, 24);Date d2(2023, 8, 24);d1.AddYear(1); //年份+1cout << d1.isSame(d2); //比较d1和d2是否相等
}

        上面的方式的确可以解决问题,但还是不够直观,每次进行运算都需要调用函数,代码未免有点挫有没有什么方法可以让类使用运算符进行运算吗?

        这就不得不谈到我们的主角----运算符重载

6.2 概念

        C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型和参数列表与普通的函数类似

        运算符重载的函数名为:关键字operator后面+需要重载的运算符符号

        其函数原型为:返回值类型 operator操作符(参数列表)

//==号运算符重载
bool operator==(const Date& d)
{//函数内容return _year == d._year && _month == d._month && _day == d._day;
}

通过运算符重载,我们可以对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型,且不改变原有的功能。

6.3 注意事项

        进行运算符重载需要注意以下几点:

  1. 不能通过连接其他符号来创建新的运算符:比如使用operator@创建新的运算符@
  2. 重载操作符必须有一个类类型参数
  3. 不能改变内置类型运算符的含义。例如:内置的整型+,不能改变其含义
  4. 不能改变操作符的操作数个数。一个操作符有几个操作数,其重载时函数就有几个参数
  5. 运算符重载也可以作为类成员函数。作为类成员函数重载时,其形参看起来会比操作数数目少一个,因为成员函数的第一个参数为隐藏的this指针
  6. 有五个运算符不支持重载,它们分别是:.:: sizeof?: .*
     

6.4 重载示例

         下面我们来看看日期类==号运算符的重载示例

         1、作为全局函数重载

class Date
{
public:Date(int year = 2000, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//private: //为了让==运算符重载函数能够访问,将成员变量设置为共有的int _year;int _month;int _day;
};//作为全局函数重载
bool operator==(const Date& d1,const Date& d2)
{//这里需要类外访问成员变量return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day; 
}
int main()
{Date d1(2022, 8, 24);Date d2(2023, 8, 24);cout << (d1 == d2);return 0;
}

当运算符重载作为全局函数时,由于我们难免需要对成员变量进行访问,我们需要类的成员函数是共有的,可这难免会破坏了类的封装性


当然,我们还可以使用友元函数来解决,关于友元在下篇会介绍到。不过最推荐的还是将其重载为成员函数

         2、作为成员函数重载

class Date
{
public:Date(int year = 2000, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// bool operator==(Date* const this, const Date& d2)// this指向调用的对象bool operator==(const Date& d) //重载为成员函数{//类内访问成员变量不受访问限定符限制return _year == d._year && _month == d._month && _day == d._day;}
private:int _year;int _month;int _day;
};int main()
{Date d1(2022, 8, 24);Date d2(2023, 8, 24);//下面两种写法是等价的cout << (d1 == d2);cout << d1.operator==(d2);return 0;
}

6.5 赋值运算符重载

        在使用类的过程中,当我们想将一个类赋值给另一个类时,我们便可以对=赋值运算符进行重载。其重载格式如下:

  • 参数类型:const T&,传递引用可以提高传参效率,const保证原来的类对象不会被修改
  • 返回值类型:T&,使用引用返回可以提高返回的效率,有返回值目的是为了支持链式访问
  • 一般会检测是否是自己给自己赋值,如果是,则不进行任何操作
  • 最终返回的是本身的引用,即*this,便于进行连续赋值
    class Date
    {
    public:Date(int year = 2023, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date& operator=(const Date& d) //赋值运算符重载{if (this != &d) //避免自己给自己赋值{_year = d._year;_month = d._month;_day = d._day;}return *this; //返回自身的引用,连续赋值}
    private:int _year;int _month;int _day;
    };int main()
    {Date d1;Date d2, d3;d3 = d2 = d1; //调用赋值运算符重载
    }

         下面是赋值运算符重载的几点注意事项

  1. 赋值运算符只能重载为成员函数不能重载成全局函数
    // 赋值运算符重载成全局函数
    Date& operator=(Date& left, const Date& right)
    {if (&left != &right){left._year = right._year;left._month = right._month;left._day = right._day;}return left;
    }
    为什么呢?实际上,在我们前面介绍的默认成员函数中,赋值运算符重载也在其中。因此,当我们在类中没有显式地实现,编译器会自动生成默认的重载函数。而如果用户又自行在类外写一个赋值运算符重载函数,就会和编译器默认生成的默认赋值运算符重载冲突,故赋值运算符只能重载为成员函数
  2. 编译器默认生成的赋值运算符重载函数,其规则和拷贝构造函数相似。对于内置类型,是按照字节序进行拷贝的;而对于自定义类型,则是去调用它的赋值运算符重载函数
  3. 那么既然编译器默认生成的赋值运算符重载已经可以完成字节序的拷贝了,那还要显式地进行实现吗?答案显然是需要的。和拷贝构造函数一样,在实现Stack这种有内存管理的类,我们需要自行实现赋值运算符重载,否则就会因为浅拷贝的问题导致程序崩溃
  4. 反之,如果程序不涉及内存管理,赋值运算符重载是否显式实现都可以

        下面来个小问题试试对拷贝构造的理解:

int main()
{Date d1, d2;Date d3 = d1; //这里调用的是拷贝构造还是赋值重载呢?d2 = d1; //这里呢?return 0;
}

答:第一问调用的是拷贝构造函数,第二问调用的是赋值重载。

解析:拷贝构造函数用已存在的对象去构造新对象,而d3就是我们需要构造的新对象,第一问就是用d1对象去构造d3对象,故调用拷贝构造,这种写法与Date d3(d1)等价。而赋值运算符载函数两个已存在对象之间进行赋值,d1和d2都是已经存在的对象,故d2=d1调用的是赋值重载。

6.6 前置++和后置++运算符重载

        前置++

        下面我们来尝试对Date类实现前置++运算符的重载,用于对天数进行自增。所谓前置++,就是先自增1再返回结果,如下:

class Date
{
public:Date(int year = 2023, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date& operator++() //前置++运算符重载{_day += 1; //先自增  (注意:这里为了演示先忽略日期进位,进位处理请看Date类的模拟实现)return *this; //返回自身,为了提高效率,用引用返回}
private:int _year;int _month;int _day;
};

        后置++

        而后置++,就是先返回结果再进行自增。但有个问题:前置++和后置++都是一元运算符,即只有一个操作数:对象本身。在作为成员函数重载时,重载函数就是无参的,那编译器要如何区分是前置++还是后置++呢?

int main()
{Date d;//编译器要如何区分哪个operator++()函数是前置++,哪个又是后置++ ???++d; //相当于d.operator++()d++; //也相当于d.operator++()return 0;
}

        为此,C++做了特殊规定:后置++重载时多增加一个int类型的参数用于占位,但调用函数时该参数不用传递,编译器会自动传递。

class Date
{
public:Date(int year = 2023, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date operator++(int) //后置++运算符重载,int用于占位区分{Date temp(*this); //由于要返回+1前的结果,所以先对对象进行拷贝_day += 1; //然后天数+1return temp; //然后将+1前的对象返回。由于temp出了函数就销毁了,故不能用引用返回}
private:int _year;int _month;int _day;
};
int main()
{Date d;++d; //相当于d.operator++()d++; //相当于d.operator++(int),int类型参数由编译器自动传递return 0;
}

七. const成员函数

7.1 问题引入

        我们来看下面的代码:

class Date
{
public:void Print(){cout << "void Print()" << endl;}
private:int _year = 2023;int _month = 1;int _day = 1;
};int main()
{const Date d;d.Print();return 0;
}

        上面的代码编译时会进行报错,报错原因如下

由于对象d被const所修饰,故其类型为const Date,表示不能对类的任何成员进行修改。当d调用Print函数时,传入的实参就是d的地址,即const Date*类型的指针,而在Print函数中,用于接收的this指针却是Date*类型的,这无疑是一种权限的放大,故编译器会进行报错。


那要怎么解决这个问题呢?很简单,给this指针加上const进行修饰即可。


7.2 定义方式

        由于this指针是 "非静态成员函数" 的隐藏形参,我们无法显式地去定义this指针,因此C++规定,在成员函数后面加上const代表它为const成员函数,其this指针的类型为const A* this,编译器会自动进行识别处理


         回到上面的代码,当我们在Print函数后加上const后,程序就正常运行啦

class Date
{
public:void Print() const //this指针的类型是const Date*{cout << "void Print() const" << endl;}
private:int _year = 2023;int _month = 1;int _day = 1;
};int main()
{const Date d;d.Print();return 0;
}


7.3 使用细则

  1. const修饰的成员函数和非const修饰的成员函数可以构成函数重载,调用时会调用最匹配的函数,一般用作读写的分离
    class Date
    {
    public: void Print()  //非const成员函数{cout << "void Print()" << endl;}void Print() const  //const成员函数{cout << "void Print() const" << endl;}
    private:int _year = 2023;int _month = 1;int _day = 1;
    };int main()
    {const Date d1;Date d2;d1.Print(); //const类型的对象调用cosnt成员函数d2.Print(); //非const类型的对象调用非const成员函数return 0;
    }

  2. 建议给只读成员函数加上const修饰,即内部不涉及修改成员变量的函数 

  3. 构造函数不能加const修饰。构造函数是对成员变量进行初始化的,显然会涉及到成员变量的修改。


 八. 取地址运算符重载

        取地址运算符重载有两个版本,一个是const的,一个是非const的。这两个成员函数也是我们一开始讲的默认成员函数,当用户没有显式定义时,编译器会自动生成。

class Date
{
public:Date* operator&() //非const版本,this指针类型为Date*{return this;}const Date* operator&()const //const版本,this指针类型为const Date*{return this;}
private:int _year; // 年int _month; // 月int _day; // 日
};

这两个版本的成员函数和上面的不同,一般使用编译器默认生成的取地址重载即可。只有特殊情况下,才需要显式定义,比如想让别人获取到指定的内容!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/77613.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Excel显示列号

默认表格打开列以字母显示 设置方法 文件 -> 工具 -> 选项 -> 常规与保存 设置后效果如下图

使用高斯混合模型进行聚类

一、说明 高斯混合模型 &#xff08;GMM&#xff09; 是一种基于概率密度估计的聚类分析技术。它假设数据点是由具有不同均值和方差的多个高斯分布的混合生成的。它可以在某些结果中提供有效的聚类结果。 二、Kmean算法有效性 K 均值聚类算法在每个聚类的中心周围放置一个圆形边…

推荐10个AI人工智能技术网站(一键收藏,应有尽有)

1、Mental AI MentalAI&#xff08;https://ai.ciyundata.com/&#xff09;是一种基于文心大模型的知识增强大语言模型&#xff0c;专注于自然语言处理&#xff08;NLP&#xff09;领域的技术研发。它具备强大的语义理解和生成能力&#xff0c;能够处理各种复杂的自然语言任务。…

Python第一次作业练习

题目分析&#xff1a; """ 参考学校的相关规定。 对于四分制&#xff0c;百分制中的90分及以上可视为绩点中的4分&#xff0c;80 分及以上为3分&#xff0c;70 分以上为2分&#xff0c;60 分以上为1分; 五分制中的5分为四分制中的4分&#xff0c;4分为3分&#…

RNA 37. SCI 文章中基于转录组计算肿瘤免疫浸润得分

这期推荐软件包 xCell:数字化描绘组织细胞异质性景观&#xff0c;通过它可以计算bulk 转录组的免疫浸润得分&#xff0c;下面我们就看看怎么来实现吧&#xff01; 简 介 组织是由许多细胞类型组成的复杂环境。在癌症领域&#xff0c;了解肿瘤微环境中的细胞异质性是一个新兴…

【css | loading】好看的loading特效

示例&#xff1a; https://code.juejin.cn/pen/7277764394618978365 html <div class"pl"><div class"pl__dot"></div><div class"pl__dot"></div><div class"pl__dot"></div><div c…

第51节:cesium 范围查询(含源码+视频)

结果示例: 完整源码: <template><div class="viewer"><el-button-group class="top_item"><el-button type=

Axure RP 10汉化版下载 Axure RP 10 mac授权码

Axure RP10汉化版是最强大的计划&#xff0c;原型设计和交付给开发人员的方法&#xff0c;而无需编写代码。能够制作逼真的&#xff0c;动态形式的原型。 Axure RP 10汉化版下载 Axure RP 10 mac授权码 RP 10有什么新功能&#xff1f; 1.显示动态面板 使用Axure RP 10&…

docker 镜像内执行命令显示:You requested GPUs: [0] But your machine only has: []

目录 问题描述&#xff1a; 问题解决&#xff1a; 问题描述&#xff1a; 在docker 镜像环境中&#xff0c;执行“docker exec -it container_name /bin/bash “进入容器之后&#xff0c;执行对应的python命令&#xff0c;显示You requested GPUs: [0] But your machine only…

【深度学习】P1 单层神经网络 - 线性回归(待完成)

单层神经网络 - 线性回归 线性回归基本要素1. 模型2. 模型训练3. 训练数据4. 损失函数5. 优化算法6. 模型预测 线性回归与神经网络1. 神经网络图 以一个简单的房屋价格预测为例&#xff0c;介绍解释线性回归这一单层神经网络。无需纠结于什么是单层神经网络&#xff0c;在本文的…

Hadoop_02

hadoop相比于传统文件系统的优点&#xff1a; 1.无限扩展 2.传统文件元数据分布在不同的机器上难以寻找&#xff0c;通过将元数据统一存放在一个服务器上解决 3.传统文件太大导致上传下载慢&#xff0c;通过分块并行上传到服务器解决 4.副本机制数据不容易丢失&#xff0c;解决…

JavaScript里面的二进制

概述 最近在做IOT设备配网开发的时候&#xff0c;处理了很多跟二进制、字节相关的事情&#xff0c;总结了一下JavaScript中有关二进制方面的一些知识点。 二进制和字节 首先&#xff0c;现代计算机是基于二进制的&#xff0c;从现代计算机电路来说&#xff0c;只有高电平/低电平…

数据在内存中的存储——练习3

题目&#xff1a; 3.1 #include <stdio.h> int main() {char a -128;printf("%u\n",a);return 0; }3.2 #include <stdio.h> int main() {char a 128;printf("%u\n",a);return 0; }思路分析&#xff1a; 首先二者极其相似%u是无符号格式进行…

【Linux】—— 在Linux上进行读写文件操作

前言&#xff1a; 在之前&#xff0c;我已经对进程的相关知识进行了详细的介绍。本期开始&#xff0c;我们将要学习的是关于 “基础I/O”的知识&#xff01;&#xff01;&#xff01; 目录 &#xff08;一&#xff09;C文件接口 &#xff08;二&#xff09;系统文件I/O 1、接…

WebDAV之π-Disk派盘 + BubbleUPnP

BubbleUPnP是一款功能强大的Android播放器,支持UPnP/DLNA多屏互动。它可以将手机内容投屏到电视大屏上,与家人和朋友一起共享。此外,BubbleUPnP还提供了丰富的音乐和影视资源,您可以在线搜索并播放喜欢的内容。 以下是BubbleUPnP的一些主要特点: 1. 支持Chromecast和转码…

WebGL 绘制矩形

上一节绘制了圆点&#xff0c;调用的绘制方法如下&#xff1a;gl.drawArrays(gl.POINTS, 0, 1); 第一个参数明显是个枚举类型&#xff0c;肯定还有其他值&#xff0c;如下所示&#xff1a; POINTS 可视的点LINES 单独线段LINE_STRIP 线条LINE_LOOP 闭合线条TRIANGLES 单独三…

【题解】2596. 检查骑士巡视方案

题解&#xff1a; class Solution {int n,m;bool st[100][100];int flag;int dx[8]{-1,-2,-2,-1,1,2,2,1};int dy[8]{-2,-1,1,2,2,1,-1,-2}; public:bool checkValidGrid(vector<vector<int>>& grid) {m grid.size();n grid[0].size();dfs(grid,0,0,0);ret…

vue3中的吸顶导航交互实现 | VueUse插件

目的&#xff1a;浏览器上下滚动时&#xff0c;若距离顶部的滚动距离大于78px&#xff0c;吸顶导航显示&#xff0c;小于78px隐藏。使用vueuse插件中的useScroll方法​​​​​​​和动态类名控制进行实现 1. 安装 npm i vueuse/core 2. 获得滚动距离 项目中导入&#xff0…

在python程序中用windows的icon

这个exe的弹窗功能会使用到一个ico文件&#xff0c;如图&#xff1a; 用软件GreenfishIconEditorProPortable或者使用在线软件将你需要的图片制作成windows的icon 用程序将ico文件生成文本文件 import base64picture_name "logo.ico" open_pic open("%s…

【100天精通Python】Day56:Python 数据分析_Pandas数据清洗和处理(删除填充插值,数据类型转换,去重,连接与合并)

目录 数据清洗和处理 1.处理缺失值 1.1 删除缺失值&#xff1a; 1.2 填充缺失值&#xff1a; 1.3 插值&#xff1a; 2 数据类型转换 2.1 数据类型转换 2.2 日期和时间的转换&#xff1a; 2.3 分类数据的转换&#xff1a; 2.4 自定义数据类型的转换&#xff1a; 3 数…