C++ 编程/代码/语句/函数
一个函数,也可以称为子程序、过程、子程序,甚至方法,执行由称为语句块的一系列语句定义的任务,这些语句只需要编写一次,并且可以根据需要被程序调用任意次来执行相同任务。
函数可能依赖于传递给它们的值,称为实参,并且可以将任务的结果传递给函数的调用者,这称为返回值。
需要注意的是,存在于全局作用域中的函数也可以称为全局函数,在类内部定义的函数称为成员函数。(术语方法通常在其他编程语言中用于指代类似于成员函数的内容,但这会导致在处理支持成员函数的虚拟和非虚拟分派的 C++ 时出现混淆。)
函数必须在使用之前声明,声明中包含一个名称来标识它,函数返回的值的类型,以及传递给它的任何参数的类型。参数必须命名并声明它接受的值的类型。如果参数没有被修改,参数应该始终作为const传递。通常函数执行操作,所以名称应该清楚地表明它做了什么。通过在函数名称中使用动词并遵循其他命名约定,可以使程序更自然地阅读。
在下面的示例中,我们定义了一个名为main
的函数,它返回一个整数类型int
的值,并且不接受任何参数。函数的内容称为函数的主体。int
这个词是关键字。C++ 关键字是保留字,即不能用于任何与它们含义不同的目的。另一方面,main不是关键字,你可以在许多不能使用关键字的地方使用它(尽管不建议这样做,因为会导致混淆)。
int main()
{
// code
return 0;
}
inline关键字声明内联函数,该声明是对编译器的一个(非绑定)请求,要求对特定函数进行内联扩展;也就是说,它建议编译器在使用该函数的每个上下文中插入函数的完整主体,因此它用于避免在简单实现子程序时进行 CPU 从代码中一个位置跳转到另一个位置,然后再返回以执行子程序所带来的开销。
inline swap( int& a, int& b) { int const tmp(b); b=a; a=tmp; }
当函数定义包含在类/结构体定义中时,它将是一个隐式内联,编译器将尝试自动内联该函数。在这种情况下,不需要使用inline
关键字;在该上下文中添加inline
关键字是合法的,但冗余,并且良好的风格是省略它。
示例
struct length
{
explicit length(int metres) : m_metres(metres) {}
operator int&() { return m_metres; }
private:
int m_metres;
};
内联可以是优化,也可以是悲观优化。它可以增加代码大小(通过在多个调用点重复函数的代码),也可以减少代码大小(如果函数的代码在优化后小于调用非内联函数所需的代码大小)。它可以提高速度(通过允许进行更多优化并避免跳转),也可以降低速度(通过增加代码大小,从而导致缓存未命中)。
内联的一个重要副作用是,更多代码可以被优化器访问。
将函数标记为内联还会影响链接:允许多个内联函数定义(只要每个定义都在不同的翻译单元中)只要它们是相同的。这允许内联函数定义出现在头文件中;在头文件中定义非内联函数几乎总是错误(尽管函数模板也可以在头文件中定义,并且通常是在头文件中定义的)。
主流 C++ 编译器,例如Microsoft Visual C++和GCC,支持一个选项,可以让编译器自动内联任何合适的函数,即使是那些没有被标记为内联函数的函数。编译器通常比人类更能判断特定函数是否应该内联;特别是,编译器可能不愿意或不能内联人类要求内联的许多函数。
过度使用内联函数会大大增加耦合/依赖关系和编译时间,并使头文件作为接口文档的用途降低。
通常在调用函数时,程序会评估并存储参数,然后调用(或跳转到)函数的代码,然后函数稍后返回到调用者。虽然函数调用速度很快(在现代处理器上通常不到一微秒),但开销有时可能很大,尤其是在函数很简单并且被多次调用时。
在某些情况下,可以使用所谓的内联
函数作为性能优化。将函数标记为内联
是对编译器的请求(有时称为提示),建议编译器考虑用该函数的代码副本替换对该函数的调用。
结果在某些方面类似于使用#define 宏,但是如之前提到,宏会导致问题,因为它们不是由预处理器评估的。内联
函数不会遇到相同的问题。
如果内联函数很大,这种替换过程(由于明显的原因被称为“内联”)会导致“代码膨胀”,导致代码变大(因此通常变慢)。但是,对于小型函数,它甚至可以减少代码大小,特别是在编译器的优化器运行后。
请注意,内联过程需要编译器能够访问函数的定义(包括代码)。特别是,从多个源文件使用的内联头文件必须完全定义在头文件中(而在普通函数中,这将是一个错误)。
指定函数为内联的最常见方法是使用inline
关键字。要记住,编译器可以配置为忽略关键字并使用自己的优化。
在处理内联成员函数时,会进行进一步的考虑,这将在面向对象编程章节中介绍。
参数和实参
[edit | edit source]函数声明定义了它的参数。参数是一个变量,它在函数调用中接受传递给它的对应实参的含义。
实参代表你在调用函数时提供给函数参数的值。调用代码在调用函数时提供实参。
函数声明中声明预期参数的部分称为参数列表,函数调用中指定实参的部分称为实参列表。
//Global functions declaration
int subtraction_function( int parameter1, int parameter2 ) { return ( parameter1 - parameter2 ); }
//Call to the above function using 2 extra variables so the relation becomes more evident
int argument1 = 4;
int argument2 = 3;
int result = subtraction_function( argument1, argument2 );
// will have the same result as
int result = subtraction_function( 4, 3 );
许多程序员根据上下文使用参数和实参,以区分它们的含义。在实践中,区分这两个术语通常对于正确使用它们或将它们的使用传达给其他程序员来说是不必要的。或者,可以使用等效术语形式参数和实际参数代替参数和实参。
参数
[edit | edit source]你可以定义一个没有参数、一个参数或多个参数的函数,但要使用带有实参的函数调用,你必须考虑定义的内容。
空参数列表
[edit | edit source]//Global functions with no parameters
void function() { /*...*/ }
//empty parameter declaration equivalent the use of void
void function( void ) { /*...*/ }
多个参数
[edit | edit source]声明和调用具有多个参数的函数的语法可能是错误的来源。当你编写函数定义时,你必须声明每个参数的类型。
// Example - function using two int parameters by value
void printTime (int hour, int minute) {
std::cout << hour;
std::cout << ":";
std::cout << minute;
}
你可能很想写(int hour, minute),但这种格式只适用于变量声明,不适用于参数声明。
但是,你不需要在调用函数时声明实参的类型。(事实上,尝试这样做会出错)。
示例
int main ( void ) {
int hour = 11;
int minute = 59;
printTime( int hour, int minute ); // WRONG!
printTime( hour, minute ); // Right!
}
在这种情况下,编译器可以通过查看它们的声明来判断hour和minute的类型。在将它们作为实参传递时,包含类型是多余的,也是非法的。
通过指针
[edit | edit source]当指向的对象可能不存在时,函数可以使用传值指针,也就是说,当你给出一个真实对象的地址或NULL时。传递指针与传递任何其他内容没有区别。它与其他任何参数一样是一个参数。指针类型的特性使其值得区分。
将指针传递给函数非常类似于将其作为引用传递。它用于避免复制开销,以及在按值传递基类对象时可能发生的切片问题(因为子类比父类具有更大的内存占用空间)。这也是 C 中的首选方法(出于历史原因),其中传值指针表示想要修改原始变量。在 C++ 中,首选使用指向指针的引用,并确保函数在对其解引用之前验证指针的有效性。
#include <iostream>
void MyFunc( int *x )
{
std::cout << *x << std::endl; // See next section for explanation
}
int main()
{
int i;
MyFunc( &i );
return 0;
}
由于引用只是一个别名,它与它所引用的内容具有完全相同的地址,如下面的示例所示
#include <iostream>
void ComparePointers (int * a, int * b)
{
if (a == b)
std::cout<<"Pointers are the same!"<<std::endl;
else
std::cout<<"Pointers are different!"<<std::endl;
}
int main()
{
int i, j;
int& r = i;
ComparePointers(&i, &i);
ComparePointers(&i, &j);
ComparePointers(&i, &r);
ComparePointers(&j, &r);
return 0;
}
该程序会告诉你指针是相同的,然后是不同的,然后是相同的,然后是不同的。
- 数组类似于指针,还记得吗?
现在可能是重新阅读关于数组部分的好时机。但是,如果你不想翻回那么远,这里有一个简要回顾:数组是内存空间的块。
int my_array[5];
在上面的语句中,my_array是内存中足够大以容纳五个int
的区域。要使用数组的元素,它必须被解引用。数组中的第三个元素(记住它们是零索引的)是my_array[2]。当你写my_array[2]时,你实际上是在说“给我数组中的第三个整数my_array”。因此,my_array是一个数组,但是my_array[2]是一个int
。
- 传递单个数组元素
所以假设你想将数组中的一个整数传递给一个函数。你该怎么做?只需传递解引用的元素,你就可以了。
示例
#include <iostream>
void printInt(int printable){
std::cout << "The int you passed in has value " << printable << std::endl;
}
int main(){
int my_array[5];
// Reminder: always initialize your array values!
for(int i = 0; i < 5; i++)
my_array[i] = i * 2;
for(int i = 0; i < 5; i++)
printInt(my_array[i]); // <-- We pass in a dereferenced array element
}
此程序输出以下内容
The int you passed in has value 0 The int you passed in has value 2 The int you passed in has value 4 The int you passed in has value 6 The int you passed in has value 8
这就像普通的整数一样传递数组元素,因为像my_array[2]这样的数组元素是整数。
- 传递整个数组
好吧,我们可以将单个数组元素传递给函数。但如果我们想传递整个数组呢?我们不能直接这样做,但你可以将数组视为一个指针。
示例
#include <iostream>
void printIntArr(int *array_arg, int array_len){
std::cout << "The length of the array is " << array_len << std::endl;
for(int i = 0; i < array_len; i++)
std::cout << "Array[" << i << "] = " << array_arg[i] << std::endl;
}
int main(){
int my_array[5];
// Reminder: always initialize your array values!
for(int i = 0; i < 5; i++)
my_array[i] = i * 2;
printIntArr(my_array, 5);
}
这将输出以下内容
The length of the array is 5 Array[0] = 0 Array[1] = 2 Array[2] = 4 Array[3] = 6 Array[4] = 8
如你所见,main 中的数组由一个指针访问。现在这里有一些重要的要点需要注意
- 一旦将数组传递给函数,它就会被转换为指针,因此函数无法知道如何猜测数组的长度。除非你始终使用大小相同的数组,否则你应该始终将数组长度与数组一起传递。
- 你传递了一个指针。my_array是一个数组,而不是指针。如果你在函数内部更改array_arg,my_array不会改变(即,如果你设置array_arg指向一个新数组)。但是,如果你更改任何元素array_arg,你正在更改array_arg指向的内存空间,即数组my_array.
按引用传递
[edit | edit source]传递变量时,使用了相同的引用概念。
示例
void foo( int &i )
{
++i;
}
int main()
{
int bar = 5; // bar == 5
foo( bar ); // increments bar
std::cout << bar << std::endl // 6
return 0;
}
示例 2:要交换两个整数的值,我们可以写
void swap (int& x, int& y)
{
int temp = x;
x = y;
y = temp;
}
在这个函数的调用中,我们给出了两个类型为int的变量
int i = 7;
int j = 9;
swap (i, j);
cout << i << ' ' << j << endl;
此输出为“9 7”。为该程序绘制一个堆栈图,以说服自己这是真的。如果参数 x 和 y 被声明为普通参数(没有“&”),swap 将不起作用。它只会修改函数swap中的 x 和 y,并且不会影响 i 和 j。
当人们开始以引用方式传递诸如整数之类的内容时,他们通常会尝试使用表达式作为引用参数。例如
int i = 7;
int j = 9;
swap (i, j+1); // WRONG!!
这是不合法的,因为表达式j+1不是一个变量 - 它不占用引用可以引用的位置。弄清楚哪些表达式可以以引用方式传递有点棘手。现在,一个好的经验法则是引用参数必须是变量。
这里展示了引用在函数参数中的两种常见用法之一 - 它们允许我们使用按值传递参数的传统语法,但可以在调用者中操作该值。
但是,引用在函数参数中有一个更常见的用法 - 它们也可以用来传递指向大型数据结构的句柄,而不会在过程中创建它的多个副本。考虑以下情况
void foo( const std::string & s ) // const reference, explained below
{
std::cout << s << std::endl;
}
void bar( std::string s )
{
std::cout << s << std::endl;
}
int main()
{
std::string const text = "This is a test.";
foo( text ); // doesn't make a copy of "text"
bar( text ); // makes a copy of "text"
return 0;
}
在这个简单的例子中,我们能够看到按值传递和按引用传递的区别。在这种情况下,按值传递只扩展了几个额外的字节,但想象一下,例如如果text包含了一整本书的文本。
我们使用常量引用而不是引用原因是,该函数的用户可以确保传递的变量的值不会在函数内部发生变化。从技术上讲,我们称之为“常量引用”。
能够以引用方式传递它可以避免我们创建字符串的副本,并避免使用指针的丑陋方式。
- 使用引用传递固定长度的数组
在某些情况下,函数需要特定长度的数组才能工作
void func(int(¶meter)[4]);
与数组转换为指针不同,parameter
不是可以转换为指针的普通数组,而是对包含 4 个 int
的数组的引用。因此,只能传递包含 4 个 int
的数组,不能传递其他长度的数组或指向 int 的指针。这有助于防止缓冲区溢出错误,因为数组对象始终会被分配,除非你通过强制类型转换绕过类型系统。
它可以用于传递数组,而无需手动指定元素数量
template<int n>void func(int(¶)[n]);
编译器在编译时生成长度值,在函数内部,n 存储元素数量。但是,使用模板会生成代码膨胀。
在 C++ 中,多维数组不能转换为多级指针,因此,以下代码无效
// WRONG
void foo(int**matrix,int n,int m);
int main(){
int data[10][5];
// do something on data
foo(data,10,5);
}
虽然 int[10][5] 可以转换为 (*int)[5],但不能转换为 int**。因此,你可能需要在函数声明中硬编码数组边界
// BAD
void foo(int(*matrix)[5],int n,int m);
int main(){
int data[10][5];
// do something on data
foo(data,10,5);
}
为了使函数更通用,应该使用模板和函数重载
// GOOD
template<int junk,int rubbish>void foo(int(&matrix)[junk][rubbish],int n,int m);
void foo(int**matrix,int n,int m);
int main(){
int data[10][5];
// do something on data
foo(data,10,5);
}
在第一个版本中使用 n 和 m 的原因主要是为了保持一致性,并处理未完全使用分配的数组的情况。它还可以用于通过比较 n/m 与垃圾/垃圾来检查缓冲区溢出。
按值传递
[edit | edit source]当我们要编写一个函数,其中参数的值独立于传递的变量时,我们使用按值传递方法。
int add(int num1, int num2)
{
num1 += num2; // change of value of "num1"
return num1;
}
int main()
{
int a = 10, b = 20, ans;
ans = add(a, b);
std::cout << a << " + " << b << " = " << ans << std::endl;
}
输出
10 + 20 = 30
上面的例子展示了按值传递的一个特性,参数是传递变量的副本,并且只存在于对应函数的 作用域中。这意味着我们必须承担复制的成本。然而,这种成本通常只针对更大更复杂的变量。
在这种情况下,"a" 和 "b" 的值被复制到 "num1" 和 "num2" 上的函数 "add()" 中。我们可以看到 "num1" 的值在第 3 行改变了。但是,我们也可以观察到 "a" 的值在传递到这个函数后保持不变。
常量参数
[edit | edit source]关键字const也可以用作保证函数不会修改传入值的保证。这实际上只对引用和指针有用(而不是按值传递的东西),尽管在语法上没有阻止使用const用于按值传递的参数。
例如以下函数
void foo(const std::string &s)
{
s.append("blah"); // ERROR -- we can't modify the string
std::cout << s.length() << std::endl; // fine
}
void bar(const Widget *w)
{
w->rotate(); // ERROR - rotate wouldn't be const
std::cout << w->name() << std::endl; // fine
}
在第一个例子中,我们试图调用一个非 const 方法 --append()-- 在作为const引用传递的参数上,从而违反了我们与调用者达成的协议,即不修改它,编译器会给我们一个错误。
对于rotate()也是一样,但是使用const指针在第二个例子中。
默认值
[edit | edit source]C++ 函数中的参数(包括成员函数和构造函数)可以声明为具有默认值,例如
int foo (int a, int b = 5, int c = 3);
然后,如果函数被调用时参数较少(但足以指定没有默认值的参数),编译器将为末尾缺少的参数假设默认值。例如,如果我调用
foo(6, 1)
这将等效于调用
foo(6, 1, 3)
在许多情况下,这可以让你不必定义两个分别接受不同数量参数的函数,这两个函数几乎完全相同,除了一个默认值。
"值" 作为默认值通常是一个常量,但可以是任何有效的表达式,包括执行任意计算的函数调用。
默认值只能用于最后一个参数;也就是说,你不能为一个参数指定默认值,而该参数后面跟着一个没有默认值的参数,因为它永远不会被使用。
一旦你在函数声明中定义了参数的默认值,你就不能在后面的声明中重新定义同一个参数的默认值,即使是相同的值。
省略号 (...) 作为参数
[edit | edit source]如果参数列表以省略号结尾,则意味着参数数量必须等于或大于指定参数的数量。它实际上会创建一个变参函数,即一个可变元数的函数;也就是说,一个可以接受不同数量参数的函数。
返回值
[edit | edit source]在声明函数时,必须根据函数将返回的类型声明它,这分三步完成,在函数声明中,函数实现(如果不同)以及在同一个函数的主体中使用 return
关键字。
- 有结果的函数
你可能已经注意到,有些函数会产生结果。其他函数执行操作,但不会 return
值。
从函数获取值的另一种方法是使用指针或引用作为参数,或使用全局变量
- 从函数获取多个值
返回值类型决定了容量,任何类型都可以使用,从数组或 std::vector、结构体或类,它只受你选择的返回值类型的限制。
- 这就引出了一些问题
- 如果你调用一个函数,但对结果不进行任何操作(即你不将其赋值给变量或不将其用作更大表达式的部分),会发生什么?
- 如果你使用没有结果的函数作为表达式的部分,比如 newLine() + 7,会发生什么?
- 我们可以编写产生结果的函数吗?或者我们只能使用 newLine 和 printTwice 之类的函数?
第三个问题的答案是 "是的,你可以编写返回值的函数"。现在,我会让你自己尝试回答其他两个问题。当你对 C++ 中合法或非法操作有任何疑问时,第一步是询问编译器。但是,你应该注意两个问题,我们在介绍编译器时已经提到过:首先,编译器可能会像任何其他软件一样存在错误,因此,并非所有在 C++ 中被禁止的源代码都被编译器正确拒绝,反之亦然。另一个问题更为危险:你可以在 C++ 中编写程序,C++ 实现不需要拒绝,但其行为不受语言定义。不用说,运行这样的程序可能会(而且偶尔会)对运行它的系统造成有害影响或产生错误的输出!
例如
int MyFunc(); // returns an int
SOMETYPE MyFunc(); // returns a SOMETYPE
int* MyFunc(); // returns a pointer to an int
SOMETYPE *MyFunc(); // returns a pointer to a SOMETYPE
SOMETYPE &MyFunc(); // returns a reference to a SOMETYPE
如果你理解了指针声明的语法,那么返回指针或引用的函数的声明应该是合乎逻辑的。上面的代码片段展示了如何声明一个返回引用或指针的函数;下面概述了这类函数的定义(实现)的样子
SOMETYPE *MyFunc(int *p)
{
//...
return p;
}
SOMETYPE &MyFunc(int &r)
{
//...
return r;
}
return
语句会导致执行从当前函数跳到调用当前函数的函数。可以返回可选的结果(返回值)。函数可能有多个 return
语句(但返回相同的类型)。
- 语法
return;
return value;
在函数体内,return
语句不应 return
指针或引用,该指针或引用在内存中具有在函数内声明的局部变量的地址,因为函数退出后,所有局部变量都会被销毁,你的指针或引用将指向你不再拥有的内存中的某个位置,因此你无法保证其内容。如果指针所指向的对象被销毁,则该指针被称为悬空指针,直到它被赋予一个新的值;对这种指针的值的任何使用都是无效的。拥有这样的悬空指针是危险的;指向局部变量的指针或引用必须不被允许从声明这些局部(也称为自动)变量的函数中逃逸。
但是,在您的函数主体中,如果您的指针或引用具有动态分配内存的结构体或类的内存地址,则使用 new 运算符,那么返回该指针或引用是合理的。
SOMETYPE *MyFunc() //returning a pointer that has a dynamically
{ //allocated memory address is valid code
int *p = new int[5];
//...
return p;
}
在大多数情况下,更好的方法是返回一个对象,例如智能指针,它可以管理内存;使用广泛分布的 new 和 delete(或 malloc 和 free)进行显式内存管理既繁琐又容易出错。至少,返回动态分配资源的函数应该仔细记录。有关更多详细信息,请参见本书关于内存管理的部分。
const SOMETYPE *MyFunc(int *p)
{
//...
return p;
}
在这种情况下,返回的指针指向的 SOMETYPE 对象可能不会被修改,如果 SOMETYPE 是一个类,那么只能对 SOMETYPE 对象调用 const 成员函数。
如果这样的 const 返回值是指向类的指针或引用,那么我们不能对该指针或引用调用非 const 方法,因为这会破坏我们不更改它的约定。
静态返回
[edit | edit source]当函数返回一个静态分配的变量(或指向它的指针)时,必须牢记每次调用使用它的函数时都可以覆盖它的内容。如果您想保存此函数的返回值,您应该手动将其保存到其他地方。大多数这样的静态返回使用全局变量。
当然,当您将其保存到其他地方时,您应该确保实际将该变量的值复制到另一个位置。如果返回值是结构体,您应该创建一个新的结构体,然后将结构体的成员复制过来。
此类函数的一个例子是标准 C 库函数 localtime。
返回“代码”(最佳实践)
[edit | edit source]有两种行为
正数表示成功
[edit | edit source]这是“逻辑”的思考方式,因此几乎所有初学者都使用它。在 C++ 中,这采取布尔真/假测试的形式,其中“真”(也为 1 或任何非零数)表示成功,“假”(也为 0)表示失败。
这种构造的主要问题是所有错误都返回相同的值(false),因此您必须有一些外部可见的错误代码才能确定错误发生的位置。例如
bool bOK;
if (my_function1())
{
// block of instruction 1
if (my_function2())
{
// block of instruction 2
if (my_function3())
{
// block of instruction 3
// Everything worked
error_code = NO_ERROR;
bOK = true;
}
else
{
//error handler for function 3 errors
error_code = FUNCTION_3_FAILED;
bOK = false;
}
}
else
{
//error handler for function 2 errors
error_code = FUNCTION_2_FAILED;
bOK = false;
}
}
else
{
//error handler for function 1 errors
error_code = FUNCTION_1_FAILED;
bOK = false;
}
return bOK;
如您所见,my_function1 的 else 块(通常是错误处理)可能与测试本身相距甚远;这是第一个问题。当您的函数开始增长时,通常很难同时看到测试和错误处理。
这个问题可以通过源代码编辑器的功能(例如折叠)或测试函数返回“false”而不是 true 来弥补。
if (!my_function1()) // or if (my_function1() == false)
{
//error handler for function 1 errors
//...
这也可能使代码看起来更像“0 表示成功”的范式,但可读性略差。
这种构造的第二个问题是它往往会破坏逻辑测试(my_function2 缩进了一级,my_function3 缩进两级),这会导致可读性问题。
这里的一个优点是您遵循结构化编程的原则,即函数具有单一入口和单一出口。
Microsoft Foundation Class Library (MFC) 是使用此范式的标准库的一个例子。
0 表示成功
[edit | edit source]这意味着如果函数返回 0,则函数已成功完成。任何其他值都表示发生了错误,返回的值可能是对发生错误的指示。
这种范式的优点是错误处理更接近测试本身。例如,前面的代码变为
if (0 != my_function1())
{
//error handler for function 1 errors
return FUNCTION_1_FAILED;
}
// block of instruction 1
if (0 != my_function2())
{
//error handler for function 2 errors
return FUNCTION_2_FAILED;
}
// block of instruction 2
if (0 != my_function3())
{
//error handler for function 3 errors
return FUNCTION_3_FAILED;
}
// block of instruction 3
// Everything worked
return 0; // NO_ERROR
在此示例中,此代码更具可读性(这并不总是如此)。但是,此函数现在有多个出口点,违反了结构化编程的原则。
C 标准库 (libc) 是使用此范式的标准库的一个例子。
组合
[edit | edit source]就像数学函数一样,C++ 函数可以组合,这意味着您可以将一个表达式用作另一个表达式的一部分。例如,您可以将任何表达式用作函数的参数
double x = cos (angle + pi/2);
此语句将 pi 的值除以 2,并将结果加到角度的值。然后将总和作为参数传递给 cos 函数。
您还可以取一个函数的结果,并将其作为参数传递给另一个函数
double x = exp (log (10.0));
此语句求 e 的 10 的对数,然后将 e 提高到该幂。结果被分配给 x;我希望您知道它是多少。
递归
[edit | edit source]在编程语言中,递归最初是在 Lisp 中实现的,其基础是早期的数学概念,它是一个概念,它允许我们将问题分解成一个或多个子问题,这些子问题在形式上类似于原始问题,在这种情况下,是让函数在某些情况下调用自身。它通常与迭代器或循环区别开来。
递归函数的一个简单示例是
void func(){
func();
}
需要注意的是,如上所示的非终止递归函数几乎从未在程序中使用(实际上,递归的一些定义会排除这些非终止定义)。终止条件用于防止无限递归。
- 示例
double power(double x, int n)
{
if (n < 0)
{
std::cout << std::endl
<< "Negative index, program terminated.";
exit(1);
}
if (n)
return x * power(x, n-1);
else
return 1.0;
}
上述函数可以像这样调用
x = power(x, static_cast<int>(power(2.0, 2)));
为什么递归有用?虽然理论上,递归可以实现的任何事情都可以通过迭代实现(即 while),但有时使用递归更方便。递归代码碰巧更容易理解,如下面的示例所示。递归代码的问题是它占用太多内存。由于函数被多次调用,而没有删除来自调用函数的数据,内存需求会显著增加。但通常,递归代码的简洁性和优雅性会超过内存需求。
递归的经典例子是阶乘:,其中 按惯例。在递归中,此函数可以简洁地定义为
unsigned factorial(unsigned n)
{
if (n != 0)
{
return n * factorial(n-1);
}
else
{
return 1;
}
}
使用迭代,逻辑更难理解
unsigned factorial2(unsigned n)
{
int a = 1;
while(n > 0)
{
a = a*n;
n = n-1;
}
return a;
}
虽然递归比迭代稍微慢一些,但在迭代会产生冗长且难以理解的代码的情况下,应该使用递归。此外,请记住递归函数在每个级别上都会占用额外的内存(在堆栈上)。因此,在迭代方法可能只使用常量内存的情况下,它们可能会耗尽内存。
每个递归函数都需要有一个基本情况。基本情况是递归函数停止调用自身并返回一个值的地方。返回的值(希望)是期望的值。
对于前面的示例,
unsigned factorial(unsigned n)
{
if(n != 0)
{
return n * factorial(n-1);
}
else
{
return 1;
}
}
基本情况是在 时达到。在这个例子中,基本情况是 else 语句中包含的所有内容(它恰好返回数字 1)。返回的总值是从 到 的所有值相乘。所以,假设我们调用该函数并传递给它值 。然后函数进行计算 并返回 6 作为调用 factorial(3) 的结果。
另一个经典的递归示例是斐波那契数列
0 1 1 2 3 5 8 13 21 34 ...
该序列的第零个元素为 0。下一个元素为 1。该序列中的任何其他数字都是它前面两个元素的总和。作为练习,编写一个使用递归返回第n个斐波那契数的函数。
main
[edit | edit source]函数main也恰好是任何(符合标准的)C++ 程序的入口点,并且必须定义。编译器安排在程序开始执行时调用该main函数。main可以调用其他函数,这些函数可以再调用其他函数。
该main函数返回一个整数值。在某些系统中,此值被解释为成功/失败代码。返回值为零表示程序成功完成。任何非零值都被认为是失败。与其他函数不同,如果控制到达main()的末尾,则会自动添加用于成功的隐式 return 0;
。为了使来自main的返回值更易读,头文件cstdlib定义了常量EXIT_SUCCESS和EXIT_FAILURE(分别表示成功/不成功完成)。
main 函数也可以这样声明
int main(int argc, char **argv){
// code
}
它将该main函数定义为返回整数值 int 并接受两个参数。该main函数的第一个参数 argc 是一个整数值 int,它指定传递给程序的参数数量,而第二个参数 argv 是一个包含实际参数的字符串数组。程序几乎总是至少传递一个参数;程序本身的名称是第一个参数,argv[0]。其他参数可以从系统传递。
示例
#include <iostream>
int main(int argc, char **argv){
std::cout << "Number of arguments: " << argc << std::endl;
for(size_t i = 0; i < argc; i++)
std::cout << " Argument " << i << " = '" << argv[i] << "'" << std::endl;
}
如果上面的程序编译成可执行文件arguments并像这样在 *nix 中从命令行执行
$ ./arguments I love chocolate cake
或在 Windows 或 MS-DOS 中的命令提示符下
C:\>arguments I love chocolate cake
它将输出以下内容(但请注意,参数 0 可能与这不太一样——它可能包含完整路径,或者只包含程序名称,或者包含相对路径,或者甚至可能为空)
Number of arguments: 5 Argument 0 = './arguments' Argument 1 = 'I' Argument 2 = 'love' Argument 3 = 'chocolate' Argument 4 = 'cake'
您可以看到程序的命令行参数被存储到argv数组中,并且argc包含该数组的长度。这使您可以根据传递给它的命令行参数更改程序的行为。
指向函数的指针
[edit | edit source]我们到目前为止查看过的指针都是数据指针,指向函数的指针(更常称为函数指针)非常相似,它们具有其他指针的相同特征,但它们指向函数而不是指向变量。创建了一个额外的间接级别,作为在 C++ 中使用函数式编程范式的一种方式,因为它方便了调用从同一代码段在运行时确定的函数。它们允许将函数作为参数或返回值传递给另一个函数。
使用函数指针与任何其他函数调用的开销完全相同,再加上额外的指针间接寻址,并且由于要调用的函数仅在运行时确定,编译器通常不会像在其他地方那样内联函数调用。由于具有此特征,使用函数指针可能比使用常规函数调用慢得多,并且应避免作为提高性能的一种方式。
要简单地声明指向函数的指针,必须将指针的名称括起来,否则将声明一个返回指针的函数。你还必须声明函数的返回类型及其参数。这些必须完全相同!
考虑
int (*ptof)(int arg);
要引用的函数必须具有与指向函数的指针相同的返回类型和相同的参数类型。函数的地址可以通过使用其名称来分配,也可以选择性地加上地址运算符 &。调用函数可以使用 ptof(<value>) 或 (*ptof)(<value>) 来完成。
所以
int (*ptof)(int arg);
int func(int arg){
//function body
}
ptof = &func; // get a pointer to func
ptof = func; // same effect as ptof = &func
(*ptof)(5); // calls func
ptof(5); // same thing.
返回 a 的函数float不能被返回 a 的指针指向double。如果两个名称相同(例如int和signed,或 atypedef名称),则允许转换。否则,它们必须完全相同。你可以通过将*与变量名组合来定义指针,就像你处理任何其他指针一样。问题是它可能被解释为返回类型而不是指针。
通常使用 typedef 来定义函数指针类型更清晰;这也提供了一个地方来为函数指针的类型指定一个有意义的名称
typedef int (*int_to_int_function)(int);
int_to_int_function ptof;
int *func (int); // WRONG: Declares a function taking an int returning pointer-to-int.
int (*func) (int); // RIGHT: Defines a pointer to a function taking an int returning int.
为了减少混淆,通常会typedef函数类型或指针类型
typedef int ifunc (int); // now "ifunc" means "function taking an int returning int"
typedef int (*pfunc) (int); // now "pfunc" means "pointer to function taking an int returning int"
如果你typedef函数类型,你可以声明,但不能定义具有该类型的函数。如果你typdef指针类型,你不能声明或定义具有该类型的函数。使用哪种方式是一个风格问题(尽管指针更流行)。
要将指针分配给函数,你只需将其分配给函数名称。该&运算符是可选的(它并不模棱两可)。如果存在,编译器会自动选择适合指针的重载函数版本。
int f (int, int);
int f (int, double);
int g (int, int = 4);
double h (int);
int i (int);
int (*p) (int) = &g; // ERROR: The default parameter needs to be included in the pointer type.
p = &h; // ERROR: The return type needs to match exactly.
p = &i; // Correct.
p = i; // Also correct.
int (*p2) (int, double);
p2 = f; // Correct: The compiler automatically picks "int f (int, double)".
使用指向函数的指针更简单 - 你只需像调用函数一样调用它。你被允许使用*运算符来解除引用它,但你不必
#include <iostream>
int f (int i) { return 2 * i; }
int main ()
{
int (*g) (int) = f;
std::cout<<"g(4) is "<<g(4)<<std::endl; // Will output "g(4) is 8"
std::cout<<"(*g)(5) is "<<g(5)<<std::endl; // Will output "g(5) is 10"
return 0;
}
回调
[edit | edit source]在计算机编程中,回调是作为参数传递给其他代码的可执行代码。它允许较低级的抽象层调用在较高层定义的函数。回调通常返回到原始调用者的级别。
通常,较高层代码首先调用较低层代码中的函数,并将指向另一个函数的指针或句柄传递给它。在较低层函数执行期间,它可能会多次调用传递的函数来执行某些子任务。在另一种情况下,较低层函数将传递的函数注册为一个处理程序,该处理程序将在稍后由较低层异步调用,以响应某些事件。
回调可以用作多态和泛型编程的更简单替代方案,因为函数的确切行为可以通过将不同的(但兼容的)函数指针或句柄传递给较低层函数来动态确定。这对于代码重用来说可能是一种非常强大的技术。在另一种常见情况下,回调首先注册,然后异步调用。
重载
[edit | edit source]函数重载是在相同作用域内使用单个名称来表示多个不同函数。共享相同名称的多个函数必须使用不同的参数集来区分。这些函数可以在它们期望的参数数量上有所不同,或者它们的类型可以有所不同。通过这种方式,编译器可以通过查看调用者提供的参数来确定要调用的确切函数。这被称为重载解析,非常复杂。
// Overloading Example
// (1)
double geometric_mean( int, int );
// (2)
double geometric_mean( double, double );
// (3)
double geometric_mean( double, double, double );
// ...
// Will call (1):
geometric_mean( 10, 25 );
// Will call (2):
geometric_mean( 22.1, 421.77 );
// Will call (3):
geometric_mean( 11.1, 0.4, 2.224 );
在某些情况下,调用可能是模棱两可的,因为两个或多个函数与提供的参数同样匹配。
例如,假设上面定义了 geometric_mean
// This is an error, because (1) could be called and the second // argument casted to an int, and (2) could be called with the first // argument casted to a double. None of the two functions is // unambiguously a better match. geometric_mean(7, 13.21); // This will call (3) too, despite its last argument being an int, // Because (3) is the only function which can be called with 3 // arguments geometric_mean(1.1, 2.2, 3);
模板和非模板可以重载。如果两种形式的函数与提供的参数同样匹配,则非模板函数优先于模板函数。
注意,你也可以在 C++ 中重载许多运算符。
重载解析
[edit | edit source]请注意,C++ 中的重载解析是该语言中最复杂的部分之一。这在任何情况下可能都是不可避免的,因为存在自动模板实例化、用户定义的隐式转换、内置的隐式转换以及更多语言特性。所以,如果你一开始不理解这一点,不要绝望。一旦你掌握了这些概念,它就变得非常自然,但是写下来看起来非常复杂。
理解重载的最简单方法是想象编译器首先找到所有可能被调用的函数,使用所有合法的转换和模板实例化。然后,编译器从这个集合中选择最佳匹配(如果有的话)。具体来说,这个集合的构造方式如下
- 所有具有匹配名称的函数(包括函数模板)都放入集合中。不考虑返回类型和可见性。模板以尽可能匹配的参数添加。成员函数被视为第一个参数是指向类类型的指针的函数。
- 转换函数被添加为所谓的代理函数,具有两个参数,第一个是类类型,第二个是返回类型。
- 所有不匹配参数数量的函数(即使在考虑默认参数和省略号之后),都从集合中删除。
- 对于每个函数,将考虑每个参数,以查看是否存在合法的转换序列来将调用者的参数转换为函数的参数。如果找不到这样的转换序列,则从集合中删除该函数。
合法的转换在下面详细说明,但简而言之,合法转换是任何数量的内置(如 int 到 float)转换,再加上最多一个用户定义的转换。最后一部分对于理解如果你正在编写内置类型的替换(例如智能指针)至关重要。用户定义的转换在上面描述过,但总结一下,就是
- 隐式转换运算符,如operator short toShort();
- 单参数构造函数(如果构造函数的所有参数除了一个之外都已默认,则认为它是单参数的)
重载解析通过尝试建立最佳匹配函数来工作。
- 优先考虑简单的转换
查看一个参数,优先的转换大致基于转换的范围。具体来说,转换按以下顺序优先考虑,其中最优先的最高
- 无转换,添加一个或多个 const,添加引用,将数组转换为指向第一个成员的指针
- const 优先用于右值(大致为常量),而非 const 优先用于左值(大致为可赋值的)
- 从短整型(bool、char、short)转换为 int,以及 float 到 double 的转换。
- 内置转换,例如在 int 和 double 之间的转换以及指针类型转换。指针转换的排名为
- 基类到派生类(指针)或派生类到基类(对于指向成员的指针),其中最派生的优先
- 转换为void*
- 转换为 bool
- 用户定义的转换,见上文。
- 与省略号匹配。(顺便说一下,这是模板元编程相当有用的知识)
最佳匹配现在根据以下规则确定
- 一个函数只有在所有参数至少匹配得一样好的情况下才是更好的匹配
简而言之,该函数必须在各个方面都更好——如果一个参数匹配得更好,而另一个参数匹配得更差,则两个函数都不被认为是更好的匹配。如果集合中没有一个函数比这两个函数都更好,则调用是模棱两可的(即它失败)示例
void foo(void*, bool);
void foo(int*, int);
int main() {
int a;
foo(&a, true); // ambiguous
}
- 非模板应该优先于模板
如果两个函数在其他方面都相等,但一个是模板,另一个不是,则优先考虑非模板函数。这很少会造成意外。
- 最专业的模板优先
当两个模板函数在其他方面都相等,但一个是比另一个更专业的,则优先考虑最专业的版本。示例
template<typename T> void foo(T); //1
template<typename T> void foo(T*); //2
int main() {
int a;
foo(&a); // Calls 2, since 2 is more specialized.
}
哪个模板更专业是一个完整的章节。
- 忽略返回类型
上面提到了这条规则,但它值得重复:返回类型从不是重载解析的一部分,即使所选函数的返回类型会导致编译失败。示例
void foo(int);
int foo(float);
int main() {
// This will fail since foo(int) is best match, and void cannot be converted to int.
return foo(5);
}
- 所选函数可能不可访问
如果所选的最佳函数不可访问(例如,它是一个私有函数,并且调用不是来自它的类的成员或友元),则调用失败。