这门课程(Responsive Web Design Fundamentals)主要介绍了关于响应式设计的基础概念,常用的技巧,常用的设计模式以及如何对网站进行优化的一些技巧。

这个课程主要是提供一些思路,涉及到技术的部分也只是介绍了最基本用法,并不会深入某个技术去展开。通过这个课程的学习,我们应该能够对响应式设计有一个基本的了解,能够知道在设计响应式的页面时需要如何做和需要注意的一些地方。

课程的大纲如下,比较基础,课程还有挺多的练习,通过练习能够掌握一些基本的用法。

Course Structure

为什么要响应式设计?(Why Responsive?)

响应式设计(Responsive Web Design,通常缩写RWD)是一种网页设计的技术,让网站在不同的设备上能够良好的显示。这个“良好的显示”是指能够给浏览网页者一个好的的体验。在移动设备普及前,一个网站只需要在一定程度上考虑不同尺寸即可,但是移动设备的普及,会引入一些很小尺寸的屏幕而且比例也不确定。这就需要响应式设计。

响应式设计除了是一门技术以外,更多的是一门艺术,如何安排在不同尺寸上的显示方式很大程度上是设计相关的东西而不是技术相关的东西。

移动优先

移动优先的设计方式是指在设计的时候从移动端先开始设计,然后再慢慢的往大尺寸去增强。这样的设计方式能够帮助设计者更好的去思考对于一个网页来说什么是最重要的内容,如何把这些内容合理的摆放才能达到效果。

从小开始(Starting Small)

既然涉及到不同的设备,那么关于尺寸的一些概念就需要在这里先搞清楚。

  • 设备分辨率(Hardware Resolution/Pixel)
  • 设备独立像素(Device Independent Pixel)
  • CSS像素(CSS Pixel)
  • Viewport

设备分辨率

这个很好理解,指得是设备本身的实际分辨率,也就是说一个设备上事实上分布的像素点的个数。

设备独立像素

设备独立像素是基于某一个系统的坐标系中的一个物理度量单位,系统会将设备独立的像素转换成设备上的实际像素。[1]

举个例子,iPhone 3G和iPhone 4S的物理尺寸都是3.5英寸,它们的设备分辨率分别是320 x 480和640 x 960。但是它们的设备独立分辨率是一样的,都是320 x 480[2][3]

课程中给出了一个很形象的图

Compare DIP and Hardware Pixels

CSS像素

在浏览器没有缩放的情况下,1个CSS像素对应1个设备独立像素。

Viewport

Viewport指的是用户在设备上的可视区域,在移动设备上这个可视区域往往比网页上的区域小。所以在老的移动浏览器上,会把页面根据实际的大小渲染出来,如果那个尺寸比手机大,用户则需要通过移动,放大和缩小来进行操作。

移动版Safari引入了“viewport meta”标签来允许开发控制viewpoint的尺寸和缩放比例,虽然这个不是标准,但是大多数的浏览器目前支持这种做法。

典型的viewport的使用方法如下,将这个meta标签加到<head>中:

1
<meta name="viewport" content="width=device-width, initial-scale=1">

关于Viewport的内容还有很多,我会另起一篇文章进行深入的学习。

从“小”开始

这里的从“小”开始指的是从小屏幕开始设计开发,这个跟上面提到的移动优先的概念是吻合的。



Udacity-RWD-Big-to-Small


Udacity-RWD-Small-to-Big

作者给出了两个图来解释,我觉得挺形象的。从小到大设计和从大到小设计就如同图中的漏斗一样。从大到小,很可能会误把一些有用的内容给过滤掉,而从小到大则不会。

另外从小开始设计还能够一开始就考虑到性能相关的问题。

逐步构建(Building Up)

构建一个响应式的引用主要需要用到一下几个概念和技术:

  • Media Query
  • Breakpoint
  • Flexbox

Media Query

Media query 是实现响应式编程使用的最重要的技术,这个是在CSS中根据某一些条件包含一些样式。

1
2
3
4
5
6
7
@media screen and (min-width: 480px) {
body {
background-color: lightgreen;
}
}
Try it Yourself »
The

这个是media query的语法定义,举个例子,比如我们想在viewport大于400px时将背景颜色改成红色。

