C++: 类和对象(中)

文章目录

  • 1. 类的6个默认成员函数
  • 2. 构造函数
    • 构造函数概念
    • 构造函数特性
      • 特性1,2,3,4
      • 特性5
      • 特性6
      • 特性7
  • 3. 析构函数
    • 析构函数概念
    • 析构函数特性
      • 特性1,2,3,4
      • 特性5
      • 特性6
  • 4. 拷贝构造函数
    • 拷贝构造函数概念
    • 拷贝构造函数特性
      • 特性1,2
      • 特性3
      • 特性4
      • 特性5
  • 5. 运算符重载
    • 一般运算符重载
    • 赋值运算符重载
      • 赋值运算符重载格式
      • 赋值运算符只能重载成类的成员函数不能重载成全局函数
      • 用户没有显式实现时, 编译器会生成一个默认赋值运算符重载, 以值的方式逐字节拷贝
    • 前置 `++` 和 后置 `++` 重载
    • 流插入 流提取 的重载
  • 6. `const` 成员
  • 7.取地址及 `const` 取地址操作符重载

1. 类的6个默认成员函数

如果一个类中什么成员, 简称为空类.

空类中并不是什么都没有, 任何类在什么都不写时, 编译器会自动生成以下 6 个默认成员函数.

默认成员函数: 用户没有显示实现, 编译器会生成的成员函数称为默认成员函数.

class Date{};

在这里插入图片描述

在我们学习这些默认生成的成员函数时, 我们需要把两点疑问放在心上

1. 我们不写这些成员函数, 编译器自己生成默认的成员函数, 这些函数做了些什么?

2. 我们自己要写这些成员函数, 应该如何实现?

2. 构造函数

构造函数概念

在类和对象(上)中, 初始化类生成的对象, 使用了成员函数 Init(), 但有时候, 会经常忘记初始化, 造成不可预料的结果.

例如日期类:

class Date
{
private:int _year;int _month;int _day;public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << '-' << _month << '-' << _day << endl;}
};

如果不初始化, 或者忘记初始化, 该对象的年月日是随机值:

int main()
{Date d;d.Print();return 0;
}

在这里插入图片描述


为了防止忘记初始化, 或者简化每次创建对象都需要初始化的行为.C++引入了构造函数

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

构造函数特性

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

特性1,2,3,4

其特征如下

  1. 函数名与类名相同.
  2. 无返回值
  3. 对象实例化时编译器自动调用对应的构造函数
  4. 构造函数可以重载

首先我们要注意的是, 全缺省函数和无参函数虽然构成函数重载, 但是不能同时存在, 会让编译器不知道具体调用哪一个:

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

下面的代码展示了构造函数是怎么起作用的:

class Date
{
private:int _year;int _month;int _day;
public://1. 无参构造函数Date(){}//2. 带参构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}
};int main()
{Date d1;                //调用无参构造函数Date d2(2000, 1, 1);    //调用带参构造函数return 0;
}

在这里插入图片描述

调用无参构造函数只能类名 对象名, 不能在后面跟上圆括号, Date d1()这是错的, 这会被编译器当成函数声明.

在这里插入图片描述


特性5

  1. 如果类中没有显示定义构造函数, 则 C++ 编译器会自动生成一个无参的默认构造函数, 一旦用户显示定义编译器将不再生成.

一开始没有写构造函数, 直接创建对象是不会出问题的, 但是生成的是随机值, 可以大概得到一个模糊的结果: 编译器会自动生成一个默认构造函数.

一开始没有写构造函数, Date d1 不会报错
在这里插入图片描述

如果自己写了带参数的构造函数, Date d1 就会报错了
在这里插入图片描述


不实现构造函数的情况下, 编译器会生成默认的构造函数. 但是生成的仍然是随机值, 看起来构造函数却没有什么用?

特性6

  1. C++ 默认生成的构造函数, 内置类型不处理, 自定义类型会调用其构造函数.

C++ 把类型分为内置类型和自定义类型. 内置类型就是语言提供的数据类型, 如:int, char, double, 指针...(自定义类型的指针也是内置类型如Date*);自定义类型就是使用class, struct, union...定义的类型.

下面的代码就可以证明这个特性:
之前做过用两个栈模拟队列的题目, 这边先创建队列的对象.

