《深度探索C++对象模型》学习笔记(1-4章)

今年静下心来重读了侯捷老师译本的Lippman大神作品《Inside the C++ Object Model》,记录下学习过程中的疑问和心得有助于以后……面试。不是开玩笑,这本书里面随便找一段出来都可以当作C++面试题,学透彻了的话,无论是面试别人或者被人面试都是相当犀利的武器。

第1章 对象

对象

OOP的成本

  • 实际上相对于struct来讲,class只是程序员友好的手段
  • 单纯使用class以及继承,内存成本并不会增加,成员函数也会由编译器进行转换(4.1函数成员:调用)
  • 真正需要额外成本的是virtual特性。

对象布局方式:简单对象模型

  • 思想:尽量降低实现的复杂度
  • 每个对象中的项目都是一个slot,按照顺序排列
  • 数据成员本身并不放置在对象中,只保存指针,避免变长对象出现
  • 没有用于实际产品中(但成为指向成员的指针的灵感来源)

对象布局方式:表格驱动的对象模型

  • 对象保存两个指针,一个指向拥有所有数据成员指针的表格,一个指向拥有所有成员函数指针的表格
  • 也没用于实际产品中(但指向函数指针表格的指针,演化为vptr)

对象布局方式:C++对象模型

  • 演化自简单对象模型
  • 非静态成员存储于类内。静态的则都位于之外。
  • vptr和vtbl的概念。用于支持RTTI的type_info位于vtble第一个slot内

继承的情况

  • 简单模型:子类描述基类的slot保存基类指针,优点是父类的修改不会影响子类
  • 另一模型:保存一个基类表格,子类拥有一个bptr指向之。表中的slot再指向具体的基类

差异

关键字差异

  • class和struct的思考:为了兼容C而多做了很多工作
  • struct表现数据集合体,而class为OOP而生
  • class的多个section不能保证成员在内存中的顺序和声明的一样
  • struct目前的作用,抽取class的一部分作为参数传给C函数

对象差异

  • 三种programming paradigms
    1. procedural model,C语言的函数调用
    2. ADT model,隐含的调用,例如等号运算符
    3. object-oriented model,面向对象特性
  • 多态的实现需要由指针或引用来实现,但C++中的指针和引用并非多态的必然结果
  • 支持多态的方法
  • 指针的转换(基类子类)
  • 虚函数调用
  • dynamic_cast或者typeid运算符

指针和多态

  • void*虽然可以指向某地址,但并不清楚具体类型,也不能通过它操作对象
  • D继承自B,D的大小为B的子对象加上D特有的部分
  • D*和B*的指针对象虽然都指向同一个开始地址(B的开头),但它们涵盖的范围不同
  • 指针在编译期会决定固定的可用接口(public)以及接口对应的访问范围,这类信息都在链接(link)中,它位于vptr和其指向的vtbl之间
  • 编译器在构造和赋值时候会决定vptr的值。如果对象含有多于1个的vptr,那么就不用基类指针进行覆盖(5.4构析拷贝:拷贝赋值)

Read more >>>

指针参数传递和.Net中String类型的表现

本来不想对这两个老掉牙的问题进行讨论,但联系起来看确实有用。先看代码:

    class Program {
        static void Foo( string s ) {
            s = "变了";
        }

        static void Main( string[] args ) {
            string s = "原始String";
            Foo( s );
            Console.WriteLine( s );
            Console.ReadKey();
        }
    }

没错,这就是最经典的“作为引用类型的String类传参时候表现值类型特点”的例子。上述程序意料之中输出为“原始String”,但其实这并不是因为String自身特殊到所谓“引用类型表现值类型特点”,为什么这么说?看下面这个例子:

    class Person {
        public String Name;
    }

    class Program {
        static void Foo( Person p ) {
            p = new Person { Name = "李四" };
        }

        static void Main( string[] args ) {
            Person p = new Person { Name = "张三" };
            Foo( p );
            Console.WriteLine( p.Name );
            Console.ReadKey();
        }
    }

这次就清楚多了,基本都能看出输出的是“张三”,关键在于自定义class用了new这个关键字、而String类的等号便是最大的迷惑点。所以说String类在当作参数传递的时候和其他引用类型并无二致。

作为本地参数的s和p同原来的变量是完全不同的引用存在,只不过引用到的是同一块内存。使用new或者重载的=进行新内存分配后,原来的变量指向并没有发生变化。

