条款17 在operator=中检查给自己赋值的情况

1
2
3
class 
X { ... };
X a;
a = a; 
// a 赋值给自己

>赋值给自己make no sense, 但却是合法的;

重要的是, 赋值给自己的情况可以以隐蔽的形式出现: a = b; 如果b是a的另一个名字(初始化为a的引用), 那也是对自己赋值; 这是一个别名的例子: 同一个对象有两个以上的名字; 别名可以以任意形式的伪装出现, 在写函数时一定要考虑到;

Note 赋值运算符中要特别注意可能出现别名的情况;

1) 效率: 如果可以在赋值运算符函数体的开始检测到是给自己赋值, 可以立即返回, 节省大量工作;

e.g. 派生类的赋值运算符必须调用它的每个基类的赋值运算符, 所以在派生类中省略赋值运算符函数体的操作可以避免大量的函数调用;

2) 正确性: 一个赋值运算符必须先释放掉一个对象的资源(去掉旧值), 然后根据新值分配新的资源;

在自己给自己赋值的情况下, 释放旧资源是灾难性的, 因为在分配新资源的时候需要的正是旧的资源;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class 
String {
public
:
    
String(
const 
char 
*value); 
// 函数定义参见条款11
    
~String(); 
// 函数定义参见条款11
//...
    
String& operator=(
const 
String& rhs);
private
:
    
char 
*data;
};
// 忽略了给自己赋值的情况的赋值运算符
String& String::operator=(
const 
String& rhs)
{
    
delete 
[] data; 
// delete old memory
    
// 分配新内存,将rhs 的值拷贝给它
    
data = 
new 
char
[
strlen
(rhs.data) + 1];
    
strcpy
(data, rhs.data);
    
return 
*
this

// see Item 15
}

>下面这种情况下会出现不可预知的错误:

1
2
String a = 
"Hello"
;
a = a; 
// same as a.operator=(a)

*this和rhs名称不同, 却都是同一个对象的不同名字;     *this data ------------> "Hello\0" <-----rhs data

赋值运算符做的第一件事是delete [] data;                  *this data ------------> ??? <-----rhs data

当赋值运算符对rhs.data调用strcpy时结果将无法确定, 因为data被delete时rhs.data也被删除了; data, this->data和rhs.data其实都是同一个指针;


Solution 对可能发生的自己给自己赋值的情况先进行检查, 如果有这种情况就立即返回;

难题是必须定义两个对象怎么样才算是"相同的"; 这个问题学术上称为object identity;

两个解决问题的基本方法:

1) 如果两个对象有相同的值, 即相同(具有相同的身份);

e.g.  String a = "Hello"; String b = "World"; String c = "Hello"; a和c有相同的值, 被认为是相同的, b和他们都不同;

1
2
3
4
5
String& String::operator=(
const 
String& rhs)
{
    
if 
(
strcmp
(data, rhs.data) == 0) 
return 
*
this
;
    
...
}

>值相等通常由operator==来检测, 对于一个用值相等来检测对象身份的类C来说, 他的赋值运算符的一般形式是:

1
2
3
4
5
6
7
C& C::operator=(
const 
C& rhs)
{
// 检查对自己赋值的情况
    
if 
(*
this 
== rhs) 
// 假设operator==存在
        
return 
*
this
;
    
...
}

Note 这个函数比较的是对象(通过operator==), 不是指针;

用值相等来确定对象身份, 与两个对象是否占用相同的内存无关, 有关系的只是他们所表示的值;

2) 两个对象当且仅当他们具有相同的地址时才是相同的;
这个定义在C++程序中运用得更广泛, 可能是因为容易实现而且计算较快;

1
2
3
4
5
6
C& C::operator=(
const 
C& rhs)
{
// 检查对自己赋值的情况
    
if 
(
this 
== &rhs) 
return 
*
this
;
    
...
}

如果需要一个复杂的机制来确定两个对象是否相同, 需要靠程序员自己实现; 最普通的方法是实现一个返回某种对象标识符的成员函数:

1
2
3
4
5
class 
C {
public
:
    
ObjectID identity() 
const

// 参见条款36
//...
};

对于两个对象指针a和b, 当且仅当a->identity() == b->identity()的时候, 它们所指的对象是完全相同的; 必须自己来实现ObjectIDs的operator==;

别名和object identity的问题不仅仅局限在operator==里, 任何一个用到的函数都有可能遇到;

在用到引用和指针的场合, 任何两个兼容类型的对象名称都可能指向了同一个对象:

1
2
3
4
5
6
7
8
9
10
11
class 
Base {
    
void 
mf1(Base& rb); 
// rb 和*this 可能相同
//...
};
void 
f1(Base& rb1,Base& rb2); 
// rb1 和rb2 可能相同
//
class 
Derived: 
public 
Base {
    
void 
mf2(Base& rb); 
// rb 和*this 可能相同
//...
};
int 
f2(Derived& rd, Base& rb); 
// rd 和rb 可能相同

>例子中使用的是引用, 指针有相似情况;

别名可以以各种形式出现, 处理它会达到事半功倍的效果; 写任何一个函数, 只要有别名可能出现, 就必须在写代码时进行处理;

---内存管理 End---

类和函数: 设计与声明

在程序中声明一个新类将产生一种新的类型: 类的设计就是类型设计; 好的类型具有自然的语法, 直观的语义和高效的实现;

设计每个类都会遇到的问题:

- 对象如何被创建和摧毁? 这将影响构造函数和析构函数的设计, 以及自定义的operator new, operator new[] 以及 operator delete, operator delete[];

- 对象初始化和对象赋值有何不同? 这决定了构造函数和赋值运算符的行为以及区别;

- 通过值来传递新类型的对象? 拷贝函数对此负责;

- 新类型的合法值有什么限制? 这决定了成员函数(特别是构造函数和赋值运算符)内部的错误检查的种类; 还可能影响到函数抛出的Exception的种类以及函数的异常规范;

- 新类型符合继承关系么? 如果是从已有类继承, 新类的设计会受限于这些类, 还要考虑被继承的类是否是虚类; 如果新类允许被继承, 要考虑函数是否是virtual的;

- 允许哪种类型转换? 如果允许类型A的对象隐式转换为类型B的对象, 就要在A中写一个类型转换函数, 或者在B中写一个单参数调用的非explicit构造函数; 如果只允许显式转换, 就要写函数来执行转换, 但不会写成类型转换运算符或单参数的非explicit构造;

- 什么运算符和函数对新类型有意义? 这决定了在类中声明什么样的函数;

- 哪些运算符和函数要被明确禁止? 把他们声明为private; [无需实现]

- 谁有权访问新类型的成员? 决定哪些成员是public, protected, private的, 哪些类/函数是友元, 将一个类嵌套到另一个类中是否有意义;

- 通用性如何? 也许你正在定义一整套的类型, 这样需要定义一个新的类模板;

C++中定义一个高效的类不那么简单, 如果自定义的类型和固定类型使用起来没什么区别, 那这个类就完成的不错;

条款18 争取使类的接口完整并且最小 

用户接口是指这个类的程序员所能访问到的接口; 典型的接口里只有函数存在, 因为在用户接口里放数据成员会有很多缺点; [安全性]

哪些函数应该作为类的接口? 一方面, 类要简单易读, 意味着函数要少, 每个函数都完成各自独立的任务;  另一方面, 类要功能强大, 意味着不时地增加函数, 提供对各种功能的支持;

Note 类接口的目标是完整且最小;

完整的接口支持用户完成任何合理的任务; 一个最小的接口是指函数尽可能少, 函数间没有重叠功能的接口;

充斥大量函数的类的接口有很多缺点:

1) 接口中函数越多, 以后就越难理解; 拥有太多的函数的类, 对使用者来说会有学习困难; 函数太多, 容易有相似的出现, 用户使用时的选择将不再简单直接;

2) 难以维护; 含有大量函数的类难以维护和升级, 难以避免重复代码(重复bug), 难以保持接口的一致性, 难以建立文档;

3) 长的类会导致长的头文件; 每次编译时会浪费时间读取头文件, 使编译时间变长;

在接口里增加函数时, 要考虑到它带来的方便是否值得: 同时带来的复杂性, 可读性, 可维护性和编译时间;

e.g. 一个类模板, 实现了用户自定义下标上下限的数组功能, 提供上下限检查选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
template
<
class 
T>
class 
Array {
public
:
    
enum 
BoundsCheckingStatus {NO_CHECK_BOUNDS = 0, CHECK_BOUNDS = 1};
    
Array(
int 
lowBound, 
int 
highBound, BoundsCheckingStatus check = NO_CHECK_BOUNDS);
    
Array(
const 
Array& rhs);
    
~Array();
    
Array& operator=(
const 
Array& rhs);
private
:
    
int 
lBound, hBound; 
// 下限, 上限
    
vector<T> data; 
// 数组内容; 关于vector,请参见条款 49
    
BoundsCheckingStatus checkingBounds;
};

>析构不是virtual的, 表示这个类不作为基类使用;

Note C++中固定类型的数组是不允许赋值的;

>数组型的模板vector(STL)允许vector对象间赋值, 所以Array对象可以使用赋值运算符;

1
Array(
int 
size, BoundsCheckingStatus check = NO_CHECK_BOUNDS);

>支持固定大小的数组声明;

带上下限参数的构造也能完成同样的功能, 所以这样就不是最小接口; 为了迎合基本语言(C语言), 可能是值得的;

1
2
T& operator[](
int 
index);
// 返回可以读/写的元素
const 
T& operator[](
int 
index) 
const
;
// 返回只读元素

>对数组的索引; 提供了对const和非const Array对象的支持, 返回值也不相同;

1
2
3
Array<
int
> a(10, 20); 
// 下标上下限为:10 到20
for 
(
int 
i = a 的下标下限; i <= a 的下标上限; ++i)
    
cout << 
"a[" 
<< i << 
"] = " 
<< a[i] << 
'\n'
;

>要获得数组的下标上下限;

1
int 
lowBound() 
const

int 
highBound() 
const
;

>const成员函数, 不会对成员进行修改, 遵循"能用const就尽量用const"的原则;

1
2
for 
(
int 
i = a.lowBound(); i <= a.highBound(); ++i)
    
cout << 
"a[" 
<< i << 
"] = " 
<< a[i] << 
'\n'
;

还需要一个类型T的operator<<, T可以隐式转换成其他类型的operator<<;

size可以通过highBound - lowBound +1获得; 各种关系运算符 <, >, ==等可以通过operator[]实现;

Note operator<<, operator>>这样的函数和关系运算符, 经常用非成员的友元函数来实现; 友元函数在实际应用中是类接口的一部分, 影响类接口的完整性和最小性;

条款19 分清成员函数, 非成员函数和友元函数

成员函数和非成员函数最大的区别: 成员函数是可以虚拟的; 如果函数必须进行动态绑定, 就要采用虚拟函数;

1
2
3
4
5
6
7
8
class 
Rational {
public
:
    
Rational(
int 
numerator = 0, 
int 
denominator = 1);
    
int 
numerator() 
const
;
    
int 
denominator() 
const
;
private
:
...
};

>有理数, 需要增加+, -, *, /等算术操作支持;

有理数的乘法和Ratinal类相联系: const Rational operator*(const Rational& rhs) const;

1
2
3
Rational oneEighth(1, 8); Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth; 
// 运行良好
result = result * oneEighth; 
// 运行良好

>Rational对象间乘法操作运行良好, 接下去试验混合类型操作: e.g. Rational和int相乘;

1
2
result = oneHalf * 2; 
// 运行良好
result = 2 * oneHalf; 
// 出错!

乘法应该满足交换律;

以上的代码可以用等价函数形式重写:

1
2
result = oneHalf.operator*(2); 
// 运行良好
result = 2.operator*(oneHalf); 
// 出错!

>oneHalf是包含operator*函数的类的实例, 而整数2没有相应的函数;

