C++入门篇3(类和对象【重点】)-CSDN博客

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

文章目录


img

C++入门篇3类和对象【重点

1、面向过程和面向对象

  • 面向过程是一种以过程为中心的编程思想它将数据和处理数据的方法封装在一起形成一种相互依存的整体——过程。在面向过程的编程中程序的执行流程是由用户在使用中决定的。它通常按功能划分为若干个基本模块这些模块形成一个树状结构各模块之间的关系尽可能简单在功能上相对独立。
    • 举例比如汽车维修可以将其看作一个问题然后按照一系列的步骤来解决如检查引擎、更换零件、修复电路等。每个步骤都是一个函数按照顺序执行
  • 面向对象则是一种更高级的编程思想它将数据和操作数据的方法封装在一起形成一个相互依存的整体——对象。在面向对象的编程中程序流程由对象之间的消息传递决定。同类对象抽象出其共性形成类。类中的大多数数据只能用本类的方法进行处理。类通过一个简单的外部接口与外界发生关系对象与对象之间通过消息进行通信。
    • 举例比如洗衣机可以将其看作一个对象具有“洗衣服”等方法人作为另一个对象具有“加洗衣粉”、“加水”等方法。然后通过对象之间的消息传递执行“人.加洗衣粉”、“人.加水”、“洗衣机.洗衣服”等操作完成洗衣过程。

2、类的引入

  • C语言中结构体只能定义变量而在C++中结构体可以声明定义变量还可以声明定义函数。 比如之前在数据结构初阶中用C语言方式实现的栈结构体中只能定义变量现在以C++方式实现会发现struct中也可以定义函数

    #include <iostream>
    
    using namespace std;
    typedef int DataType;
    
    struct Stack {
        void Init(size_t capacity) {
            _array = (DataType *) malloc(sizeof(DataType) * capacity);
            if (nullptr == _array) {
                perror("malloc申请空间失败");
                return;
            }
            _capacity = capacity;
            _size = 0;
        }
    
        void Push(const DataType &data) {
            // 扩容
            _array[_size] = data;
            ++_size;
        }
    
        DataType Top() {
            return _array[_size - 1];
        }
    
        void Destroy() {
            if (_array) {
                free(_array);
                _array = nullptr;
                _capacity = 0;
                _size = 0;
            }
        }
    
        DataType *_array;
        size_t _capacity;
        size_t _size;
    };
    
    int main() {
        Stack s;
        s.Init(10);
        s.Push(1);
        s.Push(2);
        s.Push(3);
        cout << s.Top() << endl;
        s.Destroy();
        return 0;
    }
    

    上面的结构体C++一般使用class来代替


3、类的定义

class className
{
// 类体由成员函数和成员变量组成
};  // 一定要注意后面的分号

class为定义类的关键字ClassName类的名字{}中为类的主体注意类定义结束时后面分号不能省略
类体中内容称为类的成员类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数

类的两种定义方式

  1. 声明和定义全部放在类体中需注意成员函数如果在类中定义编译器可能会将其当成内联函数处理。

    //栈
    class Stack {
        void Init(size_t capacity) {
            _array = (DataType *) malloc(sizeof(DataType) * capacity);
            if (nullptr == _array) {
                perror("malloc申请空间失败");
                return;
            }
            _capacity = capacity;
            _size = 0;
        }
    
        void Push(const DataType &data) {
            // 扩容
            _array[_size] = data;
            ++_size;
        }
    
        DataType Top() {
            return _array[_size - 1];
        }
    
        void Destroy() {
            if (_array) {
                free(_array);
                _array = nullptr;
                _capacity = 0;
                _size = 0;
            }
        }
    
        DataType *_array;
        size_t _capacity;
        size_t _size;
    };
    
    
  2. 类声明放在.h文件中成员函数定义放在.cpp文件中注意成员函数名前需要加类名::

    //Stack.h文件
    class Stack {
    public:
        void Init(size_t capacity);
    
        void Push(const DataType &data);
    
        DataType Top();
    
        void Destroy();
    
        DataType *_array;
        size_t _capacity;
        size_t _size;
    };
    
    //Stack.cpp文件
    #include "Stack.h"
    
    void Stack::Init(size_t capacity) {
        _array = (DataType *) malloc(sizeof(DataType) * capacity);
        if (nullptr == _array) {
            perror("malloc申请空间失败");
            return;
        }
        _capacity = capacity;
        _size = 0;
    }
    
    void Stack::Push(const DataType &data) {
        // 扩容
        _array[_size] = data;
        ++_size;
    }
    
    DataType Stack::Top() {
        return _array[_size - 1];
    }
    
    void Stack::Destroy() {
        if (_array) {
            free(_array);
            _array = nullptr;
            _capacity = 0;
            _size = 0;
        }
    }
    

    一般情况下采用第二种方式

成员变量命名规则建议在变量名前加_

class Date {
public:
    void Init(int year) {
        _year = year;
    }

private:
    int _year;
};

4、类的访问限定符及封装

4.1、访问限定符

在面向对象编程中类的访问限定符用于控制类成员的访问权限。以下是常见的访问限定符

publicpublic成员可以从任何位置访问包括类的外部和所有从该类派生的子类。
protectedprotected成员只能从其本身、其子类以及其友元类中访问。
privateprivate成员只能从其本身和其友元类中访问。

这三种访问级别为封装和信息隐藏提供了工具。在创建类时应尽可能使更多的成员为privateprotected从而限制对这些成员的访问。这样就可以在不破坏封装性的前提下改变这些成员的实现在一些情况下如果希望某些成员仅能从特定的派生类中访问这时可以使用protected访问级别。而public成员则应尽可能少用它们应仅用于那些真正需要公开的成员。

  • 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
  • 如果后面没有访问限定符作用域就到 } 即类结束。
  • class的默认访问权限为privatestructpublic(因为struct要兼容C)
  • 注意访问限定符只在编译时有用当数据映射到内存后没有任何访问限定符上的区别

4.2、封装

在C++中封装是面向对象编程的三大特性(封装、继承、多态)之一它是一种将数据变量和操作数据的函数捆绑在一起作为一个单独的对象类的技术。通过封装可以隐藏类的内部实现细节只通过类提供的公共接口来访问类中的成员。这有助于保护对象的状态和维护对象的行为。

封装主要通过类和成员函数来实现。类是对象的蓝图它定义了对象的数据和行为。成员函数是类的成员它们定义了如何操作类的数据。通过将数据和操作数据的函数捆绑在一起可以创建一个具有特定功能和行为的对象。

通过合理使用访问限定符可以更好地控制类的访问权限保护类的内部状态和行为不被外部代码随意修改。

举例假设你是一名自行车设计师你的工作是设计并生产自行车。你的客户群体是自行车爱好者他们关心的是自行车的性能、外观和价格。为了满足客户的需求你需要将自行车的设计、生产和销售过程封装起来使得客户只需要关心自行车的性能、外观和价格而不需要关心背后的生产过程


5、类的作用域

  • 类定义了一个新的作用域类的所有成员都在类的作用域中。在类体外定义成员时需要使用::作用域操作符指明成员属于哪个类域。

    class Person {
    public:
        void PrintPersonInfo();
    
    private:
        char _name[20];
        char _gender[3];
        int _age;
    };
    
    // 这里需要指定PrintPersonInfo是属于Person这个类域
    void Person::PrintPersonInfo() {
        cout << _name << " " << _gender << " " << _age << endl;
    }
    

6、类的实例化对象

  • 用类类型创建对象的过程称为类的实例化

    1. 类是对对象进行描述的是一个模型一样的东西限定了类有哪些成员定义出一个类并没有分配实际的内存空间来存储它比如入学时填写的学生信息表表格就可以看成是一个类来描述具体学生信息。

      类就像谜语一样对谜底来进行描述谜底就是谜语的一个实例
      谜语“年纪不大胡子一把主人来了就喊妈妈” 谜底山羊

    2. 一个类可以实例化出多个对象实例化出的对象占用实际的物理空间存储类成员变量。

      class Person {
      public:
          void showInfo();
                           
      public:
          int _age;//这里是声明不是定义所以不占用空间
          char *_name;
          char *_sex
      };
                           
      void Test() {
          //Person._age = 100;  // 编译失败error C2059: 语法错误:“.”
          Person man;//实例化一个对象占用物理空间
          man._age = 10;
          man._name = "hh";
          man._sex = "nan";
          man.showInfo();
      }
      

      做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子类就像是设计图只设计出需要什么东西但是并没有实体的建筑存在同样类也只是一个设计实例化出的对象建好的房子才能实际存储数据占用物理空间。


7、类对象模型

7.1、类对象的存储方式

只保存成员变量成员函数存放在公共的代码段

