Boolan(博览网)——C++面向对象高级编程(下)(第四周)

1. conversion function,转换函数

注意:

  1. 转换函数不写返回值, const 关键字通常都要加。
  2. 编译器先找是否有符合运算条件的“+”(发现没有相关定义),再看 f 是否能进行类型转换(有定义相应的转换函数,因此调用 operator double()将 f 转为 0.6)。

1.1 一个例子:

问:此处重载操作符“[ ]”是希望构建一个返回 bool 类型的 vector ,而返回值是 reference ,这又是如何做到的呢?
答: reference 是 __bit_reference 的别名,而 __bit_reference 这一结构体定义了 bool 类型的转换函数,因此在需要时可以让 reference 调用 operator bool()将其转化为 bool 类型。(流程见图中箭头)

2. non-explicit-one-argument ctor,非显式单实参构造函数

1 中是通过类型转换实现 d = 4 + f 的运算的,而此处要介绍的是另外一种实现方法(运算符重载):

编译器先找是否有符合条件的“+”,发现有相应的重载运算符,此时就需要将4通过构造函数构造成 Fraction 类型。

注意图中蓝色部分的构造函数有些特殊:它接受两个参数,但是第二个参数已经有了自己的默认值,因此在实际创建对象的时候,我们可以只设置第一个参数的初值(one-argument 意即只要一个参数就够了)。这样就可以实现上述运算了:

  1. 调用 non-explicit ctor 将 4 转为 Fraction(4,1)
  2. 调用 operator+,完成 d2 = f + 4 的运算(注意此处的 d2 类型为 Fraction,而 1 中为 double)

2.1 conversion functions vs. non-explicit-one-argument ctor

当我们在结构体中提供了两种实现方法时,由于它们并无优先级之分,编译器不知该用哪种方法,便产生了 ambiguous error (歧义的,二义的):

2.2 explicit-one argument ctor

关键字 explicit (显式,明确的)只用在构造函数的前面,告诉编译器只有明确使用构造函数形式,其才会被调用,不允许自动调用。

当我们加上 explicit 后,上述情形又会变成怎样呢?

这里,编译器依然去先寻找“+”号的重载,发现所定义的“+”号的重载并不符合要求(“+”号右边需要一个Fraction类型)。编译器便再看 f 是否能进行类型转换,发现有定义相应的转换函数,因此调用 operator double()将 f 转为 0.6, f + 4 变为 4.6 。但是因为不能自动调用构造函数将 double 类型的 4.6 转换为 Fraction 类型赋给 d2 ,因此编译器报错(如图)。

3. pointer-like classes

3.1 智能指针(smart pointer)

在C++中,动态内存的管理是通过一对运算符 newdelete 来完成的。动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极其困难的。有时我们会忘记释放内存,在这种情况下就会产生内存泄漏;有时在尚有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存的指针。

为了更容易(同时也更安全)地使用动态内存,智能指针(smart pointer)应运而生。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。 shared_ptr 是其中一种,允许多个指针指向同一个对象; unique_ptr 则“独占”所指向的对象。它们都定义在 memory 头文件中。

这里以 shared_ptr 为例简单介绍下 pointer-like class 的设计:

注意:

  1. 智能指针类中必然包含一个普通指针
  2. 智能指针类中一定要带 “*” 和 “->” 的操作符重载函数(这样才能跟普通指针一样使用)
  3. “->”的特殊性:当 “sp->” 返回 “px” 后自动生成“->”,因此 “sp->method()” => “px->method()”

图中以算式 new 动态配置一个默认构造的 Foo 对象,并将所得结果(一个原生指针)作为 shared_ptr<Foo> 对象 sp 的初值(注意,shared_ptr 角括号内放的是“原生指针所指对象”的类别,而不是原生指针的型别)。然后创建了一个 Foo 对象 f,调用其拷贝构造函数拷贝了 sp 指向的对象。

3.2 迭代器

类似于指针类型,迭代器也提供了对对象的间接访问:

图中先创建一个 Foo 类型的迭代器 ite ,*ite 就是返回迭代器 ite 所指元素的引用(获得一个 Foo object), ite->method() 意即调用 Foo::method() ,其相当于 (*ite).method(),也相当于(&(*ite))->method。(node 是 link_type 类型的,它是 __list_node<Foo>* 的别名,而 __list_node 中数据成员 data 的类型就是 Foo,因此(*node).data 就是一个 Foo object)

4. function-like classes,所谓仿函数

其在STL历史上有两个不同的名称:仿函数(functors)是早期的命名,C++标准规格定案后所采用的新名称是函数对象(function objects)。

就实现意义而言,“函数对象”比较贴切:一种具有函数特质的对象。不过,就其行为而言,以及就中文用词的清晰漂亮与独特性而言,“仿函数”一词比较突出。它在调用者可以像函数一样地被调用(调用),在被调用者则以对象所定义的function call operator 扮演函数的实质角色。

要将某种“操作”当做算法的参数,唯一办法就是先将该“操作”(可能拥有数条以上的指令)设计为一个函数,再将函数指针当做算法的一个参数;或是将该“操作”设计为一个所谓的仿函数(就语言层面而言是个class),再以该仿函数产生一个对象,并以此对象作为算法的一个参数。

既然函数指针可以达到“将整组操作当做算法的参数”,那又何必有所谓的仿函数呢?原因在于函数指针毕竟不能满足 STL 对抽象性的要求,也不能满足软件积木的要求——函数指针无法和 STL 其它组件(如配接器 adapter)搭配,产生更灵活的变化。

5. namespace,命名空间

大型程序往往会使用多个独立开发的库,这些库又会定义大量的全局名字,如类、函数和模板等。当应用程序用到多个供应商提供的库时,不可避免地会发生某些名字相互冲突的情况。多个库将名字放置在全局命名空间中将引发命名空间污染(namespace pollution)。
传统上,程序员通过将其定义的全局实体名字设得很长来避免命名空间污染问题,这样的名字中通常包含表示名字所属库的前缀部分:
class cplusplus_primer_Query { ... };
string cplusplus_primer_make_plural(size_t, string&);
这种解决方案显然不太理想:对于程序员来说,书写和阅读这么长的名字费时费力且过于繁琐。

命名空间(namespace)为防止名字冲突提供了更加可控的机制。命名空间分割了全局命名空间,其中每个命名空间是一个作用域。通过在某个命名空间中定义库的名字,库的作者(以及用户)可以避免全局名字固有的限制。

只要能出现在全局作用域中的声明就能置于命名空间内,主要包括:类、变量(及其初始化操作)、函数(及其定义)、模板和其他命名空间:

namespace cpluscplus_primer {
    class Sales_data { /* ... */ };
    Sales_data operator+(const Sales_data&, const Sales_data&);
    class Query { /* ... */ };
} // 命名空间结束后无须分号,这一点与块类似

命名空间既可以定义在全局作用域内,也可以定义在其他命名空间中,但是不能定义在函数或类的内部。

作业中的一些总结:

题目:分别给出下面的类型Fruit和Apple的类型大小(即对象size),并通过画出二者对象模型图以及你的测试来解释该size的构成原因。

class Fruit{
   int no;
   double weight;
   char key;
public:
   void print() {   }
   virtual void process(){   }
};
   
class Apple: public Fruit{
   int size;
   char type;
public:
   void save() {   }
   virtual void process(){   }
};
  1. 因为存在虚函数,所以Fruit和Apple中都有虚指针vptr,指向虚表vtbl。
  2. Apple继承了Fruit,也因此继承了Fruit中所有的数据成员。
  3. 此题目中要考虑的内存对齐问题:(1)第一个数据成员放在offset为0的地方,之后的每个数据成员存储的起始地址要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int型在32位机占用4字节,则要从4的整数倍地址开始存储int类型的数据成员);(2)类的总大小(即对象size),必须是其内部占用空间最大的成员所占用的字节数的整数倍,不足的要在末尾补齐。
  4. 综上所述,可得到如下运行结果及对象模型图:(32位机 GCC)

字节对齐的细节和编译器实现相关,但一般而言,满足三个准则:
1. 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2. 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
3. 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。
摘自:http://blog.csdn.net/yy13210520/article/details/6841052

推荐阅读更多精彩内容