编译器还会去搜索一个非成员的operator*函数 (在可见的名字空间里的operator*函数或全局的operator*函数): result = operator*(2, oneHalf); // 错误! 找不到函数;

对于运行良好的operator*, 编译器进行了隐式转换: 传递的值是int, 函数需要的是Rational, 但Rational的构造可以将int转换成Rational对象;

1
2
const 
Rational temp(2); 
// 从2 产生一个临时Rational 对象
result = oneHalf * temp; 
// 同oneHalf.operator*(temp);

Note 只有当所涉及的构造函数没有声明为explicit的情况下才能隐式转换;

如果Rational定义了explicit的构造:

1
explicit 
Rational(
int 
numerator = 0, 
int 
denominator = 1); 
// 此构造函数为explicit

那么, 下面的语句都无法通过编译:

1
2
result = oneHalf * 2; 
// 错误!
result = 2 * oneHalf; 
// 错误!

Note 编译器只对函数参数表中的参数进行隐式转换, 不会对成员函数所在的对象(*this指针对应的对象)进行转换;

要支持混合型的算术操作, 应该使operator*成为一个非成员函数, 允许编译器对所有的参数执行隐式类型转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
class 
Rational {
... 
// contains no operator*
};
// 在全局或某一名字空间声明, 参见条款M20 了解为什么要这么做
const 
Rational operator*(
const 
Rational& lhs, 
const 
Rational& rhs)
{
    
return 
Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
//...
Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2; 
// 工作良好
result = 2 * oneFourth; 
// 万岁, 它也工作了!

Note 当operator*可以完全通过类的公有public接口来实现, 就不需要成为友元; 尽量避免友元函数, 有时候他带来麻烦比帮助多;
某些情况下, 不是成员的函数从概念上说也算是类接口的一部分, 需要访问类的非公有成员的情况也不少;

对于String类, 如果想重载operator>>和operator<<来读写String对象, 就不能写成成员函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 一个不正确地将operator>>和operator<<作为成员函数的类
class 
String {
public
:
    
String(
const 
char 
*value);
//...
    
istream& operator>>(istream& input);
    
ostream& operator<<(ostream& output);
private
:
    
char 
*data;
};
//...
String s;
s >> cin; 
// 合法, 但有违常规
s << cout; 
// 同上

>这样容易把概念混淆, 注意这里的目标是自然的调用语法, 和前面的说到的隐式类型转换情况不同;

设计非成员操作符函数:

1
2
3
4
5
6
7
8
9
10
istream& operator>>(istream& input, String& string)
{
    
delete 
[] string.data;
//read from input into some memory, and make string.data point to it
    
return 
input;
}
ostream& operator<<(ostream& output, 
const 
String& string)
{
    
return 
output << string.data;
}

>这两个函数都要访问private的data成员, 这样就只能成为友元函数; e.g. cin >> s; cin << s;

结论:

1) 虚函数必须是成员函数;

2) operator>>和operator<<决不能是成员函数; 只有非成员函数可以对最左边的参数进行类型转换;

[C++要求赋值=, 下标[], 调用()和访问箭头->操作符必须被指定为类成员操作符; 对于::, :, *, ?不能重载]

