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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,924评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,902评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,716评论 0 239
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,783评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,166评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,510评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,784评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,476评论 0 196
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,196评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,459评论 2 243
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,978评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,321评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,964评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,046评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,803评论 0 193
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,530评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,420评论 2 265

推荐阅读更多精彩内容