1
2
3
4
5
@media screen and (min-width: 400px) {
body {
background-color: red;
}
}

Breakpoint

使用了media query以后会存在一个或多个点是的某一些样式被引用进来,比如上面的那个media query中400px就称为一个breakpoint(断点)。

断点的设置也是根据设计而来,没有严格的要求。我们可以在一个断点进行大的UI调整,也可以在断点进行一些细微的调整。这个也是一门艺术,而不是一个技术。

视频中提到了两个断点的例子,我觉得可以仔细观察一下:

Flexbox

Flexbox是flexible box的缩写,是2009年W3C提出了一种新的布局解决方案。目前已经在所有的浏览器上得到支持[5]。Flex
布局的核心思想是让flex容器有能力去修改其子元素的宽度,高度还有顺序从而更好的使用可用的空间。

课程中提到的不同响应式设计的模式都是使用flex技术实现的。

Common Responsive Patterns

课程中提供了四种比较通用的响应式模式,这几种模式只是提供了一种思路。可以根据具体的设计组合使用。

Column Drop

Column Drop

Mostly Fluid

Mostly Fluid

Layout Shifter

Layout Shifter

Off Canvas

Off Canvas

Optimizations

课程提供了几个在响应式设计的时候可以注意的优化点,包括图片、字体和表格。

图片

响应式的图片,不仅仅是要求图片的尺寸要正确,最关键的是要保证在不同的尺寸下图片显示出来的主体是一个合理的而不是只显示了一部分或者以不合理的缩放比例展示出来。在另一门「Responsive Images」有关于响应式图片的深入介绍。

字体

在适当的时候需要调整字体的大小从而使其能够更好地适应当前的尺寸。
另外,一行的字数也要控制在一个合理的而范围之内,就英语而言,每行65个字符是比较合适的

表格

表格在尺寸的屏幕上比较尴尬,因为表格的信息量比较大而且往往有比较多的列。对于表格我们可以采用一些策略:

  • 减少表格列,只显示最重要的信息。
  • 将列展平,用行的形式显示列。
  • 将表格放在一个container,将container的样式设置为overflow-x: auto, width: 100%。将滚动的区域控制在一个小范围内。

References

[1]: Wikipedia - Device-independent pixel
[2]: Apple iPhone 4s vs Apple iPhone 3G
[3]: Viewport Sizes
[4]: Mozilla - Using the viewport meta tag to control layout on mobile browsers
[5]: Can I use Flexbox

很多人的生活大部分时间都是处于公司-家的两点一线的状态,不知道你有没有过这样的经历。某一天,你开车开错路或者坐公交坐过站了,突然惊奇的发现就在你附近的一个地方跟你想象的不大一样,有时候还能够发现一些很好玩的地方。

某天晚上,在家附近吃完晚饭,突发奇想往从来都有去过的街的另一头散步走去。走出不到两公里,惊奇的发现原来离我家这么近的地方竟然还有一处迷你的公园。突然心生感慨,其实很多美好的东西可能就在你附近,只是你一直被一个隐形的边界限定住了你探索的脚步。

有一种说法是“旅游就是从一个你熟悉的地方跑到你一个别人熟悉的地方”。为什么有人喜欢旅游?因为在一个陌生的地方,人们往往能够发现一些不一样的地方,你去的地方离你越远可能这种差异性更强,能够给你带来的冲击也就越强。反过来,其他地方的人到你所生活的地方也同样会受到类似的冲击。在一个陌生的地方,人们的各个感官都处在兴奋、焦虑的状态,它们会极力得去吸取尽可能多的信息然后在脑子里飞速的运算。走在一个异国的路上,可能路边的一个垃圾桶、一个流浪汉、一个普通的招牌都能够吸引你驻足去观察,因为你从来没见过这样的东西。但如果,你在这个地方待上一段时间,你会发现你变得跟本地人一样,把自己的“感官”关闭了,不再抱有极大的好奇心去观察周遭的事物。因为你养成了习惯,习惯可以让你省去很多的思考,让你很轻松的去完成一件事情。比如说,你从地铁站出来不用思考是往左走还是往右走,你的习惯会把你很自然的带回家。

