番外篇--深扒C++函数

本文最后更新于:2023年8月30日 下午

函数指针

获取函数的地址:函数的名字就是地址

函数指针声明与使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//函数原型
double pam(int);

//函数指针声明和赋值
double (*pf)(int); //函数指针声明
pf=pam; //函数指针赋值
double (*pf)(int)=pam; //函数指针声明和赋值
auto pf=pam; //使用C++11的自动类型推断来声明和赋值

//使用
pf(123);
(*pf)(123); //注意(*pf)的括号一定要保留下来,不然意义就不同了
pam(123);
//这三种形式等价,之所以会这样是因为两种学派的理解不同;而我个人认为第一种用法比第二种用法合理;以后在使用时看到(*pf)(123),你直接看成pf(123)即可,将(*)看成多余的就好

---------------------------------------------------------------------------------------

//函数原型
double f1(int);
double f2(int);
double f3(int);

//函数指针数组的声明和赋值
double (*pfa[3])(int); //函数指针数组的声明
pfa[0]=f1; //函数指针数组的赋值示例
double (*pfa[3])(int)={f1,f2,f3}; //函数指针数组的声明和赋值
//不可以使用auto来自动初始化,因为auto只能用于单值初始化,而不能初始化列表
//ps:初始化=声明+赋值;初始化列表就是{}

---------------------------------------------------------------------------------------

//老实说,C++的指针声明挺复杂,如果能够很好的理解下面的声明,就证明你对C++的声明了如指掌了
const double *(*(*pd)[3])(const double *,int);
//理解所有指针声明的要点是,从里向外去解读;注意[]的优先级比*高

---------------------------------------------------------------------------------------
//要简化函数指针的声明过程,除了使用auto之外,还可以使用typedef,例如:

//函数原型
double pam(int);

//将函数指针类型弄成标识符形式
typedef double (*p_func)(int);

//函数指针声明
p_func pf;

非静态类成员函数指针

与类的非静态数据成员变量不同,类的非静态成员函数不会在运行时为每个类的实例对象生成一份专有的机器语言代码,类的非静态成员函数在内存中只有一份,即类的非静态成员函数的地址也是唯一的。那么为什么不同的实例调用非静态成员函数的时可以访问到属于实例本身的数据呢?这是因为非静态成员函数默认的第一个参数就是this指针,只是没有明写出来罢了

类的非静态成员函数默认的第一参数就是this

受this指针的影响, 非静态类成员函数指针 的声明和使用与 一般的函数指针(含静态成员函数指针) 都不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//类的非静态成员函数指针 声明、赋值、使用 都跟普通情况不同
//声明时需要添加类名
int (classA::*pFun)(int); //注意类名放置的位置,类名是用来指明this指针的类型的
//赋值时需要添加类名和&
pFun=&classA::Fun;
//使用时需要加上实例对象
(classAInstance->*pFun)(12345); //注意一个'('')''*'都不能少,格式就这样死死固定住了;
//如果这个语句出现在非静态成员函数中,不要认为classAInstance不写系统会自动加上this;如果你是想调用this的pFun就自己手动加上,不然会报错,


//类的静态成员函数指针 声明、赋值、使用
//声明时
int (*pFun)(int);
//赋值时需要添加类名
pFun=classA::Fun;
//使用时
pFun(12345); //
(*pFun)(12345);

函数模板

函数模板将类型作为参数传递给函数模板,使编译器生成该类型的函数;又叫通用编程或参数化类型;

由函数模板对应的具体函数称为称为模板实例(instantiation)

隐式实例化、显式实例化、显式具体化统称为具体化(specialization)

类型参数不能有默认值,但是非类型参数可以有默认值

函数模板的声明格式

1
2
3
// T 叫做类型参数
template<typename T> //可以写成 template<class T>,但是这个是C++98之前的写法;typename这个关键字是C++98定义的;函数模板参数列表中可以有多个参数,如template<typename T1,typename T2>
void Swap(T &a,T &b); //返回类型可以不可也写成T???

函数模板的定义格式

这只是一个函数模板,并不是函数;函数模板用于告诉编译器如何定义函数;只有在编译时检测到代码中需要用到这个模板,编译器才会依据具体类型按照模板格式生成模板实例,这个过程称为隐式实例化(implicit instantiation)

编译结束之后机器码中并没有函数模板,只可能存在模板实例的机器码

