zoukankan      html  css  js  c++  java
  • 高质量C /C编程指南第8章 C 函数的低级特性

    第8章 C 函数的低级特性

    对比于C言语的函数,C 增加了重载(overloaded)、内联(inline)、const和virtual四种新机制。此中重载和内联机制既可用于全局函数也可用于类的成员函数,const与virtual机制仅用于类的成员函数。

           重载和内联必定有其利益才会被C 言语给与,然则不行以当成免费的午餐而滥用。本章将根究重载和内联的利益与领域性,说明什么环境下应该给与、不该给与以及要警悟错用。

    8.1 函数重载的观点

    8.1.1 重载的来源

        自然言语中,一个词可以有很多分例如的寄义,即该词被重载了。人们可以经过进程上下文来判定该词真相是哪种寄义。“词的重载”可以使言语愈加简练。例如“用饭”的寄义很是广泛,人们没有需求每次非得说清楚过细吃什么不行。别迂腐得象孔已己,说茴喷鼻豆的茴字有四种写法。

        在C 递次中,可以将语义、功能类似的几个函数用一致个名字吐露表示,即函数重载。这样便于影象,进步了函数的易用性,这是C 言语给与重载机制的一个来由。例如示例8-1-1中的函数EatBeef,EatFish,EatChicken可以用一致个函数名Eat吐露表示,用分例如典范的参数加以区别。

     

     

    void EatBeef(…);       // 可以改为     void Eat(Beef …);

    void EatFish(…);       // 可以改为     void Eat(Fish …);

    void EatChicken(…);    // 可以改为     void Eat(Chicken …);

     

    示例8-1-1 重载函数Eat

     

        C 言语给与重载机制的另一个来由是:类的机关函数需求重载机制。因为C 划定礼貌机关函数与类同名(请拜见第9章),机关函数只能有一个名字。若是想用几种分例如的方式创立东西该如何办?别无选择,只能用重载机制来完成。所以类可以有多个同名的机关函数。

     

    8.1.2 重载是如何完成的?

        几个同名的重载函数依旧是分例如的函数,它们是如何区分的呢?我们自然想到函数接口的两个要素:参数与前往值。

    若是同名函数的参数分例如(包孕典范、递次分例如),那么轻易区别出它们是分例如的函数。

    若是同名函数仅仅是前往值典范分例如,偶然可以区分,偶然却不克不及。例如:

    void Function(void);

    int  Function (void);

    上述两个函数,第一个没有前往值,第二个的前往值是int典范。若是这样挪用函数:

        int  x = Function ();

    则可以判定出Function是第二个函数。成便是在C /C递次中,我们可以马虎函数的前往值。在这种环境下,编译器和递次员都不晓得哪个Function函数被挪用。

        所以只能靠参数而不克不及靠前往值典范的分例如来区分重载函数。编译器根据参数为每个重载函数孕育发生分例如的内部标识符。例如编译器为示例8-1-1中的三个Eat函数孕育发生象_eat_beef、_eat_fish、_eat_chicken之类的内部标识符(分例如的编译器年夜概孕育发生分例如气概的内部标识符)。

     

    若是C 递主要挪用曾经被编译后的C函数,该如何办?

    假定某个C函数的声明如下:

    void foo(int x, int y);

    该函数被C编译器编译后在库中的名字为_foo,而C 编译器则会孕育发生像_foo_int_int之类的名字用来支撑函数重载和典范安好连接。因为编译后的名字分例如,C 递次不克不及间接挪用C函数。C 供应了一个C连接交流指定符号extern“C”来处置这个成就。例如:

    extern “C”

    {

       void foo(int x, int y);

       … // 别的函数

    }

    年夜概写成

    extern “C”

    {

       #include “myheader.h”

       … // 别的C头文件

    }

    这就讲述C 编译译器,函数foo是个C连接,应该到库中找名字_foo而不是找_foo_int_int。C 编译器开拓商曾经对C规范库的头文件作了extern“C”处置惩罚,所以我们可以用#include 间接援用这些头文件。

     

        留意并不是两个函数的名字相反就能构成重载。全局函数和类的成员函数同名不算重载,因为函数的感化域分例如。例如:

        void Print(…);     // 全局函数

        >

        {…

            void Print(…);    // 成员函数

        }

        岂论两个Print函数的参数能否分例如,若是类的某个成员函数要挪用全局函数Print,为了与成员函数Print区别,全局函数被挪用时应加‘::’符号。如

        ::Print(…);    // 吐露表示Print是全局函数而非成员函数

     

    8.1.3 把稳隐式典范转换招致重载函数孕育发生二义性

        示例8-1-3中,第一个output函数的参数是int典范,第二个output函数的参数是float典范。因为数字自己没有典范,将数字算作参数时将自动举办典范转换(称为隐式典范转换)。语句output(0.5)将孕育发生编译错误,因为编译器不晓得该将0.5转换成int照旧float典范的参数。隐式典范转换在很多中心可以简化递次的誊录,然则也年夜概留下隐患。

     

    # include <iostream.h>

    void output( int x);    // 函数声明

    void output( float x);  // 函数声明

     

    void output( int x)

    {

        cout << " output int " << x << endl ;

    }

     

    void output( float x)

    {

        cout << " output float " << x << endl ;

    }

     

    void main(void)

    {

        int   x = 1;

        float y = 1.0;

        output(x);          // output int 1

        output(y);          // output float 1

        output(1);          // output int 1

    //  output(0.5);        // error! ambiguous call, 因为自动典范转换

        output(int(0.5));   // output int 0

        output(float(0.5)); // output float 0.5

    }

    示例8-1-3 隐式典范转换招致重载函数孕育发生二义性

     

    8.2 成员函数的重载、笼罩与窜伏

        成员函数的重载、笼罩(override)与窜伏很轻易混杂,C 递次员必需要搞清楚观点,不然错误将防不胜防。

     

    8.2.1 重载与笼罩

        成员函数被重载的特性:

    (1)相反的领域(在一致个类中);

    (2)函数名字相反;

    (3)参数分例如;

    (4)virtual关键字无关紧要。

        笼罩是指派生类函数笼罩基类函数,特性是:

    (1)分例如的领域(划分位于派生类与基类);

    (2)函数名字相反;

    (3)参数相反;

    (4)基类函数必需有virtual关键字。

        示例8-2-1中,函数Base::f(int)与Base::f(float)互相重载,而Base::g(void)被Derived::g(void)笼罩。

     

    #include <iostream.h>

        >

    {

    public:

                  void f(int x){ cout << "Base::f(int) " << x << endl; }

    void f(float x){ cout << "Base::f(float) " << x << endl; }

          virtual void g(void){ cout << "Base::g(void)" << endl;}

    };

     

        >

    {

    public:

          virtual void g(void){ cout << "Derived::g(void)" << endl;}

    };

     

        void main(void)

        {

          Derived  d;

          Base *pb = &d;

          pb->f(42);        // Base::f(int) 42

          pb->f(3.14f);     // Base::f(float) 3.14

          pb->g();          // Derived::g(void)

    }

    示例8-2-1成员函数的重载和笼罩

       

    8.2.2 令人疑惑的窜伏划定礼貌

        原来仅仅区别重载与笼罩并不算困难,然则C 的窜伏划定礼貌使成就庞年夜性蓦地增加。这里“窜伏”是指派生类的函数屏障了与其同名的基类函数,划定礼貌如下:

    (1)若是派生类的函数与基类的函数同名,然则参数分例如。此时,岂论有无virtual关键字,基类的函数将被窜伏(留意别与重载混杂)。

    (2)若是派生类的函数与基类的函数同名,而且参数也相反,然则基类函数没有virtual关键字。此时,基类的函数被窜伏(留意别与笼罩混杂)。

        示例递次8-2-2(a)中:

    (1)函数Derived::f(float)笼罩了Base::f(float)。

    (2)函数Derived::g(int)窜伏了Base::g(float),而不是重载。

    (3)函数Derived::h(float)窜伏了Base::h(float),而不是笼罩。

     

    #include <iostream.h>

        >

    {

    public:

        virtual void f(float x){ cout << "Base::f(float) " << x << endl; }

    void g(float x){ cout << "Base::g(float) " << x << endl; }

                void h(float x){ cout << "Base::h(float) " << x << endl; }

    };

        >

    {

    public:

        virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }

    void g(int x){ cout << "Derived::g(int) " << x << endl; }

                void h(float x){ cout << "Derived::h(float) " << x << endl; }

    };

    示例8-2-2(a)成员函数的重载、笼罩和窜伏

     

        据作者考察,很多C 递次员没有心识到有“窜伏”这回事。因为观点不敷深切,“窜伏”的发生可谓按兵不动,屡屡孕育发生令人疑惑的效果。

    示例8-2-2(b)中,bp和dp指向一致地址,按理说运转效果应该是相反的,可实际并非这样。

     

    void main(void)

    {

    Derived  d;

    Base *pb = &d;

    Derived *pd = &d;

    // Good : behavior depends solely on type of the object

    pb->f(3.14f); // Derived::f(float) 3.14

    pd->f(3.14f); // Derived::f(float) 3.14

     

    // Bad : behavior depends on type of the pointer

    pb->g(3.14f); // Base::g(float) 3.14

    pd->g(3.14f); // Derived::g(int) 3        (surprise!)

     

    // Bad : behavior depends on type of the pointer

    pb->h(3.14f); // Base::h(float) 3.14      (surprise!)

    pd->h(3.14f); // Derived::h(float) 3.14

    }

    示例8-2-2(b) 重载、笼罩和窜伏的对照

    8.2.3 挣脱窜伏

        窜伏划定礼貌惹起了不少费事。示例8-2-3递次中,语句pd->f(10)的本意是想挪用函数Base::f(int),然则Base::f(int)不幸被Derived::f(char *)窜伏了。因为数字10不克不及被隐式地转化为字符串,所以在编译时堕落。

     

    >

    {

    public:

    void f(int x);

    };

    >

    {

    public:

    void f(char *str);

    };

    void Test(void)

    {

    Derived *pd = new Derived;

    pd->f(10);    // error

    }

    示例8-2-3 因为窜伏而招致错误

     

        从示例8-2-3看来,窜伏划定礼貌彷佛很愚蠢。然则窜伏划定礼貌至多有两个存在的来由:

    u       写语句pd->f(10)的人年夜概真的想挪用Derived::f(char *)函数,只是他误将参数写错了。有了窜伏划定礼貌,编译器就可以大白指堕落误,这未必不是功德。不然,编译器会静暗公然将功补过,递次员将很难发明这个错误,流下祸胎。

    u       假定类Derived有多个基类(多重经受),偶然搞不清楚哪些基类定义了函数f。若是没有窜伏划定礼貌,那么pd->f(10)年夜概会挪用一个出人意表的基类函数f。尽管窜伏划定礼貌看起来不如何有事理,但它几乎能清除这些意外。

     

    示例8-2-3中,若是语句pd->f(10)必定要挪用函数Base::f(int),那么将类Derived批改为如下即可。

    >

    {

    public:

    void f(char *str);

    void f(int x) { Base::f(x); }

    };

    8.3 参数的缺省值

    有一些参数的值在每次函数挪用时都相反,誊录这样的语句会使人讨厌。C 言语给与参数的缺省值使誊录变得轻便(在编译时,缺省值由编译器自动拔出)。

        参数缺省值的运用划定礼貌:

    l         【划定礼貌8-3-1】参数缺省值只能出现在函数的声明中,而不克不及出现在定义体中。

    例如:

        void Foo(int x=0, int y=0);    // 准确,缺省值出现在函数的声明中

     

        void Foo(int x=0, int y=0)        // 错误,缺省值出现在函数的定义体中

        {


        }

    为什么会这样?我想是有两个缘由:一是函数的完成(定义)原来就与参数能否出缺省值有关,所以没有需求让缺省值出现在函数的定义体中。二是参数的缺省值年夜概会窜改,显然批改函数的声明比批改函数的定义要便当。

     

    l         【划定礼貌8-3-2】若是函数有多个参数,参数只能从后向前挨个儿缺省,不然将招致函数挪用语句怪模怪样。

    准确的示例如下:

    void Foo(int x, int y=0, int z=0);

    错误的示例如下:

    void Foo(int x=0, int y, int z=0);   

     

    要留意,运用参数的缺省值并没有授予函数新的功能,仅仅是使誊录变得轻便一些。它年夜概会进步函数的易用性,然则也年夜概会低潮函数的可熟悉性。所以我们只能适本地运用参数的缺省值,要防备运用不当孕育发生负面效果。示例8-3-2中,分例如理地运用参数的缺省值将招致重载函数output孕育发生二义性。

     

    #include <iostream.h>

    void output( int x);

    void output( int x, float y=0.0);

     

    void output( int x)

    {

        cout << " output int " << x << endl ;

    }

     

    void output( int x, float y)

    {

        cout << " output int " << x << " and float " << y << endl ;

    }

     

    void main(void)

    {

        int x=1;

        float y=0.5;

    //  output(x);          // error! ambiguous call

        output(x,y);        // output int 1 and float 0.5

    }

     

    示例8-3-2  参数的缺省值将招致重载函数孕育发生二义性

    8.4 运算符重载

    8.4.1 观点

        在C 言语中,可以用关键字operator加上运算符来吐露表示函数,叫做运算符重载。例如两个双数相加函数:

        Complex Add(const Complex &a, const Complex &b);

    可以用运算符重载来吐露表示:

        Complex operator (const Complex &a, const Complex &b);

        运算符与深刻函数在挪用时的分例如之处是:关于深刻函数,参数出现在圆括号内;而关于运算符,参数出现在其左、右侧。例如

       Complex a, b, c;

        …

        c = Add(a, b); // 用深刻函数

        c = a b;        // 用运算符

        若是运算符被重载为全局函数,那么只要一个参数的运算符叫做一元运算符,有两个参数的运算符叫做二元运算符。

        若是运算符被重载为类的成员函数,那么一元运算符没有参数,二元运算符只要一个右侧参数,因为东西自己成了左侧参数。

        从语法上讲,运算符既可以定义为全局函数,也可以定义为成员函数。文献[Murray , p44-p47]对此成就作了较多的论述,并总结了表8-4-1的划定礼貌。

     

    运算符

    划定礼貌

    悉数的一元运算符

    发起重载为成员函数

    = () [] ->

    只能重载为成员函数

    = -= /= *= &= |= ~= %= >>= <<=

    发起重载为成员函数

    悉数别的运算符

    发起重载为全局函数

    表8-4-1 运算符的重载划定礼貌

     

    因为C 言语支撑函数重载,才干将运算符当成函数来用,C言语就不行。我们要以泛泛心来看待运算符重载:

    (1)不要过火忧虑自己不会用,它的本性依旧是递次员们熟谙的函数。

    (2)不要过火热心肠运用,若是它不克不及使代码变得愈加易读易写,那就别用,不然会自找费事。

     

    8.4.2 不克不及被重载的运算符

        在C 运算符纠集中,有一些运算符是不许诺被重载的。这种限定是出于安好方面的思考,可防备错误和杂沓。

    (1)不克不及窜改C 内部数据典范(如int,float等)的运算符。

    (2)不克不及重载‘.’,因为‘.’在类中对任何成员都有心义,曾经成为规范用法。

    (3)不克不及重载目前C 运算符纠集中没有的符号,如#,@,$等。缘由有两点,一是难以熟悉,二是难以确定优先级。

    (4)对曾经存在的运算符举办重载时,不克不及窜改优先级划定礼貌,不然将惹起杂沓。

    8.5 函数内联

    8.5.1 用内联替代宏代码

        C 言语支撑函数内联,其目的是为了进步函数的实施屈从(速度)。

        在C递次中,可以用宏代码进步实施屈从。宏代码自己不是函数,但运用起来象函数。预处置惩罚器用复制宏代码的体式格局替代函数挪用,省去了参数压栈、生成汇编言语的CALL挪用、前往参数、实施return等过程,从而进步了速度。运用宏代码最年夜的缺陷是轻易堕落,预处置惩罚器在复制宏代码时屡屡孕育发买卖想不到的边沿效应。例如

        #define MAX(a, b)       (a) > (b) ? (a) : (b)

    语句

    result = MAX(i, j) 2 ;

    将被预处置惩罚器诠释为

        result = (i) > (j) ? (i) : (j) 2 ;

    因为运算符‘ ’比运算符‘:’的优先级高,所以上述语句并不等价于祈望的

        result = ( (i) > (j) ? (i) : (j) ) 2 ;

    若是把宏代码改写为

        #define MAX(a, b)       ( (a) > (b) ? (a) : (b) )

    则可以处置由优先级惹起的错误。然则纵然运用批改后的宏代码也不是十拿九稳的,例如语句   

    result = MAX(i , j);

    将被预处置惩罚器诠释为

        result = (i ) > (j) ? (i ) : (j);

        关于C 而言,运用宏代码另有另一种缺陷:无法操作类的公无数据成员。

     

    让我们看看C 的“函数内联”是如何使命的。关于任何内联函数,编译器在符号内外放入函数的声明(包孕名字、参数典范、前往值典范)。若是编译器没有发明内联函数存在错误,那么该函数的代码也被放入符号内外。在挪用一个内联函数时,编译器起首反省挪用能否准确(举办典范安好反省,年夜概举办自动典范转换,当然对悉数的函数都一样)。若是准确,内联函数的代码就会间接交流函数挪用,于是省去了函数挪用的开支。这个过程与预处置惩罚有较着的分例如,因为预处置惩罚器不克不及举办典范安好反省,年夜概举办自动典范转换。假定内联函数是成员函数,东西的地址(this)会被放在吻合的中心,这也是预处置惩罚器办不到的。

    C 言语的函数内联机制既具有宏代码的屈从,又增加了安好性,而且可以自由操作类的数据成员。所以在C 递次中,应该用内联函数替代悉数宏代码,“断言assert”生怕是唯一的破例。assert是仅在Debug版本起感化的宏,它用于反省“不该该”发生的环境。为了不在递次的Debug版本和Release版本惹起差别,assert不该该孕育发生任何反感化。若是assert是函数,因为函数挪用会惹起内存、代码的变卦,那么将招致Debug版本与Release版本存在差别。所以assert不是函数,而是宏。(拜见6.5节“运用断言”)

     

    8.5.2 内联函数的编程气概

        关键字inline必需与函数定义体放在一起才干使函数成为内联,仅将inline放在函数声明前面不起任何感化。如上风格的函数Foo不克不及成为内联函数:

        inline void Foo(int x, int y);     // inline仅与函数声明放在一起

        void Foo(int x, int y)

        {

            …

        }

    而如上风格的函数Foo则成为内联函数:

        void Foo(int x, int y);    

        inline void Foo(int x, int y)  // inline与函数定义体放在一起

        {

            …

        }

        所以说,inline是一种“用于完成的关键字”,而不是一种“用于声明的关键字”。寻常地,用户可以阅读函数的声明,然则看不到函数的定义。尽管在年夜多数教科书中内联函数的声明、定义体前面都加了inline关键字,但我觉得inline不该该出现在函数的声明中。这个细节当然不会影响函数的功能,然则表现了高质量C /C递次诡计气概的一个根基绳尺:声明与定义不行等量齐不都雅,用户没有需求、也不该该晓得函数能否需求内联。

        定义在类声明之中的成员函数将自动地成为内联函数,例如

        >

        {

    public:

            void Foo(int x, int y) { … }     // 自动地成为内联函数

        }

    将成员函数的定义体放在类声明之中当然能带来誊录上的便当,但不是一种优越的编程气概,上例应该改成:

        // 头文件

    >

        {

    public:

            void Foo(int x, int y);

        }

        // 定义文件

        inline void A::Foo(int x, int y)

    {


    }

     

    8.5.3 慎用内联

        内联能进步函数的实施屈从,为什么不把悉数的函数都定义成内联函数?

        若是悉数的函数都是内联函数,还用得着“内联”这个关键字吗?

        内联因此代码收缩(复制)为价格,仅仅省去了函数挪用的开支,从而进步函数的实施屈从。若是实施函数体内代码的功夫,比拟于函数挪用的开支较年夜,那么屈从的播种会很少。另一方面,每一处内联函数的挪用都要复制代码,将使递次的总代码量增年夜,消耗更多的内存空间。以下环境不宜运用内联:

    (1)若是函数体内的代码对照长,运用内联将招致内存消耗价格较高。

    (2)若是函数体内出现循环,那么实施函数体内代码的功夫要比函数挪用的开支年夜。

        类的机关函数和析构函数轻易让人误解成运用内联更有用。要把稳机关函数和析构函数年夜概会窜伏一些活动,如“偷偷地”实施了基类或成员东西的机关函数和析构函数。所以不要肆意地将机关函数和析构函数的定义体放在类声明中。

    一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说清楚明明inline不该该出现在函数的声明中)。

    8.6 一些心得体味

        C 言语中的重载、内联、缺省参数、隐式转换等机制展示了很多利益,然则这些利益的目下都窜伏着一些隐患。正如人们的饮食,少食和暴食都不行取,理应恰到利益。我们要辨证地看待C 的新机制,应该恰到好处地运用它们。当然这会使我们编程时多费一些心思,少了一些爽直,但这才是编程的艺术。



    版权声明: 原创作品,许诺转载,转载时请务必以超链接形式标明文章 原始来由 、作者信息和本声明。不然将追究司法责任。

  • 相关阅读:
    poj3181(Dollar Dayz)
    poj3666(Making the Grade)
    poj2392(Space Elevator)
    hdu5288(OO’s Sequence)
    hdu5289(Assignment)
    快学scala
    Spark Checkpointing
    Spark Performance Tuning (性能调优)
    Spark Memory Tuning (内存调优)
    Sparkstreaming and Kafka
  • 原文地址:https://www.cnblogs.com/zgqjymx/p/1974602.html
Copyright © 2011-2022 走看看