函数
- 一、函数基础
- 1.0 简介
- 形参和实参
- 形参列表
- 函数的返回类型
- 1.1 局部对象
- 自动对象
- 局部静态对象
- 1.2 函数声明
- 在头文件中进行函数声明
- 1.3 分离式编译
- 编译和链接多个源文件
- 二、参数传递
- 2.1 传值参数
- 指针形参
- 2.2 传引用参数
- 使用形参返回额外信息
- 2.3 const形参和实参
- 指针或引用形参与const
- 2.4 数组形参
- 使用标记指定数组长度
- 使用标准库规范
- 显式传递一个表示数组大小的形参
- 数组形参与const
- 数组引用形参
- 传递多维数组
- 2.5 main:处理命令行选项
- main的参数
- 2.6 含有可变形参的函数
- initializer_list形参
- 省略符形参
- 三、返回类型和return语句
- 3.1 无返回值函数
- void类型
- 3.2 有返回值的函数
- 编译器无法检查所有错误
- 不要返回局部对象的引用或指针
- 引用返回左值
- 列表初始化
- 主函数main的返回值
- 3.3 返回数组指针
- 使用类型别名
- 直接定义数组指针
- 尾置返回类型
- 使用decltype
- 四、函数重载
- 4.0 简介
- 定义重载函数
- 重载和const形参
- const_cast和重载
- 调用重载的函数
- 4.1 重载与作用域
- 五、特殊用途语言特性
- 5.1 默认实参
- 使用默认实参调用函数
- 默认实参声明
- 5.2 内联函数和constexpr函数
- 内联函数可避免函数调用的开销
- constexpr函数
- 内联函数constexpr函数的定义
- 5.3 调试帮助
- assert预处理宏
- NDEBUG预处理变量
- 六、函数匹配
- 七、函数指针
- 7.0 简介
- 使用函数指针
- 重载函数的指针
- 函数指针形参
- 返回指向函数的指针
一、函数基础
1.0 简介
- 一个典型的函数包括以下部分:返回类型(return type)、函数名字、由0个或多个形参(parameter)组成的列表、函数体。
- 我们通过调用运算符来执行函数,调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针。圆括号之内是一个用逗号隔开的实参列表,我们用实参初始化函数的形参。调用表达式的类型就是函数的返回类型。
- 使用函数的示例:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
// 函数fact计算n的阶乘
int fact(int n)
{
if (n <= 1)
return n;
return n * fact(n - 1);
}
int main()
{
cout << "5的阶乘为:" << fact(5) << endl;
return 0;
}
- 运行结果:
- 函数调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调用的函数。当调用函数时,主调函数的执行将被暂时中断,被调函数开始执行。
- 执行函数的第一步是(隐式地)定义并初始化它的形参。当遇到return语句时函数结束执行过程,return语句也完成两项工作:一是返回return语句中的值(如果有的话),二是将控制权从被调函数转移回主调函数。
形参和实参
- 实参是形参的初始值,实参按顺序对应并初始化形参。尽管实参和形参之间存在对应关系,但是并没有规定实参的求值顺序,编译器能以任意可行的顺序对实参求值。
- 实参和形参的数量必须相等,实参与对应形参的类型必须匹配。(匹配的含义是两参数类型相同或者经过自动转换后两者类型相同)
形参列表
- 函数的形参列表可以为空,为空可以直接写一个空的形参列表,也可以使用关键字void表示函数没有形参。函数应当独立地定义不同形参的类型。
函数的返回类型
- 大多数类型都能作为函数的返回类型。
- 一种特殊的类型是void,它表示函数不返回任何值。
- 函数的返回类型不能是数组类型或者函数类型,但是可以是指向数组或函数的指针。
1.1 局部对象
- 在C++语言中,名字有作用域,对象有生命周期。
- 名字的作用域是程序文本的一部分,名字在其中可见。
- 对象的生命周期是程序执行过程中该对象存在的一段时间。
- 函数体是一个语句块,块构成一个新的作用域。函数的形参和函数体内部定义的变量统称为局部变量,仅在函数的作用域内可见,同时局部变量还会隐藏(hide)在外层作用域中同名的其他所有声明。
- 局部变量的生命周期依赖于定义的方式。
自动对象
- 对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。我们只把存在于块执行期间的对象称为自动对象。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。
- 形参是一种自动对象。函数开始时为形参申请内存存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。
- 我们用传递给函数的实参初始化形参对应的自动对象。对于局部变量对于的自动对象来说:如果变量定义本身含有初始值,就用这个初始值进行初始化。否则,如果变量定义本身不含初始值,执行默认初始化。这意味着内置类型的未初始化局部变量将产生未定义的值。
局部静态对象
- 局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁。在此期间即使对象所在的函数结束执行也不会对它有影响。
- 示例代码如下:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
// 函数fact计算n的阶乘
int fact(int n)
{
// 静态局部变量的定义仅生效一次,即第二次调用fact函数不会定义和初始化number_fact的值
static int number_fact = 0;
number_fact++;
cout << "第" << number_fact << "次fact函数调用" << endl;
if (n <= 1)
return n;
return n * fact(n - 1);
}
int main()
{
cout << "5的阶乘为:" << fact(5) << endl;
return 0;
}
- 运行结果:
- 需要注意的是局部静态变量仅仅是改变了变量的生命周期,而没有改变变量的作用域。如上代码所示,number_fact虽然为静态变量,但它是局部的,即它名字的作用域仍旧只在fact函数中有效。因此静态局部变量仅在定义其的函数内有效,无法在定义其的函数外使用。如果想要获取静态局部对象的值,要么使用引用返回要么直接return通过函数返回值返回。
- 注意:静态局部变量的定义语句仅生效一次,并且定义中的初始化也仅执行一次。
- 静态局部变量一直存在,我们获取它的地址再传回main函数,以此可以实现在其他函数中直接访问不可见的静态局部变量。示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
// 函数fact计算n的阶乘
int fact(int n,int* &p)
{
static int number_fact = 0;
p = &number_fact;
number_fact++;
cout << "第" << number_fact << "次fact函数调用" << endl;
if (n <= 1)
return n;
return n * fact(n - 1,p);
}
int main()
{
int* p;
cout << "5的阶乘为:" << fact(5, p) << endl;
cout << "fact函数调用的总次数为:" << *p << endl;
return 0;
}
- 运行结果:
1.2 函数声明
- 函数的名字在使用前必须声明。函数只能被定义一次,但是可以声明多次。如果一个函数永远也不会被我们用到,那么它可以只有声明没有定义。
- 函数的声明就是去掉花括号和其中包含的函数体,直接在参数列表后跟分号即可。
- 在函数的声明中可以省略形参的名字,因为函数的声明不包括函数体也就无须形参的名字。
- 但是为每个形参写上名字有助于帮助使用者更好的理解函数的功能。
- 函数的三要素:返回类型 函数名(形参类型) 描述了函数的接口,说明了调用函数所需的全部信息。函数声明也称作函数原型。
- 示例代码:
int fact(int, int*&); // 省略形参名的函数声明
int fact(int n, int*& p); // 包含形参名的函数声明
在头文件中进行函数声明
- 建议变量在头文件中声明,在源文件中定义。
- 建议函数在头文件中声明,在源文件中定义。
- 定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。
1.3 分离式编译
编译和链接多个源文件
- 如果我们修改了其中一个源文件,那么只需重新编译那个改动了的文件。大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名是.obj(windows)或.o(UNIX)的文件,后缀名的含义是该文件包含对象代码(object code)。
二、参数传递
- 每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。形参初始化的机理与变量初始化一样。
- 和其他变量一样,形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对于的实参上,否则会将实参的值拷贝后赋给形参。
- 当形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用。这时引用形参即是实参的别名。
- 当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递或者函数被传值调用。
2.1 传值参数
指针形参
- 函数通过传入的指针,可以修改指针指向的对象。
- 示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
// 函数abs保证:调用后p指向的对象为正数
void abs(int* p)
{
// 如果p指向对象为负
if (*p < 0)
(*p) *= -1; // 则将p指向对象修改为其相反数
return;
}
int main()
{
int i = -10;
abs(&i); // 传入i的地址
// 检验i的值是否被改变
cout << "i的绝对值为:" << i << endl;
return 0;
}
- 运行结果:
- C语言程序员常常使用指针类型的形参访问函数外部的对象。在C++语言中,建议使用引用类型的形参代替指针。
2.2 传引用参数
- 示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
// 函数abs保证:调用后i为正数
void my_abs(int& i)
{
if (i < 0)
i *= -1;
return;
}
int main()
{
int i = -10;
my_abs(i); // 直接传入i
// 检验i的值是否被改变
cout << "i的绝对值为:" << i << endl;
return 0;
}
- 运行结果:
- 拷贝大的类类型对象或者容器对象比较抵消,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。
- 如果函数无须改变引用形参的值,最好将其声明为常量引用。
使用形参返回额外信息
- 一个函数只能返回一个值,有时函数需要返回多个值,引用形参为我们一次返回多个结果提供了有效途径。
- 传入引用形参,修改引用形参,在主调函数中查看引用实参的值即为额外信息。
2.3 const形参和实参
- 当形参有顶层const时,无论是常量对象或者非常量对象,都可以用来初始化形参。
指针或引用形参与const
- 如果实参具有底层const,那么它对应的形参也必须具备底层const。
- 如果实参不具有底层const,那么它对应的形参可以具备也可以不具备底层const。
- 如果一个函数不应该改变引用的值,那么我们应当把形参定义为const引用。因为非常量引用会给函数的调用者一种误导,即函数会修改它的实参的值。此外,使用引用而非常量引用将极大的限制函数所能接受的实参范围。例如:无法将const对象、字面值或者需要类型转换的对象传递给普通的引用形参,而常量引用却可以。
2.4 数组形参
- 数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组以及使用数组时(通常)会将其转换成指针。
- 因为无法拷贝数组,所以无法以值传递的方式去使用数组。因为数组会被转换为指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
- 示例代码:
void print(const int*);
void print(const int[]);
void print(const int[10]);
- 上述三种方式定义出来的函数是等价的,每个函数的唯一形参都是" const int * "类型的。当编译器处理对print函数的调用时,只检查传入的参数是否是const int * 类型。如果我们传递给print函数的是一个数组,则实参会自动的转换成指向数组首元素的指针,数组的大小对函数的调用没有影响。
- 和其他代码一样,以数组作为形参的函数也必须保证使用数组时不会越界。
- 管理指针形参有三种常用技术:
使用标记指定数组长度
- 例如C风格字符串,在数组的末尾添加一位并以空白字符’\0’作为末尾的标记。
- 示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
void print(const char* p)
{
while ((*p)) // 如果p不为'\0'就继续打印
cout << *p++; // *p++等价于*(p++)
}
int main()
{
char arr[]{ "Hello!" };
print(arr);
cout << endl;
return 0;
}
- 运行结果:
- 这种方法适用于那些有明显结束标记且该标记不会与普通数据混淆的情况,但是对于向int这样所有取值都是合法值的数据就不太有效了。
使用标准库规范
- 传递指向数首元素和尾后元素的指针,这种方法受到了标准库技术的启发。
- 示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
// 传入数组的首元素和尾后元素指针
void print(const char* p1, const char* p2)
{
while (p1 != p2)
cout << *p1++;
}
int main()
{
char arr[]{ "Hello!" };
// 利用begin和end函数安全地获得数组的指针
print(std::begin(arr), std::end(arr));
cout << endl;
return 0;
}
- 运行结果
显式传递一个表示数组大小的形参
- 示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
// 传入数组的指针和数组的长度
void print(const char* p1, int n)
{
while (n > 0) {
n--;
cout << *p1++;
}
}
int main()
{
char arr[]{ "Hello!" };
// 利用begin和end函数安全地获得数组的指针
print(arr, 6);
cout << endl;
return 0;
}
- 运行结果:
数组形参与const
- 我们把数组形参定义成了指向const的指针,关于引用的讨论一样是适用于指针的。当函数不需要对数组元素进行修改时,我们应该把数组形参写作指向const的指针。只有当函数需要修改元素的值时,我们才使用指向非常量的指针。
数组引用形参
- C++语言允许将变量定义成数组的引用。同样形参也可以是数组的引用,此时引用形参将绑定到对应的实参上,也就是绑定到数组上。
- 示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
// 实参为: 包含5个int值数组 的引用
void number_add(int (&arr)[5])
{
// 将数组中每个元素增加1
for (auto& i : arr)
i++;
}
int main()
{
int int_arr[]{ 1,2,3,4,5 };
// 调用函数传入引用实参
number_add(int_arr);
// 查看数组是否被改动
for (auto i : int_arr)
cout << i << " ";
cout << endl;
return 0;
}
- 运行结果:
传递多维数组
- 使用指向数组的指针也可以完成上文中的操作。
- 示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
// 实参为:指向 包含5个int值数组 的指针
void number_add(int (*arr)[5])
{
for (auto& i : *arr)
i++;
}
int main()
{
int int_arr[5]{ 1,2,3,4,5 };
// 调用函数传入引用实参
number_add(&int_arr);
// 查看数组是否被改动
for (auto i : int_arr)
cout << i << " ";
cout << endl;
return 0;
}
- 运行结果:
2.5 main:处理命令行选项
main的参数
- 有时我们确实需要给main传递实参,一种常见的情况是用户设置一组选项来确定函数所要执行的操作。我们可以将main函数定义为以下形式:
int main(int argc, char* argv[]);
- 其中argc为argv中有效字符串的数量,argv中按顺序定义命令字符串。
- 上述main函数形式等价于:
int main(int argc, char **argv);
- 因此我们可以通过:argv[0]=“prog”;argv[1]=“-d”; 等形式使用argv。(注意这里""形式的字符串仅表示对应的C风格字符串,不代表语法,因为字符串字面值无法直接赋值给字符数组)
- 当使用argv中的实参时,一定要记得可选的实参从argv[1]开始,因为argv[0]需要保存程序的名字,而非用户输入。
2.6 含有可变形参的函数
initializer_list形参
- 如果实参的类型相同,可以传递一个名为initializer_list的标准库模板。
- 譬如要输出错误信息,但是我们不知道信息的数量,那不如把所有信息定义在一个容器里,输出错误信息的函数只要输出容器中的信息即可。
- 示例代码
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
void print(std::initializer_list<std::string> error_li)
{
for (auto i : error_li)
cout << i << endl;
}
int main(int argc,char ** argv)
{
std::initializer_list<std::string> li = { "error0","error1","404" };
print(li);
cout << endl;
return 0;
}
- 运行结果:
省略符形参
- 省略符形参是为了便于C++程序访问某些特殊的C代码而设置的。
- 省略符形参应该仅仅用于C和C++通用的类型。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
三、返回类型和return语句
3.1 无返回值函数
void类型
- 没有返回值的return语句只能用在返回类型是void的函数中。返回void的函数不要求非得有return语句,因为在这类函数的最后一句会隐式地执行return。
- 一个返回类型是void的函数也可以使用"return exp"的形式返回,不过此时语句中的exp必须是另一个返回void的函数。
3.2 有返回值的函数
- 对于有返回值的函数,return语句返回的类型必须与函数返回类型相同,或者能隐式地转换成函数的返回类型。
- 要注意,函数体内可能有很多控制语句,导致函数体具有很多不同的执行路径。对于有返回值的函数来说,必须保证每一条路径最终都调用了return语句。而编译器无法帮助我们检测这一点。
编译器无法检查所有错误
- 编译器的试炼 (示例代码):
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
int print(int i)
{
if (i)
return i;
// 如果i为0则没有定义返回语句
}
int main(int argc,char ** argv)
{
cout << print(0) << endl;
cout << endl;
return 0;
}
- 运行结果:
不要返回局部对象的引用或指针
- 函数完成后,它所占用的存储空间也随着被释放掉。因此函数终止意味着局部变量的引用将指向不再有效的内存区域。
- 返回局部对象的引用或指针都是错误的。
引用返回左值
- 当且仅当函数返回类型为引用时,函数返回值为左值,即我们可以给它赋值。
- 示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
int& res() // 返回res函数体内静态局部变量的引用
{
static int i = 10; // 静态局部变量初始值为10
std::cout << i << endl; // 每次调用查看静态局部变量的值
return i;
}
int main()
{
res() = 100; // 给 函数返回的引用 赋值
res(); // 查看res中静态局部变量的值
cout << endl;
return 0;
}
- 运行结果:
列表初始化
- 使用vector作为返回值类型,即可使用列表初始化方式返回值。
- 示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
// 函数返回vector对象
std::vector<int> getn_vector(int n)
{
if (n == 1)
return { 1 }; // 使用列表初始化返回vector对象
else if (n == 2)
return { 1,2 }; // 使用列表初始化返回vector对象
else
return { 1,2,3 };// 使用列表初始化返回vector对象
}
int main()
{
//获取包含不同数量元素的的vector
std::vector<int> vec = getn_vector(1);
for (auto i : vec)
cout << i << " ";
cout << endl;
vec = getn_vector(2);
for (auto i : vec)
cout << i << " ";
cout << endl;
vec = getn_vector(3);
for (auto i : vec)
cout << i << " ";
cout << endl;
return 0;
}
- 运行结果:
- 如果是内置类型,列表初始化只能返回1个值。如果函数返回类类型,由本身定义初始值如何使用。
主函数main的返回值
- main函数即使没有返回值,编译器将隐式地插入一条返回0的return语句。
- main函数的返回值可以看作是状态指示器。
3.3 返回数组指针
使用类型别名
- 因为数组不能被拷贝,所以函数不能返回数组。不过函数可以返回数组的指针或引用。
- 最简单的方式是使用类型别名:
// 定义arrT为:包含10个int类型元素的数组
typedef int arrT[10];
// using arrT = int[10]; 与上面定义等价
// 传入vector对象返回指向arrT类型的指针(vector对象传入引用提高效率,const限制修改)
arrT* resVectorToArr(const vector<int>& vec);
- 示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
using std::vector;
// 定义arrT为:包含10个int类型元素的数组
typedef int arrT[10];
// using arrT = int[10]; 与上面定义等价
// 传入vector对象返回指向arrT类型的指针(vector对象传入引用提高效率,const限制修改)
arrT* resVectorToArr(const vector<int>& vec)
{
// 往函数外部返回函数内定义的数据,必须使用静态变量保证函数结束后返回值可用
// 返回的是指向arrT指针的值,因此我们需要将arrT定义为静态变量
static arrT int_arr;
// 使用vector对象的值初始化arrT对象(注意这里假设vector对象中元素的数量不会超过arrT类型中元素的数量)
for (int i = 0; i < vec.size(); i++)
int_arr[i] = vec[i];
// 返回arrT对象的地址
return &int_arr;
}
int main()
{
vector<int> vec = { 1,2,3,4,5,6,7 };
// 注意其实静态局部变量会在每次函数调用时被改变,因此不建议做返回值,此例用作学习
arrT* int_arr = resVectorToArr(vec);
// 打印arrT*指向的arrT对象
for (int i = 0; i < 7 ;i++)
cout << (*int_arr)[i] << " ";
cout << endl;
return 0;
}
- 运行结果:
直接定义数组指针
- 返回数组指针我们必须指定指针指向对象即数组的大小,一个指向 包含10个int数组 指针的定义如下:
int(*int_arr)[10];
- 如何将上述定义声明为函数的返回值呢?返回值不需要命名,因此省略去int_arr仅剩下"int * 【10】“。类似于变量的定义时我们将【10】放在变量名int_arr之后,函数声明返回值时我们也需要将【10】放在函数名之后。由于函数的形参列表必须紧跟函数名,因此我们将【10】放在函数的形参列表之后,然后我们再加上一个括号类似于”(*int_arr)[10]"使得函数名先与 * 结合再与【10】结合。最后的函数声明如下:
int (*resVectorToArr(const vector<int>& vec))[10];
- 对函数复杂返回类型的解读和对复杂指针或引用的解读一样,由里向外。比如上文先是*函数名,表示函数返回一个指针,【10】表示返回一个指向 包含10个元素数组 的指针,int表示元素的类型。
- 示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
using std::vector;
int (*resVectorToArr(const vector<int>& vec))[10]
{
static int int_arr[10];
for (int i = 0; i < vec.size(); i++)
int_arr[i] = vec[i];
return &int_arr;
}
int main()
{
vector<int> vec = { 1,2,3,4,5,6,7 };
int(*int_arr)[10] = resVectorToArr(vec);
for (int i = 0; i < 7; i++)
cout << (*int_arr)[i] << " ";
cout << endl;
return 0;
}
- 运行结果:
尾置返回类型
- C++11新标准中有一种方法可用简述上文函数的声明和定义的过程,这就是使用尾置返回类型。
- 对上文函数使用尾置返回类型的声明为:
auto resVectorToArr(const vector<int>& vec) -> int(*)[10] ;
- 尾置返回类型就是在函数原本返回类型的位置用auto代替,然后在函数的参数列表后添加"->“符号,在”->"符号后紧跟函数返回对象的类型即可。要注意必须(*),否则编译器会认为你返回的是数组而报错,括号保证了返回的类型的根本是指针。
使用decltype
- 可以使用decltype返回指向数组的指针:
decltype(arr) *resVectorToArr(const vector<int>& vec);
- 其中arr是变量,它的定义为:int arr[10]。decltype(arr)返回的是包含10个元素的整形数组类型,decltype不会将数组类型转换成对应的指针,因此我们还需要在其后加一个"*"表示:函数返回指向decltype(arr)类型的指针。
- 示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
using std::vector;
// 要注意arr必须在全局作用域,这样函数的声明才可使用它
int arr[10];
decltype(arr) *resVectorToArr(const vector<int>& vec)
{
static int int_arr[10];
for (int i = 0; i < vec.size(); i++)
int_arr[i] = vec[i];
return &int_arr;
}
int main()
{
vector<int> vec = { 1,2,3,4,5,6,7 };
int(*int_arr)[10] = resVectorToArr(vec);
for (int i = 0; i < 7; i++)
cout << (*int_arr)[i] << " ";
cout << endl;
return 0;
}
- 运行结果:
四、函数重载
4.0 简介
- 定义:在同一个作用域中,具备多个名字相同但形参列表不同的函数,我们称它们为重载函数。
- 函数重载在在一定程度上可以减轻程序员起名字、记名字的负担。
- 注意main函数不能重载。
定义重载函数
- 注意重载函数只要求函数名相同和形参列表不同,没有对函数返回值类型的要求,即不同的重载函数可以定义不同的返回值类型,但是区分同名函数是否重载成功仅取决于两个函数的形参列表是否不同。
- 正确重载函数示例:
int add(int a, int b);
double add(double a, double b);
string add(string a, string b);
- 错误重载函数示例:
// 重载错误:比较形参列表比较的是类型,与形参名无关(编译器不会检测出错误)
int add(int, int);
int add(int a, int b);
// 重载错误:wage类型本质为double,下列两声明本质相同(编译器不会检测出错误)
double add(double a, double b);
double add(wage a, wage b);
// 重载错误:重载函数必须形参不同,下列两同名函数形参列表相同(编译器会检测出错误)
string add(string a, string b);
char* add(string a, string b);
重载和const形参
- 顶层const不影响传入函数的对象。
- 示例代码:
// 重载错误:重复声明函数add (编译器不会显示错误,但一旦运行调用函数会显示重复声明错误)
string add(string a, string b);
string add(const string a, const string b);
- 上述代码错误的原因在于:一个拥有顶层const和一个没有顶层const的形参无法区分开来,因此const string等价于string,因此只能声明其中一者。
- 底层const可以区分形参类型。
- 示例代码:
// 重载正确
string add(string& a, string& b);
string add(const string& a, const string& b);
- 注意对上述代码,如果我们有一个string对象作为实参,但我们不想修改它,那么我们直接传入string对象会调用第一行代码,这可能导致string对象被修改。因此我们需要再定义一个const string对象,传入即可调用第二行代码。(非常量实参可以初始化常量和非常量形参,但是编译器会优先选择非常量形参)(使用const_cast进行强制类型转换将非常量实参转换为常量实参,即可调用任意重载函数)
- 示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
using std::vector;
using std::string;
// 下列三个函数形参不同
string add(string& a, string& b)// 一形参为非常量,二形参为非常量
{
cout << 1;
return "";
}
string add(const string& a, const string& b)// 一、二形参为常量
{
cout << 2;
return "";
}
string add(const string& a,string& b)// 一形参为常量,二形参为非常量
{
cout << 3;
return "";
}
int main()
{
string str = "hello";
const string str1 = str;
// 调用add,第一个形参传入常量,第二个形参传入非常量
add(str1, str);
cout << endl;
return 0;
}
- 运行结果:
- 建议只重载那些操作非常相似的函数,因为有些情况下,为函数起不同的名字能使得程序更利于理解。
const_cast和重载
- 使用const_cast类型转换可以实现常量和非常量之间的相互转化。
- 将string& 类型转换为const string&类型的代码如下:
string str = "hello";
const string str2 = const_cast<const string&>(str);
- 示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
using std::vector;
using std::string;
// 要注意函数的顺序,常量add被非常量add使用因此要放在上面
string add(const string& a, const string& b)
{
cout << "调用常量add版本" << endl;
return "";
}
string add(string& a, string& b)
{
cout << "调用非常量add版本" << endl;
return add(const_cast<const string&>(a), const_cast<const string&>(b));
}
int main()
{
string str = "hello";
const string str2 = const_cast<const string&>(str);
add(str, str);
cout << endl;
return 0;
}
- 运行结果:
调用重载的函数
- 函数匹配是指一个过程,在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数重载也叫重载确定。由编译器比较调用实参与重载集合中每一个函数进行比较,最后根据比较的结果决定调用哪个函数。
- 当调用函数重载函数时有三种可能的结果:
- 编译器找到一个最佳匹配,并生成调用该函数的代码。
- 找不到任何一个函数匹配,编译器发出误匹配的错误信息。
- 有多于一个函数可以匹配,但每一个都不是最佳选择。此时也将发生错误,称为二义性调用。
4.1 重载与作用域
- 尽量不要在局部作用域声明或定义函数。
- 如果在局部作用域(如函数内)声明函数,则次函数无法与全局作用域的同名函数发生重载,因为C++无法重载不同作用域中的函数。
- 在C++语言中,名字查找发生在类型检查之前。如果我们在局部定义了一个print(int a)函数,在全局定义了一个print(double a)函数,那么当我们在定义局部print的函数中使用print(1)时,因为编译器在局部检测到pirnt(int a)并且可以将double实参转换成int类型,所以它会调用局部的print(int a)函数。
五、特殊用途语言特性
5.1 默认实参
- 默认实参允许为函数的的实参指定一个默认值,当我们调用包含默认实参的函数时,可以包含默认实参的初始值,也可以省略它的初始值。
- 示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
using std::vector;
using std::string;
// 含有默认实参的函数
void createWindow(int width = 800, int height = 600, string title = "Program")
{
// 打印形参的值
cout << "width:" << width << endl;
cout << "height:" << height << endl;
cout << "title:" << title << endl;
}
int main()
{
createWindow();
cout << endl;
return 0;
}
- 运行结果:
使用默认实参调用函数
- 具有默认实参的形参在函数调用时可省略,为了使得实参和形参能一一对应,要求在默认实参后不能再定义普通形参。
- 函数定义示例:
void createWindow(int width, int height, string title = "Program");
void createWindow(int width, int height = 600, string title = "Program");
void createWindow(int width = 800, int height = 600, string title = "Program");
- 当我们调用含有默认实参的函数时,我们只能省略参数列表中尾部的一部分参数,实参列表的值将一一对应形参列表的值,因此省略的形参必须处于列表尾部并且是连续的。
- 函数调用示例:
createWindow();
createWindow(800);
createWindow(800,600);
createWindow(800,600,"Program");
默认实参声明
- 允许对一个函数进行多次声明,但是对包含默认实参的声明来说,每一次声明都必须给之前没有默认值的实参提供默认值。
- 通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。
- 我们可以使用变量来指定默认实参的值,但前提是这个变量必须可见。要注意这种形式的默认实参值会随着变量值的改变而变。
- 示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
using std::vector;
using std::string;
int my_width, my_height;
string my_title;
void createWindow(int width = my_width, int height = my_height, string title = my_title)
{
// 打印形参的值
cout << "width:" << width << endl;
cout << "height:" << height << endl;
cout << "title:" << title << endl;
}
int main()
{
my_width = 800;
my_height = 600;
my_title = "hello";
createWindow();
my_title = "hello world!";
createWindow();
cout << endl;
return 0;
}
- 运行结果:
5.2 内联函数和constexpr函数
- 调用函数一般比求等价表达式的值要慢一些。在大多数机器上,一次函数调用其实包含着一系列工作。
内联函数可避免函数调用的开销
- 将函数指定为内联函数,通常就是将它在每个调用点上"内联地"展开。
- 内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。
- 一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。
- 示例代码:
// inline关键字标识函数为内联函数
inline int add(int a, int b)
{
return a + b;
}
constexpr函数
- constexpr函数是指能用于常量表达式的函数。定义constexpr函数的方法和其他函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型。
- 示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
using std::vector;
using std::string;
// constexpr函数
constexpr int res(int i)
{
return i;
}
int main()
{
constexpr int i = res(1);
int k = 2;
// 调用res(k)可以,但它返回的不是常量,将它赋值给constexpr类型值将导致此处编译器报错
constexpr int j = res(k);
cout << endl;
return 0;
}
- 注意constexpr函数不一定返回常量表达式,如果传入实参为常量则返回常量表达式,否则返回普通表达式。只有常量表达式才可以赋值给constexpr定义的常量。
内联函数constexpr函数的定义
- 和普通函数不同,由于编译器需要展开内联函数并且检测constexpr函数,因此我们通常将内联函数和constexpr函数直接定义在头文件中。
5.3 调试帮助
assert预处理宏
- assert是一种预处理宏。assert宏使用一个表达式作为它的条件。如果条件为假则assert输出信息并终止程序的执行,否则assert什么也不做。
- 示例代码:
#include <iostream>
#include <vector>
// assert位于cassert头文件中,属于预处理名字,无需std::
#include<cassert>
using std::cin;
using std::cout;
using std::endl;
using std::vector;
using std::string;
int main()
{
// 终止程序
assert(0);
cout << endl;
return 0;
}
- 运行结果:
- assert的行为依赖于一个名为NDEBUG的预处理变量的状态,如果定义了NDEBUG则assert什么也不做。默认情况下没有定义NDEBUG。
NDEBUG预处理变量
- 除了用assert外,还可以使用NDEBUG编写自己的条件调试代码,如果NEDBUG未定义则执行调试代码。
- 示例代码:
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
using std::vector;
using std::string;
#include<cassert>
int add(int a,int b)
{
#ifndef NDEBUG
cout << "add[a:" << a << " b:" << b << "]" << endl;
#endif // !NDEBUG
return a + b;
}
int main()
{
assert(1);
add(1, 1);
cout << endl;
return 0;
}
- 运行结果:
- 无论是使用assert还是NDEBUG,都是想在开发程序时使用调试代码,而在程序编写完准备发布时直接屏蔽所有调试代码。如调试时我们可以不定义NDEBUG,在程序发布时定义NDEBUG。
六、函数匹配
…
七、函数指针
7.0 简介
- 函数指针指向的是函数而非对象。
- 函数指针指向某种特定类型,函数的类型仅由它的返回类型和形参类型共同决定,而与函数名无关。
- 要定义函数的指针,我们直接将函数名替换成(*p)即定义了指向这种函数类型的指针p。
- 示例代码:
// 定义一个函数
int add(int a, int b)
{
return a + b;
}
// 定义一个指向特定函数类型的指针p
int (*p)(int a, int b);
使用函数指针
- 当我们把函数名当作一个值使用时,该函数自动地转换成指针。
- 示例代码:
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
// 定义一个函数
int add(int a, int b)
{
return a + b;
}
int main()
{
// 定义一个指向特定函数类型的指针p
int (*p)(int a, int b);
// 函数指针的初始化(以下两种形式等价,是否使用取地址运算符&都行)
p = add;
p = &add;
// 通过函数指针调用函数(以下两种调用形式等价)
cout << p(0, 1) << endl;
cout << (*p)(0, 1) << endl;
cout << endl;
return 0;
}
- 指向不同类型函数的指针间不存在转换规则。如果一个指针的值等于nullptr,说明它不指向任何函数。
重载函数的指针
- 当我们使用重载函数的指针时,上下文必须清晰地界定到底使用哪个函数。
- 通过指针调用重载函数时,编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配。
函数指针形参
- 和数组类似,虽然不能定义函数类型的形参,但可以定义指向函数指针类型的形参。
- 我们可以直接把函数作为实参使用,这时它会自动转换成指针。
- 我们也可以显式地将形参定义成指向函数的指针。
- 示例代码:
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
void printInt(int i)
{
cout << "printInt:" << i << endl;
}
void printDouble(double b)
{
cout << "printDouble:" << b << endl;
}
// 直接使用printInt和printDouble的函数声明形式作为形参 (当然把名字改一下作为形参名)
void print(void pI(int i), void pD(double b), int i, double b)
{
// 通过形参函数指针调用函数
pI(i);
pD(b);
}
int main()
{
// 直接使用函数原名初始化此函数类型的指针
print(printInt, printDouble, 1, 2.0);
cout << endl;
return 0;
}
- 运行结果:
- 下列代码显式定义形参为函数指针类型,运行结果不变:
// 显式地将形参定义成指向函数的指针
void print(void (*p1)(int i), void (*p2)(double b), int i, double b)
{
// 通过形参函数指针调用函数
p1(i);
p2(b);
}
返回指向函数的指针
- 和数组类似,虽然返回一个函数,但是能返回一个指向函数类型的指针。
- 要注意的是,和函数类型的形参不一样,返回类型不会自动转换成指针。
- 示例代码:
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int add(int a, int b)
{
return a + b;
}
// 以下三种声明方式等价,都是int(*)(int a, int b)指针类型
// 使用auto 并在函数参数列表使用尾置返回类型
auto getAddP1() -> int (*)(int a,int b)
{
return add;
}
// 使用decltype(函数)推断得到函数类型,注意得到是函数而非指向函数的指针,因此加上"*"
decltype(add)* getAddP2()
{
return add;
}
// 直接定义返回什么类型的函数指针,getAddp()表示它是一个函数,"*"表示返回一个指针
// int和后面的(int a,int b)表示返回什么类型的指针,要注意里面的括号一组都不能少
int (*getAddP3())(int a, int b)
{
return add;
}
int main()
{
cout << getAddP1()(0, 1) << endl;
cout << getAddP2()(0, 1) << endl;
cout << getAddP3()(0, 1) << endl;
cout << endl;
return 0;
}
- 运行结果: