第一部分 基本语法

C++ Primer是一本C++入门经典的书,已经看过很多遍,每次看的感受都不一样。

变量和基本类型

const限定符:

      const说明指针变量,组合的情况可能会显得很复杂。使用指针时要涉及两个目标,即指针本身和指针所指的对象。关于const指针变量,可归结为以下三种:

     1.指向常量的指针变量;
   2.常指针变量;
   3.指向常量的常指针变量。
  下面来分别谈谈这三种情况。

       一、指向常量的指针变量:
       声明格式: const type * var_name;或 type const * var_name;
       特点: 可改值。
  将指针声明冠以const,使指向的对象为常量,而不是指针为常量。注意:指向常量的指针不一定指向真正的常量,它也可以指向常量,只是从该指针的角度来看,它所指向的对象是常量,通过该指针不能修改它指向的对象。它还可以指向其它的对象,可以不初始化。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int a =  0, b =  1;
const  int c =  3;
const  int *pi;  //等同于 (const int*) pi;
pi = &a;
*pi =  10//错误:不能修改它指向的对象。
a =  10;
pi = &b;
*pi = &b;
*pi =  20//错误:不能修改它指向的对象。
b =  20;
pi = &c;
*pi = &c;
*pi =  30//错误:不能修改它指向的对象。

const  char *pc =  "asdf";
pc[ 3] =  'a'//错误:不能修改它指向的对象。
pc =  "ghik";
---- -eg3:
const  char *step[ 3] =
{ "left""right""hop"};
step[ 2] =  "skip";
step[ 2][ 1] =  'i'//错误:不能修改它指向的对象。

      二、常指针常量:

     声明格式: type* const var_name;
     特点: 可改对象。
  要把指针本身,而不是它指向的对象声明为常量,采用运算符 *const,必须初始化,通过该指针可以修改它指向的对象,但它不可以指向其他的对象。

 C++ Code 
1
2
3
4
int a =  0, b =  1;
int * const pi = &a;  //等于 int* (const pi) = &a;
*pi =  10;
pi = &b;  //错误:pi本身为常量,不能指向其他对象。
      三、指向常量的常指针变量:

      声明格式: const type * const var_name;
       特点: 值与对象均不能改。
  要使两个目标都是常量,两者都要声明为 const 。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//eg:
int a =  0, b =  1;
const  int c =  3;
const  int * const pi = &a;  //相当于: (const int*)(const pi) = &a;
*pi =  10//错误:不能修改它的对象。
a =  10;
pi = &b;  //错误:不能指向其它对象。
//eg2:
const  char * const pc =  "asdf";
pc[ 3] =  'a'//错误:不能修改它的对象。
pc =  "ghik"//错误:不能指向其它对象。
//eg3:
const  char * const step[ 3] =
{ "left""right""hop"};
step[ 2] =  "skip"//错误:不能指向其它对象。
step[ 2][ 1] =  'i'//错误:不能修改它的对象。

Bjarne在他的The C++ Programming Language里面给出过一个助记的方法: 
把一个声明从右向左读。 


char  * const cp; ( * 读成 pointer to ) 
cp is a const pointer to char 


const char * p; 
p is a pointer to const char; 


char const * p; 
同上因为C++里面没有const*的运算符,所以const只能属于前面的类型。


const   在*之"前"表示内容不变   
const   在*之"后"表示指针不变


后记:今天看到了一种写法char const * p,感觉怪怪的,因为以前只是见过const char *p和char * const p,查了资料才知道 const char * 和char const *竟然是等价的


总结:有一个规则可以很好的区分const是修饰指针,还是修饰指针指向的数据——画一条垂直穿过指针声明的星号(*),如果const出现在线的左边,指针指向的数据为常量;如果const出现在右边,指针本身为常量。而引用本身与天俱来就是常量,即不可以改变指向。



typedef 名字:用来定义类型的同义词。


enum枚举:

    枚举定义包括关键字enum,气候是一个可选的枚举类型,枚举成员

 C++ Code 
1
2
//input is 0,output is 1,
enum open_modes {input,output};

    默认的,第一个枚举成员赋值为0,后面的每个枚举成员赋值比前面大1.


标准库类型

标准库bitset:
参考:http://blog.csdn.net/qll125596718/article/details/6901935


深入理解计算机各种类型大小(sizeof)

1. 用法
1.1 sizeof和new、delete等一样,是关键字,不是函数或者宏。
1.2 sizeof返回内存中分配的字节数,它和操作系统的位数有关。例如在常见的32位系统中,int类型占4个字节;但是在16位系统中,int类型占2个字节。
1.3 sizeof的参数可以是类型,也可以是变量,还可以是常量。对于相同类型,以上3中形式参数的sizeof返回值相同。

 C++ Code 
1
2
3
4
int a;
sizeof(a);  // = 4
sizeof( int);  // = 4
sizeof( 1);  // = 4


1.4 sizeof在编译阶段处理。由于sizeof不能被编译成机器码,所以sizeof的参数不能被编译,而是被替换成类型。

  1. int a = -1;   
  2. sizeof(a=3); // = sizeof(a) = sizeof(int) = 4   
  3. cout<<a<<endl; // 输出-1。由于“=”操作符返回左操作数的类型,赋值操作没有执行。 
2. 在32位系统中不同类型的内存分配

2.1 基本类型

  1. sizeof(int);        // = 4   
  2. sizeof(double);     // = 8   
  3. sizeof(char);       // = 1   
  4. sizeof(bool);       // = 1   
  5. sizeof(short);      // = 2   
  6. sizeof(float);      // = 4   
  7. sizeof(long);       // = 4  
2.2 指针

指针在32位系统中占4个字节。

  1. sizeof(int *);         // = 4   
  2. sizeof(double *);      // = 4   
  3. sizeof(char *);        // = 4  
2.3 数组

2.3.1 数组的sizeof返回整个数组所占的字节数,即(数组元素个数×每个元素所占字节)。
  1. int ai[] = {1, 2};   
  2. sizeof(ai);          // = 2*4 = 8  
2.3.3 数组和指针所占的字节数不同,应注意区分。

  1. int *pi = new int[10]; //这是指针   
  2. sizeof(pi);            // = 4   
  3.   
  4. int ai[10];   
  5. int *p = ai;           //这还是指针   
  6. sizeof(p);             // = 4   
  7.   
  8. double* (*a)[3][6];    //看成(double *) (*a)[3][6],即一个3×6的二维数组,数组元素为指针,指向double类型。   
  9. sizeof(a);             // = 4,a为指向上述二维数组的指针   
  10. sizeof(*a);            // = sizeof(double *)*3*6 = 72,*a表示上述二维数组       
  11. sizeof(**a);           // = sizeof(double *)*6 = 24,**a即*(*a),表示double*[6],是元素为double指针的一维数组。   
  12. sizeof(***a);          // = sizeof(double *) = 4,表示上述一维数组中的第一个元素,元素类型为double指针。   
  13. sizeof(****a);         // = sizeof(double) = 8,表示上述数组首元素指向的double类型。  
2.3.4 函数形式参数中的数组会蜕变为指针,原因是数组参数“传址调用”,调用者只需将实参的地址传递过去。有一种情况例外,那就是参数是指向数组的指针。

  1. void acf(char p[3])     //参数类型是int[],表示指向int的指针   
  2. {   
  3.     sizeof( p );        // = 4   
  4. }   
  5. void aif(int p[])       //参数类型是int[],表示指向int的指针   
  6. {   
  7.     sizeof( p );        // = 4   
  8. }   
  9. void pif(int (*p)[6])   //参数类型是int (*)[6],表示指向int数组的指针   
  10. {   
  11.     sizeof( p);         // = 4   
  12.     sizeof( *p );       // = sizeof(int)*6 = 24   
  13. }   
  14. void ppf(int *p[6])     //参数类型是int *[],表示指向int指针的指针   
  15. {   
  16.     sizeof( p );        // = 4   
  17.     sizeof( *p );       // = 4   
  18. }  
2.4. 类和结构体的内存分配。
2.4.1 空类或空结构体占一个字节。

  1. class CEmpty { };   
  2. sizeof(CEmpty); // = 1   
  3.   
  4. struct SEmpty { };   
  5. sizeof(SEmpty); // = 1  
2.4.2 非空类和结构体所占字节为所有成员占字节的和,但是不包括成员函数和静态成员所占的空间。
  1. class CInt : public CEmpty {   
  2.     int i;   
  3. };   
  4. sizeof(CInt); // = 4;   
  5.   
  6. class CFunc {   
  7.     void f() {}   
  8. };   
  9. sizeof(CFunc); // = 1;   
  10.   
  11. struct SInt : SEmpty {   
  12.     static int i;   
  13. };   
  14. sizeof(SInt); // = 1;  