typedef int STDataType;
class Stack
{
private:STDataType* _array;int _capacity;int _top;
public:Stack(int capacity = 4){_array = (STDataType*)malloc(sizeof(STDataType) * capacity);if (nullptr == _array){perror("malloc fail");exit(-1);}_capacity = 4;_top = 0;}
};class MyQueue
{
private://自定义类型Stack _pushst;Stack _popst;//内置类型int _size;
};int main()
{Stack st;MyQueue mq;return 0;
}

我只创建了 Stack 的构造函数, MyQueue 并没有创建.
但是调试发现 MyQueue 创建的对象 mq 的自定义类型成员被默认初始化了, 内置类型成员却仍然是随机值.
在这里插入图片描述

如果我把 Stack 的构造函数删去, 则会出现下面的结果:
所有的类型全是随机值, 即使创建 mq 的时候会调用 Stack 类型的构造函数, 但是是默认构造函数, 而 Stack 类中的所有成员变量都是内置类型, 所以最后所有的成员都是随机值.
在这里插入图片描述

由此可以看出, C++ 默认生成的构造函数, 不处理内置类型成员变量, 自定义类型成员变量会调用其构造函数


对于 C++ 默认构造函数只处理自定义类型的缺陷

C++11 中打了补丁, 即:内置类型成员变量在声明中可以给默认值

class Time
{
private:int _hour;int _minute;int _second;
public:Time(){cout << "Time()" << endl; _hour = 0;_minute = 0;_second = 0;}
};class Date
{
private:// 内置类型int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;
};int main()
{Date d1;return 0;
}

打印 d1 的所有成员, 均初始化完毕
在这里插入图片描述


如果同时存在缺省值和自己创建的构造函数, 优先构造函数.

在上面的 Date 类中添加了自己写的 Date 构造函数, 结果编译器优先使用构造函数进行构造.

Date()
{_year = 2000;_month = 4;_day = 1;
}

程序运行如下:
在这里插入图片描述


特性7

  1. 无参构造函数, 全缺省构造函数, 我们没写编译器默认生成的构造函数, 都可以认为是默认构造函数.
    但是只能有一个默认构造函数.

在这里插入图片描述

//无参构造函数
Date()
{_year = 2000;_month = 4;_day = 1;
}
//全缺省构造函数
Date(int year = 2000, int month = 4, int day = 1)
{_year = year;_month = month;_day = day;
}

推荐使用全缺省构造函数作为默认构造函数, 这样可以做到程序自己初始化, 自己自定义初始化.

int main()
{Date d1;Date d2(1999, 1, 1);return 0;
}

在这里插入图片描述


关于如何写构造函数的策略:

  1. 一般情况下, 自己要写构造函数
  2. 如果成员都是自定义类型, 或者声明类时已经给了缺省值, 可以考虑让编译器自己生成

3. 析构函数

析构函数概念

构造函数可以对对象进行初始化, 那么这些数据是如何归还给操作系统的呢?

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

析构函数特性

特性1,2,3,4

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

  1. 析构函数名是在类名前面加上字符~
  2. 无参数无返回类型
  3. 一个类只能有一个析构函数. 若未显示定义, 系统会自动生成默认的析构函数. 注意: 析构函数不能重载
  4. 对象生命周期结束时, C++ 编译系统自动调用析构函数
class Date
{
private:int _year = 1970;int _month = 1;int _day = 1;
public: ~Date(){//严格来说, Date类不需要析构函数cout << "~Date()" << endl;}
};int main()
{Date d1;return 0;
}

在对象被自动销毁前, 程序会自行调用析构函数, 先把类中的成员得到的资源归还给操作系统.
在这里插入图片描述


特性5

