zoukankan      html  css  js  c++  java
  • c++函数学习-关于c++函数的林林总总

     

    本文是我在学习c++过程中的一些思考和总结,主要是c++中关于函数的林林总总。欢迎大家批评和指正,共同学习。

    os version: ubuntu 12.04 LTS
    gcc version: gcc 4.6.3
    文中以 $ 开头语句表示 shell command

    0.this 指针

    我觉得首先得讲明白这个东东,让大家明白c++中函数与c语言中函数的区别
    什么是 this 指针? 这里我直接选自 ISO c++ 中关于 this 定义(注:我会大量援引ISO c++,相信大家应该都看得懂,哈哈)

    In the body of a non-static member function, the this is a prvalue expression whose value is a address of the object for which the function is called.

    这个定义里面,我们需要注意两点:
    (1) 只有类中的 non-static member function 才有隐含的 this 指针,这样 类中的 static 函数、不属于类的全局函数 都没有 this 指针

    (2) this 是常指针,一直指向调用该函数的类对象,其指向(地址值)不可更改,即 Widget* const this

    复制代码
    class Widget {
    public:
        int fun() { return a + b; }
    private:
        int a;
        int b;
    };
    复制代码

    c++编译器会将 member function 转化为对等 non-member function
    a.改写函数原型,安插额外的隐含参数到 member function,用以提供一个存储通道,使得该类的所有对象都可以调用该函数

    int Widget::fun(Widget* const this)

    b.将每一个 non-static data member 的存取操作改写成 经由 this 指针来存取

    int Widget::fun(Widget* const this) {
        return this->a + this->b;
    }

    注:只有属于类并且 non-static 的 data member 才由 this 指针来存取,如果这样:int fun(int x, int y) { return x + y; }, 则会如下改写:

    int Widget::fun(Widget* const this, int x, int y) {
        return x + y;
    }

    c.将 member function 重新写成一个外部函数,对函数进行 "name mangling"处理,使它在程序中成为独一无二的词汇
    注: name mangling 确保每一个符号都有唯一的名字,函数重载部分我会详细讲讲这个
    我们对示例代码 cpp_function.cc :

    $ g++ cpp_function.cc -o cpp_function.o -W -g -c -std=c++0x

    我们对生成的 cpp_function.o: 

    $ nm cpp_function.o | grep -E fun

    可以将 "name mangling"视为编码过程,当然有对应的解码过程"demangling"

    $ c++filt _ZN6Widget3funEv

     

    d.现在假如我们调用 fun 函数:
    成员调用符:widget.fun(); 实际上是: fun(&widget);
    指针调用: pwidget->fun(); 实际上是: fun(pwidget);

    知识小结:

    0.1 任何一个non-static data member 都是且只能通过this指针存取的

    0.2 编译器在每个 non-static 的 member function的第一个参数都安插一个隐藏this指针参数

    0.3 调用 non-static non-virtual 的 member function:

      // non-virtual function 调用不需要通过this寻址,而是通过编译器的符号表
      widget.fun()   ==>  Widget_3fun(&widget);  //编译器安插一个this指针
      pwidget->fun() ==>  Widget_3fun(pwidget);  //编译器安插一个this指针

    0.4 调用 virtual member function:

      // virtual function 调用首先需要通过this指针、vptr虚表指针寻址
      widget.vfun()   ==>  (*(widget.vptr[1])) (&widget) //&widget为this指针,1为vfun虚表下标
      pwidget->vfun() ==>  (*(pwidget->vptr[1])) (pwidget) 

    0.5 调用 static member function:

       static member function 无 额外安插的this指针,因此:
       a.不能存取 non-static data member (这些data必须通过this指针存取)
       b.不能为 virtual function (virtual function都是有 this 有vptr的)
       c.不能为 const member function(const member function 都是 const Widget* const this)
       widget.sfun()  ==>  Widget_4fun();  // 无额外安插的this指针
       pwidget.sfun() ==>  Widget_4fun();  // 无额外安插的this指针

    好吧,this 指针这部分暂时告一段落吧

     

    1. inline 函数

    可能有的同学觉得 inline 函数很简单,其实这里面还是有点门道的
    当函数被声明 inline 函数之后,编译器可能会将其内联展开,无需按照通常的函数调用机制调用 inline 函数
    这里我重点突出了两个字"可能",难道我将一个函数声明为 inline,编译器敢违背我命令不进行内联展开吗? 是的,完全可能
    inline 声明对编译器来说只是一个建议,编译器可以选择忽略这个建议

    优点: inline 函数可以避免函数调用的开销,令目标码更加高效
    缺点:inline 一个很大的函数将增加目标码大小

    究竟哪些函数应该声明 inline?
    Google c++ 编程规范里面: 只将小于10行的函数进行 inline
    (1)编译器隐式地将在类内定义的成员函数当作 inline 函数(注:建议在类定义函数时 显式声明为 inline,喜欢她就要最大声最直白的表达出来,哈哈)
    (2)inline 函数应该在头文件中定义,确保调用函数时所使用的定义是相同的,并且编译器对 inline 函数的定义保持可见
    (3)不要将内含循环语句的函数 inline
    (4)大多数编译器不支持递归函数 inline 
    (5)大多数编译器不支持 virtual 函数 inline (因为 virtual 函数意味着 运行时确定调用,而 inline 函数需要编译时内联展开)
    (6)编译器一般不支持 构造函数 和 析构函数 inline (因为这两个函数实际上做的工作比我们想象的多,尤其是涉及到继承时)
    (7)编译器一般不支持 "通过函数指针而进行的调用" 实施 inline 
    c++ sort 通过函数对象(将 operator()函数 inline)进行比较 快于 c语言中qsort通过自定义comp函数指针进行比较 (详见Effective STL 第46条)

     

    2. 函数重载

    ISO c++: Two delarations in the same scope that declare the same name but with different types are called overloaded

    a. 一定要出现在相同作用域
    b. 函数具有相同的名字但是函数原型一定不相同
    什么是函数原型? 

    函数原型 = 函数名 + 函数参数个数 + 函数参数类型

    这里我们可以知道 仅仅靠 函数返回值类型不同 或者 函数参数名称不同 都不能 构成 重载
    我们在第0部分提到过: 编译器通过 "name mangling" 确保每一个符号都有唯一的独一无二的名字

    class Widget {
    public:
        int fun(int a, int b);
        int fun(double a, double b);
        int fun(int a, int b, int c);
    };

     

    我们由图中可以看到 经过 name mangling 过后,每个符号名字带有:

    类名信息、函数名长度、函数名、所有参数的第一个字母缩写

    如果我们 添加诸如: int fun(int c, int d); double fun(int a, int b) 都是编译错误

    我们再来看看这3组:

    复制代码
    //第一组
    int fun(int a);
    int fun(const int a);
    
    //第二组
    int fun(int* a);
    int fun(const int* a);
    
    //第3组
    int fun(int& a);
    int fun(const int& a);
    复制代码

     

    上图中三次编译分别依次对应于上述三组情况
    我们可以看到:
    第1组编译错误,不能重载
    第2组构成重载,通过符号名字可以大概猜测到: P代表 pointer,K代表 const
    第3组构成重载,通过符号名字可以大概猜测到:R代表 reference, K代表 const

     

    首先我说说 c++中的值语义和对象语义,这部分具体可以详见 http://www.cnblogs.com/solstice/archive/2011/08/16/2141515.html

    值语义: 对象的拷贝与原对象无关,两个对象拷贝之后互相分离,彼此无关。c++的内置类型(bool/int/double/char)都是值语义
    eg:int a; int b; a = b; //b值赋值给a后,a与b彼此分离
    对象语义:对象拷贝之后与原对象并不分离,而是共享同一资源。c++指针、引用、含有各种资源(内存、文件描述符、socket、TCP连接、数据库连接等)的对象
    eg:int* a; int* b; b = a;    //b值(地址)赋值给a后,a与b并未分离,而是共同指向同一地址

    引用也是如此,作用在引用上的所有操作事实上都是作用在该引用绑定的对象上。其实引用最终是靠指针实现的.

    好了,我们再来说这3组
    (1)第一组,根据编译器给出的错误提示,感觉编译器直接把 const 吞了,有没有 const 都一样,好吧,const 在这里好没存在感,哈哈
    第一组参数既不是指针,也不是引用,绝对的值语义对象。现假设传入实参 int b = 1; 
    两个函数都发生对实参对象的拷贝,拷贝之后,实参与形参分离,两个函数都是在操作实参的拷贝,而且这个拷贝已经跟实参没有任何联系了
    这样一来,这两个函数对于 实参 来说,具有完全相同的语义,并无本质区别,编译器当然得制止这种行为,不然显得太弱智了...

    (2)第二组和第三组的原因其实相同,这里我只解释第2组
    通过上面介绍,我们知道第2组传递的参数具有对象语义. 现假设传入实参 &b
    第一个函数发生如下的实参拷贝: int* a = &b;
    第二个函数发生如下的实参拷贝: const int* a = &b;(注:non-const 对象地址可以赋值给 const 指针,隐式转换. 只能 non-const ==> const 反之错误)
    拷贝之后,实参与形参没有分离,而是共同指向同一地址
    但是,这两个函数有不同的语义:
    第一个函数可以通过形参a 更改 实参b的值,比如:*a = 2; //这时 实参b所指元素变成了2
    第二个函数不能通过形参a 更改 实参b的值,因为 const int* a(a is a pointer to const int),a所指元素为 const,不可修改
    这样一来,这两个函数对于 实参 来说,具有不同的语义(一个可以改变实参,另一个不可以),有本质区别,编译器得允许这种行为.

     

    3. const member function
    为什么我会在将函数重载之后将这个呢? 哈哈,当然是有因果关系!

    class Widget {
    public:
        int fun(int a) {...}
        int fun(int b) const {...}
    };

    当我们写出这样的代码,编译器竟然没有抱怨出错,为什么? 函数名和参数列表(参数名称和参数个数)都相同,为什么没有违反函数重载规则??
    编译器处理后的结果:

    注意 const member function 经过 "name mangling"之后多了一个"K".

     

    首先 我们来看看 const member function 的语义: const member function 不能修改调用该函数的对象
    编译器是怎样保证这种特性的呢? 答案是 this 指针

    第0部分 我们讲到 第一个fun 函数可以改写为:

    int Widget::fun(Widget* const this, int a); //this 是常指针

    第二个fun 函数因为 const 的原因,改写为:

    int Widget::fun(const Widget* const this, int a); // this 是常指针 并且指向 常对象(不可修改)

    看到这两个函数改写形式,是不是有点眼熟? 正是! 正是第2部分 函数重载中第2组情况,所以这部分我就此打住
    还是多说一句吧:const member function 有两种不同的语义(个人觉得很恶心),详见 Effective c++ 第3条

     

    4. static function

     

    复制代码
    class Widget {
    public:
        int fun1() {...}
        virtual int fun2() {...}
        static int fun2() {...}
    private:
        int m1;
        static int m2;
    };
    复制代码

    static 成员函数和成员数据都独立于该类的任意对象而存在.不是类对象的组成部分
    static 成员遵循正常的 public/private 访问规则

    (1)static member function
    static 成员是类的组成部分但不是任何对象的组成部分,因此,static 成员函数没有 this 指针
    static 函数没有 this 指针,所以 static 函数不能是 const member function 和 virtual function

    (2)static member data
    static member data 不属于任何对象,所以不是通过构造函数进行初始化的
    static member data 在类定义体外部定义,且在外部定义时进行初始化(static const int a = 0 可以在类内定义. 真心不喜欢这种打补丁式的特性设计)
    static double ClassName::StaticData = 0.0;

    (3)调用规则
    ClassName::StaticFunc(...);
    static function 可以直接使用该类的 static data,不能使用该类的 non-static data(因为所有 non-static data 必须经由 this 指针调用)
    static function 与 non-static function 之间:
    static function 属于类,在该类实例化对象之前已经定义并且分配内存空间,而 static function 必须在类实例化对象之后才定义分配内存空间,故:
    static function 调用 non-static function 是错误的
    non-static function 调用 static function 是正确的

     

    5. virtual function

    复制代码
    class Base {
    public:
        virtual void func() const = 0;
    };
    
    class Derived : public Base {
    public:
        virtual void func() const;
    };
    Base* pBase = new Base;
    Base* pDerived = new Derived;
    复制代码

    (1)对象的静态类型:对象在程序中被声明时所采用的类型
    pBase声明为 Base*,所以 pBase 静态类型为 Base*(不论真正指向的对象类型)
    pDerived声明为 Base*,所以 pDerived 静态类型为 Base*(不论真正指向的对象类型)

    (2)对象的动态类型:目前真正所指对象的类型
    pBase真正指向的对象类型为 Base*,所以 pBase 动态类型为 Base*
    pDerived真正指向的对象类型为 Derived*,所以 pDerived 动态类型为 Derived*

    virtual 函数系动态绑定而来,最终调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型

    复制代码
    class Shape {
    public:
        virtual void draw() const = 0;
        virtual void error(const std::string& msg);
        int objectID() const;
    };
    class Rectangle : public Shape {...};
    class Elllipse  : public Shape {...};
    复制代码

    成员函数的接口总是被继承,因为 public 继承意味着 is-a,所以对base class 为真的任何事情一定也对其 derived class 为真

    我们现在将类的 non-static function 分为三类:

    a. pure virtual member function
    b. non-pure virtual member function
    c. non-virtual member function

    (1) pure virtual member function:
    pure virtual 函数有两个最突出的特性:
    a.必须在所有 Derived class 中重定义该函数
    b.它们在 抽象基类里面通常没有定义

    声明一个 pure virtual 函数是为了让 derived class 只继承 函数接口

    (2) non-pure virtual member function
    non-pure virtual 函数是为了让 derived class 继承 函数接口 和 函数缺省实现
    函数为 non-pure virtual 函数,表明 Base class 可以选择是否为该函数提供一个缺省实现,并且让 Derived class 继承该函数缺省实现
    a. 若 Base class 没有提供缺省实现,Derived class 也没有重定义该函数,当然会编译错误
    b. 若 Base class 没有提供缺省事项,Derived class 重定义了该函数,调用函数的重定义版本
    c. 若 Base class 提供了缺省实现,Derived class 没有重定义该函数,则默认继承该函数的缺省实现
    d. 若 Base class 提供了缺省实现,Derived class 重定义了该函数,则触发c++多态机制,最终调用的函数取决于发出调用的对象的动态类型

    (3) non-virtual member function:
    声明 non-virtual 函数是为了让 derived class 继承 函数接口 和 函数强制性实现(注意区别于 函数缺省实现)
    实际上 non-virtual 函数表现出 不变性(所有Derived class 都不能重定义该函数,并且都共享 Base class 的同一份强制性函数实现)

     

    6.To be continue
    最初学c++时,最初的印象就是一个函数貌似有好多的关键词可以修饰, inline、const、static、virtual...
    最大的迷惑就是这些关键词到底谁和谁可以在一起,谁和谁不能在一起?
    慢慢的学习,陆续的理解了这些关键词背后的语义之后,觉得也不过如此
    在这里我根据自己的理解,结合本文最后奉上一幅图

    ps:我没有加入 friend function(这是c++封装与访问机制的妥协物,是一个典型的c++补丁式特性)

    写在最后的话:
    每次我看到这幅函数图,我都想仰天长叹,喃喃自语: c++啊,想说爱你不容易...

    欢迎大家批评指正,共同学习...

    转载请注明出处,原文地址:http://www.cnblogs.com/wwwjieo0/p/3452930.html

  • 相关阅读:
    单 GPU 程序 转 多 GPU 程序
    Ubuntu 开启ssh 连接,设置静态 ip 地址。
    Ubuntu 添加新硬盘 之 分区+格式化+挂载
    夜间模式的开启与关闭,父模板的制作
    开始Flask项目
    完成登录与注册页面的前端
    JavaScript 基础,登录前端验证
    CSS实例:图片导航块
    导航,头部,CSS基础
    web基础,用html元素制作web页面
  • 原文地址:https://www.cnblogs.com/wodehao0808/p/3642436.html
Copyright © 2011-2022 走看看