2.4.3 字节对齐
为了加快计算机的取数速度,编译器默认对内存进行字节对齐。对结构体(包括类)进行字节对齐的原则是:
1)结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2)结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
3)结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。

  1. struct SByte1   
  2. {   
  3.     double d;    // 偏移量0~7   
  4.     char j;      // 偏移量8   
  5.     int a;       // 偏移量12~15,由于9不能整除4,故先填充9~11   
  6. };    
  7. sizeof(SByte1);  // = 16   
  8.   
  9. struct SByte2   
  10. {         
  11.     char j;      // 偏移量0   
  12.     double d;    // 偏移量8~15,由于1不能整除8,故先填充1~7   
  13.     int a;       // 偏移量16~19   
  14. };    
  15. sizeof(SByte2);  // = 24,为了凑成8的倍数,填充20~23  
另外,可以通过#pragma pack(n)来设定变量以n字节对齐方式。

  1. #pragma pack(push) //保存对齐状态   
  2. #pragma pack(4)    //设定为4字节对齐   
  3. class CByte   
  4. {   
  5.     char c;        //偏移量0   
  6.     double d;      //偏移量4~11,由于1不能整除4,故先填充1~3   
  7.     int i;         //偏移量12~15   
  8. };   
  9. #pragma pack(pop)  //恢复对齐状态   
  10. sizeof(CByte); // = 16  

2.4.4 位域
有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态,用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域, 并说明每个区域的位数。
 
2.4.4.1 位域以比特位作为单位,其长度不能大于一个字节。一个位域必须存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。

  1. struct SBit1   
  2. {   
  3.   
  4.     char a : 3;   
  5.     char b : 4;   
  6.     char c : 5;   
  7. };   
  8. sizeof(SBit1); // = (3+4+1+5+3) bits = 2 bytes  
2.4.4.2 使用空域可以有意使某位域从下一单元开始,但是空域不能使用。

  1. struct SBit2   
  2. {   
  3.     char a : 3;   
  4.     char   : 0;   // 空域   
  5.     char b : 4;   
  6.     char c : 5;   
  7. };   
  8. sizeof(SBit2); // = (3+4+1+5+3) bits = 3 bytes  

2.5 联合

联合表示若干数据成员取其一,故以叠加方式分配内存,所占字节数为最大数据成员所占的字节数。
  1. union U   
  2. {   
  3.     int i;   
  4.     char c;   
  5.     double d;   
  6. };   
  7. sizeof(U); // = Max(sizeof(i), sizeof(c), sizeof(d)) = sizeof(d) = 8  
64位机和32位机的比较:


组和指针


指针与typedef

我们直接上代码:

 C++ Code 
1
2
3
4
5
6
typedef string *pstring
const pstring cstr;
//error
const string *cstr  //wrong interpretation of const pstring cstr
//right
string * const cstr;  //equent to const pstring cstr;
很多人错误的认为:cstr的类型是const pstring的一个指针,指向string类型的const对象。

错误原因在于:声明const pstring时,const修饰的是pstring的类型,这是一个指针。



表达式


显示转换

dynamic_cast 支持运行时识别指针或引用所指向的对象。

const_cast 顾名思义,将转换掉表示式的const性质。

 C++ Code 
1
2
const  char *p_str;
char *pc = string_copy( const_cast< char *>(p_str));  //去掉cont性质
static_cast 编译器隐式执行的任何类型转换都可以有static_cast显式完成

 C++ Code 
1
2
3
4
5
double d =  97. 0;
char ch =  static_cast< char>(d);

void *p = &d;
double *dp =  static_cast< double *>(p);

reinterpret_cast 通常为操作数的位模式提供较低层次的重新解释。



语句


try.. catch.. throw..

 C++ Code 
1
2
3
4
5
6
int main()
{
#ifdef NDEBUG    //如果NDEBUG有定义则跳过下面的代码
    cerr <<  "start main" << endl;
#endif
}

调试技能:

使用assert进行断言预处理宏


函数


千万不要返回局部对象的引用
 C++ Code 
1
2
3
4
5
const stringg &manip( const string &s)
{
    string ret = s;
     return ret;  //wrong
}
当函数执行完毕时,将释放分配局部对象的存储空间。此时,对局部变量的引用会指向不确定的内存。上面的程序会在运行时出错,因为它返回了局部对象的引用。当程序执行完毕,字符串ret占用的存储空间被释放,函数返回值指向了对于这个程序来说不再有效的内存空间。