旅游只是一个简单的例子,除了旅游以外我们的生活中还有非常多的类似的例子。我认为在几乎所有你做的事情上,这种现象都是存在的。小到刷牙洗脸,大到公司决策,都或多或少的收到习惯的影响。

我们的大脑很聪明也很懒,它帮我们养成了很多的习惯,这样我们就不需要处处进行思考,很多时候跟着熟悉的感觉就能够完成一件事情了。大脑的聪明是很有必要的,在多数情况下能够让我们变得更轻松。但是,这种聪明也为我们设立了一个无形的边界,一个我们不留意可能都不会去注意到的一个边界。在这个边界内,我们是”安全”的,因为某些路径已经被多次证明过是可行的。

然而一条“安全”可行的路径不代表这条路径是最优的,即使在当下是最优的,也不代表它在外部条件变化的情况下依然是最优的。所以说这种安全是不可靠的,因为外界的条件不可能永远不变。而且习惯是有一定的惯性的,一个习惯你坚持的越久它的惯性就越大,你要改变它的难度及所带来的痛苦也就越大。

所以,我的结论是要需要经常去「界外」看看,让我们拥有随时改变路径的能力

那么这个结论就引出了另外几个问题:

  1. 作为一个身处于边界里面的人如何去发现自己的边界。都说要Think out of box,但是如何才能够发现自己的box?
  2. 发现以后到外面看看,肯定不能是简单地看看就完了。如何保持一个开放的心态去看待界外的事物?
  3. 如何才能够拓宽自己的边界?

这几个问题,容我再想想。

== end of file ==

4. 设计与声明

条款18:让接口容易被使用,不易被误用

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

这个条款的思想其实适用于所有的编程语言,只要你写的代码需要被其他地方调用,你都需要考虑接口的易用性。即使不暴露,良好的接口设计也能够提高代码的可读性。作者提出的这几点只是在C++这个语言的基础上需要注意的一些小点,我认为设计一个良好的易用的接口,仅仅做到这几点还远远不够。

接口一致性

作者认为除非有充分的理由,否则最好让类型与内置的类型的行为尽量一致。这个可以降低使用者错误使用的概率。

我想补充的另外一点是,比如你提供一整套的接口,你需要让自己的接口相互保持一致的风格。包括但不仅限于:函数的命名风格、函数的行为尽量保持统一的风格。

防止误用

任何接口如果要求客户必须记得做某件事情,就是有着“不正确使用”的倾向,因为客户可能会忘记做那件事。

条款19:设计class犹如设计type

Class 的设计就是type的设计。在定义一个新type之前,确保阅读了本条目的所有讨论主题。

  • 新type的对象应该如何被创建和销毁? 这个涉及到构造函数、构造函数以及operator new, operator delete, operator new[], operator delete[]。
  • 对象的初始化和对象的赋值该有什么样的差别?这个主要是要考虑构造函数和赋值操作符的行为。
  • 新type的对象如果被passed by value,意味着什么?
  • 什么事新type的“合法值”?这点主要是要注意类型的成员变量和合法性检查。
  • 你的新type需要配合某个继承图系吗?如果你继承其他的类,需要考虑它们的函数virtual和non-virtual性。如果你的类可能被继承就需要注意你声明的函数——尤其是析构函数。
  • 你的新type需要什么样的转换?如果你的类型需要转换成其他的类型,需要考虑定义显式转换或者隐式转换的函数。
  • 什么样的操作符和函数对此新type而言是合理的?参考条款23,24,26.
  • 谁该取用新type的成员?这个可以使用者的角度来帮助你判断哪一些成员为public,哪个为protected。
  • 你的新type有多么一般化(generic)?如果很一般化,可以考虑设计一个class template。
  • 你真的需要一个新type吧?如果只是定义新的derived class以便为既有的class添加功能,那么说不定单纯定义一个或多个non-member函数或template更能够达到目标。

这些问题并不是那么容易回答,在设计一个类型的时候尽量的去考虑这些问题能够更好地设计出一个易用合理的类型。

条款20:宁以 pass-by-reference-to-const 替换 pass-by-value

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

C++默认的传递对象的方法是by-value的方式,当一个对象有很多的成员变量或者有很深的继承结构。那么对于这个对象的构造和销毁会带来很大的开销。这种情况下用pass by reference的方式就能够节省下这种开始。加上const是避免对传入的参数进行修改。

Pass by value的另一个问题就是对象切割的问题。如果一个子类对象以pass-by-value的形式传递到一个函数中来,那么它的对象将会被切割而失去子类特有的成员变量,而且同时也会失去多态性。pass-by-reference可以解决这个问题,因为reference往往以指针实现,所以截断对象也同时会保有多态性。

决定一个对象pass by value还是pass by reference的依据不是简单地类型的成员数量,还要考虑这个类型在拷贝的时候是否会做一些比较昂贵的操作。

有一些例外,即内置类型,以及STL的迭代器和函数对象。除此之外 pass-by-reference-to-const 优于 pass-by-value。

条款21:必须返回对象是,别妄想返回其reference

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

返回一个pointer或者reference指向一个local stack对象明显是不合理的,因为local stack的生命周期在函数返回的时候就已经结束了,那么返回的那个指针或者引用将会指向一块被销毁的对象的内存。

指向一个heap-allocated的对象也是不合理的,因为你把销毁对象的任务移交给了使用函数的人,很多情况下人们会忘记。或者说所写的代码并没有让他有机会去销毁这个对象。比如说:

1
2
3
4
5
6
7
8
const Rational& operator * (const Rational& lhs,
const Rational& rhs) {
...
return *result;
}

Rational w, x, y, z;
w = x * y * z;

只有在类似于单例的情况下,返回一个reference指向一个static的对象是合理的。比如说条款4中FileSystem的例子。

条款22:将成员变量声明为private

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

为什么public不好?

public的成员变量破坏了类的封装性,它将成员直接暴露给客户,那么在未来修改字段的时候就会影响到使用者。

为什么protected也不好?

有一个原则,即某些东西的封装性与“其内容改变时可能造成的代码破坏量”成反比。如果从这个原则出发的话,protected虽然比public好,但是也并没有好太多。因为如果说你这个类型是对外暴露的,你不能阻止用户继承你这个类型,然后去访问其中的成员变量(C++暂时没有类似于final class的概念)。所以说protected的成员变量也不好。

2 构造/析构/赋值运算

条款10:令 operator= 返回一个 reference to *this

  • 令赋值(assignment)操作符返回一个 reference to *this。

有以下两个主要原因:

  1. 允许连锁赋值。比如说,int x, y, z; x = y = z;
  2. 这个协议被普遍的遵守(包括标准库中的很多类型),为了保持类的行为与其他类一致性。

条款11:在operator=中处理“自我赋值”

  • 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为为仍然正确。

一个对象可能被自我赋值,比如:

1
2
3
Widget w[];
// some operation
w[i] = w[j]; //w[i]和w[j]指向同一个对象

可以在operator= 操作中做一下简单的指针比较来避免不必要的操作

1
2
3
4
Widget& Widget::operator=(const Widget& rhs) {
if (this == &rhs) return *this;
...
}

当你需要在赋值操作中做一些复杂的控制逻辑,你不仅需要保证起“自我赋值安全性”还要保证“异常安全性”。这个会在条款xx中具体进行说明。

条款12:复制对象时勿忘其每一个成分

  • Copying 函数应该确保复制“对象内的所有成员变量”及“所有base class成分”
  • 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。

copy构造函数和copy assignment操作符我们统称为copying函数。当我们自己定义copying务必要确保所有的字段都进行了复制,同事如果有基类要保证相对应的copying函数被正确得调用。

作者认为虽然说copy构造函数和copy assignment操作符做的操作绝大多数是相同的,但是不应该尝试以某一个copying函数实现调用另一个copying函数。这样的操作并不合理。

3. 资源管理

条款13: 以对象管理资源

  • 为防止资源泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
  • 两个常被使用的RAII classes分别是tr1::shared_ptrauto_ptr(C++ 11 中有std::shared_ptrstd::auto_ptr)

内存泄漏和资源泄露很多情况都是因为一个内存/资源分配在堆上,被用于一个函数内,然后在控制流离开函数的时候没有得到正确的释放。对于这种情况引起的资源泄露,可以把资源包装成对象来实现合理的释放。

