重载 << 操作符 操作符 < < 的原生意义是按位左移 , 例 : 1« 2 ; 其意义是将整数 1 按位左移 2 位 , 即 : 0000 0001 ==》 0000 0100 重载左移操作符 , 将变量或常量左移到一个对象中 !
我们已经实现了将1和'\n'
字符传送到命令行中。我们可以继续优化,<<能够连续地接收输入,并且将换行符修改为常量endl
接着继续实现重载函数,将字符串,浮点数等等都能够接收输入。
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++的对象!!
C++标准库
C+ + 标准库并不是 C+ + 语言的一部分
C+ + 标准库是由类库和函数库 组成的集合
C+ + 标准库中定义的类和对象 都位于 s t d 命名空间中
C+ + 标准库的头文件都不带 .h 后缀
C+ + 标准库涵盖了 C 库的功能
C++扩展语法对于不同编译器是不一样的,由C++编译器生产产生实现,而C++标准语法模块是一致的。
C语言兼容库 可以兼容之前C语言实现的代码,C++编译器也能实现C语言实现的代码。如:<stdio.h> <string.h> <stdlib.h> <math.h>
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+ + 中的内置操作符 (和’+’,’-‘地位相同) - 数组访问符的原生意义是数组访问 和指针运算
数组访问操作符( [ ] )
只能通过类的成员函数重载
重载函数能且仅能使用一个参数
可以定义不同参数 的多个重载函数
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); } 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 已放弃 (核心已转储)
重载赋值操作符要求
返回值必须是引用,遵循左值赋值的特性,
参数必须是const &类型
一定要判断地址是否相等,地址不同才要进行进行深拷贝。
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;
才不会创建一个新对象。
空类
关于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 ; }
这么做是非常危险的 行为不定 因为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。
那应该怎么修改呢?
既然使用了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) )
C++通过函数调用扩展操作符 的功能
进入函数体前必须完成所有参数的计算
函数参数的计算次序 是不定 的
短路法则完全失效
逻辑操作符重载后无法完全实现原生的语义。
一些有用的建议
实际工程开发中避免重载逻辑操作符
通过重载比较操作符 代替逻辑操作符重载
直接使用成员函数 代替逻辑操作符重载
如果实在一定要用逻辑操作符,那么使用全局函数 对逻辑操作符进行重载
重载逗号操作符 逗号表达式 逗号操符( , ) 可以构成逗号表达式
逗号表达式用于将多个子表达式连接为一个表达式
逗号表达式的值为最后一个子表达式的值
逗号表达式中的前 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
数组默认初始值:
全局数组,未初始化时,默认值都是 0;
局部数组,未初始化时,默认值为随机的不确定的值;
局部数组,初始化一部分时,未初始化的部分默认值为 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)); cout << tt.value () << endl; 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
观察发现虽然最终结果是正确的但是函数执行顺序竟然是反的,由从左向右变成了从右向左!
问题的本质分析
C++通过函数调用扩展操作符的功能
进入函数体前必须完成所有参数的计算
函数参数的计算次序是不定的
重载后无法严格从左向右计算表达式
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
观察结果,函数顺序正确。我们重载,
表达式并没有什么意义而且还出错了,不重载依然能正确执行
结论:
工程中不要重载逗号操作符 !
前置操作符和后置操作符 编译器的优化 下面的两行语句是否有区别?为什么?
而在实际工程中编译器会对单独执行的两条语句做优化,i++与++i将没有区别
用eclipse反汇编
我们用vs2010反汇编
dword ptr [i]
意思是: i标识符所对应四个字节的内存。
mov eax,dword ptr [i]
意思是:i标识符所对应四个字节的内存传送到eax寄存器中
add eax,1
然后将寄存器eax加一
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; } };
前置++没有生成额外对象,节约了栈空间,无需调用构造函数和析构函数
对于基础类型的变量 前置 ++ 的效率与后置 ++ 的效率基本相同 根据项目组编码规范进行选择
对于类类型的对象 前置 ++ 的效率高于后置 + + 尽量使用前置 ++ 操作符提高程序效率