目录:点我
一、函数原型
1. 不指定参数列表
void say_bye(...); // 使用省略号
2. ANSIC 不指定参数列表
void say_bye(); // 括号为空
3. 函数返回过程
函数通过将返回值复制到指定的 CPU 寄存器或内存单元中来将其返回,随后,调用程序将查看该内存单元。返回函数和调用函数必须就该内存单元中存储的数据的类型保持一致。
4. 为什么需要函数原型
原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型以及参数的类型和数量告诉编译器,将确保以下几点:
- 编译器正确处理函数的返回值;
- 编译器检查使用的参数数目是否正确;
- 编译器检查使用的参数类型是否正确,如果不正确,尝试转换为正确的类型。
编译器为什么需要函数原型?如果没有函数原型,编译器将不得不在文件中进行查找,一方面这样做效率不高,在查找过程中会停止对主函数的编译;另一方面由于 C++ 允许将一个程序放在多个文件中,因此函数可能不在该文件中。唯一避免使用函数原型的方法是在首次使用函数之前定义它,但是 C++ 的编程风格要求将主函数放在最前面,因此建议还是使用函数原型。
二、参数声明
传递给函数的变量叫做实参(argument),函数接收后生成的副本叫做形参(parameter)。
1. 指针与 const
:
一方面保证常量不会被错误的改变,另一方面使用 const
使得函数能够处理 const
和非 const
实参,若不使用 const
则只能处理非 const
实参:
const int a = 1;
const int *pt = &a; // valid
const int b = 2;
int *pe = &b; // invalid
int c = 3;
const *pc = &c; // valid
*pc = 1; // invalid
const
指针还有一个特性:
int a = 1, b = 2;
const int *pt = &a;
pt = &b; // valid
*pt = 3; // invalid
因此可以把 const
指针理解为不可改变值的指针,但是该指针可以改变地址。要想防止指针改变地址,可以采取以下定义:
int a = 1, b = 2;
int * const pt = &a;
pt = &b; // invalid
还可组合使用:
int a = 1, b = 2;
const int * const pt = &a;
pt = &b; // invalid
*pt = b; // invalid
2. 数组
对于一维数组,使用 int arr[]
和 int *arr
均是正确的形参定义,因为他们均表示数组中第一个元素的地址。
使用二维数组最重要的是如何正确的进行指针声明,假设此时调用一个二维数组:data[3][4]
,大小为三行四列,调用函数为:int total = Fun(data, 3);
,此时函数的声明如下:
int Fun(int (*data)[4], int size); // 第一种方式
int Fun(int data[][4], int size); // 第二种方式
该形参表示定义一个指向由 4 个 int
组成的数组的指针,对于第一种方式,若不加括号则表示定义一个由 4 个指向 int
的指针组成的数组,具体原理见上一章。由于这种定义限定了列的长度,而没有限定行的长度,因此实参数组的行可以是任意的。
三、函数指针
普通的函数调用是在某个函数中调用另外一个函数,被调用函数需要提前在调用函数中声明才可以使用;而函数指针则提供了一种新的方法,即使用指针将函数的地址当作参数进行传递,本质上与其他指针调用的操作并无区别,但却很方便的使用户能够随意的更换调用函数,以满足不同的实际需求。
1. 获取函数地址
一个函数:Fun(a, b);
直观来看分为名:Fun
,和参:(a, b)
。但是这里需要区分清楚函数的“地址”和函数的“名”两个概念:
- 函数的名:实际上就是:
Fun(a, b)
。 - 函数的地址:实际上是:
Fun
。
在传递参数时,一定要注意传递的是函数名还是函数地址。
2. 声明函数指针
声明指向函数的指针时,需要指定指针指向的函数类型。这意味着声明应指定指针的返回类型以及函数的特征标(参数列表)。举个栗子:
//--- 函数原型 ---//
double pam(int);
//--- 函数指针声明 ---//
double (*pf)(int);
// 指定double为返回类型;(*pf)函数指针变量名,用括号保证优先级;(int)为参数列表
pf = pam; // 给函数指针pf赋值
编写函数指针时,编写原型较难,但传递地址则非常简单,例如:
Fun(pam); // 直接传递地址即可
3. 使用指针来调用函数
由上述讨论已经知道函数指针为pf
,因此调用函数时可以直接使用(*pf)
作为函数名(直观上看的函数名)来使用,举个栗子:
pam(4); // 原本的函数调用
(*pf)(4); // 函数指针的调用
pf(4); // C++允许直接使用函数指针进行调用
四、内联函数
经典的函数调用时,系统将会进行程序的跳转,在函数执行结束后再跳转回来,这种方式函数只需一个副本。而内联函数则是将函数体插入要执行的位置,程序无需跳转,顺序执行即可,这种方式函数需要多个副本,增加了内存开销,但节省了程序跳转的时间开销。由于内联函数的特性,因此不能用于定义递归功能的函数。
1. 定义方法
只需在函数定义前加上 inline
即可,由于内联函数一般要求短小精悍,因此通常省略函数原型的声明,而是将整个函数定义在函数原型处,举个栗子:
inline int Fun(int a) {return a * a;}
2. 与宏的区别
前面文章中我们提到过,宏:#define
实际上就是一种文本替换,会带来一些意外的问题,因此使用内联函数或者别名更加安全可靠。
五、引用变量
左值:可被引用的数据对象(变量、数组元素、结构成员、引用和解除引用的指针);
非左值:不可被引用的数据对象(除字符串外的字面常量、多项表达式);
const :引入关键字 const 后,常规变量和 const 变量由于可通过地址访问,因此均可视为左值,但常规变量属于可修改左值,而 const 则不可修改。
1. 定义
C++新特性中定义了引用变量,可以使其作为其他变量的别名来使用,举个栗子:
int rats;
int & rodents = rats; // &不是地址运算符,而是类型标识符的一部分,即int &是一个整体
引用看起来跟指针非常相似,但有一个很大的区别,即引用需要在定义时赋值,而指针则是先定义,再赋值。引用更像是 const 指针,一旦定义即确定下来,不像指针那样可以再指向其他值。
引用通常使用在函数的形参中,最常见的按值传递需要创建一个副本,而引用传递则无需副本,因此有更高的性能和效率。
2. 按引用传递
按引用传递很常见,但是很多人一直将其当作地址传递来对待(虽然效果上一样),举个栗子马上就能明白:
Fun(times); // 调用函数
void Fun(int x); // 普通传递方式,首先创建int型变量x,再将times的值赋给x使用
void Fun(int &x); // 按引用传递,将x作为times的别名进行使用
引用函数按值传递还有一个不同之处,按值传递允许下面的使用方式:
// 1.
Fun(times + 3);
// 2.
int k = 3;
Fun(k);
// 3.
int k[3] = {1, 2, 3};
Fun(k[2]);
但是按引用传递则不允许,理由是按引用传递所传递的是变量而不是值,举个错误栗子:
Fun(times + 3); // 错误
此时错误的原因是:
times + 3 = times; // 这样是错误的,times + 3不是变量
3. 当引用与 const 冲突
当按引用传递与 const 的传递冲突时,系统通常会生成一个新的副本,来避免冲突,当然这样做也牺牲了引用传递的特性,但是保护了原始数据。举个栗子:
Fun(const int & x); // 若函数中修改了x,那么系统会保护原始数据,将其作为按值传递处理
使用 const 的好处:
- 可以避免无意中修改数据的编译错误;
- 能够处理 const 和非 const 两种实参;
- 函数能正确生成并使用临时变量。
若要使用返回引用,考虑下面这种情况:
int & fun(int &ret) {
return ret;
}
int x = 1;
fun(x) = 2;
此时编译器不会报错,因为函数返回值为引用类型,是一个左值,因此可以对其进行赋值。这将会导致一些潜在错误,因此可以使用 const 来限定:
const int & fun(int &ret) {
return ret;
}
此时不会出现上述情况的问题。
4. 右值引用
右值引用使用 && 表示:
double && rref = sqrt(36.00); // 6.0
double j = 15.0;
double && jref = 2.0 * j + 18.5; // 48.5
5. 何时使用引用参数
- 需要修改调用函数中的数据对象;
- 针对结构、类等数据类型的传递时,能够大幅提高程序运行速度;
- 使用传递的值但不做修改:
- 如果数据对象很小,应采用按值传递;
- 如果数据对象是数组,则使用 const 指针;
- 如果数据对象是较大的结构,则使用 const 指针或 const 引用;
- 如果数据对象是类对象,则使用 const 引用;
- 使用传递的值并且修改:
- 如果数据对象是内置数据类型,则使用指针;
- 如果数据对象是数组,则只能使用指针;
- 如果数据对象是结构,则使用引用或指针;
- 如果数据对象是类对象,则使用引用。
六、默认参数
当传递的参数缺省时,则会对其进行默认值处理,举个栗子:
// 函数定义
int Fun(int a, int b = 7) {
return a * b;
}
// 函数使用
int a = 1, b = 5;
Fun(a); // 结果为1 * 7 = 7
Fun(a, b); // 结果为1 * 5 = 5;
七、函数重载
函数重载是 C++ 多态性的体现,对相同的函数使用不同的参数列表来满足不同的需求。举个栗子:
// 函数定义
int Fun(int a, int b);
int Fun(double a, double b);
// 函数使用
Fun(1, 2); // 使用第一种定义
Fun(1.0, 2.0); // 使用第二种定义
由于函数重载要求特征标(参数列表)不同,因此下面的定义方式是错误的:
int Fun(int a, int b);
double Fun(int a, int b);
使用函数重载需要判断一下,默认参数能解决的问题就没有必要使用重载来解决了。对于函数重载的不同版本,编译器使用名称修饰来区分版本,简单来说就是用某种编码方式做标记,将不同的函数重载类型进行特定的编码,例如:
long fun(int, float); // 函数声明
?fun@@YAXH // 名称修饰
八、函数模板
优先级:非模板 > 显式具体化 > 模板
1. 通用模板
有时候对于 int 型的变量和 double 型的变量,需要构造两个相同操作的函数来处理,这就增加了开发者繁琐而不必要的工作量,因此可以使用函数模板来进行快捷操作,举个栗子:
// 定义
template <typename AnyType> // typename关键字是新增特性,老版本中使用关键字class,AnyType通常也被简写作T
void Swap(AnyType &a, AnyType &b) {
AnyType temp = a;
a = b;
b = temp;
}
// 使用
int a = 1, b = 2;
Swap(a, b);
double a = 1.0, b = 2.0;
Swap(a, b);
此时编译器会根据传入的参数类型自动按照模板生成需要的函数并进行使用,无需开发者额外编写。特别注意的是,面对相同结果但不同的算法(例如值、指针等操作),模板也可以使用函数重载进行处理。
2. 局限性以及解决方案
在使用常规变量时不会出现错误,但是当使用结构体等高级变量时则容易出现问题,举个栗子:
// 部分模板代码
{
if(a > b)
return true;
}
但是假如a和b的类型是结构体,因此不存在比大小的操作,此时程序错误。
面对这种情况,有两种解决方法:
- 重载运算符
- 为特定类型提供具体化模板定义
3. 显示具体化
// 结构体
struct job{
string name;
string time;
int num;
};
// 函数原型
template <> void Swap<job>(job &a, job &b);
// 函数定义,假设只需要交换name和time
template <> void Swap<job>(job &a, job &b) { // 显式具体化的定义
string name = a.name, time = a.time;
a.name = b.name;
a.time = b.time;
b.name = name;
b.time = time;
}
在这里要区分实例化与具体化的差别:
- 由编译器判断生成的实例叫做隐式实例化,举个栗子:
Swap(a, b); // 由编译器自己判断类型,因此是隐式实例化
- 由人为的显示定义的实例叫做显式实例化,举个栗子:
Swap<int>(a, b); // <int>内指明了变量类型,因此是显式实例化
但是!!!显式实例化、隐式实例化、显式具体化统称为具体化。它们的相同之处在于它们都表示使用了具体类型的函数定义。
4. 版本选择
大致过程:
- 创建候选函数列表,其中包含与被调用函数的名称相同的函数和函数模板;
- 使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,曲中包含实参类型与相应的形参类型完全匹配的情况;
- 确定是否具有最佳的可行函数,如果有则调用它,繁殖调用出错。
举个栗子,对于may('B');
,有如下版本:
void may(int); // #1
float may(float, float = 3); // #2
void may(char); // #3
char * may(const char *); // #4
char may(const char &); // #5
template<class T> void may(const T &); // #6
template<class T> void may(T *); // #7
首先,由于整型不能被隐式转换为指针类型,因此 #4 和 #7 不可行;
然后,对于剩余 5 各版本需要进行排序,原则如下:
- 完全匹配,但常规函数优先于模板;
- 提升转换(例如 char 和 shorts 自动转换为 int ,float 自动住那换为 double);
- 标准转换(例如 int 转换为 char ,long 转换为 double);
- 用户自定义转换,如类声明中定义的转换。
此时因为 char 到 int 为提升转换,而 char 到 float 为标准转换,因此 #1 优于 #2 ;#3 、#5 和 #6 均为完全匹配,而 #6 是模板,因此优先级低于另外 2 个。,此时出线 2 个完全匹配,通常这是一种错误,但存在例外:
- 完全匹配和最佳匹配:由于 C++ 存在某些无关紧要的转换(例如 char 到 const char),因此 #3 将是最佳匹配,因为 #4 存在无关紧要的转换。
5. 发展
-
decltype 关键字:由于在模板中使用类型容易出现无法确定类型的情况,因此诞生了decltype关键字,举个栗子:
int x; decltype(x) y; // 声明一个和x一样类型的变量
但是遗憾的是 decltype 关键字需要先访问目标变量才能创建变量,因此不适用于下面这种情况:
decltype(x + y) Fun(T x, T y) { // 由于x和y不在作用域内,因此decltype语句无法执行 return x + y; }
-
auto 占位符:为解决上述问题,C++ 11 引入了 auto 占位符特性,可以用以下方法解决问题:
auto Fun(T x, T y) -> decltype(x + y){ return x + y; }