0
点赞
收藏
分享

微信扫一扫

《Effective C++》笔记(二)

elvinyang 2022-01-30 阅读 110

《Effective C++》笔记(二)

四、设计与声明

18.让接口更容易被正确使用,不易被误用

首先,必须考虑客户可能做出什么样的错误。

例如:

class Date{
public:
    Date(int month, int day, int year);
};

Date d(30, 3, 1995);//不合理,应该(3, 30, 1995)
Date d(2, 30, 1995);//不合理,2月30号无效日期

考虑导入简单的外覆类型,具备了更高的类型安全性:

struct Day{
explicit Day(int d)
    :val(d)	{ }
int val;
};

struct Month{
explicit Month(int m)
    :val(m)	{ }
int val;
};

struct Year{
explicit Year(int y)
    :val(y)	{ }
int val;
};

class Date{
public:
    Date(const Month& m, const Day& d, const Year& y);
    //...
};

Date d(30, 3, 1995);//错误!不正确类型
Date d(Day(30), Month(3), Year(1995));//错误!不正确的类型
Date d(Month(3), Day(30), Year(1995));//OK,类型正确

请记住:

  • 好的接口很容易被正确使用,不容易被误用。应该在所有接口中努力达成这些性质。
  • “促进正确使用”,要重视接口一致性,以及与内置类型的行为兼容。
  • “阻止误用”,办法包括建立新类型,限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
  • tr1::shared_ptr支持定制删除器。这可防范DLL问题,可被用来自动解除互斥锁。
//std::shared_ptr的删除器使用示例
#include <iostream>
#include <memory>
 
struct Foo { int i; };
void foo_deleter(Foo * p)
{
    std::cout << "foo_deleter called!\n";
    delete p;
}
 
int main()
{
    std::shared_ptr<int> aptr;
 
    {
        // 创建拥有一个 Foo 和删除器的 shared_ptr
        auto foo_p = new Foo;
        std::shared_ptr<Foo> r(foo_p, foo_deleter);
        aptr = std::shared_ptr<int>(r, &r->i); // 别名使用构造函数
        // aptr 现在指向 int ,但管理整个 Foo
    } // r 被销毁(不调用删除器)
 
    // 获得指向删除器的指针:
    if(auto del_p = std::get_deleter<void(*)(Foo*)>(aptr))
    {
        std::cout << "shared_ptr<int> owns a deleter\n";
        if(*del_p == foo_deleter)
            std::cout << "...and it equals &foo_deleter\n";
    } else
        std::cout << "The deleter of shared_ptr<int> is null!\n";
} // 于此调用删除器

19.设计class犹如设计type

设计高效的class,需要面对的问题:

  • 新type的对象应该如何被创建和销毁?影响的是class的构造函数、析构函数、内存分配函数和释放函数(operator new,operator new[], operator delete和operator delete[])
  • 对象的初始化和对象的赋值该有什么样的差别?决定你的构造函数和赋值操作符的行为,以及两者之间的差异。
  • 新type的对象如果被pass by value(值传递),意味着什么?记住,copy构造函数用来定义一个type的pass-by-value该如何实现。
  • 什么是新type的“合法值”?对class的成员变量而言,通常只有某些数值集是有效的,那些数值集决定了你的class必须维护的约束条件,也就决定了你的成员函数必须进行的错误检查工作,它也影响函数抛出的异常。
  • 你的新type需要配合某个继承图系吗?继承自某些既有的classes,则受到那些classes的设计的束缚,尤其是virtual火non-virtual影响。同理,如果允许其他classes继承你的class,那会影响你所声明的函数,尤其是virtual。
  • 你的新type需要什么样的转换?需要提供类型转换函数。特别的,如果你只允许explicit构造函数存在,就得写出专门负责执行转换的函数,且不得为类型转换操作符或non-explicit-one-argument构造函数。
  • 什么样的操作符和函数对此新type而言是合理的?决定你将为你的class声明哪些函数。
  • 什么样的标准函数应该被驳回?那些正是你必须声明为private者。
  • 谁该取用新type的成员?决定成员的public,protected,private属性,和哪一个classes和函数应该是friends及其嵌套于另一个之内是否合理。
  • 什么是新type的“未声明接口”?它对效率、异常安全性以及资源运用(例如多任务锁定和动态内存)提供何种保证?
  • 你的新type有多么一般化?如果需要定义的是一整个types家族,则应该定义一个新的class template。
  • 你真的需要一个新type吗?如果只是定义新的子类以便为既有的类添加新的功能,那么说不定单纯定义一个或多个non-member函数或templates,更能达到目标。

20.宁以pass-by-reference-to-const替换pass-by-value

c++编译器的底层,reference往往以指针实现出来,因此pass-by-value通常意味真正传递的指针。因此如果你有个对象属于内置类型,pass-by-value往往比pass-by-reference的效率更高。这一点也适用于STL的迭代器和函数对象,因此习惯上它们都被设计为pass-by-value。

