• 【C++】类和对象——中


    目录

    前言

    一、构造函数

    二、析构函数

    三、构造函数和析构函数的调用顺序

    ​编辑

    五、运算符重载函数

    1、=运算符重载

    2、==运算符重载

    ​编辑

    ​编辑

    六、七、取地址重载函数

    总结


    前言

    成员函数是C++ 的类和结构体的一个重要特性。这些数据类型可以包含作为其成员的函数。成员函数分为静态成员函数与非静态成员函数。静态成员函数只能访问该数据类型的对象的静态成员。而非静态成员函数能够访问对象的所有成员。在非静态成员函数的函数体内,关键词this指向了调用该函数的对象。这通常是通过thiscall调用协议,将对象的地址作为隐含的第一个参数传递给成员函数。

    如果一个类中什么成员都没有,简称为空类。
    空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
    默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

    一、构造函数

    构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。特别的一个类可以有多个构造函数 ,可根据其参数个数的不同或参数类型的不同来区分它们 即构造函数的重载。

    构造函数是一种特殊的成员函数,构造函数的主要任务是初始化对象,而不是开辟空间来创建对象

    创建对象主要是由操作系统来完成,以普通的局部变量为例。

    创建一个对象要在栈上建立栈帧,用ebp和esp两个与栈帧的创建和维护的寄存器来维护栈帧。

    编译器预先计算栈空间大小,提前预留足够的空间,ebp和esp声明这块空间已经被使用。当对象销毁时,esp和ebp向上移动。所谓的空间销毁并不是真的销毁这块空间,而是声明这块空间不能再被使用。

    而初始化对象是对象已经创建好了,我们给它赋值。就好比int a = 10;给a初始化赋值为10。

    1.构造函数的函数名与类名相同。

    2.无返回值。

    3.对象实例化时编译器自动调用对应的构造函数。
    4.构造函数可以重载。
    5.如果类中没有显式定义构造函数,C++编译器会自动生成一个无参的默认构造函数,一旦
    用户显示定义,编译器将不再生成
    在日常实例化对象时,我们要将对象初始化,可能会忘记!导致崩溃出现随机值
    为了保证对象一定初始化,我们可以使用构造函数。
    一般的类都不会让编译器默认生成构造函数,都会自己写,显示写一个全缺省的构造函数就可以了
    特殊情况下才会默认生成。
    例如。我们用两个栈实现一个队列,队列这个类中的成员变量是两个栈类型的变量,
    队列就可以直接用编译器默认提供的构造函数就可以了。前提是栈的构造函数已经写好了。

    C++中,数据类型的分类:

    内置类型/基本类型:int / double / char /long / 各种指针(包括自定义类型的指针)

    自定义类型:struct / class / union / enum ……

    C++的默认构造函数对内置类型是不会进行处理的,自定义类型回去调用它的默认构造函数

    1. #include
    2. using namespace std;
    3. class Date
    4. {
    5. public:
    6. void Show()
    7. {
    8. cout << _year << "/ " << _month << "/ " << _day << endl;
    9. }
    10. private:
    11. int _year;
    12. int _month;
    13. int _day;
    14. };
    15. void TestDate()
    16. {
    17. Date d1;
    18. d1.Show();
    19. }
    20. int main()
    21. {
    22. TestDate();
    23. return 0;
    24. }

    将日期类的数据打印出来是随机值。

    既然说到了默认构造函数,那么什么是默认构造函数?

     我们自己不写,编译器帮我们自动生成的。还有无参的构造函数和全缺省的构造函数都被称为默认构造函数,并且默认构造函数只能有一个。

    因为编译器自动生成的和我们写好的必然只能有一个,无参的和全缺省的构造函数虽然构成函数重载,并且语法上是支持这种写法的。但是当我们实例化对象时,不传参数时,编译器无法确定到底要调用哪个函数会出现

    代码如下:

    1. #include
    2. using namespace std;
    3. class Date
    4. {
    5. public:
    6. Date()
    7. {
    8. _year = 1970;
    9. _month = 1;
    10. _day = 1;
    11. }
    12. Date(int year = 1970, int month = 1, int day = 1)
    13. {
    14. _year = year;
    15. _month = month;
    16. _day = day;
    17. }
    18. void Show()
    19. {
    20. cout << _year << "/ " << _month << "/ " << _day << endl;
    21. }
    22. private:
    23. int _year;
    24. int _month;
    25. int _day;
    26. };
    27. void TestDate()
    28. {
    29. Date d1;
    30. d1.Show();
    31. }
    32. int main()
    33. {
    34. TestDate();
    35. return 0;
    36. }

    一般情况下,对象初始化惯例分为两种,默认值初始化,给定值初始化

    我们合二为一,给一个全缺省的构造函数。

    我们会发现C++中的构造函数及其诡异,在C++11中,打了一个补丁,使我们能够方便一点来定义构造函数

    1. class Date
    2. {
    3. public:
    4. Date(int year = 1970, int month = 1, int day = 1)
    5. {
    6. _year = year;
    7. _month = month;
    8. _day = day;
    9. }
    10. void Show()
    11. {
    12. cout << _year << "/ " << _month << "/ " << _day << endl;
    13. }
    14. private:
    15. int _year = 1;
    16. int _month = 1;
    17. int _day = 1;
    18. };

    注意这里面不是初始化,这里是我们给它缺省值,因为这是类的声明部分,声明没有开辟空间,自然不可能是定义。

    总结:默认构造函数,不传参就可以调用

    我们自己不写 :编译器自动生成

    我们自己写的 :无参构造函数

    我们自己写的 :全缺省构造函数

    二、析构函数

    析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象的销毁是由编译器完成的。对象在销毁时自动调用析构函数,完成对象中的资源清理工作。

    1. 析构函数名是在类名前加上字符 ~。
    2. 无参数无返回值类型。
    3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重
    4. 对象生命周期结束时,C++编译系统系统自动调用析构函数

    析构函数与构造函数类似,对于内置类型并不处理,对于自定义类型去调用自定义类型的析构函数。

    一些类需要显示的写析构函数。比如Stack Queue……

    一些类并不需要显示的写出析构函数。比如:没有动态开辟的类。或者是类中的成员变量是自定义类型。

    1. class Date
    2. {
    3. public:
    4. Date(int year = 1970, int month = 1, int day = 1)
    5. {
    6. _year = year;
    7. _month = month;
    8. _day = day;
    9. }
    10. void Show()
    11. {
    12. cout << _year << "/ " << _month << "/ " << _day << endl;
    13. }
    14. ~Date()
    15. {
    16. cout << "this is ~Date" << endl;
    17. }
    18. private:
    19. int _year = 1;
    20. int _month = 1;
    21. int _day = 1;
    22. };

    因为Date并不需要我们显示的写出析构函数,我们只是打印出一句话来证明已经调用了析构函数。

    三、构造函数和析构函数的调用顺序

    我们知道一个对象的生成要调用构造函数,销毁调用析构函数。但是多个对象创建时的构造函数和析构函数的顺序呢?

    1. #include
    2. using namespace std;
    3. class A
    4. {
    5. public:
    6. A(int a)
    7. {
    8. _a = a;
    9. cout << "A(int a)->" << _a << endl;
    10. }
    11. ~A()
    12. {
    13. cout << "~A()->" << _a << endl;
    14. }
    15. private:
    16. int _a;
    17. };
    18. A aa3(3);
    19. void test()
    20. {
    21. static A aa0(0);
    22. A aa1(1);
    23. A aa2(2);
    24. static A aa4(4);
    25. }
    26. int main()
    27. {
    28. test();
    29. return 0;
    30. }

     

    我们以这个例子为例:

    先来说明构造函数的顺序。第一个创建的是aa3,因为aa3是全局对象,在编译阶段,地址就以确定,就已经成功创建了。而其它几个对象需要test函数建立栈帧,然后创建。

    所以接下来是aa0,aa1,aa2,aa3。因为我们只是创建了对象,并没有使用它,所以很快对象就销毁了,我们还是要明确一点,具有相同生命周期的对象,构造函数和析构函数调用的顺序相反。

    又因为aa4定义为static,它没有存储在test函数的栈帧,而是储存在静态区,所以它不是第一个析构。aa2和aa1析构之后,test函数栈帧销毁,就来到了main函数,因为static修饰的变量生命周期变长,所以和aa3这个全局对象的声明周期一样长aa4是后构造的所以先析构。

    四、拷贝构造函数

    复制构造函数是构造函数的一种,也称拷贝构造函数,它只有一个参数,参数类型是本类的引用。

    复制构造函数的参数可以是 const 引用,也可以是非 const 引用。 一般使用前者,这样既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数去初始化其他对象。一个类中写两个复制构造函数,一个的参数是 const 引用,另一个的参数是非 const 引用,也是可以的。

    如果类的设计者不写复制构造函数,编译器就会自动生成复制构造函数。大多数情况下,其作用是实现从源对象到目标对象逐个字节的复制,即使得目标对象的每个成员变量都变得和源对象相等。编译器自动生成的复制构造函数称为“默认复制构造函数”。

    拷贝构造函数也是构造函数,它是构造函数的一个重载形式,拷贝构造函数的参数只能有一个且必须是类类型对象的引用,使用传值的方式编译器会直接报错,因为会引发无穷递归。

    1. A(A aa)
    2. {
    3. _a = A._a;
    4. }

    至于为什么会出现无穷递归也是比较好理解的;

    我们要想先调用拷贝构造函数,首先要传参,而传参又要调用拷贝构造函数。它们两者相互调用构成递归。当我们传引用时,传递的是对象的别名,对象已经调用过构造函数了,所以不会出现,无穷递归。

    若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成
    拷贝,这种拷贝叫做浅拷贝,或者值拷贝

    所谓的浅拷贝是指如果类中有指针类型,编译器会自动生成一个与要拷贝的类的指针指向同一块空间的指针。如果前一个对象修改内容,拷贝之后的对象也会跟着修改,这样就不叫做拷贝了。并且如果前一个对象销毁了,拷贝对象接着使用已经销毁的空间,出现野指针问题。

    浅拷贝也就是一个对象修改会影响另一个对象,前一个对象销毁,会造成同一块空间析构两次,程序崩溃。解决办法是自己实现深拷贝。

    拷贝构造函数典型调用场景:

    使用已存在对象创建新对象

    函数参数类型为类类型对象

    函数返回值类型为类类型对象

    五、运算符重载函数

    C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
    函数名字为:关键字operator后面接需要重载的运算符符号。
    函数原型:返回值类型 operator操作符(参数列表)
    注意:
    不能通过连接其他符号来创建新的操作符:比如operator@
    重载操作符必须有一个类类型参数
    用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
    作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
    .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
    第一个运算符不是 . 而是.*  用来矩阵运算的
    我们知道一件事:内置类型可以直接使用运算符运算,因为编译器知道要如何计算
    自定义类型无法直接使用运算符运算。编译器不知道要如何运算,为了能够支持运算,需要我们自己实现运算符重载

    1、=运算符重载

    我们以时间类为例,我们要重载赋值操作符=,使两个Date类类型能够直接赋值
    同时要避免,自己给自己赋值
    我们在类内实现,操作符重载,传入的第一个参数是左操作数,传入的第二个操作数是右操作数
    在类中实现,我们知道会隐式的传入一个this指针,我们只要在传入一个类类型的对象就可以了,又因为类类型对象一般比较大,所以传引用,并且为了防止修改对象,加上const来修饰。
    同时要返回一个类类型的引用,为了能够实现,链式访问,连续赋值。
    1. Date& Date::operator=(const Date& d)
    2. {
    3. if (this != &d)
    4. {
    5. _year = d._year;
    6. _month = d._month;
    7. _day = d._day;
    8. }
    9. return *this;
    10. }

    总结:

    参数类型:const T&,传递引用可以提高传参效率
    返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
    检测是否自己给自己赋值
    返回*this :要复合连续赋值的含义

    同时赋值操作符重载要在类内部,不能重载成全局函数

    原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的
    赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的
    成员函数

    2、==运算符重载

    我们已经重载过一个运算符,对于==来说就得心应手了
    两个日期相等的条件是 年和年相等,月和月相等,日和日相等。
    只要有一个不相等就是不相等
    1. bool Date::operator==(const Date& d)
    2. {
    3. return _year == d._year && _month == d._month && _day == d._day;
    4. }

    3、>运算符重载

    一个日期大于另一个日期的条件是 年大的就大,年相等的,月大的就大,月相等的看天数

    1. bool Date::operator>(const Date& d)
    2. {
    3. if ((_year > d._year)
    4. || (_year == d._year && _month > d._month)
    5. || (_year == d._year && _month == d._month && _day > d._day))
    6. {
    7. return true;
    8. }
    9. else
    10. {
    11. return false;
    12. }
    13. }

    4、<运算符重载

    为了增强代码复用性,小于实际上就是,不大于和等于。而对于日期类的大于和等于我们已经实现好了,直接使用即可。

    1. bool Date::operator<(const Date& d)
    2. {
    3. return !((*this > d) || (*this == d));
    4. }

    5、>=运算符重载

    大于等于就是小于的对立面

    1. bool Date::operator>=(const Date& d)
    2. {
    3. return !(*this < d);
    4. }

    6、!=运算符重载

    1. bool Date::operator!=(const Date& d)
    2. {
    3. return !(*this == d);
    4. }

    7、+=运算符重载

    对于日期类来说,日期加日期是没有意义的,只有日期加天数有意义,而日期加天数并不是直接相加的,还要考虑每个月的天数,是否是平年闰年。我们将获取每个月的天数封装成一个函数

    1. // 获取某年某月的天数
    2. int GetMonthDay(int year, int month)
    3. {
    4. static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
    5. int day = days[month];
    6. if (month == 2
    7. && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
    8. {
    9. day += 1;
    10. }
    11. return day;
    12. }

    同时,日期加上一个负数就相当与减去一个日期

    1. Date& Date::operator+=(int day)
    2. {
    3. if (day < 0)
    4. {
    5. return *this -= -day;
    6. }
    7. _day += day;
    8. while (_day > GetMonthDay(_year, _month))
    9. {
    10. _day -= GetMonthDay(_year, _month);
    11. _month++;
    12. if (_month == 13)
    13. {
    14. _month = 1;
    15. _year++;
    16. }
    17. }
    18. return *this;
    19. }

    8、+操作符重载

    +操作符就是返回相加之后的对象,而原对象不发生改变

    这时我们需要利用拷贝构造函数来创建一个临时变量,将临时变量利用+=操作符改变之后,将其返回。

    1. Date Date::operator+(int day)
    2. {
    3. Date tmp = *this;
    4. tmp += day;
    5. return tmp;
    6. }

    9、-=操作符重载

    -=的重载思路与+=类似,都是先让day减去天数,然后改变月份和年份

    1. Date& Date::operator-=(int day)
    2. {
    3. if (day < 0)
    4. {
    5. return *this += (-day);
    6. }
    7. _day -= day;
    8. while (_day <= 0)
    9. {
    10. _month--;
    11. if (_month == 0)
    12. {
    13. _month = 12;
    14. _year--;
    15. }
    16. _day += GetMonthDay(_year, _month);
    17. }
    18. return *this;
    19. }

    这里进入循环就进行--month是因为我们要向上一个月进行借位。循环条件是_day <= 0是因为,一个月不可能有0天。

    10、++操作符重载

    ++分为前置++和后置++,前置++比较容易实现,直接天数相加然后返回就可以,而后置++返回的是++之前的值,然后它在加一,后置++需要进行拷贝,返回拷贝即可

    又因为前置++后置++的操作符都是++,编译器无法区分,因此规定后置++的形参列表有一个int标记。

    前置++与后置++构成了函数重载

    1. //前置++
    2. Date& Date::operator++()
    3. {
    4. return *this += 1;
    5. }
    6. //后置++
    7. Date Date::operator++(int)
    8. {
    9. Date tmp = *this;
    10. *this += 1;
    11. return tmp;
    12. }

    11、--操作符重载

    --与++类似

    1. Date Date::operator--(int)
    2. {
    3. Date tmp = *this;
    4. *this -= 1;
    5. return tmp;
    6. }
    7. Date& Date::operator--()
    8. {
    9. return *this -= 1;
    10. }

    12、日期相减返回天数

    我们可以铆钉一个基准值例如1月1日,然后分别从1月1日到两个日期所间隔的天数。

    但是在我们已经实现了多个运算符重载的条件下,我们只要让小的日期加到大的日期,计算加的次数就可以了。

    1. //日期相减返回天数
    2. int Date::operator-(const Date& d)
    3. {
    4. Date max = *this;
    5. Date min = d;
    6. int flag = 1;
    7. if (max < min)
    8. {
    9. max = d;
    10. min = *this;
    11. flag = -1;
    12. }
    13. int day = 0;
    14. while (min < max)
    15. {
    16. ++min;
    17. ++day;
    18. }
    19. return flag * day;
    20. }

    13、友元

    友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多
    用。
    友元分为:友元函数和友元类
    友元函数
    问题:现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对
    象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用
    中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办
    法访问成员,此时就需要友元来解决。operator>>同理。
    友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声
    明,声明时需要加friend关键字
    说明:
    友元函数可访问类的私有和保护成员,但不是类的成员函数
    友元函数不能用const修饰
    友元函数可以在类定义的任何地方声明,不受类访问限定符限制
    一个函数可以是多个类的友元函数
    友元函数的调用与普通函数的调用原理相同

    14、>> <<操作符重载

    我们想让Date类像内置类型一样可以使用cout直接打印出,我们想到了将<<操作符重载

    要想重载<<我们要了解一下cout和cin是什么

    cin使istream的全局对象,cout是ostream的全局对象

    cin和cout能够对内置类型直接使用是因为:库里面写好了运算符重载,自动识别类型是因为它们构成了函数重载。

    运算符重载:让自定义类型对象可以用运算符,转换成调用这个重载函数

    函数重载:支持函数名相同的函数同时存在

    运算符重载和函数重载虽然都用了重载这个词,但是它们之间并没有什么必然联系。

     

    我们在类内实现

    1. ostream& operator<<(ostream& out)
    2. {
    3. out << _year << "/ " << _month << "/ " << _day;
    4. return out;
    5. }

     这是我们在类中实现的<<重载,乍一看是没有什么问题,当我们调用时就会出现这种情况

    1. Date d(2022, 7, 25);
    2. cout << d << endl;

     这是什么情况?

    我们以它的原生的方式调用

    1. Date d(2022, 7, 25);
    2. d.operator<<(cout);

    发现什么错误也没有。

    我们反向调用试一下

    1. Date d(2022, 7, 25);
    2. d << cout;

     以这种诡异的方式<<重载竟然调用成功了

     

    流提取运算符和流插入运算符都要在全局实现

    因为操作符重载,第一个参数是左操作数,第二个参数是右操作数。而<< 和>>操作符都是作为左操作数使用的,在类中它们被当作右操作数使用所以出现了这种情况。

    1. inline ostream& operator<<(ostream& out, const Date& d)
    2. {
    3. out << d._year << "/ " << d._month << "/ " << d._day;
    4. return out;
    5. }
    6. inline istream& operator>>(istream& in, Date& d)
    7. {
    8. cin >> d._year >> d._month >> d._day;
    9. return in;
    10. }

    使用inline来修饰<<和>>重载,因为这两个重载函数调用次数比较频繁

    六、七、取地址重载函数

    将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this
    指针,表明在该成员函数中不能对类的任何成员进行修改。
    1. Date* operator&()
    2. {
    3. return this;
    4. }
    5. const Date* operator&() const
    6. {
    7. return this;
    8. }

    它们是默认成员函数,我们不写编译器会自动生成,自动生成的就够用了,所以一般不需要我们自己写。

    我们知道this指针是 Date* const this 它是为了防止将this的指向改变,而我们在函数后面加入的const是为了防止this指针解引用之后的值修改,也就是为了防止成员变量的值被修改而设立的。


    总结

    以上就是类和对象中篇的内容。

  • 相关阅读:
    React@16.x(42)路由v5.x(7)常见应用场景(4)- 路由切换动画
    Linux中进程管理
    凯撒密码-加密
    java计算机毕业设计ssm前途招聘求职网站的设计与实现
    8、matlab彩色图和灰度图的二值化算法汇总
    kafka知识点总结
    Java求数组中的重复数字
    DailyPractice.2023.10.14
    Redis的各种部署
    【从零开始学习 SystemVerilog】6.1、SystemVerilog 接口—— Interface 概述
  • 原文地址:https://blog.csdn.net/m0_62179366/article/details/125961943