高质量C/C++编程之C++高级特性

相对于C函数,C++增加了重载overload、内联inline、const和virtual四种新机制。
重载和内联机制既可以用于全局元素也可以用于类的成员函数。
const和virtual机制仅用于类的成员函数。

1.函数重载

在C++中,可以将语义、功能相似的几个函数使用同一个名字表示。

使用重载的理由:
1)便于记忆,提供函数的易用性。
2)类的构造函数需要重载机制,因为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"是告诉C++编译器,函数foo是一个C连接,应该在库中找名字_foo而不是找_foo_int_int。

    C++编译器开发商已经对C标准库的头文件做了extern “C”处理,所以我们可以使用#include直接使用。

    函数重载的前提是同名的函数在相同的作用域。
    void Print();
    class A{
        void Print();
    }
    作用域不同的同名函数不能构成重载。
    为了与类的成员函数Print相区别,全局函数被调用时需要加::,::Print()以表示Print是全局函数。

# 当心隐式类型转换导致重载函数的二义性
    void output(int x);
    void output(float x);
    两个重载函数声明,使用如下时,产生二义性:
    output(0.5);        //error
    改进办法:output(int(0.5));        output(float(0.5));

2.成员函数的重载、覆盖和隐藏

# 重载和覆盖

    1)成员函数被重载的特征:
    @1 相同的范围,即在同一个类中
    @2 函数名字相同
    @3 函数参数不相同
    @4 virtual关键字可有可无

    2)覆盖override
    覆盖是派生类函数覆盖基类函数:
    @1 不同的范围,分别在基类和派生类
    @2 函数名字相同
    @3 函数参数相同
    @4 必须有virtual关键字    //意味着覆盖必须是发生在虚函数的前提上的

    3)隐藏
    隐藏是派生类函数屏蔽了与其同名的基类函数
    @1 如果派生类的函数和基类函数同名,但参数不同,则无论有无virtual关键字,基类的函数都将被隐藏
    @2 如果派生类的函数和基类函数同名,参数也相同,但基类函数没有virtual关键字,基类函数将被隐藏
    显然,如果是发生在虚函数的同参同名函数,那是虚函数覆盖了

    那么再进一步的总结就是:
    @1 重载发生在相同范围的,覆盖和隐藏发生在不同范围的
    @2 覆盖是以虚函数为前提,所以可以称为虚函数覆盖

    如何更好地判断覆盖还是隐藏:
    如果是发生在不同范围的,也即是基类和派生类之间的同名函数,如果不是虚函数覆盖,那么一定是隐藏
    所以,只要看是否发生的是同名同参虚函数,如果是就是虚函数覆盖,否则就是隐藏了

    #include <iostream.h>
    class Base{
        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;}
    }
    class Derived:pulic Base{
        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;}        //同名同参非虚函数,隐藏
    }

    void test(){    
        Derived d;
        Base *pb = &d;
        Derived *pd = &d;

        //Good:虚函数覆盖,行为完全依赖对象的类型,既然是覆盖,那么结果是一样的,派生类覆盖基类,总是派生类的函数,因为对象是派生类型
        pb->f(3.14f);        //Derived::f(float) 3.14
        pd->f(3.14f);        //Derived::f(float) 3.14
        
        //Bad:隐藏,行为依赖于指针的类型        
        pb->g(3.14f);        //Base::g(float) 3.14
        pd->g(3.14f);        //Derived::g(int) 3

        //Bad:隐藏,行为依赖于指针的类型
        pb->h(3.14f);        //Base::h(float) 3.14
        pd->h(3.14f);        //Derived::h(float) 3.14
    }

    分析:
     覆盖:
    何为覆盖,在数据结构上,就是在对象地址前端的虚函数表中派生类虚函数覆盖了同名同参的基类虚函数
    知识有限,我还没有查到是否在对象中两个虚函数代码都存在,
    但是虚函数取地址是通过虚函数表的,如果在派生类对象虚函数表中被覆盖,那么基类的虚函数再也找不回了

    隐藏:
    隐藏则不同,派生类对象中有继承自基类的部分,有属于自己的部分。
    对于非虚函数,这些函数在派生类对象中是并存的(他们没有什么函数表),根据指针的不同是否就意味着优先查找的方式不同呢??

    隐藏存在的意义:
    当多重继承发生时,根据指针可以指向多个基类或子类的实现不同但是同名的函数。