7.2、结构体类内存对齐规则

  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字对齐数的整数倍的地址处。
    注意对齐数 = 编译器默认的一个对齐数 与 该成员类型里最大的那个类型 的较小值
    VS中默认的对齐数为8
  3. 结构体总大小为最大对齐数所有变量类型最大者与默认对齐参数取最小的整数倍
  4. 如果嵌套了结构体的情况嵌套的结构体对齐到自己的最大对齐数的整数倍处结构体的整
    体大小就是所有最大对齐数含嵌套结构体的对齐数的整数倍。
  • 计算下面程序的类的大小

    // 类中既有成员变量又有成员函数
    class A1 {
    public:
        void f1() {}
    
    private:
        int _a;
    };
    
    // 类中仅有成员函数
    class A2 {
    public:
        void f2() {}
    };
    
    // 类中什么都没有---空类
    class A3 {
    };
    
    int main() {
        cout << sizeof(A1) << endl << sizeof(A2) << endl << sizeof(A3) << endl;//4 1 1
        return 0;
    }
    

    结论一个类的大小实际就是该类中“成员变量”之和当然要注意内存对齐
    注意空类的大小空类比较特殊编译器给了空类一个字节来唯一标识这个类的对象。


8、this指针

8.1、this指针的引出

  • 我们先来定义一个日期类 Date

    class Date {
    public:
        void Init(int year, int month, int day) { //void Init( Date* const this, int year, int month, int day)
            _year = year;
            _month = month;
            _day = day;
          	//this->_year = year;
            //this->_month = month;
            //this->_day = day;
        }
    
        void Print() { // void Print(Date* const this) 
            cout << _year << "-" << _month << "-" << _day << endl;
            //cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
        }
    
    private:
        int _year;// 年
        int _month;// 月
        int _day;// 日
    };
    
    int main() {
        Date d1, d2;
        d1.Init(2022, 1, 11);
      	//d1.Init(&d1, 2022, 1, 11);	
        d2.Init(2022, 1, 12);
        d1.Print();
      	//d1.Print(&d1);
        d2.Print();
        return 0;
    }
    
    • 这里我们注意到Date类实例化了两个对象d1d2d1d2都调用了Init函数和Print函数那么这是怎么做到两个对象传递的数据不会混淆呢其实是this指针帮我们解决的这个问题对于函数void Init(int year, int month, int day);其实它是隐含了this指针的相当于是void Init( Date* const this, int year, int month, int day);那么代码d1.Init(2022, 1, 11);就相当于是d1.Init(&d1, 2022, 1, 11);。函数Print也一样隐含了this指针。有了this指针之后将d1d2分别传给它们的this指针把数据分别给它们的成员变量。

      注意这里&d1this不能显式给出但是this可以显式使用

    • this指针出处C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数this让该指针指向当前对象(函数运行时调用该函数的对象)在函数体中所有“成员变量”的操作都是通过该指针去访问。只不过所有的操作对用户是透明的即用户不需要来传递编译器自动完成。

8.2、this指针的特性

以下是this指针的主要特性

  1. 只能在成员函数中使用this指针只能在类的成员函数中使用不能在全局函数、静态成员函数或其他函数中使用。
  2. 指向调用对象this指针指向调用对象本身也就是当前对象。在成员函数中可以通过this指针来访问对象的成员变量或成员函数。
  3. 构造和析构在成员函数中this指针是在成员函数的开始前构造并在成员函数的结束后清除
  4. 存储位置this指针的存储位置取决于编译器可能是堆栈、寄存器或全局变量这取决于编译器的实现。
  5. 类型this指针的类型是类==类型* const也就是说它是一个指向常量对象的指针也就是this指针不可以修改指向除了传参的时候初始化的时候==。
  6. 隐含的形参this指针是成员函数第一个隐含的指针形参一般情况由编译器通过ecx寄存器自动传递不需要用户传递
  7. 无法获取位置由于this指针只有在成员函数中才有定义所以获得一个对象后不能通过对象使用this指针所以也就无法知道一个对象的this指针的位置。不过可以在成员函数中指定this指针的位置。
// 1.下面程序编译运行结果是 A、编译报错 B、运行崩溃 C、正常运行
class A {
public:
    void Print() {
        cout << "Print()" << endl;
    }

private:
    int _a;
};

int main() {
    A *p = nullptr;
    p->Print();//正常运行
    return 0;
}

// 2.下面程序编译运行结果是 A、编译报错 B、运行崩溃 C、正常运行
class A {
public:

    void PrintA() {
        cout << _a << endl;
    }

private:
    int _a;
};

int main() {
    A *p = nullptr;
    p->PrintA();//运行崩溃
    return 0;
}
  • 为什么第一个程序正常运行因为p是类A的类对象指针那么代码p->Print();相当于p->Print(p),那么函数void Print();里面this指针就指向nullptr虽然传过去的是空指针但是this并没有访问对象也就是让this初始化指向空了。所以程序正常运行。
  • 为什么第二个程序运行崩溃与第一个程序不同的是类A的成员函数Print里面访问了成员变量_a那么也就是访问this->_a但是此时this已经是nullptr了所以不能再去访问成员变量_a了。因此程序运行崩溃。

8.3、C语言和C++实现Stack的对比

  • C语言实现

    typedef int DataType;
    typedef struct Stack {
        DataType *array;
        int capacity;
        int size;
    } Stack;
    
    void StackInit(Stack *ps) {
        assert(ps);
        ps->array = (DataType *) malloc(sizeof(DataType) * 3);
        if (NULL == ps->array) {
            assert(0);
            return;
        }
        ps->capacity = 3;
        ps->size = 0;
    }
    
    void StackDestroy(Stack *ps) {
        assert(ps);
        if (ps->array) {
            free(ps->array);
            ps->array = NULL;
            ps->capacity = 0;
            ps->size = 0;
        }
    }
    
    void CheckCapacity(Stack *ps) {
        if (ps->size == ps->capacity) {
            int newcapacity = ps->capacity * 2;
            DataType *temp = (DataType *) realloc(ps->array,
                                                  newcapacity * sizeof(DataType));
            if (temp == NULL) {
                perror("realloc申请空间失败!!!");
                return;
            }
            ps->array = temp;
            ps->capacity = newcapacity;
        }
    }
    
    void StackPush(Stack *ps, DataType data) {
        assert(ps);
        CheckCapacity(ps);
        ps->array[ps->size] = data;
        ps->size++;
    }
    
    int StackEmpty(Stack *ps) {
        assert(ps);
        return 0 == ps->size;
    }
    
    void StackPop(Stack *ps) {
        if (StackEmpty(ps))
            return;
        ps->size--;
    }
    
    DataType StackTop(Stack *ps) {
        assert(!StackEmpty(ps));
        return ps->array[ps->size - 1];
    }
    
    int StackSize(Stack *ps) {
        assert(ps);
        return ps->size;
    }
    
    int main() {
        Stack s;
        StackInit(&s);
        StackPush(&s, 1);
        StackPush(&s, 2);
        StackPush(&s, 3);
        StackPush(&s, 4);
        printf("%d\n", StackTop(&s));
        printf("%d\n", StackSize(&s));
        StackPop(&s);
        StackPop(&s);
        printf("%d\n", StackTop(&s));
        printf("%d\n", StackSize(&s));
        StackDestroy(&s);
        return 0;
    }
    

    可以看到在用C语言实现时Stack相关操作函数有以下共性

    • 每个函数的第一个参数都是Stack*

    • 函数中必须要对第一个参数检测因为该参数可能会为NULL

    • 函数中都是通过Stack*参数操作栈的

    • 调用时必须传递Stack结构体变量的地址所以每次都需要开辟一个指针的大小的空间存储指针

    结构体中只能定义存放数据的结构操作数据的方法不能放在结构体中即数据和操作数据的方式是分离开的而且实现上相当复杂一点涉及到大量指针操作稍不注意可能就会出

    错。

  • C++实现

    typedef int DataType;
    
    class Stack {
    public:
        void Init() {
            _array = (DataType *) malloc(sizeof(DataType) * 3);
            if (NULL == _array) {
                perror("malloc申请空间失败!!!");
                return;
            }
            _capacity = 3;
            _size = 0;
        }
    
        void Push(DataType data) {
            CheckCapacity();
            _array[_size] = data;
            _size++;
        }
    
        void Pop() {
            if (Empty())
                return;
            _size--;
        }
    
        DataType Top() { return _array[_size - 1]; }
    
        int Empty() { return 0 == _size; }
    
        int Size() { return _size; }
    
        void Destroy() {
            if (_array) {
                free(_array);
                _array = NULL;
                _capacity = 0;
                _size = 0;
            }
        }
    
    private:
        void CheckCapacity() {
            if (_size == _capacity) {
                int newcapacity = _capacity * 2;
                DataType *temp = (DataType *) realloc(_array, newcapacity *
                                                              sizeof(DataType));
                if (temp == NULL) {
                    perror("realloc申请空间失败!!!");
                    return;
                }
                _array = temp;
                _capacity = newcapacity;
            }
        }
    
    private:
        DataType *_array;
        int _capacity;
        int _size;
    };
    
    int main() {
        Stack s;
        s.Init();
        s.Push(1);
        s.Push(2);
        s.Push(3);
        s.Push(4);
    
        printf("%d\n", s.Top());
        printf("%d\n", s.Size());
        s.Pop();
        s.Pop();
        printf("%d\n", s.Top());
        printf("%d\n", s.Size());
        s.Destroy();
        return 0;
    }
    

    C++中通过类可以将数据 以及 操作数据的方法进行完美结合通过访问权限可以控制哪些方法在类外可以被调用即封装在使用时就像使用自己的成员一样更符合人类对一件事物的认知。而且每个方法不需要传递Stack*的参数了编译器编译之后该参数会自动还原即C++中 Stack * 参数是编译器维护的C语言中需用用户自己维护。


9、类的6个默认成员函数

  • 如果一个类里面什么都没有如class A{};我们叫这个类为空类。但是这个空类里面真的什么都没有吗并不是。任何类在什么都不写时编译器会自动生成以下6个默认成员函数

  • 默认成员函数默认成员函数指的是在类中用户没有显式实现编译器自动为所有对象实例化时提供默认行为的特殊成员函数。


10、构造函数

10.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来实现的但是我们C++是提供了默认初始化的函数的默认构造函数可以让我们省去新写一个函数的时间。

    构造函数概念构造函数是一个特殊的成员函数名字与类名相同创建类类型对象时由编译器自动调用以保证每个数据成员都有 一个合适的初始值并且在对象整个生命周期内只调用一次。

10.2、构造函数的特性

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

  • 构造函数的特性包括

    1. 函数名与类名相同
    2. 无返回值
    3. 构造函数可以重载即在类内可以编写多个参数不同的构造函数。
    4. 构造函数在对象构造阶段所调用用于初始化和赋初值
    5. 构造函数在对象整个生命周期内只调用一次
    class Date {
    public:
        //无参构造函数
        Date() {
            _year = 1;
            _month = 1;
            _day = 1;
        }
    
        //带参构造函数
        Date(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;
    };
    
    //Date d1();函数声明
    
    int main() {
        Date d1;//调用无参(默认)构造函数
        //为什么不是 Date d1(); ? 因为这和函数的声明的格式冲突了
        d1.Print();
        Date d2(2023, 10, 23);//调用带参构造函数
        d2.Print();
        return 0;
    }
    
    1. 如果没有显式实现构造函数编译器会自动生成默认构造函数但自定义类型必须调用构造函数进行初始化(后面演示)。如果已经显示实现了构造函数则编译器不会生成默认构造函数这时候调用构造函数初始化就得传参数了。
    class Date {
    public:
    
    //    // 如果用户显式定义了构造函数编译器将不再生成
    //    Date(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类中构造函数屏蔽后代码可以通过编译因为编译器生成了一个无参的默认构造函数
        // 将Date类中构造函数放开代码编译失败因为一旦显式定义任何构造函数编译器将不再生成
        // 无参构造函数放开后报错error C2512: “Date”: 没有合适的默认构造函数可用
        Date d1;
        return 0;
    }
    
    1. 关于编译器生成的默认成员函数很多童鞋会有疑惑不实现构造函数的情况下编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用d1对象调用了编译器生成的默认构造函数但是d1对象_year/_month_day依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用
      解答C++把类型分成==内置类型(基本类型)自定义类型==。内置类型就是语言提供的数据类型如int/char...自定义类型就是我们使用class/struct/union等自己定义的类型看看下面的程序就会发现编译器生成默认的构造函数会对**自定类型成员_t**调用的它的默认成员函数如果这个自定义类型没有默认构造函数那么就需要用到初始化列表下面会讲不过这就相当于套娃因为这个自定义类型的默认构造函数也需要初始化不是随机值的那种下面会讲缺省值等方法。
    class Time {
    public:
        Time() {
            cout << "Time()" << endl;
            _hour = 0;
            _minute = 0;
            _second = 0;
        }
    
    private:
        int _hour;
        int _minute;
        int _second;
    };
    
    class Date {
    private:
        // 基本类型(内置类型)
        int _year;
        int _month;
        int _day;
        // 自定义类型
        Time _t;
    };
    
    int main() {
        Date d;
        return 0;
    }
    

    注意C++11 中针对内置类型成员不初始化的缺陷又打了补丁即内置类型成员变量在类中声明时可以给默认值

    class Time {
    public:
        Time() {
            cout << "Time()" << endl;
            _hour = 0;
            _minute = 0;
            _second = 0;
        }
    
    private:
        int _hour;
        int _minute;
        int _second;
    };
    
    class Date {
    private:
        // 基本类型(内置类型)
        int _year = 1970;
        int _month = 1;
        int _day = 1;
        // 自定义类型
        Time _t;
    };
    
    int main() {
        Date d;
        return 0;
    }
    
    1. 构造函数在对象整个生命周期内只调用一次。无参的构造函数和全缺省的构造函数都称为默认构造函数并且默认构造函数只能有一个。注意无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数都可以认为是默认构造函数。

      class Date {
      public:
          //默认构造函数(无参构造函数)
      //    Date() {
      //        _year = 1;
      //        _month = 1;
      //        _day = 1;
      //    }
      //   //默认构造函数(全缺省构造函数)
      //    Date(int year = 1, int month = 1, int day = 1) {
      //        _year = year;
      //        _month = month;
      //        _day = day;
      //    }
        
      	//我们没写编译器默认生成的构造函数对应下面的缺省成员变量但是如果有构造函数非默认存在你已经定义了自己的构造函
        //数编译器就不会再自动生成默认构造函数还想要调用默认构造函数的话需要声明函数 Date(){}否则调用默认构造函数		//编译器会找不到默认构造函数
      
          void Print() {
              cout << _year << "-" << _month << "-" << _day << endl;
          }
      
      private:
          //基本类型(内置类型),这里使用缺省的时候可以直接使用编译器默认的构造函数会传值帮它初始化为这个缺省值而不是随机值
          int _year = 1970;
          int _month = 1;
          int _day = 1;
      };
      
      int main() {
          Date d;
          d.Print();
          return 0;
      }
      
      class Date {
      public:
      
        	//Date(){} //存在非默认构造函数时候配合下面缺省成员变量使用
          Date(int year, int month, int day) { //你已经定义了自己的构造函数编译器就不会再自动生成默认构造函数
              _year = year;
              _month = month;
              _day = day;
          }
      
          void Print() {
              cout << _year << "-" << _month << "-" << _day << endl;
          }
      
      private:
          //基本类型(内置类型),这里使用缺省的时候可以直接使用编译器默认的构造函数会传值帮它初始化为这个缺省值而不是随机值
          int _year = 1970;
          int _month = 1;
          int _day = 1;
      };
      
      int main() {
          Date d;//报错找不到默认构造函数
          d.Print();
          return 0;
      }
      

11、析构函数

11.1、析构函数概念

析构函数是C++中的一种特殊成员函数它与构造函数相反用于执行一些清理任务通常用于释放分配给对象的内存空间等。

析构函数名是在类名前加上字符~没有参数和返回值一个类有且只有一个析构函数如果用户没有显式定义系统会自动生成默认的析构函数在对象销毁时会自动调用析构函数完成类的一些资源清理工作

11.2、析构函数特性

  • 析构函数的特性包括

    1. 析构函数与类名相同但它前面必须加上波浪号~用以与构造函数相区别。
    2. 析构函数没有返回类型甚至不能声明为void类型。
    3. 析构函数没有参数因此不能被重载
    4. 对象的生命周期结束时系统自动调用析构函数。
    typedef int DataType;
    
    class Stack {
    public:
        Stack(size_t capacity = 3) {
            _array = (DataType *) malloc(sizeof(DataType) * capacity);
            if (NULL == _array) {
                perror("malloc申请空间失败!!!");
                return;
            }
            _capacity = capacity;
            _size = 0;
        }
    
        void Push(DataType data) {
            // CheckCapacity();
            _array[_size] = data;
            _size++;
        }
    
        // 其他方法...
        ~Stack() {
            if (_array) {
                free(_array);
                _array = NULL;
                _capacity = 0;
                _size = 0;
            }
        }
    
    private:
        DataType *_array;
        int _capacity;
        int _size;
    };
    
    void TestStack() {
        Stack s;
        s.Push(1);
        s.Push(2);
    }
    
    int main(){
        TestStack();
        return 0;
    }
    
    1. 编译器生成的默认析构函数对自定类型成员调用它的析构函数。
    class Time {
    public:
        ~Time() {
            cout << "~Time()" << endl;
        }
    
    private:
        int _hour;
        int _minute;
        int _second;
    };
    
    class Date {
    private:
        // 基本类型(内置类型)
        int _year = 1970;
        int _month = 1;
        int _day = 1;
        // 自定义类型
        Time _t;
    };
    
    int main() {
        Date d; //输出 ~Time()
      	// 在main方法中根本没有直接创建Time类的对象为什么最后会调用Time类的析构函数
        // 因为main方法中创建了Date对象d而d中包含4个成员变量其中_year, _month,_day三个是
        // 内置类型成员销毁时不需要资源清理最后系统直接将其内存回收即可而_t是Time类对象所以在
        // d销毁时要将其内部包含的Time类的_t对象销毁所以要调用Time类的析构函数。但是main函数
        // 中不能直接调用Time类的析构函数实际要释放的是Date类对象所以编译器会调用Date类的析构函
        // 数而Date没有显式提供则编译器会给Date类生成一个默认的析构函数目的是在其内部调用Time
        // 类的析构函数即当Date对象销毁时要保证其内部每个自定义对象都可以正确销毁
        // main函数中并没有直接调用Time类析构函数而是显式调用编译器为Date类生成的默认析构函数
        // 注意创建哪个类的对象则调用该类的构造函数销毁那个类的对象则调用该类的析构函数
        return 0;
    }
    
    1. 默认析构函数通常用于释放==对象在其生命周期中分配的资源例如内存、文件句柄等。当对象==不再使用时系统会自动调用默认析构函数以释放对象所占用的资源如果对象动态申请了内存那么就只释放对象这个申请的内存不一定释放可能造成内存泄露)。

      需要注意的是如果类中没有显式定义析构函数系统会自动生成一个默认析构函数。但是如果类中分配了动态内存或创建了其他需要清理的资源那么显式定义析构函数是必要的以便正确释放这些资源比如Stack类

    2. 对象对构造函数和析构函数的调用顺序

      局部对象和全局对象的构造函数和析构函数的调用顺序是不同的。

      对于局部对象它们的构造函数将在它们被创建时立即调用而析构函数将在它们所在的作用域结束时被调用。也就是说当程序离开该作用域时局部对象的析构函数将被调用。

      如果一个作用域内定义了多个局部对象那么它们的构造函数将按照它们在代码中定义的顺序依次被调用。同样地它们的析构函数也将按照相反的顺序被调用即最后创建的局部对象将首先被销毁。

      对于全局对象它们的构造函数将在程序开始时被调用而析构函数将在程序结束时被调用。因此全局对象的构造函数和析构函数的调用顺序与程序的生命周期相同。

      对于静态对象分为静态局部对象静态全局对象静态局部对象调用构造函数顺序和局部对象一样只会在程序第一次调用此函数时调用一次调用析构函数是在函数调用结束时被调用。静态全局对象调用构造函数在程序中的所有函数包括main函数执行之前调用调用析构函数是在main函数执行完毕或调用exit函数时被调用。

      需要注意的是全局对象可以被定义在多个文件中。如果全局对象在不同的文件中定义那么每个文件中全局对象的构造函数将按照它们在文件中定义的顺序依次被调用。同样地每个文件中全局对象的析构函数也将按照它们在文件中定义的顺序依次被调用。因此在编写多文件程序时需要注意确保全局对象在所有相关的文件中都被正确地初始化和销毁。

      • 下列程序对象的构造函数和析构函数的调用顺序是

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

        答案构造函数C A B D析构函数 B A D C

        分析:

        1. 类的析构函数调用一般按照构造函数调用的相反顺序进行调用但是要注意static对象的存在因为static改变了对象的生存作用域需要等待程序结束时才会析构释放对象
        2. 全局对象先于局部对象进行构造
        3. 局部对象按照出现的顺序进行构造无论是否为static
        4. 所以构造的顺序为 C A B D
        5. 析构的顺序按照构造的相反顺序析构只需注意static改变对象的生存作用域之后会放在局部对象之后进行析构
        6. 因此析构顺序为B A D C