引用返回左值
返回引用的函数返回一个左值,因此,这样的函数用于任何要求左值的地方。
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
char &get_val(string &str, string::size_type ix)
{
     return str[x];
}
int main()
{
    string s( "a value");
    cout << s << endl;
    get_val(s,  0) =  'A'//changes s[0] to A
    cout << s << endl;  //prints A value
     return  0;
}
给函数返回值赋值可能让人惊讶,由于函数返回的一个引用,因此正确的,该引用是被放回元素的同义词。
如果不希望引用返回的值被修改,返回值应该声明为const

千万不要返回指向局部对象的指针
函数的返回类型可以是大多数类型,特别是,函数也可以返回指针类型,和局部对象的引用一样,返回指向局部对象的指针也是错误的,一旦程序结束,局部对象被释放,返回指针旧变成了指向不在存在的对象的悬垂指针了。


静态局部变量

一个变量入股位于函数的作用域内,但生命期跨越了这个函数多次调用,这种变量往往是很有用,我们定义为static

static局部对象确保不迟于在程序执行流程第一次经过该对象定义语句时初始化。当函数调用结束时,静态局部变量的结果不会被撤销。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
size_t cout_calls()
{
     static size_t ctr =  0//value will cross calls
     return ++ctr;
}
int main()
{
     for(size_t i =  0; i !=  10; i++)
    {
        cout << cout_calls();
                << endl;
    }
     return  0;
}

内联函数:

内联函数类似于C里边的宏,只是宏经常有一些不安全因素,比如 宏定义 add() 是 a+b,调用的时候宏定义直接展开,不做任何的判断,如果调用res = add()*c; 得到的代码就是res = a+b*c;不是想要的结果。 

   内联函数可以理解成一个安全的宏定义,他就是一个真正的函数,只是不用压栈出栈,速度快,而且比宏定义安全,所以利用 短且频率 比较高的函数可以写成内联函数inline。 
   内联函数在编译时,会将此内联函数代码直接编译到调用函数之处。减少了一次函数调用时的跳转、数据压栈等操作。所以相比调用行数,内联函数的执行效率要相对高点。不过也正因为如此,所以最终生成的程序文件要稍微大点。

1.内联函数可减少cpu的系统开销,并且提高整体程序的速度,但当内联函数很大时,会有相反的作用,因此一般比较小的函数才使用内联函数。
2.有两种内联函数的声明方法,一种是在函数前使用inline关键字;另一种是在类的内部定义函数的代码,这样的函数将自动转换为内联函数,而且没必要将inline放在函数前面。编译器隐式的将在类内定义的成员函数当做内联函数。
3.内联是一种对编译器的请求,像register说明符一样。下面这些情况会阻止编译器服从这项请求.如果函数中包含有循环,switch或goto语句,递归函数,含有static的函数.


C++的函数重载


函数重载的重要性不言而明,但是你知道C++中函数重载是如何实现的呢(虽然本文谈的是C++中函数重载的实现,但我想其它语言也是类似的)?这个可以分解为下面两个问题

  • 1、声明/定义重载函数时,是如何解决命名冲突的?(抛开函数重载不谈,using就是一种解决命名冲突的方法,解决命名冲突还有很多其它的方法,这里就不论述了)
  • 2、当我们调用一个重载的函数时,又是如何去解析的?(即怎么知道调用的是哪个函数呢)

这两个问题是任何支持函数重载的语言都必须要解决的问题!带着这两个问题,我们开始本文的探讨。本文的主要内容如下:

  •  1、例子引入(现象)
    • 什么是函数重载(what)?
    • 为什么需要函数重载(why)?
  • 2、编译器如何解决命名冲突的?
    • 函数重载为什么不考虑返回值类型
  • 3、重载函数的调用匹配
    • 模凌两可的情况
  • 4、编译器是如何解析重载函数调用的?
    • 根据函数名确定候选函数集
    • 确定可用函数
    • 确定最佳匹配函数
  • 5、总结
1、例子引入(现象)
1.1、什么是函数重载(what)?
函数重载是指在 同一作用域内 ,可以有一组具有 相同函数名 不同参数列表 的函数,这组函数被称为重载函数。重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处。

看下面的一个例子,来体会一下:实现一个打印函数,既可以打印int型、也可以打印字符串型。在C++中,我们可以这样做:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<iostream>
using  namespace std;

void print( int i)
{
    cout <<  "print a integer :" << i << endl;
}

void print(string str)
{
    cout <<  "print a string :" << str << endl;
}

int main()
{
    print( 12);
    print( "hello world!");
     return  0;
}
通过上面代码的实现,可以根据具体的print()的参数去调用print(int)还是print(string)。上面print(12)会去调用print(int),print("hello world")会去调用print(string)


1.2、为什么需要函数重载(why)?
  • 试想如果没有函数重载机制,如在C中,你必须要这样去做:为这个print函数取不同的名字,如print_int、print_string。这里还只是两个的情况,如果是很多个的话,就需要为实现同一个功能的函数取很多个名字,如加入打印long型、char*、各种类型的数组等等。这样做很不友好!
  • 类的构造函数跟类名相同,也就是说:构造函数都同名。如果没有函数重载机制,要想实例化不同的对象,那是相当的麻烦!
  • 操作符重载,本质上就是函数重载,它大大丰富了已有操作符的含义,方便使用,如+可用于连接字符串等!

通过上面的介绍我们对函数重载,应该唤醒了我们对函数重载的大概记忆。下面我们就来分析,C++是如何实现函数重载机制的。

2、编译器如何解决命名冲突的?

为了了解编译器是如何处理这些重载函数的,我们反编译下上面我们生成的执行文件,看下汇编代码(全文都是在Linux下面做的实验,Windows类似,你也可以参考《一道简单的题目引发的思考》一文,那里既用到Linux下面的反汇编和Windows下面的反汇编,并注明了Linux和Windows汇编语言的区别)。我们执行命令objdump -d a.out >log.txt反汇编并将结果重定向到log.txt文件中,然后分析log.txt文件。

