今天继续更新《Effective C 》和《C 并发编程实战》的读书笔记,下面是已经更新过的内容:
《C 并发编程实战》读书笔记(1):并发、线程管控
《C 并发编程实战》读书笔记(2):并发操作的同步
《C 并发编程实战》读书笔记(3):内存模型和原子操作
《C 并发编程实战》读书笔记(4):设计并发数据结构
《Effective C 》读书笔记(1):让自己习惯C
《Effective C 》读书笔记(2):构造/析构/赋值运算
《Effective C 》读书笔记(3):资源管理
《Effective C 》读书笔记(4):设计与声明
大多数情况下,适当地提出声明与定义是花费心力最多的事情,而相应的实现大多直截了当。但仍有一些细节值得注意。
条款26、尽可能延后变量定义式的出现时间
当程序运行到对象的定义式时就肯定会多出了一次构造、一次析构的成本。
过早地声明某对象,如果因为种种原因(条件分支、过早返回、异常等)没有使用该对象,那么不仅降低了程序的清晰度,还浪费了上述的构造、析构的成本。
条款27、尽量少做转型动作
C 中兼容C式的转型操作,还有四个新式转型;后者容易被辨识,目标也更狭窄,易于编译器、程序员诊断。
代码语言:javascript复制//C
(T)expression
T(expression)
//C
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
转型破坏了类型系统,可能导致任何种类的麻烦;它并非只是让编译器将某类型视为另一种类型,而是往往真的产生一些代码。
如果可以,尽量避免转型。在注重效率的代码中避免dynamic_cast,因为它的很多实现版本执行得很慢;尤其要避免一连串的判断dynamic_cast,不仅又大又慢,而且基础不稳,每次类有修改该代码也需要调整。
对于需要转型的设计,试着发展无需转型的替代设计。
代码语言:javascript复制class Widget { ... };
class SpecialWidget : public Widget{
public:
void f();
};
//需要转型
std::vector<std::shared_ptr<Widget>> ptrs;
for(auto iter = ptrs.begin(); iter != ptrs.end(); iter){
if(SpecialWidget *psw = dynamic_cast<SpecialWidget*>(iter->get())){
psw->f();
}
}
//无需转型
std::vector<std::shared_ptr<SpecialWidget>> ptrs;
for(auto iter = ptrs.begin(); iter != ptrs.end(); iter){
(*iter)->f();
}
//或者将f()实现为虚函数,则也无需转型
如果转型是必要的,试着将它隐藏于某函数背后;用户调用该函数而不是使用转型。
条款28、避免返回handles指向对象内部成分
避免返回handles(包括引用、指针、迭代器)指向对象内部。即使使用const修饰返回值,仍然可能存在handles所指对象或所属对象不存在的问题。
代码语言:javascript复制class Foo {
public:
Foo() { m_name = new std::string("foo"); }
~Foo() { delete m_name; }
void dtor() { m_name = nullptr; }
const std::string& getName() const { return *m_name; }
private:
std::string* m_name;
};
void exit(const Foo& foo) {
// m_name指针为nullptr时,访问它会导致未定义行为
std::cout << foo.getName() << std::endl;
}
int main() {
Foo foo;
foo.dtor();
exit(foo);
return 0;
}
遵守该条款可增加封装性,帮助const成员函数的行为像个const,并将空悬handles的可能性降至最低。
条款29、为“异常安全”而努力是值得的
抛出异常时,异常安全的函数会不泄露任何资源、不允许数据败坏。函数的“异常安全保证”等于所调用的各个函数的“异常安全保证”中的最弱者。
异常安全函数提供以下三个保证之一:1、基本承诺。异常时所有对象处于内部前后一致的情况,但显示状态不可预料。2、强烈保证。异常时程序状态不改变。3、不抛掷保证。承诺不抛出异常。
“强烈保证”往往能够以“copy and swap”实现出来。
代码语言:javascript复制class Widget {
public:
Widget() { value = new int(0); }
Widget(const Widget &rhs) {
value = new int(*rhs.value);
}
// copy and swap
Widget& operator=(Widget rhs) {
swap(rhs);
return *this;
}
void swap(Widget &rhs) {
using std::swap;
swap(value, rhs.value);
}
~Widget() { delete value; }
private:
int *value;
};
条款30、透彻了解inlining的里里外外
将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化。
不过目前inline更多代表允许多重定义,例如head-only库可以用inline在头文件中定义变量。
条款31、将文件间的编译依存关系降至最低
该原则是为了减少不必要的编译时间和编译错误,提高代码的可维护性。其基本思想是:依赖于声明式而非定义式,头文件仅有声明式。基于此有两个手段:
1、Handle classes。把类分割为两个类,一个只包含接口与真正对象的指针,另一个负责对象实现的细节;这种设计称为pimpl。
类的用户完全与其实现细节分离,任何实现的修改都不需要客户端重新编译,真正实现接口与实现分离。
代码语言:javascript复制#include<string>
#include<memory>
//Person实现类的前置声明
class PersonImpl;
//Person接口用到的类 前置声明
class Data;
class Address;
class Person{
public:
Person(const std::string& name, const Data& birtyday, const Address& addr);
...
private:
std::shared_ptr<PersonImpl> pImpl;
};
2、Interface classes。提供一个抽象基类,目的是描述派生类的接口,因此它不提供成员变量、构造函数,只提供虚析构函数与一组纯虚函数来描述所有接口。
代码语言:javascript复制class Person{
public:
virtual ~Person();
virtual std::string name() const = 0;
...
};