文章目录
- 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
其特征如下
- 函数名与类名相同.
- 无返回值
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载
首先我们要注意的是, 全缺省函数和无参函数虽然构成函数重载, 但是不能同时存在, 会让编译器不知道具体调用哪一个:
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
- 如果类中没有显示定义构造函数, 则 C++ 编译器会自动生成一个无参的默认构造函数, 一旦用户显示定义编译器将不再生成.
一开始没有写构造函数, 直接创建对象是不会出问题的, 但是生成的是随机值, 可以大概得到一个模糊的结果: 编译器会自动生成一个默认构造函数.
一开始没有写构造函数, Date d1
不会报错
如果自己写了带参数的构造函数, Date d1
就会报错了
不实现构造函数的情况下, 编译器会生成默认的构造函数. 但是生成的仍然是随机值, 看起来构造函数却没有什么用?
特性6
- 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
- 无参构造函数, 全缺省构造函数, 我们没写编译器默认生成的构造函数, 都可以认为是默认构造函数.
但是只能有一个默认构造函数.
//无参构造函数
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;
}
关于如何写构造函数的策略:
- 一般情况下, 自己要写构造函数
- 如果成员都是自定义类型, 或者声明类时已经给了缺省值, 可以考虑让编译器自己生成
3. 析构函数
析构函数概念
构造函数可以对对象进行初始化, 那么这些数据是如何归还给操作系统的呢?
析构函数: 与构造函数功能相反, 析构函数不是完成对象本身的销毁, 局部对象销毁工作是由编译器完成的. 而对象在销毁时会自动调用析构函数, 完成对象中的资源清理工作.
析构函数特性
特性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
- 编译器生成的默认析构函数, 对内置类型不做处理, 自定义类型调用它的析构函数
还是双栈构造队列的代码:
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
- 如果类中没有申请资源时, 析构函数可以不写, 直接使用编译器生成的默认析构函数, 比如
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
类的析构函数后, 崩溃了
分析一下程序的操作
-
首先创建了一个
Stack
类的对象调用了一次构造函数 -
接着调用
func
, 函数是传值操作, 编译器直接创建了一个Stack
类类型的对象,(其实这个时候也调用了系统默认生成的构造函数) 并同时将main
中stack
的成员数据复制给给func
中的st
. 这个时候发现,stack
和st
两个对象的_array
的地址是一样的, 即指向一片空间
-
func
函数结束前, 需要销毁创建的临时变量st
, 调用析构函数, 直接释放了_array
的空间, 而这一片空间同时也被main
方法中的stack
使用着. -
最后
main
方法结束, 销毁stack
就出现错误了, 调用析构函数释放已经释放过的内存空间, 造成了内存泄漏.
由此可以看到, 编译器默认能做的只是浅拷贝, 如果拷贝需要动态开辟空间的类型对象, 会出现问题.
而自己写的拷贝构造函数就要做到"深拷贝", 如果被拷贝对象有动态开辟的空间, 需要把这一块空间的数据也拷贝一份到新的对象中去.
拷贝构造函数特性
特性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
- 若未显示定义, 编译器会生成默认的拷贝构造函数. 默认的拷贝构造函数对象按内存存储按字节序完成拷贝, 这种拷贝叫做浅拷贝, 或者值拷贝.
默认生成的拷贝构造函数完成值拷贝, 也是 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
- 如果类中涉及资源申请, 必须要自己写拷贝构造函数进行深拷贝; 如果不涉及, 不写使用编译器默认的拷贝构造函数也是可以的.
特性5
- 拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
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 << 对象;
, 需要自己实现
首先要知道, cout
和 cin
其实分别是 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
打印对象, 要方便的多.
总结:
-
- 其他运算符一般是实现成成员函数
-
- 流运算符必须实现全局, 这样才能用流对象作为第一个参数
6. const
成员
之前写过 Date
类的成员函数 Print()
, 用来打印对象的数据
但是这个函数是不安全的, 对象的内容可能会被修改, 正常的操作应该是将传入的参数前加上 const
修饰, 但是成员函数的 this
指针参数被隐藏了, 应该怎么办呢?
C++ 引入了可以对类成员函数修饰的 const
将
const
修饰的"成员函数"称之为const
成员函数,const
修饰类成员函数, 实际修饰该成员函数隐含的this
指针, 表明在该成员函数中不能对类的任何成员进行修改.
思考以下几个问题:
-
const
对象可以调用非const
成员函数吗?不可以.
const
修饰的对象为只读类型, 若调用非const
成员函数, 属于权限放大, 只读权限变为可读可写. -
非
const
对象可以调用const
成员函数吗?可以. 权限缩小, 可读可写变为只读.
-
const
成员函数内可以调用其它的非const
成员函数吗?不可以.
const
修饰的对象为只读类型, 若调用非const
成员函数, 属于权限放大, 只读权限变为可读可写. -
非
const
成员函数内可以调用 其它的const
成员函数吗?可以. 权限缩小, 可读可写变为只读.
成员函数的定义规则:
- 能定义成
const
的成员函数都应该定义成const
, 这样const
和非const
对象都能调用 - 要修改成员变量的成员函数不能定义成
const
,const
对象不能调用, 非const
对象可以调用
7.取地址及 const
取地址操作符重载
这两个默认成员函数一般不用定义, 编译器会自己生成
Date* Date::operator&()
{
return this;
}
const Date* Date::operator&() const
{
return this;
}
这两个运算符一般不需要重载, 使用编译器生成的默认取地址的重载即可, 只有特殊情况, 才需要重载, 比如想要别人获取到指定的内容
本章完.