12、拷贝构造函数

12.1、拷贝构造函数概念

拷贝构造函数是一种特殊的构造函数它用于创建一个对象的副本。在C++中拷贝构造函数用于复制一个对象以便在程序中进行各种操作。拷贝构造函数的形参必须是引用且形参必须是只有一个当前类对象单形参但并不限制为const一般普遍的会加上const限制。此函数经常用在函数调用时用户定义类型的值传递及返回。如果没有显式地定义拷贝构造函数编译器会生成一个默认的拷贝构造函数这个默认的拷贝构造函数会简单地复制每个数据成员的值。如果显式地定义了拷贝构造函数编译器就不会生成默认的拷贝构造函数程序员需要自己实现拷贝构造函数以确保正确地复制对象的数据。在拷贝构造函数中通常需要将源对象的数据成员的值复制到新对象中特别地如果类中包含指针成员需要确保拷贝出来的对象不与源对象共享这些指针所指向的内存防止被析构两次

12.2、拷贝构造函数的特性

  • 拷贝构造函数是一种特殊的构造函数其特性包括

    1. 拷贝构造函数是构造函数的一个重载形式

    2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用使用传值方式编译器直接报错因为会引发无穷递归调用

      class Date {
      public:
          Date(int year = 1900, int month = 1, int day = 1) {
              _year = year;
              _month = month;
              _day = day;
          }
          // Date(const Date& d)  // 正确写法
          Date(const Date d)
          // 错误写法编译报错会引发无穷递归
          {
              _year = d._year;
              _month = d._month;
              _day = d._day;
          }
      private:
          int _year;
          int _month;
          int _day;
      };
      
      int main() {
          Date d1;
          Date d2(d1);
          return 0;
      }
      

    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 = 1970;
          int _month = 1;
          int _day = 1;
          // 自定义类型
          Time _t;
      };
      
      int main() {
          Date d1;
          // 用已经存在的d1拷贝构造d2此处会调用Date类的拷贝构造函数
          // 但Date类并没有显式定义拷贝构造函数则编译器会给Date类生成一个默认的拷贝构造函数
          Date d2(d1);
          return 0;
      }
      

      注意在编译器生成的默认拷贝构造函数中内置类型是按照字节方式直接拷贝的而自定义类型调用其拷贝构造函数完成拷贝的

    4. 拷贝构造函数用于创建一个新的对象并将其初始化为另一个已存在的对象的副本。

    5. 拷贝构造函数通常需要将源对象的数据成员的值复制到新对象中。特别地如果类中包含指针成员需要确保拷贝出来的对象不与源对象共享这些指针所指向的内存即需要完成深拷贝得自己重写拷贝构造函数

      // 这里会发现下面的程序会崩溃掉这里就需要我们以后讲的深拷贝去解决。
      typedef int DataType;
      
      class Stack {
      public:
          Stack(size_t capacity = 10) {
              _array = (DataType *) malloc(capacity * sizeof(DataType));
              if (nullptr == _array) {
                  perror("malloc申请空间失败");
                  return;
              }
              _size = 0;
              _capacity = capacity;
          }
      
          void Push(const DataType &data) {
              // CheckCapacity();
              _array[_size] = data;
              _size++;
          }
      
          ~Stack() {
              if (_array) {
                  free(_array);
                  _array = nullptr;
                  _capacity = 0;
                  _size = 0;
              }
          }
      
      private:
          DataType *_array;
          size_t _size;
          size_t _capacity;
      };
      
      int main() {
          Stack s1;
          s1.Push(1);
          s1.Push(2);
          s1.Push(3);
          s1.Push(4);
          Stack s2(s1);
          return 0;
      }
      

      这里s1s2_array指向同一块空间那么当调用结束后会调用两次析构函数s1s2分别一次,则_array指向的空间就被释放两次第二次释放的时候就会报错第一次释放后这块空间就不属于_array

      所以解决办法就是不使用默认的拷贝构造函数自己来写拷贝构造函数让这个s1s2_array不指向同一个空间s2_array重新开辟一块空间这就是深拷贝也就是啥变量空间都重新搞一次

      typedef int DataType;
      
      class Stack {
      public:
          Stack(size_t capacity = 10) {
              _array = (DataType *) malloc(capacity * sizeof(DataType));
              if (nullptr == _array) {
                  perror("malloc申请空间失败");
                  return;
              }
              _size = 0;
              _capacity = capacity;
          }
      
          Stack (const Stack& st){
              this->_array = (DataType *) malloc(st._capacity * sizeof(DataType));
              if (nullptr == this->_array) {
                  perror("malloc申请空间失败");
                  return;
              }
              this->_size = st._size;
              this->_capacity = st._capacity;
          }
      
          void Push(const DataType &data) {
              // CheckCapacity();
              _array[_size] = data;
              _size++;
          }
      
          ~Stack() {
              cout << "~Stack()" << endl;
              if (_array) {
                  free(_array);
                  _array = nullptr;
                  _capacity = 0;
                  _size = 0;
              }
          }
      
      private:
          DataType *_array;
          size_t _size;
          size_t _capacity;
      };
      
      int main() {
          Stack s1;
          s1.Push(1);
          s1.Push(2);
          s1.Push(3);
          s1.Push(4);
          Stack s2(s1);
          return 0;
      }
      

      注意类中如果没有涉及资源申请时拷贝构造函数是否写都可以一旦涉及到资源申请时则拷贝构造函数是一定要写的否则就是浅拷贝

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

      • 使用已存在对象创建新对象
      • 函数参数类型为类类型对象
      • 函数返回值类型为类类型对象
      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(2023, 10, 25);
          Test(d1);
          return 0;
      }
      

      为了提高程序效率一般对象传参时尽量使用引用类型返回时根据实际场景能用引用尽量使用引用


13、赋值运算符重载

13.1、运算符重载

C++中的运算符重载是指将已存在的运算符赋予新的含义从而改变它的行为。在C++中我们可以通过重载运算符来定义自己的操作规则使得这些运算符能够适用于自定义的数据类型

运算符重载的语法格式为

返回类型 operator 运算符(参数列表) {  
    // 操作代码  
}

