重载 << 操作符

操作符 < < 的原生意义是按位左移 , 例 :
1« 2 ;
其意义是将整数 1 按位左移 2 位 , 即 :
0000 0001 ==》 0000 0100
重载左移操作符 , 将变量或常量左移到一个对象中 ! image-20220224191858704

我们已经实现了将1和'\n'字符传送到命令行中。我们可以继续优化,<<能够连续地接收输入,并且将换行符修改为常量endl

image-20220224192148187

接着继续实现重载函数,将字符串,浮点数等等都能够接收输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>

const char endl = '\n';

class Console
{
public:
Console& operator << (int i)
{
printf("%d", i);

return *this;
}
Console& operator << (char c)
{
printf("%c", c);

return *this;
}
Console& operator << (const char* s)
{
printf("%s", s);

return *this;
}
Console& operator << (double d)
{
printf("%f", d);

return *this;
}
};

Console cout;

int main()
{
cout << 1 << endl;
cout << "D.T.Software" << endl;

double a = 0.1;
double b = 0.2;

cout << a + b << endl;

return 0;
}

重复发明轮子并不是一件有创造性的事 ,站在巨人的眉膀上解决问题会更加有效 !

我们的前辈想到了面向对象的思想,自然就将显示器和键盘映射到C++的对象!!

image-20220224193535183

C++标准库

  • C+ + 标准库并不是 C+ + 语言的一部分
  • C+ + 标准库是由类库和函数库组成的集合
  • C+ + 标准库中定义的类和对象都位于 s t d 命名空间中
  • C+ + 标准库的头文件都不带 .h 后缀
  • C+ + 标准库涵盖了 C 库的功能

image-20220224192504824

C++扩展语法对于不同编译器是不一样的,由C++编译器生产产生实现,而C++标准语法模块是一致的。

C语言兼容库可以兼容之前C语言实现的代码,C++编译器也能实现C语言实现的代码。如:<stdio.h> <string.h> <stdlib.h> <math.h>

image-20220224192751751

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>

using namespace std;


int main()
{
printf("Hello world!\n");

char* p = (char*)malloc(16);

strcpy(p, "Software");

double a = 3;
double b = 4;
double c = sqrt(a * a + b * b);

printf("c = %f\n", c);

free(p);

return 0;
}

C++标准库<cstdio><cstring><cstdlib><cmath>可以兼容C库的实现,上述代码运行效果与C语言实现效果一样

同时C++编译器厂商为了自己的产品能够卖出,即使在C++编译器里用<stdio.h>这样的C库,代码也能正常运行,就提供了C语言兼容库,里面存储的是<stdio.h>这样的文件

C++中的字符串类

  • C+ + 语言直接支持 C 语言的所有概念
  • C+ + 语言中没有原生的字符串类型
  • C+ + 标准库提供了 string 类型
    - string 直接支持字符串连接
    - string 直接支持字符串的大小比较
    - string 直接支持子串查找和提取
    - string 直接支持字符串的插入和替换

string -> 数 字

1
2
3
istringstream iss(123.45.1);
double num;
iss >> num;

数字 -> string

1
2
3
ostringstream oss;
oss << 543.21;
string s = oss.str();

字符串类的兼容性

  • string 类最大限度的考虑了 C 字符串的兼容性
  • 可以按照使用 C 字符串的方式使用 string 对象
1
2
3
4
5
6
7
string s = "a1b2c3d4e5"
int n = 0;
for(int i = 0; i < s.length(); i++)
{
if( isdigit(s[i]) )
n++;
}

那么类的对象怎么支持数组的下标访问 ?

重载数组访问操作符

被忽略的事实。 。 。
- 数组访问符是 C/C+ + 中的内置操作符(和’+’,’-‘地位相同)
- 数组访问符的原生意义是数组访问指针运算

image-20220225142525362

数组访问操作符( [ ] )

  • 只能通过类的成员函数重载
  • 重载函数能且仅能使用一个参数
  • 可以定义不同参数的多个重载函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <iostream>
#include <string>

using namespace std;

class Test
{
int a[5];
public:
int& operator [] (int i)
{
return a[i];
}

int& operator [] (const string& s)
{
if( s == "1st" )
{
return a[0];
}
else if( s == "2nd" )
{
return a[1];
}
else if( s == "3rd" )
{
return a[2];
}
else if( s == "4th" )
{
return a[3];
}
else if( s == "5th" )
{
return a[4];
}

return a[0];
}

int length()
{
return 5;
}
};

int main()
{
Test t;

for(int i=0; i<t.length(); i++)
{
t[i] = i;
}

for(int i=0; i<t.length(); i++)
{
cout << t[i] << endl;
}

cout << t["5th"] << endl;
cout << t["4th"] << endl;
cout << t["3rd"] << endl;
cout << t["2nd"] << endl;
cout << t["1st"] << endl;

return 0;
}

类的对象字符串的[ ]重载,这样兼容了c语言下标访问,同样的数组类也应该重载[ ]操作符。

如上述代码t[i]等价于t.operator[ ]( i ),调用了int& operator [] (int i)
t[“1st”]等价于t.operator[ ]( 1st ),调用了int& operator [] (const string& s)
注意返回的类型是引用int&,这样返回值可以作为左值使用!

若函数的返回值为引用(&),则编译器就不为返回值创建临时变量了。直接返回那个变量的引用。所以千万不要返回临时变量的引用

函数对象分析

客户需求 编写一个函数:

  • 函数可以获得斐波那契数列每项的值
  • 每调用一次返回一个值
  • 函数可根据需要重复使用
1
2
3
for(int i = 0; i<10;i++){
fib();
}

fib应该是一个带状态的函数,因此考虑静态局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <string>

using namespace std;

int fib()
{
static int a0 = 0;
static int a1 = 1;

int ret = a1;

a1 = a0 + a1;
a0 = ret;

return ret;
}


int main()
{
for(int i=0; i<10; i++)
{
cout << fib() << endl;
}

cout << endl;

for(int i=0; i<5; i++)
{
cout << fib() << endl;
}

return 0;
}

函数一旦开始调用就无法重来

  • 静态局部变量处于函数内部 , 外界无法改变
  • 函数为全局函数 , 是唯一的 , 无法多次独立使用
  • 无法指定某个具体的数列项作为初始值

函数对象

  • 使用具体的类对象取代函数
  • 该类的对象具备函数调用的行为
  • 构造函数指定具体数列项的起始位置
  • 多个对象相互独立的求解数列项

函数调用操作符 ()

  • 只能通过类的成员函数重载
  • 可以定义不同参数的多个重载函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <iostream>
#include <string>

using namespace std;

class Fib
{
int a0;
int a1;
public:
Fib()
{
a0 = 0;
a1 = 1;
}

Fib(int n)
{
a0 = 0;
a1 = 1;

for(int i=2; i<=n; i++)
{
int t = a1;

a1 = a0 + a1;
a0 = t;
}
}

int operator () ()
{
int ret = a1;

a1 = a0 + a1;
a0 = ret;

return ret;
}
};

int main()
{
Fib fib;

for(int i=0; i<10; i++)
{
cout << fib() << endl;
}

cout << endl;

for(int i=0; i<5; i++)
{
cout << fib() << endl;
}

cout << endl;

Fib fib2(10);

for(int i=0; i<5; i++)
{
cout << fib2() << endl;
}

return 0;
}

关于赋值的疑问

  • 什么时候需要重载赋值操作符 ?
  • 编译器是否提供默认的赋值操作?

答案:

  • 编译器为每个类默认重载了赋值操作符
  • 默认的赋值操作符仅完成浅拷贝
  • 当需要进行深拷贝时必须重载赋值操作符
  • 赋值操作符与拷贝构造函数有相同的存在意义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <string>

using namespace std;

class Test
{
int* m_pointer;
public:
Test()
{
m_pointer = NULL;
}
Test(int i)
{
m_pointer = new int(i);
}
Test(const Test& obj)
{
m_pointer = new int(*obj.m_pointer);
}
// Test& operator = (const Test& obj)
// {
// if( this != &obj )
// {
// delete m_pointer;
// m_pointer = new int(*obj.m_pointer);
// }

// return *this;
// }
void print()
{
cout << "m_pointer = " << hex << m_pointer << endl;
}
~Test()
{
delete m_pointer;
}
};

int main()
{
Test t1 = 1;
Test t2;

t2 = t1;

t1.print();
t2.print();

return 0;
}

默认的赋值操作符:

1
2
3
4
5
6
fengyun@ubuntu:~/share$ g++ test.cpp -o test
fengyun@ubuntu:~/share$ ./test
m_pointer = 0x564f2d0a5eb0
m_pointer = 0x564f2d0a5eb0
free(): double free detected in tcache 2
已放弃 (核心已转储)

image-20220225153941491

重载赋值操作符要求

  1. 返回值必须是引用,遵循左值赋值的特性,
  2. 参数必须是const &类型
  3. 一定要判断地址是否相等,地址不同才要进行进行深拷贝。
  4. return *this;必须要返回自己
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Test(const Test& obj)
{
m_pointer = new int(*obj.m_pointer);
}
Test& operator = (const Test& obj)
{
if( this != &obj )
{
delete m_pointer;
m_pointer = new int(*obj.m_pointer);
}

return *this;
}

一定要判断地址是否相等,地址不同才要进行进行深拷贝。这样自赋值t1 = t1;才不会创建一个新对象。

空类

image-20220225154324073

关于string疑问

下面代码输出什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <string>

using namespace std;

int main()
{
string s = "12345";
const char* p = s.c_str();

cout << p << endl;

s.append("abced"); //插入新字符

cout << p << endl;

return 0;
}

image-20220225153852163

这么做是非常危险的 行为不定 因为append之后 内部存储字符串的内存可能发生改变。

但是究竟有没有发生改变 没人说得清楚 用法上是:不要把原生指针和string成员函数交替使用

C语言与C++混合编程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <string>

using namespace std;

int main()
{
const char* p = "12345";
string s = "";

s.reserve(10);

for(int i=0; i<5; i++)
{
s[i] = p[i];
}

cout << s << endl;

return 0;
}

观察输出结果,s输出竟然为空值?!

1
2
3
fengyun@ubuntu:~/share$ g++ test.cpp -o test
fengyun@ubuntu:~/share$ ./test

这是因为代表字符串长度的m_length值仍然为0。

image-20220225155900725

那应该怎么修改呢?

既然使用了C++string那么就贯彻到底,就抛弃char*

重载逻辑操作符

逻辑运算符的原生语义

  • 操作数只有两种值( true 和 false )
  • 逻辑表达式不用完全计算就能确定最终值
  • 最终结果只能是 true 或者 false

比如a&&b,如果a是false,b将不会继续判断

重载逻辑操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>
#include <string>

using namespace std;

class Test
{
int mValue;
public:
Test(int v)
{
mValue = v;
}
int value() const
{
return mValue;
}
};

bool operator && (const Test& l, const Test& r)
{
return l.value() && r.value();
}

bool operator || (const Test& l, const Test& r)
{
return l.value() || r.value();
}

Test func(Test i)
{
cout << "Test func(Test i) : i.value() = " << i.value() << endl;

return i;
}

int main()
{
Test t0(0);
Test t1(1);

if( func(t0) && func(t1) )
{
cout << "Result is true!" << endl;
}
else
{
cout << "Result is false!" << endl;
}

cout << endl;

if( func(1) || func(0) )
{
cout << "Result is true!" << endl;
}
else
{
cout << "Result is false!" << endl;
}

return 0;
}

注意我们的程序42行判断func(t0) && func(t1)语句,按照C语言规则,先判断func(t0)是true还是false,再判断func(t1),如果func(t0)是false,那么不需要判断func(t1)了,然而运行程序,事实是不仅两个都判断了,而且判断顺序是反的。

同理53行func(1) || func(0)应该先判断func(t0)是true还是false,再判断func(t1),如果func(t0)是true,那么不需要判断func(t1)了。然而运行程序结果显示,不仅两个都判断了,而且判断顺序是反的。

1
2
3
4
5
6
7
8
9
fengyun@ubuntu:~/share$ g++ test.cpp -o test
fengyun@ubuntu:~/share$ ./test
Test func(Test i) : i.value() = 1
Test func(Test i) : i.value() = 0
Result is false!

Test func(Test i) : i.value() = 0
Test func(Test i) : i.value() = 1
Result is true!

问题的本质分析

func(t0) && func(t1)换成函数调用的方式等价于operator && ( func(t0), func(t1) )

  1. C++通过函数调用扩展操作符的功能
  2. 进入函数体前必须完成所有参数的计算
  3. 函数参数的计算次序不定
  4. 短路法则完全失效

逻辑操作符重载后无法完全实现原生的语义。

一些有用的建议

  • 实际工程开发中避免重载逻辑操作符
  • 通过重载比较操作符代替逻辑操作符重载
  • 直接使用成员函数代替逻辑操作符重载
  • 如果实在一定要用逻辑操作符,那么使用全局函数对逻辑操作符进行重载

重载逗号操作符

逗号表达式

逗号操符( , ) 可以构成逗号表达式

  • 逗号表达式用于将多个子表达式连接为一个表达式
  • 逗号表达式的值为最后一个子表达式的值
  • 逗号表达式中的前 N-1 个子表达式可以没有返回值
  • 逗号表达式按照从左向右的序计算每个子表达式的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <string>

using namespace std;

void func(int i)
{
cout << "func() : i = " << i << endl;
}

int main()
{
int a[3][3] = {
(0, 1, 2),
(3, 4, 5),
(6, 7, 8)
};

int i = 0;
int j = 0;

while( i < 5 )
func(i),

i++;

for(i=0; i<3; i++)
{
for(j=0; j<3; j++)
{
cout << a[i][j] << " ";
}
cout << endl;
}

(i, j) = 6;

cout << "i = " << i << endl;
cout << "j = " << j << endl;

return 0;
}

注意观察结果,while循环并没有变成死循环,因为func(i),i++;算一条语句,
而for循环打印结果只有部分初始化,是因为逗号表达式只初始化了前三个,其他的默认值为0。数组正确初始化应该用大括号{}初始化
37行(i, j) = 6;等价于j = 6因此j的值未发生改变