“以对象管理资源” 通常也被称为”资源取得时机便是初始化时机”(Resource Acquisition Is Initialization: RAII)。RAII的两个核心思想:

  • 获得资源后立刻放进管理对象内。
  • 管理对象运用析构函数确保资源得到释放。

我们可以根据自己的需求定制RAII类,同时也可以使用系统提供的智能指针可以用来作为管理对象。

  • auto_ptr - auto_ptr被销毁时会自动删除它所指之物。它的缺点是,如果多个auto_ptr指向同一个对象,那么那个对象会被删除一次以上。另外通过copy构造函数或copy assignment操作符复制它们的时候,它们会变成null。
  • shared_ptr - shared_ptr是一个引用计数原理的智能指针。它能够持续追踪有多少个对象指向某个资源,而且它的复制行为也看上去正常许多。它的缺点是,它无法打破循环引用的问题。

条款14:在资源管理类中小心copying行为

  • 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
  • 普遍而常见的RAII class copying行为是:禁止copying、施行引用计数法(reference counting)。不过其他行为也都可能被实现。

当我们定制RAII对象的时候,需要根据具体的需求来控制其拷贝的行为。不然可能会出现一些“不愉快”的事情。

拷贝的行为基本有下面几种:

  • 禁止复制。 很多情况对RAII对象进行复制是不合理的,比如说一个Lock类,保存着一个锁。合理的行为是禁止这个类型的复制行为。

  • 对底层资源祭出“引用计数法”。 如果我们希望保有资源,知道它的最后一个使用者被销毁。这种情况我们就应该在内部使用一个引用计数来管理资源,我们可以用shared_ptr来做引用计数。

  • 复制底部资源。 拷贝时将内部的资源进行一个完整的深层拷贝。比如说,一个字符串被复制的时候,它不仅要复制指向内存中字符串的指针,同时也应该复制对应的字符串内存。

  • 转移底层资源的拥有权 在一些比较特殊的场合可能希望只有一个RAII对象指向一个资源,当被复制的时候,资源的拥有权将会进行转移。这个行为跟auto_ptr的行为是一样的。

条款15:在资源管理类中提供对原始资源的访问

  • APIs 往往要求访问原始资源(raw resources),所以每一个RAII classes应该提供一个“取得其所管理之资源”的办法。
  • 对原始资源的访问可能经由显式转换或隐式转换。一般而言显示转换比较安全,但是隐式转换对客户比较方便。

在一些情况下我们的需要访问资源管理类中的原始资源,这个时候我们就应该提供一个能够获得原始资源的方法。

这个看上去破坏了封装性,但是资源管理类设计的初衷是为了更好地管理资源的申请和释放,并非对资源进行完全的封装。所以提供获取原始资源的方法也还是比较合理的做法。

可以通过显式和隐式转换两种方法。

显式转换

1
2
3
4
5
6
class Font {
public:
...
FontHandle get() const { return f; }
...
}

隐式转换

1
2
3
4
5
6
class Font {
public:
...
operator FontHandle() const { return f; }
...
}

虽然两种方式都可行,但个人认为显式的这种方式更好一些。因为这个可以避免很多不小心进行的转换,只有在需要的时候进行显式的调用。

条款16:成对使用new和delete时要采用相同形式

如果你在new表达式中使用[],必须在相对应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相对应的delete表达式中使用[]

条款17:以独立语句将newed对象置入智能指针

以独立语句将newed对象存储于智能指针内。如果不这么做,一旦异常被抛出,有可能导致难以察觉的资源泄露。

例子:

1
2
3
4
5
6
7
8
9
int priority();
void processWidget(std::shared_ptr<Widget> pw, int p);

// Bad
processWidget(std::shared_ptr<Widget>(new Widget), priority());

// Good
std::shared_ptr<Widget> pw(new Widget)
processWidget(pw, priority());

为什么第一个例子有问题?因为在C++编译器生成代码的时候有可能会出现以下这种情况:

  1. 执行”new Widget”
  2. 调用priority()
  3. 调用std::shared_ptr构造函数

如果第二部抛出异常,那么第一步申请出来的资源还未放入智能指针内,所以没有人去释放它。于是就造成了资源泄露。

