1.运算符重载
之前有一个案例如下所示 其中我们可以通过add方法将两个点组成一个新的点
class Point {
friend Point add(Point, Point);
int m_x;
int m_y;
public:
Point(int x, int y) : m_x(x), m_y(y) {
}
void display() {
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
};
Point add(Point p1, Point p2) {
return Point(p1.m_x + p2.m_x, p1.m_y + p2.m_y);
}
int main() {
Point p1(10, 20);
Point p2(20, 30);
Point p3 = add(p1, p2);
p3.display();
getchar();
return 0;
}
但是有一个想法就是 可不可以直接通过加法运算将两个顶点组成新的点 即Point p3 = p1 + p2
答案是可以的 我们可以通过实现operator+来为加法运算增加新功能
以下案例中 我们通过重载方法operator实现了Point对象的加法运算
class Point {
friend Point operator+(Point p1, Point p2);
int m_x;
int m_y;
public:
Point(int x, int y) : m_x(x), m_y(y) {
}
void display() {
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
};
Point operator+(Point p1, Point p2) {
return Point(p1.m_x + p2.m_x, p1.m_y + p2.m_y);
}
int main() {
Point p1(10, 20);
Point p2(20, 30);
Point p3 = p1 + p2;
p3.display();
getchar();
return 0;
}
其实p1 + p2的本质就是调用了operator+(p1, p2)
我们再来看一下以下这个案例
class Point {
friend Point operator+(Point p1, Point p2);
int m_x;
int m_y;
public:
Point(int x, int y) : m_x(x), m_y(y) {
}
void display() {
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
};
Point operator+(Point p1, Point p2) {
return Point(p1.m_x + p2.m_x, p1.m_y + p2.m_y);
}
int main() {
Point p1(10, 20);
Point p2(20, 30);
Point p3(30, 40);
Point p4 = p1 + p2 + p3;
p4.display();
getchar();
return 0;
}
我们应该可以知道p1 + p2 + p3的本质其实就是调用了两次operator+
我们可以对上述operator+方法进行优化 具体改写成
修改原因:1.对象类型做参数时 可能会产生不必要的中间对象 所以尽量使用引用/指针来避免
2.参数用const修饰可以使得参数接受范围更大 因为他可以接收const和非const参数 而非const形参显然只能接收非const实参
基于以上两点原因 有了以下优化代码
Point operator+(const Point& p1, const Point& p2) {
return Point(p1.m_x + p2.m_x, p1.m_y + p2.m_y);
}
说到这里 我们应该就可以明白拷贝构造函数形参如此写法的原因了
原因有二:1.如果我们的形参是一个对象类型的话 那么当我们调用拷贝构造函数创建一个新对象时 就会无限次的调用该拷贝构造函数 原因在于将实参赋值给形参就会创建新对象 从而调用拷贝构造函数 调用了之后 又会将实参赋值给形参从而调用构造函数 之后就一直重复这个过程 但当我们将对象类型改成引用类型以后 我们就可以杜绝无限调用的现象
2.如果我们传递的实参是const修饰 那么显然非const形参是无法接收这个实参的 所以必须要将形参设计为const修饰 才能接收更大范围的实参
其实对于运算符重载函数而言 我们还可以进一步优化 将其内置于类内部 变成成员函数 这样做的好处是我们可以不用通过友元函数的声明来实现对类内部私有成员的访问 在类内部的成员函数就可以直接访问类内部的私有成员了
class Point {
int m_x;
int m_y;
public:
Point(int x, int y) : m_x(x), m_y(y){}
void display() {
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
Point operator+(const Point& p2) {
return Point(m_x + p2.m_x, m_y + p2.m_y);
}
};
int main() {
Point p1(10, 20);
Point p2(20, 30);
// 相当于调用了p1对象内部的operator+函数 即p1.operator+(p2)
Point p3 = p1 + p2;
p3.display();
getchar();
return 0;
}
模仿以上案例 我们可以自己来实现一下减法运算符重载函数 其中p2 - p1的本质就是调用了p2.operator-(p1)
class Point {
int m_x;
int m_y;
public:
Point(int x, int y) : m_x(x), m_y(y){}
void display() {
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
Point operator+(const Point& p2) {
return Point(m_x + p2.m_x, m_y + p2.m_y);
}
Point operator-(const Point& p2) {
return Point(m_x - p2.m_x, m_y - p2.m_y);
}
};
int main() {
Point p1(10, 20);
Point p2(20, 30);
Point p3 = p2 - p1;
p3.display();
getchar();
return 0;
}
但是呢 有一个潜在的问题 我们知道 对一个临时数据赋值是没有任何意义的 因为他马上就要销毁了 而在赋值到销毁的这段过程中完全是发生在当前语句中 所以压根不能被任何人使用
在c++中 允许对一个表达式进行赋值操作的 也允许对对象之间的运算结果进行赋值操作 但是不允许对常量进行赋值操作
表达式中对象之间的运算结果便是一个临时数据(未用一块内存进行储存) 虽然可以对其赋值 但是没有任何意义 所以我们要杜绝这种赋值行为
我们可以将返回值设置成常量 这样表达式的结果就是一个常量 自然而然是不可以对一个常量进行赋值操作的
class Point {
int m_x;
int m_y;
public:
Point(int x, int y) : m_x(x), m_y(y){}
void display() {
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
const Point operator+(const Point& p2) {
return Point(m_x + p2.m_x, m_y + p2.m_y);
}
const Point operator-(const Point& p2) {
return Point(m_x - p2.m_x, m_y - p2.m_y);
}
};
int main() {
Point p1(10, 20);
Point p2(20, 30);
(p1 + p2) = Point(40, 50);// error
getchar();
return 0;
}
但是一旦运算符重载函数的返回值修改成了常量 那么之前的三数相加或者三数相减的操作就不被允许了 为什么呢 这是因为const对象只能调用const函数 不能调用非const函数的缘故 所以我们需要将运算符重载函数修改成const修饰的函数
class Point {
int m_x;
int m_y;
public:
Point(int x, int y) : m_x(x), m_y(y){}
void display() {
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
const Point operator+(const Point& p2) {
return Point(m_x + p2.m_x, m_y + p2.m_y);
}
const Point operator-(const Point& p2) {
return Point(m_x - p2.m_x, m_y - p2.m_y);
}
};
int main() {
Point p1(10, 20);
Point p2(20, 30);
Point p3(30, 40);
Point p4 = p1 + p2 + p3;// error
getchar();
return 0;
}
class Point {
int m_x;
int m_y;
public:
Point(int x, int y) : m_x(x), m_y(y){}
void display() {
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
const Point operator+(const Point& p2) const {
return Point(m_x + p2.m_x, m_y + p2.m_y);
}
const Point operator-(const Point& p2) const {
return Point(m_x - p2.m_x, m_y - p2.m_y);
}
};
int main() {
Point p1(10, 20);
Point p2(20, 30);
Point p3(30, 40);
Point p4 = p1 + p2 + p3;// ok
p4.display();
getchar();
return 0;
}
除了加法和减法运算 我们还可以实现一下+=运算符的重载函数
class Point {
int m_x;
int m_y;
public:
Point(int x, int y) : m_x(x), m_y(y){}
void display() {
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
const Point operator+(const Point& p2) const {
return Point(m_x + p2.m_x, m_y + p2.m_y);
}
const Point operator-(const Point& p2) const {
return Point(m_x - p2.m_x, m_y - p2.m_y);
}
void operator+=(const Point& p2){
m_x += p2.m_x;
m_y += p2.m_y;
}
};
int main() {
Point p1(10, 20);
Point p2(20, 30);
p1 += p2;
p1.display();
getchar();
return 0;
}
但是a += b这个表达式是可以被赋值的 因为表达式的结果是一块内存 所以p1 += p2是可以进行赋值操作的 我们希望可以有+=重载函数有一个返回值 这样由此创建的表达式就可以进行赋值操作了
class Point {
int m_x;
int m_y;
public:
Point(int x, int y) : m_x(x), m_y(y){}
void display() {
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
const Point operator+(const Point& p2) const {
return Point(m_x + p2.m_x, m_y + p2.m_y);
}
const Point operator-(const Point& p2) const {
return Point(m_x - p2.m_x, m_y - p2.m_y);
}
Point operator+=(const Point& p2){
m_x += p2.m_x;
m_y += p2.m_y;
return *this;
}
};
int main() {
Point p1(10, 20);
Point p2(20, 30);
(p1 += p2) = Point(40, 50);
p1.display();
getchar();
return 0;
}
但是 打印的结果不符合我们的预期 因为实际上p1 += p2的结果是一个全新的对象 只不过他拷贝了p1的数据而已 为了防止函数调用完毕局部变量被回收的现象
所以说为了避免中间对象的产生(主要是为了减少内存开销 但是可能会指向一块回收后再次分配的内存)而导致数据的错误 我们需要将返回值改成引用类型的
class Point {
int m_x;
int m_y;
public:
Point(int x, int y) : m_x(x), m_y(y){}
void display() {
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
const Point operator+(const Point& p2) const {
return Point(m_x + p2.m_x, m_y + p2.m_y);
}
const Point operator-(const Point& p2) const {
return Point(m_x - p2.m_x, m_y - p2.m_y);
}
Point& operator+=(const Point& p2){
m_x += p2.m_x;
m_y += p2.m_y;
return *this;
}
};
int main() {
Point p1(10, 20);
Point p2(20, 30);
(p1 += p2) = Point(40, 50);
p1.display();
getchar();
return 0;
}
由于+=重载方法中需要修改成员变量 而const函数不能修改 所以+=重载方法是不可以由const修饰的
我们在来看一下==运算符重载函数的实现 但是需要注意的是如果是const对象调用operator ==方法的话 那么该方法只能够是const修饰
class Point {
int m_x;
int m_y;
public:
Point(int x, int y) : m_x(x), m_y(y){}
void display() {
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
const Point operator+(const Point& p2) const {
return Point(m_x + p2.m_x, m_y + p2.m_y);
}
const Point operator-(const Point& p2) const {
return Point(m_x - p2.m_x, m_y - p2.m_y);
}
Point& operator+=(const Point& p2){
m_x += p2.m_x;
m_y += p2.m_y;
return *this;
}
bool operator==(const Point& p2) const {
return ((m_x == p2.m_x) && (m_y == p2.m_y));
}
};
int main() {
Point p1(10, 20);
Point p2(20, 30);
cout << (p1 == p2) << endl;
getchar();
return 0;
}
我们实现一下!=运算符重载函数 但是需要注意的是如果是const对象调用operator!=方法的话 那么该方法只能够是const修饰
class Point {
int m_x;
int m_y;
public:
Point(int x, int y) : m_x(x), m_y(y){}
void display() {
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
const Point operator+(const Point& p2) const {
return Point(m_x + p2.m_x, m_y + p2.m_y);
}
const Point operator-(const Point& p2) const {
return Point(m_x - p2.m_x, m_y - p2.m_y);
}
Point& operator+=(const Point& p2){
m_x += p2.m_x;
m_y += p2.m_y;
return *this;
}
bool operator==(const Point& p2) const {
return ((m_x == p2.m_x) && (m_y == p2.m_y));
}
bool operator!=(const Point& p2) const {
return ((m_x != p2.m_x) || (m_y != p2.m_y));
}
};
int main() {
Point p1(10, 20);
Point p2(20, 30);
cout << (p1 != p2) << endl;
getchar();
return 0;
}
我们实现一下负号运算符重载函数 请注意 负号是不会改变操作数的数值的
既然负号不会改变操作数的数值 那么我们就需要返回一个新建的对象
由于返回值构成的表达式可以被赋值 而赋值操作没有任何意义 所以我们需要禁止赋值 所以用const修饰返回值
如果我们接连使用多次负号 那么就会产生const对象调用的问题 const对象只能够调用const函数 所以我们需要用const修饰函数
class Point {
int m_x;
int m_y;
public:
Point(int x, int y) : m_x(x), m_y(y){}
void display() {
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
const Point operator+(const Point& p2) const {
return Point(m_x + p2.m_x, m_y + p2.m_y);
}
const Point operator-(const Point& p2) const {
return Point(m_x - p2.m_x, m_y - p2.m_y);
}
Point& operator+=(const Point& p2){
m_x += p2.m_x;
m_y += p2.m_y;
return *this;
}
bool operator==(const Point& p2) {
return ((m_x == p2.m_x) && (m_y == p2.m_y));
}
bool operator!=(const Point& p2) {
return ((m_x != p2.m_x) || (m_y != p2.m_y));
}
const Point operator-() const {
return Point(-m_x, -m_y);
}
};
int main() {
Point p1(10, 20);
Point p2(20, 30);
Point p3 = (-(-p1));
p1.display();
p3.display();
getchar();
return 0;
}
我们在来实现一下自增运算符重载函数
这得分成两类 一类是前缀自增运算符重载函数 一类则是后缀自增运算符重载函数 这两类在写法上有所区分 前者就是默认的写法 而后者则是基于默认写法的基础上在参数列表中加入int
我们先来讲讲前缀自增运算符重载函数
由于前缀自增表达式可以被赋值 所以需要有返回值
再者 为了防止产生中间对象而导致数据错乱 所以我们需要让返回值变成引用类型
class Point {
int m_x;
int m_y;
public:
Point(int x, int y) : m_x(x), m_y(y){}
void display() {
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
const Point operator+(const Point& p2) const {
return Point(m_x + p2.m_x, m_y + p2.m_y);
}
const Point operator-(const Point& p2) const {
return Point(m_x - p2.m_x, m_y - p2.m_y);
}
Point& operator+=(const Point& p2){
m_x += p2.m_x;
m_y += p2.m_y;
return *this;
}
bool operator==(const Point& p2) {
return ((m_x == p2.m_x) && (m_y == p2.m_y));
}
bool operator!=(const Point& p2) {
return ((m_x != p2.m_x) || (m_y != p2.m_y));
}
const Point operator-() const {
return Point(-m_x, -m_y);
}
Point& operator++() {
m_x++;
m_y++;
return *this;
}
};
int main() {
Point p1(10, 20);
Point p2(20, 30);
(++p1) = Point(40, 50);
p1.display();
getchar();
return 0;
}
然后再来讲讲后缀自增运算符重载函数
由于后缀自增表达式可以参与到加法运算中 所以需要返回值Point 但是同时也可以进行赋值运算了
但是由于我的预期是对最新的值进行赋值操作 而不是针对旧值 所以说 我们不能进行赋值操作 所以需要用const修饰返回值
class Point {
int m_x;
int m_y;
public:
Point(int x, int y) : m_x(x), m_y(y){}
void display() {
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
const Point operator+(const Point& p2) const {
return Point(m_x + p2.m_x, m_y + p2.m_y);
}
const Point operator-(const Point& p2) const {
return Point(m_x - p2.m_x, m_y - p2.m_y);
}
Point& operator+=(const Point& p2){
m_x += p2.m_x;
m_y += p2.m_y;
return *this;
}
bool operator==(const Point& p2) {
return ((m_x == p2.m_x) && (m_y == p2.m_y));
}
bool operator!=(const Point& p2) {
return ((m_x != p2.m_x) || (m_y != p2.m_y));
}
const Point operator-() const {
return Point(-m_x, -m_y);
}
Point& operator++() {
m_x++;
m_y++;
return *this;
}
const Point operator++(int) {
Point old(m_x, m_y);
m_x++;
m_y++;
return old;
}
};
int main() {
Point p1(10, 20);
Point p2(20, 30);
Point p3 = p2++ + Point(40, 50);
p3.display();
getchar();
return 0;
}
我们再来实现一下左移运算符重载函数
首先左移运算符重载函数不可以写在类内部 因为这样会导致<<左边必须为对象类型 而真实代码中<<左边必须是cout(cout是一个类对象 是ostream类对象 ) 所以需要将重载方法定义为全局方法 但是我们就需要将该方法声明为友元方法 这样才能够访问类内部的私有成员
再者 重载方法需要返回值 因为<<可以连用 并且<<左边必须是cout对象 所以返回值是ostream类型 为了防止中间对象的产生 所以返回值需要为ostream&引用类型 至于说要不要加上const修饰 通过测试基本类型的cout的赋值操作(比如:cout << 1 = cout) 我们可以知道赋值操作是行不通的 所以我们需要将返回值设置为常量 即用const修饰返回值 而且由于返回值是const的缘故 导致形参中cout也必须是const修饰
我说实话 const只能够修饰成员函数 并不能修饰全局函数(const对象只能调用const成员函数 但是对于全局函数来说 有无const没有任何影响)
并且由于C++内置的cout行为所在的方法中形参cout也是非const修饰 基于这一点 我们都不能把返回值设置为const修饰
至于说 左移运算符重载函数内部要不要写死endl 如果你是想要将endl灵活的布局在cout语句中 那么请不要写死 反之可以写死 而且千万不要有这种想法 就是cout << endl;执行的是我们自定义的这个方法 实际上他执行的是C++内置的行为
class Point {
friend ostream& operator<<(ostream& cout, const Point& p);
int m_x;
int m_y;
public:
Point(int x, int y) : m_x(x), m_y(y){}
void display() {
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
const Point operator+(const Point& p2) const {
return Point(m_x + p2.m_x, m_y + p2.m_y);
}
const Point operator-(const Point& p2) const {
return Point(m_x - p2.m_x, m_y - p2.m_y);
}
Point& operator+=(const Point& p2){
m_x += p2.m_x;
m_y += p2.m_y;
return *this;
}
bool operator==(const Point& p2) {
return ((m_x == p2.m_x) && (m_y == p2.m_y));
}
bool operator!=(const Point& p2) {
return ((m_x != p2.m_x) || (m_y != p2.m_y));
}
const Point operator-() const {
return Point(-m_x, -m_y);
}
Point& operator++() {
m_x++;
m_y++;
return *this;
}
const Point operator++(int) {
Point old(m_x, m_y);
m_x++;
m_y++;
return old;
}
};
ostream& operator<<(ostream& cout, const Point& p) {
cout << "(" << p.m_x << ", " << p.m_y << ")";
return cout;
}
int main() {
Point p1(10, 20);
Point p2(20, 30);
cout << p1 << p2 << endl;
getchar();
return 0;
}
最后再来实现一下右移运算符重载函数 同样的 由于成员函数要求>>左边必须是对象类型 而不是istream类型 所以我们得将其声明为全局函数
接着 由于C++中内置的cin的行为所在的函数的形参cin是非const修饰 所以编译器为了让我们统一格式 所以也要求我们自定义的方法中形参的cin必须不被const所修饰 所以也就要求我们函数的返回值不被const修饰
我们也不能用const修饰全局函数
由于我们键入数据的行为相当于对指定变量进行修改 所以我们不能够将形参中的对象类型用const修饰 因为常量是不可以修改的
class Point {
friend ostream& operator<<(ostream& cout, const Point& p);
friend istream& operator>>(istream& cin, Point& p);
int m_x;
int m_y;
public:
Point(int x, int y) : m_x(x), m_y(y){}
void display() {
cout << "(" << m_x << ", " << m_y << ")" << endl;
}
const Point operator+(const Point& p2) const {
return Point(m_x + p2.m_x, m_y + p2.m_y);
}
const Point operator-(const Point& p2) const {
return Point(m_x - p2.m_x, m_y - p2.m_y);
}
Point& operator+=(const Point& p2){
m_x += p2.m_x;
m_y += p2.m_y;
return *this;
}
bool operator==(const Point& p2) {
return ((m_x == p2.m_x) && (m_y == p2.m_y));
}
bool operator!=(const Point& p2) {
return ((m_x != p2.m_x) || (m_y != p2.m_y));
}
const Point operator-() const {
return Point(-m_x, -m_y);
}
Point& operator++() {
m_x++;
m_y++;
return *this;
}
const Point operator++(int) {
Point old(m_x, m_y);
m_x++;
m_y++;
return old;
}
};
ostream& operator<<(ostream& cout, const Point& p) {
cout << "(" << p.m_x << ", " << p.m_y << ")";
return cout;
}
istream& operator>>(istream& cin, Point& p) {
cin >> p.m_x;
cin >> p.m_y;
return cin;
}
int main() {
Point p1(10, 20);
Point p2(20, 30);
cin >> p1 >> p2;
cout << p1 << p2 << endl;
getchar();
return 0;
}
我们在自定义对象类型的相关运算符的重载函数时 优先考虑成员函数 不行在定义为全局函数
那么为什么对于刚才的左移运算符重载函数和右移运算符重载函数 他们的返回值既然都是非const 那理应可以进行赋值操作 为什么反而不行 这是因为在ostream/istream类中 赋值运算符重载函数是一个私有的函数 不可访问
从这个解答 我们也可以得出一个思路:
之前的对象拷贝操作如果没有自定义拷贝构造函数的话 那么就得对所有的成员进行赋值操作 但是如果不想要对所有的成员赋值的话 那么我们也可以通过拷贝构造函数进行部分成员的赋值 但是如果连拷贝构造函数都没有的话 那么我们也可以在类中自定义一个赋值运算符重载函数已完成部分成员的赋值操作
总结一下 对于以上重载函数的书写 有个大致的思路:
首先我们可以将基本类型的解决方法代入到对象类型的程序流程中
接着我们需要判断是否需要返回值 这取决于我们是否需要连用、赋值或者参与运算符等其他操作
接着需要判断一下返回值是否需要设置为引用类型 如果没有引用类型会使得预期出现偏差的话 那么我们就需要将返回值设计为引用类型
接着如果返回值确定为不可赋值的话 那么显然为常量 所以返回值需要用const修饰
然后函数需不需要用const修饰取决于你是否需要通过const对象去调用成员函数 因为对于const对象而言 他只可以调用const修饰的成员函数 但是对于全局函数 有无const不影响