您的位置:首页 > 编程语言 > C语言/C++

Effective C++ 总结4 类和函数:设计与声明 (条款18 - 28)

2010-12-07 11:22 337 查看
18. 争取使类的接口完整并且最小

1) 一个完整的接口是指那种允许用户做他们想做的任何合理的事情的接口

2) 一个最小的接口,是指那种函数尽可能少、每两个函数都没有重叠功能的接口

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

1)

class rational {
public:

...

const rational operator*(const rational& rhs) const;
};

rational onehalf(1, 2);

result = onehalf * 2; // 运行良好

result = 2 * onehalf; // 出错! 试图找全局的operation*()

result = operator*(2, onehalf); // 错误! 但没有这样一个参数为int和rational的非成员operator*函数,所以搜索失败。

对象onehalf是一个包含operator*函数的类的实例,所以编译器调用了那个函数。而整数2没有相应的类,所以没有operator*成员函数。编译器还会去搜索一个可以象下面这样调用的非成员的operator*函数(即,在某个可见的名字空间里的operator*函数或全局的operator*函数)

2) 隐式类型转换

result = onehalf * 2; // 运行良好

=

const rational temp(2); // 从2产生一个临时 rational对象

result = onehalf * temp; // 同onehalf.operator*(temp);

3) 避免隐式类型转换

explicit构造函数

如果需要的话,编译器会对每个函数的每个参数执行这种隐式类型转换。但它只对函数参数表中列出的参数进行转换,决不会对成员函数所在的对象(即,成员函数中的*this指针所对应的对象)进行转换。

4) 本条款得出的结论如下。假设f是想正确声明的函数,c是和它相关的类:

·虚函数必须是成员函数。如果f必须是虚函数,就让它成为c的成员函数。

·operator>>和operator<<决不能是成员函数。如果f是operator>>或operator<<,让f成为非成员函数。如果f还需要访问c的非公有成员,让f成为c的友元函数。

·只有非成员函数对最左边的参数进行类型转换。如果f需要对最左边的参数进行类型转换,让f成为非成员函数。如果f还需要访问c的非公有成员,让f成为c的友元函数。

·其它情况下都声明为成员函数。如果以上情况都不是,让f成为c的成员函数。

20. 避免public接口出现数据成员

21. 尽可能使用const

1)

char *p = "hello"; // 非const指针,
// 非const数据

const char *p = "hello"; // 非const指针,
// const数据

char * const p = "hello"; // const指针,
// 非const数据

const char * const p = "hello"; // const指针,
// const数据

2) 作为返回值

const rational operator*(const rational& lhs,
const rational& rhs);

(a * b) = c; // 对a*b的结果赋值 error

3) 作为函数参数

关于const参数没什么特别之处要强调——它们的运作和局部const对象一样

4) const成员函数

·仅在const方面有不同的成员函数可以重载

class string {
public:

...

// 用于非const对象的operator[]
char& operator[](int position)
{ return data[position]; }

// 用于const对象的operator[]
const char& operator[](int position) const
{ return data[position]; }

private:
char *data;
};

string s1 = "hello";
cout << s1[0];                  // 调用非const
// string::operator[]
const string s2 = "world";
cout << s2[0];                  // 调用const
// string::operator[]

通过重载operator[]并给不同版本不同的返回值,就可以对const和非const string进行不同的处理:

string s = "hello";              // 非const string对象

cout << s[0];                    // 正确——读一个
// 非const string

s[0] = 'x';                      // 正确——写一个
// 非const string

const string cs = "world";       // const string 对象

cout << cs[0];                   // 正确——读一个
// const string

cs[0] = 'x';                     // 错误!——写一个
// const string


·const函数不能修改数据

·const函数不能调用非const 函数

·const成员只能调用const函数

5) mutable

mutable成员可以被const函数修改

22. 尽量用“传引用”而不用“传值”

1) 传值会导致copy constructor函数被调用 倒置效率降低