其中返回类型可以是任何有效的数据类型包括基本数据类型和自定义数据类型运算符可以是C++中已存在的任何运算符包括算术运算符、比较运算符、逻辑运算符等。参数列表可以是零个或多个参数具体取决于运算符的定义。

  • 注意运算符重载需要注意以下几点

    1. 除了类属关系运算符".“、成员指针运算符”.*“、作用域运算符”::“、sizeof运算符和三目运算符”? :"以外C++中的所有运算符都可以重载

    2. 重载运算符限制在C++语言中已有的运算符范围内的允许重载的运算符之中不能创建新的运算符。

    3. 运算符重载实质上是函数重载因此编译程序对运算符重载的选择遵循函数重载的选择原则。

    4. 重载之后的运算符不能改变运算符的优先级和结合性也不能改变运算符操作数的个数及语法结构。

    5. 运算符重载不能改变该运算符用于内部类型对象的含义。它只能和用户自定义类型的对象一起使用或者用于用户自定义类型的对象和内部类型的对象混合使用时。

    6. 运算符重载是针对新类型数据的实际需要对原有运算符进行的适当的改造重载的功能应当与原有功能相类似避免没有目的地使用重载运算符。

    7. 重载=运算符时容易忘记写返回值。

    8. 重载赋值运算符时记得加const因为赋值操作必须是固定的右值。

    9. 重载时写在类中的只能有一个参数(实际有两个参数另外一个是this指针我们看不见而已)需要两个参数的时候要写在类外用友元在类内声明作为类成员函数重载时其形参看起来比操作数数目少1因为成员函数的第一个参数为隐

      藏的this

    10. 重载递增运算符时要注意哪个要加引用哪个不用加引用。

// 全局的operator==
class Date {
public:
    Date(int year = 1900, 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;
}

void Test() {
    Date d1(2018, 9, 26);
    Date d2(2018, 9, 27);
    cout << (d1 == d2) << endl;
}

int main() {
    Test();
    return 0;
}
// 类里面的operator==
class Date {

public:
    Date(int year = 1900, int month = 1, int day = 1) {
        _year = year;
        _month = month;
        _day = day;
    }

    //干脆重载成成员函数。
    //bool operator==(const Date* this,const Date &d2)
    //隐含了this
    bool operator==(const Date &d2) {
        return this->_year == d2._year
               && _month == d2._month
               && _day == d2._day;
    }

private:
    int _year;
    int _month;
    int _day;
};


void Test() {
    Date d1(2018, 9, 26);
    Date d2(2018, 9, 27);
    cout << (d1 == d2) << endl;//流插入运算符优先级比重载的运算符 == 高 所以加括号提升优先级
    //d1 == d2 --> d1.operator==(d2)
    cout << d1.operator==(d2) << endl;//相当于d1.operator==(&d1,d2);
}

int main() {
    Test();
    return 0;
}

13.2、赋值运算符重载

  • 赋值运算符重载格式

    • 参数类型const T&传递引用可以提高传参效率
    • 返回值类型T&返回引用可以提高返回的效率有返回值目的是为了支持连续赋值
    • 检测是否自己给自己赋值
    • 返回*this 支持连续赋值a = b = c
    class Date {
    public :
        Date(int year = 1900, int month = 1, int day = 1) {
            _year = year;
            _month = month;
            _day = day;
        }
    
        Date(const Date &d) {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
    
        //Date &operator=(const Date* this, const Date &d)
        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;
    };
    
    void Test() {
        Date d1(2018, 9, 26);
        Date d2(2018, 9, 27);
        d1 = d2;//d1 = d2 --> d1.operator=(d2)
        d1.operator=(d2);//相当于 d1.operator=(&d1,d2);
    }
    
    int main(){
        Test();
        return 0;
    }
    

  • 赋值运算符只能重载成类的成员函数不能重载成全局函数

    class Date {
    public:
        Date(int year = 1900, int month = 1, int day = 1) {
            _year = year;
            _month = month;
            _day = day;
        }
    
        int _year;
        int _month;
        int _day;
    };
    
    // 赋值运算符重载成全局函数注意重载成全局函数时没有this指针了需要给两个参数
    Date &operator=(Date &left, const Date &right) {
        if (&left != &right) {
            left._year = right._year;
            left._month = right._month;
            left._day = right._day;
        }
        return left;
    }
    // 编译失败
    // error C2801: “operator =”必须是非静态成员
    

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

  • 用户没有显式实现时编译器会生成一个默认赋值运算符重载以值的方式逐字节拷贝。注意内置类型成员变量是直接赋值的而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值也需要在自定义类型类里看情况要不要实现这个赋值运算符。

    class Time {
    public:
        Time() {
            _hour = 1;
            _minute = 1;
            _second = 1;
        }
    
        //这里都是自定义类型赋值其实可以不用自己实现
        Time &operator=(const Time &t) {
            if (this != &t) {
                _hour = t._hour;
                _minute = t._minute;
                _second = t._second;
            }
            return *this;
        }
    
    private:
        int _hour;
        int _minute;
        int _second;
    };
    
    class Date {
    private:
        // 基本类型(内置类型)
        int _year = 1970;
        int _month = 1;
        int _day = 1;
        // 自定义类型
        Time _t;
    };
    
    int main() {
        Date d1;
        Date d2;
        d1 = d2;
        return 0;
    }
    

    既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了还需要自己实现吗当然像日期类这样的类是没必要的。那么下面的类呢验证一下试试

    // 这里会发现下面的程序会崩溃掉这里就需要我们以后讲的深拷贝去解决。
    typedef int DataType;
    
    class Stack1 {
    public:
        Stack1(size_t capacity = 10) {
            _array = (DataType *) malloc(capacity * sizeof(DataType));
            if (nullptr == _array) {
                perror("malloc申请空间失败");
                return;
            }
            _size = 0;
            _capacity = capacity;
        }
    
        void Push(const DataType &data) {
            // CheckCapacity();
            _array[_size] = data;
            _size++;
        }
    
    
        ~Stack1() {
            if (_array) {
                free(_array);
                _array = nullptr;
                _capacity = 0;
                _size = 0;
            }
        }
    
    private:
        DataType *_array;
        size_t _size;
        size_t _capacity;
    };
    
    int main() {
        Stack1 s1;
        s1.Push(1);
        s1.Push(2);
        s1.Push(3);
        s1.Push(4);
        Stack1 s2;
        s2 = s1;
        return 0;
    }
    

    解决办法和拷贝构造函数一样。让这个s1s2_array不指向同一个空间s2_array重新开辟一块空间这就是深拷贝也就是啥变量空间都重新搞一次。

    //解决办法
    typedef int DataType;
    
    class Stack1 {
    public:
        Stack1(size_t capacity = 10) {
            _array = (DataType *) malloc(capacity * sizeof(DataType));
            if (nullptr == _array) {
                perror("malloc申请空间失败");
                return;
            }
            _size = 0;
            _capacity = capacity;
        }
    
      	//让这个s1和s2的_array不指向同一个空间s2的_array重新开辟一块空间这就是深拷贝也就是啥变量空间都重新搞一次
        Stack1& operator=(const Stack1 &st) {
            this->_array = (DataType *) malloc(st._capacity * sizeof(DataType));
            if (nullptr == this->_array) {
                perror("malloc申请空间失败");
                exit(-1);
            }
            this->_size = st._size;
            this->_capacity = st._capacity;
            return *this;
        }
    
        void Push(const DataType &data) {
            // CheckCapacity();
            _array[_size] = data;
            _size++;
        }
    
    
        ~Stack1() {
            if (_array) {
                free(_array);
                _array = nullptr;
                _capacity = 0;
                _size = 0;
            }
        }
    
    private:
        DataType *_array;
        size_t _size;
        size_t _capacity;
    };
    
    int main() {
        Stack1 s1;
        s1.Push(1);
        s1.Push(2);
        s1.Push(3);
        s1.Push(4);
        Stack1 s2;
        s2 = s1;
        return 0;
    }
    

    注意如果类中未涉及到资源管理赋值运算符是否实现都可以一旦涉及到资源管理则必须要实现。

13.3、前置++和后置++重载

  • 前置+++1再计算
  • 后置++先计算再+1
  • 前置++运算符重载规定无参数
  • 后置++运算符重载规定参数加一个int类型
class Date {
public:
    //默认构造函数与下面全缺省成员变量配合使用
    Date() {
    }

    Date(int year, int month, int day) { //你已经定义了自己的构造函数编译器就不会再自动生成默认构造函数
        _year = year;
        _month = month;
        _day = day;
    }

    // 前置++ 加完原来的值也改变
    // *this就是d1
    Date &operator++() {
        _day += 1;
        return *this;
    }

    Date operator++(int ){ //后置++运算符重载就是这样规定的参数加个int类型
        Date temp(*this);
        _day += 1;
        return temp;//临时对象不能引用
    }

private:
    int _year = 1900;
    int _month = 1;
    int _day = 1;
};


int main() {
    Date d1;
    Date d;
    d = ++d1;//d1.operator++(&d1);
    d = d1++;
    return 0;
}

14、日期类的实现

// Date.h 文件
#include <iostream>

using namespace std;

class Date {
public:
    // 获取某年某月的天数
    int GetMonthDay(int year, int month) {
        static int days[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30,
                               31};
        int day = days[month];
        if (month == 2
            && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
            day += 1;
        }
        return day;
    }

    // 全缺省的构造函数
    Date(int year = 1900, int month = 1, int day = 1);

    // 拷贝构造函数
    // d2(d1)
    Date(const Date &d);

    // 赋值运算符重载
    // d2 = d3 -> d2.operator=(&d2, d3)
    Date &operator=(const Date &d);

    // 析构函数
    ~Date();

