欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

《effective c++》学习笔记(四)

程序员文章站 2022-07-01 18:33:18
# 让接口容易被正确使用,不易被误用 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质 “促进正确使用”的办法包括接口上的一...

# 让接口容易被正确使用,不易被误用

好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质 “促进正确使用”的办法包括接口上的一致性,以及与内置类型的行为兼容 “阻止误用”的办法包括建立新类型,限制类型上的操作,束缚对象值,以及消除客户的资源管理责任 shared_ptr支持定制型删除器。这可防范DLL问题,可被用来自动解除互斥锁等

设计class犹如设计type

在设计一个class时,应该考虑以下几个问题:

新type的对象应该如何被创建和销毁 对象的初始化和对象的赋值有什么样的差别? 新type的对象如果被pass by value,意味着什么? 什么是新type的“合法值” 你的新type需要配合某个继承图系吗 你的新type需要什么样的转换 什么样的操作符和函数对此新type而言是合理的? 什么样的标准函数应该驳回? 谁该取用新type的成员? 什么是新type的“未声明接口” 你的新type多么一般化 你真的需要一个新type吗

 

Class的设计就是type的设计。在定义一个新type之前,请确定你已经考虑过本条款覆盖的所有讨论主题

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

考虑下面这份代码:

class Foo {
    // something
private:
    string str;
};

void bar(Foo f) {
    // something
}

当我们调用bar时,并且在函数中只是对f做一些访问操作(即并不需要修改f的值)时,会引起f的拷贝构造的析构操作,原因是我们以pass-by-value方式进行传递值。所以当str很大时,会造成性能上的影响。

但如果以pass-by-reference方式调用bar,则不会造成任何额外的拷贝和析构操作:

class Foo {
    // something
private:
    string str;
};

void bar(Foo &f) {
    // something
}

而当在bar里对f并没有修改,或者是不想改变实参的值,我们可以使用const修饰符将f保护起来:

class Foo {
// something
private:
string str;
};

void bar(const Foo &f) {
// something
}

而对于C++的内置类型(int、指针等等),或者迭代器等(内部实际是一个指针),则以pass-by-value类型传递较为高效。

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

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

当返回一个reference时,需要考虑是否返回了一个临时的reference,例如下面这份代码:
Bar &foo() {
Bar b;
return b;
}

当调用foo得到的Bar对象实际上是一个未定义的值

而就算使用new在堆上申请空间,也需要用户来主动释放,实际上这并不是一个很好的设计,因为有时候还是会产生未释放的指针,考虑下面这份代码:

Bar &operator*(const Bar &lhs, const Bar &rhs) {
    Bar *b = new Bar;
    return *b;
}

Bar a;
Bar b;
Bar c;

Bar d = a * b * c;

实际上Bar d = a * b * c;会得到两个Bar对象,但最后我们只会得到一份,另一份没有办法得到,所以也就没有办法去释放它

如果声明为static变量,也不是一个很好的方法,考虑下面这份代码:

Bar &operator*(const Bar &lhs, const Bar &rhs) {
    static Bar b;
    return b;
}

Bar a;
Bar b;
Bar c;
Bar d;

if (a * b == c * d) {
    // something
}

这里的条件则会永真

实际上正确的写法应该是,直接返回一个Bar对象,而不是引用,这样就不会再出现上述的问题。

绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象

将成员变量声明为private

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

宁以non-member、non-freiend替换member函数

宁以non-memboer non-friend函数替换member函数。这样做可以增加封装性、包裹弹性和机能扩充性

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

class Integer {
    friend Integer operator*(const Integer &lhs, const Integer &rhs);
public:
    Integer() = default;
    Integer(int i) : x(i) { }
private:
    int x;
};

Integer operator*(const Integer &lhs, const Integer &rhs) {
    Integer res;
    res.x = lhs.x * rhs.x;
    return res;
}

int main() {
    Integer a;
    Integer b = 5 * a;
    return 0;
}

为了实现int和Integer的互相转换,如果要把operator*声明为non-member,如果operator*为member函数,那么上述代码就不能通过编译

如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member

考虑写一个不抛异常的swap

STL库中的swap大致是这样:

template
void swap(const T &lhs, const T &rhs) {
    T temp(lhs);
    lhs = rhs;
    rhs = temp;
}

对于一般情况,这样的swap就可以满足要求,但对于类中有vector、string这种存储大量数据的成员、或者使用ptimpl手法时,这样拷贝实际上会消耗大量的时间,而实际上只需要置换两个对象的指针即可

所以需要做以下几个事情:

提供一个public swap成员函数,让它高效的置换你的类型的两个对象值,并且绝不应该抛出异常 在你的class或者template所在的命名空间提供一个non-member swap,并令它调用上述swap成员函数 如果你正在编写一个class,为你的class特化std::swap。并令它调用你的swap成员函数 如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸的使用swap

 

当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes,也请特化std::swap 调用swap时应针对std::swap使用using声明式,然后调用swap并不带任何“命名空间资格修饰” 为”用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西