2) 传值可能会导致对象切割(slicing)

当一个派生类的对象作为基类对象被传递时,它(派生类对象)的作为派生类所具有的行为特性会被“切割”掉,从而变成了一个简单的基类对象。

3) 引用几乎都是通过指针来实现的,所以通过引用传递对象实际上是传递指针。因此,如果是一个很小的对象——例如int——传值实际上会比传引用更高效。

23. 必须返回一个对象时不要试图返回一个引用

1) 不能尝试返回一个栈对象的引用

2) 返回堆对象的结果可能会导致内存泄漏,调用者有时没有机会delete

// 写此函数的第一个错误方法
inline const rational& operator*(const rational& lhs,
const rational& rhs)
{
rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}

// 写此函数的第二个错误方法
inline const rational& operator*(const rational& lhs,
const rational& rhs)
{
rational *result =
new rational(lhs.n * rhs.n, lhs.d * rhs.d);
return *result;
}

// 对于2, 连乘导致调用者没机会释放对内存

rational w, x, y, z;

w = x * y * z;

// 写此函数的第三个错误方法
inline const rational& operator*(const rational& lhs,
const rational& rhs)
{
static rational result; // 将要作为引用返回的
// 静态对象

lhs和rhs 相乘,结果放进result;

return result;
}

if ((a * b) == (c * d)) {

处理相等的情况;

} else {

处理不相等的情况;

}

看出来了吗?((a*b) == (c*d)) 会永远为true,不管a,b,c和d是什么值!

24. 在函数重载和设定参数缺省值间慎重选择

1) 一般来说,如果可以选择一个合适的缺省值并且只是用到一种算法,就使用缺省参数(参见条款38)。否则,就使用函数重载。

2) 另一种必须使用重载函数的情况是:想完成一项特殊的任务,但算法取决于给定的输入值。这种情况对于构造函数很常见:“缺省”构造函数是凭空(没有输入)构造一个对象,而拷贝构造函数是根据一个已存在的对象构造一个对象:

25. 避免对指针和数字类型重载

26. 当心潜在的二义性

1)

class B;                    // 对类B提前声明
//
class A {
public:
A(const B&);              // 可以从B构造而来的类A
};

class B {
public:
operator A() const;       // 可以从A转换而来的类B
};


void f(const A&);

B b;

f(b); // 错误!——二义

一看到对f的调用,编译器就知道它必须产生一个类型A的对象,即使它手上拿着的是一个类型B的对象。有两种都很好的方法来实现(见条款M5)。一种方法是调用类A的构造函数,它以b为参数构造一个新的A的对象。另一种方法是调用类B里自定义的转换运算符,它将b转换成一个A的对象。因为这两个途径都一样可行,编译器拒绝从他们中选择一个。

2)

void f(int);
void f(char);

double d = 6.02;

f(d); // 错误!——二义

d是该转换成int还是char呢?两种转换都可行,所以编译器干脆不去做结论。幸运的是,可以通过显式类型转换来解决这个问题:

f(static_cast<int>(d)); // 正确, 调用f(int)
f(static_cast<char>(d)); // 正确, 调用f(char)

3) 多继承

class Base1 {
public:
int doIt();
};
class Base2 {
public:
void doIt();
};

class Derived: public Base1,     // Derived没有声明
public Base2 {    // 一个叫做doIt的函数
...

};

Derived d;

d.doIt();                   // 错误!——二义


d.Base1::doIt(); // 正确, 调用Base1::doIt

d.Base2::doIt(); // 正确, 调用Base2::doIt

27. 如果不想使用隐式生成的函数就要显式地禁止它

1) 将不允许调用的函数申明成private 且不写实现

写成private: 不允许外面调用

不写实现:不允许内部和友元函数调用

正确的写法

template<class T>
class Array {
private:
// 不要定义这个函数!
Array& operator=(const Array& rhs);

...

};


29. 划分全局名字空间
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: