廊坊住房和城乡建设厅网站,门户网站开发招标,多平台网站开发,网站建设 小白11.7.2 值语义与生命期
值语义的一个巨大好处是生命期管理很简单#xff0c;就跟int一样——你不需要操心int的生命期。值语义的对象要么是stack object#xff0c;要么直接作为其他object的成员#xff0c;因此我们不用担心它的生命期#xff08;一个函数使用自己stack上…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拥有ChildChild的生命期由其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_ptrChild myChild;
};在上面这个设计中Child的指针不能泄露给外界否则仍然有可能出现空悬指针。
如果Parent与Child的生命期相互独立就要麻烦一些
class Parent;
typedef boost::shared_ptrParent ParentPtr;// class的默认继承方式是private
// struct的默认继承方式是public
class Child : boost::noncopyable
{
public:explicit Child(const ParentPtr myParent_): myParent(myParent_){ }private:boost::weak_ptrParent myParent;
};typedef boost::shared_ptrChild ChildPtr;class Parent : public boost::enable_shared_from_thisParent,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_ptrweak_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 ArrayListChild myChildren;
}public class Child
{private Parent myMom;private Parent myDad;
}如果用C来实现如何才能避免出现空悬指针同时避免出现内存泄漏呢借助shared_ptr把裸指针转换为值语义我们就不用担心这两个问题了
class Parent;
typedef boost::shared_ptrParent ParentPtr;class Child : boost::noncopyable
{
public:explicit Child(const ParentPtr myMom_, const ParentPtr myDad_): myMom(myMom_), myDad(myDad_){}private:boost::weak_ptrParent myMom;boost::weak_ptrParent myDad;
};
typedef boost::shared_ptrChild 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_ptrParent mySpouse;std::vectorChildPtr 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 assigningunique_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 copyC不得不定义RVOReturn Value Optimization返回值优化当函数返回一个局部对象非引用时通常会触发一个拷贝构造函数将局部对象的副本返回给调用者RVO通过直接在调用者的内存中构建返回值而不是创建一个局部对象再拷贝从而避免了不必要的拷贝操作来解决性能问题。因为函数返回int时是make a copy。
8.以class type为成员时数据成员是嵌入的。例如paircomplexdouble, size_t的layout就是complexdouble挨着size_t。
这些设计带来了性能上的好处原因是memory locality。比方说我们在C里定义complexdouble classarray of complexdoublevectorcomplexdouble 它们的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 typeADT”一种模型它定义了数据类型的操作和规定了这些操作的行为而不关注具体的实现细节想必如雷贯耳。
“支持数据抽象”一直是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脱离关系。
比方说
stackint a;
a.push(10);
stackint 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然后用不同的类型来具现化它从而得到vectorint、vectordouble、vectorcomplex、vectorstring等具体类型。
不是每个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);// 万有引力公式为FGMm/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-11C 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定义了析构函数那么编译器就不应该自动生成拷贝构造函数和赋值操作符。似乎C11已经做了类似的规定没有做类似规定比如防止有人误将对象语义的class放入标准库容器。