    // 日期+=天数
    Date &operator+=(int day);

    // 日期+天数
    Date operator+(int day);

    // 日期-天数
    Date operator-(int day);

    // 日期-=天数
    Date &operator-=(int day);

    // 前置++
    Date &operator++();

// 后置++
    Date operator++(int);

    // 后置--
    Date operator--(int);

    // 前置--
    Date &operator--();

    // >运算符重载
    bool operator>(const Date &d);

    // ==运算符重载
    bool operator==(const Date &d);

    // >=运算符重载
    bool operator>=(const Date &d);

    // <运算符重载
    bool operator<(const Date &d);

    // <=运算符重载
    bool operator<=(const Date &d);

    // !=运算符重载
    bool operator!=(const Date &d);

    // 日期-日期 返回天数
    int operator-(const Date &d);

private:
    int _year;
    int _month;
    int _day;
}
// Date.cpp 文件
#include "Date.h"

// 全缺省的构造函数
//声明和定义分离需要指定类域
Date::Date(int year, int month, int day) {
    if (year >= 0 && (month >= 1 && month <= 12) && (day <= GetMonthDay(year, month))) {
        _year = year;
        _month = month;
        _day = day;
    } else {
        cout << "初始化的日期有误" << endl;
        assert(year >= 0 && (month >= 1 && month <= 12) && (day <= GetMonthDay(year, month)));
    }

}

// 拷贝构造函数
// d2(d1)
Date::Date(const Date &d) {
    _year = d._year;
    _month = d._month;
    _day = d._day;
}

// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date &Date::operator=(const Date &d) {
    if (this != &d) {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    return *this;
}

// 析构函数
Date::~Date() {
//    cout << "~Date()" << endl;
}

// 日期+=天数  -- 改变原值
Date &Date::operator+=(int day) {
    //如果输入的day小于0
    if (day < 0) {
        *this -= -day;
        return *this;
    }
    _day += day;
    //加后的天数大于当月天数的最大值
    while (_day > GetMonthDay(_year, _month)) {
        _day -= GetMonthDay(_year, _month);
        _month++;
        if (_month == 13) {
            _month = 1;
            _year++;
        }
    }
    return *this;
}

// 日期+天数  --  不改变原值
Date Date::operator+(int day) {
    Date temp(*this);
    temp += day;
    return temp;
}

// 日期-=天数  --  改变原值
Date &Date::operator-=(int day) {
    //如果输入的day小于0
    if (day < 0) {
        *this += -day;
        return *this;
    }
    _day -= day;
    while (_day <= 0) {
        _month--;
        if (_month == 0) {
            _year--;
            if (_year == 0) {
                printf("错误\n");
                exit(-1);
            }
            _month = 12;
        }
        _day += GetMonthDay(_year, _month);
    }
    return *this;
}

// 日期-天数  --  不改变原值
Date Date::operator-(int day) {
    Date temp(*this);
    temp -= day;
    return temp;
}


// 前置++  --  先+1再计算
Date &Date::operator++() {
    *this += 1;
    return *this;
}

// 后置++  --  先计算再+1
Date Date::operator++(int) {
    Date temp(*this);//拷贝构造
    temp += 1;
    return temp;
}

// 前置--  --  先-1再计算
Date &Date::operator--() {
    *this -= 1;
    return *this;
}

// 后置--  --  先计算再-1
Date Date::operator--(int) {
    Date temp(*this);//拷贝构造
    temp -= 1;
    return temp;
}


// >运算符重载
bool Date::operator>(const Date &d) {
    if (_year >= d._year) {
        if (_year > d._year)
            return true;
        else {
            //_year == d._year
            if (_month >= d._month) {
                if (_month > d._month)
                    return true;
                else {
                    //_month == d._month
                    if (_day >= d._day) {
                        if (_day > d._day)
                            return true;
                        else
                            return false;
                    }
                }
            }
        }
    }
    return false;
}

// ==运算符重载
bool Date::operator==(const Date &d) {
    return _year == d._year && _month == d._month && _day == d._day;
}

// >=运算符重载
bool Date::operator>=(const Date &d) {
    return (*this > d) || (*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);
}


// 日期-日期 返回天数
int Date::operator-(const Date &d) {
    //假设第一个参数的日期更大
    int flag = 1;
    int count = 0;
    Date max = *this;
    Date min = d;
    if (*this < d) {
        flag = -1;
        max = d;
        min = *this;
    }
    while (max != min) {
        ++min;
        count++;
    }
    return count * flag;
}
// main.cpp 文件
#include "Date.h"

//Date类的实现
int main() {
    Date d;
    Date d1(2023, 10, 27);
    Date d2;
    Date d3(2100, 7, 22);

//    d2 += 10;
//    d3 = d2 + 10;
//
//    d2 -= 10;
//    d3 = d2 - 10;

    d1 -= 100;
    d1 -= -100;

    cout << (d1 - d2) << endl;

    return 0;
}

15、const成员

15.1、const修饰成员变量

const关键字用于修饰成员函数或成员变量。当一个成员变量被声明为const时它表示这个成员变量的值不能被修改。

class Date {
public:

    void operator++() {
        (this->x)++;
        //(this->y)++;//y是const成员变量不能修改

    }

private:
    int x; // 非const变量
    const int y = 1; // const变量const变量必须初始化
};

15.2、const修饰成员函数

当一个成员函数被声明为const时它表示这个成员函数不能修改类的任何non-const非const成员变量。这有助于确保数据的一致性和安全性。其中const修饰的是*this如下面代码那么隐含的this的类型就是const Date* const this

class Date {
public:

    Date(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
    }

    bool operator++() const{//相当于bool operator++(const Date* const this)
        //(*this)._year++;//报错const修饰的是*this所以*this不能改变
    }

private:
    int _year; // 非const变量
    int _month;
    int _day;
    const int y = 1; // const变量
};

思考下列程序的结果

class Date {
public:
    Date(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
    }

    void Print() {
        cout << "Print()" << endl;
        cout << "year:" << _year << endl;
        cout << "month:" << _month << endl;
        cout << "day:" << _day << endl << endl;
    }

    void Print() const {
        cout << "Print()const" << endl;
        cout << "year:" << _year << endl;
        cout << "month:" << _month << endl;
        cout << "day:" << _day << endl << endl;
    }

private:
    int _year; // 年
    int _month; // 月
    int _day; // 日
};

void Test() {
    Date d1(2022, 1, 13);
    d1.Print();//Print()
    const Date d2(2022, 1, 13);//Printf()const
    d2.Print();
}

int main(){
  Test();
	return 0;
}

请思考下面的几个问题

  1. const对象可以调用非const成员函数吗

    答不可以权限只能缩小或者平移不能变大

    class Date {
    public:
    
        Date(int year, int month, int day) {
            _year = year;
            _month = month;
            _day = day;
        }
    
        bool operator==(const Date &d) { //相当于bool operator==(Date* const this, const Date &d)
            return _year == d._year && _month == d._month && _day == d._day;
        }
    
    private:
        int _year; // 非const变量
        int _month;
        int _day;
        const int y = 1; // const变量
    };
    
    int main() {
        const Date d1(2022, 1, 1);
        const Date d2(2022, 1, 2);
        //int ret = d1 == d2;//报错这里const对象传给thisthis的类型是Date* const而不是 const Date* 因为*this才是对象this是指针
        //cout << ret << endl;
    
        return 0;
    }
    

    解决办法将该成员函数用const修饰

    //解决办法
    class Date {
    public:
    
        Date(int year, int month, int day) {
            _year = year;
            _month = month;
            _day = day;
        }
    
        bool operator==(const Date &d) const{
            return _year == d._year && _month == d._month && _day == d._day;
        }
    
    private:
        int _year; // 非const变量
        int _month;
        int _day;
        const int y = 1; // const变量
    };
    
    int main() {
        const Date d1(2022, 1, 1);
        const Date d2(2022, 1, 2);
        int ret = d1 == d2;
        cout << ret << endl;
    
        return 0;
    }
    
  2. 非const对象可以调用const成员函数吗

    答可以。权限可以缩小

    //权限缩小
    class Date {
    public:
    
        Date(int year, int month, int day) {
            _year = year;
            _month = month;
            _day = day;
        }
    
        bool operator==(const Date &d) const{
            return _year == d._year && _month == d._month && _day == d._day;
        }
    
    private:
        int _year; // 非const变量
        int _month;
        int _day;
        const int y = 1; // const变量
    };
    
    int main() {
        Date d1(2022, 1, 1);
        Date d2(2022, 1, 2);
        int ret = d1 == d2;//这里非const调用const
        cout << ret << endl;
    
        return 0;
    }
    
  3. const成员函数内可以调用其它的非const成员函数吗

    可以。虽然非const成员函数可能会改变类的成员变量但它们不能改变const成员函数的局部变量这个局部变量是const类型如果有的话也不能改变const成员函数所引用的成员变量但是可以修改局部对象因为这个类型不是const。

    class Date {
    public:
             
        Date(int year, int month, int day) {
            _year = year;
            _month = month;
            _day = day;
        }
             
        bool operator++() {
            (*this)._year++;
        }
             
