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

原文地址:https://www.cnblogs.com/wwwjieo0/p/3452930.html