函数基础
- 函数调用完成两项工作:一是实参初始化函数对应的形参,二是将控制权转移给被调用函数。此时主调函数的执行被暂时中断,被调函数开始执行
- 实参是形参的初始值,我们可以用
double
型的实参初始化int
型的形参,但存在精度丢失的问题 - 函数的形参列表可以为空,但是不可以省略,可以写成
()
或(void)
- 函数的返回类型不可以是数组类型或者是函数类型,但可以是指向数组或者函数的指针
1. 局部对象
在C++
语言中,名字有作用域,对象有生命周期lifetime
:
- 名字的作用域是程序文本的一部分,名字在其中可见
- 对象的生命周期是程序执行过程中该对象存在的一段时间
函数体是一个语句块,形参和函数体内部定义的变量统称为局部变量local variable
,仅在函数的作用域内可见,同时局部变量还会隐藏hide
在外层作用域中同名的其他声明中。
在所有函数体之外定义的对象存在于程序的整个执行过程中,此类对象在程序启动时被创建,直到程序结束时才被销毁,局部变量的生命周期依赖于定义的方式。
- 自动对象
普通局部变量都是自动对象,它们只存在于块执行期间。比如形参就是一种自动对象,函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也被销毁。
我们用传递给函数的实参初始化形参对应的自动对象,对于局部变量对应的自动对象来说,分为两种情况:如果变量定义本身含有初始化值则用初始值进行初始化;如果变量本身不含初始值则进行默认初始化。这意味着内置类型的未初始化局部变量将产生未定义的值。
- 局部静态对象
有时候需要令局部变量的生命周期贯穿函数调用及之后的时间,可以将局部变量定义为static
类型从而获得这样的对象。局部静态对象local static object
在程序执行路径第一次经过对象定义语句时初始化,直到程序终止时才被销毁。
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;
}
2. 函数声明
函数的名字必须在使用之前声明,类似于变量,函数只能定义一次,但是可以声明多次。唯一的例外是:如果一个函数永远也不会被我们用到,那么它可以只有声明而没有定义。
建议在头文件而非源文件中声明函数,这样做的原因在于可以确保所有函数的所有声明保持一致,一旦我们想改变函数的接口,只需要改变一条声明语句即可。
3. 分离式编译
下面CC
是编译器名字,$
是系统提示符。大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名为.obj
(windows)或者.o
(UNIX)的文件,后缀名的含义是该文件包含对象代码object code
。编译器会负责把对象文件链接到一起形成可执行文件。
$ CC -c factMain.cc # generate factMain.o
$ CC -c fact.cc # generate fact.o
$ CC factMain.o fact.o # generate factMain.exe or a.out
$ CC factMain.o fact.o -o main # generate main or main.exe
参数传递
每次调用函数时都会重新创建它的形参,并用传入的实参对形参初始化。参数传递包括引用传递和值传递:
- 引用传递:形参是引用类型,这时候引用形参是它绑定的对象的别名
- 值传递:实参的值被拷贝给形参,这两者是独立的对象
1. 传值参数
- 实参的值被拷贝给形参,对变量的改动不会影响到初始值
- 指针形参:当执行指针拷贝操作时,拷贝的是指针的值,拷贝之后两个指针是不同的指针,但是我们可以通过指针来修改它所指对象的值。
熟悉
C
语言的程序员常常使用指针类型的形参访问函数外部的对象,在C+++
中建议使用引用类型的形参来替代指针。
2. 传引用参数
- 使用引用避免拷贝:拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括
IO
类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。由于string
对象可能非常长,我们应该尽量避免直接拷贝它们,这时候使用引用传参也是比较明智的选择,比如const string &s1
。 - 使用引用形参返回额外信息:我们可以通过给函数传入一个额外的引用实参来实现多返回值,这种做法可能比定义一个新的数据类型接受参数要简单地多。
3. const形参和实参
当形参有顶层const
时,传给它常量或者非常量对象都是可以的,const
的意义在于函数可以读取值但是不能修改:
void fcn(const int i) {/* fcn可以读取i, 但是不能向i写值 */}
尽量使用常量引用:把函数不会改变的形参定义成普通的引用是一种常见的错误,这会导致两个问题:一是给函数的调用者一种误导,即函数可以修改它实参的值;二是会限制函数所能接受的实参类型,比如我们不能把const
对象、字面值或者需要类型转换的对象传递给普通的引用形参。
数组形参
数组拥有两个特殊性质:
- 不允许拷贝数组:意味着我们不能用值传递的方式使用数组
- 使用数组时会将其转换成指针:为函数传递一个数组时,本质上传递的是指向数组首元素的指针
下面这三个函数是等价的,编译器只会检查参数是否是const int*
类型:
void print(const int*);
void print(const int[]);
void print(const int[10]); // 维度表示我们期望数组含有10个元素,实际上不一定
1.管理指针形参三种常用的技术
- 使用标记指定数组长度: 典型的就是
C
风格字符串,函数在处理C
风格字符串时遇到空字符就停止。 - 使用标准库规范:传递指向数组首元素和尾后元素的指针
void print(const int *beg, const int *end)
{
// 输出所有元素
while (beg != end)
cout << *beg++ << endl; // 输出当前元素并将指针向前移动一个位置
}
int j[2] = {0, 1};
print(begin(j), end(i))
- 显式传递一个指向数组大小的形参:在
C
程序和老版本的C++
中常使用这种方法
// const int ia[]等价于const int *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));
2. 数组形参与const
当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const
的指针,只有当函数确实要改变元素值的时候,才把形参定义为指向非常量的指针。
3. 数组引用传参
- 维度
10
是类型的一部分,这意味着函数只能作用于大小为19
的整型数组 -
&arr
两端的括号必不可少
void print(int (&arr)[10])
{
for (auto elem : arr)
cout << elem << endl;
}
4. 传递多维数组
C++
中多维数组本质上是数组的数组,真正传递的是指向数组首元素的指针,首元素本身就是一个数组。
// matrix声明成指向含有10个整数的数组的指针
void print(int (*matrix)[10], int rowSize) {/*...*/}
// 等价定义
void print(int matrix[][10], int rowSize) {/*...*/}
main处理命令行选项
// 第二个形参是一个数组,它的元素是指向C风格字符串的指针
// 第一个形参表示数组中字符串的数量
int main(int argc, char *argv[]) { ... }
// 等价于
int main(int argv, char **argv) { ... }
// 调用方式: prog是二进制的名字
prog -d -o ofile data0
// 这时候argc为5
argv[0] = "prog"; // 保存程序的名字,而非用户输入
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0; // 尾后指针
含有可变形参的函数
有时候我们无法提前预知应该向函数传递几个参数,为了能编写处理不同数量实参的函数,C++
新标准提供了两种主要的方法:
- 如果所有实参类型相同,可以用
initializzer_list
标准库 - 如果实参类型不同,可以用可变参数模板,涉及模板的内容后续单独讲解
1. initializer_list 形参
initializer_list<T> lst;
initializer_list<T> lst{a,b,c...};
lst2(lst1) // 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素,拷贝后原始列表和副本共享元素
lst2 = lst // 同上
lst.size() // 列表中元素数量
lst.begin() // 首元素指针
lst.end() // 指向lst尾元素下一位置的指针
2. 省略符形参
省略符形参是为了便于C++
程序访问某些特殊的C
代码而设置的,这些代码使用了varargs
的C
标准库功能。省略符形参只能出现在形参列表最后一个位置:
void foo(parm_list, ...);
void foo(...);
返回类型和return语句
1. 值是如何返回的
返回一个值的方式和初始化一个变量或者形参的方式完全相同:返回的值用于初始化调用点的一个临时量,该临时量就是调用的结果。注意以下两种写法:
// 返回word的副本或者一个未命名的临时string对象
string make_plurak(size_t ctr, const string &word, const string &ending)
{
return (ctr > 1) ? word + ending : word;
}
// 形参和返回类型都是const string的引用,不管是调用函数还是返回结果都不会真正拷贝string对象
const string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
2. 不要返回局部对象的引用或指针
函数完成后,它所占用的存储空间也随之被释放掉,这意味着函数终止后局部变量的引用将指向不再有效的内存区域。
3. 引用返回左值
函数的返回类型决定函数调用是否是左值,调用一个返回引用的函数得到左值,其他返回类型返回右值。
我们能为返回类型是非常量引用的函数的结果赋值。
4. 列表初始化返回值
C++11
新标准规定,函数可以返回花括号包围的值的列表。
vector<string> process()
{
// 返回空的vector
return {};
// 返回列表初始化的vector对象
return {"functionX", "okay"};
}
5. 主函数main的返回值
如果控制到达了main
函数的结尾并且没有return
语句,编译器将隐式地插入一条返回0
的return
语句。为了使返回值和机器无关,cstdlib
头文件定义了两个预处理变量,我们可以用这两个变量分别表示成功和失败:
return EXIT_FAILURE; // 定义在cstdlib头文件中
return EXIT_SUCCESS; // 定义在cstdlib头文件中
6. 返回数组指针
因为数组不能拷贝,所以函数不能返回数组,不过可以返回数组的指针或引用。
- 声明一个返回数组指针的函数
Type (*function(parammeter_list))[dimension]
int (*func(int i))[10];
// 注意括号不能省略, 返回int[10]数组的指针
- 使用尾置返回类型
auto func(int i) -> int(*)[10];
- 使用decltype关键字声明返回类型
int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
// 返回指向5个整数的数组指针
decltype(odd) *arrPtr(int i)
{
return (i % 2) ? &odd : &even;
}
函数重载
如果同一作用域的几个函数名字相同但形参列表不同,则称为重载
overloaded
函数。
1. 重载和const形参
顶层const不影响传入函数的对象,一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
Record lookup(Phone);
Record lookup(const Phone); // 重复声明
Record lookup(Phone*);
Record lookup(Phone* const); // 重复声明
但是如果形参是某种类型的指针或引用,则通过区分指向的是常量还是非常量可以实现重载,此时是底层const
:
Record lookup(Account&);
Record lookup(const Account&); // 新函数,作用于常量引用
Record lookup(Account*);
Record lookup(const Account*); // 新函数,作用于指向常量的指针
2. 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);
}
3. 重载和作用域
如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体,在不同的作用域中无法重载函数名。
string read();
void fooBar(int ival) {
bool read = false; // 新作用域,隐藏了外层的read
string s = read(); // 错误:read是一个布尔值
}
特殊用途语言特性
1. 默认实参
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
// 调用时省略
string window;
window = screen();
window = screen(66);
window = screen(66, 256);
window = screen(66, 256, '#');
一旦某个形参被赋予了默认值,它后面所有形参都必须有默认值。
2. 内联函数和constexpr函数
在工程中我们经常把规模较小的操作定义成函数:
- 函数式编程可以提高程序的可读性
- 使用函数可以保证行为统一,即每次相同的操作都能按照同样的方式进行
但是函数有一个缺点;调用函数一般比求等价表达式的值要慢一些。在大多数机器上,一次函数调用意味着一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序需要转向一个新的位置继续执行。
将函数定义成内联函数可以消除函数的运行时开销,只需要在函数前加上
inline
关键字即可。一般来说,内联函数用于优化规模较小、流程直接、频繁调用的函数。
constexpr
函数指的是能用于常量表达式的函数,不过需要满足:
- 函数的返回值和所有形参都必须是字面值类型
- 函数体重有且仅有一条
return
语句
内联函数和
constexpr
函数可以在程序中多次定义,毕竟编译器想要展开函数仅有函数声明是不够的,还需要函数的定义。但是对于某个给定的函数,它的多个定义必须完全一致,因此我们一般把constexpr
函数和内联函数定义在头文件中。
调试帮助
C++
程序可以包含一些用于调试的代码,但是仅在开发程序时使用,当应用程序编写完成准备发布时要屏蔽掉调试代码。
1. assert预处理宏
assert
是一个预处理宏,需要注意的是:
-
assert
宏定义在cassert
头文件中,预处理名字由预处理器而非编译器管理,因此我们可以直接使用预处理名字而不需提供using
声明 - 和预处理变量一样,宏名字在程序内必须唯一,含有
cassert
头文件的程序不能再定义名为assert
的变量、函数或者任何实体
// 表达式为假: assert输出信息并终止程序运行
// 表达式为真:assert不做任何处理
assert(expr);
2. NDEBUG变量
assert
的行为依赖于一个名为NDEBUG
的预处理变量,如果定义了NDEBUG
则assert
什么也不做,默认状态下没有定义NDEBUG
,此时assert
将执行运行时检查。
$ CC -D NDEBUG main.C # 这条命令等价于在`main.C`文件的开头写#define NDEBUG
C++
预处理器定义了一些对于程序调试很有用的名字:
__func__: 存放函数的名字
__FILE__: 存放文件名的字符串字面值
__LINE__: 存放当前行号的整型字面值
__TIME__: 存放文件编译事件的字符串字面值
__DATE__: 存放文件编译日期的字符串字面值
函数匹配
当几个重载函数的形参数量相等以及某些形参的类型可以由其他类型转换而来时,这项工作就不容易了。
函数匹配的步骤如下:
- 选定本次调用对应的重载函数集合,称为候选函数,需要同时满足:与被调用的函数同名;其声明在调用点可见
- 从候选函数中选出可以被这组实参调用的函数,称为可行函数:形参数量与调用提供的实参数量相同;每个实参的类型与对应的形参类型相同或者可转换成形参类型
- 寻找最佳匹配:逐一检查函数调用提供的实参,寻找形参类型和实参类型最匹配的那个可行函数,如果无法确定哪个函数是最佳匹配则编译器会因为二义性而拒绝请求
函数指针
函数指针指向的是函数而非对象,函数指针指向某种特定类型,函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
// 比较两个string对象长度的函数
bool lengthCompare(const string &, const string &);
// 声明一个可以指向该函数的指针
bool (*pf)(const string &, const string &); // 未初始化
// 初始化
pf = lengthCompare; // pf指向名为lengthCompare的函数
pf = &lengthCompare; // 等价的赋值语句, &符可省略
// 解引用
bool b1 = pf("hello", "goodbye");
bool b2 = (*pf)("hello", "goodbye");
1. 函数指针形参
我们可以将函数指针作为形参使用:
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
可以讲话使用了函数指针的代码:
// 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; // 等价的类型
// 上面的冗长形式可以改写成:
void useBigger(const string &s1, const string &s2, Func); // 编译器自动把函数类型转换成指针
void useBigger(const string &s1, const string &s2, FuncP2);
2. 返回指向函数的指针
想要声明一个返回函数指针的函数,最简单的方法是使用类型别名:
using F = int(int*, int); // F是函数类型
using PF = int(*)(int*, int); // PF是函数指针
和函数类型的形参不一样,返回类型不会自动把函数累习惯转换为指针
PF f1(int); // 正确, 返回函数指针
F f1(int); // 错误, 返回类型是函数类型
F *f1(int); // 正确, 返回函数指针
对比一下繁琐的声明:
int (*f1(int))(int*, int);
当然我们也可以通过尾置返回类型的方式声明一个返回函数指针的函数:
auto f1(int) -> int (*)(int*, int);