• 类和对象下


    目录

    日期类的前置--和后置--

    前置--

    日期类相减:

    cin和cout

     ​编辑

    流插入:

    友元声明:

    流提取:

     const成员

    取地址重载:

    const修饰的取地址重载:

    初始化列表

    为什么要使用初始化列表:

    引用也必须在初始化列表初始化

     

    初始化顺序


    日期类的前置--和后置--

    前置--

    1. Date& Date::operator--()
    2. {
    3. *this -= 1;
    4. return *this;
    5. }

    后置--:

    1. Date Date::operator--(int)
    2. {
    3. Date ret = *this;
    4. *this -= 1;
    5. return ret;
    6. }
    1. #include"Date.h"
    2. void TestDate1()
    3. {
    4. //Date d1(2022, 10, 19);
    5. ///*
    6. //(d1 + (-10)).Print();*/
    7. //(++d1).Print();
    8. //(d1++).Print();
    9. Date d1(2022, 10, 19);
    10. (--d1).Print();
    11. (d1--).Print();
    12. }
    13. int main()
    14. {
    15. TestDate1();
    16. return 0;
    17. }

     对于自定义对象,最好用前置:

    1:前置是返回--之后的值

    2:前置的传引用返回。

    日期类相减:

    1. int Date::operator-(const Date&d)
    2. {
    3. Date max = *this;
    4. Date min = d;
    5. int flag = 1;
    6. if (*this < d)
    7. {
    8. max = d;
    9. min = *this;
    10. flag = -1;
    11. }
    12. int n = 0;
    13. while (min != max)
    14. {
    15. ++n;
    16. ++min;
    17. }
    18. return n*flag;
    19. }
    1. #include"Date.h"
    2. void TestDate1()
    3. {
    4. //Date d1(2022, 10, 19);
    5. ///*
    6. //(d1 + (-10)).Print();*/
    7. //(++d1).Print();
    8. //(d1++).Print();
    9. Date d1(2022, 10, 19);
    10. /*(--d1).Print();
    11. (d1--).Print();*/
    12. Date d2(2023, 11, 23);
    13. cout << d1 - d2 << endl;
    14. }
    15. int main()
    16. {
    17. TestDate1();
    18. return 0;
    19. }

    cin和cout

     

    cin是istream类型的对象 ,cout是ostream类型的对象。

    istream是一个类,ostream也是一个类。

     为什么cin和cout可以自动识别参数的类型:

     cout是流插入,表示把d1的数据插入到cout对象里去。

    cin是流提取,表示把输入的数据提取到d1里去。

    因为库中有这些内置类型的函数重载。

    cout是ostream类型的对象,相当于cout符号重载<<。

    流插入:

    假如我们要在类里面定义流插入函数:

    这种写法是错误的:首先在类内定义的函数,this指针会默认抢占第一个参数的位置,并且对于<<,有两个操作数,也就是有两个函数参数,一个是Date类型的对象,一个是ostream类型的对象。

     我们需要这样写:

    1. void operator<<(ostream&out)
    2. {
    3. out << _year << " " << _month << " " << _day;
    4. }
    1. void TestDate1()
    2. {
    3. Date d1, d2;
    4. /*cin >> d1;
    5. cout << d2;*/
    6. d1 << cout;
    7. }
    8. int main()
    9. {
    10. TestDate1();
    11. return 0;
    12. }

    但是我们发现只能这样写:

    这样就没有可读性了,原因在哪里?

     对于双操作数的符号,第一个参数参数就是左操作数,而对于成员函数,第一个位置被this指针抢占,所以我们在类内无法实现cout。

    我们只好写成全局函数:

    1. void operator<<(ostream&out, const Date&d)
    2. {
    3. out << d._year << " " << d._month << " " << d._day;
    4. }

    但是在全局的函数无法访问类内私有的成员变量。

    我们可以先把成员变量设置为共有的。

    报错的原因是:我们在头文件中定义了全局函数,头文件在两个源文件变为目标文件时进行展开 

    两个目标文件分别有两个同名函数,两个符号表也有两个同名函数,所以重定义问题。

    不仅仅是我们运算符重载出现的问题,我们在头文件中定义全局函数就容易产生问题:

    1. void Print(const Date&d)
    2. {
    3. cout << d._year << "/" <"/" << d._day << endl;
    4. }

    注意:在头文件中尽量不要定义全局变量和全局函数,否则会导致重定义的问题。 

     我们如何解决这个问题:

    静态函数:

    static修饰全局变量和全局函数时会改变其链接属性,使其只在当前文件中可见,其他文件不可见,也无法使用。

    声明和定义分离:

    我们在cpp文件中进行定义,在.h文件中进行声明。

    头文件Date.h会在两个cpp文件中展开,但是Date.h的文件中只有声明没有定义,所以无法进入符号表。

    1. void TestDate1()
    2. {
    3. Date d1, d2;
    4. /*cin >> d1;
    5. cout << d2;*/
    6. cout << d1;
    7. }
    8. int main()
    9. {
    10. TestDate1();
    11. return 0;
    12. }

     需要优化的问题:

    无法像cout调用内置类型一样连续调用。 

    从左往右进行调用,cout<

    1. ostream& operator<<(ostream&out, const Date&d)
    2. {
    3. out << d._year << " " << d._month << " " << d._day;
    4. return out;
    5. }
    1. void TestDate1()
    2. {
    3. Date d1, d2;
    4. /*cin >> d1;
    5. cout << d2;*/
    6. cout << d1 << d2 << endl;
    7. }
    8. int main()
    9. {
    10. TestDate1();
    11. return 0;
    12. }

     我们还可以用内联函数解决:

    注意:内联函数直接定义在头文件中即可,不能声明和定义分离:

    1. inline ostream& operator<<(ostream&out, const Date&d)
    2. {
    3. out << d._year << " " << d._month << " " << d._day;
    4. return out;
    5. }

    内联的原理:内联会在调用时直接展开,不会进入符号表。

    但是以上的方法都是在我们的成员变量为公有的前提下,假如成员变量为私有呢?

    第一种方法:创建函数取成员变量。

    1. int GetYear() const
    2. {
    3. return _year;
    4. }
    5. int GetMonth() const
    6. {
    7. return _year;
    8. }
    9. int GetDay() const
    10. {
    11. return _year;
    12. }
    1. inline ostream& operator<<(ostream&out, const Date&d)
    2. {
    3. out <GetYear() << " " << d.GetMonth() << " " << d.GetDay();
    4. return out;
    5. }

    友元声明:

    friend ostream& operator<<(ostream&out, const Date&d);

    我们在类中进行声明,表示我们这个运算符重载函数是一个友元函数,友元函数可以直接访问成员变量。

    流提取:

    1. inline istream& operator>>(istream& in, Date&d)
    2. {
    3. in >> d._year >> d._month >> d._day;
    4. return in;
    5. }

    对于流提取函数,我们也要进行友元声明。

     

     const成员

    1. void TestDate2()
    2. {
    3. const Date d1(2022, 9, 10);
    4. d1.Print();
    5. }
    6. int main()
    7. {
    8. TestDate2();
    9. return 0;
    10. }

    我们发现,用const修饰的对象无法调用Print函数:

    1. void Print()
    2. {
    3. cout << _year << "/" << _month << "/" << _day << endl;
    4. }

    原因如下:

    在我们的Print函数中,有一个隐藏的this指针,该指针的类型是Date*,const用来修饰this,防止对this本身进行修改。

    而const修饰的是d2,我们传参的时候传递的也是d2,d2用const修饰是只读的,而对于this,我们却可以访问this指向的数据。

    更简单的理解:

     

    把d2的地址传递给this,发生了权限的扩大。

    我们如何避免这种问题呢?

    我们可以后置const

    后置const表示把this的类型由Date*this修改为const Date*this的形式。

    1. void TestDate2()
    2. {
    3. const Date d1(2022, 9, 10);
    4. d1.Print();
    5. Date d2(2022, 9, 11);
    6. d2.Print();
    7. }
    8. int main()
    9. {
    10. TestDate2();
    11. return 0;
    12. }

    并且对于普通的对象也可以调用print函数,因为权限的缩小是不受限制的。

    我们再举一个例子:

    同样属于权限的放大,我们后置const。
     

    凡是函数内部没有改变成员变量的,这些成员函数的声明和定义都要加上const修饰。

    取地址重载:

    1. Date*operator&()
    2. {
    3. return this;
    4. }
    1. void TestDate2()
    2. {
    3. const Date d1(2022, 9, 10);
    4. d1.Print();
    5. Date d2(2022, 9, 11);
    6. d2.Print();
    7. cout << &d2 << endl;
    8. }
    9. int main()
    10. {
    11. TestDate2();
    12. return 0;
    13. }

    这里的取地址相当于调用取地址重载函数:

    const修饰的取地址重载:

    1. const Date*operator&() const
    2. {
    3. return this;
    4. }

    对于const类型的对象,我们调用该函数。

    1. void TestDate2()
    2. {
    3. const Date d1(2022, 9, 10);
    4. d1.Print();
    5. Date d2(2022, 9, 11);
    6. d2.Print();
    7. cout << &d2 << endl;
    8. cout << &d1 << endl;
    9. }
    10. int main()
    11. {
    12. TestDate2();
    13. return 0;
    14. }

    这两个函数,我们即使不写,编译器也会自动生成:

    1. void TestDate2()
    2. {
    3. const Date d1(2022, 9, 10);
    4. d1.Print();
    5. Date d2(2022, 9, 11);
    6. d2.Print();
    7. cout << &d2 << endl;
    8. cout << &d1 << endl;
    9. }
    10. int main()
    11. {
    12. TestDate2();
    13. return 0;
    14. }

    我们注释掉取地址重载函数进行运行:

    我们如何使用取地址重载呢?

    假如我们规定,在日期类中不能使用取地址,我们可以这样写:

    1. Date*operator&()
    2. {
    3. return nullptr;
    4. }
    5. const Date*operator&() const
    6. {
    7. return nullptr;
    8. }

    初始化列表

    我们可以在构造函数中使用初始化列表,构造函数可以什么都不用写(针对的是日期类)

     写一个栈类的初始化列表:

    1. Stack(int capacity=4)
    2. :_a(((int*)malloc(sizeof(int)*_capacity)))
    3. , _capacity(capacity)
    4. , _top(0)
    5. {
    6. if (_a == nullptr)
    7. {
    8. perror("malloc fail");
    9. exit(-1);
    10. }
    11. memset(_a, 0, sizeof(int)*capacity);
    12. }

    也可以这样写:

    1. Stack(int capacity=4)
    2. : _capacity(capacity)
    3. , _top(0)
    4. {
    5. _a = ((int*)malloc(sizeof(int)*_capacity));
    6. if (_a == nullptr)
    7. {
    8. perror("malloc fail");
    9. exit(-1);
    10. }
    11. memset(_a, 0, sizeof(int)*capacity);
    12. }

    所以初始化列表和函数体内初始化可以混着一起使用。

    为什么要使用初始化列表:

    1. class B
    2. {
    3. public:
    4. B()
    5. {
    6. _n = 10;
    7. }
    8. private:
    9. const int _n;
    10. };
    11. int main()
    12. {
    13. B b;
    14. return 0;
    15. }

    _n是cons类型的对象,我们知道const类型的对象只能在定义的时候初始化,我们的构造函数是无法对_n进行修改的。

    const修饰的对象定义的时候必须初始化。

    在这里是_n的声明,创建对象时才会定义。

     

    这里是定义,是整体定义,我们无法对类内的一些const修饰的对象定义。

     对象的每一个成员变量是在初始化列表时定义的。

    我们可以用初始化列表来定义const修饰的成员变量:

    1. B()
    2. :_n(10)
    3. {
    4. ;
    5. }

    每一个成员变量都会走初始化列表,就算没有显示,也会走。

    1. class B
    2. {
    3. public:
    4. B()
    5. :_n(10)
    6. {
    7. ;
    8. }
    9. private:
    10. const int _n;
    11. int _m;
    12. };

    这里的_m也会走初始化列表:

    如果我们没有在初始化列表中显示的写,对于内置类型,默认是随机值,对于自定义类型调用其默认构造。

     对于这个随机值,c++打了一个补丁:

    我们可以在成员变量的声明位置给缺省值。

     这个缺省值是在初始化列表中使用的,假如我们的初始化列表没有显示写成员变量,就用缺省值。

    总结:

     

    1. class A
    2. {
    3. public:
    4. A(int a)
    5. {
    6. ;
    7. }
    8. private:
    9. int _aa;
    10. };
    11. class B
    12. {
    13. public:
    14. B()
    15. :_n(10)
    16. , _m(2)
    17. {
    18. ;
    19. }
    20. private:
    21. const int _n;
    22. int _m=1;
    23. A _a;
    24. };
    25. int main()
    26. {
    27. B b;
    28. return 0;
    29. }

     _a走初始化列表时,_a是自定义类型,对于自定义类型调用默认构造,因为A类没有默认构造,所以报错。

    有两种方法解决:

    1:注释掉A的构造函数,使用系统给的默认构造:

    这时候的_aa是随机值。

    方法2:

     

    1. class A
    2. {
    3. public:
    4. A(int a)
    5. {
    6. ;
    7. }
    8. private:
    9. int _aa;
    10. };
    11. class B
    12. {
    13. public:
    14. B()
    15. :_n(10)
    16. , _m(2)
    17. , _a(11)
    18. {
    19. ;
    20. }
    21. private:
    22. const int _n;
    23. int _m=1;
    24. A _a;
    25. };
    26. int main()
    27. {
    28. B b;
    29. return 0;
    30. }

    我们可以在初始化列表给_a一个值,相当于调用_a的构造函数。

    再举一个例子:

    1. class MyQueue
    2. {
    3. public:
    4. MyQueue()
    5. {
    6. }
    7. void push(int x)
    8. {
    9. _pushST.Push(x);
    10. }
    11. private:
    12. Stack _pushST;
    13. Stack _popST;
    14. size_t _size = 0;
    15. };
    16. int main()
    17. {
    18. MyQueue m1;
    19. return 0;
    20. }

    这样写不会报错:对于自定义类型Stack,调用其默认构造,对于内置类型我们有缺省值,就给缺省值0.

    当我们把栈的函数写成是非默认构造就会报错。

     

    引用也必须在初始化列表初始化

     

    1. class B
    2. {
    3. public:
    4. B()
    5. :_n(10)
    6. , _m(2)
    7. , b(_m)
    8. {
    9. ;
    10. }
    11. private:
    12. const int _n;
    13. int _m=1;
    14. int&b;
    15. };
    16. //class MyQueue
    17. //{
    18. //public:
    19. // MyQueue()
    20. // {
    21. // }
    22. // void push(int x)
    23. // {
    24. // _pushST.Push(x);
    25. // }
    26. //private:
    27. // Stack _pushST;
    28. // Stack _popST;
    29. // size_t _size = 0;
    30. //};
    31. int main()
    32. {
    33. //MyQueue m1;
    34. B b;
    35. return 0;
    36. }

    初始化顺序

    1. class A
    2. {
    3. public:
    4. A(int a)
    5. :_a1(a)
    6. , _a2(_a1)
    7. {
    8. }
    9. void Print()
    10. {
    11. cout << a1 << "" << a2 << endl;
    12. }
    13. private:
    14. int _a2;
    15. int _a1;
    16. };
    17. int main()
    18. {
    19. A aa(1);
    20. aa.Print();
    21. }

     初始化顺序是按照成员变量的声明顺序的:

    先对_a2进行定义,_a1还不存在,所以报错原因是编译不通过。

  • 相关阅读:
    USACO 2020 December Contest, Silver
    手写RPC框架(六)整合Netty
    kubernetes Service详解
    23年下半年软考中级软件设计师备考攻略(含报名时间)
    分布式系统中的相关概念
    华为云云耀云服务器L实例评测|企业项目最佳实践之压测 (十一)
    动手学深度学习——Windows下的环境安装流程(一步一步安装,图文并配)
    maven环境变量,安装源,本地仓库配置
    生物通路数据库收录1600+整合的经典通路
    喜报|百华鞋业成功入选2022 年临沂市内外贸产品“三同”企业名单
  • 原文地址:https://blog.csdn.net/qq_66581313/article/details/132897061