11.7.2 值语义与生命期
值语义的一个巨大好处是生命期管理很简单,就跟int一样——你不需要操心int的生命期。值语义的对象要么是stack object,要么直接作为其他object的成员,因此我们不用担心它的生命期(一个函数使用自己stack上的对象,一个成员函数使用自己的数据成员对象)。相反,对象语义的object由于不能拷贝,因此我们只能通过指针或引用来使用它。
一旦使用指针和引用来操作对象,那么就要担心所指的对象是否已被释放,这一度是C++程序bug的一大来源。此外,由于C++只能通过指针或引用来获得多态性,那么在C++里从事基于继承和多态的面向对象编程有其本质的困难——对象生命期管理(资源管理)。
考虑一个简单的对象建模——家长与子女:a Parent has a Child, a Child knows its Parent。在Java中很好写,不用担心内存泄漏,也不用担心空悬指针:
public class Parent
{private Child myChild;
}public class Child
{private Parent myParent;
}
只要正确初始化myChild和myParent,那么Java程序员就不用担心出现访问错误。一个handle是否有效,只需要判断其是否non null。
在C++中就要为资源管理费一番脑筋:Parent和Child都代表的是真人,肯定是不能拷贝的,因此具有对象语义。Parent是直接持有Child吗?抑或Parent和Child通过指针互指?Child的生命期由Parent控制吗?如果还有ParentClub和School两个class,分别代表家长俱乐部和学校:ParentClub has many Parent(s), School has many Child(ren),那么如何保证它们始终持有有效的Parent对象和Child对象?何时才能安全地释放Parent和Child?
直接但是易错的写法:
class Child;class Parent : boost::noncopyable
{Child *myChild;
};class Child : boost::noncopyable
{Parent *myParent;
};
如果直接使用指针作为成员,那么如何确保指针的有效性?如何防止出现空悬指针?Child和Parent由谁负责释放?在释放某个Parent对象的时候,如何确保程序中没有指向它的指针?那么释放某个Child对象的时候呢?
这一系列问题一度是C++面向对象编程头疼的问题,不过现在有了smart pointer,我们可以借助smart pointer把对象语义转换为值语义(即像持有int一样持有对象(的智能指针),其实智能指针本身既不是值语义也不是对象语义),从而轻松解决对象生命期问题:让Parent持有Child的smart pointer,同时让Child持有Parent的smart pointer,这样始终引用对方的时候就不用担心出现空悬指针。当然,其中一个smart pointer应该是weak pointer,否则会出现循环引用,导致内存泄漏。到底哪一个是weak reference,则取决于具体应用场景。
如果Parent拥有Child,Child的生命期由其Parent控制,Child的生命期小于Parent,那么代码就比较简单:
class Parent;class Child : boost::noncopyable
{
public:explicit Child(Parent *myParent_) : myParent(myParent_){ }private:Parent *myParent;
};class Parent : boost::noncopyable
{
public:Parent(): myChild(new Child(this)){ }private:boost::scoped_ptr<Child> myChild;
};
在上面这个设计中,Child的指针不能泄露给外界,否则仍然有可能出现空悬指针。
如果Parent与Child的生命期相互独立,就要麻烦一些:
class Parent;
typedef boost::shared_ptr<Parent> ParentPtr;// class的默认继承方式是private
// struct的默认继承方式是public
class Child : boost::noncopyable
{
public:explicit Child(const ParentPtr &myParent_): myParent(myParent_){ }private:boost::weak_ptr<Parent> myParent;
};typedef boost::shared_ptr<Child> ChildPtr;class Parent : public boost::enable_shared_from_this<Parent>,private boost::noncopyable
{
public:Parent(){ }void addChild(){myChild.reset(new Child(shared_from_this()));}private:ChildPtr myChild;
};int main()
{ParentPtr p(new Parent);p->addChild();
}
上面这个shared_ptr+weak_ptr的做法似乎有点小题大做。
考虑一个稍微复杂一点的对象模型:“a Child has parents: mon and dad; a Parent has one or more Child(ren); a Parent knows his/her spouse.”这个对象模型用Java表述一点都不复杂,垃圾收集会帮我们搞定对象生命期。
public class Parent
{private Parent mySpouse;private ArrayList<Child> myChildren;
}public class Child
{private Parent myMom;private Parent myDad;
}
如果用C++来实现,如何才能避免出现空悬指针,同时避免出现内存泄漏呢?借助shared_ptr把裸指针转换为值语义,我们就不用担心这两个问题了:
class Parent;
typedef boost::shared_ptr<Parent> ParentPtr;class Child : boost::noncopyable
{
public:explicit Child(const ParentPtr &myMom_, const ParentPtr &myDad_): myMom(myMom_), myDad(myDad_){}private:boost::weak_ptr<Parent> myMom;boost::weak_ptr<Parent> myDad;
};
typedef boost::shared_ptr<Child> ChildPtr;class Parent : boost::noncopyable
{
public:Parent(){}void setSpouse(const ParentPtr &spouse){mySpouse = spouse;}void addChild(const ChildPtr &child){myChildren.push_back(child);}private:boost::weak_ptr<Parent> mySpouse;std::vector<ChildPtr> myChildren;
};int main()
{ParentPtr mom(new Parent);ParentPtr dad(new Parent);mom->setSpouse(dad);dad->setSpouse(mom);{ChildPtr child(new Child(mom, dad));mom->addChild(child);dad->addChild(child);}{ChildPtr child(new Child(mom, dad));mom->addChild(child);dad->addChild(child);}
}
如果不使用smart pointer,用C++做面向对象编程将会困难重重。
11.7.3 值语义与标准库
C++要求凡是能放入标准容器的类型必须具有值语义。准确地说:type必须是SGIAssignable concept的model(即可以通过赋值操作符进行赋值的类型)。但是,由于C++编译器会为class默认提供copy constructor和assignment operator,因此除非明确禁止,否则class总是可以作为标准库的元素类型——尽管程序可以编译通过,但是隐藏了资源管理方面的bug。
因此,在写一个C++ class的时候,让它默认继承boost::noncopyable,几乎总是正确的。
在现代C++中,一般不需要自己编写copy constructor或assignment operator,因为只要每个数据成员都具有值语义的话,编译器自动生成的member-wise copying & assigning(即对一个对象的成员进行逐个复制和赋值的操作)就能正常工作;如果以smart ptr为成员来持有其他对象,那么就能自动启用或禁用copying & assigning(unique_ptr会禁用,而shared_ptr和weak_ptr会启用)。例外:编写HashMap这类底层库时还是需要自己实现copy control。
11.7.4 值语义与C++语言
C++的class本质上是值语义的,这才会出现object slicing(当派生类对象赋值给基类对象时,派生类对象的附加信息被截断,只保留了基类部分的信息)这种语言独有的问题,也才会需要程序员注意pass-by-value和pass-by-const-reference的取舍。在其他面向对象编程语言中,这都不需要费脑筋。
值语义是C++语言三大约束之一,C++的设计初衷是让用户定义的类型(class)能像内置类型(int)一样工作,具有同等的地位。为此C++做了以下设计(妥协):
1.class的layout与C struct一样,没有额外开销。定义一个“只包含一个int成员的class”的对象开销和定义一个int一样。
2.甚至class data member都默认是uninitialized,因为函数局部的int也是如此。
3.class可以在stack上创建,也可以在heap上创建。因为int可以是stack variable。
4.class的数组就是一个个class对象挨着,没有额外的indirection。因为int数组就是这样的。因此派生类数组的指针不能安全转换为基类指针。
5.编译器会为class默认生成copy constructor和assignment operator。其他语言没有copy constructor一说,也不允许重载assignment operator。C++的对象默认是可以拷贝的,这是一个尴尬的优越性。
6.当class type传入函数时,默认是make a copy(除非参数声明为reference)。因为把int传入函数时是make a copy。
C++的“函数调用”比其他语言复杂之处在于参数传递和返回值传递。C、Java等语言都是传值(Java中传递的是对象的引用值),简单地复制几个字节的内存就行了。但是C++对象是值语义,如果以pass-by-value方式把对象传入函数,会涉及拷贝构造。代码里看到一句简单的函数调用,实际背后发生的可能是一长串对象构造操作,因此减少无谓的临时对象是C++代码优化的关键之一。
7.当函数返回一个class type时,只能通过make a copy(C++不得不定义RVO(Return Value Optimization,返回值优化,当函数返回一个局部对象(非引用)时,通常会触发一个拷贝构造函数,将局部对象的副本返回给调用者,RVO通过直接在调用者的内存中构建返回值,而不是创建一个局部对象再拷贝,从而避免了不必要的拷贝操作)来解决性能问题)。因为函数返回int时是make a copy。
8.以class type为成员时,数据成员是嵌入的。例如pair<complex<double>, size_t>
,的layout就是complex<double>
挨着size_t。
这些设计带来了性能上的好处,原因是memory locality。比方说我们在C++里定义complex<double>
class,array of complex<double>
,vector<complex<double> >
,它们的layout如图11-8所示(re和im分别是复数的实部和虚部)。
而如果我们在Java里干同样的事情,layout大不一样,memory locality也差很多(见图11-9)(图11-9中的handle是Java的reference,为了避免与C++引用混淆,这里换个写法)。
在Java中每个object都有head,在常见的JVM中至少有两个word的开销。对比Java和C++,可见C++的对象模型要紧凑得多。
11.7.5 什么是数据抽象
本节谈一谈与值语义紧密相关的数据抽象(data abstraction),解释为什么它(指数据抽象)是与面向对象并列的一种编程范式,为什么支持面向对象的编程语言不一定支持数据抽象。C++在最初的时候以data abstraction为卖点,不过随着时间的流逝,现在似乎很多人只知Object-Oriented,不知data abstraction了。C++的强大之处在于“抽象”不以性能损失为代价,本节我们将看到具体例子。
数据抽象(data abstraction)是与面向对象(object-oriented)并列的一种编程范式(programming paradigm)。说“数据抽象”或许显得陌生,它的另外一个名字“抽象数据类型(abstract data type,ADT)”(一种模型,它定义了数据类型的操作和规定了这些操作的行为,而不关注具体的实现细节)想必如雷贯耳。
“支持数据抽象”一直是C++语言的设计目标,Bjarne Stroustrup在他的《The C++ Programming Language(第2版)》(1991年出版)中写道:
The C++ programming language is designed to
1.be a better C
2.support data abstraction
3.support object-oriented programming
这本书的第3版(1997年出版)增加了一条:
C++ is a general-purpose programming language with a bias towards systems programming that
1.is a better C,
2.supports data abstraction,
3.supports object-oriented programming, and
4.supports generic programming.
在C++的早期文献(http://www.softwarepreservation.org/projects/c_plus_plus/index.html#cfront)中有一篇Bjarne Stroustrup于1984年写的《Data Abstraction in C++》(http://www.softwarepreservation.org/projects/c_plus_plus/cfront/release_e/doc/DataAbstraction.pdf)。在这个页面还能找到Bjarne写的关于C++操作符重载和复数运算的文章,作为数据抽象的详解与范例。可见C++早期是以数据抽象为卖点的,支持数据抽象是C++相对于C的一大优势。
作为语言的设计者,Bjarne把数据抽象作为C++的四个自语言之一。这个观点不是被普遍接受的,比如作为语言的使用者,Scott Meyers在[EC3]中把C++分为四个子语言:C、Object-Oriented C++、Template C++、STL。在Scott Meyers的分类法中,就没有出现数据抽象,而是归入了Object-Oriented C++。
那么到底什么是数据抽象?
简单地说,数据抽象是用来描述(抽象)数据结构的。数据抽象就是ADT。一个ADT主要表现为它支持的一些操作,比方说stack::push()、stack::pop(),这些操作应该具有明确的时间和空间复杂度。另外,一个ADT可以隐藏其实现细节,例如stack既可以用动态数组实现,又可以用链表实现。
按照这个定义,数据抽象和基于对象(object-based)很像,那么它们的区别在哪里?语义不同。ADT通常是值语义,而object-based是对象语义。(这两种语义的定义见11.7.1 “什么是值语义”)。ADT class是可以拷贝的,拷贝之后的instance与原instance脱离关系。
比方说
stack<int> a;
a.push(10);
stack<int> b = a;
b.pop();
这时候a里仍然有元素10。
C++标准库中的数据抽象
C++标准库里complex<>、pair<>、vector<>、list<>、map<>、set<>、string、stack、queue都是数据抽象的例子。vector是动态数组,它的主要操作有size()、begin()、end()、push_back()等等,这些操作不仅含义清晰,而且计算复杂度都是常数。类似地,list是链表,map是有序关联数组,set是有序集合、stack是FILO栈、queue是FIFO队列。“动态数组”、“链表”、“有序集合”、“关联数组”、“栈”、“队列”都是定义明确(操作、复杂度)的抽象数据类型。
数据抽象与面向对象的区别
本文把data abstraction、object-based、object-oriented视为三个编程范式。这种细致的分类或许有助于理解区分它们之间的差别。
庸俗地讲,面向对象(object-oriented)有三大特征:封装、继承、多态。而基于对象(object-based)则只有封装,没有继承和多态,即只有具体类,没有抽象接口。他们两个都是对象语义。
面向对象真正核心的思想是消息传递(messaging),“封装继承多态”只是表象。关于这一点,孟岩(http://blog.csdn.net/myan/article/details/5928531,“程序是由一组对象组成,这些对象各有所能,通过消息传递实现协作”)和王益(http://cxwangyi.wordpress.com/2011/06/19/杂谈现代高级编程语言/)都有精彩的论述,笔者不再赘言。
数据抽象与它们两个的界限在于“语义”,数据抽象不是对象语义,而是值语义。比方说muduo里的TcpConnection和Buffer都是具体类,但是前者是基于对象的(object-based),而后者是数据抽象。
类似地,mudou::Date、muduo::Timestamp都是数据抽象。尽管这两个class简单到只有一个int/long数据成员,但是它们各自定义了一套操作(operation),并隐藏了内部数据,从而让它从data aggregation变成了data abstraction。
数据抽象是针对“数据”的,这意味着ADT class应该可以拷贝,只要把数据复制一份就行了。如果一个class代表了其他资源(文件、员工、打印机、账号),那么它通常就是object-based或object-oriented,而不是数据抽象。
ADT class可以作为Object-based/object-oriented class的成员,但反过来不成立,因为这样一来AST class的拷贝就失去意义了。
11.7.6 数据抽象所需的语言设施
不是每个语言都支持数据抽象,下面简要列出“数据抽象”所需的语言设施。
支持数据聚合
数据聚合即data aggregation,或者叫value aggregates。即定义C-style struct,把有关数据放到同一个struct里。FORTRAN 77没有这个能力,FORTRAN 77无法实现ADT。这种数据聚合struct是ADT的基础, struct List、 struct HashTable等聚合起来就能把链表和哈系表结构的数据放到一起,而不是用几个零散的变量来表示它。
全局函数与重载
例如我定义了complex,那么我可以同时定义complex sin(const complex &x)和complex exp(const complex &x)等等全局函数来实现复数的三角函数和指数运算。sin()和exp()不是complex的成员,而是全局函数double sin(double)和double exp(double)的重载。这样能让double a = sin(b);和complex a = sin(b);具有相同的代码形式,而不必写成complex a = b.sin();。
C语言可以定义全局函数,但是不能与已有的函数重名,也就没有重载。Java没有全局函数,而且Math class是封闭的,并不能往其中添加sin(Complex)。
成员函数与private数据
数据也可以声明为private,防止外界意外修改。不是每个ADT都适合把数据声明为private,例如complex、Point、pair<>这样的ADT使用public data更加合理。
要能够在struct里定义操作,而不是只能用全局函数来操作struct。比方说vector有push_back()操作,push_back是vector的一部分,它必须直接修改vector的private data members,因此无法定义为全局函数。
这两点其实就是定义class,现在的语言都能直接支持,C语言除外。
拷贝控制(copy control)
copy control是拷贝stack a; stack b = a;和赋值stack b; b = a;的合称。
当拷贝一个ADT时会发生什么?比方说拷贝一个stack,是不是应该把它的每个元素按值拷贝到新stack?
如果语言支持显式控制对象的生命期(比方说C++的确定性析构(确定性析构指对象在离开作用域或被删除时,会立即调用析构函数,而在C#等语言中,由于垃圾回收机制,我们不能知道对象的析构函数什么时候会被调用)),且ADT用到了动态分配的内存,那么copy control更为重要,可防止访问已经失效的对象。
由于C++ class是值语义,copy control是实现深拷贝的必要手段,而且ADT用到的资源只涉及动态分配的内存,所以深拷贝是可行的。相反,object-based编程风格中的class往往代表某样真实的事物(Employee、Account、File等等),深拷贝无意义。
C语言没有copy control,也没有办法防止拷贝,一切要靠程序员自己小心在意。FILE *可以随意拷贝,但是只要关闭其中一个copy,其他copy也都失效了,跟空悬指针一般。整个C语言对待资源(malloc()得到的内存,open()打开的文件,socket()打开的连接)都是这样的,用整数或指针来代表(即“句柄”)。而整数和指针类型的“句柄”是可以随意拷贝的,很容易就造成重复释放、遗漏释放、使用已经释放的资源等等常见错误。这方面C++是一个显著的进步,作者认为boost::noncopyable是Boost里最值得推广的库。
操作符重载
如果要写动态数组,我们希望能像使用内置数组一样使用它,比如支持下标操作。C++可以重载operator[]来做到这一点。
如果要写复数,我们希望能像使用内置的double一样使用它,比如支持加减乘除。C++可以重载operator+等操作符来做到这一点。
如果要写日期与时间,我们希望它能直接用大于或小于号来比较先后,用==来判断是否相等。C++可以重载operator<等操作符来做到这一点。
这要求语言能重载成员与全局操作符。操作符重载是C++与生俱来的特性,1984年的CFront E(一个早期的C++编译器)就支持操作符重载,并且提供了一个complex class,这个class与目前标准库的complex<>在使用上无区别。
如果没有操作符重载,那么用户定义的ADT相比内置类型用起来就不一样了(想想有的语言要区分==和equals,代码写起来实在很累赘)。Java里有BigInteger,但是BigInteger用起来和普通int/long大不相同:
public static BigInteger mean(BigInteger x, BigInteger y)
{BigInteger two = BigInteger.valueOf(2);return x.add(y).devide(two);
}public static long mean(long x, long y)
{return (x + y) / 2;
}
当然,操作符重载容易被滥用,因为这样显得很“酷”。作者认为只在ADT表示一个“数值”的时候才适合重载加减乘除,其他情况下用具名函数为好,因此muduo::Timestamp只重载了关系操作符,没有重载加减操作符。另外一个理由见12.6 “采用有利于版本管理的代码格式”。
效率无损
“抽象”不代表低效。在C++中,提高抽象的层次并不会降低效率。不然的话,人们宁可在低层次上编程,而不愿使用更便利的抽象,数据抽象也就失去了市场。后面我们将看到一个具体的例子。
模板与泛型
如果我写了一个IntVector,那么我不想再为double和string再实现一遍同样的代码。我应该把vector写成template,然后用不同的类型来具现化它,从而得到vector<int>
、vector<double>
、vector<complex>
、vector<string>
等具体类型。
不是每个ADT都需要这种泛型能力,一个Date class就没必要让用户指定该用哪种类型的整数,int32_t足够了。
根据上面的要求,不是每个面向对象语言都能原生支持数据抽象,也说明数据抽象不是面向对象的子集。
11.7.7 数据抽象的例子
下面我们看看数值模拟N-body问题的两个程序,前一个是用C语言,后一个是用C++语言。这个例子来自编程语言的性能对比网站。两个程序使用的算法相同。
C语言版,完整代码见recipes/puzzle/file_nbody.c,下面是核心代码。struct planet保存行星位置、速度、质量,位置和速度各有三个分量。程序模拟几大行星在三维空间中受引力支配的运动。
其中最核心的算法是advance()函数实现的数值积分,它根据各个星球之间的距离和引力,算出加速度,再修正速度,然后更新星球的位置。这个naive算法(简单直接的算法)的复杂度是O(N 2 ^{2} 2)。
struct planet
{double x, y, z;double vx, vy, vz;double mass;
};void advance(int nbodies, struct planet *bodies, double dt)
{for (int i = 0; i < nbodies; ++i){struct planet *p1 = &(bodies[i]);for (int j = i + 1; j < nbodies; ++j){struct planet *p2 = &(bodies[j]);double dx = p1->x - p2->x;double dy = p1->y - p2->y;double dz = p1->z - p2->z;double distance_squared = dx * dx + dy * dy + dz * dz;double distance = sqrt(distance_squared);double mag = dt / (distance * distance_squared);// 万有引力公式为F=GMm/r²// dx除msg中的distance是x方向上的分量// 万有引力在x方向的加速度是对方质量除r²(这里把G记为1),再乘x方向的分量,记为ax// ax乘mag里的dt就是x方向上速度的增量p1->vx -= dx * p2->mass * mag;p1->vy -= dy * p2->mass * mag;p1->vz -= dz * p2->mass * mag;p2->vx += dx * p1->mass * mag;p2->vy += dy * p1->mass * mag;p2->vz += dz * p1->mass * mag;}}for (int i = 0; i < nbodies; ++i){struct planet *p = &(bodies[i]);p->x += dt * p->vx;p->y += dt * p->vy;p->z += dt * p->vz;}
}
C++数据抽象版,完整代码见recipes/puzzle/file_nbody.cc,下面是其代码骨架。首先定义Vector3这个抽象,代表三维向量,它既可以是位置,又可以是速度。本处略去了Vector3的操作符重载(Vector3支持常见的向量加减乘除运算)。然后定义Planet这个抽象,代表一个行星,它有两个Vector3成员:位置和速度。需要说明的是,按照语义,Vector3是数据抽象,而Planet是object-based。
struct Vector3
{Vector3(double x, double y, double z): x(x), y(y), z(z){ }double x;double y;double z;
};struct Planet
{Planet(const Vector3 &position, const Vector3 &velocity, double mass): position(position), velocity(velocity), mass(mass){ }Vector3 position;Vector3 velocity;const double mass;
};
相同功能的advance()代码则简短得多,而且更容易验证其正确性(设想假如把C语言版的advance()中的vx、vy、vz、dx、dy、dz写错位了,这种错误较难发现)。
void advance(int nbodies, Planet *bodies, double delta_time)
{for (Planet *p1 = bodies; p1 != bodies + nbodies; ++p1){for (Planet *p2 = p1 + 1; p2 != bodies + nbodies; ++p2){Vector3 difference = p1->position - p2->position;double distance_squared = magnitude_squared(difference);double distance = std::sqrt(distance_squared);double magnitude = delta_time / (distance * distance_squared);p1->velocity -= difference * p2->mass * magnitude;p2->velocity += difference * p1->mass * magnitude;}}for (Planet *p = bodies; p != bodies + nbodies; ++p){p->position += delta_time * p->velocity;}
}
尽管C++使用了更高层的抽象Vector3,但它的性能和C语言一样快。看看memory layout就会明白。
C struct的成员是连续存储的,struct数组也是连续的,如图11-10所示。
尽管C++定义了Vector3这个抽象,但它的内存布局并没有改变(见图11-11),C++ Planet的布局和C planet一模一样,Planet[]的布局也和C数组一样。
另一方面,C++的inline函数在这里也起了巨大作用,我们可以放心地调用Vector3::operator+=()等操作符,编译器会生成和C一样高效的代码。
不是每个编程语言都能做到在提升抽象的时候不影响性能,来看看Java的内存布局。如果我们用class Vector3、class Planet、Planet[]的方式写一个Java版本的N-body程序,内存布局将会是如图11-12所示的样子。这样大大降低了memory locality,有兴趣的读者可以对比Java和C++的实现效率。
注:这里的N-body算法只为比较语言之间的性能与编程的便利性;真正科研中用到的N-body算法会使用更高级和底层的优化,复杂度是O(NlogN),在大规模模拟时其运行速度也比本naive算法快得多。
更多的例子
1.Date与Timestamp,这两个class的“数据”都是整数,各定义了一套操作,用于表达日期与时间这两个概念。
2.BigInteger,它本身就是一个“数”。如果用C++实现BigInteger,那么阶乘函数写出来十分自然。下面第二个函数是Java语言的版本。
// C++ code
BigInteger factorial(int n)
{BigInteger result(1);for (int i = 1; i <= n; ++i){resuilt *= i;}return result;
}
// Java code
public static BigInteger factorial(int n)
{BigInteger result = BigInteger ONE;for (int i = 1; i <= n; ++i){// 调用BigInteger.valueOf静态方法,将整数值i转换为对应的BigInteger对象result = result.multiply(BigInteger.valueOf(i));}return result;
}
高精度运算库gmp(它提供了大整数和大浮点数的高效算术运算功能,能够处理任意精度的数值计算)有一套高质量的C++封装(http://gmplib.org/manual/C_002b_002b-Interface-General.html#C_002b_002b-Interface-General)。
3.图形学中的三维齐次坐标(一种表示三维空间中点的坐标系统,它由四个数值组成,通常表示为(x,y,z,w),其中(x,y,z)是点在三维笛卡尔坐标系中的坐标,w是一个常数,一个点的坐标(x,y,z)可以表示为(wx,wy,wz,w),例如,三维空间中的点(1,2,3)可以表示为(2,4,6,2)、(3,6,9,3)等等)Vector4和对应的4×4变换矩阵Matrix4。
4.金融领域经常成对出现的“买入价/卖出价”,可以封装为BidOffer struct,这个struct的成员可以有mid()(中间价)、spread()(买卖差价)、加减操作符等等。
小结
数据抽象是C++的重要抽象手段,适合封装“数据”,它的语义简单,容易使用。数据抽象能简化代码书写,减少偶然错误。
在新写一个class的时候,先想清楚它是值语义还是对象语义。一般来说,一个项目里只有少量的class是值语义,比如一些snapshot的数据,而大多数class都是对象语义。
如果是对象语义的class,那么应该立刻继承boost::noncopyable,防止编译器自动生成的拷贝构造函数和赋值操作符在无意中破坏程序行为(作者认为C++最好修改语言规则,一旦class定义了析构函数,那么编译器就不应该自动生成拷贝构造函数和赋值操作符。似乎C++11已经做了类似的规定?(没有做类似规定))(比如防止有人误将对象语义的class放入标准库容器)。