第10章 引用和拷贝构造函数
一、使用引用时有一定的规则:
1) 当引用被创建时,它必须被初始化。(指针则可以在任何时候被初始化。)2) 一旦一个引用被初始化为指向一个对象,它就不能被改变为对另一个对象的引用。(指针则可以在任何时候指向另一个对象。)3) 不可能有N U L L 引用。必须确保引用是和一块合法的存储单元关连。
二、参数传递准则
当给函数传递参数时,人们习惯上应该是通过常量引用来传递。虽然最初看起来似乎仅出于对效率的考虑(通常在设计和装配程序时并不考虑效率),但像本章以后部分介绍的,这里将会存在很多的危险。(常量引用的使用不仅仅提高效率,而且避免了拷贝构造函数的带来的麻烦)拷贝构造函数需要通过值来传递对象,但这并不总是可行的。这种简单习惯可以大大提高效率:传值方式需要调用构造函数和析构函数,然而如果不想改变参数,则可通过常量引用传递,它仅需要将地址压栈。事实上,只有一种情况不适合用传递地址方式,这就是当传值是唯一安全的途径,否则将会破坏对象时(而不是修改外部对象,这不是调用者通常期望的) (疑问,什么时候出现这种情况,必须要传值呢?)
三、函数调用栈的框架
当编译器为函数调用产生代码时,它首先把所有的参数压栈,然后调用函数。在函数内部,产生代码,向下移动栈指针为函数局部变量提供存储单元。(在这里“下”是相对的,在压栈时,机器栈指针可能增加也可能减小。)在汇编语言CALL 中,CPU 把程序代码中的函数调用指令的地址压栈,所以汇编语言RETURN 可以使用这个地址返回到调用点。当然,这个地址是非常神圣的,因为没有它程序将迷失方向。这儿提供一个在C ALL 后栈框架的样子,此时在函数中已为局部变量分配了存储单元。 图1 函数参数
返回地址
局部变量
对于函数返回值的存放位置的思考:1)栈:如果调用函数试图从普通函数中返回堆栈中的值将会发生什么。因为不能触及堆栈返回地址以上任何部分,所以函数必须在返回地址下将值压栈。但当汇编语言RETURN执行时,堆栈指针必须指向返回地址(或正好位于它下面,这取决于机器。),所以恰好在RETURN语句之前,函数必须将堆栈指针向上移动,以便清除所有局部变量。如果我们试图从堆栈中的返回地址下返回数值,因为中断可能此时发生,此时是我们最易被攻击的时候。这个时候ISR将向下移动堆栈指针,保存返回地址和局部变量,这样就会覆盖掉我们的返回值。(你的ISR已经移动了,但是值还是放在下面的内存中,已经归还给程序进程了。因为这是突然来了另外一个函数调用时,就覆盖掉你刚才的内容了)。2)全局数据区:我们的下一个想法可能是在全局数据区域返回数值,但这不可行。重入意味着任何函数可以中断任何其他的函数,包括与之相同的函数。因此,如果把返回值放在全局区域,我们可能又返回到相同的函数中,这将重写返回值。对于递归也是同样道理。3)寄存器:寄存器,问题是当寄存器没有足够大用于存放返回值时该怎么做。答案是把返回值的地址像一个函数参数一样压栈,让函数直接把返回值信息拷贝到目的地。这样做不仅解决了问题,而且效率更高。程序在调用的时候会将你的返回值的地址压栈,然后呢在被调用的函数中就把值直接放到某个内存区,而把内存地址记录下来。在返回的时候,再将该内存内容拷贝给你的返回值赋给的变量。
第11章 运算符重载
一、语法
函数参数表中参数的个数取决于两个因素:
1) 运算符是一元的(一个参数)还是二元的(两个参数)。
2) 运算符被定义为全局函数(对于一元是一个参数,对于二元是两个参数)还是成员函数
(对于一元没有参数,对于二元是一个参数—对象变为左侧参数)。
二、自增和自减
重载的++和--号运算符出现了两难选择的局面,这是因为希望根据它们出现在它们作用的对象前面(前缀)还是后面(后缀)来调用不同的函数。解决是很简单的,但一些人在开始时却发现它们容易令人混淆。例如当编译器看到++a(先自增)时,它就调用operator++(a) ;但当编译器看到a++时,它就调用operator++(a,int)。即编译器通过调用不同的函数区别这两种形式。
四、参数和返回值
1) 对于任何函数参数,如果仅需要从参数中读而不改变它,缺省地应当按const引用来传递它。普通算术运算符(像+和-号等)和布尔运算符不会改变参数,所以以const引用传递是使用的主要方式。当函数是一个类成员的时候,就转换为const成员函数。只是对于会改变左侧参数的赋值运算符(operator-assignment,像+=)和运算符'=',左侧参数才不是常量(constant),但因为参数将被改变,所以参数仍然按地址传递。
2) 应该选择的返回值取决于运算符所期望的类型。(可以对参数和返回值做任何想做的事)如果运算符的效果是产生一个新值,将需要产生一个作为返回值的新对象。例如,integer::operator+必须生成一个操作数之和的integer 对象。这个对象作为一个const通过传值方式返回,所以作为一个左值结果不会被改变。
3) 所有赋值运算符改变左值。为了使得赋值结果用于链式表达式(像A=B=C),应该能够返回一个刚刚改变了的左值的引用。但这个引用应该是const还是nonconst呢?虽然我们是从左向右读表达式A=B=C ,但编译器是从右向左分析这个表达式,所以并非一定要返回一个nonconst值来支持链式赋值。然而人们有时希望能够对刚刚赋值的对象进行运算,例如(A=B).foo(),这是B 赋值给A后调用foo( )。因此所有赋值运算符的返回值对于左值应该是nonconst 引用。(为了便于级联,所以虽然A已经在函数中改变了,但是还是需要返回一个引用;这里应该返回nonconst,因为foo作为一个member function,并非一定是const函数,所以如果A=B返回的值是const,那么将不能调用foo函数)
4) 对于逻辑运算符,人们希望至少得到一个int返回值,最好是bool返回值。(在大多数编译器支持C++内置bool类型之前开发的库函数使用int或typedef 等价物)
5) 因为有前缀和后缀版本,所以自增和自减运算符出现了两难局面。两个版本都改变对象,所以不能把这个对象看作一个const。因此,前缀版本返回这个对象被改变后的值。这样,用前缀版本我们只需返回*this作为一个引用。因为后缀版本返回改变之前的值,所以被迫创建一个代表这个值的单个对象并返回它。因此,如果想保持我们的本意,对于后缀必须通过传值方式返回。(注意,我们经常会发现自增和自减运算返回一个int值或bool值,例如用来指示是否有一个循环子(iterator)在表的结尾)。现在问题是:这些应该按const被返回还是按nonconst被返回?如果允许对象被改变,一些人写了表达式(++A).foo(),则foo()作用在A上。但对于表达式(A++).foo(),foo()作用在通过后缀运算符++号返回的临时对象上。临时对象自动定为const,所以被编译器标记。但为了一致性,使两者都是const更有意义,就像这儿所做的。因为想给自增和自减运算符赋予各种意思,所以它们需要就事论事考虑。也就是说都返回const数据
1. 按const通过传值方式返回
按const通过传值方式返回,开始看起来有些微妙,所以值得多加解释。我们来考虑二元运算符+号。假设在一个表达式像f(A+B)中使用它,A+B的结果变为一个临时对象,这个对象用于f()调用。因为它是临时的,自动被定为const,所以无论使返回值为const还是不这样做都没有影响。
2. 返回效率
当为通过传值方式返回而创建一个新对象时,要注意使用的形式。例如用运算符+号:return integer (left.i + right.i) ;一开始看起来像是一个“对一个构造函数的调用”,但其实并非如此。这是临时对象语法,它是这样陈述的:“创建一个临时对象并返回它”。因为这个原因,我们可能认为如果创建一个命名的本地对象并返回它结果将会是一样的。其实不然。如果像下面这样表示,将发生三件事。首先,tmp对象被创建,与此同时它的构造函数被调用。然后,拷贝构造函数把tmp拷贝到返回值外部存储单元里。最后,当tmp在作用域的结尾时调用析构函数。integer tmp(left.i + right.i) ; return tmp ; 相反,“返回临时对象”的方法是完全不同的。看这样情况时,编译器明白对创建的对象没有其他需求,只是返回它,所以编译器直接地把这个对象创建在返回值外面的内存单元。因为不是真正创建一个局部对象,所以仅需要单个的普通构造函数调用(不需要拷贝构造函数),并且不会调用析构函数。因此,这种方法不需要什么花费,效率是非常高的。
五、灵巧(smart)指针
如果想为类包装一个指针以使得这个指针安全, 或是在一个普通的循环子(iterator)的用法中,则这样做特别有用。循环子是一个对象,这个对象可以作用于其他对象的包容器或集合上,每次选择它们中的一个,而不用提供对包容器实现的直接访问。(在类函数里经常发现包容器和循环子。)灵巧指针必须是成员函数。它有一个附加的非典型的内容:它必须返回一个对象(或对象的引用),这个对象也有一个灵巧指针或指针,可用于选择这个灵巧指针所指向的内容.
六、
基本规则 运算符 建议使用
所有的一元运算符 成员
= ( ) [] -> 必须是成员
+= -= /= *= ^= &= |= %= >>= <<= 成员
所有其他二元运算符 非成员
七、拷贝构造函数和运算符=
foo B ;
foo A = B ; // 定义了对象A 。一个新对象先前不存在,现在正被创建。因为我们现在知道了C++编译器关于对象初始化是如何保护的,所以知道在对象被定义的地方构造函数总是必须被调用。A是从现有的foo对象创建的,所以只有一个选择:拷贝构造函数。所以虽然这里只包括一个‘=’,但拷贝构造函数仍被调用。
A = B ; // 在‘=’左侧有一个以前初始化了的对象。很清楚,不用为一个已经存在的对象调用构造函数。在这种情况下,为A调用foo::operator=,把foo::operator=右侧的任何东西作为参数。当然这里的赋值符也可以重载