C++系统学习之六:函数
1、函数基础
典型的函数定义包括:返回类型、函数名、由0个或多个形参组成的列表以及函数体。
2、参数传递
形参初始化的机理和变量初始化一样。
有两种方式:引用传递和值传递
2.1 传值参数
当形参是非引用类型时,形参初始化和变量初始化一样,将实参的值拷贝给形参。
指针形参
当执行指针拷贝操作时,拷贝的是指针的值,拷贝之后,两个指针是不同的指针。但通过指针可以修改它所指的对象。
2.2 传引用参数
使用引用避免拷贝
拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。
使用引用形参返回额外信息
2.3 const形参和实参
当用实参初始化const形参时会忽略顶层const。因此,当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。
void fun1(const int i){.......} void fun2(int i){.....}
上述两个函数不能算是重载,两个函数是一样的,程序会报错,fun2重复定义了fun1.
指针或引用形参与const
可以使用非常量初始化一个底层const对象,但是反过来不行。同时一个普通的引用必须用同类型的对象初始化。
尽量使用常量引用
2.4 数组形参
数组有两个重要的特性:
- 不允许拷贝
- 使用数组时会转换成指针
尽管不能以值传递的方式传递数组,但是可以将形参写成类似数组的形式
void print(const int*); void print(const int[]); void print(const int[10]); 以上三种形式的声明等价
NOTE:当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针。只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。
当数组作为函数形参时,因此应该提供一些额外信息来确定数组的确切尺寸,管理数组形参有三种常用的技术:
使用标记指定数组长度
要求数组本身包含一个结束标记。例如C风格字符串以空字符结尾。
使用标准库规范
传递指向数组首元素和尾元素的指针。
void print(const int *beg,const int *end) { while(beg!=end) { cout<<*beg++<<endl; } }
int arr[2]={0,1};
print(begin(arr),end(arr));
显式传递一个表示数组大小的形参
专门定义一个表示数组大小的形参。
void print(const int ia[], size_t size); int j[]={0,1}; print(j, end(j)-begin(j));
数组引用形参
形参可以是数组的引用,此时,引用形参绑定到对应的实参上,也就是绑定到数组上。
void print(int (&arr)[10]) { for(auto elem:arr) { cout<<elem<<endl; } } 形参是数组的引用,维度是类型的一部分
NOTE:arr两端的括号必不可少
f(int &arr[10]); //错误,将arr声明成了引用的数组 f(int (&arr)[10]); //正确,arr是具有10个整数的整型数组的引用
传递多维数组
数组第二维的大小都是数组类型的一部分,不能省略。传递多维数组传递的是指向数组的指针,实际还是指向首元素的指针。(多维数组就是数组的数组,数组的首元素还是数组,所以是指向数组的指针)。
void print(int (*matrix)[10],int size); matrix是一个指针,指向有10个整数的数组
也可以用:
void print(int matrix[][10],int size); matrix和上面一样的意义
2.5 含有可变形参的函数
C++提供两种方法:
实参类型相同,可以传递一个名为initializer_list的标准库类型
initializer_list形参
lnitializer_list和vector一样都是模板类型,不同的是initializer_list对象中的元素永远是常量值,不能改变。
void error_msg(initializer_list<string> ls) { for (auto beg = ls.begin(); beg != ls.end(); ++beg) { cout << *beg << " "; } cout << endl; }
error_msg({ "hello" });
error_msg({ "hello!", "world!!" }); //注意值的传递要放在花括号里
省略符形参
省略符形参是为了便于C++程序访问某些特殊的C代码而设置的。通常,省略符形参不应用于其他目的。省略符形参只能出现在形参列表的最后一个位置。
实参类型不同,使用可变参数模板
3、返回类型和return语句
3.1 无返回值函数
返回类型是void类型的函数
3.2 有返回值函数
值是如何被返回的
返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
不要返回局部对象的引用或指针
返回类类型的函数和调用运算符
auto sz=getstring().size(); //getstring返回的string对象再调用size函数
引用返回左值
调用一个返回引用的函数得到左值,其他返回类型得到右值。
列表初始化返回值
函数可以返回花括号包围的值的列表。
vector<string> process() { return {"ni","hao"}; }
递归
如果一个函数调用了它自身,不管这种调用是直接还是间接的,都称该函数为递归函数。
int factorial(int val) { if(val>1) return factorial(val-1)*val; return 1; } 求1x2x3x4......
3.3 返回数组指针
因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。
最直接的方法是使用类型别名
typedef int arrT[10]; using arrT=int[10];
声明一个返回数组指针的函数
int arr[10]; //arr是一个含有10个整数的数组 int *p1[10]; //p1是一个含有10个整型指针的数组 int (*p2)[10]=&arr; //p2是一个指针,其指向一个有10个整数的数组
如果要定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后,并且函数的形参列表应该先于数组的维度。
int (*func(int a,int b))[10];
此函数返回的是一个指向有10个整数数组的指针。
使用尾置返回类型
任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或引用。
尾置返回类型跟在形参列表后面并以一个->符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto。
auto func(int i)->int (*)[10];
使用decltype
4、函数重载
如果同一作用域内的几个函数名字相同但形参列表不同,称为函数重载。注意必须是形参列表不同,仅仅只是返回类型不同不可以称为重载。
重载和const形参
顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
int f1(int i); int f1(const int i); //不构成重载,重复声明了f1 int f2(int *i); int f2(int *const i); //不构成重载,重复声明了f2
但底层const不同,可以构成重载
int f1(int &i); int f1(const int &i); //重载,新函数 int f2(int *i); int f2(const int *i); //重载,新函数
NOTE:最好只重载那些确实非常相似的操作。
const_cast和重载
const_cast在重载函数的情景中最有用。
const string &shorterString(const string &s1, const string &s2) { return s1.size() <= s2.size() ? s1 : s2; } string &shorterString(string &s1, string &s2) { auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2)); return const_cast<string&>(r); }
4.1 重载与作用域
如果在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。
void func() { } int main() { int func=0; func(); //错误,此时func是int类型的变量,不是函数,隐藏了外层的函数定义 return 0; }
5、特殊用途语言特性
默认实参、内联函数和constexpr函数。
5.1 默认实参
一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
string screen(int i=10, int a=1, stirng s=" ");
使用默认实参调用函数
在调用函数的时候省略该实参就可以。
默认实参声明
在给定的作用域中一个形参只能被赋予一次默认实参。
默认实参初始值
局部变量不能作为默认实参。除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参。
5.2 内联函数和constexpr函数
调用函数一般比求等价表达式的值要慢一些。
内联函数可避免函数调用的开销
在函数的返回类型前面加上关键字inline。
一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。
constexpr函数
constexpr函数是指能用于常量表达式的函数。
定义constexpr函数要遵循:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句。
constexpr int new_sz() { return 42; } constexpr int foo=new_sz(); //foo是一个常量表达式
为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。
NOTE:constexpr函数不一定返回常量表达式。
把内联函数和constexpr函数放在头文件内
和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义。不过,对于某个给定的内联函数或者constexpr函数来说,它的多个定义必须完全一致。因此,内联函数和constexpr函数通常定义在头文件中。
5.3 调试帮助
两项预处理功能:assert和NDEBUG
assert预处理宏
assert宏常用于检查“不能发生”的条件。
assert(expr);
如果expr为假,assert输出信息并终止程序执行,如果为真,assert什么也不做。
NDEBUG预处理变量
assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做,默认情况下没有定义NDEBUG。
可以使用#define语句定义NDEBUG,从而关闭调试状态。
6、函数指针
函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
int func(int a, string s);
该函数的类型是int(int , string).要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可。
int (*p)(int ,string ) //未初始化
NOTE:*p的括号必须加上
使用函数指针
当把函数名作为一个值使用时,该函数自动地转换成指针。
int (*p)(int ,string )=func;
可以使用函数指针直接调用该函数,而不需要解引用该指针。
指向不同函数类型的指针之间不存在相互转换,可以给函数指针赋值nullptr和0,表示指针没指向任何一个函数。
重载函数的指针
如果定义了指向重载函数的指针,编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配。
函数指针形参
和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。可以直接把函数作为实参使用,此时它会自动转换成指针。
返回指向函数的指针
将auto和decltype用于函数指针类型
注意将decltype用于函数名时,返回的是函数类型,而非指针类型,如果要表示函数指针,需要自己加上*。