【c++ Prime 学习笔记】第6章 函数
6.1 函数基础
-
函数定义包括:
返回类型
、函数名字
、由0个或多个形参
组成的列表
以及函数体
-
通过
调用运算符()
来执行函数,它作用于一个表达式,该表达式是函数或函数指针。圆括号内是一个逗号隔开的实参
列表,调用时用实参初始化形参。 -
调用函数完成两项工作:
- 隐式定义形参并用实参初始化函数对应的形参
- 将控制权从
主调函数
转移给被调函数
-
return
语句完成两项工作:- 返回return语句中的值(如果有),将其作为调用表达式的结果
- 将控制权从
被调函数
转移回主调函数
-
实参是形参的初始值
,它们一一对应,实参与形参类型需匹配或能转换。但调用运算符并未规定实参的求值顺序。 -
为与C兼容,可用
void关键字
表示函数没有形参。 -
每个形参是含有一个声名符的声明。但即使两个形参类型一样,也必须都写出类型,不可省略。
-
函数最外层作用域的局部变量不能与形参同名。
-
形参名是可选的,不会被用到的形参不需命名,但需提供实参。
-
函数返回类型不可以是数组或函数类型,但可以是指向数组或函数的指针。
6.6.1 局部对象
-
名字的
作用域
是程序文本的一部分,名字在其中可见。 -
对象的
生命周期
是程序执行过程中该对象存在的一段时间。 -
形参和函数体内定义的局部变量统称为
局部变量
,它们仅在函数体内可见。 -
所有函数之外定义的对象存在于函数的整个执行过程中,此类对象在程序启动时创建,程序结束才被销毁。
-
自动对象
:只存在于块执行期间的对象 -
形参是一种自动对象,函数开始时创建,函数终止时销毁
-
若局部变量对应的自动对象定义时有初始值,则用初始值初始化,若无初始值则
默认初始化
。即,内置类型的未初始化局部变量产生未定义的值
。 -
局部静态对象
:将局部变量定义成static类型
,在程序执行第一次经过定义语句时初始化,直到程序终止才被销毁。 -
若局部静态对象没有显式初始值,将执行
值初始化
。即,内置类型的局部静态变量被初始化为0
size_t count_calls(){
static size_t ctr=0; //调用结束后不会销毁
return ++ctr; //每次调用时+1,该变量表示该函数被调用的次数
}
int main(){
for(size_t i=0; i!=10; ++i)
cout<<count_calls()<<endl;
return 0;
}
6.1.2 函数的声明
-
函数的名字必须在使用前声明。函数只能定义一次,但可声明多次。如果一个函数不会被使用,则可只声明不定义。
-
函数声明又叫
函数原型
,函数声明的三要素(返回类型、函数名、形参类型)描述了函数的接口。 -
函数声明不需函数体,用分号代替函数体。由于不使用形参,故声明中形参可不命名。
-
建议函数在
头文件
中声明,在源文件
中定义。和变量一样。
6.1.3 分离式编译
-
C++支持分离式编译,即允许将程序分割到几个文件中,每个文件单独编译。
-
每个文件单独编译时产生后缀名为
.obj
(windows)或.o
(UNIX)的文件,含义是该文件中包含对象
代码 -
单独编译后
链接器
将对象文件
链接起来成为可执行文件
$ CC -c factMain.cc #产生factMain.o
$ CC -c fact.cc #产生fact.o
$ CC factMain.o fact.o -o main #将两个对象文件链接为可执行文件
6.2 参数传递
-
每次调用函数时都会重新创建形参并用实参初始化
-
如果形参是
引用
类型,将被绑定
到对应实参。否则,将实参的值拷贝
给形参。 -
两种传参:
引用传递
:形参是引用类型,引用形参绑定到对应实参的对象,是实参的别名。说这样的实参被引用传递
,或者说函数被传引用调用
。值传递
:实参的值被拷贝给形参,形参和实参是两个独立的对象。说这样的实参被值传递
,或者说函数被传值调用
6.2.1 传值参数
- 初始化一个非引用类型的变量时,初始值被拷贝给变量。此时对变量的改变不会影响初始值。
- 函数对形参做的操作不影响实参
指针参数
- 当指针被拷贝后,两个指针是不同的指针,但指向相同的对象。通过任一个指针修改时,对象都被改变了。
- C程序员常用指针类型形参修改函数外的对象,但在C++中,建议用引用类型形参代替指针
传引用参数
-
对于引用的操作实际上是作用在引用绑定的对象上
-
通过引用传参,允许函数改变实参的值
-
使用引用传参的情形:
-
拷贝
大的类类型对象
或容器对象
效率较低,最好用引用形参访问 -
有的类型不支持拷贝
(如IO类),只能用引用传参访问 -
引用传参为函数
一次返回多个结果
提供了途径,只需将多个对象作为引用传进函数即可修改。 -
无需修改引用形参时,最好用
常量引用
//& occurs 为函数返回的额外信息 string::size_type find_char(const string &s, char c, string::size_type &occurs)
-
6.2.3 const 形参和实参
当形参是const时,要注意关于顶层const有关的讨论:实参初始化形参时会忽略顶层const。即,当形参有顶层const时,传const对象或非const对象都可以
//下面两个函数可接受相同参数,故是相同函数,不可重载
void fcn(const int i){}
void fcn(int i){} //错,重复定义
-
可以用非常量初始化底层const,反之不可
-
C++允许用字面值初始化常量引用
-
使用非常量引用会极大限制函数能接受的实参类型:不能把const对象、字面值、需要类型转换的对象传递给非常量引用形参
-
下图函数声明
void reset(int &i);
6.2.4 数组形参
-
由于
不能拷贝数组
,故不能以值传递方式传入数组 -
由于
数组会被转为指针
,故向函数传入数组时,实际上是传指针。(所以函数内不能用sizeof
等得到数组大小) -
形参数组的大小对函数没有影响,函数也不知道实参数组的尺寸
//以下3个函数等价,形参类型都是const int *
void print(const int *);
void print(const int []);
void print(const int [10]); //数组大小传入时被丢弃
int i=0,j[2]={0,1};
print(&i); //正确
print(j); //正确
管理指针形参三种技术:
-
要求数组本身有结束标记,如C风格字符串
-
传入
一对迭代器
(首指针和尾后指针),可由begin和end函数得到,类似标准库操作 -
定义一个表示
数组大小
的形参
void print(const char *cp){} //C风格字符串
void print(const int *beg, const int *end){} //一对迭代器
void print(const int ia[], size_t size){} //传入大小
数组形参和 const
- 若函数不需要写数组元素,则数组形参应是指向const的指针。
只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。
数组引用形参
- 形参可以是
数组的引用
,此时引用形参绑定到对应数组,此时避免数组被转为指针,给实参时大小也需与形参大小相等。
void print(int (&arr)[10]){ //形参是数组的引用,数组不转指针,实参大小也必须符合
for(auto elem:arr)
cout<<elem<<endl;
}
int i=0, j[2]={0,1};
int k[10]={0,1,2,3,4,5,6,7,8,9};
print(&i); //错误,实参不是含有10个整数的数组
print(j); //错误,实参不是含有10个整数的数组
print(k); //正确,实参是含有10个整数的数组
传递多维数组
- 传递多维数组时,多维数组的首元素本身就是数组,因此首元素指针就是指向数组的指针。传入函数时,首元素指针指向的对象仍是数组,因此数组的第二维以及其后所有维度的大小都是数组类型的一部分,不能省略。
//以下两定义等价
void print(int (*matrix)[10], int rowSize){} //指针形式传入二维数组
void print(int matrix[][10], int rowSize){} //数组形式传入二维数组
int *matrix[10]; //指针数组:指针构成的数组
int (*matrix)[10];//数组指针:指向数组的指针
6.2.5 main:处理命令行选项
- 可以给main传递实参,在命令行运行程序时传递给main函数
argv
是一个数组,其元素是指向C风格字符串的指针。argc
表示数组长度
//main的两种定义方式
int main(int argc, char *argv[]){}
int main(int argc, char **argv){}
$ prog -d -o ofile data # argc=5, argv={"prog","-d","-o","ofile","data"}
6.2.6 含有可变形参的函数
-
有时候无法预知函数会被传入多少个实参,此时用
可变形参
-
可变形参3种选择:
-
若实参类型相同,使用
initializer_list
标准库类型的对象 -
若实参类型不同,使用
可变参数模板
-
用于与C的接口时,可用
省略符...
-
initializer_list
-
initializer_list是一种标准库类型,是模板,用于表示某种特定类型的值的数组,定义在
initializer_list头文件
中 -
initializer_list对象中的元素永远是常量,因为传入参数不可改变。
-
向initializer_list形参中传递一个值的序列,必须把序列放在花括号
{}
中。实际上,列表初始化vector等容器
也是用了形参为initializer_list的拷贝构造函数
-
initializer_list包含
begin
和end
方法,可以使用范围for循环
void error_msg(ErrCode e, initializer_list<srting> il){ //定义传入initializer_list的函数
cout<<e.msg()<<": ";
for(const auto &elem:il)
cout<<elem<<" ";
cout<<endl;
}
string expected, actual;
if(expected!=actual)
error_msg(ErrCode(42), {"functionX",expected,actual}); //向initializer_list传参
else
error_msg(ErrCode(0), {"functionX","OK"}); //向initializer_list传参
省略符形参
- 省略符形参是为便于C++访问某些特殊C代码设计的,这些代码使用了
varargs
的C标准库功能。 - 省略符形参只能用于C和C++通用的类型,大多数类类型对象通过它传参时无法正确拷贝
- 省略符形参只能出现在形参列表的最后,省略符形参对应的实参无需类型检查
6.3 返回类型和 return 语句
return语句终止当前正在执行的函数,并将控制权返回到调用该函数的地方
return;
return expression;
6.3.1 无返回值函数
-
无返回值的return语句只能用在返回类型是
void
的函数中,这样的函数不一定需要显式的return,它们最后一行后面会隐式执行return。 -
如果想在返回void的函数中间退出,可用return
-
返回类型是void的函数也可用
return expression
形式,只是expression
必须是另一个返回void的函数,否则报错。
6.3.2 有返回值函数
-
只要函数返回类型不是void,则函数内的每个return都必须返回值。return返回的类型必须与函数返回类型相同,或者能隐式转换为函数返回类型
-
有返回值的函数必须通过显式的return返回,最后不存在隐式的return。未通过显式return返回是未定义行为。
值是如何被返回的
- 函数返回值的方式和初始化变量、形参一样:返回的值用于初始化调用点的一个
临时量
,该临时量是调用表达式
的结果 - 若函数的返回类型非引用,则被拷贝到调用点。若函数返回引用,则该引用它仅是它所引对象的别名。
不可返回局部对象的引用或指针
- 函数终止意味着局部变量的引用将指向不再有效的内存区域。
- 返回局部引用是错误的
- 返回局部指针也是错误的
返回类类型的函数和调用运算符
- 调用运算符
()
的优先级与点运算符.
和箭头运算符>
相同,且满足左结合律
。因此,如果函数返回类类型对象或其引用,就可在调用表达式后直接取成员
uto sz=shorterString(s1,s2).size(); //调用shorterString返回string对象,可立即取成员
引用返回左值
- 返回引用的函数被调用后得到左值,其他类似得到右值。故可为返回非常量引用的调用表达式赋值
char &get_val(string &str, string::size_type ix){ //返回char的引用
return str[ix]; //假定索引有效,不做检查
}
int main(){
string s("a value");
cout<<s<<endl;
get_val(s,0)='A'; //函数调用返回左值,可赋值
cout<<s<<endl;
return 0;
}
列表初始化返回值
- 函数可返回花括号包围的值的列表,这表示对返回的临时量做列表初始化。若列表为空,临时量被值初始化。
- 若返回的时内置类型,则花括号内最多包含一个值;若返回类类型,由类本身定义初始化行为。
vector<string> process(){
if(expected.empty())
return {}; //返回vector被值初始化
else if(expected==actual)
return {"functionX","OK"}; //返回vector被列表初始化
else
return {"functionX",expected,actual}; //返回vector被列表初始化
}
主函数 main 的返回值
-
允许main函数没有return语句直接结束,相当于最后隐式执行
return 0;
,返回0表示成功,其他值表示失败 -
cstdlib头文件
中定义了两个预处理变量
分别表示成功与失败:EXIT_SUCCESS
(成功)和EXIT_FAILURE
(失败)
递归
-
递归函数
:一个函数调用了它自身 -
递归函数中必有某条路径不包含递归调用,否则函数永远不会终止。
递归循环
-
main不可递归调用
6.3.3 返回数组指针
数组不能拷贝,故函数不能返回数组,但可返回数组的指针或引用
typedef int arrT[10]; //arrT等价于长度为10的整型数组,不会转指针
using arrT=int[10]; //等价于上一行,arrT等价于长度为10的整型数组,不会转指针
arrT *func(int i); //函数返回指向arrT的指针
声明一个返回数组指针的函数
-
若不使用类型别名,就必须记得将数组维度带上。
-
要定义一个返回数组指针的函数,则返回数组的维度必须放在形参列表之后。形如:
type (*function(parameter_list))[dimension]
。-
function是形参列表为parameter_list的函数,它返回一个指针,该指针指向大小为dimension的数组,元素类型是type
-
示例:
int ( *fun(int i) ) [10];
-
使用尾置返回类型
- 为简化复杂返回类型的定义,任何函数的定义都可用尾置返回。
- 尾置返回类型放在形参列表后,并以->开头,代替原来返回类型的地方使用auto。
- 尾置返回常用于返回数组指针或引用的函数
auto func(int i) -> int(*)[10]; //尾置返回声明,函数返回指针,该指针指向大小为10的int数组
int (*func(int i))[10]; //等价于上一行,是它对应的普通声明
使用decltype
- 如果知道函数返回的指针指向哪个数组,就可用decltype声明返回类型。
- decltype作用于数组时不会转指针,故指针符号
*
需手动加上
int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};
decltype(odd) *arrPtr(int i){ //推导返回类型为数组指针
return (i%2)?(&odd):(&even);
}
// 返回一个引用,该引用所引的对象是一个含有 5 个整数的数组。
decltype(odd) &arrPtr(int i) {
return (i % 2) ? odd : even; // 返回数组的引用。
}
6.4 函数重载
-
同一作用域内几个函数名字相同但形参列表不同。即应该在
形参
的数量
或类型
上有所区分(仅返回类型不同不叫重载)。 -
调用重载函数时,编译器根据实参类型推断出想要的是哪个函数
-
main函数不可重载
-
不允许两个函数除
返回类型
外其他所有的要素都相同 -
声明时,形参的名字可以省略
重载和 const 形参
-
顶层const不影响传入函数的对象,顶层const的形参无法和顶层非const的形参区分,因此
顶层const不可重载
-
若形参是指针或引用,则通过区分指向对象是否是const可实现重载,即
底层const可以重载
Record lookup(Phone);
Record lookup(const Phone); //顶层const,不可重载,重复声明
Record lookup(Phone *);
Record lookup(Phone * const); //顶层const,不可重载,重复声明
Record lookup(Account &);
Record lookup(const Account &); //底层const,可以重载
Record lookup(Account *);
Record lookup(const Account *); //底层const,可以重载
const_cast 和重载
-
编译器可通过实参底层是否是const判断该调用哪个函数。
-
const对象/指向const的指针只能传递给底层const形参
-
非const对象/指向非const的指针可以传递给底层const形参或底层非const形参,但编译器优先选择底层非const版本
-
const_cast
在重载函数的情形中很有用,它可以修改指针或引用的底层const权限。若一个引用/指针指向了非常量对象
,而该引用/指针却被声明为常量引用/指向常量的指针,则不可通过此引用/指针来修改对象,除非用const_cast将此引用/指针的底层const资格去掉。 -
const_cast只可在
底层本来为非常量对象
时,才能去掉引用/指针的底层const
//返回两字符串的较短者,输入输出都是常量引用
const string &shorterString(const string &s1, const string &s2){
return (s1.size()<=s2.size()>)?(s1):(s2);
}
//上一个函数的重载,输入输出都是非常量引用的版本
string &shorterString(string &s1, string &s2){ //输入对象本来是非常量
//修改引用权限,使引用为底层const
auto &r=shorterString( const_cast<const string &>(s1),
const_cast<const string &>(s2));
//底层对象实际仍为非常量,只是引用是底层const,此时可用const_cast修改引用权限
return const_cast<string &>(r);
}
调用重载函数
-
函数匹配
又叫重载确定
,编译器首先将调用的实参与重载集合中每一个函数的形参比较,再根据比较结果决定调用哪个函数 -
调用重载函数时可能有3种结果:
-
找到
最佳匹配
的函数 -
找不到任何函数与实参匹配,
无匹配
错误 -
匹配到多于一个函数,但每一个都不是最佳,
二义性调用
错误
-
6.4.1 重载与作用域
-
将函数声明放在局部作用域内不太好
-
一旦在内层作用域中找到所需名字,编译器将忽略外层作用域中的同名实体
-
在不同作用域中不可重载函数名,作用域相同才能重载
-
C++中,
名字查找发生在类型检查之前
6.5 特殊用途语言特性
6.5.1 默认实参
-
默认实参
:函数调用时可以不指定对应的形参,此时该形参被初始化为默认实参。 -
带有默认实参的形参,既可接受默认实参,也可在调用时被赋予指定值
-
默认实参只能在形参列表最后
:函数调用时实参按照位置解析,默认实参负责填补函数调用缺少的尾部实参
使用默认实参调用函数
- 设计含有默认实参的函数时,需要合理安排形参顺序
默认实参声明
- 多次声明同一个函数合法,但同一作用域中一个形参只可被赋予一次默认实参。即,函数的其他声明只能为前面声明中没有默认实参的形参添加默认实参,且该形参右侧的所有形参都必须有默认值
string screen(sz, sz, char=' '); //第一条声明
string screen(sz, sz, char='*'); //错,重复声明
string screen(sz=24, sz=80, char); //对,添加默认实参
默认实参初始值
- 函数内的局部变量不可作为默认实参。此外,只要表达式能转换成形参所需的类型,该表达式就能作为默认实参
- 用作默认实参的名字在函数
声明
所在的作用域中解析,但求值过程发生在函数调用
时
sz wd=80;
char def=' ';
sz ht();
string screen(sz=ht(), sz=wd, char=def); //声明函数,带有默认实参
string window=screen(); //screen(ht(),80,' ')
void f2(){
def='*'; //def变量仍是外部的
sz wd=100; //内部重新定义了wd变量
window=screen(); //默认实参的名字在外部解析,故为screen(ht(),80,'*')
}
6.5.2 内联函数和constexpr函数
调用函数一般比求等价的表达式更慢,因为运行函数有开销
内联函数可避免函数调用的开销
- 内联函数:将函数在每个调用点处内联地展开(类似宏定义),避免运行时开销。形式是在函数返回类型前加
inline
关键字
inline const string &shorterString( const string &s1, const string &s2 ){/* */};
constexpr函数
-
constexpr函数是指能用于常量表达式的函数,其返回类型及所有形参类型都是字面值类型,且函数体有且仅有一条return语句
-
constexpr函数
应在编译期就可确定结果,其返回值是常量表达式
,故可给constexpr类型
变量赋值。 -
为在编译期随时展开,constexpr函数被
隐式
的指定为内联函数
-
允许constexpr的返回值是非常量,只要保证输入常量表达式时能输出常量表达式(即编译期求值)即可。
constexpr int new_sz() {return 42;} //返回常量表达式
constexpr size_t scale(size_t cnt) {return new_sz()*cnt;} //形参非常量,返回值非常量
int arr[scale(2)]; //对,scale输入常量表达式时输出即是常量表达式
int i=2;
int a2[scale(i)]; //错,scale输入不是常量表达式,输出也不是常量表达式
把内联函数和 constexpr 函数放在头文件内
- 普通函数只可多次声明不可多次定义,但内联函数和constexpr函数可多次定义,他的多个定义必须完全一致。基于这个原因,内联函数和 constexpr 函数通常定义在头文件内。
6.5.3 调试帮助
- 程序可包含一些用于调试的代码,只在开发时使用,发布时需屏蔽掉调试代码。这种方法用到两项预处理功能:
assert
和NDEBUG
assert 预处理宏
-
表达式
assert(expr)
。对expr求值,若为true则什么也不做,若为false则输出信息并终止程序 -
assert 定义在 cassert头文件中
-
预处理名字由预处理器而非编译器管理,不在std命名空间中
-
宏名字在程序内必须唯一
-
assert 常用于检查”不能发生“的条件
NDEBUG 预处理变量
-
assert宏的行为依赖于预处理变量NDEBUG,若定义了NDEBUG,则assert宏什么也不做。
-
默认状态下NDEBUG未被定义,可用#define NDEBUG来定义它,从而关闭调试状态,不执行assert检查
-
可在编译选项中指定预处理变量
$ CC -D NDEBUG main.cc #定义预处理变量NDEBUG。在VC中用/D
-
assert可当作调试手段,但不可代替真正的运行时逻辑检查、错误检查等。
-
可用NDEBUG编写自己的条件测试代码,类似于
头文件保护
-
_ _func_ _
:编译器为每个函数定义了该变量,它是const char
的静态数组,存放函数的名字 -
预处理器定义了另外几个对调试有用的名字:
-
_ _FILE_ _
存放文件名的字符串字面值 -
_ _LINE_ _
存放当前行号的整型字面值 -
_ _TIME_ _
存放文件编译时间的字符串字面值 -
_ _DATE_ _
存放文件编译日期的字符串字面值
-
-
void print(const int ia[], size_t size){ #ifndef NDEBUG //只有调试状态才执行中间的代码 cerr<<_ _func_ _<<": array size is "<<size<<endl; #endif //... }
-