[读书笔记] -《C++ API设计》第6章 C++用法

1、使用命名空间特性会产生冗长的符号名,尤其是那些包含在几层嵌套命名空间中的符号。好在C++提供了使用using关键字,是的命名空间里的符号更加容易使用:

using namespace std;
string str("Look, no std::");

而更好的方式(因为它限定了引入全局命名空间的符号范围)是:

using std::string;
string str("Look, no std::");

然而,任何时候都不应该在公用API头文件的全局作用域内使用using关键字!这样做会导致所引用命名空间的全部符号在全局命名空间可见。这便破坏了使用命名空间的初衷。如果你希望在头文件中引用另一个命名空间的符号,应当使用完整限定名,例如:std::string。

 

2、“三大件”(Big Three)规则:析构函数、复制构造函数和赋值操作符这3个成员函数始终应该一起出现。

如果类分配了资源,则应该遵循“三大件”规则,同时定义析构函数、复制构造函数和赋值操作符。

 

3、为那些只接受一个参数的构造函数添加explicit是一种好的做法,用于阻止构造对象时特定的构造函数被隐式调用。

 

4、讨论将操作符定义为自由函数还是方法的最佳做法。

C++标准要求将下列操作符声明为成员方法,以确保它们接受左值作为其第一个操作数:

        1> = 辅助;

        2> [] 下标;

        3> -> 类成员访问;

        4> ->* 指针成员选择;

        5> () 函数调用;

        6> (T) 类型转换,即C风格的转换;

        7> new/delete

其余的可重载操作符既可以定义为自由函数,也可以定义为类成员函数。站在良好API设计的角度,建议在定义操作符时尽量选择自由函数,而非类成员方法,原因如下。

        1> 操作符对称性。如果二元操作符被定义为类的方法,它就必须有一个能用作左操作数的对象。以*操作符为例,这意味着用户能够编写类似currency*2这种表达式,(前提是已经定义了接受int类型的隐式构造函数或特定的*操作符。)但不能编写2*currency这种表达式,因为2.operator*(currency)没有意义。这破坏了用户期望的操作符交换性,即x*y应该和y*x等价。把*操作符声明为自由函数能带来很多益处。这样,如果没有显示声明构造函数,自由函数可以为左操作数和右操作数进行隐式类型转换。

        2>降低耦合度。自由函数不能访问类的私有细节。由于它只能访问公有方法,因此降低了与类的耦合度。这是第2章所述的通用API设计理念:将不必访问私有成员或受保护的成员的类方法定义为自由函数,从而降低API的耦合度。

总结:除非操作符必须访问私有成员或受保护的成员,或它是=、[]、->、->*、()、(T)、new、delete其中之一,否则应该尽量将其声明为自由函数。

 

5、为类添加操作符

+=操作符会修改对象的内容,而我们知道所有的成员变量应该都是私有的,因此很可能需要将+=操作符定义为成员方法。另外,由于+操作符不会修改左操作数,从而也就不必访问私有成员,所以可以定义为自由函数。把这个操作符定义为自由函数还可以满足对称性的要求。实际上,+操作符可以通过+=操作符实现,这允许我们重用代码并且提供更加一致的行为。同时也减少了派生类需要重载的方法数量。

Currency operator + (const Currency& lhs, const Currency& rhs)
{
    return Currency(lhs) += rhs;
}

同样的技巧可以应用于其他算术操作符,比如-、-=、*、*=、/以及/=。例如,*=可以实现成员函数,而*可以使用*=操作符实现为自由函数。

至于关系操作符==、!=、<、<=、>及>=,必须实现为自由函数以保证对称性。在Currency类这个例子中,可以使用公有的GetValue()方法实现这些操作符。但如果这些操作符还需要访问对象的私有成员,那么有一种方法可以解决这个矛盾。这种情况下,可以提供公有方法来测试等于和小于条件,比如IsEqualTo()和IsLessThan()。进而可以用这两种基本函数实现所有关系操作符。

bool operator == (const Currency& lhs, const Currency& rhs)
{
    return lhs.IsEqualTo(rhs);
}

bool operator != (const Currency& lhs, const Currency& rhs)
{
    return !(lhs == rhs);
}

bool operator < (const Currency& lhs, const Currency& rhs)
{
    return lhs.IsLessThan(rhs);
}

bool operator <= (const Currency& lhs, const Currency& rhs)
{
    return !(lhs > rhs);
}

bool operator > (const Currency& lhs, const Currency& rhs)
{
    return rhs < lhs;
}

bool operator >= (const Currency& lhs, const Currency& rhs)
{
    return rhs <= lhs;
}