        int jj(int x){
            return ++x;
        }
        bool operator==(const Date &d) const{
            Date a(11,1,1);
            ++a;
            int b = 1;
            //int c= jj(this,b);//报错jj不是const成员函数
            return _year == d._year && _month == d._month && _day == d._day;
        }
    
    
    
    private:
        int _year; // 非const变量
        int _month;
        int _day;
        const int y = 1; // const变量
    };
    
  4. 非const成员函数内可以调用其它的const成员函数吗

    可以。const关键字表示这个成员函数不会修改类的任何数据成员因此在非const成员函数内调用不会有什么风险。

    class Date {
    public:
    
        Date(int year, int month, int day) {
            _year = year;
            _month = month;
            _day = day;
        }
    
        bool operator==(const Date &d) const {
            return _year == d._year && _month == d._month && _day == d._day;
        }
    
        bool operator++() {
            Date aa(2022, 1, 1);
            *this == aa;
            (*this)._year++;
        }
    
    private:
        int _year; // 非const变量
        int _month;
        int _day;
        const int y = 1; // const变量
    };
    

总结1、权限可以平移或者缩小不能放大

2、const成员函数内可以调用其它的非const成员函数

3、非const成员函数内可以调用其它的const成员函数


16、取地址及const取地址操作符重载

  • 这两个默认成员函数一般不用重新定义 编译器默认会生成。

    class Date {
    public :
    
        Date() : _year(1), _month(1), _day(1) { //初始化列表后面马上会讲
            _year = 2;
            _month = 3;
            _day = 4;
        }
    
        Date *operator&() {
            cout << "operator&()" << endl;
            return this;
    
        }
    
        const Date *operator&() const {
            cout << "operator&() const" << endl;
            return this;
        }
    
    private :
        int _year; // 年
        int _month; // 月
        int _day; // 日
    };
    
    int main() {
        Date d1, d2;
        const Date d3;
        cout << &d1 << endl << &d2 << endl << &d3 << endl;
        return 0;
    }
    
  • 这两个运算符一般不需要重载使用编译器生成的默认取地址的重载即可只有特殊情况才需要重载比如想让别人获取到指定的内容比如误导别人


17、初始化列表重点

首先在之前学到的知识我们知道对象是在构造函数里初始化的比如下面这个场景。

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(2023,11,3);
    return 0;
}

我们注意到这里每次初始化都需要在对象调用构造函数的时候给值如果有时候我们忘了怎么办有人会说使用默认构造函数初始化如下

class Date {
public :
    Date() {
        _year = 1;
        _month = 1;
        _day = 1;
    }
    

private :
    int _year; // 年
    int _month; // 月
    int _day; // 日
};

int main() {
    Date d1;
    return 0;
}

或者使用全缺省的默认构造函数或者采用缺省的成员变量这些都可以。但是考虑一个问题如果要初始化的是一个引用、一个常成员变量、或者一个没有默认构造函数的自定义类型成员变量。阁下又该如何应对呢仅使用上述的构造函数不能解决这个问题。那么就需要初始化列表来处理了


17.1、初始化列表概念

初始化列表是C++中的一个概念用于在构造函数初始化期间对类的成员变量进行初始化。初始化列表主要分为两种成员变量初始化列表和构造函数初始化列表上述构造函数{}里初始化

成员变量初始化列表在类定义中以冒号开头后跟一系列以逗号分隔的初始化字段。这些初始化的成员变量在对象创建时会按照初始化列表中==声明一般是private访问限定符下的成员变量的声明顺序==的顺序进行初始化

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(2023, 11, 3);
    return 0;
}

17.2、初始化列表特性

  1. 类中包含以下成员必须放在初始化列表位置进行初始化
    • 引用成员变量
    • const成员变量
    • 自定义类型成员(且该类没有默认构造函数时)
  2. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)构造函数里面可以进行赋值初始化上述三个成员不能在构造函数里面初始化。
class Time {
public:
    Time(int hour, int minute, int second) {
        cout << "Time(int ,int , int )" << endl;
    }

private:
    int _hour;
    int _minute;
    int _second;
};

class Date {
public :
    Date(int year, int month, int day) : _year(year), _month(month), _day(day), _n(year), _x(1), _t(1, 1, 1) {
    }


private :
    int _year; // 年
    int _month; // 月
    int _day; // 日
    int &_n;//引用
    const int _x;//const成员变量
    Time _t;//没有默认构造函数的自定义类型成员变量
};

int main() {
    Date d1(2023, 11, 3);
    return 0;
}
  1. 尽量使用初始化列表初始化因为不管你是否使用初始化列表对于自定义类型成员变量一定会先使用初始化列表初始化

    class Time {
    public:
        Time(int hour = 0)
                : _hour(hour) {
            cout << "Time()" << endl;
        }
    
    private:
        int _hour;
    };
    
    class Date {
    public:
        Date(int day) {
            cout << "Date()" << endl;
        }
    
    private:
        int _day;
        Time _t;
    };
    
    int main() {
        Date d(1);//先Time() 再 Date()
    }
    
  2. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序与其在初始化列表中的先后次序无关

    class A {
    public:
        A(int a)
                : _a1(a), _a2(_a1) {}
    
        void Print() {
            cout << _a1 << " " << _a2 << endl;//a2先声明但是并没有初始化
        }
    
    private:
        int _a2;
        int _a1;
    };
    
    int main() {
        A aa(1);// 1 随机值
        aa.Print();
    }
    

17.3、构造函数的隐式类型转换

构造函数不仅可以构造与初始化对象对于单个参数或者半缺省或者全缺省的构造函数还具有类型转换的作用

class Date {
public:

//    Date(int year)
//            : _year(year) {}

    Date(int year, 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(11);
    // 用一个整型变量给日期类型对象赋值
    // 实际编译器背后会用2023构造一个无名对象最后用无名对象给d1对象进行赋值
    d1 = 2023;//2023先转换为Date类型然后用2023构造一个无名对象相当于 d1 =  d2(2023)
    d1 = (2021,2,1); //逗号表达式的值是最后一个数值这里是1 相当于  d1 =  d3(1)
    d1 = {2021, 2, 1};//相当于 d1 = d4(2021, 2, 1)
    return 0;
}

我们可以注意到这里给给多参数的构造函数进行隐式类型转换传参可以使用{}

  • 需要注意的是需要隐式类型转换的变量类型必须和构造函数的参数类型匹配

    class Date {
    public:
    
    //    Date(int year)
    //            : _year(year) {}
    
        //这里验证构造函数的隐式类型转换功能  ---  需要隐式类型转换的变量类型必须和构造函数的参数类型匹配
        Date(int *n) {}
    