好,那么就引出标题里面的第一个问题了“指针参数传递”,在C/C++中很容易见到这样的错误:

void Foo( int* a, int* b )
    {
    int* tmp = a;
    a = b;
    b = tmp;
    }

int main(int argc, char *argv[])
    {
    int a = 0;
    int b = 5;
    Foo( &a, &b );
    cout<<a<<","<<b<<endl;
    }

对于形参a和b而言,它们只是在栈上对于原本变量的一份拷贝,指向同一块内存,改变它的指向是没有意义的。

a(变量) --|
         |--->  0
a(参数) --|

如果没有在这上面碰过钉子的请受我一拜。如果希望改变原有指针的内容,需要直接改变被指向区域的值或者传递二级指针:

void Foo( int* a, int* b )
    {
    int tmp = *a;
    *a = *b;
    *b = tmp;
    }

void FooAlloc( int** p, int size )
    {
    *p = malloc( sizeof( int ) * size );
    }

可以说这个问题和开始提到的String的特殊表现殊途同归:

  • 归根结底,不管是指针还是引用,当作参数进行传递的时候仍然是值。
  • 指针本身的操作和指针指向内容的操作是不同的,星星太多就容易乱。同理的还有const int* i和int* const i。

还是那句话,String并不特殊,它在参数传递上表现得和其他引用类型没什么不同,但不同点在于String是一个immutable类型,所有对其进行的修改,例如SubString、连接(+)、赋值(=)等等,都会当作创建新拷贝的操作对待,这也就是导致String表现出类似于值类型特点的原因。

参考资料

Windows Phone上为WebBrowser增加链接的Context Menu

(似乎这篇文章被垃圾评论认准了,所以无奈关评)

问题

对于浏览器而言,Context Menu使用起来是用户操作页面元素最直接、同时也是对页面执行各种操作中路径最短的。下面这个是系统自带浏览器的效果:

可惜如大家所见,WebBrowser控件不支持这个功能。原本以为会有个OnLinkPressed或者OnImageSelected之类的回调供开发者使用,结果到了Mango一看,还是没有。但前两天下载了UC浏览器,发现它实现了这个功能:

从效果来看,很显然微软没有给UC开小灶,而是UC的工程师凭借自己努力搞出来的,原因有二:

  • 这个菜单只有在页面完全载入后才能按出来
  • 菜单的触发灵敏程度不稳定,有的时候长按N秒没有反应;有的时候手指移动到链接上动一下就出来了。

实现

据此猜测,UC浏览器用的是注入Javascript的方式实现上下文菜单的互动的。OK,在(我力所能及的)其他方法行不通的情况下,那我们跟也根据这个思路去实现这个功能。既然目标明确,那么需要实现的具体功能也很明确:

  • 用户长按页面链接(暂且只支持链接,图片之类可以举一反三)时,WebBrowser可以通知code behind事件发生
  • 事件通知的同时还需要带上所点击链接的文本和Url属性
  • 在用户所长按位置出现Context Menu,点击具体的Menu Item则利用文本和Url进行后续操作(例如新页面打开、加入收藏等)

和桌面的.net 平台一样,WP7的WebBrowser控件也是通过Navigate方法和Navigated回调进行页面控制的。对于前端页面事件回调,自然率先想到的就是WebBrowser::ScriptNotify()回调,试一下如何?在页面里面写入这样的内容(刚发现blog没装代码高亮插件……):

<a href="#" onclick="javascript:window.external.notify('hi')";>你好</a>

后台绑定方法:

private void webBrowser_ScriptNotify(object sender, NotifyEventArgs e)  {
    MessageBox.Show( e.value.ToString() );
}

效果不错。继续考虑实现,注入Script则使用InvokeScript。这个方法有两个版本,分别带有1个和2个参数,前者直接执行页面中的函数;后者则是带有参数执行。很显然我们要操作的是外部的网页,并非自己的HTML页面,显然外部的页面是不会如此好心帮你给每一个<a>加上需要的代码执行事件,那么此时就需要Javascript一个特殊的函数eval。eval函数带有一个参数,可以将传入的字符串当作代码执行。很好,于是我们可以用InvokeScript两个参数的版本:

// 函数声明不写了,我把这个函数命名为InjectScripts()
                String script =
                @"
                window.onLinkPressed = function() {
                    var elem = event.srcElement;
                    if ( elem != null ) {
                        window.external.notify( elem.getAttribute('href') + '|' + elem.innerHTML );
                    }
                    return false;
                };

                window.doBindLinks = function() {
                    var elems = document.getElementsByTagName('a');
                    for (var i = 0; i < elems.length; i++) {
                        var elem = elems[i];
                        elem.attachEvent('onmousedown', onLinkPressed);
                    }
                };";
            webBrowser.InvokeScript( "eval", script );
            webBrowser.InvokeScript( functionName );

// 可以在页面初始化时候绑定好,页面载入后注入脚本
            webBrowser.Navigated += ( s, e ) => {
                if ( e.Uri.OriginalString.StartsWith( "http" ) ) {
                    InjectScripts();
                }
            };

第一次InvokeScript时候用eval函数执行我们在代码中写好的JS脚本,将需要的方法attach到window节点上。而第二次InvokeScript直接执行需要的方法,为每一个<a>绑定点击事件处理函数。

运行后达到了我们的预期效果。对于ScriptNotify而言,在Mango版本前,从外部通过eval注入的方法调用window.external.notify()是不好用的。也就是说,只有页面中静态的Javascript才有能力回调code behind。而谢天谢地谢M$,Mango里面终于可以这样做了,省了不少事情。另外这里需要注意的是attachEvent方法所绑定的事件为onmousedown,而不是onclick。

如果希望在Windows Phone 7.0上实现同样的效果,可以走前辈给的指路:在Understanding Web Browser Control of Windows Phone 7 – Part 1一文中的实现方法为注入的Script保留一个window节点上面的全局变量,保存链接后得到的属性。code behind内则有一个每1秒执行一次的Timer去不停地抓取这个属性,发现值不为空情况下则当作用户对链接进行了操作。

从这一点也可以见得别看今天我们费了很大力气才能实现这么个功能,到下一个版本微软一边说“嗟,来用”一边开放了相关接口,实现相同的功能可能就是分分钟的事情了。

接下来只剩下Context Menu的UI展现,参考WP7 ContextMenu in depth | Part1: key concepts and API,下载Silverlight Tookit for Windows Phone,导入程序集,将ContextMenu扔到WebBrowser元素里面:

<phone:WebBrowser x:Name="webBrowser" IsScriptEnabled="False">
    <toolkit:ContextMenuService.ContextMenu>
        <toolkit:ContextMenu x:Name="mnuContext" IsZoomEnabled="False" >
            <toolkit:MenuItem Header="新窗口中打开" Click="MenuItem_Click" />
            <toolkit:MenuItem Header="链接文本" Click="MenuItem_Click_1" />
        </toolkit:ContextMenu>
    </toolkit:ContextMenuService.ContextMenu>
</phone:WebBrowser>

后台绑定好Menu Item的事件,直接拿到在ScriptNotify里面保存的链接信息即可,至此功能完成。看看效果:

实现比较简单,代码就不扔了。题外话,利用InvokeScript还可以做很多事情,例如去掉干扰页面正常打开的target=”_blank”等。同样对于本文的应用而言,可以将不同的链接绑定不同的Context Menu,实现更加丰富的功能。

参考资料

新浪、腾讯和人人网三家OAuth和API问题相关

最近工作中需要用到网站帐号绑定、OAuth授权相关的功能,所以尽管本意是不想接触OAuth这个明显是“程序员折磨程序员”类型的技术,但也只能硬着头皮去解决问题。今天决定把之中遇到的麻烦和解决过程都记录下来,以免再有人走弯路。

在此之前,推荐大家阅读arthurchenjs的文章从实现iPhone的OAuth封装看国内互联网和开放平台,写得太好了,基本能扫清所有障碍。如果看完之后还有问题没解决、或者你觉得iPhone平台和其他平台实现有区别的话,再继续看我这篇吧。

如果你之前没接触过OAuth,那么最好还是阅读下RFC 5849 – The OAuth 1.0 Protocol,这是OAuth协议的非正式RFC文档。其实所有陌生的技术在了解之初最方便、最省事的方法就是读协议——细化到互联网和IT界,自然是RFC最大。虽然看起来文档协议什么的太长(现在很多人似乎逛BBS逛久了再加上微博兴起,网上文章超过一屏就懒的看)耗费时间,但其实这是最短路径。

然后选择OAuth实现库——没错,我们最好别自己造方形轮子。OAuth协议实现细节比较碎,自己实现费时费力,选择现有的库是最佳方案。但一定要注意所选择代码的协议,例如是否可以不开源、商业应用等。Java平台推荐用SINGPOST,架构灵活,扩展方便,代码量适中。基于同样的原因在iPhone平台上可以选择ShareKit,但我对这个不熟悉,不多说。

