1 函数声明
函数声明的作用是告诉编译器即将要定义的函数的名字是什么,返回值的类型是什么以及函数是什么。函数的声明可以有多次,但是函数的定义只能有一次。如果只有函数声明没有函数定义,则可以通过编译,但是链接时会报错。
通常把函数声明叫做函数原型,把函数定义叫做函数实现。
1.1 函数声明的基本语法
函数声明(函数原型)的语句结构:
返回值类型 函数名(参数1, 参数2, ...)
函数的声明和变量的声明一样,是一句语句。所以在语句结束要加上分号。比如:
int add(int a, int b);
函数名:类似于变量名,函数名就是函数的名字,即函数的标识符。函数名由字母、数字以及下画线组成,并且不能以数字开头。
返回值类型:指的是函数会返回数据的类型。如果某个函数不返回任何值,则定义其返回类型是 void 。
参数列表:输入到函数内部的数据类型。函数的参数位于一个括号中,并且用逗号分隔,括号中的部分就称做函数的参数列表。
1.2 constexpr 关键字
constexpr 关键字在 C++11 中引入,使用 该关键字声明的函数可以在编译时进行计算。这样可以提供更好的性能和编译时优化。同时,编译器还可以在编译时对 constexpr 表达式进行类型检查和错误检查。样例代码如下:
#include <iostream>
constexpr int add(int a, int b)
{
return a + b;
}
int main()
{
int type = 2;
switch (type)
{
case add(1, 1): //编译时会将 add(1, 1) 替换为 2
{
printf("hello constexpr!\n");
break;
}
default:
break;
}
return 0;
}
1.3 内联化
内联函数的目的是为了提高函数的执行效率,用关键字 inline 放在函数声明的前面即可将函数指定为内联函数。编译器会将内联函数复制在程序中的每一个调用点上,如此便可省去调用函数过程中的参数入栈、函数跳转、保护现场、回复现场等过程,提高性能。但是由此付出的代价是打包后的程序体积会变大。样例代码如下:
inline int add(int a, int b);
1.4 noexcept 关键字
noexcept 关键字告诉编译器,函数中不会发生异常,这有利于编译器对程序做更多的优化。如果 noexecpt 函数在运行时向外抛出了异常,程序会直接终止。样例代码如下:
int add(int a, int b) noexcept
{
throw(0); //警告: warning C4297: “add”: 假定函数不引发异常,但确实发生了
return a + b;
}
2 函数定义
函数定义也叫做函数实现,与函数声明的不同之处在于函数定义具有函数体(即组成函数的代码)。 函数定义的格式是:
返回值类型 函数名(参数1, 参数2, ...)
{
//函数体
}
在 C++ 程序中,函数的定义必须写,而函数的声明有时必须写,有时可以省略不写。
如果在调用前函数已经定义,则不必再写函数的声明了。例如:
#include <iostream>
//函数的定义
int add(int a, int b)
{
return a + b;
}
int main()
{
add(1, 2); //直接调用
return 0;
}
3 函数参数
C++ 函数的参数分为形式参数和实际参数。形式参数是定义在函数声明或定义中的参数,也称为形参。实际参数是在调用函数时传递给函数的值或变量,也称为实参。
3.1 函数参数传递方式
C++ 支持三种参数传递方式:按值传递、按引用传递以及按指针传递。
3.1.1 按值传递
按值传递是将实际参数的值复制到形式参数。使用这种方式,调用函数本身不对实参进行操作。例如:
#include <iostream>
//函数的定义
void swap(int a, int b)
{
int c=a;
a=b;
b=c;
}
int main()
{
int a=1,b=2;
swap(a,b); //a,b 交换值失败
printf("a=%d, b=%d\n",a,b);
return 0;
}
上面代码的输出为:
a=1, b=2
3.1.2 按引用传递
按引用传递是将实际参数的地址传递给形式参数,此时的形参相当于实参的别名。使用这种方式,对形参的改变会影响到实参。例如:
#include <iostream>
//函数的定义
void swap(int &a, int &b)
{
int c=a;
a=b;
b=c;
}
int main()
{
int a=1,b=2;
swap(a,b); //a,b 交换值成功
printf("a=%d, b=%d\n",a,b);
return 0;
}
上面代码的输出为:
a=2, b=1
3.1.3 按指针传递
按指针传递是指将实参的地址传递给形参,函数内部可以通过指针来操作实参的值。但需要注意指针为空的情况。样例代码如下:
#include <iostream>
//函数的定义
void swap(int* a, int* b)
{
int c=*a;
*a=*b;
*b=c;
}
int main()
{
int a=1,b=2;
swap(&a,&b); //a,b 交换值成功
printf("a=%d, b=%d\n",a,b);
return 0;
}
上面代码的输出为:
a=2, b=1
需要注意的是:从效率上来说,按引用传递与按指针传递基本一样(按值传递有拷贝过程,性能很差),不过从安全角度出发,引用传递在参数传递过程中执行强类型检查,而指针传递的类型检查较弱,特别的,如果参数被声明为 void ,那么就不会做类型检查。所以推荐只用引用传递,最好不用指针传递。
3.2 设计函数参数的原则
设计函数参数的原则是:能用引用的就用引用(提高性能),能用 const 的就用 const(方便调用)
。比如创建一个用于打印字符串的函数。从这个需求来看,这个函数有一个字符串的入参,在其函数体中,只要读入这个字符串入参即可,无需对其修改。如果设计一个按值传递的函数,如下:
void printStr(string str)
{
printf("%s\n",str.c_str());
}
这样设计有一个弊病:每次调用该函数,都需要做一次 string 赋值操作(按值传递的弊病),非常耗时。如果改成按引用传递参数,如下:
void printStr(string& str)
{
printf("%s\n",str.c_str());
}
这样就会带来另外两个问题,第一:该函数只需要读入字符串的入参,并不用对其做修改,而按引用的传递,赋予了这个函数不应该有的权限。第二:不方便调用,比如有如下调用方式:
void printStr(string& str)
{
printf("%s\n",str.c_str());
}
int main()
{
printStr("hello"); //错误:由于字符串 "hello" 是 const 类型,所以这里编译会报错。
return 0;
}
综上所述,最好的设计如下:
void printStr(const string& str)
{
printf("%s\n", str.c_str());
}
4 函数调用
C++ 中函数的调用包含参数入栈、函数跳转、保护现场、回复现场等过程,以如下代码为例( 64 位程序):
#include <iostream>
int add(int a, int b)
{
int sum = a + b;
return sum;
}
int main()
{
int sum = add(1, 2);
return 0;
}
首先给 main()
函数的第一行 int sum = add(1, 2);
打上断点,调试运行程序。
程序暂停后,查看当前汇编代码( VS2017 查看方法:右击当前代码页,选择转到反汇编
):
int main()
{
00007FF67D8AA630 push rbp
00007FF67D8AA632 push rdi
00007FF67D8AA633 sub rsp,108h
00007FF67D8AA63A lea rbp,[rsp+20h]
00007FF67D8AA63F mov rdi,rsp
00007FF67D8AA642 mov ecx,42h
00007FF67D8AA647 mov eax,0CCCCCCCCh
00007FF67D8AA64C rep stos dword ptr [rdi]
00007FF67D8AA64E lea rcx,[__81FC6F77_main2@cpp (07FF67D9E41D7h)]
00007FF67D8AA655 call __CheckForDebuggerJustMyCode (07FF67D874108h)
int sum = add(1, 2);
00007FF67D8AA65A mov edx,2
00007FF67D8AA65F mov ecx,1
00007FF67D8AA664 call add (07FF67D87584Bh)
00007FF67D8AA669 mov dword ptr [sum],eax
return 0;
00007FF67D8AA66C xor eax,eax
}
在汇编代码中,程序暂停在第 14 行(00007FF67D8AA65A mov edx,2
)。后面的两行是传入参数的过程,其中,edx是数据寄存器,常用于存储一些大于 AX 寄存器的 16 位数和 32 位数的运算中的高位数。在函数调用中, edx 寄存器用于存储第一个参数值。ecx是计数寄存器,常用于存储循环计数器和移位操作的计数器。在函数调用中, ecx 寄存器用于存储第二个参数值。通过这两行传入的值可以看出,调用函数时,参数入栈时从右往左。
汇编行00007FF67D8AA664 call add (07FF67D87584Bh)
用于跳转到待调用的函数内,但这里需要注意的是,地址07FF67D87584Bh
并不是待调用的函数的地址,该代码会执行到下面这一行:
00007FF67D87584B jmp add (07FF67D8AA5C0h)
这里的地址07FF67D8AA5C0h
才是真正待调用函数的地址。下面即进入被调用函数内部:
int add(int a, int b)
{
00007FF67D8AA5C0 mov dword ptr [rsp+10h],edx
00007FF67D8AA5C4 mov dword ptr [rsp+8],ecx
00007FF67D8AA5C8 push rbp
00007FF67D8AA5C9 push rdi
00007FF67D8AA5CA sub rsp,108h
00007FF67D8AA5D1 lea rbp,[rsp+20h]
00007FF67D8AA5D6 mov rdi,rsp
00007FF67D8AA5D9 mov ecx,42h
00007FF67D8AA5DE mov eax,0CCCCCCCCh
00007FF67D8AA5E3 rep stos dword ptr [rdi]
00007FF67D8AA5E5 mov ecx,dword ptr [rsp+128h]
00007FF67D8AA5EC lea rcx,[__81FC6F77_main2@cpp (07FF67D9E41D7h)]
00007FF67D8AA5F3 call __CheckForDebuggerJustMyCode (07FF67D874108h)
int sum = a + b;
00007FF67D8AA5F8 mov eax,dword ptr [b]
00007FF67D8AA5FE mov ecx,dword ptr [a]
00007FF67D8AA604 add ecx,eax
00007FF67D8AA606 mov eax,ecx
00007FF67D8AA608 mov dword ptr [sum],eax
return sum;
00007FF67D8AA60B mov eax,dword ptr [sum]
}
这段汇编代码的第 2 行到第 15 行之间是对该函数的栈初始化工作,由编译器自动添加。其中 rsp ( 32 位程序中是 esp ) 、rbp ( 32 位程序中是 ebp )、rdi ( 32 位程序中是 edi )是常用的寄存器:
rsp 为栈指针,常用来指向栈顶。上面汇编代码中第 6 行00007FF67D8AA5CA sub rsp,108h
的意思是将栈顶指针往上移动 108h Byte。这个区域为间隔空间,将被调用的 add 函数与 main 函数的栈区域隔开一段距离,同时还要预留出存储局部变量的内存区域。
rbp 为基址指针,常用来指向栈底。
rdi 为目的变址寄存器。
上面汇编代码的第 17 行到第 21 行之间是进行两数相加的逻辑操作。
执行到第最后一行后打开寄存器查看器( VS2017 查看方法:调试–>窗口–>寄存器),可以查看到如下值:
RAX = 0000000000000003 RBX = 0000000000000000 RCX = 0000000000000003 RDX = 0000000000000002 RSI = 0000000000000000 RDI = 0000005BD30FFA58 R8 = 0000020993014F70 R9 = 0000005BD30FF954 R10 = 0000000000000013 R11 = 00000209930242E0 R12 = 0000000000000000 R13 = 0000000000000000 R14 = 0000000000000000 R15 = 0000000000000000 RIP = 00007FF67D8AA60B RSP = 0000005BD30FF950 RBP = 0000005BD30FF970 EFL = 00000206
0x0000005BD30FF974 = 00000003
查看寄存器 RDI 的内存值( VS2017 查看方法:调试–>窗口–>内存->内存1):
0000005bd30ffb78 0000005bd30ffa90 00007ff67d8aa669 00007ff600000001 cccccccc00000002 cccccccccccccccc cccccccccccccccc cccccccccccccccc cccccccccccccccc cccccccccccccccc cccccccccccccccc
其中第三个值 00007ff67d8aa669
是 main 函数中调用该函数后的下一行汇编代码。
至此,整个调用过程结束。
5 函数指针
注:该部分内容涉及到 C++ 中指针以及类的相关知识。
5.1 函数指针的概念
函数指针是指向函数的指针变量。 通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数的变量。 函数指针用于调用函数、传递参数。 函数指针的定义方式为:
函数返回值类型 (*
指针变量名) (函数参数列表);
函数返回值类型:表示该指针变量所指向函数的返回值类型。
指针变量名:表示该指针变量的名称。
函数参数列表:表示该指针变量所指向函数的参数列表。
需要注意的是函数指针没有 ++ 和 – 运算。
为了使用方便,一般会用关键字 typedef
来定义函数指针,即:typedef 函数返回值类型 (*
指针变量名) (函数参数列表) 。例如:
typedef int (*ADD)(int,int);
ADD addFunc;
使用这种方式可以目标函数看作为一个类型,然后再用它去定义指针,增强复用性。
对于无参数或者无返回值的函数,需要使用用 void 关键字,例如:
typedef void (*TESTFUNC)(void); //无参数和返回值
5.2 函数指针的使用
使用函数指针和使用其他类型的指针变量一样,其可以作为函数的入参,可以作为函数的返回值,也可以是类的成员变量。
5.2.1 指向全局函数的函数指针
以如下代码为例:
#include <iostream>
int add(int a, int b)
{
int sum = a + b;
return sum;
}
int main()
{
typedef int(*ADDFUNC)(int, int);
ADDFUNC f1 = add;
int sum1 = f1(1, 2); //直接使用函数名
int sum2 = (*f1)(1, 2); //取函数地址
printf("sum1 = %d\n",sum1);
printf("sum2 = %d\n", sum2);
return 0;
}
上面代码的输出为:
sum1 = 3
sum2 = 3
特别注意的是,因为函数名本身就可以表示该函数地址(指针),因此在获取函数指针时,可以直接用函数名,也可以取函数的地址。因此,上面代码中 int sum1 = f1(1, 2);
以及 int sum2 = (*f1)(1, 2);
作用是相同的。
5.2.2 指向对象成员函数的函数指针
以如下代码为例:
#include <iostream>
class MyAdd
{
public:
MyAdd() {}
~MyAdd() {}
public:
int add(int a, int b)
{
int sum = a + b;
return sum;
}
};
int main()
{
MyAdd myAddObj;
typedef int(MyAdd::*ADDFUNC)(int, int);
ADDFUNC f1 = &MyAdd::add;
int sum = (myAddObj.*f1)(1, 2);
printf("sum = %d\n", sum);
return 0;
}
上面代码的输出为:
sum = 3
注意:对象的成员函数属于类,所以其存储位置在对象外的空间中,由所有的类对象共享。因此, MyAdd 类中的 add() 成员函数,不是属于 myAddObj 对象的,而是属于 MyAdd 类。所以使用 &类名::成员函数名
的形式将该成员函数赋给函数指针。
5.2.3 回调函数
回调函数是函数指针的一个重要应用场景,比如在使用 C++ 的容器类时,经常会自定义回调函数用以实现定制化功能。以 vector 的自定义排序为例,代码如下:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Student
{
string id;
double score;
};
bool compareByScore(Student& stu1, Student& stu2)
{
return stu1.score < stu2.score;
}
int main()
{
vector<Student> students;
students.emplace_back(Student{ "s1",98.2 });
students.emplace_back(Student{ "s2",97.6 });
students.emplace_back(Student{ "s3",92.8 });
students.emplace_back(Student{ "s4",95 });
students.emplace_back(Student{ "s5",99 });
printf("before sort\n");
for (size_t i = 0; i < students.size(); i++)
{
printf("%s(%lf) ", students[i].id.c_str(), students[i].score);
}
printf("\n");
sort(students.begin(), students.end(), compareByScore);
printf("after sort\n");
for (size_t i = 0; i < students.size(); i++)
{
printf("%s(%lf) ", students[i].id.c_str(), students[i].score);
}
printf("\n");
return 0;
}
上面代码的输出为:
before sort
s1(98.200000) s2(97.600000) s3(92.800000) s4(95.000000) s5(99.000000)
after sort
s3(92.800000) s4(95.000000) s2(97.600000) s1(98.200000) s5(99.000000)
其中,函数 compareByScore
便作为一个函数指针的入参传递给函数 sort
。
5.2.4 函数指针和指针函数的区别
函数指针和指针函数是两种不同的编程概念,前者是一个指针,后者是一个函数,除了名字比较容易混淆,实际上是完全不同的概念。
上面内容已经说明了函数指针的含义与作用,指针函数的定义如下:
(1)指针函数本身就是一个函数,其返回的类型是指针。
(2)指针函数用于返回指针类型的值,例如动态分配的对象或数组的指针。