发现函数void print(int i) 编译之后为:(注意它的函数签名变为——_Z5printi


发现函数void print(string str) 编译之后为:(注意它的函数签名变为——_Z5printSs


我们可以发现编译之后,重载函数的名字变了不再都是print!这样不存在命名冲突的问题了,但又有新的问题了——变名机制是怎样的,即如何将一个重载函数的签名映射到一个新的标识?我的第一反应是:函数名+参数列表,因为函数重载取决于参数的类型、个数,而跟返回类型无关。但看下面的映射关系:

void print(int i)                    -->         _Z5printi 
void print(string str)         -->         _Z5printSs

进一步猜想,前面的Z5表示返回值类型,print函数名,i表示整型int,Ss表示字符串string,即映射为返回类型+函数名+参数列表。最后在main函数中就是通过_Z5printi_Z5printSs来调用对应的函数的:

80489bc:       e8 73 ff ff ff          call   8048934 <_Z5printi> 
…………… 
80489f0:       e8 7a ff ff ff          call   804896f <_Z5printSs>

我们再写几个重载函数来验证一下猜想,如:

void print(long l)           -->           _Z5printl 
void print(char str)      -->           _Z5printc 
可以发现大概是int->i,long->l,char->c,string->Ss….基本上都是用首字母代表,现在我们来现在一个函数的返回值类型是否真的对函数变名有影响,如:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
using  namespace std;

int max( int a,  int b)
{
     return a >= b ? a : b;
}

double max( double a,  double b)
{
     return a >= b ? a : b;
}
int main()
{
    cout <<  "max int is: " << max( 13) << endl;
    cout <<  "max double is: " << max( 1. 21. 3) << endl;
     return  0;
}

int max(int a,int b) 映射为_Z3maxiidouble max(double a,double b) 映射为_Z3maxdd,这证实了我的猜想,Z后面的数字代码各种返回类型。更加详细的对应关系,如那个数字对应那个返回类型,哪个字符代表哪重参数类型,就不去具体研究了,因为这个东西跟编译器有关,上面的研究都是基于g++编译器,如果用的是vs编译器的话,对应关系跟这个肯定不一样。但是规则是一样的:“返回类型+函数名+参数列表”。

既然返回类型也考虑到映射机制中,这样不同的返回类型映射之后的函数名肯定不一样了,但为什么不将函数返回类型考虑到函数重载中呢?——这是为了保持解析操作符或函数调用时,独立于上下文(不依赖于上下文),看下面的例子

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
float sqrt( float);
double sqrt( double);

void f( double da,  float fla)
{
     float fl = sqrt(da);  //调用sqrt(double)
     double d = sqrt(da);  //调用sqrt(double)

    fl = sqrt(fla);  //调用sqrt(float)
    d = sqrt(fla);  //调用sqrt(float)
}

如果返回类型考虑到函数重载中,这样将不可能再独立于上下文决定调用哪个函数。

至此似乎已经完全分析清楚了,但我们还漏了函数重载的重要限定——作用域。上面我们介绍的函数重载都是全局函数,下面我们来看一下一个类中的函数重载,用类的对象调用print函数,并根据实参调用不同的函数:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>
using  namespace std;

class test
{
public:
     void print( int i)
    {
        cout <<  "int" << endl;
    }
     void print( char c)
    {
        cout <<  "char" << endl;
    }
};
int main()
{
    test t;
    t.print( 1);
    t.print( 'a');
     return  0;
}

我们现在再来看一下这时print函数映射之后的函数名:

void print(int i)                    -->            _ZN4test5printEi

void print(char c)               -->            _ZN4test5printEc

注意前面的N4test,我们可以很容易猜到应该表示作用域,N4可能为命名空间、test类名等等。这说明最准确的映射机制为:作用域+返回类型+函数名+参数列表

3、重载函数的调用匹配

现在已经解决了重载函数命名冲突的问题,在定义完重载函数之后,用函数名调用的时候是如何去解析的?为了估计哪个重载函数最适合,需要依次按照下列规则来判断:

  • 精确匹配:参数匹配而不做转换,或者只是做微不足道的转换,如数组名到指针、函数名到指向函数的指针、T到const T;
  • 提升匹配:即整数提升(如bool 到 int、char到int、short 到int),float到double
  • 使用标准转换匹配:如int 到double、double到int、double到long double、Derived*到Base*、T*到void*、int到unsigned int;
  • 使用用户自定义匹配
  • 使用省略号匹配:类似printf中省略号参数

如果在最高层有多个匹配函数找到,调用将被拒绝(因为有歧义、模凌两可)。看下面的例子:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void print( int);
void print( const  char *);
void print( double);
void print( long);
void print( char);

void h( char c,  int i,  short s,  float f)
{
    print(c); //精确匹配,调用print(char)
    print(i); //精确匹配,调用print(int)
    print(s); //整数提升,调用print(int)
    print(f); //float到double的提升,调用print(double)

    print( 'a'); //精确匹配,调用print(char)
    print( 49); //精确匹配,调用print(int)
    print( 0); //精确匹配,调用print(int)
    print( "a"); //精确匹配,调用print(const char*)
}
定义太少或太多的重载函数,都有可能导致模凌两可,看下面的一个例子:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
void f1( char);
void f1( long);

void f2( char *);
void f2( int *);

void k( int i)
{
    f1(i); //调用f1(char)? f1(long)?
    f2( 0); //调用f2(char*)?f2(int*)?
}
这时侯编译器就会报错,将错误抛给用户自己来处理:通过显示类型转换来调用等等(如f2(static_cast<int *>(0),当然这样做很丑,而且你想调用别的方法时有用做转换)。上面的例子只是一个参数的情况,下面我们再来看一个两个参数的情况:

 C++ Code 
1
2
3
4
5
6
7
int pow( int ,  int);
double pow( doubledouble);

void g()
{
     double d = pow( 2. 02//调用pow(int(2.0),2)? pow(2.0,double(2))?
}
4、编译器是如何解析重载函数调用的?

编译器实现调用重载函数解析机制的时候,肯定是首先找出同名的一些候选函数,然后从候选函数中找出最符合的,如果找不到就报错。下面介绍一种重载函数解析的方法:编译器在对重载函数调用进行处理时,由语法分析、C++文法、符号表、抽象语法树交互处理,交互图大致如下:


这个四个解析步骤所做的事情大致如下:

  • 由匹配文法中的函数调用,获取函数名;
  • 获得函数各参数表达式类型;
  • 语法分析器查找重载函数,符号表内部经过重载解析返回最佳的函数
  • 语法分析器创建抽象语法树,将符号表中存储的最佳函数绑定到抽象语法树上

 

下面我们重点解释一下重载解析,重载解析要满足前面《3、重载函数的调用匹配》中介绍的匹配顺序和规则。重载函数解析大致可以分为三步:

  • 根据函数名确定候选函数集
  • 从候选函数集中选择可用函数集合
  • 从可用函数集中确定最佳函数,或由于模凌两可返回错误
4.1、根据函数名确定候选函数集

根据函数在同一作用域内所有同名的函数,并且要求是可见的(像private、protected、public、friend之类)。“同一作用域”也是在函数重载的定义中的一个限定,如果不在一个作用域,不能算是函数重载,如下面的代码:

 C++ Code 
1
2
3
4
5
6
7
void f( int);

void g()
{
     void f( double);
    f( 1);  //这里调用的是f(double),而不是f(int)
}

内层作用域的函数会隐藏外层的同名函数同样的派生类的成员函数会隐藏基类的同名函数。这很好理解,变量的访问也是如此,如一个函数体内要访问全局的同名变量要用“::”限定。

为了查找候选函数集,一般采用深度优选搜索算法:

step1:从函数调用点开始查找,逐层作用域向外查找可见的候选函数 
step2:如果上一步收集的不在用户自定义命名空间中,则用到了using机制引入的命名空间中的候选函数,否则结束

在收集候选函数时,如果调用函数的实参类型为非结构体类型,候选函数仅包含调用点可见的函数;如果调用函数的实参类型包括类类型对象、类类型指针、类类型引用或指向类成员的指针,候选函数为下面集合的并:

  • (1)在调用点上可见的函数;
  • (2)在定义该类类型的名字空间或定义该类的基类的名字空间中声明的函数;
  • (3)该类或其基类的友元函数;

下面我们来看一个例子更直观:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void f();
void f( int);
void f( doubledouble =  314);
names pace N
{
     void f(char3 , char3);
}
classA
{
public:
    operat  or  double() { }
};
int main ( )
{
     using names pace N;  //using指示符
    A a;
    f(a);
     return  0;
}

根据上述方法,由于实参是类类型的对象,候选函数的收集分为3步:

(1)从函数调用所在的main函数作用域内开始查找函数f的声明, 结果未找到。到main函数 
作用域的外层作用域查找,此时在全局作用域找到3个函数f的声明,将它们放入候选集合;

(2)到using指示符所指向的命名空间 N中收集f ( char3 , char3 ) ;

(3)考虑2类集合。其一为定义该类类型的名字空间或定义该类的基类的名字空间中声明的函 
数;其二为该类或其基类的友元函数。本例中这2类集合为空。

最终候选集合为上述所列的 4个函数f。

4.2、确定可用函数

可用的函数是指:函数参数个数匹配并且每一个参数都有隐式转换序列。

  • (1)如果实参有m个参数,所有候选参数中,有且只有 m个参数;
  • (2)所有候选参数中,参数个数不足m个,当前仅当参数列表中有省略号;
  • (3)所有候选参数中,参数个数超过 m个,当前仅当第m + 1个参数以后都有缺省值。如果可用 
    集合为空,函数调用会失败。

这些规则在前面的《3、重载函数的调用匹配》中就有所体现了。

4.3、确定最佳匹配函数

确定可用函数之后,对可用函数集中的每一个函数,如果调用函数的实参要调用它计算优先级,最后选出优先级最高的。如对《3、重载函数的调用匹配》中介绍的匹配规则中按顺序分配权重,然后计算总的优先级,最后选出最优的函数。

 

5、总结

本文介绍了什么是函数重载、为什么需要函数重载、编译器如何解决函数重名问题、编译器如何解析重载函数的调用。通过本文,我想大家对C++中的重载应该算是比较清楚了。说明:在介绍函数名映射机制是基于g++编译器,不同的编译器映射有些差别;编译器解析重载函数的调用,也只是所有编译器中的一种。如果你对某个编译器感兴趣,请自己深入去研究。

最后我抛给大家两个问题:

  • 1、在C++中加号+,即可用于两个int型之间的相加、也可以用于浮点数数之间的相加、字符串之间的连接,那+算不算是操作符重载呢?换个场景C语言中加号+,即可用于两个int型之间的相加、也可以用于浮点数数之间的相加,那算不算操作符重载呢?
  • 2、模板(template)的重载时怎么样的?模板函数和普通函数构成的重载,调用时又是如何匹配的呢?

指向函数的指针

 (一) 用函数指针变量调用函数

  可以用指针变量指向整形变量、字符串、数组、结构体、也可以指向一个函数。一个函数在编译时被分配一个入口地址。这个入口地址就称为函数指针。可以用一个指针变量指向函数,然后通过该指针变量调用此函数。用简单的数值比较为例:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>

int main()
{
     int max( intint);
     int (*p)( intint);
     int a, b, c;
    p = max;
    scanf( "%d,%d", &a, &b);
    c = (*p)(a, b);
    printf( "a=%d,b=%d,max=%d\n", a, b, c);
     return  0;
}

int max( int x,  int y)
{
     int z;
     if(x > y) z = x;
     else z = y;
     return(z);
}


main函数中的" c = max(a,b); " 包括了一次函数的调用。每一个函数都占用一段内存单元。因此,可以用一个指针变量指向一个函数,通过指针变量来访问它指向的函数。


  第7行:int (*p)( int,int );  用来定义 p 是一个指向函数的指针变量,该函数有两个整形参数,函数值为整形。注意 *p 两侧的括号不可省略,表示 p 先与 * 结合,是指针变量,然后再与后面的 ( ) 结合,表示此指针变量指向函数,这个函数值 (即函数的返回值) 是整形的。如果写成 int *p ( int,int ) ,由于( )的优先级高于 *,它就成了声明一个函数P( 这个函数的返回值是指向整形变量的指针)。


  赋值语句 p = max ; 作用是将函数 max 的入口地址赋给指针变量p。和数组名代表数组首元素地址类似,函数名代表该函数的入口地址。这时 p 就是指向函数 max 的指针变量,此时 p 和 max都指向函数开头,调用 *p 就是调用 max 函数。但是p作为指向函数的指针变量,它只能指向函数入口处而不可能指向函数中间的某一处指令处,因此不能用 *(p + 1)来表示指向下一条指令。


  注意:


  (1) 指向函数的指针变量的一般定义形式为:


  数据类型 (*指针变量名)(函数参数列表)


  这里数据类型就是函数返回值的类型


  (2) int (* p) ( int,int ); 它只是定义一个指向函数的指针变量 p, 它不是固定指向哪一个函数的,而只是表示定义这样一个类型的变量,它是专门用来存放函数的入口地址的。在程序中把哪一函数(该函数的值应该是整形的,且有两个整形参数)的地址赋给它,他就指向哪一个函数。在一个函数中,一个函数指针变量可以先后指向同类型的不同函数。


  (3) p = max; 在给函数指针变量赋值时,只需给出函数名而不必给出函数参数,因为是将函数的入口地址赋给 p ,而不涉及 实参和形参的结合问题,不能写成 p = max(a,b);


  (4) c = (*p)(a,b) 在函数调用时,只需将( *p ) 代替函数名即可,后面实参依旧。


  (5) 对于指向函数的指针变量,像 p++ ,p+n.....是无意义的。


(二) 用指向函数的指针作为函数参数


  函数指针变量通常的用途之一就是把指针作为参数传递到其他函数。


  函数的参数可以是变量、指向变量的指针变量、数组名、指向数组的指针变量,也可以是指向函数的指针也可以作为参数,以实现函数地址的传递,这样就能够在被调用的函数中使用实参函数。


 C++ Code 
1
2
3
4
5
6
7
8
9
void  sub (  int ( *x1) ( int),  int (*x2) ( intint) )
    
{
        int a, b, i, j;
      a = (*x1)(i);
       /* 调用 f1 函数 */
      b = (*x2)(i)(j);
      /* 调用 f2 函数 */       
}

如果实参为两个 函数名 f1 和 f2. 在函数首部定义x1、x2为函数指针变量,x1指向的函数有一个整形形参,x2指向的函数有两个形参。i 和 j 是函数f1 和 f2所要的参数。函数sub的形参 x1、x2(指针变量)在函数 sub 未被调用时并不占用内存单元,也不指向任何函数。在sub被调用时,把实参函数 f1 和 f2的入口地址传给形式指针变量 x1 和 x2.


  既然在 sub 函数中要调用 f1 和 f2 函数,为什么不直接调用f1 和 f2而要用函数指针变量呢? 确实,如果只是用到f1 和 f2 函数,完全可以在sub函数中直接调用f1 和 f2,而不必设指针变量 x1 和 x2。 但是,如果在每次调用sub时,调用的函数不是固定的,下次是f3 和 f4,再是f5 和 f6...这时用指针变量就比较方便了。







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值