1. 构造拷贝函数:用一个已有的对象,构造和它同类型的副本;
class xxx { xxx (const xxx &that) {...} };
&that是引用,拷贝构造函数推荐使用引用,如果直接传递值,那么会导致无限递归;
如果一个类没有定义拷贝构造函数,系统会提供一个缺省拷贝构造函数;
缺省拷贝构造函数对于基本类型的成员变量,按字节复制;
对于类类型成员变量,调用相应类型的拷贝构造函数,如 string;
class A {
public:
A (int data) {...}
// 当执行构造函数时,相当于执行下面这条缺省拷贝构造函数:
// A (const A &that) { data(that.data) {...} };
private:
int m_data;
};
void foo (A data) {...}
A bar() { A i; return i; }
int main(){
A a(10);
A b(a); // 拷贝构造,a 和 b 的值一样;ok
A c = a; // 这里用 a 初始化 c,也是拷贝构造;相当于 A c(a); ok
foo(a); // 也构成拷贝;ok
A d = bar(); // error,虽然这里的 bar 返回的也是 A 类型,但由于编译器自身的优化原则,这里不属于拷贝构造;
// 只是把 bar 的匿名对象起个新名字 d,并没有调用拷贝构造
A e(bar()); // 同上;error
A f(e); // 此时就调用拷贝构造;
return 0;
}
2. 在某些情况下,缺省拷贝构造函数只能实现浅拷贝,
如果需要获得深拷贝的复制效果,就需要自己定义拷贝构造函数;
一般下面两种情况需要自定义拷贝构造函数:
如果成员里面包含指针或者引用;
如果成员对象指向 new 地址;
class A {
public:
A (int data) : m_data(new int(data)) { delete m_data; }
private:
int *m_data; // 含有指针
};
int main(){
A a(10);
A b(a);
return 0;
}
编译时,会提示地址两次释放错误;
原因:创建 a 的时候,构造函数通过 new 在堆里面分配空间,并把地址给 a;
当把 a 赋值给 b 的时候,调用拷贝构造函数,同时也就把 new 的地址给了 b;
也就是说,a 和 b 指向同一个堆内存;当 a 执行完,调用析构,释放了那个内存;
b 执行完也要析构那个内存,但是此时内存已经不存在了,所以编译器报错,重复释放堆内存;
这里 a 和 b 地址一样,属于浅拷贝;
当 a 的值改变的时候,b 也跟着改变,这不符合拷贝原则;
拷贝原则是,拷贝完以后,a 和 b 是两个独立的元素;
解决这个问题的办法是:自己写一个拷贝构造函数,拷贝时,给 b 单独分配一个内存:
A (const A &that) { new int data(that.data) {...} }; // 深拷贝
3. 拷贝赋值运算符:
class X { X& operator= (const X& that) {...} };
如果一个类没有定义拷贝赋值运算符,系统会提供一个缺省拷贝赋值运算符;
缺省拷贝赋值运算符对于基本类型的成员变量,按字节复制;
对于类类型成员变量,调用相应类型的拷贝赋值运算符函数;
class A {
public:
A (int data) : m_data(new int(data)) { delete m_data; }
// A& operator= (const A& that) { m_data = that.m_data); }
int set (int data) {
return *m_data = data;
}
private:
int *m_data;
};
int main(){
A a(10);
A c(20);
A c(a); // 拷贝构造,c 值变为 10;
A c.set(30); // 给 c 重新赋值 30;
cout << a; // 最后 a 的值变为 30;而不是原来的 10;
return 0;
}
分析:上面另定义了 c 初值为 20,
当调用拷贝构造函数,把 a 复制给 c 后,
重新给 c 赋值,那么这个时候,a 也就被改变了,变成 30;
因为系统默认调用下面函数:
A& operator= (const A& that) { m_data = that.m_data); }
4. 缺省拷贝赋值运算符只能实现浅拷贝;
如果需要获得深拷贝的复制效果,就需要自己定义拷贝赋值运算符函数;
解决上面的办法,可以用下面的代码:(也是自定义拷贝赋值运算符函数的固定格式)
A& operator= (const A& that){ // 参数里的 & 是引用符号
if (&that != this){ // 防止自赋值,这里 & 是取地址,和 this 匹配;
delet m_data; // 释放旧资源
m_data = new int (*that.m_data); // 申请新空间,拷贝新数据
}
return *this; // 返回自引用
}
如果一个类什么都没写,那么编译器会帮我们写下下面的内容:
class A {
int x;
public:
A() {} // 构造
A(const A&) {} // 拷贝构造
A& oprator= (const A& that) {} // 拷贝赋值
~A() {} // 析构
};
5. 拷贝构造和拷贝赋值私有化:
class A {
public:
A (void) {...}
private:
A (const A&);
A operator= (const A&);
};
6. 静态成员变量和静态成员函数是属于类的,而非属于对象;
静态成员变量,可以被多个对象所共享,只有一份实例;
可以通过对象访问,也可以通过类访问;
由于静态成员变量不属于对象,通过对象访问,也就是通过对象访问类,再由类访问静态成员变量;
必须在类的外部定义,并初始化;
class A {
public:
static int m_i; // 声明
};
int A::m_i; // 定义,切记:给静态成员变量分配内存空间;也可以给其初始化;
int main() {
A::m_i = 10; // 类访问
A a1, a2;
++a1.m_i; // 对象访问,并 +1
a2.m_i; // m_i 为 11
return 0;
}
分析:虽然 m_i 被多个对象访问,但是这里只是一个 m_i;
首先通过类 A 访问 m_i 并赋值为 10;这时,所有 m_i 都为 10;
然后 a1 访问 m_i,并增加 1,这时,所有 m_i 都变为 11;
所以虽然最后 a2 调用 m_i,没作任何操作,但是此时的 m_i 值为 11;
静态成员变量本质上和全局变量没有区别;
只是多了作用域和访控属性的限制;
我们推荐对静态成员的访问用类来访问,这样可以增加可读性,且不用构造对象;
8. 静态成员函数,区别是没有 this 指针;
所以无法访问非静态的成员;
普通的成员函数可以访问静态成员变量;
class A {
public:
int i;
static int j;
void bar() {
j++; // ok, 非静态函数可以访问静态成员
foo(); // ok
}
static void foo() {
j++; // ok, 静态访问静态
i++; // error, 静态不能访问非静态
bar(); // error, 无 this 指针,无法访问非静态
cout << this; // error
}
};
int main(){
A::foo(); // ok, 类名调用,推荐
A a;
a.foo(); // ok, 对象调用
}
当功能不需要对象访问,只用类直接就可以访问,那么就定义为静态的;
9. 单例模式(静态实现)
客户只能创建一个对象;
思路:把构造私有化,不让创建对象;
在类里面自己建立一个对象,也私有化;
然后用静态类,创建一个引用出来,不是对象;
这时需要把拷贝构造也私有化,要不然会通过静态类创建多个对象;
饿汉模式:(hungry.cpp)
#include <iostream>
using namespace std;
class Single {
private:
Single () {} // 构造写到私有里
Single (const Single&); // 拷贝构造
static Single s_inst; // 内部声明
public:
static Single& getInst(){ return s_inst; } // 和全局函数一样,不能用 const,因为无 this 指针;
};
Single Single::s_inst; // 外部定义,分配空间;
int main() {
//Single s; // error,Single被私有
Single& s1 = Single::getInst(); // 拷贝构造被私有,所以这里只能用引用,不创造新对象;
Single& s2 = Single::getInst();
Single& s3 = Single::getInst();
return 0;
}
分析:虽然这里有 s1,s2,s3,但是用的是同一个 getInst();
所以s1,s2,s3 的地址是一样的;
懒汉模式:(lazy.cpp)
#include <iostream>
using namespace std;
class Single {
public:
static Single& getInst() {
if(!m_inst) // 如果为空
m_inst = new Single;
++m_cn; // 虽然只new一次,但是每引用一次,就加 1
return *m_inst;
}
void releaseInst(){ // 释放资源
if(m_cn && --m_cn == 0)
delete this; // 完了调用析构,所以在析构里置空
}
private:
static Single* m_inst;
static unsigned int m_cn;
Single () {} // 构造
Single (const Single&); // 拷贝构造
~Single () { m_inst = NULL; }
};
Single* Single::m_inst = NULL;
unsigned int Single::m_cn = 0;
int main() {
Single& s1 = Single::getInst(); // 调用构造
Single& s2 = Single::getInst();
Single& s3 = Single::getInst();
s3.releaseInst(); // 释放
s2.releaseInst();
s1.releaseInst();
return 0;
}
结果:只构造了一次,三个地址一样;
同样,析构也只析构了一次,最后一次调用的时候析构;
10. 成员变量指针
是一个相对地址,具体参考下面的举例
1)定义:
成员变量类型 类名 ::*指针变量名;
Student s1; // Student 类假设之前定义过了,里面有 m_name 成员;
sring* p = &s.m_name; // 这个p不是成员指针,因为它只指向 s1;
string Student::*p_name; // p_name 指向 Student 类中所有 string 类型的成员
2)初始化:
指针变量名 = &类名 ::成员变量名;
p_name = &Student::m_name;
定义和初始化写在一起:
string Student::*p_name = &Student::m_name;
3)解引用
对象 .* 指针变量名;
对象指针 ->* 指针变量名;
Student s, *p = &s;
s .* p_name = "zhangfei";
cout << p ->* p_name << endl;
这里 “点星” 和 “箭头星” 中间不能空格,这是一个完整操作符,c++ 特有;
成员变量指针(c++03b.avi)
是一个相对地址,具体参考下面的举例:
class Date {
public:
int year;
int month;
int day;
};
int main() {
Date d = {2012, 2, 23};
int Date::* p1 = &Date::year;
int Date::* p2 = &Date::monty;
int Date::* p3 = &Date::day;
cout << d.*p1; // 输出 2012;d.*p1 相当于 d.year;
cout << p1 << p2 << p3; // 输出 1 1 1
printf("%d%d%d", p1, p2, p3); // 输出 0 4 8
}
分析:cout 输出 1 1 1,是由于 c++ 自身原因导致的,1 代表 ture,不是真实结果;
利用 printf 输出 0 4 8,是真正的相对地址;year 相对于 Date 地址为 0,下面每个相差一个 int 大小;
11. 成员函数指针:
1)定义:
成员函数返回类型 (类名 ::*指针变量名)(参数表)
假如Student类里面有learn函数:
void learn(const string& lesson) const {}
则定义如下:
void (Student::*p_learn)(const string&) const;
2)初始化:
指针变量名 = &类名 ::成员函数名;
p_learn = &Student::learn;
3)解引用
(对象 . *指针变量名)(实参表);
(对象指针 -> *指针变量名)(实参表);
(s.*p_learn)("C++");
(p->*p_learn)("Linux");
如果是静态成员函数:
static void hello(void){...}
那么声明一样:
void (*phello)(void) = Student::hello;
但是调用不需要对象,可以直接用,类似于C:
phello();
12. 运算符重载,可以实现不同类型数据间的运算;
运算符分类:
1)双目操作符:L#R
成员函数形式操作符:L.operator# (R)
左调右参
全局函数形式操作符::: operator# (L, R)
左一右二,左操作数作第一个参数,右操作数作第二个参数
2)单目运算符:#O/O#
成员函数形式操作符:O.operator# ()
全局函数形式操作符::: operator# (O)
3)三目操作符:不考虑,无法重载;
13. 双目运算符
加、减、乘、除
操作数在计算前后不变;
表达式的值是右值,不可被赋值;
(a + b) = c; // error,编译错
复数举例:实现输出类似于 3 + 4 i 效果
class Complex{
public:
Complex (int r = 0, int i = 0):m_r(r), m_i(i){}
void print(void) const { // 输出
cout << m_r << '+' << m_r << 'i' << endl;
}
Complex add(Complex& c) { // 实现加法
return Complex(m_r + c.m_r, m_i + c.m_i);
}
private:
int m_r;
int m_i;
};
int main(){
Complex c1(1, 2);
Complex c2(3, 4);
Complex c3 = c1.add(c2);
c.print(); // 输出:4+6i
return 0;
}
上面这个复数例子,可以作以下修改:
可以把 add 换为 operator+,输出结果一样;
那么 Complex c3 = c1.add(c2);
可以改为:Complex c3 = c1.operator+(c2);
还可以进一步修改为:Complex c3 = c1 + c2; // 运算符重载
这里 operator 是关键字;
class Complex{
public:
Complex (int r = 0, int i = 0):m_r(r), m_i(i){}
void print(void) const { // 输出
cout << m_r << '+' << m_r << 'i' << endl;
}
const Complex operator+(const Complex& c) const { // 尽量使用 const,提高安全性
// Complex c3 = c1.operator+(c2);
// 第一个 const 返回右值,或者说函数返回值为常量,目的是让 c1+c2 不可以再被赋值;对应 c3
// 第二个 const 支持常量型右操作数,或者说支持传入常量实参;对应 c2,
// 第三个 const 支持常量型左操作数,或者说允许常量(this)调用此函数;对应 c1
// 第一个缩小作用域,第二第三都是扩大作用域;
return Complex(m_r + c.m_r, m_i + c.m_i);
}
private:
int m_r;
int m_i;
friend const Complex operator-(const Complex&, const Complex&); // 友员声明,为了访问私有成员变量
};
const Complex operator-(const Complex& l, const Complex& r) { // 这三个 const 和上面的功能一样
// Comple c3 = operator-(c1, c2);
// 另外这个是友员(全局)函数,无 this 指针,没有最后一个 const
return Complex(l.m_r - r.m_r, l.m_i - r.m_i);
}
int main(){
Complex c1(1, 2);
Complex c2(3, 4);
Complex c3 = c1 + c2;
c3.print(); // 输出:4+6i
c3 = c1 - c2;
return 0;
}
这里加法重载用的是成员函数,减法重载用的是全局(友员)函数;
我们推荐用全局写,原因如下:
c3 = c1 + 200; // ok, c3 = c1.operator+(200);
c3 = 200 + c1; // error, c3 = 200.operator+(c1);
友员函数会隐式的把 200 转换为类类型,具体看后期的类型转换;
class xxx { xxx (const xxx &that) {...} };
&that是引用,拷贝构造函数推荐使用引用,如果直接传递值,那么会导致无限递归;
如果一个类没有定义拷贝构造函数,系统会提供一个缺省拷贝构造函数;
缺省拷贝构造函数对于基本类型的成员变量,按字节复制;
对于类类型成员变量,调用相应类型的拷贝构造函数,如 string;
class A {
public:
A (int data) {...}
// 当执行构造函数时,相当于执行下面这条缺省拷贝构造函数:
// A (const A &that) { data(that.data) {...} };
private:
int m_data;
};
void foo (A data) {...}
A bar() { A i; return i; }
int main(){
A a(10);
A b(a); // 拷贝构造,a 和 b 的值一样;ok
A c = a; // 这里用 a 初始化 c,也是拷贝构造;相当于 A c(a); ok
foo(a); // 也构成拷贝;ok
A d = bar(); // error,虽然这里的 bar 返回的也是 A 类型,但由于编译器自身的优化原则,这里不属于拷贝构造;
// 只是把 bar 的匿名对象起个新名字 d,并没有调用拷贝构造
A e(bar()); // 同上;error
A f(e); // 此时就调用拷贝构造;
return 0;
}
2. 在某些情况下,缺省拷贝构造函数只能实现浅拷贝,
如果需要获得深拷贝的复制效果,就需要自己定义拷贝构造函数;
一般下面两种情况需要自定义拷贝构造函数:
如果成员里面包含指针或者引用;
如果成员对象指向 new 地址;
class A {
public:
A (int data) : m_data(new int(data)) { delete m_data; }
private:
int *m_data; // 含有指针
};
int main(){
A a(10);
A b(a);
return 0;
}
编译时,会提示地址两次释放错误;
原因:创建 a 的时候,构造函数通过 new 在堆里面分配空间,并把地址给 a;
当把 a 赋值给 b 的时候,调用拷贝构造函数,同时也就把 new 的地址给了 b;
也就是说,a 和 b 指向同一个堆内存;当 a 执行完,调用析构,释放了那个内存;
b 执行完也要析构那个内存,但是此时内存已经不存在了,所以编译器报错,重复释放堆内存;
这里 a 和 b 地址一样,属于浅拷贝;
当 a 的值改变的时候,b 也跟着改变,这不符合拷贝原则;
拷贝原则是,拷贝完以后,a 和 b 是两个独立的元素;
解决这个问题的办法是:自己写一个拷贝构造函数,拷贝时,给 b 单独分配一个内存:
A (const A &that) { new int data(that.data) {...} }; // 深拷贝
3. 拷贝赋值运算符:
class X { X& operator= (const X& that) {...} };
如果一个类没有定义拷贝赋值运算符,系统会提供一个缺省拷贝赋值运算符;
缺省拷贝赋值运算符对于基本类型的成员变量,按字节复制;
对于类类型成员变量,调用相应类型的拷贝赋值运算符函数;
class A {
public:
A (int data) : m_data(new int(data)) { delete m_data; }
// A& operator= (const A& that) { m_data = that.m_data); }
int set (int data) {
return *m_data = data;
}
private:
int *m_data;
};
int main(){
A a(10);
A c(20);
A c(a); // 拷贝构造,c 值变为 10;
A c.set(30); // 给 c 重新赋值 30;
cout << a; // 最后 a 的值变为 30;而不是原来的 10;
return 0;
}
分析:上面另定义了 c 初值为 20,
当调用拷贝构造函数,把 a 复制给 c 后,
重新给 c 赋值,那么这个时候,a 也就被改变了,变成 30;
因为系统默认调用下面函数:
A& operator= (const A& that) { m_data = that.m_data); }
4. 缺省拷贝赋值运算符只能实现浅拷贝;
如果需要获得深拷贝的复制效果,就需要自己定义拷贝赋值运算符函数;
解决上面的办法,可以用下面的代码:(也是自定义拷贝赋值运算符函数的固定格式)
A& operator= (const A& that){ // 参数里的 & 是引用符号
if (&that != this){ // 防止自赋值,这里 & 是取地址,和 this 匹配;
delet m_data; // 释放旧资源
m_data = new int (*that.m_data); // 申请新空间,拷贝新数据
}
return *this; // 返回自引用
}
如果一个类什么都没写,那么编译器会帮我们写下下面的内容:
class A {
int x;
public:
A() {} // 构造
A(const A&) {} // 拷贝构造
A& oprator= (const A& that) {} // 拷贝赋值
~A() {} // 析构
};
5. 拷贝构造和拷贝赋值私有化:
class A {
public:
A (void) {...}
private:
A (const A&);
A operator= (const A&);
};
6. 静态成员变量和静态成员函数是属于类的,而非属于对象;
静态成员变量,可以被多个对象所共享,只有一份实例;
可以通过对象访问,也可以通过类访问;
由于静态成员变量不属于对象,通过对象访问,也就是通过对象访问类,再由类访问静态成员变量;
必须在类的外部定义,并初始化;
class A {
public:
static int m_i; // 声明
};
int A::m_i; // 定义,切记:给静态成员变量分配内存空间;也可以给其初始化;
int main() {
A::m_i = 10; // 类访问
A a1, a2;
++a1.m_i; // 对象访问,并 +1
a2.m_i; // m_i 为 11
return 0;
}
分析:虽然 m_i 被多个对象访问,但是这里只是一个 m_i;
首先通过类 A 访问 m_i 并赋值为 10;这时,所有 m_i 都为 10;
然后 a1 访问 m_i,并增加 1,这时,所有 m_i 都变为 11;
所以虽然最后 a2 调用 m_i,没作任何操作,但是此时的 m_i 值为 11;
静态成员变量本质上和全局变量没有区别;
只是多了作用域和访控属性的限制;
我们推荐对静态成员的访问用类来访问,这样可以增加可读性,且不用构造对象;
8. 静态成员函数,区别是没有 this 指针;
所以无法访问非静态的成员;
普通的成员函数可以访问静态成员变量;
class A {
public:
int i;
static int j;
void bar() {
j++; // ok, 非静态函数可以访问静态成员
foo(); // ok
}
static void foo() {
j++; // ok, 静态访问静态
i++; // error, 静态不能访问非静态
bar(); // error, 无 this 指针,无法访问非静态
cout << this; // error
}
};
int main(){
A::foo(); // ok, 类名调用,推荐
A a;
a.foo(); // ok, 对象调用
}
当功能不需要对象访问,只用类直接就可以访问,那么就定义为静态的;
9. 单例模式(静态实现)
客户只能创建一个对象;
思路:把构造私有化,不让创建对象;
在类里面自己建立一个对象,也私有化;
然后用静态类,创建一个引用出来,不是对象;
这时需要把拷贝构造也私有化,要不然会通过静态类创建多个对象;
饿汉模式:(hungry.cpp)
#include <iostream>
using namespace std;
class Single {
private:
Single () {} // 构造写到私有里
Single (const Single&); // 拷贝构造
static Single s_inst; // 内部声明
public:
static Single& getInst(){ return s_inst; } // 和全局函数一样,不能用 const,因为无 this 指针;
};
Single Single::s_inst; // 外部定义,分配空间;
int main() {
//Single s; // error,Single被私有
Single& s1 = Single::getInst(); // 拷贝构造被私有,所以这里只能用引用,不创造新对象;
Single& s2 = Single::getInst();
Single& s3 = Single::getInst();
return 0;
}
分析:虽然这里有 s1,s2,s3,但是用的是同一个 getInst();
所以s1,s2,s3 的地址是一样的;
懒汉模式:(lazy.cpp)
#include <iostream>
using namespace std;
class Single {
public:
static Single& getInst() {
if(!m_inst) // 如果为空
m_inst = new Single;
++m_cn; // 虽然只new一次,但是每引用一次,就加 1
return *m_inst;
}
void releaseInst(){ // 释放资源
if(m_cn && --m_cn == 0)
delete this; // 完了调用析构,所以在析构里置空
}
private:
static Single* m_inst;
static unsigned int m_cn;
Single () {} // 构造
Single (const Single&); // 拷贝构造
~Single () { m_inst = NULL; }
};
Single* Single::m_inst = NULL;
unsigned int Single::m_cn = 0;
int main() {
Single& s1 = Single::getInst(); // 调用构造
Single& s2 = Single::getInst();
Single& s3 = Single::getInst();
s3.releaseInst(); // 释放
s2.releaseInst();
s1.releaseInst();
return 0;
}
结果:只构造了一次,三个地址一样;
同样,析构也只析构了一次,最后一次调用的时候析构;
10. 成员变量指针
是一个相对地址,具体参考下面的举例
1)定义:
成员变量类型 类名 ::*指针变量名;
Student s1; // Student 类假设之前定义过了,里面有 m_name 成员;
sring* p = &s.m_name; // 这个p不是成员指针,因为它只指向 s1;
string Student::*p_name; // p_name 指向 Student 类中所有 string 类型的成员
2)初始化:
指针变量名 = &类名 ::成员变量名;
p_name = &Student::m_name;
定义和初始化写在一起:
string Student::*p_name = &Student::m_name;
3)解引用
对象 .* 指针变量名;
对象指针 ->* 指针变量名;
Student s, *p = &s;
s .* p_name = "zhangfei";
cout << p ->* p_name << endl;
这里 “点星” 和 “箭头星” 中间不能空格,这是一个完整操作符,c++ 特有;
成员变量指针(c++03b.avi)
是一个相对地址,具体参考下面的举例:
class Date {
public:
int year;
int month;
int day;
};
int main() {
Date d = {2012, 2, 23};
int Date::* p1 = &Date::year;
int Date::* p2 = &Date::monty;
int Date::* p3 = &Date::day;
cout << d.*p1; // 输出 2012;d.*p1 相当于 d.year;
cout << p1 << p2 << p3; // 输出 1 1 1
printf("%d%d%d", p1, p2, p3); // 输出 0 4 8
}
分析:cout 输出 1 1 1,是由于 c++ 自身原因导致的,1 代表 ture,不是真实结果;
利用 printf 输出 0 4 8,是真正的相对地址;year 相对于 Date 地址为 0,下面每个相差一个 int 大小;
11. 成员函数指针:
1)定义:
成员函数返回类型 (类名 ::*指针变量名)(参数表)
假如Student类里面有learn函数:
void learn(const string& lesson) const {}
则定义如下:
void (Student::*p_learn)(const string&) const;
2)初始化:
指针变量名 = &类名 ::成员函数名;
p_learn = &Student::learn;
3)解引用
(对象 . *指针变量名)(实参表);
(对象指针 -> *指针变量名)(实参表);
(s.*p_learn)("C++");
(p->*p_learn)("Linux");
如果是静态成员函数:
static void hello(void){...}
那么声明一样:
void (*phello)(void) = Student::hello;
但是调用不需要对象,可以直接用,类似于C:
phello();
12. 运算符重载,可以实现不同类型数据间的运算;
运算符分类:
1)双目操作符:L#R
成员函数形式操作符:L.operator# (R)
左调右参
全局函数形式操作符::: operator# (L, R)
左一右二,左操作数作第一个参数,右操作数作第二个参数
2)单目运算符:#O/O#
成员函数形式操作符:O.operator# ()
全局函数形式操作符::: operator# (O)
3)三目操作符:不考虑,无法重载;
13. 双目运算符
加、减、乘、除
操作数在计算前后不变;
表达式的值是右值,不可被赋值;
(a + b) = c; // error,编译错
复数举例:实现输出类似于 3 + 4 i 效果
class Complex{
public:
Complex (int r = 0, int i = 0):m_r(r), m_i(i){}
void print(void) const { // 输出
cout << m_r << '+' << m_r << 'i' << endl;
}
Complex add(Complex& c) { // 实现加法
return Complex(m_r + c.m_r, m_i + c.m_i);
}
private:
int m_r;
int m_i;
};
int main(){
Complex c1(1, 2);
Complex c2(3, 4);
Complex c3 = c1.add(c2);
c.print(); // 输出:4+6i
return 0;
}
上面这个复数例子,可以作以下修改:
可以把 add 换为 operator+,输出结果一样;
那么 Complex c3 = c1.add(c2);
可以改为:Complex c3 = c1.operator+(c2);
还可以进一步修改为:Complex c3 = c1 + c2; // 运算符重载
这里 operator 是关键字;
class Complex{
public:
Complex (int r = 0, int i = 0):m_r(r), m_i(i){}
void print(void) const { // 输出
cout << m_r << '+' << m_r << 'i' << endl;
}
const Complex operator+(const Complex& c) const { // 尽量使用 const,提高安全性
// Complex c3 = c1.operator+(c2);
// 第一个 const 返回右值,或者说函数返回值为常量,目的是让 c1+c2 不可以再被赋值;对应 c3
// 第二个 const 支持常量型右操作数,或者说支持传入常量实参;对应 c2,
// 第三个 const 支持常量型左操作数,或者说允许常量(this)调用此函数;对应 c1
// 第一个缩小作用域,第二第三都是扩大作用域;
return Complex(m_r + c.m_r, m_i + c.m_i);
}
private:
int m_r;
int m_i;
friend const Complex operator-(const Complex&, const Complex&); // 友员声明,为了访问私有成员变量
};
const Complex operator-(const Complex& l, const Complex& r) { // 这三个 const 和上面的功能一样
// Comple c3 = operator-(c1, c2);
// 另外这个是友员(全局)函数,无 this 指针,没有最后一个 const
return Complex(l.m_r - r.m_r, l.m_i - r.m_i);
}
int main(){
Complex c1(1, 2);
Complex c2(3, 4);
Complex c3 = c1 + c2;
c3.print(); // 输出:4+6i
c3 = c1 - c2;
return 0;
}
这里加法重载用的是成员函数,减法重载用的是全局(友员)函数;
我们推荐用全局写,原因如下:
c3 = c1 + 200; // ok, c3 = c1.operator+(200);
c3 = 200 + c1; // error, c3 = 200.operator+(c1);
友员函数会隐式的把 200 转换为类类型,具体看后期的类型转换;