1.函数的定义
定义函数三要素:函数名、参数列表、返回值;
1.2.函数原型
在函数使用前,必须对它进行声明,这个声明也称为函数原型。如:
#include <iostream>
//add()函数原型
int add(const int& i,const int& j);
int main()
{
using namespace std;
int i = 45;
int j = 200;
int sum = add(i,j);
system("pause");
return 0;
}
int add(const int& i, const int& j) {
std::cout << "i:" << i << ",j:" << j << std::endl;
return i + j;
}
函数原型和函数唯一的区别就是函数原型不提供函数体,它描述了函数到编译器的接口,由于不需要方法体,因此函数声明不要求提供变量名,不过应尽可能写上。
函数原型一般在头文件中声明。
2.函数参数的传递
函数参数的传递有三种方式:按值传递、指针传递、引用传递,下面我们来看他们各自的特点。
2.1.按值传递
c++中通常按值传递(非引用类型),也就是说当实参传递给形参时,将实参的值赋给了形参,实参并没有影响,如:
#include <iostream>
void Swap(int i,int j);
int main()
{
using namespace std;
int fir = 100;
int sec = 300;
Swap(fir, sec);
cout << "fir:" << fir << ",sec:" << sec << endl;
system("pause");
return 0;
}
void Swap(int i, int j) {
int temp;
temp = i;
i = j;
j = temp;
}
运行该程序,发现并没有交换fir和sec的值,在函数调用时,函数将创建一个int类型的变量i,然后将fir的值100赋给它,因此实际上函数Swap()中操作的是局部变量i,而非变量fir。
按值传递导致被调用函数使用调用程序传递的值的拷贝。
2.2.按指针传递
当函数传递指针类型参数时,实际上,也是按值传递,但是这个值是一个地址,因此,就可以通过这个指针来修改所指对象的值了。如:
#include <iostream>
void Swap(int* i,int* j);
int main()
{
using namespace std;
int fir = 100;
int sec = 300;
int* p1 = &fir;
int* p2 = &sec;
cout << "fir:" << fir << ",sec:" << sec << endl;
Swap(p1, p2);
cout << "fir:" << fir << ",sec:" << sec << endl;
system("pause");
return 0;
}
void Swap(int* i, int* j) {
int temp;
temp = *i;
*i = *j;
*j = temp;
}
在运行Swap()函数时,函数首先创建了两个指向int类型的指针i和j,然后分别将指针p1、p2的值赋给他们,而这个值是一个地址,因此,i和j也就指向p1和p2所指的对象了,从而操作所指的对象的值。
2.2.1.数组形参
数组有两个特殊性质:
- 1.不允许拷贝;
- 2.当且仅当数组作为函数参数时,数组名和指针等价。
因此,当数组作为函数参数时,将其当做指针处理。以下三个函数原型声明的函数完全相同:
const double add(const double* );
const double add(const double arr[]);
const double add(const double [10]);
以上每个函数的参数都是const double*
。
无论函数参数为指针还是数组,传递参数时并不违反按值传递的方法,只不过这个值是一个地址,而不是数组的内容。
2.3.按引用传递
对于基本数据类型而言,按值传递参数可以实现功能,但如果是数据较大的类型,如类、结构等,按值传递则影响不仅消耗内存,还影响时间。因此可以将指针作为参数传递,除了传递指针以外,还可以传递引用。
我们知道,引用是一个变量的别名,在声明引用时就会为它初始化。引用变量的主要用途就是用于形参,如下示例中使用引用来交换两个变量的值:
#include <iostream>
void Swap(int& i, int& j);
int main()
{
using namespace std;
int fir = 100;
int sec = 300;
cout << "fir:" << fir << ",sec:" << sec << endl;
Swap(fir, sec);
cout << "fir:" << fir << ",sec:" << sec << endl;
system("pause");
return 0;
}
void Swap(int& i, int& j) {
int temp = i;
i = j;
j = temp;
}
/*
fir:100,sec:300
fir:300,sec:100
*/
在这个示例中,当调用Swap(fir,sec)
函数时,将引用类型的形参i和j作为了实参fir和sec的别名,从而完成值的交换。
2.3.1.尽量使用常量引用作为形参
当引用作为形参时,因尽量使用常量引用,其原因有三:
- 1.const限定符可以保护实参,避免无意识的修改;
- 2.使用const限定符可以处理const类型实参和非const类型实参,否则只能使用非const类型数据;
- 3.使用const限定符后,如果函数参数类型不匹配,则将会生成一个临时变量,并让形参指向这个临时变量,否则不会生成临时变量。
如:
double multiply(double& h, double& k);
double add(const double& a, const double& b);
int i = 3,j = 4;
multiply(i,j);//BAD,不能使用int类型的值初始化double类型的引用
add(i,j);//OK,此时a指向一个临时变量,将i值拷贝给了临时变量
当函数返回类型为引用时,有一点非常重要需要注意,要避免返回函数结束时不再存在的内存单元的引用,即不能返回自动变量(局部变量)。
2.4.总结
- 1.如果数据对象很小,如基本类数据或小型结构,则按值传递,但若需要修改数据对象,则使用指针;
- 2.如果数据对象是数组,则使用指针。若不需要修改数据,则声明为指向const的指针(如const int *);
- 3.如果数据对象为结构或类,则使用引用,若不需要修改数据,则声明为const引用。
3.内联函数
在函数原型或函数定义时,加上关键字inline
就可以普通函数变为内联函数。
一般的做法是省略函数原型,将整个函数的定义放在函数原型处。
inline void show(const int& i);
内联函数和普通函数相比,区别不在于他们的编写方式,而是C++编译器如何将他们进行组合。
C++ 编译器在调用普通函数时,是让程序跳转到独立的代码段,执行完毕后再跳转回来。
而对于内联函数,C++编译器将使用函数代码替换函数的调用,不会进行代码段的跳转。
因此,内联函数的运行速度比普通函数快,但却需要占用更多的内存。
使用内联函数时需要注意:
- 1.只有对于较短的函数,才可以使用内联函数;
- 2.内联函数不能递归。
4.函数默认参数
可以在声明函数原型时,指定一个默认参数,如:
double play(std::string name, int times=1);
使用默认参数时需要注意两点:
- 1.默认参数只在函数原型中指定,函数定义中和普通参数完全相同;
- 2.必须从右向左添加默认值。
通过定义默认参数,可以减少方法的重载数量。
函数重载
函数重载是指两个或多个函数名相同,但参数列表不同的函数。
特点
- 1.其特征标是函数参数列表,对返回值无要求。
- 2.引用和类型本身视为同一个特征标;
int show(const int & i,double d);
//void show(int & i1,double d);//bad
重载和const形参
重载函数中const形参比较有趣。在一组重载函数中,如果形参为顶层const,则和非const形参不会进行区分,比如:
void show(const std::string s);
void show(std::string s);//BAD,重复声明
但如果如果形参是底层const,则会进行区分,如:
void show(const std::string& s);
void show(std::string& s);//OK,函数重载
顶层const:表示任何的对象是常量。
底层const:表示指针所指的对象是一个常量,以及用于声明引用的const。
重载函数的调用
定义了一组重载函数后,需要以合理的实参去调用他们,对于参数个数不同或类型不同的重载函数,编译器可以很容器确定要调用的函数,但是在有些情况下,就比较困难了,比如一组重载函数的参数数量相同且参数类型可以互相转换。
当调用重载函数时,有三种可能的结果:
- 1.编译器找到一个和实参最佳匹配的函数并调用;
- 2.编译器找不到任何一个可匹配的函数,此时编译器将报出无匹配错误信息;
- 3.编译器找到多于一个的函数匹配,但都不是最佳匹配,此时编译器将报出二义性错误信息。
如下面示例中定义了一组重载函数:
#include <iostream>
#include <string>
void print(std::string& s );
void print(double);
void print(int);
void print(const char *);
void print(const std::string &);
int main()
{
using namespace std;
string s1 = "welcom c++ world";
const string s2 = "hello world";
const char * ch = "let's do it";
int i = 30;
double d = 12.3;
print(s1);
print(d);
print(i);
print(ch);
print(s2);
const int& i2 = 23;
print(i2);
system("pause");
return 0;
}
void print(std::string& s) {
std::cout << "print(std::string& s):" << s << std::endl;
}
void print(double d) {
std::cout << "print(double d):" << d << std::endl;
}
void print(int i) {
std::cout << "print(int i):" << i << std::endl;
}
void print(const char * pc) {
std::cout << "print(const char * pc):" << pc << std::endl;
}
void print(const std::string & s) {
std::cout << "print(const std::string & s):" << s << std::endl;
}
/*
print(std::string& s):welcom c++ world
print(double d):12.3
print(int i):30
print(const char * pc):let's do it
print(const std::string & s):hello world
print(int i):23
*/
5.函数模板
函数模板属于C++的泛型编程,可以通过泛型来定义函数,在定义函数时,将类型作为参数传递给模板,从而使得编译器生成该类型的定义。
5.1.为何需要函数模板?
在程序中,可能会存在多个函数功能相同,但参数不同的情况,比如在交换值时分别定义了交换int类型和double类型的函数Swap()
:
void Swap(int * i, int * j );
void Swap(double * d, double * p);
如果还有其他类型,则还需要重载Swap()
函数多次。
因此,C++编译器提供了函数模板特性,当需要对不同类型使用同一种算法时,就可以使用汗俗话模板了。
5.2.格式
定义函数模板时,格式如下:
template <typename T>
void Swap(T t1, T t2);
关键字typename
可以使用class
代替。
在函数原型和函数定义处,都需要有关键字template 修饰。
5.3.工作原理
函数模板不会创建任何函数,函数模板实际上经过了二次编译,分别在编译cpp文件时和函数调用时,在函数调用时,将会根据传入的参数生成对应的函数。
5.4.模板的重载
和常规函数一样,函数模板也可以进行重载,如下面示例中:
template <typename T,typename N>
void Swap(T & t1, N & n1);
template <typename T>
void Swap(T & t1, T & t2);