内置类型都相当小,因此有人认为,所有小型types都是pass-by-values的合格候选人,甚至它们都是用户自定义的class亦然,这是个不可靠的推论。对象小并不意味着其copy构造函数并不昂贵。许多对象 - 包括大多数STL容器 - 内含的东西只比一个指针多一些,但复制这种对象却需承担“复制那些指针所指的每一样东西”。那将非常昂贵。另外还有某些编译器对待“内置类型”和“用户自定义类型”的态度截然不同。纵使两者拥有相同的底层描述。如编译器拒绝把只由一个double组成的对象放进缓存器内,却很乐意在一个正规基础上对光秃秃的doubles那么做。另外作为一个用户自定义类型,其大小可能会有所变化。

请记住:

  • 尽量以pass-by-value-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题
  • 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。

21.必须返回对象时,别妄想返回其reference

在stack空间创建新对象,

const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
    Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
    return result;
}

这个函数返回一个reference指向result,但result是个local对象,而local对象在函数退出前被销毁了。

在heap空间创建对象,

const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
    Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
    return *result;
}

//像下面这样就会有资源泄露问题
//例1.
Rationl w,x,y,z;
w = x * y * z; //与operator*(operator*(x, y), z)相同

除了依旧必须付出一个“构造函数调用”代价,还有一个新的问题,谁该对new出来的对象进行delete?

如代码块中例1,同一个语句内调用了两次operator*,因而两次使用new,也就需要两次delete。但却没有合理的办法让operator *使用者进行那些delete调用,因为没有合理的办法让他们去的operator *返回的reference背后隐藏的那个指针。

为了避免任何构造函数被调用,有可能想到这样的实现代码,

const Rational& operator* (const Rational& lhs, const Rational& rhs)
{
    static Rational result;
    result = /*...*/;
    return result;
}

//首先,显而易见这样不是多线程安全的
//此外,看如下这种情况,
bool operator==(const Rational& lhs, const Rational& rhs);
Rational a,b,c,d;
if((a * b) == (c * d))
{
    //...
}
else
{
    //...
}
//这样表达式(a * b) == (c * d)总是被核算为true
//其等价函数形式(operator==(operator*(a, b), operator*(c, d))),
//这样子两个operator*返回的reference指向的是同一个static Rational对象

请记住:

  • (1)不要返回指针或引用指向一个local stack对象,(2)不要返回引用指向heap-allocated对象,(3)不要返回指针或引用指向一个local static对象而有可能同时需要多个这样的对象。

22.将成员变量声明为private

为什么成员变量不该是public?

  • 语法一致性。如果成员变量不是public,客户唯一能够访问对象的方法就是通过成员函数。使用函数可以让你对成员变量的处理有更精确的控制。
  • 封装性。如果你通过函数访问成员变量,日后可改以某个计算替换这个成员变量,而class客户一点也不会知道class的内部实现已经起了变化。将成员变量隐藏在函数接口的背后,可以为“所有可能的实现”提供弹性。

请记住:

  • 切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。

  • protected并不比public更具封装性。

23.宁以non-member-no-friend替换member函数

//一个网页浏览器类
class WebBrowser{
public:
    void clearCache();//清除下载元素高速缓存区
    void clearHistory();//清除访问过的URLs历史记录
    void removeCookies();//移除系统中的所有cookies
};

如果想一整个执行所有这些动作,可以有两种方式,

//1.WebBrower也提供这样一个函数
class WebBrower{
public:
    void clearEverything()
    {
        clearCache();
        clearHistory();
        removeCookies();
    }
};

//2.提供一个普通的no-member函数
void clearBrower(WebBrower& wb)
{
    wb.clearCache();
    wb.clearHistory();
    wb.removeCookies();
}

哪一个比较好?

面向对象守则要求数据应该尽可能被封装,然而与直观相反的,member函数clearEverything带来的封装性比non-member函数clearBrowser低。此外,提供non-member函数可允许对WebBrowser相关机能有较大的包裹弹性,能有更低的编译相侬度,增加WebBrowser的可延伸性。

愈少代码可以看到数据,愈多的数据可被封装,而我们也就愈能自由地改变对象数据,例如改变成员变量的数量、类型等等。成员变量应该是private,如果不是,就有无限量的函数可以访问它们,也就毫无封装性。能够访问private成员变量的函数只有class的member函数和friend函数而已。如果在一个member函数和一个non-member-non-friend函数做抉择,且两者提供相同功能,那么,使用non-member-non-friend函数封装性更好,因为它并不增加能够访问class内之private成分的函数数量。

让clearBrowser成为一个non-member函数并且位于WebBrowser所在的同一个namespace内,比起在class内,能降低编译依存性。

24.若所有参数皆需类型转换,请为此采用non-member函数

  • 如果你需要为某个函数的所有参数(包括this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。(不一定是friend函数,无论何时如果你可以避免friend函数就该避免)

25.考虑写出一个不抛异常的swap函数

swap是个有趣的函数。原本它只是STL的一部分,而后成为异常安全性编程的脊柱,以及用来处理自我赋值可能性的一个常见机制。

举报

相关推荐

0 条评论