Effective C++ 第二版 17)operator=检查自己 18)接口完整 19)成员和友元函数的更多相关文章

  1. Effective Java 第二版 Enum

    /** * Effective Java 第二版 * 第30条:用enum代替int常量 */ import java.util.HashMap;import java.util.Map; publi ...

  2. Effective C++ 第二版 8) 写operator new 和operator delete 9) 避免隐藏标准形式的new

    条款8 写operator new 和operator delete 时要遵循常规 重写operator new时, 函数提供的行为要和系统缺省的operator new一致: 1)正确的返回值; 2 ...

  3. Effective C++ 第二版 40)分层 41)继承和模板 42)私有继承

    条款40 通过分层来体现"有一个"或"用...来实现" 使某个类的对象成为另一个类的数据成员, 实现将一个类构筑在另一个类之上, 这个过程称为 分层Layeri ...

  4. Effective C++ 第二版 10) 写operator delete

    条款10 写了operator new就要同时写operator delete 写operator new和operator delete是为了提高效率; default的operator new和o ...

  5. 《Effective Java第二版》总结

    第1条:考虑用静态工厂方法代替构造器 通常我们会使用 构造方法 来实例化一个对象,例如: // 对象定义 public class Student{ // 姓名 private String name ...

  6. Effective C++ 第二版 5)new和delete形式 6) 析构函数里的delete

    内存管理 1)正确得到: 正确调用内存分配和释放程序; 2)有效使用: 写特定版本的内存分配和释放程序; C中用mallco分配的内存没有用free返回, 就会产生内存泄漏, C++中则是new和de ...

  7. Effective C++ 第二版 1)const和inline 2)iostream

    条款1 尽量用const和inline而不用#define >"尽量用编译器而不用预处理" Ex. #define ASPECT_R 1.653    编译器永远不会看到AS ...

  8. Effective C++ 第二版 31)局部对象引用和函数内new的指针 32)推迟变量定义

    条款31 千万不要返回局部对象的引用, 不要返回函数内部用new初始化的指针的引用 第一种情况: 返回局部对象的引用; 局部对象--仅仅是局部的, 在定义时创建, 在离开生命空间时被销毁; 所谓生命空 ...

  9. 《Effective Java 第二版》读书笔记

    想成为更优秀,更高效程序员,请阅读此书.总计78个条目,每个对应一个规则. 第二章 创建和销毁对象 一,考虑用静态工厂方法代替构造器 二, 遇到多个构造器参数时要考虑用builder模式 /** * ...

随机推荐

  1. mysqld -install命令时出现install/remove of the service denied错误的原因和解决办法

    原因:没有使用管理员权限 解决方法:使用管理员权限打开cmd,然后运行mysqld -install,服务安装成功

  2. pku,杨建武:文本挖掘技术

    http://webkdd.org/course/ http://www.icst.pku.edu.cn/lcwm/course/WebDataMining/ http://www.icst.pku. ...

  3. bzoj3940: [Usaco2015 Feb]Censoring

    AC自动机.为什么洛谷水题赛会出现这种题然而并不会那么题意就不说啦 .终于会写AC自动机判断是否是子串啦...用到kmp的就可以用AC自动机水过去啦 #include<cstdio> #i ...

  4. 几种通讯协议的比较RMI > Httpinvoker >= Hessian >> Burlap >> web service

    一.综述本文比较了RMI,Hessian,Burlap,Httpinvoker,web service等5种通讯协议的在不同的数据结构和不同数据量时的传输性能.RMI是java语言本身提供的远程通讯协 ...

  5. Java [leetcode 27]Remove Element

    题目描述: Given an array and a value, remove all instances of that value in place and return the new len ...

  6. Spring配置bean的详细知识

    在Spring中配置bean的一些细节.具体信息请参考下面的代码及注释 applicationContext.xml文件 <?xml version="1.0" encodi ...

  7. NOR型flash与NAND型flash的区别

    1) 闪存芯片读写的基本单位不同  应用程序对NOR芯片操作以“字”为基本单位.为了方便对大容量NOR闪存的管理,通常将NOR闪存分成大小为128KB或者64KB的逻辑块,有时候块内还分成扇区.读写时 ...

  8. xhtml知识及head部分名词解

    W3C: World Wide Web Con??? DTD: Document Type Defination DOCTYPE:Document Type meta:????? http-equiv ...

  9. position属性

    所有主流浏览器支持position属性: 任何版本的ie浏览器都不支持属性值“inherit”. position属性规定元素的定位类型,任何元素都可以定位,不过绝对定位或固定元素会生成一个块级框,不 ...

  10. 【转载】解析提高PHP执行效率的50个技巧

    1.用单引号代替双引号来包含字符串,这样做会更快一些.因为PHP会在双引号包围的字符串中搜寻变量, 单引号则不会,注意:只有echo能这么做,它是一种可以把多个字符串当作参数的”函数”(译注:PHP手 ...