C++ Primer是一本C++入门经典的书,已经看过很多遍,每次看的感受都不一样。
变量和基本类型
const限定符:
const说明指针变量,组合的情况可能会显得很复杂。使用指针时要涉及两个目标,即指针本身和指针所指的对象。关于const指针变量,可归结为以下三种:
1.指向常量的指针变量;
2.常指针变量;
3.指向常量的常指针变量。
下面来分别谈谈这三种情况。
一、指向常量的指针变量:
声明格式: const type * var_name;或 type const * var_name;
特点: 可改值。
将指针声明冠以const,使指向的对象为常量,而不是指针为常量。注意:指向常量的指针不一定指向真正的常量,它也可以指向常量,只是从该指针的角度来看,它所指向的对象是常量,通过该指针不能修改它指向的对象。它还可以指向其它的对象,可以不初始化。
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,必须初始化,通过该指针可以修改它指向的对象,但它不可以指向其他的对象。
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 。
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,气候是一个可选的枚举类型,枚举成员
1
2 |
//input is 0,output is 1, enum open_modes {input,output}; |
默认的,第一个枚举成员赋值为0,后面的每个枚举成员赋值比前面大1.
标准库类型
深入理解计算机各种类型大小(sizeof)
1. 用法
1.1 sizeof和new、delete等一样,是关键字,不是函数或者宏。
1.2 sizeof返回内存中分配的字节数,它和操作系统的位数有关。例如在常见的32位系统中,int类型占4个字节;但是在16位系统中,int类型占2个字节。
1.3 sizeof的参数可以是类型,也可以是变量,还可以是常量。对于相同类型,以上3中形式参数的sizeof返回值相同。
1
2 3 4 |
int a;
sizeof(a); // = 4 sizeof( int); // = 4 sizeof( 1); // = 4 |
1.4 sizeof在编译阶段处理。由于sizeof不能被编译成机器码,所以sizeof的参数不能被编译,而是被替换成类型。
- int a = -1;
- sizeof(a=3); // = sizeof(a) = sizeof(int) = 4
- cout<<a<<endl; // 输出-1。由于“=”操作符返回左操作数的类型,赋值操作没有执行。
2.1 基本类型
- sizeof(int); // = 4
- sizeof(double); // = 8
- sizeof(char); // = 1
- sizeof(bool); // = 1
- sizeof(short); // = 2
- sizeof(float); // = 4
- sizeof(long); // = 4
指针在32位系统中占4个字节。
- sizeof(int *); // = 4
- sizeof(double *); // = 4
- sizeof(char *); // = 4
2.3.1 数组的sizeof返回整个数组所占的字节数,即(数组元素个数×每个元素所占字节)。
- int ai[] = {1, 2};
- sizeof(ai); // = 2*4 = 8
- int *pi = new int[10]; //这是指针
- sizeof(pi); // = 4
- int ai[10];
- int *p = ai; //这还是指针
- sizeof(p); // = 4
- double* (*a)[3][6]; //看成(double *) (*a)[3][6],即一个3×6的二维数组,数组元素为指针,指向double类型。
- sizeof(a); // = 4,a为指向上述二维数组的指针
- sizeof(*a); // = sizeof(double *)*3*6 = 72,*a表示上述二维数组
- sizeof(**a); // = sizeof(double *)*6 = 24,**a即*(*a),表示double*[6],是元素为double指针的一维数组。
- sizeof(***a); // = sizeof(double *) = 4,表示上述一维数组中的第一个元素,元素类型为double指针。
- sizeof(****a); // = sizeof(double) = 8,表示上述数组首元素指向的double类型。
- void acf(char p[3]) //参数类型是int[],表示指向int的指针
- {
- sizeof( p ); // = 4
- }
- void aif(int p[]) //参数类型是int[],表示指向int的指针
- {
- sizeof( p ); // = 4
- }
- void pif(int (*p)[6]) //参数类型是int (*)[6],表示指向int数组的指针
- {
- sizeof( p); // = 4
- sizeof( *p ); // = sizeof(int)*6 = 24
- }
- void ppf(int *p[6]) //参数类型是int *[],表示指向int指针的指针
- {
- sizeof( p ); // = 4
- sizeof( *p ); // = 4
- }
2.4.1 空类或空结构体占一个字节。
- class CEmpty { };
- sizeof(CEmpty); // = 1
- struct SEmpty { };
- sizeof(SEmpty); // = 1
2.4.2 非空类和结构体所占字节为所有成员占字节的和,但是不包括成员函数和静态成员所占的空间。
- class CInt : public CEmpty {
- int i;
- };
- sizeof(CInt); // = 4;
- class CFunc {
- void f() {}
- };
- sizeof(CFunc); // = 1;
- struct SInt : SEmpty {
- static int i;
- };
- sizeof(SInt); // = 1;
2.4.3 字节对齐
为了加快计算机的取数速度,编译器默认对内存进行字节对齐。对结构体(包括类)进行字节对齐的原则是:
1)结构体变量的首地址能够被其最宽基本类型成员的大小所整除;2)结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
3)结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。
- struct SByte1
- {
- double d; // 偏移量0~7
- char j; // 偏移量8
- int a; // 偏移量12~15,由于9不能整除4,故先填充9~11
- };
- sizeof(SByte1); // = 16
- struct SByte2
- {
- char j; // 偏移量0
- double d; // 偏移量8~15,由于1不能整除8,故先填充1~7
- int a; // 偏移量16~19
- };
- sizeof(SByte2); // = 24,为了凑成8的倍数,填充20~23
- #pragma pack(push) //保存对齐状态
- #pragma pack(4) //设定为4字节对齐
- class CByte
- {
- char c; //偏移量0
- double d; //偏移量4~11,由于1不能整除4,故先填充1~3
- int i; //偏移量12~15
- };
- #pragma pack(pop) //恢复对齐状态
- sizeof(CByte); // = 16
2.4.4 位域
有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态,用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域, 并说明每个区域的位数。
2.4.4.1 位域以比特位作为单位,其长度不能大于一个字节。一个位域必须存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。
- struct SBit1
- {
- char a : 3;
- char b : 4;
- char c : 5;
- };
- sizeof(SBit1); // = (3+4+1+5+3) bits = 2 bytes
- struct SBit2
- {
- char a : 3;
- char : 0; // 空域
- char b : 4;
- char c : 5;
- };
- sizeof(SBit2); // = (3+4+1+5+3) bits = 3 bytes
2.5 联合
联合表示若干数据成员取其一,故以叠加方式分配内存,所占字节数为最大数据成员所占的字节数。
- union U
- {
- int i;
- char c;
- double d;
- };
- sizeof(U); // = Max(sizeof(i), sizeof(c), sizeof(d)) = sizeof(d) = 8
数组和指针
指针与typedef
我们直接上代码:
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; |
错误原因在于:声明const pstring时,const修饰的是pstring的类型,这是一个指针。
表达式
显示转换
dynamic_cast 支持运行时识别指针或引用所指向的对象。
const_cast 顾名思义,将转换掉表示式的const性质。
1
2 |
const
char *p_str;
char *pc = string_copy( const_cast< char *>(p_str)); //去掉cont性质 |
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 通常为操作数的位模式提供较低层次的重新解释。
语句
1
2 3 4 5 6 |
int main()
{ #ifdef NDEBUG //如果NDEBUG有定义则跳过下面的代码 cerr << "start main" << endl; #endif } |
调试技能:
使用assert进行断言预处理宏
函数
1
2 3 4 5 |
const stringg &manip(
const string &s)
{ string ret = s; return ret; //wrong } |
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; } |
static局部对象确保不迟于在程序执行流程第一次经过该对象定义语句时初始化。当函数调用结束时,静态局部变量的结果不会被撤销。
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;不是想要的结果。
C++的函数重载
函数重载的重要性不言而明,但是你知道C++中函数重载是如何实现的呢(虽然本文谈的是C++中函数重载的实现,但我想其它语言也是类似的)?这个可以分解为下面两个问题
- 1、声明/定义重载函数时,是如何解决命名冲突的?(抛开函数重载不谈,using就是一种解决命名冲突的方法,解决命名冲突还有很多其它的方法,这里就不论述了)
- 2、当我们调用一个重载的函数时,又是如何去解析的?(即怎么知道调用的是哪个函数呢)
这两个问题是任何支持函数重载的语言都必须要解决的问题!带着这两个问题,我们开始本文的探讨。本文的主要内容如下:
- 1、例子引入(现象)
- 什么是函数重载(what)?
- 为什么需要函数重载(why)?
- 2、编译器如何解决命名冲突的?
- 函数重载为什么不考虑返回值类型
- 3、重载函数的调用匹配
- 模凌两可的情况
- 4、编译器是如何解析重载函数调用的?
- 根据函数名确定候选函数集
- 确定可用函数
- 确定最佳匹配函数
-
5、总结
1、例子引入(现象)
1.1、什么是函数重载(what)?
函数重载是指在 同一作用域内 ,可以有一组具有 相同函数名 , 不同参数列表 的函数,这组函数被称为重载函数。重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处。看下面的一个例子,来体会一下:实现一个打印函数,既可以打印int型、也可以打印字符串型。在C++中,我们可以这样做:
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; } |
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….基本上都是用首字母代表,现在我们来现在一个函数的返回值类型是否真的对函数变名有影响,如:
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( 1, 3) << endl; cout << "max double is: " << max( 1. 2, 1. 3) << endl; return 0; } |
int max(int a,int b) 映射为_Z3maxii、double max(double a,double b) 映射为_Z3maxdd,这证实了我的猜想,Z后面的数字代码各种返回类型。更加详细的对应关系,如那个数字对应那个返回类型,哪个字符代表哪重参数类型,就不去具体研究了,因为这个东西跟编译器有关,上面的研究都是基于g++编译器,如果用的是vs编译器的话,对应关系跟这个肯定不一样。但是规则是一样的:“返回类型+函数名+参数列表”。
既然返回类型也考虑到映射机制中,这样不同的返回类型映射之后的函数名肯定不一样了,但为什么不将函数返回类型考虑到函数重载中呢?——这是为了保持解析操作符或函数调用时,独立于上下文(不依赖于上下文),看下面的例子
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函数,并根据实参调用不同的函数:
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中省略号参数
如果在最高层有多个匹配函数找到,调用将被拒绝(因为有歧义、模凌两可)。看下面的例子:
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*) } |
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*)? } |
1
2 3 4 5 6 7 |
int pow(
int ,
int);
double pow( double, double); void g() { double d = pow( 2. 0, 2) //调用pow(int(2.0),2)? pow(2.0,double(2))? } |
4、编译器是如何解析重载函数调用的?
编译器实现调用重载函数解析机制的时候,肯定是首先找出同名的一些候选函数,然后从候选函数中找出最符合的,如果找不到就报错。下面介绍一种重载函数解析的方法:编译器在对重载函数调用进行处理时,由语法分析、C++文法、符号表、抽象语法树交互处理,交互图大致如下:
这个四个解析步骤所做的事情大致如下:
- 由匹配文法中的函数调用,获取函数名;
- 获得函数各参数表达式类型;
- 语法分析器查找重载函数,符号表内部经过重载解析返回最佳的函数
- 语法分析器创建抽象语法树,将符号表中存储的最佳函数绑定到抽象语法树上
下面我们重点解释一下重载解析,重载解析要满足前面《3、重载函数的调用匹配》中介绍的匹配顺序和规则。重载函数解析大致可以分为三步:
- 根据函数名确定候选函数集
- 从候选函数集中选择可用函数集合
- 从可用函数集中确定最佳函数,或由于模凌两可返回错误
4.1、根据函数名确定候选函数集
根据函数在同一作用域内所有同名的函数,并且要求是可见的(像private、protected、public、friend之类)。“同一作用域”也是在函数重载的定义中的一个限定,如果不在一个作用域,不能算是函数重载,如下面的代码:
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)该类或其基类的友元函数;
下面我们来看一个例子更直观:
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( double, double = 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)的重载时怎么样的?模板函数和普通函数构成的重载,调用时又是如何匹配的呢?
指向函数的指针
(一) 用函数指针变量调用函数
可以用指针变量指向整形变量、字符串、数组、结构体、也可以指向一个函数。一个函数在编译时被分配一个入口地址。这个入口地址就称为函数指针。可以用一个指针变量指向函数,然后通过该指针变量调用此函数。用简单的数值比较为例:
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( int, int); int (*p)( int, int); 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.....是无意义的。
(二) 用指向函数的指针作为函数参数
函数指针变量通常的用途之一就是把指针作为参数传递到其他函数。
函数的参数可以是变量、指向变量的指针变量、数组名、指向数组的指针变量,也可以是指向函数的指针也可以作为参数,以实现函数地址的传递,这样就能够在被调用的函数中使用实参函数。
1
2 3 4 5 6 7 8 9 |
void sub (
int ( *x1) (
int),
int (*x2) (
int,
int) )
{ 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...这时用指针变量就比较方便了。