新浪微博、腾讯微博和人人网中,前两家用的OAuth 1.0,人人网则使用2.0(因为twitter用1.0而facebook用2.0?)。SIGNPOST的实现基于1.0,所以需要进行一定的改写,或者进行再次封装。

基于同一协议实现的不同网站API使用过程中,会出现有的好用有的却不好用的情况,显然是细节的不同。新浪微博和腾讯微博虽然都声称基于RFC 5849开发,但其明显的区别有:

  1. 用OAuth进行帐号认证时候,新浪微博的验证参数需要通过Header中Authorization域进行传递。腾讯微博则只支持url参数传递。这个地方我当时卡了半天。
  2. 对于桌面客户端,获取verifier时候,新浪微博callback url要传递oob,然后自己从网页中抠那个六位数出来;腾讯微博要传递null,跳转的页面url中会带有verifier参数。
  3. 发送内容的时候,验证参数的位置和OAuth认证时候一样。
  4. 这也是最重要的不同点,在计算Signature Base String时候,新浪微博的参数不需要在组合前进行URL编码,而该死的腾讯必须编码,也就是说,组成SBS时候腾讯微博的参数值实际上被URL编码两次,举例来说就是SBS中content部分的值会有大量的%25字符出现(也就是百分号编码后)。但发送的时候,却只编码一次。我无法理解SBS内容和实际POST出去的内容为什么要不同?但事实就是这样,至于你信不信,试一下就知道了。

在这里顺便对腾讯微博的API文档和不靠谱的论坛技术支持表示无语,API文档里面基本每页都有拼写错误;返回的错误码在文档里找不到说明;甚至于最关键的POST图片时候的包体说明竟然是空白的。

人人网用的OAuth 2.0,大大简化了步骤,自己实现起来速度也很快。不过人人网的文档和API测试机制很扭曲。文档太乱,虽然是Wiki形式但完全看不出组织结构,通常一段内容会在多个页面出现,希望能下功夫整理一下。为什么说测试机制很扭曲,是因为它每个高级接口使用时候都必须经过审核,并且在审核通过前只能用测试帐号进行测试。这个测试帐号的好友数还不能多于5个。但总体来说人人网的API开放会让人觉得不错,是因为出现错误时候返回的信息,准确而且详细。

Befunge语言和文言文编程

Project Euler通关的时候有个题用遍历循环过了后觉得应该有更好的方法,于是去论坛看别人的解法,结果发现了一种变态语言Befunge。去维基看了看:

这门语言由Chris Pressey在1993年创造,本意为设计一种尽量难编译的语言……结果马上出现了一批编译器。

还真是够欠啊……简单来说这种语言以二位形式书写,程序维护一个指令指针(IP)指向当前执行的指令,用^v<>作为指针移动的依据、四则运算则是直接取出两个栈上的值进行运算。和汇编一样,代码中出现的任何符号可以是数据也可以是指令,甚至可以是注释(不用特别标识,只要永远不执行即可)。比如下面这段代码就是寻找第10001个素数(来自于Jarjar):

25*:*:*1-20p 300p030p040p  v
     @.g04<v        `g02g03<
          |<        v        g00    <
      vg00<>00g:40p > 2+00p^
     >        :10p1-!v!-1p01:-1g01<
           ^ p03+1g03_00g10g%    #^_^
      >93*\: 50p60p>50g:60g\/v
                   |:-1p05/2+<
     ^        $\g05<

看完这让人斯巴达的语言后我想了想中文编程内有没有类似的恶搞语言呢?基于Whitespace的“草泥马语言”算是一个,当年看到时候笑到敲地。然后突然想到文言文编程何如?没想到已经有高人完成了:唐鳳,被称为计算机怪才的Perl Guru,致力于用Haskell实现Perl 6的自由程序员。她的故事不赘述,想了解的话可以google一下,我们这里只看看PerlYuYan。文言文的Perl写出来像是下面这样,有兴趣可以去CPAN上下载回来玩玩。

#!/usr/local/bin/perl

use Lingua::Sinica::PerlYuYan;

用警兮用嚴。

印道
一至一
哉兮

印編曰雜申雜申矣
  又纖曰龍鼠矣
    又曰一矣

亂曰
國無人莫我知兮    又何懷乎故都
既莫足與為美政兮  吾將從彭咸之所居