最后考虑的操作符是<<, 我会把它用于流的输出(而非移位)。流操作符应该声明为自由函数,因为第一个参数是流对象。你也可以使用公有的GetValue()方法达到此目的。如果流操作符需要访问类的私有成员,那么可以为<<创建公有的ToString()方法供其调用,从而避免使用友元。

 

6、转换操作符

转换操作符用于定义将对象自动转换成不同类型的对象。典型的例子是定义定制的字符串类,它可以传递给接收const char*指针的函数,比如C标准库函数strcmp()和strlen()。

class MyString
{
public:
    MyString(const char* string);

    //把MyString转换成C风格的字符串
    operator const char* () { return mBuffer; }
private:
    char* mBuffer;
    int mLength;
};

MyString mystr("Haggis");
int same = strcmp(mystr, "Edible");
int len = strlen(mystr);

注意,转换操作符没有指定返回值类型。这是因为编译器可以根据操作符名字推断出类型,而且转换操作符没有参数。

总结:给类添加转换操作符,从而利用自动类型强制转换。

 

7、避免使用#define定义常量

#define预处理指令本质上是用一个字符串替换源码中的另一个字符串。例如:

#define SETUP_NOISE(i, b0, b1, r0, r1) \
    t = vec[i] + 0x1000; \
    b0 = (lltrunc(t)) & 0xff; \
    b1 = (b0 + 1) & 0xff; \
    r0 = t - lltrunc(t); \
    r1 = r0 - 1.f;

无论如何都不应该在公有API头文件中用这种方式使用#define, 因为它会泄露实现细节。如果想在.cpp文件中使用这一技巧,并且理解#define的所有特性,那么尽管去做,但是绝不要在公有头文件中这样做。

        剩下的问题是使用#define为API指定常量,比如:

#define MORPH_FADEIN_TIME  0.3f
#define MORPH_IN_TIME      1.1f
#define MORPH_FADEOUT_TIME 1.4f

应该避免采用#define的这种用法(当然,除非你正在编写纯C API),原因如下所述。

        1>没有指定类型。#define不涉及为定义的常量做任何类型检查。因此必须确保显示地指定定义的常量类型,以避免歧义,比如对单精度浮点型常量使用f后缀。如果将一个浮点常量定义为10, 那么某些情况下它会被假定为整型,继而引起意料之外的数学取整错误。

        2>没有指定作用域。#define语句是全局的,不会被限制于诸如单个类的特定作用域。可以使用#undef预处理指令取消之前的#define,但这对于声明客户能够使用的常量而言没有什么意义。

        3>没有访问控制。不能把#define标记为公有的、受保护的或私有的。它本质上是公有的。因此不能使用#define指定一个只能被你定义的某个基类的派生类访问的常量。

        4>没有符号。前面给出的例子中,像MORPH_IN_TIME这样的符号名可能会被预处理器从代码中剥离,这样,编译器就无法看见这个名字,也就不能将其加入符号表。当客户试图调试使用API的代码时,这可能会隐藏一些重要信息,因为客户在调试器中只能看到一些没有任何描述性名字的常量值。

与使用#define声明API常量相比,更可取的办法是声明const常量,虽然定义const变量在某种程度上可能会使客户代码更臃肿。这里仅给出#define示例的另一个更好的版本:

class Morph
{
public:
    static const float FadeInTime;
    static const float InTime;
    static const float FadeOutTime;
    ...
};

这些常量的实际值在关联的.cpp文件中指定。(如果确实想让用户知道这些常量的值,可以在Morph类的API文档中告知这些信息。)注意,这种表示法不存在前面提到的任何问题:常量被表明为浮点型,作用域在Morph类中,显示标记为公有访问,并且会在符号表中生成表项。

总结:使用静态const数据成员而非#define表示类常量。

#define的另一个用途是为给定的变量提供一系列可能的值。例如,

#define LEFT_JUSTIFIED    0
#define RIGHT_JUSTIFIED   1
#define CENTER_JUSTIFIED  2
#define FULL_JUSTIFIED    3

可以通过enum关键字定义枚举类型,以便更好地表达这一语义。使用枚举使得类型更加安全,因为此时编译器会使用符号名而不是直接以整数设定枚举值(除非把int显示转换成枚举类型)。这也让传递非法值更加困难,比如在前面给出的例子中无法传递-1或者23。可以把前面的#define定义代码转换成枚举类型,如下所示:

enum JustificationType {
    LEFT_JUSTIFIED,
    RIGHT_JUSTIFIED,
    CENTER_JUSTIFIED,
    FULL_JUSTIFIED
};

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值