好久没有更新了,虽然自己最近有点忙但是还有更新的,最近办了几件大事,只有我知道~~。但愿将来能过好吧!!闲话少说吧~ 函数是命了名的代码块,代码是存储在代码段里面的。
6.1 函数基础
一个典型的函数定义包括以下几个部分,返回类型、函数名字由0个或者多个形参构成。我们在调用函数的过程中,一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数。此时,主调函数(calling function)的执行被暂时中断,被调函数(called function) 开始执行。这里还得讲到实参和形参了。实参是形参的初始值~函数定义的过程变量那个就是形参,当调用函数的时候用实参开始调用。实参就是穿进去的值~。
函数的形参可以为空但不能省略,如下:
void f1(){ /* ... */ } // implicit void parameter list
void f2(void){ /* ... */ } // explicit void parameter list
形参列表中的形参通常用逗号隔开,其中每个形参都是含有一个声明符的声明。即使两个形参的类型一样,也必须把两个类型都写出来:
int f3(int v1, v2) { /* ... */ } // error
int f4(int v1, int v2) { /* ... */ } // ok
大多数类型都能用作函数的返回类型。一种特殊的返回类型是void,它表示函数不返回任何值。函数的返回类型不能是数组(参见3.5节,第101页)类型或函数类型,但可以是指向数组或函数的指针。
局部对象: 在c++语言中,名字有作用域(参见224节,第43页),对象有生命周期(lifetime)o 理解这两个概念非常重要。名字的用域是程序文本的一部分,名字在其中可见。对象的生命周期是程序执行过程中该对象存在的一段时间。如我们所知,函数体是一个语句块。块构成一个新的作用域,我们可以在其中定义变量。形参和函数体内部定义的变量统称为局部变量(localvariable)o它们对函数而言是“局部”的,仅在函数的作用域内可见,同时局部变量还会隐藏(hide)在外层作用域中同名的其他所有声明中。说白了,举个例子就是:当局部变量和全局变量具有相同的名称时,局部变量会覆盖全局变量,在局部变量所在的块或者函数内,对变量的操作不影响全局变量的值,全局变量不起作用,在局部变量所在的代码块或函数外部,全局变量才起作用。
自动对象: 对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。我们把只存在于块执行期间的对象称为自动对象 (automatic object)o当块的执行结束后,块中创建的自动对象的值就变成未定义的了。形参是一种自动对象。函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。
局部静态对象:某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成static类型从而获得这样的对象。局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使一对象所在的函数结束执行也不会对它有影响。举个例子:
size_t count_calls()
{
static size_t ctr = 0; // value will persist across calls
return ++ctr;
}
int main()
{
for (size_t i = 0; i != 10; ++i)
cout << count_calls() << endl;
return 0;
}
函数声明:和其他名字一样,函数的名字也必须在使用之前声明。类似于变量(参见222节,第41页),函数只能定义一次,但可以声明多次。函数的三要素(返回类型、函数名、形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称作函数原型。看起来把函数的声明直接放在使用该函数的源文件中是合法的,也比较容易被人接受:但是这么做可能会很烦琐而且容易出错。相反,如果把函数声明放在头文件中,就能确保同一函数的所有声明保持一致。而且一旦我们想改变函数的接口,只需改变一条声明即可。
分离式编译:随着程序越来越复杂,我们希望把程序的各个部分分别存储在不同文件中。例如,可以把6.1节练习(第184页)的函数存在一个文件里,把使用这些函数的代码存在其他源文件中。为了允许编写程序时按照逻辑关系将其划分开来,C++语言支持所谓的分离式编译(separate compilation)分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。
6.2参数传递
关于函数的参数传递其实比较复杂,这里也是一点一点分开讲。和其他变量一样,形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对应的实参上:否则,将实参的值拷贝后赋给形参。当形参是引用类型时,我们说它对应的实参被引用传递(passed byreference)或者函数被传引用调用(calledbyreference)。和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名。当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递(passed by value)或者函数被传值调用(called by value)。
6.2.1传值参数:
传值参数类似初始值拷贝到变量,此时变量的改动不会影响初始值!传值参数的机理完全一样,函数对形参做的所有操作都不会影响实参。当形参是指针参数的时候,和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值。和C语言常用的指针传参概念一样。
6.2.2传引用参数:
举个例子:
// function that takes a reference to an int and sets the given object to zero
void reset(int &i) // i is just another name for the object passed to reset
{
i = 0; // changes the value of the object to which i refers
}
int j = 42;
reset(j); // j is passed by reference; the value in j is changed 这里在赋值直接初始化引用了
cout << "j = " << j << endl; // prints j = 0
和其他引用一样,引用形参绑定初始化它的对象。当调用这一版本的reset函数时,i 绑定我们传给函数的int对象,此时改变i也就是改变i所引对象的值。此例中,被改变的对象是传入reset的实参。调用这一版本的reset函数时,我们直接传入对象而无须传递对象的地址:在上述调用过程中,形参i仅仅是j的又一个名字。在reset内部对i的使用即是对j 的使用。
使用引用类型可以避免拷贝操作,拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。所以引用可以提高函数的使用效率~~~
6.2.3const形参和实参:
关于const其实很麻烦,C++设计的这点我很容易搞晕!闲话少说吧!当形参是const时,所述,顶层const作用于对象本身:
const int ci = 42; // we cannot change ci; const is top-level
int i = ci; // ok: when we copy ci, its top-level const is
ignored
int * const p = &i; // const is top-level; we can't assign to p
*p = 0; // ok: changes through p are allowed; i is now 0
和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层consto换句话说,形参的顶层const被忽略掉了。当形参有顶层const时,传给它常量对象或者非常量对象都是可以的:
void fcn(const int i) { /* fcn can read but not write to i */ }
void fcn(const int i) { /* fcn can read but not write to i */ }
void fcn(int i) { /* . . . */ } // error: redefines fcn(int)
调用fcn函数时,既可以传入const int也可以传入int忽略掉形参的顶层const 可能产生意想不到的结果:在c++语言中,允许我们定义若干具有相同名字的函数,不过前提是不同函数的形参列表应该有明显的区别。因为顶层const被忽略掉了,所以在上面的代码中传入两个fcn函数的参数可以完全一样。因此第二个fcn是错误的,尽管形式上有差异,但实际上它的形参和第一个fcn的形参没什么不同。
指针或引用形参与const:
形参的初始化方式和变量的初始化方式是一样的,所以回顾通用的初始化规则有助于理解本节知识。我们可以使用非常量初始化一个底层const对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化:
int i = 42;
const int *cp = &i; // ok: but cp can't change i (§ 2.4.2 (p. 62))
const int &r = i; // ok: but r can't change i (§ 2.4.1 (p. 61))
const int &r2 = 42; // ok: (§ 2.4.1 (p. 61))
int *p = cp; // error: types of p and cp don't match (§ 2.4.2 (p. 62))
int &r3 = r; // error: types of r3 and r don't match (§ 2.4.1 (p. 61))
int &r4 = 42; // error: can't initialize a plain reference from a literal (§ 2.3.1 (p.50))
将同样的初始化规则应用到参数传递上可得如下形式:其中 reset的形式参数为int*
int i = 0;
const int ci = i;
string::size_type ctr = 0;
reset(&i); // calls the version of reset that has an int* parameter
reset(&ci); // error: can't initialize an int* from a pointer to a const int object
reset(i); // calls the version of reset that has an int& parameter
reset(ci); // error: can't bind a plain reference to the const object ci
reset(42); // error: can't bind a plain reference to a literal
reset(ctr); // error: types don't match; ctr has an unsigned type
// ok: find_char's first parameter is a reference to const
find_char("Hello World!", 'o', ctr);
要想调用引用版本的reset,只能使用int类型的对象,而不能使用字面值、求值结果为int的表达式、需要转换的对象或者const int类型的对象。类似的,要想调用指针版本的reset,只能使用int* 另一方面,我们能传递一个字符串字面值作为find_char的第一个实参,这是因为该函数的引用形参是常量引用,而C++允许我们用字面值初始化常量引用。
尽量使用常量引用:
把函数不会改变的形参定义成(普通的)引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。就像刚刚看到的,我们不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参。这种错误绝不像看起来那么简单,它可能造成出人意料的后果。举个例子:
// bad design: the first parameter should be a const string&
string::size_type find_char(string &s, char c,string::size_type &occurs);/
find_char("Hello World", 'o', ctr);
上述find_char调用会发生错误,编译不过的因为“hello world”为常量引用。
6.2.4数组形参:
数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组以及使用数组时(通常)会将其转换成指针。因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式:
void print(const int*);
void print(const int[]); // shows the intent that the function takes an
array
void print(const int[10]); // dimension for documentation purposes (atbest)
如果我们传给print函数的是一个数组,则实参自动地转换成指向数组首元素的指针,数组的大小对函数的调用没有影响。因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术。
管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针,这种方法受到了标准库技术的启发,关于其细节将在第二部分详细介绍。使用该方法,我们可以按照如下形式输出元素内容:
void print(const int *beg, const int *end)
{
// print every element starting at beg up to but not including end
while (beg != end)
cout << *beg++ << endl; // print the current element
// and advance the pointer
}
为了调用这个函数,我们需要传入两个指针:一个指向要输出的首元素,另一个指向尾元素的下一位置:
print(begin(j), end(j)); // begin and end functions, see § 3.5.3 (p. 118)
6.2.5 main函数处理命令首选项
main函数是演示C++程序如何向函数传递数组的好例子。到目前为止,我们定义的 main函数都只有空形参列表:
int main(){…………}
然而,有时我们确实需要给main传递实参,一种常见的情况是用户通过设置一组选项来确定函数所要执行的操作。例如,假定main函数位于可执行文件prog之内,我们可以向程序传递下面的选项:prog —d一0 Ofile data。
这些命令行选项通过两个(可选的)形参传递给main函数。
int main(int argc, char *argv[]) { ... }
第二个形参argv是一个数组,它的元素是指向c风格字符串的指针:第一个形参argc 表示数组中字符串的数量。因为第二个形参是数组,所以main函数也可以定义成:
int main(int argc, char *argv[]) { ... }
以上提供的命令为例,argc应该等于5,argv应该包含如下的C风格字符串:
argv[0] = "prog"; // or argv[0] might point to an empty string
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;
6.3返回类型和return语句
return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。没有返回值的return语句只能用在返回类型是void的函数中。返回void的函数不要求非得有return语句,因为在这类函数的最后一句后面会隐式地执行return 0.强行令void函数返回其他类型的表达式将产生编译错误。
return语句的第二种形式提供了函数的结果。只要函数的返回类型不是void,则该函数内的每条return语句必须返回一个值。return语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成(参见4.11节,第141页)函数的返回类型。
不要返回局部对象的引用!这句话是一个书中的标题,但是这句话的意思并不能代表一切的情况,如果局部对象是栈上的局部对象,那么函数结束的时候返回指向其局部对象的指针,肯定是行不通的,但是如果局部对象是堆上的,是可以返回的。
如果我们想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后。然而,函数的形参列表也跟在函数名字后面且形参列表应该先于数组的维度。因此,返回数组指针的函数形式如下所示:
Type (*function(parameter_list))[dimension]
int (*func(int i))[10];
6.4函数重载
如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载 (overloaded)函数。对于重载的函数来说,它们应该在形参数量或形参类型上有所不同。在上面的代码中,虽然每个函数都只接受一个参数,但是参数的类型不同。不允许两个函数除了返回类型外其他所有的要素都相同。假设有两个函数,它们的形参列表一样但是返回类型不同,则第二个函数的声明是错误的:
Record lookup(const Account&);
bool lookup(const Account&); // error: only the return type is different
重载和const 形参:顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来:
Record lookup(Phone);
Record lookup(const Phone); // redeclares Record lookup(Phone)
Record lookup(Phone*);
Record lookup(Phone* const); // redeclares Record lookup(Phone*)
在两组函数的声明中,每一组的第二个申明和第一个申明是等价的。另一方面,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的:
Record lookup(Account&); // function that takes a reference to Account
Record lookup(const Account&); // new function that takes a const reference
Record lookup(Account*); // new function, takes a pointer to Account
Record lookup(const Account*); // new function, takes a pointer to const
在上面的例子中,编译器可以通过实参是否是常量来推断应该调用哪个函数。因为const 不能转换成其他类型,所以我们只能把const对象(或指向 const的指针)传递给const形参。
函数重载与作用域:请看下面的例子:
void print(double); // overloads the print function
void fooBar(int ival)
{
bool read = false; // new scope: hides the outer declaration of read
string s = read(); // error: read is a bool variable, not a function
// bad practice: usually it's a bad idea to declare functions at local scope
void print(int); // new scope: hides previous instances of print
print("Value: "); // error: print(const string &) is hidden
print(ival); // ok: print(int) is visible
print(3.14); // ok: calls print(int); print(double) is hidden
}
上面函数中的read是一个布尔类型,隐藏了外面的作用域。大多数读者都能理解调用read函数会引发错误。因为当编译器处理调用read的请求时,找到的是定义在局部作用域中的read。
6.5特殊用途语言特性
某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参(default argument)o调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。
内联函数通常就是通常就是将它在每个调用点上“内联地”展开。省去了函数调用,可以理解为直接在运行过程中的一个替换。内联函数用于规模小,流程直接,频繁调用的函数。
constexpr函数(constexpr function)是指能用于常量表达式的函数。定义constexpr函数的方法与其他函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句:执行该初始化任务时,编译器把对constexpr函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz(); // ok: foo is a constant expression
和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义。毕竟,编译器要想展开函数仅有函数声明是不够的,还需要函数的定义。不过,对于某个给定的内联函数或者constexpr函数来说,它的多个定义必须完全一致。基于这个原因,内联函数和constexpr函数通常定义在头文件中。
当应用程序编写完成准备发布时,要先屏蔽掉调试代码。这种方法用到两项预处理功能:assert和NDEBUG。
assert预处理宏assert是一种预处理宏(preprocessor marco)o所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert宏使用一个表达式作为它的条件: assert(expr).首先对expr求值,如果表达式为假(即0),assert输出信息并终止程序的执行。如果表达式为真(即非0),assert什么也不做。assert宏定义在cassert头文件中。assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG, 则assert什么也不做。
除了C++编译器定义的func之外,预处理器还定义了另外4个对于程序调试很有用的名字:
_ _FILE_ _ string literal containing the name of the file
_ _LINE_ _ integer literal containing the current line number
_ _TIME_ _ string literal containing the time the file was compiled
_ _DATE_ _ string literal containing the date the file was compiled
6.6函数指针
在C里面经常讲,函数指针,其实就是回调函数~。函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。该函数的类型是bool (const string&, const string&)。要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可:从我们声明的名字开始观察,pf前面有个*,因此pf是指针;右侧是形参列表,表示pf 指向的是函数:再观察左侧,发现函数的返回类型是布尔值。因此,pf就是一个指向函数的指针,其中该函数的参数是两个const string的引用,返回值是bool类型。pf两端的括号必不可少,如果不写这对括号,则pf是一个返回类型为bool的类型。
bool lengthCompare(const string &, const string &);
bool (*pf)(const string &, const string &); // uninitialized
当我们把函数名作为一个值使用时,该函数自动地转换成指针。例如,按照如下形式我们可以将lengthCompare的地址赋给pf:此外,我们还能直接使用指向函数的指针调用该函数,无须提前解引用指针:
pf = lengthCompare; // pf now points to the function named lengthCompare
pf = &lengthCompare; // equivalent assignment: address-of operator is optional
bool b1 = pf("hello", "goodbye"); // calls lengthCompare
bool b2 = (*pf)("hello", "goodbye"); // equivalent call
bool b3 = lengthCompare("hello", "goodbye"); // equivalent call