模板函数的定义可以放在使用到函数模板的函数后面

1
2
3
4
5
6
7
8
template<typename T>			
void Swap(T &a,T &b)
{
T temp;
temp=a;
a=b;
b=temp;
}

模板函数的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//使用时都不用指明类型,编译器会自动检测参数列表的数据类型来确定函数的类型
int main()
{
int i=10;
int j=20;

Swap(i,j); //隐式实例化
Swap<>(i,j); //不带参数的显式实例化,确保编译器使用函数模板的实例化版本

double x=1.3;
double y=3.6;

Swap(x,y); //隐式实例化
Swap<>(x,y); //不带参数的显式实例化,确保编译器使用函数模板的实例化版本

//疑问:
//Swap(i,x); //会怎样?

return 0;
}

函数模板的重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
template<typename T>			
void Swap(T &a,T &b)
{
T temp;
temp=a;
a=b;
b=temp;
}

template<typename T>
void Swap(T *a,T *b,int n)
{
T temep;
for(int i=0;i<n;++i)
{
temp=a[i];
a[i]=b[i];
b[i]=temp;
}
}

int main()
{
int i=10;
int j=20;

Swap(i,j);

int array1[3]={1,2,3};
int array2[3]={4,5,6};

Swap(array1.array2);

return 0;
}

函数模板的局限性

1
2
3
4
5
6
7
template <typename T>
void f(T a,T b)
{
//a=b; //原本是想做内容赋值操作的,但如果T类型是数组指针,那么最终只是a变成了b的指针,a并没有获得b数组的内容;又或者当前T类型并没有'='操作;
//a>b; //原本是想做内容比较操作的,但如果T类型是数组指针,那么最终只是比较了a指针和b指针,并没有进行内容比较;又或者当前T类型并没有'>'操作;
//以后注意T只处理非数组类型,T*才是用来处理数组类型的;
}

函数模板的显式具体化(explicit specialization)

所谓的显式具体化就是手动写一个函数模板的模板实例

函数模板显式具体化的声明格式:

1
2
3
4
5
6
7
8
9
10
11
//函数模板
template <typename T>
void Swap(T &,T &);

//函数模板的显式具体化,第一种写法
template <>
void Swap<job>(job &,job &); //注意参数列表一定要和对应的模板相同;

//函数模板的显式具体化,第二种写法
template <>
void Swap(job &,job &); //注意参数列表一定要和对应的模板相同;

如果一个名称f同时存在非模板函数、 显式具体化函数、常规模板,将遵循第三代函数使用优先顺序(C++98标准):非模板函数 > 显式具体化函数 > 常规模板

实例化和具体化

  • 实例化和具体化是不同的概念:
    • 实例化是由编译器自动生成模板实例的过程
    • 具体化是手动写出一个模板实例的过程
    • 实例化 template 后面没有 <>
    • 具体化 template 后面有 <>

显式实例化(explicit instantiation)的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//函数模板
template <typename T>
void Swap(T &,T &);

//函数模板的显式实例化,第一种方式
template void Swap<int>(int,int); //编译器看到这条指令将马上自动生成Swap的int版本,不必等到调用;这样做有什么用?

//函数模板的显式实例化,第二种方式
...主函数中
int i=10;
double x=1.3;
Swap<double>(i,x); //生成Swap的double类型的实例以备调用,如果没有显式实例化的话,到这里会报错;因为没有办法隐式实例化;
//ps:但是其实即使使用了显式化,这里由于Swap的参数是double引用类型,可是变量i是int类型;同样会报错,除非Swap函数参数列表改成Swap(const T &,T &),[笑哭.jpg]这样有意思吗
...

ps:同一个文件中如果同时出现同一类型函数的 显式实例化 和 显式具体化 ,将出错

编译器选择使用哪个函数版本

重载解析(overloading resolution):面对函数重载、函数模板、函数模板重载时,C++决定使用哪一个函数的过程

详细解释这个策略需要将近一章的篇幅= =,下面只讲大致步骤

函数调用的步骤(假设函数调用为may('B'))

  1. 先找出所有名称为may的函数
  2. 再从这些函数中找出可以只接受一个参数'B'的may函数(实参类型可强转为形参类型的也算)
  3. 再从这些函数中选择一个最佳函数,优先级为: 常规函数完全匹配 > 模板函数完全匹配 > 提升转换(例:char和short自动转为int,float自动转为double) > 标准转换(例:int转换为char,long转换为double) > 用户定义的转换

完全匹配:实参类型与形参类型完全等价,等价表如下所示:

ps:倒数第二列是不是错了???

  • 如果使用以上优先级选择之后还是剩下一个以上的完全匹配函数,那么将要使用以下规则继续筛选:
    • 非模板函数优先于模板函数
    • 非const的指针和引用的实参优先与非const的指针和引用的形参配对
    • 如果剩下的都是完全匹配的模板函数,则使用函数模板的部分排序规则(partial ordering rules)(C++98特性,主要是处理指针与非指针的问题)来选择最具体的

如果使用以上规则选择之后还是剩下一个以上的候选函数,那么编译器将报错;

例子:调用函数map('B');

编号 候选函数 第一步结果 第二步结果 优先级
1 void may(int); T T 4
2 void may(double,double); T F
3 float may(float,float = 3); T T 5
4 void may(char); T T 1
5 char * may(const char *); T F
6 char may(const char &); T T 1
7 template< typename T > void may(const T &); T T 3
8 template< typename T > void may(T *); T F

ps:整数类型是不会隐式转换成指针的

以上只是介绍了单参函数调用的函数原型匹配问题,若是有多个参数,情况将非常复杂;这里不再介绍

模板函数的发展

问题一

在C++98标准下的函数模板存在着很多不完美的地方,有时候由于不知道具体的类型是什么,导致无法正常编写函数模板定义,例:

1
2
3
4
5
6
7
template<typename T1,typename T2>
void ft(T1 x,T2 y)
{
...
?type? xpy = x + y; //这时不知道xpy要定义为什么类型才能正确接收到结果
...
}

为了解决上面的问题,C++11引入了一种新的声明数据类型方式,使用关键字decltype

decltype是在运行时确定变量类型的

decltype确定类型的方式,满足以下哪个形式就用哪个形式的判定方法

1
2
3
4
5
6
7
8
9
10
11
12
13
//形式一:
...
int x;
decltype{x} y; //判定方法:y的类型将为{}中的**数值**类型;这里y的类型将会是int

//更多例子:
int j = 3;
int &k = j;
int &n = j;
decltype(j+6) i1; //i1的类型为 int
decltype(100L) i2; //i2的类型为 long
decltype(k+n) i3; //i3的类型为 int
...
1
2
3
4
5
6
//形式二:
...
long indeed(int); //函数声明
decltype{indeed(3)} y; //判定方法:y的类型将为{}中函数的返回值类型;这里y的类型将会是long(书上写说y的类型是int,错了吧?)
//注意:indeed(3)并不会真的被调用
...
1
2
3
4
5
6
7
8
9
10
11
12
//形式三:
...
double xx = 4.4;
decltype{(xx)} y; //判定方法:y的类型将为{}中变量类型的引用;这里y的类型将会是double &
//注意:这里的括号不能省略且括号中必须是变量(左值)


//这里引入一个和函数模板话题不沾边的小概念:
//xx = 98.6;
//(xx) = 98.6;
//以上的两条表达式的作用是一样的,就是赋值,无差别
...

于是上面的问题就可以解决:

1
2
3
4
5
6
7
8
9
10
11
template<typename T1,typename T2>
void ft(T1 x,T2 y)
{
...
decltype{x + y} xpy = x + y; //xpy的类型就是x+y得到的结果数值的类型
...

//如果要多次使用上面这种类型,每次都要写这么长,会觉得很麻烦,那么可以使用关键字typedef,例:
typrdef decltype{x + y} xpytype;

}

问题二

但是即使有了decltype还是存在问题,有时函数返回类型不能确定,例:

1
2
3
4
5
6
template<typename T1,typename T2>
?type? gt(T1 x,T2 y) //返回类型无法确定,无法将?type?改为decltype{x + y},因为那时x和y还没有声明
{
'''
return x + y;
}

为了解决上面的问题,C++11引入了一个新语法后置返回类型,格式为:

1
2
auto h(int x,float y) -> double;
//该函数的返回类型是double,auto只是占位用的

番外篇--深扒C++函数
https://roudersky.com/posts/f02e98d5.html
作者
Rouder
发布于
2017年11月8日
许可协议