C++基础总结:函数
文章目录
一、函数基础
- 函数调用过程:
- 将控制权转移给调用函数,函数隐式地定义并初始化它的形参;
- 当执行到return语句或者执行完函数内全部语句后返回结果。
- 实参和形参:实参是形参的初始值
- 函数的返回类型
- 大多数类型都可以作为函数的返回类型。void也可以,它表示不返回任何值。
- 函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
- 局部静态对象
- 局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序最终才被销毁。
- 局部静态对象只能被初始化一次,不会被多次初始化!如果没有显示初始值,它将执行值默认初始化,内置类型的局部静态变量初始化为0
- 示例:
输出结果:int count_calls() { static int count = 0; return ++count; } int main() { for(int i = 0; i < 6; ++i) std::cout <<count_calls() << ", "; }
1, 2, 3, 4, 5,
二、参数传递
- 形参初始化的原理和变量初始化一样。
- 实参可以通过引用传递或者值传递给形参初始化。
1. 传值参数
- 通过值传递的形参是实参的拷贝,两者独立,不会相互影响
- 指针形参
- 当执行指针拷贝操作时,拷贝的是指针的值。拷贝后,两个指针是不同的指针。所以,我们可以通过指针形参访问或修改实参指向的值,如果修改形参的指向,实参指向并不会因此改变。
2. 传引用参数
- 拷贝大的类类型对象或者容器对象时效率低,甚至有的类类型根本不支持拷贝操作,此时我们可以使用引用形参访问该类型的对象。
- 如果函数无须改变引用形参的值,最好将其声明为常量引用。
- 内置类型初始化一般会比引用效率高,所以内置类型无须使用常量引用,可以使用引用或传值参数。
- 使用引用形参是方便返回多个结果的有效途径。
3. const形参和实参
- 某变量或字面值是否可以用作参数传递,可以考虑其是否可以用于变量初始化的值。
- 我们要尽可能的使用常量引用代替普通引用。因为普通引用会给使用不需要修改参数函数的使用者一种误导,并且普通引用会限制可传入参数类型。我们不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参。
4. 数组形参
- 数组有连个特性,分别是:不允许拷贝数组以及使用数组时(通常)会将其转换成指针。
- 因为不能拷贝数组,所以无法使用值传递的方式使用数组参数。
- 因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
- 尽管不能以值传递的方式传递数组,但我们可以把形参写成类似数组的形式:
尽管表现形式不同,但三个函数等价:每个函数的唯一形参都是const int*类型。//尽管形式不同,但是下面三个函数是等价的 void print(const int*); void print(const int[]); //函数的用途是作用于一个数组 void print(const int[10]); //这里的维度表示我们期望数组含有多少元素,实际并不一定
int i =0, j[2] = {0, 1}; print(&i); //正确:&i的类型是int* print(j); //正确:j转换成int*并指向j[0]
- 为防止数组指针越界,管理指针形参有三种常用技术:
- 使用标记指定数组长度
这种方法是在数组结尾有一个结束标志,典型就是C风格字符数组。字符数组最后跟着一个空字符,函数在处理C风格字符串时遇到空字符停止:
这种方式适用于有明显结束标记的数组,但对于int这种数组就不太有效了。void print(const char* cp) { if(cp) //若cp不是空指针 while(*cp) //若指针所指字符不是空字符 cout << *cp++; //输出当前字符并指向下一个字符 }
- 使用标准库规范
第二种方法来自标准库技术,是传入数组首元素和尾后元素的指针。
可以通过调用标准库的begin和end函数,代码安全地获取数组首指针和尾后指针void print(const int* beg, const int* end) { //输出beg和end之间(不含end)的所有元素 while(beg != end) cout << *beg++ << endl; //输出当前元素并将指针向前移动一个位置 }
int j[2] = {0, 1}; print(std::begin(j), std::end(j));
- 显式传递一个表示数组大小的形参
第三种方法是专门定义一个表示数组大小的形参,在C程序和旧的C++程序中常常使用这个方法。//const int ia[]等价于const int* ia //size表示数组的大小,将它显式地传给函数用于控制对ia元素的访问 void print(const int ia[], size_t size) { for(size_t i = 0; i != size; ++i) { cout << ia[i] << endl; } } int j[] = {0, 1}; print(j, end(j)-begin(j));
- 使用标记指定数组长度
- 数组引用形参
C++语言允许将变量定义成数组的引用,所以,形参也可以是数组的引用。引用形参绑定到对应的实参上,也就是绑定到数组上:void print(int (&arr)[10]) //&arr两端的括号必不可少,后边的10与传入数组指针形参不同,这个具有实际意义,即数组长度必须为10 { for(auto elem : arr) cout << elem << endl; }
- 传递多维数组
- 和所有数组一样,当将多为数组传递给函数时,真正传递的是指向数组首元素的指针。
//matrix指向数组的首元素,该数组的元素是由10个整数构成的数组 void print(int (*matrix)[10], int rowSize){/* ... */} 等价于 void print(int matrix[][10], int rowSize){/* ... */} matrix看起来是一个二维数组,实际上形参是指向含有10个整数的数组的指针 int *matrix[10]; //10个指针构成的数组 int (*matrix)[10]; //指向含有10个整数的数组的指针
5. main:处理命令行选项
- 通常,我们使用空参主函数,但我们也可以给主函数传入参数
int main(int argc, char* argv[]){...}
第一个形参是数组长度,第二个形参是C风格字符串数组,argv指向char*,所以也可以写成:
int main(int argc, char** argv){...}
当我们使用argv中的参数时,一定要记得可选的实参从argv[1]开始,argv[0]保存程序的名字,而非用户输入
6. 含有可变形参的函数
C++11标准提供了两种主要方法:如果所有实参类型相同,可以传递一个名为initializer_list的标准库类型;如果实参的类型不同,我们可以使用可变参数模板,此方法在后续的模板章节中总结。除此之外,C++还提供了一种特殊的形参类型,即省略符形参。
initializer_list形参
-
initializer_list是一种标准库类型,用于表示某种特定类型的值的数组。initializer_list类型定义在同名的头文件中。
-
和vector一样,initializer_list也是一种模板类型,定义对象时,必须说明列表中所含元素的类型。
-
和vector不同的是,initializer_list对象中的元素永远是常量值,无法改变initializer_list对象中元素的值
-
示例:
void error_msg(initializer_list<string> il) { for(auto beg = il.begin(); beg != il.end(); ++beg) cout << *beg << " "; } error_msg({"functionx", "okay"}); //通过一对花括号传入序列
-
initializer_list提供的操作
操作 说明 initializer_list lst; 默认初始化;T类型元素的空列表 initializer_list lst{a, b, c…}; lst的元素数量和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const lst2(lst) 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素 lst2 = lst 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素 lst.size() 类表中的元素数量 lst.begin() 返回指向lst中首元素的指针 lst.end() 返回指向lst中尾后元素的指针
省略符形参
省略符形参是为C++便于访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。通常,省略符形参不应用于其他目的。
省略符形参应该仅仅用于C和C++通用的类型。特别注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参只能出现在形参列表的最后一个位置
void foo(parm_list, ...);
void foo(...);
我们可以添加cstdarg文件,使用其中定义的函数方法访问省略符形参
#include <cstdarg>
void VarArgFunc(int i, ...)
{
va_list pArg = nullptr; //创建参数指针
va_start(pArg, i) //初始化参数指针
int a = va_arg(pArg, int); //获取第一个参数
char b = va_arg(pArg, char); //获取第二个参数
va_end(pArg); //清空参数指针
}
三、返回类型和return语句
1. 有返回值函数
- 返回一个值的方式和初始化一个变量和形参的方式完全一样:返回的值用于初始化调用点的一个临时变量,该临时量就是函数调用的结果。
- 不要返回局部对象的引用或指针!所以,我们返回的引用或指针需要是函数调用之前就已经存在的变量。
- 调用一个返回引用的函数会得到左值,其它返回类型得到右值。
- C++11新标准规定,函数可以返回花括号包围的值的列表。
- main函数可以没有显式返回值,main函数的返回值可以看作是状态指示器。返回0表示执行成功,返回其他值表示执行失败。为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量,我们可以使用这两个变量表示成功与失败:EXIT_FAILURE和EXIT_SUCCESS
- 如果一个函数调用了自身,无论是直接还是间接,都称该函数为递归函数。如果函数出现不断调用自身直到程序栈空间耗尽为止,我们称其为循环递归。另外,main函数不能调用自己。
2. 返回数组指针
- 因为数组不能拷贝,所以函数不能返回数组。但是,函数可以返回数组的指针或引用。
使用类型别名
typedef int arrT[10]; //arrT是一个类型别名,表示的类型是含有10个整数的数组
using arrT = int[10]; //与上条等价
arrT* func(int i); //func返回一个指向含有10个整数的数组的指针
声明一个返回数组指针的函数
int arr[10]; //arr是一个含有10个整数的数组
int* p1[10]; //p1是一个含有10个指针的数组
int (*p2)[10] = &arr; //p2是一个指针,它指向含有10个整数的数组
下面的函数声明:
int (*func(int i))[10];
- func(int i)表示调用func函数时需要一个int类型的实参
- (*func(int i))意味着我们可以对函数调用的结果执行解引用操作
- (*func(int i))[10]表示解引用func的调用将得到一个大小是10的数组
- int (*func(int i))[10]表示数组中的元素是int类型
使用尾置返回类型
尾置返回类型是C++11的新标准,常用语比较复杂的返回类型,比如数组的指针或数组的引用。
下例为函数的声明:
auto func(int i) -> int(*)[10]; //返回类型放到形参之后,原来放置返回类型的地方使用auto
四、函数重载
同一作用域内,函数名相同,形参列表不同的函数称之为重载函数。
main函数不能重载。
重载和const形参
拥有顶层const的形参无法与另一个没有顶层const的形参区分:
Record lookup(Phone);
Record lookup(const Phone); //重复声明了Record lookup(Phone)
Record lookup(Phone*);
Record lookup(Phone* const); //重复申明了Record lookup(Phone*)
底层const可以重载:
Record lookup(Account&); //函数作用于Account引用
Record lookup(const Account&); //新函数,作用于常量引用
Record lookup(Account*); //新函数,作用于指向Account的指针
Record lookup(const Account*); //新函数,作用于指向常量的指针
const_cast和重载
string& shorterString(string& s1, string& s2)
{
auto& r = shorterString(const_cast<const string&>(s1),
const_cast<const string&>(s2));
return const_cast<string&>(r);
}
五、特殊用途语言特性
1. 默认实参
- 局部变量不能作为默认实参
2. 内联函数和constexpr函数
调用函数一般比求等价表达式的值慢一些。大多数机器上,函数调用过程为:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。
内联函数可避免函数调用的开销
在函数声明前加inline,可以将其声明成内联函数。内联函数是向编译器发送请求,将其在编译时直接展开到函数调用处,当然,编译器也可以忽略这个请求。
很多编译器都不支持内联递归调用。
constexpr函数
constexpr函数是指能用于常量表达式的函数。定义constexpr函数需要遵循约定:
- 函数的返回类型及所有形参的类型都得是字面值类型
- 函数体中必须有且只有一条return语句:
constexpr int new_sz() {return 42;}
constexpr int foo = new_sz(); //正确:foo是一个常量表达式
如果arg是常量表达式,则scale(arg)也是常量表达式:
constexpr size_t scale(size_t cnt){return new_sz() * cnt;}
int arr[scale(2)]; //正确:scale(2)是常量表达式
int i = 2;
int a2[scale(i)]; //错误:scale(i)不是常量表达式
constexpr函数不一定返回常量表达式。
内联函数和constexpr函数通常定义在头文件中。
3. 调试帮助
程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用,当应用程序开发完成后,要先屏蔽调试代码。这种方法用到两项预处理功能:assert和NDEBUG。
assert预处理宏
- assert是一种预处理宏,预处理宏是一个预处理变量,行为类似于内联函数。
- assert宏定义在cassert头文件中。
- 可以像下面代码一样,检查表达式输出结果,如果表达式为假(0),assert输出信息并中止程序执行;如果为真(非0),程序继续执行:assert(expr);
NDEBUG预处理变量
assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下,没有定义NDEBUG,此时assert将检查运行时检查。
六、函数指针
函数指针指向的是函数,而非对象。函数的类型由他的返回类型和形参类型共同决定,与函数名无关。要想声明一个可以指向函数的指针,只需要用指针替代函数名即可:
bool (*pf)(const string&, const string&); //未初始化
*pf两端的括号必不可少。如果不写括号,则pf是一个返回值为bool*的函数
使用函数指针
当函数名作为值使用时,该函数自动地转换成指针。
pf = lengthCompare; //pf指向名为lengthCompare的函数
pf = &lengthCompare; //等价的赋值语句:取地址符可选
我们还可以直接使用指向函数的指针调用该函数,无须提前解引用指针:
bool b1 = pf("hello", "goodbye"); //调用lengthCompare函数
bool b2 = (*pf)("hello", "goodbye"); //一个等价的调用
bool b3 = lengthCompare("hello", "goodbye");//另一个等价的调用
函数指针形参
和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际却是指针:
void useBigger(const string& s1, const string& s2, bool pf(const string&, const string&));
等价于
void useBigger(const string& s1, const string& s2, bool(*pf)(const string&, const string&));
也可以直接把函数作为实参使用,此时他会自动转换成指针
useBigger(s1, s2, lengthCompare);
类型别名和decltype可以简化函数指针的代码:
//Func和Func2是函数类型
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2; //等价的类型
//FuncP和FuncP2是指向函数的指针
typedef bool(*FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2; //等价的类型
可以使用以下形式声明useBigger:
void useBigger(const string&, const string&, Func);
void useBigger(const string&, const string&, Func2);
返回指向函数的指针
和数组类似,虽然不能返回一个函数,但是可以返回指向函数类型的指针。
要想声明一个返回函数指针的函数,最简单的方法是使用类型别名:
using F = int(int*, int); //F是函数类型,不是指针
using PF = int(*)(int*, int); //PF是指针类型
我们必须显式地将返回类型指定为指针:
PF f1(int); //正确:PF是指向函数的指针,f1返回指向函数的指针
F f1(int); //错误:F是函数类型,f1不能返回一个函数
F* f1(int); //正确:显式地指定返回类型时指向函数的指针
我们也能直接声明f1:
int (*f1(int))(int*, int);
也可以使用尾置返回类型的方式:
auto f1(int) -> int (*)(int*, int);
将auto和decltype用于函数指针类型
string::size_type sumLength(const string&, const string&);
string::size_type largerLength(const string&, const string&);
//getFcn函数返回指向sumLength或者largerLength的指针
decltype(sumLength)* getFcn(const string&);
当我们将decltype作用于某个函数时,它返回函数类型而非指针类型。因此,我们显式地加上*以表明我们需要返回指针,而非函数本身。