  1. 编译器生成的默认析构函数, 对内置类型不做处理, 自定义类型调用它的析构函数

还是双栈构造队列的代码:

typedef int STDataType;
class Stack
{
private:STDataType* _array;int _capacity;int _top;
public:Stack(int capacity = 4){//Stack的构造函数_array = (STDataType*)malloc(sizeof(STDataType) * capacity);if (nullptr == _array){perror("malloc fail");exit(-1);}_capacity = 4;_top = 0;}~Stack(){//Stack的析构函数cout << "~Stack()" << endl; free(_array);_array = nullptr;_capacity = _top = 0;}
};class MyQueue
{
private://自定义类型Stack _pushst;Stack _popst;//内置类型int _size = 0;
};int main()
{Stack st;MyQueue mq;return 0;
}

可以看到, 虽然 MyQueue 没有写析构函数, 但是系统生成的默认析构函数调用了其自定义类型成员的析构函数, 所以一共会有三次调用 ~Stack() 的记录.

在这里插入图片描述

main 方法中只创建了 1 次 Stack 类的对象, 但是最终会调用 3 次 Stack 类的析构函数.

原因是, 系统要销毁 mq 对象前, 会先调用 MyQueue 类的析构函数, 但是我们没有写, 会调用系统默认的析构函数.

系统默认的析构函数, 会自动调用自定义类型成员的析构函数, 正好 MyQueue 类有两个 Stack 类的成员, 调用了 2 次.

剩下的内置类型空间交由系统释放, 入栈出栈用栈帧指针控制.


特性6

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

4. 拷贝构造函数

拷贝构造函数概念

在创建对象的时候, 可否创建一个与已存在对象一模一样的新对象呢?

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


那么拷贝构造函数有什么用呢? 系统明明会自己拷贝, 例如在函数调用的时候.

我们都知道当调用函数时, 函数形参是实参的拷贝.

若实参所占的内存空间很大, 传值效率很低. 如果该实参类型的对象是需要申请空间的, 如果还是传值操作, 编译器的默认操作就会造成错误:

typedef int STDataType;
class Stack
{
private:STDataType* _array;int _capacity;int _top;
public:void Print(){cout << "_array: " << _array << endl;cout << "_capacity: " << _capacity << endl;cout << "_top: " << _top << endl << endl;}// 构造函数Stack(int capacity = 4){cout << "Stack()" << endl; _array = (STDataType*)malloc(sizeof(STDataType) * capacity);if (nullptr == _array){perror("malloc fail");exit(-1);}_capacity = 4;_top = 0;}// 析构函数~Stack(){cout << "~Stack()" << endl;free(_array);_array = nullptr;_capacity = _top = 0;}
};// 写一个函数, 将 Stack 类类型的变量传值操作当作参数
void func(Stack st)
{cout << "func方法中的st:" << endl; st.Print();
}int main()
{Stack stack;cout << "main方法中的stack:" << endl; stack.Print();func(stack);   //传值操作return 0;
}

程序调用了两次 Stack 类的析构函数后, 崩溃了

在这里插入图片描述

分析一下程序的操作

  1. 首先创建了一个 Stack 类的对象调用了一次构造函数

  2. 接着调用 func, 函数是传值操作, 编译器直接创建了一个 Stack 类类型的对象,(其实这个时候也调用了系统默认生成的构造函数) 并同时将 mainstack 的成员数据复制给给 func 中的 st. 这个时候发现, stackst 两个对象的 _array 的地址是一样的, 即指向一片空间
    在这里插入图片描述

  3. func 函数结束前, 需要销毁创建的临时变量 st, 调用析构函数, 直接释放了 _array 的空间, 而这一片空间同时也被 main 方法中的 stack 使用着.