2 构造/析构/赋值运算

条款05:了解C++默默编写并调用哪些函数

编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。

定义一个啥也没有的类:

1
class Empty { }

编译器有可能会帮你生成:

1
2
3
4
5
6
7
class Empty {
public:
Empty() {...} //默认构造函数
Empty(const Empty& rhs) {...} //拷贝构造函数
~Empty(){...} //析构函数
Empty& operator=(const Empty& rhs) {...} // copy assignment操作符
}

是否生成这些方法取决于使用的时候是否需要,比如下面这段代码就用到了这些函数

1
2
3
4
Empty e1;		//默认构造函数
//析构函数
Empty e2(e1); //拷贝构造函数
e2 = e1; //copy assignment操作符

条款06:若不想使用编译器自动生成的函数,就该明确拒绝

  • 为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法。

正如条款5中提到的,即使你自己没有显示定义,编译器默认会为我们创建一些函数。而在某一些条件下,这些函数所带来的行为并不是你想要的。

那么这个时候你就应该把你想要隐藏的方法声明为private并不提供任何的定义,这样任何尝试去使用的人都会收到一个链接错误。

什么的场景下可能需要使用这种方法?

  • private 构造函数
    当你希望用户不能够随意的自己去创建实例而是通过某种方法获得一个实例的时候。比如说单例模式。
  • private 析构函数
    当你希望一个实例的生命周期由另一个类来管理的时候使用。比如说,你需要你的实例使用引用计数,如果不是0则不能够析构。这个时候你就需要提供比如说:Acquire,Release的方法。我觉得如果只是reference count的话,利用shared_ptr也能够实现类似的功能。
    参考链接:Private Destructors
  • private 拷贝构造函数和copy assignment 操作符
    当你希望一个对象是不可拷贝的时候。
    Example:
    1
    2
    3
    4
    5
    6
    Class Uncopyable {
    ...
    private:
    Uncopyable(const Uncopyable&);
    ~Uncopyable& operator=(const Uncopyable&);
    }

条款07:为多态基类声明virtual析构函数

  • polymoriphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数
  • Classes的设计目的如果不是作为base classes使用,或者不是为了具备多态性(polymorphically),就不该声明virtual析构函数。

为什么带有多态性质的基类应该声明一个virtual析构函数?

带有多态性质的基类意味着一个基类的指针可能存放着子类的实例。在这种情况下如果析构函数没有声明为virtual的话,delete一个基类指针不能够正确的调用到子类的析构函数。这就意味着会造成资源的泄露。

但是如果一个类设计的时候没有想让别的类继承,如果另一个类继承了,就可能会出现资源的泄露。因为C++在语言层面没有提供类似于C# seal的语法,所以没有办法杜绝这种情况。如果发现一个类的析构函数不是virtual的那么就不应该去继承它。

反过来看就更简单了,如果一个类设计的目的不是作为基类使用,也就意味着它的指针不可能指向一个子类的对象。那么声明成virtual的就没有太大必要,反而会因为virtual而增长了这个对象的内存大小。

条款08:别让异常逃离析构函数

  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或者结束程序。
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

为什么析构函数中抛出异常不好?

  1. 析构函数被调用的地方不好控制,而且不好对其进行处理。因为任何对象都有析构函数,你不可能在任何地方对异常进行捕捉。(自己补充的)
  2. 如果析构函数中所抛出异常未处理会导致未定义行为。

所以如果在析构函数中有可能执行到一些会抛异常的方法,需要对其进行捕捉。如果你认为异常可以忍受则默默吞下,记下log,否则记下log然后直接结束程序。

有什么更好的办法?

如果在析构函数里需要进行某个可能抛出异常的方法,那么就应该提供一个普通方法允许用户自己调用。这样子用户就有机会自己对其进行处理。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class DBConn {
public:
~DBConn() {
db.close(); //bad, may throw exception
}
private:
DBConnection db;
}

class DBConn {
public:
void close() {
db.close();
closed = true;
}
~DBConn() {
if (!closed) {
try {
db.close(); //good, catch exception and provide normal method to close
}
catch (...) {
//log
}
}
}
private:
DBConnection db;
}