突破编程_C++_基础教程(函数(一))

1 函数声明

函数声明的作用是告诉编译器即将要定义的函数的名字是什么,返回值的类型是什么以及函数是什么。函数的声明可以有多次,但是函数的定义只能有一次。如果只有函数声明没有函数定义,则可以通过编译,但是链接时会报错。
通常把函数声明叫做函数原型,把函数定义叫做函数实现。

1.1 函数声明的基本语法

函数声明(函数原型)的语句结构:
返回值类型 函数名(参数1, 参数2, ...)
函数的声明和变量的声明一样,是一句语句。所以在语句结束要加上分号。比如:

int add(int a, int b);

函数名:类似于变量名,函数名就是函数的名字,即函数的标识符。函数名由字母、数字以及下画线组成,并且不能以数字开头。
返回值类型:指的是函数会返回数据的类型。如果某个函数不返回任何值,则定义其返回类型是 void 。
参数列表:输入到函数内部的数据类型。函数的参数位于一个括号中,并且用逗号分隔,括号中的部分就称做函数的参数列表。

1.2 constexpr 关键字

constexpr 关键字在 C++11 中引入,使用 该关键字声明的函数可以在编译时进行计算。这样可以提供更好的性能和编译时优化。同时,编译器还可以在编译时对 constexpr 表达式进行类型检查和错误检查。样例代码如下:

#include 

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 

//函数的定义
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 

//函数的定义
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 

//函数的定义
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 

//函数的定义
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 

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 

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 

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 
#include 
#include 

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)指针函数用于返回指针类型的值,例如动态分配的对象或数组的指针。

你可能感兴趣的:(突破编程_C++_基础教程,c++)