1
2
3
4
5
6
7
8
9
10
11
12
fengyun@ubuntu:~/share$ g++ test.cpp -o test
fengyun@ubuntu:~/share$ ./test
func() : i = 0
func() : i = 1
func() : i = 2
func() : i = 3
func() : i = 4
2 5 8
0 0 0
0 0 0
i = 3
j = 6

数组默认初始值:

  1. 全局数组,未初始化时,默认值都是 0;
  2. 局部数组,未初始化时,默认值为随机的不确定的值;
  3. 局部数组,初始化一部分时,未初始化的部分默认值为 0;

重载逗号操作符

  • 在 C+ + 中重载逗号操作符是合法的
  • 使用全局函数对逗号操作符进行重载
  • 重载函数的参数必须有一个是类类型
  • 重载函数的返回值类型必须是引用
1
2
3
4
Test& operator , (const Test& a, const Test& b)
{
return const_cast<Test&>(b);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <string>

using namespace std;

class Test
{
int mValue;
public:
Test(int i)
{
mValue = i;
}
int value()
{
return mValue;
}
};

Test& operator , (const Test& a, const Test& b)
{
return const_cast<Test&>(b);
}

Test func(Test& i)
{
cout << "func() : i = " << i.value() << endl;

return i;
}

int main()
{
Test t0(0);
Test t1(1);
Test tt = (func(t0), func(t1)); // Test tt = func(t1);

cout << tt.value() << endl; // 1

return 0;
}
1
2
3
4
5
fengyun@ubuntu:~/share$ g++ test.cpp -o test
fengyun@ubuntu:~/share$ ./test
func() : i = 1
func() : i = 0
1

观察发现虽然最终结果是正确的但是函数执行顺序竟然是反的,由从左向右变成了从右向左!

问题的本质分析

  1. C++通过函数调用扩展操作符的功能
  2. 进入函数体前必须完成所有参数的计算
  3. 函数参数的计算次序是不定的
  4. 重载后无法严格从左向右计算表达式

Test tt = (func(t0), func(t1)); 等价于Test tt = ( operator , ( func(t0), func(t1) ) )

而假设我们不重载,表达式,直接运行

1
2
3
4
5
fengyun@ubuntu:~/share$ g++ test.cpp -o test
fengyun@ubuntu:~/share$ ./test
func() : i = 0
func() : i = 1
1

观察结果,函数顺序正确。我们重载,表达式并没有什么意义而且还出错了,不重载依然能正确执行

结论:

工程中不要重载逗号操作符 !

前置操作符和后置操作符

编译器的优化

下面的两行语句是否有区别?为什么?

1
2
i++; // i 的值作为返回值 , i 自增 1
++i; // i 自增1, i 的值作为返回值

而在实际工程中编译器会对单独执行的两条语句做优化,i++与++i将没有区别

用eclipse反汇编

image-20220226095501656

我们用vs2010反汇编

image-20220226094716900

dword ptr [i]意思是: i标识符所对应四个字节的内存。

  1. mov eax,dword ptr [i]意思是:i标识符所对应四个字节的内存传送到eax寄存器中
  2. add eax,1然后将寄存器eax加一
  3. mov dword ptr [i],eax再将寄存器eax的值传送回i标识符所对应四个字节的内存

观察汇编代码,除了寄存器使用不同,语句执行的含义是一样的

这是由于单独的i++; ++i;并没有使用返回值,那么编译器会将返回值抛弃掉,既然返回值抛弃掉了,那么它们本质上就是一样的了。

结论

  • 现代编译器产品会对代码进行优化
  • 优化使得最终的二进制程序更加高效
  • 优化后的二进制程序丟失了 C/C+ + 的原生语义
  • 不可能从编译后的二进制程序还原 C/C++程序

++操作符重载

+ + 操作符可以被重载

  • 全局函数和成员函数均可进行重载
  • 重载前置 ++ 操作符不需要额外的参数
  • 重载后置 ++ 操作符需要一个int 类型的占位参数(区分前置和后置)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Test{
private:
int mValue;
public:
Test(int i){
mValue = i;
}

int value(){
return mValue;
}

Test& operator ++(){
++mValue;
return *this;
}

Test operator ++(int){ //注意返回值不是引用
Test ret(mValue) //要暂时将当前对象保存下来
++mValue; //自增加一
return ret; //将未自增的对象返回下来
}

};

前置++没有生成额外对象,节约了栈空间,无需调用构造函数和析构函数

  • 对于基础类型的变量
    前置 ++ 的效率与后置 ++ 的效率基本相同
    根据项目组编码规范进行选择
  • 对于类类型的对象
    前置 ++ 的效率高于后置 + +
    尽量使用前置 ++ 操作符提高程序效率