  4. 最后 main 方法结束, 销毁 stack 就出现错误了, 调用析构函数释放已经释放过的内存空间, 造成了内存泄漏.

由此可以看到, 编译器默认能做的只是浅拷贝, 如果拷贝需要动态开辟空间的类型对象, 会出现问题.

而自己写的拷贝构造函数就要做到"深拷贝", 如果被拷贝对象有动态开辟的空间, 需要把这一块空间的数据也拷贝一份到新的对象中去.

拷贝构造函数特性

特性1,2

拷贝构造函数也是特殊的成员函数

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

如果拷贝构造函数使用传值操作, 会不停的创建临时对象调用拷贝构造, 造成无穷递归
在这里插入图片描述

同时编译器也会直接报错, 提示只能传引用
在这里插入图片描述

下面是正确的拷贝构造函数写法, 当然传指针也是可以的, 但是明显引用更好.

typedef int STDataType;
class Stack
{
private:STDataType* _array;int _capacity;int _top;
public:void Print(){cout << "_array: " << _array << endl;cout << "_capacity: " << _capacity << endl;cout << "_top: " << _top << endl << endl;}// 构造函数Stack(int capacity = 4){cout << "Stack()" << endl; _array = (STDataType*)malloc(sizeof(STDataType) * capacity);if (nullptr == _array){perror("malloc fail");exit(-1);}_capacity = 4;_top = 0;}// 拷贝构造函数Stack(const Stack& st){cout << "Stack(const Stack& st)" << endl;_array = (STDataType*)malloc(sizeof(STDataType) * st._capacity);if (_array == nullptr){perror("malloc fail");exit(-1);}memcpy(_array, st._array, sizeof(STDataType) * st._top);_top = st._top;_capacity = st._capacity;}// 析构函数~Stack(){cout << "~Stack()" << endl;free(_array);_array = nullptr;_capacity = _top = 0;}
};// 写一个函数, 将 Stack 类类型的变量传值操作当作参数
void func(Stack st)
{cout << "func方法中的st:" << endl; st.Print();
}int main()
{Stack stack;cout << "main方法中的stack:" << endl; stack.Print();func(stack);   //传值操作return 0;
}

程序正常运行.
在这里插入图片描述


特性3

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

默认生成的拷贝构造函数完成值拷贝, 也是 C++ 对 C语言 的传承.

class Time
{
private:int _hour;int _minute;int _second;
public:// 构造函数Time(){_hour = 1;_minute = 1;_second = 1;}// 拷贝构造函数Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;}void Print(){cout << _hour << ": " << _minute << ": " << _second << endl;}
};class Date
{
private:// 内置类型int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;
public:void Print(){cout << _year << '-' << _month << '-' << _day << ' ';_t.Print();}};int main()
{Date d1;d1.Print();// 用已经存在的 d1 拷贝构造 d2, 此处会调用 Date 类的拷贝构造函数// 但 Date 类没有显式定义拷贝构造函数, 则编译器会给 Date 类生成一个默认的拷贝构造函数Date d2(d1);d2.Print();return 0;
}

两个对象完全一样.
在这里插入图片描述

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


特性4

  1. 如果类中涉及资源申请, 必须要自己写拷贝构造函数进行深拷贝; 如果不涉及, 不写使用编译器默认的拷贝构造函数也是可以的.

特性5

  1. 拷贝构造函数典型调用场景:
  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象
class Date
{
public:// 构造函数Date(int year, int minute, int day){cout << "Date(int, int, int)" << this << endl;}// 拷贝构造函数Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;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(2000, 1, 1);Test(d1);return 0;
}

在这里插入图片描述

如果我们返回的是一个树怎么办, 需要一个一个结点进行拷贝, 这所消耗的时间和空间都是巨大的.

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

5. 运算符重载

一般运算符重载

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

内置对象可以直接使用各种运算符, 但是自定义类型是不可以的.

运算符重载主要是用在类的比较上面的, 类比较是不能直接用语言自带的运算符的.例如下面的 Date 类:

class Date
{
private:int _year = 1970;int _month = 1;int _day = 1;public:// 构造函数Date(){}Date(int year, int month, int day){_year = year;_month = month;_day = day;}
};int main()
{Date d1;Date d2(2023, 1, 1);cout << (d1 > d2) << endl;return 0;
}

编译器会直接报错: 内置 > 运算符不支持类对象
在这里插入图片描述


涉及到类对象相关运算符操作, 就需要自己写相关比较函数了.

当然, 不可以使用外部函数 Compare()...

  • 函数名不够见名知意, 谁也不知道这个函数究竟是用来干什么的.
  • 类的成员变量一般都是 private 的, 不可以直接进行访问, 还需要写 get 成员函数, 太麻烦

C++ 提供了运算符重载的语法
函数名字为: 关键字 operator 后面接需要重载的运算符符号
函数原型: 返回值类型 operator 操作符(参数列表)

注意:

  • 不能通过连接其他符号来创建新的操作符: 比如 operator@
  • 重载操作符必须有一个类类型参数(操作符至少有一个对象)
  • 用于内置类型的运算符, 其含义不能改变. 例如: 内置的整型+, 不能改变其含义.
  • 作为类成员函数重载时, 其形参看起来比操作数少1, 因为成员函数的第一个参数为隐藏的 this 指针
  • .* :: sizeof ?: .注意以上5个运算符不能重载

首先写一下比较两个日期是否相等的运算符重载
先在类外面尝试一下, 这里先将 Date 的成员变量设置成 public

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;Date d2(2023, 1, 1);Date d3(1970, 1, 1);// 可以写成函数调用的形式cout << operator==(d1, d2) << endl;// 最好写成正常运算符的形式, 编译器会自己转化成运算符重载函数cout << (d1 == d3) << endl;return 0;
}

程序运行结果如下:

在这里插入图片描述

使用运算符重载可以规避乱取名, 可以直接使用运算符进行比较, 提高了程序的可读性.


但是成员公有的话, 就不能保证封装性了, 可以直接将运算符重载包装入类中, 需要简单修改一下:

class Date
{//...// 包装入类中后 只有一个参数了bool operator==(const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}
};int main()
{Date d1;Date d2(2023, 1, 1);Date d3(1970, 1, 1);// 两种调用也都是可以的cout << d1.operator==(d2) << endl;cout << (d1 == d3) << endl;return 0;
}

程序没有问题:
在这里插入图片描述

  • 不同于之前写在类外的运算符重载, 包装在类中的运算符重载只有一个参数, 另一个参数就是指向调用该成员函数的对象的 this 指针. 这和构造函数等都是一样的.等同于operator=(Date* const this, const Date& d)

  • 同时, 调用可以使用 对象.成员函数 的格式, 但是为了维护抽象性, 推荐直接使用运算符来进行调用, 编译器会自己来处理.


接下来是 > 运算符重载的实现

class Date
{//...int getMonthDay(int year, int month){int day[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};// 如果是二月且是闰年if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))){return 29;}return day[month];}bool operator>(const Date& d){if (_year > d._year)return true;if (_month > d._month)return true;if (_day > d._month)return true;return false;}
};int main()
{Date d1;Date d2(2023, 1, 1);Date d3(1970, 1, 1);cout << (d1 > d2) << endl; cout << (d2 > d3) << endl;return 0;
}

程序运行正确:
在这里插入图片描述


运算符重载和函数重载没有关系

  • 函数重载: 可以允许参数不同的同名函数
  • 运算符重载: 自定义类型可以直接使用运算符

++= 的运算符重载

首先先实现 += 的运算符重载, 再用 += 复用写 + 的运算符重载

Date& Date::operator+=(int day)
{// 如果 day 是负数, 直接进行-=if (day < 0){return (*this -= -day);}_day += day;// 如果日超出则进月份while(_day > getMonthDay(_year, _month)){_day -= getMonthDay(_year, _month);++_month;// 如果月超出则进年份if (_month == 13){++_year;_month = 1;}}return *this;
}Date Date::operator+(int day)
{Date temp(*this);temp += day;return temp;
}

+= 的实现需要注意的就是函数返回类型是 Date& 的, 这样可以省去一次的对象拷贝构造, 函数内部返回 *this 即可, 这里的 this 指针出函数作用域不会被销毁, 所以返回 *this 的引用是不会造成错误的.

重点需要研究的是 使用 += 复用 + 和 使用 + 复用 += 哪个效率更高.
在这里插入图片描述

目前只是日期类, 拷贝不会消耗太多性能, 但是如果是拷贝一个有着一万个结点的树呢? 那肯定拷贝会消耗很多性能.

+= 复用创建 + 明显是效率更高的. 同时, 如果要将一个对象进行加操作, += 的效率肯定是要比 + 效率高的.

赋值运算符重载

赋值运算符重载格式

