概述
目录
函数基础
局部对象
函数声明
参数传递
main : 处理命令行选项
特殊用途语言特性
调试帮助
函数匹配
函数指针
函数是一个命名了的代码块,通过调用相应的函数来执行相应的代码,函数可以有0或多个参数,通常会产生一个结果,也可以重载函数,即同一个名字可以对应几个不同的函数。
函数基础
函数包括:
返回类型,函数名字,0或多个形参组成的列表,函数体;
通过调用运算符来执行函数--调用运算符是一对圆括号,作用于一个表达式,表达式是函数或者是指向函数的指针;圆括号里面是一个用逗号隔开的实参列表,我们用实参初始化函数的形参。
编写函数(阶乘)
int fact (int val)
{
int ret = 1;
while(val > 1)
ret *= val--;
return ret;
}
调用函数
int main()
{
int j = fact(5);
cout << "5! = " << j << endl;
return 0;
}
函数的调用完成两项工作:1.用实参初始化函数对应的形参;2.将控制权转移给被调用函数;主函数的调用暂时被中断。
return语句完成两项工作:1.返回return语句中的值;2.将控制权从被调用函数转移至主调函数;
实参和形参
实参和形参一一对应,类型必须相匹配(可以隐式的转换成相应类型也可以);有几个形参,必须提供相同数量的实参,函数调用规定:实参和形参数量一致,所以形参一定会被初始化;
函数的形参列表可以为空,但是不能省略,也可以使用关键字void
void f1();
void f1(void); 等价
形参列表中的形参必须用逗号隔开,类型声明符不能省略
int f2(int val1,val2); 错误
int f2(int val1,int val2); 正确
返回类型
大多数类型都能作为函数的返回类型,特殊的是void类型,表示函数不返回任何值。
局部对象
c++中,名字有作用域,对象由生命周期;
名字的作用域是程序文本的一部分,名字在其中可见;
对象的生命周期是程序执行过程中该对象存在的一段时间;
函数体是一个语句块,块构成一个新的作用域,可以在其中定义新的变量。形参和函数体内定义的变量叫做局部变量,仅在函数的作用域内可见,同时局部变量还会隐藏在外层作用域中同名的其他所有声明中。
在函数体外定义的对象存在与程序的整个执行过程中,程序执行时被创建,程序结束时被销毁,局部变量的生命周期依赖于定义的方式。
自动对象
只存在于块执行期间的对象称为自动对象,当块的执行结束,块中创建的自动对象就变成未定义的了。
形参是一种自动对象,函数开始时为它申请存储空间,函数终止,形参销毁;我们用传递给函数的实参初始化形参对应的自动对象。记住:对于局部变量对应的自动对象来说,若变量本身含有初始值,则用这个初始值进行初始化,否则,默认初始化。
局部静态对象
有时候需要令局部变量的生命周期贯穿函数调用及之后的时间,则将局部变量定义为static类型.局部静态变量在程序执行经过对象定义语句时初始化,在程序终止时销毁。函数执行结束不会对他有影响。
统计函数调用次数
size_t count_calls()
{
static size_t ctr = 0;
return ++ctr;
}
int main()
{
for(size_t i = 0; i != 10; ++i)
cout << count_calls() << endl;
return 0;
}
函数声明
函数可以声明多次,但是只能定义一次。函数的声明和定义类似,区别是函数声明无需函数体,用一个分号代替即可。一般地写上形参;函数声明也称作函数原型;
把函数声明放在头文件中;
分离式编译
把程序分离到几个文件中去,每个文件独立编译。
参数传递
形参初始化机理和变量初始化一样。如果形参是引用类型,则将他绑定到对应实参上,否则,拷贝。
形参是引用类型时,则对应的实参被引用传递或函数被传引用调用,引用形参是他对应的实参的别名;当实参的值是被拷贝给形参时,形参和实参是两个独立的对象,我们说这样的实参是值传递或函数被传值调用。
传值参数
指针形参
指针的行为和非引用类型一样,当执行指针拷贝操作时,拷贝的是指针的值,拷贝之后,两个指针是不同的指针,通过指针可以修改所指对象的值;
int n = 0,i = 42;
int *p = &n,*q = &i;
*p = 42; n改变,p不变
p =q; p指向了i,但是n和i的值都不变
传引用参数
int n =0,i =42;
int &r = i r绑定i
注意:使用引用避免拷贝,string对象比较长,所以避免拷贝他们。
使用引用形参返回额外信息
一个函数只能返回一个值,但是有时候我们需要返回多个值,引用形参为我们提供了途径。
返回s中c第一次出现的位置索引,引用形参occurs负责统计c出现的总次数
string::size_type find_char(const string &s,char c,string::size_type &occurs)
{
auto ret = s.size();
occurs = 0;
for(decltype(ret) i = 0; i != s.size();++i){ // i 的类型和ret类型相同
if(s[i] == c){
if(ret == s.size()){
ret = i; //记录c第一次出现的位置
}
++occurs; //出现的次数通过occurs隐式的返回
}
}
}
注:string::size_type类型,::是指size_type是在string类中定义的,它是一个无符号的值并且可以存放下任何string对象的大小。所有用于存放string类的size函数返回值的变量,都应该是string::size_type类型。
表达式中由size函数就不要使用int了,避免混用int 和unsigned可能带来的问题。
const 形参和实参
用实参初始化形参时会忽略掉顶层const,,当形参有顶层const 时,传给它常量对象或者非常量对象都是可以的。
void f(const int i); f能够读取i, 但是不能向i写值
void f(int i); 错误,重复定义f();
因为:在c++中,我们允许重载函数,但是前提是不同函数的形参列表应该由明显的区别,因为顶层const 被忽略掉了,所有上面的两个函数实际上是一样的!
指针或引用形参与const
我们可以使用非常量初始化一个底层const对象,但是反过来不行,同时一个普通的引用必须用同类型的对象初始化。
int i = 42;
const int *cp = &i; 指向常量的指针(从右向左看),cp不能改变i,不管i是否是常量
const int &r = i; r不能改变i
int *p = cp; 错误,p和cp 类型不同
int &r3 = r; 错误,r3和r类型不同
int &r4 = 42; 错误,不能用字面值初始化一个非常量引用
int i = 42;
const int ci = i;
string::size_type ctr = 0;
reset(&r); true
reset(&ci); 错误
reset(i);
reset(ci); 错误,不能把普通引用绑定到const对象ci上
reset(42); 错误,不能把普通引用绑定到字面值上
reset(ctr); 错误,类型不匹配
void reset(int *ip);
注:如果向调用引用版的reset()函数,则只能使用int类型的对象;
引用版:void reset(int &i);
c++允许用字面值初始化常量引用;
注意:把函数不会改变的形参定义成普通的引用是比较常见的错误,,此外使用引用而非常量引用也会极大的限制函数所能接收的实参类型,就像上面的,我们不能把const对象,字面值或需要类型转换的对象传递给普通的引用形参。
数组形参
数组的两个特殊的性质对我们定义和使用作用在数组上的函数有影响。
- 不允许拷贝数组;(不能使用值传递的方式使用数组参数)
- 使用数组时会将其转换成指针;(传递一个数组时,实际上传递的是指向数组首元素的指针)
不能使用值传递的方式使用数组参数,但是可以把形参写成类似数组的形式
void print(const int*);
void print(const int []);
void print(const int[10]); //这里的维度表示我们期望数组含有多少元素,实际上不一定
以上三个函数等价,都有const int*类型的形参
int i = 0,j[2] = {0,1};
print(&i); //true &i的类型是int*
print(j) //true j转换成int*并且指向j[0]
注意:我们传递给函数的是一个数组,则实参会自动的转换成指向数组首元素的指针,数组的大小对函数的调用没有影响。
以数组作为形参的函数也必须确保使用数组时不会越界;数组时以指针的形式传递给函数的,所以函数并不知道数组的确切尺寸,所以应该提供一些额外的信息,管理指针形参技术常用的有三种:
- 使用标记指定数组长度;
- 使用标准库规范;
- 显示传递一个表示数组大小的形参;
使用标记指定数组长度
要求数组本身含有一个结束标记,典型的就是c字符风格串,最后一个字符后面跟着一个空字符,函数处理时遇到空字符停止。
使用标准库规范
传递指向数组首元素和尾元素的指针,begin(),end()函数
void print(const int *beg, const int *end){
while(beg !=end)
cou << *beg++ << endl;
}
显示传递一个表示数组大小的形参
专门定义一个表示数组大小的形参,
void print(const int ia[], size_t size)
{
for(size_t i = 0; i != size;++i)
cout << ia[i] << endl;
}
通过形参size确定需要输出多少个元素,调用print函数时必须传入表示这个数组大小的值
数组形参和const
当函数不需要对数组元素执行操作的时候,数组形参应该是指向const的指针,只有当函数需要改变元素值的时候,才把形参定义成指向非常量的指针。
数组引用形参
void print(int (&arr)[10]){
for(auto elem : arr)
cout << elem << endl;
}
注意:括号不能缺少
传递多维数组
c++语言中的多维数组实际上时数组的数组
void print(int (*matrix)[10], int rowSize) 等价于
void print(int matrix[][10], int rowSize)
matrix 定义为含有10个整数的数组的指针
第二个编译器会一如既往的忽略掉第一个维度
main : 处理命令行选项
可以向main 函数传递实参
int main(int argc, char *argv[]){}
argc -- 表示数组中字符串的数量
argv -- 是一个数组,它的元素时指向c风格字符串的指针
等价于
int main(int argc, char **argv){}
argv 指向*char
注意:可选实参从argv[1]开始,argv[0]保存程序的名字
含有可变形参的函数
有时我们无法提前预知应该向函数传递几个实参。为了能够处理不同数量实参的函数,c++又两种方法:
实参类型相同,传递一个initializer_list的标准库类型;
实参类型不同,编写一种特殊函数,就是可变参数模板
此外,还有一种特殊的形参类型(省略符),可以用来传递可变数量的实参。
initializer_list<T> lst; 默认初始化;T类型元素的空列表
initializer_list<T> lst{a,b,c,...}; lst的数量和初始值一样多,lst的元素时对用初始值的副本,例表中的元素时const
lst2(lst) 拷贝一个initializer_list对象,但是不会拷贝列表中的元素,原始列表和副本共享元素
lst2 = lst 赋值一个initializer_list对象,但是不会拷贝列表中的元素,原始列表和副本共享元素
lst.size() 列表中元素的数量
lst.begin() 返回指向lst中首元素的指针
lst.end() 返回指向lst中尾元素下一位置的指针
initializer_list和vector一样也是一种模板类型。
initializer_list<string> ls;
initializer_list和vector不一样的是里面的元素永远是常量值,无法改变;
eg
void error_msg(initializer_list<string> il){
for(auto beg = i1.begin(); beg != i1.end(); ++beg)
cout << *beg << " ";
cout << endl;
}
//expected, actual是string对象
if(expected != actual)
error_msg({"functionX", expected, actual});
else
error_msg({"functionX","okay"});
注意:若想用initializer_list形参中传递一个值的序列,必须把序列放在一对花括号内;
第一次调用传入3个值,第二次调用传入2个值;
含有initializer_list形参也可以拥有其他形参,如ErrCode类表示不同类型的错误
void error_msg(ErrCode e, initializer_list<string> il){
cout << e.msg() << " ";
for(const auto &elem : i1)
cout << *elem << " ";
cout << endl;
省略符形参
是为了方便C++程序访问特殊的c代码而设计的,大多数类型的对象在传递给省略符形参是都无法正确拷贝;
所以,还是别用了!
返回类型和return语句
return 语句是中止当前正在指向的函数并将控制权返回到调用函数的地方,return有两种形式:
return;
return expression;
没有返回值的return语句只能用在返回类型是void的函数中,返回void的函数不要求非得有return语句,因为这类函数会隐式地执行return;若void函数想在中间退出,则可以使用return语句,此时类似于break语句;若void函数使用return expression语句,则expression必须是一个能返回void的函数;
有返回值函数
若函数的返回类型不是void,则函数的每一条return 语句必须返回一个值,返回值的类型必须于函数返回值的类型相同,或能隐式地的转换成函数的返回类型。
值是如何被返回的
返回一个值的方式和初始化一个变量或者形参的方式完全一样,返回值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
不要返回局部对象的引用或指针
函数完成后,他所占用的内存空间随之也会被释放掉,所以函数终止意味着局部变量的引用将指向不在有效的内存区域。
eg
const string &manip()
{
string ret;
if(!ret.empty())
return ret; 错误--返回局部对象的引用
else
return "empty"; 错误--"empty"是一个局部临时量
}
注意:empty 是字符串字面值会转成一个局部临时string对象,
返回局部对象的引用是错误的,返回局部的对象的指针也是错误的。
返回类类型的函数和调用运算符
调用运算符符合优先级和结合律。调用运算符的优先级与点运算符和箭头运算符相同,也符合左结合律。
列表初始化返回值
c++规定:函数可以返回花括号包围的值的列表。此处的列表也用来表示函数返回的临时量进行初始化。若列表为空,临时量执行值初始化,否则,返回的值由函数返回类型决定。
eg
vector<string> process()
{
if(expected.empty())
return {}; //返回空的vector对象
else if (expected == autual)
return {"functionX", "okay"}; //返回列表初始化的vector对象
else
return {"functionX",expected,actual};
}
注意:第一个return 语句返回空列表,
如果函数返回的是类类型,由类本身定义初始值如何使用。
main的返回值
允许main函数在没有return 的情况下自行结束。编译器会隐式的在main函数后面加入return语句。
main函数返回值可以看作是状态指示器,返回0表示指向成功,返回非0表示失败,具体返回数值由机器决定。若想使返回值与机器无关,则
头文件定义了两个预处理变量。
#include<cstdlib>
int main(){
if(some_failure)
return EXIT_FAILUER;
else
return EXIT_SUCESS;
}
递归
若有函数调用了它自身,不管是直接调用还是间接调用,都称该函数为递归函数。
递归函数实现求阶乘
int factorial(int val){
if(val > 1)
return factorial(val-1)*val;
return 1;
}
注:递归函数中一定有某一条路径是不包含递归调用的,因为函数不能一直运行下去。
main函数不能调用他自己。
返回数组指针
函数不能被拷贝,所以函数不能返回数组。函数可以返回数组的指针或引用。
1.使用类型别名返回数组指针或引用。
typedef int arrT[10]; arrT是一个类型别名,表示含有10个整数的数组
using arrT = int[10]; 等价于上一个
arrT* func(int i); func返回一个指向含有10个整数数组的指针
2.声明一个返回数组指针的函数
eg
int (*func(int i)[10]);
func(int i)--调用func函数需要一个int类型的实参
(*func(int i))--对函数调用的结果执行解引用
(*func(int i)[10])--对函数调用的结果执行解引用得到的是一个大小是10的数组
int (*func(int i)[10])--数组中的元素是int类型。
3.使用尾置返回类型
任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效。比如:返回类型是数组的指针或是数组的引用。
eg
auto func(int i) -> int (*)[10]; // func接受一个int类型实参,返回一个指针,指向含有10个整数的数组。
4.decltype
若知道函数返回指针将指向哪个数组,使用这个办法。
eg
int odd[] = {1,3};
int even[] = {0,2};
decltype(odd) *arrPtr(int i){
return (i % 2) ? &odd : &even;
}
注:arrPtr 使用关键字decltype 表示它的返回类型是指针。并且该指针所指的对象和odd的类型一致。decltype并不负责把数组类型转换成对应的指针,所以decltype的结果是个数组,想要表示arrPtr返回指针必须在函数声明时加 “*” 符号。
函数重载
若同一作用域内几个函数名字相同,但是形参列表不同,我们称之为重载函数。
main函数不能重载;
重载函数应该在形参数量或形参类型上有所不同,不允许两个函数除了返回类型之外其他方面都相同。
Record lookup(const Account &acc);
Record lookup(const Account &);
上面两个函数时相同的,第二个时省略了形参的名字。
typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&);
上面两个函数时相同的,Phone Telno 类型时一样的,就相当于Phone是Telno的别名。
重载和const形参
顶层const 不影响传入函数的对象,一个拥有顶层const 的形参无法和一个没有顶层const 的形参区分开来。
record looup(phone);
record looup(const phone); --重复声明
record looup(phone *);
record looup(phone* const);
每一组的第一个和第二个等价;
若形参是某种类型的指针或者引用,则通过区分器指向的常量对象还是非常量对象可以实现函数的重载,此时的const 是底层的。
record looup(account&);
record looup(const account&); --新函数,作用与常量引用
record looup(account*); --新函数,作用指向account 的指针
record looup(const account*); --新函数,作用指向常量的指针
注:原则上只是重载那些确实非常相似的操作。
const_cast 和重载
暂时略
调用重载的函数
定义了一组重载函数之后,我们需要以合理的实参调用他们。函数匹配时指一个过程,在这个过程中我们把函数嗲用与一组重载函数中的一个关联起来,函数匹配也叫重载确定。
匹配结果:
- 编译器中找到最佳匹配的函数,并生成调用该函数的代码。
- 找不到匹配函数,编译器发出无匹配;
- 有多个函数可以匹配,但是每一个都不是最佳的选择,报错,成为二义性调用。
重载与作用域
c++中,名字查找发生在类型检查之前。
特殊用途语言特性
默认实参;内联函数;constexpr函数;
默认实参
默写函数有这样一种形参,在函数的很多次调用中都被赋予一个相同的值,我们把这个反复出现的值叫做默认实参。调用含有默认实参的函数时,可以包含该实参,也可以省略该实参,
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char background = " ");
我们为每一个形参都提供了一个默认的实参,我们可以为一个或者多个形参定义默认值;但注意:一旦某个形参被赋予了默认值,它后面所有的形参都必须有默认值。
若我们想用默认实参调用函数,则需要调用函数的时候省略该实参就行了。
string screen(); --等价 string screen(24, 80, " ");
string screen(66,256); --等价 string screen(66, 256, " ");
string screen(, , "?"); --false 只能省略尾部的实参
string screen("?") -- string screen("?", 80, " ");
注:合理设置形参顺序,尽量让不用默认值的形参出现在前面,让经常使用默认值的形参出现在后面;
默认实参声明
函数声明一次,但是多次声明也时合法的。
string screen(sz, sz, char = " "); --前两个形参没有默认值;
string screen(sz, sz, char = "*"); --不能修改一个已经存在的默认值,false 重复声明;
string screen(sz, sz, char); --true 添加默认实参
注:通常在函数声明中指定默认实参,并且将该声明放在合适的头文件中。
内联函数
调用函数一般比求等价表达式的值要慢一些。函数调用包括:调用前保存寄存器,返回时恢复,可能需要拷贝实参,程序转向新的位置继续执行。
内联函数可以避免函数调用时的开销。
将函数指定为内联函数,就是将它在每个调用点上”内联的“展开。
cout << shortString(s1,s2) << endl;
编译时:
cout << (s1.size() < s2.size() ? s1:s2) << endl;
在函数返回类型的前面使用关键字 inline 就可以
inline const string &;
shortString(const string &s1,const string &s2){
return s1.size() < s2.size() ? s1:s2
}
内联说明只是想编译器发出一个请求,但是编译器可以选择忽略。
内敛机制用于游湖规模较小,流程直接,频繁调用的函数,很多编译器不支持内联递归函数。
constexpr函数
constexpr函数只能是用于常量表达式的函数,
使用规则:
- 函数的返回类型及所有形参类型都得是字面值类型;
- 函数体中有且只有一条return 语句。
constexpr函数 不一定返回常量表达式;
注:constexpr函数 和内联函数通常定义在头文件中。
调试帮助
assert 预处理宏
assert 预处理宏其实时一个预处理变量,定义在cassert 头文件中。
用法:assert(expression)
对expr求值,若表达式为假(0),assert输出信息并且中止函数的执行;若表达式为真,则assert什么都不做。
assert 预处理宏通常用于检查不能发生的条件。
NDEBUG预处理变量
assert 预处理宏依赖于一个NDEBUG预处理变量的状态,若定义NDEBUG预处理变量,则assert 什么都不做,默认是没有定义NDEBUG预处理变量。
可以使用一个#define 语句定义NDEBUG,从而关闭调试状态。
除了assert 预处理宏之外,也可以使用NDEBUG编写自己的条件调试代码。若未定义NDEBUG,将执行#ifndef 和 #endif 之间的代码。
void print (const int ia[],size_t size){
#ifndef NDEBUG
//_ _func_ _ 是编译器的一个局部静态变量,用于存放函数的名字
cerr << _ _func_ _ << ": array size is " << size << endl;
#endif
.....
}
_ _func_ _ 是当前调式的函数的名字,编译器为每一个函数都定义了 _ _func_ _ ,是 const char 的一个静态数组,用于存放函数的名字。
_ _FILE_ _ 存放函数名字的字符串字面值
_ _LINE_ _ 存放当前行号的整型字面值
_ _TIME_ _ 存放文件编译时间的字符串字面值
_ _DATA_ _ 存放文件编译日期的字符串字面值
cerr << _ _func_ _ << ": array size is " << size <<
"compiled on " << _ _DATA_ _ <<endl;
函数匹配
当几个重载函数的形参数量相等且某些形参的类型可以由其他类型转换而来的时候,就不容易确定调用的是哪个重载函数。
确定候选函数和可行函数
- 函数匹配第一步是 选定本次调用对应的重载函数集,集合中的函数被称为候选函数,候选函数两个特征:于被调函数同名;声明在调用点可见。
- 考察本次调用提供的实参。可行函数有两个特征:形参数量和本次提供的实参数量相同;每个实参的类型于对应形参的类型相同,或者可以转化成形参类型。
- 选择最佳匹配函数;
调用重载函数是应该尽量避免强制类类型转换,如果出现这种情况,说明我们设计的形参不合理。
函数指针
函数指针指向的是函数而非对象,函数指针指向某种特殊类型。函数类型由它的返回类型和形参类型共同决定,与函数名无关。
bool (*pf)(const string &,const string &);
--若不写括号
bool *pf(const string &,const string &); -- pf 是返回值为bool指针的函数
使用函数指针
当我们把函数名作为一个值使用时,该函数自动转换为指针。
pf = lenthCompare;
pf = & lenthCompare;
-- 等价的表达,取地址符可选的。
使用指向函数的指针调用该函数;无需提前解引用指针。
bool b1 = pf("hello","goodbye");
bool b2 = (*pf)("hello","goodbye");
bool b3 = lenthCompare("hello","goodbye");
-- 三个等价的调用。
注:指向不同函数类型的指针间不存在转换规则。我们可以为函数指针赋bullptr或者值为0的整型常量表达式,表示指针式没有指向任何一个函数。
重载函数的指针;
函数指针形参
不能定义函数类型的形参,但是形参可以是指向函数的指针。
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 &))
-- 和第一个等价
直接使用函数指针类型是冗长繁琐的,所以使用类型别名和decltype 来简化。
typedef bool Func(const string&,const string&);
typedef decltype(lengthCompare) Func2; --等价 Func 和Func2 是函数雷西那个
typedef bool Func(*FuncP)(const string&,const string&);
typedef decltype(lengthCompare) *FuncP2; --等价 FuncP和FuncP2 是指向函数的指针;
void useBigger(const string &s1,const string &s2,Func);
void useBigger(const string &s1,const string &s2,Func2);-- 等价声明
返回指向函数的指针
虽然不能返回一个函数,但是能返回指向函数类型的指针。我们必须把返回类型写成指针的形式。编译器不会自动的将函数返回类型当成对应的指针类型处理。
若想声明一个返回函数指针的函数,最简单的是使用类型别名。
using F = int(int*,int); F 是函数类型
using PF = int(*)(int*,int); PF是指针类型
PF f1(int); //true PF 是指向函数的指针,f1返回指向函数的指针
F f1(int); //false F 是函数类型
F *f1(int); //true 显式de指定返回类型是指向函数的指针
致谢:
【c++primer第五版】
最后
以上就是野性小笼包为你收集整理的【c++primer第五版】第六章函数-函数基础、参数传递、返回类型、函数重载、函数指针的全部内容,希望文章能够帮你解决【c++primer第五版】第六章函数-函数基础、参数传递、返回类型、函数重载、函数指针所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复