3.参数的缺省值

规则1: 参数缺省值只能出现在函数的声明中,不能出现在定义体中。
    void foo(int x = 0,int y = 0);

规则2: 如果函数存在多个参数,那么只能从后向前挨个缺省,否则将导致函数调用语句怪模怪样。
    void foo(int x,int y = 0,int z = 0);

不合理使用参数的缺省值可能导致重载函数产生二义性
void foo(int x);
void foo(int x, float y = 0.0);
调用的时候:
foo(1);        //error,ambiguous call
foo(1,0.5);    //ok

4.运算符重载

运算符和普通函数在调用之时不同之处是:
对于普通函数,参数出现在圆括号内,而对于运算符,参数出现在它的左右侧。

如果运算符被重载为全局函数,有一个参数的称为一元运算符,有两个参数的称为二元运算符。
如果运算符被重载为类的成员函数,那么一元操作符没有参数,二元操作符只有一个右侧参数,对象自己成了左侧参数

那么一元操作符只能被重载为成员函数
对于二元操作符+=、-=、*=、%=、&=、|=、~=、<、<=、>、>+建议重载为成员函数,其他二元操作符重载为全局函数

不能被重载的运算符:
1)不可改变C++中内部数据类型(如int、float等)的运算符
2)不能重载”.“
3) 不得重载C++运算符集合中没有的符号
    一是难以理解,而是难以确定优先级
4)对已经重载的运算符进行重载,不可改变其原有的优先级

5.函数内联

使用内联取代宏代码

# C语言使用宏代码提高执行效率:
    宏代码不是函数,但使用起来象函数。
    预处理器使用复制宏代码的方式取代函数调用,省去了参数压栈、生成汇编语言的CALL调用、返回参数、执行return等过程,从而提高了效率。
    使用宏代码最大的缺点就是容易出错,预处理器在复制宏代码时常常产生意想不到的边际效应。

    定义1:#define MAX(a,b)  (a)>(b)?(a):(b)
    使用:result = MAX(x,j) + 1;
    预处理复制宏代码 result = (x)>(y)?(x):(y)+1        //error

    对策:整个宏代码加上大括号
    正确定义2:#define MAX(a,b)  ((a)>(b)?(a):(b))

    但是依然还有出错的可能:
    调用:result = MAX(++x,j)
    复制宏代码:result = ((++x)>(y)?(++x):(y))    //x值做了两次++

# C++使用内联取代宏代码来提高效率:

函数内联的实现:

对于任何内联函数,编译器在符号表中放入函数的声明(包括名字,参数和返回值);
编译器检查内联函数没有错误,那么就把该函数的代码也放入到符号表中
当调用一个内联函数时,编译器会检查调用是否正确(进行类型安全检查,或进行类型自动转换,对所有函数一样);
如果正确,内联函数的代码直接替换函数调用,于是省去了函数调用的开销。

内联实现VS预处理
1)预处理不能进行类型安全检查,更不能进行自动类型转换
2)如果内联函数是成员函数,对象的地址this会被放在合适的地址,这是预处理所做不到的
C++的函数内联,具备了宏代码的效率,也提高了安全性,同时还可以自由操作类的数据成员。

除了assert以外,在C++中应该使用内联函数取代所有的宏代码。

内联函数的编程风格:

关键字inline必须与函数定义体放在一起才能使函数成为内联,仅将inline放在函数声明前面不起任何作用。
也就是说关键字inline是用于实现的关键字,而不是用于声明的关键字。

正确的定义:
声明:void foo();
定义:inline void foo(){}

定义在类声明中的成员函数将自动地成为内联函数,但这不是一种良好的编程规范。
可以在类外显式定义inline。

慎用内联:

内联以代码膨胀(复制)为代价
1)省去了函数调用的开销,提高了效率
2)每一处内联函数的调用都要复制代码,将使程序的总代码量增加,消耗更多的内存空间

不宜使用内联的情况:
1)代码较长
2)代码中出现循环
3)类的构造函数和析构函数
    如果存在基类继承,函数会“偷偷地”执行了基类或成员对象的构造函数和析构函数,所以也不要随便将构造函数和析构函数定义在类声明体中
有些优秀的编译器会自动取消不值得的内联

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值