  • 参数类型: const T&, 传递引用可以提高传参效率
  • 返回值类型: T&, 返回引用可以提高返回的效率, 有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回*this: 要符合连续赋值的含义
Date& Date::operator=(const Date& d)
{// 如果赋值自己本身, 直接返回自身if (&d == this){return *this;}_year = d._year;_month = d._month;_day = d._day;// *this就是类对象return *this;
}

赋值运算符重载和拷贝构造还是有点区别的, 赋值运算符重载使用的情况是: 已经有两个对象. 而拷贝构造则是: 只有一个已经存在的对象, 用该对象去拷贝初始化一个新对象.

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

在这里插入图片描述

如果我直接在类外定义赋值运算符重载, 编译器会直接报错: operator= 必须是非静态成员.

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

用户没有显式实现时, 编译器会生成一个默认赋值运算符重载, 以值的方式逐字节拷贝

对于 Date 类, 显然是没有必要进行显式的进行赋值运算符重载的. 但是对于需要开辟空间的类来说, 就需要自己显示实现赋值运算符重载, 具体内容涉及深拷贝, 之后会详细讲解.

前置 ++ 和 后置 ++ 重载

Date& Date::operator++()
{*this += 1;return *this;
}Date Date::operator++(int)
{Date temp(*this);*this += 1;return temp;
}

C++ 规定 operator++(int) 是前置 ++; operator++(int) 是后置 ++. 这是语法涉及, 涉及到无法逻辑闭环的操作, 只能进行特殊处理了.

让它们构成函数重载, 这样编译器就能进行区分了.

可以看出, 前置 ++ 的效率更高, 少了两次拷贝构造.

流插入 流提取 的重载

如果需要打印对象, 我们不可以直接 cout << 对象;, 需要自己实现

首先要知道, coutcin 其实分别是 ostream 类和 istream 类的对象
在这里插入图片描述

<<>> 分别接受两个参数, 左操作数必须是 ostream 或者 istream 的对象, 右操作数是要打印的对象.

学过了函数重载, 就可以很好理解为什么流插入和流读取可以自动识别类型了.
std 命名空间里, C++已经提供了常用的内置类型:
在这里插入图片描述

但是自定义类型的流插入和流提取需要我们自己实现
首先先像之前一样, 直接写在类域里面:

ostream& Date::operator<<(ostream& out)
{out << _year << '-' << _month << '-' << _day << endl; 
}istream& Date::operator>>(istream& in)
{in >> _year >> _month >> _day;
}

为了让 cout << 对象 后还能继续流插入, 需要返回 ostream 类型的引用

但是在main函数中, 却出现了问题:
在这里插入图片描述

正常方向写出现了错误, 但是反方向写却是正确的.

这里复习一下, 类中的成员函数, 第一个参数是一个隐含的 this 指针指向调用该函数的对象, 但是运算符重载第一个参数是左操作数, 第二个参数是右操作数.

这明显就反了, 为了将左右操作数顺序正确, 必须要将流插入和流提取的运算符重载放到全局中.

但这又会出现一个新的问题, private 的成员变量是不允许类外访问的

在这里插入图片描述

为了解决类外访问的问题, 这里简单引入一下友元的概念

在类外定义的函数, 如果在类域中声明时在最前面添加 friend, 表示该函数可以访问类成员私有变量

在这里插入图片描述

最终, 流插入流提取操作符重载实现完毕

在这里插入图片描述

可以看出, C++引入流的概念, 相比于 C语言使用 printf 打印对象, 要方便的多.


总结:

    1. 其他运算符一般是实现成成员函数
    1. 流运算符必须实现全局, 这样才能用流对象作为第一个参数

6. const 成员

之前写过 Date 类的成员函数 Print(), 用来打印对象的数据
但是这个函数是不安全的, 对象的内容可能会被修改, 正常的操作应该是将传入的参数前加上 const 修饰, 但是成员函数的 this 指针参数被隐藏了, 应该怎么办呢?

C++ 引入了可以对类成员函数修饰的 const

const 修饰的"成员函数"称之为 const 成员函数, const 修饰类成员函数, 实际修饰该成员函数隐含的 this 指针, 表明在该成员函数中不能对类的任何成员进行修改.

在这里插入图片描述


思考以下几个问题:

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

    不可以. const 修饰的对象为只读类型, 若调用非 const 成员函数, 属于权限放大, 只读权限变为可读可写.

  2. const 对象可以调用 const 成员函数吗?

    可以. 权限缩小, 可读可写变为只读.

  3. const 成员函数内可以调用其它的非 const 成员函数吗?

    不可以. const 修饰的对象为只读类型, 若调用非 const 成员函数, 属于权限放大, 只读权限变为可读可写.

  4. const 成员函数内可以调用 其它的 const 成员函数吗?

    可以. 权限缩小, 可读可写变为只读.

成员函数的定义规则:

  • 能定义成 const 的成员函数都应该定义成 const, 这样 const 和非 const 对象都能调用
  • 要修改成员变量的成员函数不能定义成 const, const 对象不能调用, 非 const 对象可以调用

7.取地址及 const 取地址操作符重载

这两个默认成员函数一般不用定义, 编译器会自己生成

Date* Date::operator&()
{return this;
}const Date* Date::operator&() const
{return this;
}

这两个运算符一般不需要重载, 使用编译器生成的默认取地址的重载即可, 只有特殊情况, 才需要重载, 比如想要别人获取到指定的内容

本章完.

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

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

相关文章

Instant-NGP论文笔记

文章目录 论文笔记 论文笔记 instant-ngp的nerf模型与vanilla nerf的模型架构相同。 instant-ngp的nerf模型包含两个MLP&#xff0c;第一个MLP就两个全连接&#xff0c;输入维度是32&#xff08;16层分辨率x2&#xff09;&#xff0c;输出是16&#xff08;用于预测密度&#x…

高防CDN:游戏应用抵御DDoS攻击的坚固堡垒

在当今的数字时代&#xff0c;游戏应用已经成为人们生活的一部分&#xff0c;而面临的网络威胁也日益复杂。其中&#xff0c;DDoS&#xff08;分布式拒绝服务&#xff09;攻击是游戏应用的一项严重威胁&#xff0c;可能导致游戏服务不可用&#xff0c;用户流失&#xff0c;以及…

Oracle安全基线检查

一、账户安全 1、禁止SYSDBA用户远程连接 用户具备数据库超级管理员(SYSDBA)权限的用户远程管理登录SYSDBA用户只能本地登录,不能远程。REMOTE_LOGIN_PASSWORDFILE函数的Value值为NONE。这意味着禁止共享口令文件,只能通过操作系统认证登录Oracle数据库。 1)检查REMOTE…

MASK-RCNN tensorflow环境搭建

此教程默认你已经安装了Anaconda&#xff0c;且tensorflow 为cpu版本。为什么不用gpu版本&#xff0c;原因下面解释。 此教程默认你已经安装了Anaconda。 因为tensorflow2.1后的gpu版&#xff0c;不支持windows。并且只有高版本的tensorflow才对应我的CUDA12.2&#xff1b; 而…

升级 MacOS 系统后,playCover 内游戏打不开了如何解决

我们有些小伙伴在升级了 macOS 系统后大概率会遇到之前能够正常使用的 playCover 突然游戏打不开了&#xff0c;最近 mac 刚刚正式推出了 MacOS 14.1 ,导致很多用户打开游戏会闪退&#xff0c;我们其实只需要更新一下 playCover 就可以解决 playCover 正式版更新会比较慢所以我…

vue-admin-template 安装遇到的问题

vue-element-admin 是一个后台前端解决方案&#xff0c;它基于 vue 和 element-ui实现。 参考文档&#xff1a; 官网&#xff1a; https://panjiachen.github.io/vue-element-admin-site/zh/guide/#%E5%8A%9F%E8%83%BD遇到的问题&#xff1a; npm ERR! Error while executing…

最新Ai系统ChatGPT程序源码+以图生图+Dall-E2绘画+支持GPT4+Midjourney绘画

一、AI创作系统 SparkAi创作系统是基于OpenAI很火的ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如…

APP攻防--ADB基础

进入app包 先使用 adb devices查看链接状态 手机连接成功的 adb shell 获取到手机的一个shell 此时想进入app包时没有权限的&#xff0c;APP包一般在data/data/下。没有执行权限&#xff0c;如图 Permission denied 权限被拒绝 此时需要手机root&#xff0c;root后输入 su …

旅游业为什么要选择VR全景,VR全景在景区旅游上有哪些应用

引言&#xff1a; VR全景技术的引入为旅游业带来了一场变革。这项先进技术不仅提供了前所未有的互动体验&#xff0c;还为景区旅游文化注入了新的生机。 一&#xff0e;VR全景技术&#xff1a;革新旅游体验 1.什么是VR全景技术&#xff1f; VR全景技术是一种虚拟现实技术&am…

合肥中科深谷嵌入式项目实战——人工智能与机械臂(六)

订阅&#xff1a;新手可以订阅我的其他专栏。免费阶段订阅量1000 python项目实战 Python编程基础教程系列&#xff08;零基础小白搬砖逆袭) 说明&#xff1a;本专栏持续更新中&#xff0c;订阅本专栏前必读关于专栏〖Python网络爬虫实战〗转为付费专栏的订阅说明作者&#xff1…

【ES专题】ElasticSearch集群架构剖析

目录 前言阅读对象阅读导航前置知识笔记正文一、ES集群架构1.1 为什么要使用ES集群架构1.2 ES集群核心概念1.2.1 节点1.2.1.1 Master Node主节点的功能1.2.1.2 Data Node数据节点的功能1.2.1.3 Master Node主节点选举流程 1.2.2 分片1.3 搭建三节点ES集群1.3.1 ES集群搭建步骤1…

数据库实验:SQL的数据更新

目录 实验目的实验内容实验要求实验步骤实验过程总结 再次书接上文&#xff0c;sql基础的增删改查 实验目的 (1) 掌握DBMS的数据查询功能 (2) 掌握SQL语言的数据更新功能 实验内容 (1) update 语句用于对表进行更新 (2) delete 语句用于对表进行删除 (3) insert 语句用于对表…

MSF暴力破解SID和检测Oracle漏洞

暴力破解SID 当我们发现 Oracle 数据库的 1521 端口时,我们可能考虑使用爆破 SID(System Identifier)来进行进一步的探测和认证。在 Oracle 中,SID 是一个数据库的唯一标识符。当用户希望远程连接 Oracle 数据库时,需要了解以下几个要素:SID、用户名、密码以及服务器的 I…

正点原子嵌入式linux驱动开发——Linux 网络设备驱动

网络驱动是linux里面驱动三巨头之一&#xff0c;linux下的网络功能非常强大&#xff0c;嵌入式linux中也常常用到网络功能。前面已经讲过了字符设备驱动和块设备驱动&#xff0c;本章就来学习一下linux里面的网络设备驱动。 嵌入式网络简介 嵌入式下的网络硬件接口 本次笔记…

分享一次无线话筒和接收机的配对经历BK9521/9522

最近老婆喜欢上了唱歌。我就需要为她准备歌曲和设备。装了台点歌机&#xff0c;买了软件&#xff0c;用4天的时间下了4T容量的歌曲&#xff0c;听过的没听过的都在里面&#xff0c;真的是太多了。 有了歌曲&#xff0c;就要有唱歌设备了。当我准备买无线话筒的时候&#xff0c…

centos7中多版本go安装

安装go的方式 官网下载tar.gz包安装 # 1.下载tar包 wget https://go.dev/dl/go1.18.1.linux-amd64.tar.gz # 2.解压tar包到指定路径 tar -xvf go1.18.1.linux-amd64.tar.gz -C /usr/local/go1.18 # 3.配置环境变量&#xff0c;打开 /etc/profile 文件添加以下文件每次开机时…

论文阅读:One Embedder, Any Task: Instruction-Finetuned Text Embeddings

1. 优势 现存的emmbedding应用在新的task或者domain上时表现会有明显下降&#xff0c;甚至在相同task的不同domian上的效果也不行。这篇文章的重点就是提升embedding在不同任务和领域上的效果&#xff0c;特点是不需要用特定领域的数据进行finetune而是使用instuction finetun…

C++对象模型

思考&#xff1a;对于实现平面一个点的参数化。C的class封装看起来比C的struct更加的复杂&#xff0c;是否意味着产生更多的开销呢&#xff1f; 实际上并没有&#xff0c;类的封装不会产生额外的开销&#xff0c;其实&#xff0c;C中在布局以及存取上的额外开销是virtual引起的…

【漏洞复现】Apache Log4j Server 反序列化命令执行漏洞(CVE-2017-5645)

感谢互联网提供分享知识与智慧&#xff0c;在法治的社会里&#xff0c;请遵守有关法律法规 文章目录 1.1、漏洞描述1.2、漏洞等级1.3、影响版本1.4、漏洞复现1、基础环境2、漏洞扫描3、漏洞验证 1.5、深度利用1、反弹Shell 说明内容漏洞编号CVE-2017-5645漏洞名称Log4j Server …

Python基础入门例程47-NP47 牛牛的绩点(条件语句)

最近的博文&#xff1a; Python基础入门例程46-NP46 菜品的价格&#xff08;条件语句&#xff09;-CSDN博客 Python基础入门例程45-NP45 禁止重复注册&#xff08;条件语句&#xff09;-CSDN博客 Python基础入门例程44-NP44 判断列表是否为空&#xff08;条件语句&#xff0…