        Date(int year, 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() {
        int *p = nullptr;
        Date d1(2023, 11, 6);
        d1 = p;//这里p进行隐式类型转换了先创建一个临时对象再将临时对象赋值给d1
    
        int** num = nullptr;
        //d1 = num;//报错这里num和构造函数的参数类型不匹配
        Date d2 = {2023, 11, 6};
        const Date &d3 = {2023, 11, 6};
        
        return 0;
    }
    

17.4、explicit关键字

  • 用explicit修饰构造函数将会禁止构造函数的隐式转换

    class Date {
    public:
    
        // 1. 单参构造函数没有使用explicit修饰具有类型转换作用
        // explicit修饰构造函数禁止类型转换---explicit去掉之后代码可以通过编译
    //  explicit  Date(int year)
    //            : _year(year) {}
    
    
        // 2. 虽然有多个参数但是创建对象时后两个参数可以不传递没有使用explicit修饰具有类型转换作用
        // explicit修饰构造函数禁止类型转换
        explicit Date(int year, 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(11);
        // 用一个整形变量给日期类型对象赋值
        // 实际编译器背后会用2023构造一个无名对象最后用无名对象给d1对象进行赋值
        //d1 = 2023;//报错有explicit关键字隐式转换不了
        //d1 = {2023, 11, 6};//同样报错
        // 将1屏蔽掉2放开时则编译失败因为explicit修饰构造函数禁止了构造函数类型转换的作用
        return 0;
    }
    

18、static成员

18.1、static成员概念

  • 声明为static的类成员称为类的静态成员用static修饰的成员变量称之为静态成员变量用static修饰的成员函数称之为静态成员函数没有this指针。静态成员变量一定要在类外进行初始化

    class Date {
    public:
    
    //    Date(int year)
    //            : _year(year) {}
    
        Date(int year, int month = 1, int day = 1)
                : _year(year), _month(month), _day(day) {}
    
    
        //static成员函数没有this指针
        static void Show(int n) {
            cout << n << endl;
        }
    
    private:
        int _year;
        int _month;
        int _day;
        static int n;//声明
    };
    
    int Date::n = 1;//定义
    
    int main() {
        Date::Show(2);
        return 0;
    }
    
  • 面试题实现一个类计算程序中创建出了多少个类对象?

    class A {
    public:
        A() { ++_scount; }
    
        A(const A &t) { ++_scount; }
    
        ~A() { --_scount; }
    
        static int GetACount() { return _scount; }
    
    private:
        static int _scount;
    };
    
    int A::_scount = 0;
    
    int main() {
        cout << A::GetACount() << endl;
        A a1, a2;
        A a3(a1);//拷贝构造函数
        cout << A::GetACount() << endl;
    }
    

18.2、static成员特性

  1. 静态成员为所有类对象所共享不属于某个具体的对象存放在静态区

  2. 静态成员变量必须在类外定义定义时不添加static关键字类中只是声明即静态成员变量必须声明和定义分离

  3. 类静态成员函数即可用 类名::静态成员函数名 或者 对象.静态成员函数名 来访问

  4. 静态成员函数没有隐藏的this指针不能访问任何非静态成员

  5. 静态成员也是类的成员受 public、protected、private 访问限定符的限制

class Date {
public:

//    Date(int year)
//            : _year(year) {}

    Date(int year, int month = 1, int day = 1)
            : _year(year), _month(month), _day(day) {}


    //static成员函数没有this指针
    static void Show(int n) {
      	//cout << _year << endl;//报错没有this找不到_year
        cout << n << endl;
    }

private:
    int _year;
    int _month;
    int _day;
    static int n;//声明
};

int Date::n = 1;//定义

int main() {
    Date::Show(2);
  	Date().Show(2);
    return 0;
}

问题

  1. 静态成员函数可以调用非静态成员函数吗

    不可以。因为静态成员函数其实是受限的全局函数与类有关调用非静态成员函数需要和类外面调用成员函数一样使用对象调用但是在静态成员函数里创建不了对象所以调用不了非静态成员函数

  2. 非静态成员函数可以调用类的静态成员函数吗

    可以。因为静态成员函数其实是受限的全局函数与类有关非静态成员函数在函数体里可以通过函数名直接调用静态成员函数参考在类外面调用静态成员函数只需要加上类限定符::即可以访问


19、友元

友元提供了一种突破封装的方式有时提供了便利。但是友元会增加耦合度破坏了封装所以友元不宜多用。
友元分为友元函数友元类

19.1、友元函数

友元函数是指某些不是类成员却能够访问类的所有成员的函数。类授予它的友元特别的访问权友元函数在类的作用域外定义但需要在类体中加上关键字friend进行说明

需要注意的是友元函数不是类的成员函数在函数体中访问对象的成员需要使用对象名加运算符"."加对象成员名。同时友元函数可以访问类中的所有成员不受public、private、protected的限制但其作用域不是该类的作用域。

  • 前面我们有使用到这个友元函数在日期类的实现里面的 <<>>运算符重载因为如果要重载这两个运算符如果重载在类的成员函数里涉及到一个隐含的this指针作为第一个参数不符合预期。但是放在类外面重载运算符又访问不了私有的成员变量所以使用友元函数解决了这个问题。

    class Date {
        friend ostream &operator<<(ostream &_cout, const Date &d);
    
        friend istream &operator>>(istream &_cin, Date &d);
    
    public:
        Date(int year = 1900, int month = 1, int day = 1)
                : _year(year), _month(month), _day(day) {}
    
    private:
        int _year;
        int _month;
        int _day;
    };
    
    ostream &operator<<(ostream &_cout, const Date &d) {
        _cout << d._year << "-" << d._month << "-" << d._day;
        return _cout;
    }
    
    istream &operator>>(istream &_cin, Date &d) {
        _cin >> d._year;
        _cin >> d._month;
        _cin >> d._day;
        return _cin;
    }
    
    int main() {
        Date d;
        cin >> d;
        cout << d << endl;
        return 0;
    }
    
  • 注意

    1. 友元函数可访问类的私有和保护成员但不是类的成员函数
    2. 友元函数不能用const修饰
    3. 友元函数可以在类定义的任何地方声明不受类访问限定符限制
    4. 一个函数可以是多个类的友元函数
    5. 友元函数的调用与普通函数的调用原理相同

19.2、友元类

友元类是指一个类可以访问另一个类的私有成员。友元类的概念是为了提高代码的灵活性和可扩展性。具体来说友元类允许我们将某些类中的私有成员暴露给其他类而不必公开这些成员。

  • 友元关系是单向的不具有交换性
    • 比如上述Time类和Date类在Time类中声明Date类为其友元类那么可以在Date类中直接访问Time类的私有成员变量但想在Time类中访问Date类中私有的成员变量则不行。
  • 友元关系不能传递
    • 如果C是B的友元 B是A的友元则不能说明C时A的友元。
  • 友元关系不能继承在继承的地方展开讲
  • 友元类可以在类里面的任何位置声明
class Time {
    friend class Date;  // 声明日期类为时间类的友元类则在日期类中就直接访问Time类中的私有成员变量
public:
    Time(int hour = 0, int minute = 0, int second = 0)
            : _hour(hour), _minute(minute), _second(second) {}
            
private:
    int _hour;
    int _minute;
    int _second;
    
};

class Date {
public:
    Date(int year = 1900, int month = 1, int day = 1)
            : _year(year), _month(month), _day(day) {}

    void SetTimeOfDate(int hour, int minute, int second) {
        // 直接访问时间类私有的成员变量
        _t._hour = hour;
        _t._minute = minute;
        _t._second = second;
    }

private:
    int _year;
    int _month;
    int _day;
    Time _t;
};

20、内部类

20.1、内部类概念

如果一个类定义在另一个类的内部这个内部类就叫做内部类。内部类是一个独立的类它不属于外部类更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

注意内部类天然就是外部类的友元类参见友元类的定义内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

class A {
private:
    static int k;
    int h;
public:
    class B // B天生就是A的友元
    {
    public:
        void foo(const A &a) {
            cout << k << endl;//OK
            cout << a.h << endl;//OK
        }
    };
};

int A::k = 1;

int main() {
    A::B b;
    b.foo(A());
    return 0;
}

20.2、内部类特性

  1. 内部类在外部类的public、protected、private地方定义都是可以的。

  2. 注意内部类可以直接访问外部类中的static成员不需要外部类的对象/类名。

  3. sizeof(外部类)=外部类和内部类没有任何关系内部类相当于外部类的成员函数。

  4. 内部类可以嵌套。

class A {
public:
    class B // B天生就是A的友元
    {
    public:
        class C {
        public:
            void fun_c() {}

            class D {//内部类可以嵌套
            public:
                void fun_d() {
                    cout << "A::B::C::D" << endl;
                };
            private:
                int _a;
                int _b;
            };

        private:
            int _x;
            int _y;
        };

        void foo(const A &a) {
            cout << k << endl;//OK
            cout << a._h << endl;//OK
        }

    private:
        int _mm;
        int _nn;
    };

private:
    static int k;
    int _h;

    class E {
    public:
        void fun_e() {}

    private:
        int _aa;
        int _bb;
    };
};

int A::k = 1;

int main() {
    A::B b;
    b.foo(A());
    A::B::C::D abcd;
    abcd.fun_d();
    cout << sizeof(A) << endl;
    return 0;
}

21、匿名对象

匿名对象是一个没有被命名的对象通常用于执行一次性操作或者作为中间结果匿名对象不被分配给任何变量因此它在使用后会被销毁

class A {
public:
    A(int a = 0)
            : _a(a) {
        cout << "A(int a)" << "_a = " <<_a << endl;
    }

    ~A() {
        cout << "~A()" << endl;
    }

private:
    int _a;
};

class Solution {
public:
    int Sum_Solution(int n) {
        //...
        return n;
    }
};

int main() {
    A aa1;

    //A aa1();// 不能这么定义对象因为编译器无法识别下面是一个函数声明还是对象定义
    // 但是我们可以这么定义匿名对象匿名对象的特点不用取名字
    // 但是他的生命周期只有这一行我们可以看到下一行他就会自动调用析构函数
    A();//这里就是匿名对象匿名对象的作用域就在这一行出了这一行就销毁了
    A(2);//匿名对象参数是2
    A aa2(2);

    // 匿名对象在这样场景下就很好用当然还有一些其他使用场景这个我们以后遇到了再说
    Solution().Sum_Solution(10);
    return 0;
}

22、构造和拷贝构造时编译器的一些优化

  • 有些编译器可能会对同一表达式中对象在构造和拷贝构造时候进行优化不同编译器可能不同。

    构造 + 构造 优化为一次构造 (一般不是同一个表达式)

    构造 + 拷贝构造 优化为一次构造

    拷贝构造 + 拷贝构造 优化为一次拷贝构造

    class Date {
    public:
        //构造函数
        Date(int year, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {
            cout << "Date(int year, int month, int day) " << endl;
        }
    
        //拷贝构造函数
        Date(Date &d) {
            cout << "Date(Date &d)" << endl;
        }
    
    
    private:
        int _year;
        int _month;
        int _day;
    };
    
    Date func1(Date d) {
        Date tmp = d;
        return tmp;
    }
    
    void func2() {
        Date d1(1, 2, 3);
        //构造 + 构造 --> 构造
        Date d2 = d1;
        Date d3 = d2;
    }
    
    int main() {
    //    Date d1(1999, 11, 14);
    //    Date d2(2020,11,14);
    
        //同一个表达式中 构造 + 拷贝构造 --> 构造
        Date d3 = 1;
        //同一个表达式中 拷贝构造 + 拷贝构造 --> 拷贝构造
        Date d4 = func1(d3);
        func2();
    }
    

OKOKC++入门篇3就到这里。如果你对Linux和C++也感兴趣的话可以看看我的主页哦。下面是我的github主页里面记录了我的学习代码和的一些题的题解有兴趣的可以看看。

Xpccccc的github主页

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++