- 函数在使用前也需要声明
- 如果一个函数永远不会被调用,可以只有声明,没有实现
- 与变量一样,函数声明应该放在头文件中,函数的实现应该放在源文件中
- 在函数中声明函数是一个不好的习惯
如果函数要使用参数,则必须声明接受参数值的变量。这些变量称为函数的形式参数。形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁。当调用函数时,有两种向函数传递参数的方式:
- 传值调用:该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数对实际参数没有影响。
- 指针调用:该方法把参数的地址复制给形式参数。在函数内,该地址用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。
- 引用调用:该方法把参数的引用复制给形式参数。在函数内,该引用用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。
引用与常量参数:
- 把参数声明为引用类型,可以避免是实参被拷贝
- 如果无需修改参数值,最好把参数声明为常量。
- 使用普通引用而非常量引用极大的限制了函数能接受的实参类型,这个函数只能接受非常量类型
从 C 语言背景转到 C++ 的程序员习惯通过传递指针来实现对实参的访问。在 C++ 中,使用引用形参则更安全和更自然。
//交换两个int指针的值
void ptrswap(int *&v1, int *&v2)
{
int *tmp = v2;
v2 = v1;
v1 = tmp;
}
从避免复制 vector 的角度出发,应考虑将形参声明为引用类型。然而事实上,C++ 程序员 倾向于通过传递指向容器中需要处理的元素的迭代器来传递容器
// pass iterators to the first and one past the last element to print
void print(vector<int>::const_iterator beg,
vector<int>::const_iterator end)
{
while (beg != end) {
cout << *beg++;
if (beg != end) cout << " "; // no space after last element
}
cout << endl;
}
因为数组会被自动转化为指针,所以处理数组的函数通常通过操纵指向数组指向数组中的元素的指针来处理数组。
//三种等效的声明方式
void printValues(int*) { /* ... */ }
void printValues(int[]) { /* ... */ }
void printValues(int[10]) { /* ... */ }
通过引用传递数组:和其他类型一样,数组形参可声明为数组的引用。如果形参是数组的引用, 编译器不会将数组实参转化为指针,而是传递数组的引用本身。和使用指针的区别是, 在这种情况下,数组大小成为形参和实参类型的一部分。编译器检查数组的实参的大小与形参的大小是否匹配
// 参数是数组的引用;数组的大小是固定的
//printValues 函数只严格地接受含有 10 个 int 型数值的数组,这限制了哪些数组可以传递。然而,由于形参是引用,在函数体中依赖数组的大小是安全的:
void printValues(int (&arr)[10]) { /* ... */ }
int main() {
int i = 0, j[2] = {0, 1};
int k[10] = {0,1,2,3,4,5,6,7,8,9};
printValues(&i); // error: argument is not an array of 10 ints
printValues(j); // error: argument is not an array of 10 ints
printValues(k); // ok: argument is an array of 10 ints
return 0;
}
数组参数的边界处理:非引用数组形参的类型检查只是确保实参是和数组元素具有同样类型的指针,而不会检查实参实际上是否指向指定大小的数组。 有三种常见的编程技巧确保函数的操作不超出数组实参的边界。
- 使用特殊标记:在数组本身放置一个标记来检测数组的结束。C 风格字符串就是采用这种方法的一个例子,它是一种字符数组, 并且以空字符作为结束的标记。处理 C 风格字符串的程序就是使用这个标记停止数组元素的处理。
- 标准库规范:传递指向数组第一个和最后一个元素的下一个位置的指针。这种编程风格由标准库所使用的技术启发而得。
void printValues(const int *beg, const int *end) {
while (beg != end) {
cout << *beg++ << endl;
}
}
- 显式传递表示数组大小的形参:将第二个形参定义为表示数组的大小,这种用法在 C 程序和标准化之前的 C++ 程序中十分普遍。
当形参是const时,顶层const作用于对象本身
const int ci = 32;//不能改变ci的值,const是鼎顶层的
int a = ci;//正确,拷贝ci时,忽略了它的const
int * const p = &a;//const是顶层的,不能给p赋值
*p = 0;//可以
当实参初始化形参时,会忽略掉顶层const,形参的顶层const被忽略掉了, 当形参有顶层const,传递给它const int或者int都是可以的
void fun(const int i);//可以读取i,不能修改i
void fun(int i);//重复声明fun(int)
形参的初始化方式与变量的初始化方式一样
int i = 43;
const int *cp = &i;//正确
const int &r = i;//正确
const int &r2 = 43;//正确
int *p = cp;//错误,p和cp类型不匹配
int &r3 = r;//错误,类型不匹配
int &r4 = 42;//错误,不能用字面值初始化非常量引用
对于reset(int &i)
和reset(int *i)
函数
int i = 0;
const int ci = i;
string::size_type ctr = 0;
rest(&i)//正确,调用reset(int *i)
reset(&ci)//错误,不能用指向const int的指针初始化int*
rest(i)//正确,调用reset(int &i)
rest(ci)//错误,不能把普通引用绑定到const对象ci上
rest(43)//错误,不能把普通引用绑定到字面值上
rest(ctr)//错误,类型不匹配
为了编写能处理不同数量实参的参数,C++11提供了两种方法:
- 如果实参的类型相同,可以传递initializer_list的标准类型库
- 如果实参类型不相同,可以使用可变参数模板
另外,C++还有一种特殊的形参类型(省略符),它可以传递可变数量的实参,...
省略符一般是为了c++访问某些特殊的c代码而使用。
//`...`省略符一般是为了c++访问某些特殊的c代码而使用。
static void varargsC(...) {
}
返回一个值的方式和初始化一个变量的方式完全一样:返回值用于初始化调用点的一个临时变量,该临时变量就是函数调用的结果。
- 一般情况下,返回值将被拷贝到调用点
- 如果函数返回的时引用类型,则不会存在拷贝
- 不要返回局部对象的指针或引用。
要确保返回值的安全,可以问自己:引用所引的是在函数之前已经存在的哪个对象?
C++ 11规定,函数可以返回花括号包围的值的列表。
当函数执行完毕时,将释放分配给局部对象的存储空间。此时,对局部对象的引用就会指向不确定的内存。
//严重错误:返回局部变量的引用
static string &getString() {
string ret;
if (ret.empty()) {
ret = "empty";
}
return ret;
}
C++11 提供了对匿名函数的支持,称为 Lambda 函数(也叫 Lambda 表达式)。Lambda 表达式把函数看作对象。 Lambda 表达式可以像对象一样使用,比如可以将它们赋给变量和作为参数传递,还可以像函数一样对其求值。
Lambda 表达式本质上与函数声明非常类似。Lambda 表达式具体形式是:
[capture](parameters)->return-type{body}
[capture](parameters){body}
没有返回值类型
示例:
[](int x, int y){ return x < y ; }
[]{ ++global_x; }
[](int x, int y) -> int { int z = x + y; return z + x; }
在Lambda表达式内可以访问当前作用域的变量,这是Lambda表达式的闭包(Closure)行为。C++闭包变量传递有传值和传引用的区别。可以通过前面的[]来指定:
[] // 沒有定义任何变量。使用未定义变量会引发错误。
[x, &y] // x以传值方式传入(默认),y以引用方式传入。
[&] // 任何被使用到的外部变量都隐式地以引用方式加以引用。
[=] // 任何被使用到的外部变量都隐式地以传值方式加以引用。
[&, x] // x显式地以传值方式加以引用。其余变量以引用方式加以引用。
[=, &z] // z显式地以引用方式加以引用。其余变量以传值方式加以引用。
对于*[=]或[&]的形式,lambda 表达式可以直接使用 this 指针。但是,对于[]*的形式,如果要使用 this 指针,必须显式传入:
[this]() { this->someFunc(); }();
函数重载省去为函数起名并记住函数名字的麻烦,函数重载简化了程序的实现,使程序更容易理解。 函数名只是为了帮助编译器判断调用的是哪个函数而已。
顶层const不影响传入函数的对象,一个拥有顶层const形参无法和另一个没有顶层const的形参区分开来, 另一方面,如果形参是某种类型的指针或者引用,则通过区分其指向的是常量还是非常量实现函数的重载。
重复声明:
Record lookup(Phone)
Record lookup(const Phone)
Record lookup(Phone*)
Record lookup(Phone* const)//Phone是指针常量,不能通过修改指针的指向,而可以通过参数修改对象的内容,不是函数重载
函数重载
Record lookup(Phone&)
Record lookup(const Phone&)
Record lookup(Phone*)
Record lookup(const Phone*)//const Phone*是常量类型的指针
当传递一个非常量对象或指向非常量对象的指针时,编译器会优先选用非常量版函数。
函数重载确定,即函数匹配是将函数调用与重载函数集合中的一个函数相关联的过程。 通过自动提取函数调用中实际使用的实参与重载集合中各个函数提供的形参做比较, 编译器实现该调用与函数的匹配。匹配结果有三种可能:
- 编译器找到与实参最佳匹配的函数,并生成调用该函数的代码。
- 找不到形参与函数调用的实参匹配的函数,在这种情况下,编译器将给出编译错误信息。
- 存在多个与实参匹配的函数,但没有一个是明显的最佳选择。该调用具有二义性。
当定义一个函数时,可以为参数列表中后边的每一个参数指定默认值。当调用函数时,如果实际参数的值留空,则使用这个默认值。 这是通过在函数定义中使用赋值运算符来为参数赋值的。调用函数时,如果未传递参数的值,则会使用默认值,如果指定了值,则会忽略默认值,使用传递的值。
局部变量不能作为参数的默认参数,初次之外,只要表达式的类型能够转换成形参所需的类型,该表达式就能作为默认实参。
既可以在函数声明也可以在函数定义中指定默认实参。但是,在一个文件中,只能为一个形参指定默认实参一次。
设计带有默认实参的函数,其中部分工作就是排列形参,使最少使用默认实参的形参排在最前,最可能使用默认实参的形参排在最后。 通常,应在函数声明中指定默认实参,并将该声明放在合适的头文件中。
将函数声明为inline则表示该函数为内联函数,内联函数可以避免程序调用函数的开销,这可以理解为使用内存空间来换更短的执行时间。 使用内联函数应该注意:
- 内联函数一般都是1-5行的小函数。
- 在内联函数内不允许使用循环语句和开关语句。
- 内联函数的定义必须出现在内联函数第一次调用之前。
- 类结构中所在的类说明内部定义的函数是内联函数。
- 内联那些包含循环或
switch
语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 switch 语句从不被执行)。 - 内联只是向编译器发送一个请求,编译器可以选择忽略这个请求。 比如虚函数和递归函数就不会被正常内联。
constexpr函数可用于常量表达式,执行初始化任务时,编译器把对constexpr函数的调用替换成结果值,为了能在编译过程中展开,constexpr函数被 隐式的声明为内联函数,声明constexpr函数有如下规则
- 函数的返回值和所有形参类型都必须是字面值类型
- 函数有且只有一条return语句
- assert预处理宏
- assert的行为以依赖一个名为NDEBUG的预处理变量,如果定义了NDEBUG则assert什么都不做,默认没有定义,我们可以在程序中定义这个宏,让assert不再工作。
其他预定义宏:
_ _fun_ _
当前函数名_ _FILE_ _
存放文件名的字符串字面值_ _LINE_ _
存放当前行号_ _TIME_ _
存放文件编译时间_ _DATE_ _
存放文件编译日期
- 函数的类型由它的返回值和参数共同决定,与函数名无关
- 函数指针指向一个函数
- 注意区分函数指针与返回指针的函数
bool (*pf)(const string &,const string &) 函数指针
bool *pf(const string &,const string &) 返回指针的函数
通过函数指针调用函数
pf("hello", "world"); 可以不解引用直接调用
(*pf)("hello", "world"); 解引用后调用