《C++Primer 第五版》——第六章 函数

6.1 函数基础

  1. 函数(function) :是一个命名的代码块,我们可以通过调用函数来执行其中的代码。
  2. 函数的定义:① 返回类型(return type) ②函数名字 ③由0个或多个 形式参数(parameter) 组成的列表,简称形参列表 ④函数体。
  3. 用户通过 调用运算符(call operator) 来执行函数。调用运算符的形式是一对圆括号 ()它作用于一个表达式,该表达式是函数或者指向函数的指针 。圆括号内是一个用逗号隔开的 实际参数(argument) 列表,用户用实参初始化函数的形参。整个表达式的类型就是函数的返回类型。
  4. 函数的调用完成两项工作:①是用实参初始化函数对应的形参,②是将控制权转移给被调用函数。 主调函数(calling function) 的执行被暂时中断, 被调用函数(called function) 开始执行。
  5. 注意 :尽管实参和形参存在对应关系,但是并 没有规定实参的求值顺序 ,编译器能以任何可行的顺序对实参求值。
  6. 实参的类型必须能隐式转换成形参的类型,且个数相同,也就是实参与对应的形参类型和个数相匹配。
  7. 函数的形参列表可以为空,但是不能省略。 定义一个不带形参的函数,最常用的方法是写一个空的形参列表。有两种形式:
//隐式定义空形参列表
void fcpp(){}
//显式定义空形参列表
void fc(void){}
  1. 每个形参的参数类型都必须写出来(即使形参的类型一样)并使用逗号分隔任意两个形参不能同名,而且函数最外层作用域中的局部变量也不能与函数形参同名
    需要注意的是,形参名是可选的,有无形参名都会生成一个局部变量,但是无名的形参在函数中无法使用,所以这种格式一般用于 函数声明 或 用于函数模板的模板实参推导,更高级的用法是用于模板函数的重载
//错误
void fcpp_e(int v1, v2){}
//正确
void fcpp1(int v1, int v2){}
//正确
void fcpp2(int v1, int ){}
// 函数声明
void fc(int );
void fc(int a){}

// 函数模板的模板实参推导
template <class T>
inline T* _allocate(ptrdiff_t size, T*) {	// 第二个参数并没有形参名自然不会被使用到,它只是用于获取一个内置指针的value_type
	_set_new_handler(0);
	// 分配size*sizeof(T)容量的空间,此时也仅仅是分配内存而已
	T* tmp = (T*)(::operator new((size_t)(size*sizeof(T))));
	if (tmp == 0) {     // 内存分配失败
		std::cerr << "out of memory" << std::endl;
		exit(1);
	}
	return tmp;
}

由下图可以看出编译器将实参1、2压入栈中,然后在模板函数huhu中创建了两个形参被1、2初始化。
在这里插入图片描述在这里插入图片描述在这里插入图片描述
9. 大部分类型都可以用作函数的返回类型,一种特殊的返回类型是 void,它表示函数无返回值。 函数的返回值不能是数组或函数,但是可以是指向数组或函数的指针。
10. 函数头 :包括函数名、返回类型、参数列表三个部分。

6.1.1 局部对象

  1. 在 C++ 中,名字有作用域,对象有 生命周期(lifetime),概念如下:
  • 名字的作用域 :程序文本的一部分,名字在其中可见
  • 对象的生命周期 :程序执行过程中该对象存在的一段时间
  1. 函数体是一个块, 一个块构成一个新的作用域 ,我们可以在其中定义变量。
  2. 局部变量(local variable) :形参和函数体内定义的变量。
    它们对于该函数而言是“局部”,仅在该函数的作用域内可见。同时局部变量还会 隐藏(hide) 在外层作用域中同名的其他所有声明中——内外两层作用域都有同名变量,以当前所在作用域内为主。
  3. 在形参列表中较早出现的形参已在该函数的作用域中,比如:
int a;
int f(int a, int b = a); // 这里的a是形参a,当然这条语句是错误的,将在6.5.1的默认实参中讲解
  1. 在所有函数体之外定义的对象存在于程序的整个执行过程中。此类对象在程序启动时被创建,直到程序结束时才会销毁。
  2. 局部变量的生命周期取决于定义的方式。
  3. 自动对象(automatic object) :只存在于块执行期间的对象。
    自动对象的生命周期就是块执行的期间。
    所以当块的执行结束后,块中创建的自动对象的值就变成未定义的了。
  4. 内置类型的未初始化局部变量的值是未定义的。
  5. 局部静态变量(local static object) :类型为 static 的局部变量。
    局部静态变量的生命周期贯穿函数调用及之后的时间。局部静态变量在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁。
    例如:
static int sint;    // 定义一个类型为 static 的 int 局部变量 sint
  1. 局部静态变量如果没有显式初始化,它将执行 值初始化 —— 内置类型的局部静态变量初始化为0

6.1.2 函数声明

  1. 大部分函数只能被定义一次,可以多次声明。 例外是 虚函数、内联函数和 constexpr 函数 ,如果一个函数永远都不会被用到,那么它可以只有声明没有定义。
  2. 函数的声明可以无需函数体,只需要一个分号替代即可,而且还可以省略形参名。
  3. 函数原型(function prototype) :函数声明的别称。
  4. 函数的三要素(返回类型、函数名、形参类型)描述了函数的接口。
  5. 定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。

6.1.3 分离式编译

  1. C++ 支持 分离式编译(separate compilation) ,分离式编译允许我们把程序分割到几个文件去,每个文件独立编译。
  2. 可执行文件(executable file)
  3. 大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名是 .obj(Windows).o(UNIX) 的文件。
    这两个后缀名的含义:该文件包含 对象代码(object code)
  4. 编译的过程:例如
#假设 fact 函数定义位于一个名为 fact.cc 的文件中,它的声明位于 Chapter6.h 的头文件中。 fact.cc 应该包含 Chapter6.h 的头文件。另外在名为 factMain.cc 的文件中创建 main 函数, main 函数中将调用 fact 函数。要生成可执行文件,必须告诉编译起我们用到的代码在哪里,编译过程如下:
$ CC factMain.cc fact.cc # generates factMain.exe or a.out
$ CC factMain.cc fact.cc -o main # generates main or main.exe

其中的 CC 是编译器的名字, $是系统提示符, #后面是命令行下的注释语句。

#修改了其中一个源文件,只需要重新编译改动的文件
$ CC -c fact.cc
$ CC -c factMain.cc
$ CC factMain.o fact.o
$ CC factMain.o fact.o -o main

6.2 参数传递

  1. 每次调用函数时都会重新创建它的形参,并使用实参对形参进行初始化。
  2. 如果形参是引用类型,它将绑定对应的实参;否则将实参的值拷贝后赋给形参。
  3. 当形参是引用类型时,我们说它对应的实参被 引用传递(passed by reference) 或者函数被 传引用调用(called by reference) 。同样地,引用形参也是对应实参的别名。

6.2.1 传值参数

  1. 传值参数的机理:函数对形参做的所有操作都不会影响实参。而程序员可以通过指针的值访问外部对象,或是通过引用形参来改变外部东西。
  2. 在 C 中,程序员常常使用指针类型的参数和解引用运算符来访问外部的对象。在 C++ 中,建议使用引用类型的形参替代指针。

6.2.2 传引用参数

  1. 通过使用引用形参,允许函数改变一个或多个实参的值。
  2. 拷贝大的类类型对象或者容器对象比较低效,甚至 有的类( class )类型(包括 IO 类型在内)根本不支持拷贝类型当某种类型不支持拷贝类型时,函数只能通过引用形参访问该类型的对象。
  3. 一个函数只能返回一个值,引用形参为我们一次返回多个结果提供了有效的途径——给函数传入额外的引用实参用于存储其他返回结果。
  4. 如果函数无需改变引用形参的值,最好将其声明为常量引用。

6.2.3 const 形参和实参

  1. 若引用形参是 const 时,则 const 作用于被引用的对象本身。
  2. 与其他初始化一样,使用实参初始化时会忽略掉实参的顶层 const ,所以当实参有顶层 const 时,用它初始化常量形参或者非常量形参都是可以的。
  3. 在 C++ 中,允许我们定义若干相同函数名的函数,前提是参数列表有明显的区别。因为 top-level-const 形参中的 const 会被忽略掉 ,所以下面两个函数声明的参数列表会被编译器认为是一样的,会报错。
void fcn(const int i);
//确实在这个函数中无法改变i的值,但没有意义
//因为传进来的只是实参的副本。
void fcn(int i);
  1. 形参的初始化方式和变量的初始化方式一样 ,所以引用参数的初始化必须要用一个相匹配类型的对象。而我们可以使用一个非常量初始化一个 low-level-const 形参,反之不行。

6.2.4 数组形参

数组的两个特殊性质:
①不允许将数组以数组名拷贝赋值的方式赋值给别的数组,因为数组;
②使用数组名时(通常)会将其转换成指针。

不能以值传递的方式传递数组,也就无法定义数组类型的形参,但是 可以把形参声明写成类似数组声明的形式,但是实际上最后得到的还是指针形参

三个都是等价的,他们的形参都是 const int* ,而不是数组类型的形参
void print(const int*);
//函数的意图是作用于一个数组
void print(const int[]);
//表示我们期望数组含有多少元素,实际不一定
void print(const int[10]);

C++ 形参列表中的每个函数形参的类型根据下列规则确定(选出和数组有关的):
1) 首先,以如同在任何声明中的方式,组合声明说明符序列和声明符以确定它的类型。
2) 如果类型是“T 的数组”或“T 的未知边界数组”,那么它会被替换成类型“指向 T 的指针”
3) 如果类型是函数类型 F,那么它被替换成类型“指向 F 的指针”
4) 从形参类型中丢弃顶层 cv 限定符(此调整只影响函数类型,但不改动形参的性质:int f(const int p, decltype(p)*);int f(int, const int*); 声明同一函数)

因为这些规则,下列函数声明确切地声明同一函数:
int f(char s[3]);
int f(char[]);
int f(char* s);
int f(char* const);
int f(char* volatile s);

管理指针实参有三种常用的技术:
①数组本身包括一个结束标记,比如 C 风格字符串的结尾为空字符;
②传递指向数组首元素和尾后元素的指针(指向尾元素的下一个地址的指针);
③专门定义一个表示数组元素大小的形参。

可以通过定义 low-level-const 的形参来防止对数组内元素进行改写。

C++ 允许将变量定义成数组的引用,所以形参可以是对数组的引用 ,此时形参绑定了实参,也就是数组。

// arr 是一个对具有10个元素的 int 数组的引用
void print(int (&arr)[10])

注意: 因为数组的大小也是数组类型的一部分 ,所以在这里只能将一个具有10个元素的 int 数组作为实参传入。

int i = 0 ,m[10]{};
int n[2] {};
//错误
print(&i);
print(n);
//正确
print(m);

7.传递多维数组时,函数的引用形参类型必须与数组的维度个数和大小相同。
8.使用数组的语法定义函数时,编译器也会忽略掉第一个维度。

//等价定义
void print(int (*matrix)[10]);
void print(int matrix [] [10]);
//实际上形参是一个指向含有10个 int 元素的数组

6.2.5 main:处理命令行选项

  1. main 函数有时需要用户传递实参,一种常见的情况是用户设置一组选项来确定函数所要执行的操作。
    比如当 main 函数位于可执行文件 prog 之内,我们可以通过命令行向程序传递下面选项: prog -d -o ofile data0
    这些命令行选项通过两个(可选)形参(argc 和 argv)传递给 main 函数:
//下面两个是等价的
int main(int argc, char *argv[]){return 0;}
//因为 argv 实际上是数组,所以 *argv[] 可以写成 **argv 
int mian(int argc, char **argv){return 0;}

当实参传给 main 函数时, main 形参的值如下:
argv 的首元素指向程序的名字或一个空字符串
接下来的元素依次传递命令行提供的素材
尾后元素的值保证为0
以上面的命令行为例, argc 为5, argv 的元素如下:

// argv[0] 可以是一个空字符串
argv[0] = "prog";  
argv[1] = "-b";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;
  1. 注意: 当使用 argv 时,一定要记得可选的形参从 argv[1] 开始; argv[0] 保存程序的名字,要不就是0,不是用户的输入。

6.2.6 含有可变形参的函数

  1. C++11 为了用户能编写处理未知数量实参的函数,提供了两种主要的方法:
    ①如果所有的 实参类型相同 ,可以传递一个名为 initializer_list标准库类型
    ②如果 实参类型不同 ,我们可以编写一种特殊的函数,也就是所谓的 可变参数模板
  2. C++ 还有一种特殊的 形参类型(即省略符) ,可以用它传递可变数量的实参。
    注意:这种功能一般只用于与 C 函数交互的接口程序。
  3. initializer_list 类型是一种 标准库类型用于表示某种特定类型的值的数组 ,类似 vector 容器类型,它也是一种 模板类 ,它定义在同名的头文件中。
    它提供了一些 initializer_list 的操作如下:
默认初始化:初始化 lst 为 T 类型的空列表
initializer_list<T> lst;

 lst 的元素个数和初始值一样多;
 lst 的元素是对应初始值的副本;列表中的元素是 const 
initializer_list<T> lst{a, b, c . . . };

//拷贝或赋值一个 initializer_list 对象不会拷贝列表中的元素,而是拷贝对象的地址;
//拷贝后,原始列表和副本共享元素——就是原始列表和副本同时指向同一个临时内存
//(两个对象指向同一个,就是存放初始化 lst 时所用常量副本的一个临时内存地址)
lst2 = lst
lst2(lst)

//对象中元素数量
lst.size();

//返回指向 lst 首元素的指针
lst.begin()

//返回指向 lst 尾后元素的指针
lst.end()

注意:
①定义 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 << " ";
      cout << endl;
}

调用如下:

// exp 和 act 是 string 对象
//下列函数调用传入了三个实参
error_msg("functionX", exp, act);

含有 initializer_list 形参的函数还可以声明别的形参

void error_ggg(int a, initializer_list<string> il);

注意 initializer_list 形参的初始化需要使用列表 ,两种调用如下:

// exp 和 act 是 string 对象
error_ggg(12, {"functionX", exp, act});
// sstrlst 是 initializer_list 对象
error_ggg(12, strlst);
  1. 省略符形参 是为了便于 C++ 程序访问某些特殊的 C 代码 而设置的,这些代码使用了名为 varargs 的 C 标准库功能。
    注意:
    大多数类型的对象在传递给省略符形参时都无法正确拷贝。
    省略符形参只能出现在形参列表的最后一个位置 ,它的形式为以下两种:
/parm_list 是参数列表
void foo(parm_list, ...);
void foo(...);

第一种形式制定了 foo 函数的部分形参类型,对应这些形参的实参会执行正确的类型检查。省略符形参对应的实参无需类型检查。第一种形式中,形参声明后面的逗号是可选。


6.3 返回类型和 return 语句

  1. return 语句 终止当前正在执行的函数将控制权返回到调用该函数的地方
  2. return 语句有两种形式:
return ;
return expression; // expression 是表达式
  1. 一个函数内, return 语句可以有多个。

6.3.1 无返回值函数

  1. 没有返回值的 return 语句只能用在返回值类型为 void 的函数中。
  2. 返回值类型为 void 的函数不一定要有 return 语句 ,因为在返回值类型为 void 的函数执行完最后一句语句后,编译器会隐式执行没有返回值的 return 语句。
  3. 如果 void 函数想在它的中间位置提前退出,可以使用无返回值的 return 语句,类似于使用 break 语句提前退出循环一样。
  4. 一个返回类型是 void 的函数也能使用 return 语句的第二种形式,不过 expression 必须是另一个返回 void 的函数 ,否则强行令 void 函数返回其他类型的表达式将出现编译错误。

6.3.2 有返回值函数

  1. return 语句第二种形式的表达式 expression 的类型必须 与函数的返回类型相同 或者 能隐式转换成函数的返回类型
  2. 只要函数返回类型不是 void ,则除了 main 函数以外,函数内每条 return 语句都必须显式返回一个值。
  3. C++ 编译器能保证每个 return 语句的返回值类型正确。 若函数的 return 语句返回了与当前函数声明的返回值类型不同类型的值,编译将会失败。
  4. 注意:编译器有时不能检测到在含有 return 语句的循环语句之后应该含有一条 return 语句的错误。例如:
int text(int n){
  while(n != 10){
    if(n < 10)
      return n; //若 n 小于10则返回 n 的值
    n--;
  }
  //若 n 跳出循环则不返回任何值就结束了函数的运行
  //编译器可能检查不出这一错误
}
  1. C++ 如何实现函数值的返回:将函数的 return 语句的返回值用于初始化被调用函数的调用位置的一个临时量 (既可以是临时变量,也可以是临时对象,由编译器自动生成),该临时量就是该函数调用的结果。换句话说就是将返回值拷贝到这个临时量中。
    注意:
    ①若函数的返回值类型是引用类型,则编译器并不会生成一个临时量用于拷贝返回值,而是返回返回值的一个引用,换句话说就是别名
    ②函数的返回值类型不是引用类型,则编译器会生成一个临时量用于拷贝返回值
  2. 函数结束后,它所占的存储空间将会被释放掉(将函数栈帧删除),因此函数的局部变量将会被释放,其引用将不再指向有效的内存空间。所以 不要返回局部变量的引用或指针
  3. 调用运算符 ()的优先级与点运算符和箭头运算符相同,符合左结合律,函数名为其左侧运算对象。
  4. 可以通过点运算符和箭头运算符来访问函数的返回值中的成员 ,例如:
//通过点运算符访问 shorterString 函数返回值,一个 string 对象的成员方法 size 
auto sz = shorterString(s1, s2).size();
  1. 函数的返回类型决定函数调用是否为左值。调用一个返回类型为引用类型的函数得到左值,调用其他返回类型的函数得到右值。
  2. 当然如果返回类型是常量引用,我们不能给函数调用的结果赋值。
  3. C++11 规定:函数可以返回花括号包围的值的列表
    ①如果函数返回类型是 C++ 的内置类型,则花括号包围的列表最多包含一个值,而且该值的大小不应该大于函数返回类型大小。
    ②如果函数返回类型是 C++ 的类类型,则由类本身定义初始值如何使用。
    例如:
vector<string> process(string &expected,string &actual){
  if(expected.empty()){
    return {};  //用空列表初始化一个 vector<string> 临时量
  }
  else if(expected == actual){
    return {"function", "okay"};
  }
  else return {"function", expected, actual};
}
  1. 若 main 函数没有 return 语句直接结束,则编译器将隐式插入一条 return 0;
  2. 在 cstdlib 头文件里定义了两个预处理变量用于判断 main 函数是否执行成功: EXIT_FAILURE 和 EXIT_SUCCESS 。
  3. 递归函数(recursive function) :一个函数直接或间接地调用了它本身。递归函数将会一直执行直到执行 return 语句或者栈空间耗尽为止。

6.3.3 返回数组指针

  1. 可以定义数组类型的别名,如 typedef int arrT[10];// arrT 是类型别名using arrT = int[10];// C++11 中的别名声明arrT* fun();是返回一个指向长度为10的一维数组的首指针, 注意是指向指定维度数组
  2. 注意:
arrT* funtion1() {
    int a[10] = {};
    return a; //这是错误的, a 的类型是 int[10] ,而编译器无法隐式将 int[10] 的值转换成 int(*)[10]  
}
arrT* funtion2() {
    int b[11] = {};
    return &b; //这是错误的, b 的类型是 int(*)[11] 
}
arrT* funtion3() {
    int c[10] = {};
    return &c; //这是正确的
}
  1. 在不使用类型别名的情况下,如果想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后。
    函数声明格式如下 type (*function(parameter_list))[dimension];, type 是数组元素类型, function 是函数名, parameter_list 是参数列表, dimension 是数组大小。
    例如 int (*func(int i))[10];和 第1条的 arrT* fun();等价,其中 arrT 是 int[10] 的类型别名。
  2. C++11 新标准增添了一种可以简化定义一个返回数组指针的函数的方法,就是使用 尾置返回类型(trailing return type)任何函数的定义都可以使用尾置返回类型 ,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或数组的引用。
    函数声明形式如下: auto func(parameter_list) -> type,其中 auto 关键字放在函数名前, func 是函数名, parameter_list 是参数列表, type 是函数的返回类型。
    例如 auto func(int i) -> int(*)[10];
  3. 如果知道函数返回的指针指向哪个数组,则 可以使用 decltype 关键字声明返回类型
    例如:

6.4 函数重载

  1. 重载函数(overloaded function) :同一作用域内的几个函数的函数名相同,但 参数列表不同 。参数列表不同指的是参数类型和数量不全相同。 如果只是返回类型不同,这么定义重载函数是错误的 ,因为大多数重载函数是根据形参来匹配相应重载函数。
    第七章还会学到一种特殊情况,常量成员函数,它支持在参数列表相同的情况下,通过在()后添加 const 实现重载。
  2. main 函数不能重载。
  3. 以下的情况的形参类型相同:
1. 使用类型别名
2. 在函数声明中省略形参名
3. 使用顶层 const ( top-level-const1. 
typedef  int typeint;
void hh1(typeint a);
void hh1(int a); //一样的,使用了类型别名
2. 
void hh2(typeint a);
void hh2(typeint); //一样的,忽略了形参名
3. 
void hh3(typeint* const a);
void hh3(typeint* a); //一样的,使用了顶层 const 
void hh3(const int a);
void hh3(int a); //一样的,使用了顶层 const 
  1. 对于形参是某种类型的引用或者指针时,则通过区分其指向的是常量对象还是非常量对象以实现函数重载。这里的 const 是底层 const 。
//以下4个函数的形参类型不同
void look(const int* a);
void look (int* a);

void look(const int & a);
void look(int & a);
  1. 可以通过C++的强制类型转换的 const_cast 来实现一些函数的重载,以达到 DRY 编程原则(简单说就是不写重复的代码片段用来复用)和 pass-by-reference-to-const (通过 const 引用传送参数)。
    例如
const string &shorterString(const string &s1,const string &s2)
{
  return s1.size()<=s2.size()?s1:s2;
}
//重载函数
string &shorterString(string &s1,string &s2)
{
  //这里就实现了 DRY 编程原则
  auto &r = shorterString(const_cast<const string&>(s1),const_cast<const string&>(s2));
  return const_cast<string &>(r);
}
  1. 函数匹配(function matching) :是指一个把函数调用与一组重载函数中的某一个关联起来的过程,也叫 重载确定(overload resolution) ,编译器首先将调用的实参与一组重载函数中每一个函数的参数列表进行比较,再根据比较的结果选择并调用最佳函数。
  2. 在某些特殊情况下,函数匹配将会比较困难,比如函数参数的数量相同且类型可以互相转换。
  3. 调用重载函数会有三种可能的结果:
  • 编译器找到一个与实参 最佳匹配(best match) 的函数,并生成调用该函数的代码。
  • 找不到任何一个函数与调用的实参匹配,此时编译器发出 无匹配(no match) 的错误信息。
  • 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生名为 二义性调用(ambiguous call) 的错误。

6.4.1 重载与作用域

  1. 在不同作用域无法重载函数名,内层作用域声明的标识符会覆盖掉外层作用域的标识符(包括外层定义的函数或变量)。例如定义一个函数 fool ,在 main 函数内定义一个名为 fool 的 int 型变量,则在 main 函数内使用 fool 代表一个 int 型变量。
  2. C++的名字查找发生在类型检查之前 。 C++ 先寻找同名标识符,若找到就忽略掉外层作用域的同名实体。

6.5 特殊用途语言特性

6.5.1 函数的默认参数

默认实参(default argument) :在函数的定义时,为形参提供了一个默认值,此时这个默认值被称为默认实参。

一旦某个形参被赋予了一个默认实参,那么它后面(右边)的形参都必须有默认值。
例如:void defa(int a, int b = 1, int c);//错误的, c 也必须有默认值

有三个重点要注意:

在相同作用域中,无论是函数的定义还是声明,一个形参只能被赋予一次默认实参。在别的作用域中赋予某个形参的默认实参,不会影响(传递)到另一作用域中的同一个形参详解
含有默认参数的函数定义或声明语句所在的位置,需要在主调函数的语句的前面
默认实参不是函数类型的一部分,需要函数类型的时候要注意。

使用默认实参调用函数

如何使用默认形参:

void screen(int = 0, int = 0, string = "red");//符合函数声明形参名可以被省略的规定
int main(){
  screen();  //调用全部的默认形参
  screen(66);  //相等于 screen(66, 0, "red"); 
  screen(66, 66);  //相等于 screen(66, 66, "red"); 
  screen(66, 66, "white");
}

注意:

因为函数调用时,实参按位置对应赋值给形参,但是赋值的先后顺序未知。所以一般情况下, 默认实参负责填补函数调用缺少的尾部实参(靠右位置) 。所以一般设计含有默认实参的函数时,要将不经常使用默认实参的参数位置提前(左移)。

screen(, , 66);// 错误:想要覆盖第三个形参的默认值,只能先对前两个形参赋值

不同作用域中的默认实参

函数的声明一般是放在头文件中,并且一般一个函数只声明一次,但是一个函数多次声明也是合法的。

注意:

可以通过声明函数的形式给形参添加默认实参,但是在给定的作用域中一个形参只能被赋予一次默认实参。在别的作用域中赋予某个形参的默认实参,不会影响(传递)到另一作用域中的同一个形参。
而且要通过声明函数的形式给某个形参添加默认实参,就必须保证在之前的声明或定义中已经给该形参之后(右边)所有的形参都赋予了默认实参。

红字展开来说,即在给定的作用域中函数的后续声明只能为之前没有默认值的形参添加默认实参,换句话说在给定的作用域中不能通过声明修改已被赋予默认实参的形参的默认值。
比如:

//假设在头文件 TT.h 的全局作用域中
void jl(int a, int b = 0, int flag = 0); 	// 正确
void jl(int a =1, int b, int flag);			// 正确
//添加默认形参 a 后,相当于void jl(int a = 1, int b = 0, int flag = 0);

void screen(int a, int b, int flag);	// 因为没有从尾部形参开始赋予默认实参,所以下面语句会报错
void screen(int a, int b = 0, int flag);// 错误:缺少参数3(即flag)的默认实参
void t1(int , int t = 2);
void t1(int , int t = 1);				// 错误:重复声明,在给定的作用域中一个形参只能被赋予一次默认值

void jl(int x, int y = 0) {}// 正确:全局作用域的jl函数定义中赋予了 y 默认实参
void jl(int = 1, int );		// 正确:在声明中添加默认实参
int main() 
{ 
	void jl(int, int); 		// 正确:因为在main的作用域的jl函数的两个形参都不具有默认实参
	void t1(int , int = 2);	// 正确:所以可以重新赋予同一个形参默认实参,而不会报错
}	

默认实参的初始值

默认实参的初始值的注意事项:

①局部变量不能作为默认实参(除非用于不求值的表达式中,C++14 起);

class X {
public:
    int a;
    int mem1(int ss = sizeof(a));	// 正确:C++14 起,用于不求值语句中的局部变量可以作为默认实参
};

② this 指针不能作为默认实参;
原因: 普通成员函数包含this形参,但因为函数参数的赋值先后顺序是未定的,所以该默认值也是未定的,如int function(class_type *this, int n = this->a); // 实际上没有class_type *this,这是隐式的,可能先执行第二个参数的赋值,此时的this是未定义的标识符,自然this->a就是错误的。

class A { void f(A* p = this); }; // 错误:不允许 this 作为默认实参

③默认实参不允许使用非static的类成员(即使它们不被求值),除非它被用于实现类成员访问表达式或被用于形成指向类成员的指针;

// 非静态类成员被用于形成指向类成员的指针
class X
{
    int a;
    int mem1(int X::* i = &X::a);
};
int X::mem1(int X::* i) {
    // same as `return a;` if called with default argument
    // but if called with `mem1(&X::b)` same as `return b;`
    return this->*i; 
}

// 非静态类成员被用于实现类成员访问表达式
class A
{
    int a;
    static A x;
    int mem1(int i = x.a); // ok: `.` is member access
    //int mem2(int i = this->a); // error: because of `this` is can not use to default argument, but `->` is member access
};

④默认实参不允许使用函数形参(即使它们不被求值,C++14 前)(除非它们不被求值 ,C++14 起)。注意,形参列表中较早出现的形参已在该函数的作用域中。比如:

int a;
int f(int a, int b = a); // 错误:形参用作默认实参
int g(int a, int b = sizeof(a)); // CWG 2082 前为错误
// CWG 2082 后为OK:用于不求值语境是 OK 的

用作默认实参的表达式如果改变,则默认实参的值也会改变。 比如:

//wd、def和ht的声明必须出现在函数之外
sz wd =80;
char def = ' ';
sz ht();
string screen(sz=ht(),sz=wd,char=def);
string window =screen();			//调用screen(ht(),80,' ')

//用作默认实参的名字在函数声明所在的作用域内解析
//而这些名字的求职过程发生在函数调用时:
void f2()
{
	def = '*';					//改变用作默认实参的表达式的值
	sz wd =100;					//隐藏了外层定义的wd,但是没有改变默认值
	window = screen();			//调用screen(ht(),80,'*')
}

6.5.2 内联函数和 constexpr 函数

  1. 调用函数会导致一些时间和空间上的开销:保存调用者保存寄存器和被调用者保存寄存器,并在函数返回时恢复;将参数拷贝进栈中;栈顶指针寄存器和基址指针寄存器的存取等操作会消耗时间和空间。
  2. 内联函数(inline function) :在每个调用点上“内联地”展开的函数,不会产生常规函数时间和空间上的开销,与宏类似。
  3. 内联函数的声明:在函数声明前面加上关键字 inline
    例如:
inline const string & shorterString(const string &s1, const string & s2)
{
  return s1.size() <= size() ? s1 : s2;
}
  1. 内联函数适用于优化规模较小、流程直接、频繁调用的函数。
  2. 很多编译器都不支持内联递归函数。
  3. constexpr 函数(constexpr function) :是指能用于常量表达式的函数。
  4. 定义 constexpr 函数的要求:

①函数的返回类型和所有形参的类型都必须是字面值类型,并且返回类型前还要加上关键字 constexpr ;
②函数体中有且只有一条 return 语句;
③函数体中的语句除了 return 语句外,其它语句必须是在运行时不执行任何操作的,例如空语句、声明类型别名、 using 声明等语句;

  1. constexpr 函数被隐式地指定为内联函数 ,以方便在编译过程中展开。
  2. 允许 constexpr 函数不返回常量,若有参数,且实参是常量表达式,则函数返回常量表达式。
//如果arg是常量表达式,则scale(arg)也是常量表达式
constexpr size_t scale(size_t cnt){ return new_sz() * cnt; }

//当scale的实参是常量表达式时,它的表达式也是常量表达式,反之则不然
int arr[scale(2)];	//正确:scale(2)是常量表达式
int i = 2;			//i不是常量表达式
int a2[scale(i)];	//错误:scale(i)不是常量表达式
  1. 与其他函数不同,内联函数和 constexpr 函数可以定义多次,但是它的多个定义必须完全一致。 所以内联函数和 constexpr 函数一般都定义在头文件中。

6.5.3 调试帮助

  1. C++ 程序员可以通过一种技术,以便有选择地调式代码。基本思想是程序包含一些用于调试的代码,但是这些代码只会在开发程序时使用。当应用程序编写完准备发布时,要先屏蔽掉调试代码才能发布。这种方法用到两项预处理功能: assert 和 NDEBUG
  2. 断言( assert )预处理宏 : assert 是一个定义在头文件 assert.h ( C++ 还有一个 cassert )中的预处理宏。 assert(expr);通过一个表达式 expr 作为它的条件,如果表达式为假,assert 输出信息并终止程序执行;如果表达式为真,则 assert 什么都不做。
  3. 预处理宏(preprocessor marco) :其实就是一个预处理变量,预处理器会在编译器进行编译前对所有的预处理宏进行处理替换。
    宏的名字在同一个程序内必须唯一,不能再定义一个同名的变量、对象、函数或其他用处的标识符。
  4. assert 的行为依赖于一个名为 NDEBUG 的预处理变量的状态。如果定义了宏 NDEBUG ,从而关闭了调试状态,则无论表达式为什么, assert 什么都不做。
    注意: NDEBUG 宏要在头文件 assert.h 或 cassert 前定义
#include <iostream>
#define NDEBUG
#include <cassert>

或是通过一个命令行选项 $ CC -D NDEBUG main.C # use /D with the Microsoft compiler,这两个方法是等价的。
5. 一般 debug 版本的 NDEBUG 不会自动定义的(但会自动定义一个名为 _DEBUG 的宏),但是 release 版本会自动定义 NDEBUG 。
6. 除了使用 assert 外,还可以使用 NDEBUG 编写自己的条件调试代码,如果定义了宏 NDEBUG ,则忽略掉 #ifndef 和 #endif 之间的代码。

#ifdef NDEBUG
  //如果没有定义了 NDEBUG ,则执行 #ifndef 和 #endif 之间的代码
  //通过通过 __func__ 宏可以输出当前调试的函数名
  // cerr :输出到标准错误的一个 ostream 对象,常用于程序错误信息;
  cerr << __func__ <<  endl;
#endif 
  1. 除了 C++ 编译器定义的局部静态 char 数组 __func__外,预处理器还定义了另外4个对程序调试有用的宏:
作用
__FILE__存放文件名的字符串字面值
__LINE__存放当前行号的整型字面值
__TIME__存放文件编译时间的字符串字面值
__DATE__存放文件编译日期的字符串字面值

6.6 重载函数的匹配

  1. 函数匹配的步骤:

①选定本次调用对应的重载函数集(候选函数);
②根据本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数(可行函数);
③根据本次调用提供的实参,从可行函数中选择与本次调用最匹配的函数(实参类型与形参类型越接近越好),如果有且只有一个函数满足以下条件则匹配成功。

  1. 该函数的 每个 实参的匹配 都不劣于 其他可行函数需要的匹配;
  2. 满足第一个条件下,至少有一个实参的匹配优于其他可行函数提供的匹配。
重载函数 f 有以下声明
void f(int, int);
void f(double, double = 3.14);//符合形参名可以被省略的规定
void f(int);

有以下调用(有对有错)
f(5.6);//该调用只有一个 double 实参,符合条件1和2,所以是正确调用
f(5.6, 1);//该调用只符合条件2,不符合条件1,所以产生二义性错误
  1. 候选函数(candidate function) :函数匹配时调选的重载函数集合中的函数。
    候选函数具有两个特征:

①与被调用的函数同名;
②其声明在调用点可见。

  1. 可行函数(viable function) :从候选函数中选出,能被这组实参调用的函数。
    可行函数具有两个特征:

①形参数量与本次调用提供的实参数量相同;
②每个对应的实参类型与形参类型相同,或是能(隐式)转换成形参类型。

  1. 如果函数含有默认形参,则我们在调用该函数时传入的实参数量可能少于它实际使用的实参数量。
  2. 如果没有找到可行函数,编译器则报告无匹配函数的错误。
  3. 如果没有找到最佳函数,编译器则报告二义性调用的错误。
  4. 调用重载函数时,应该尽量避免强制类型转换。如果使用了强制类型转换,就说明重载函数设计不合理。

6.6.1 实参类型转换

  1. 寻找函数匹配的最佳函数时, 如何判断每一个实参匹配的优劣 (1>2>3>4>5):
  1. 精确匹配,包括以下情况:
  • 实参类型和形参类型相同
  • 实参从 数组类型或函数类型转换成对应的指针类型
  • 向实参 添加 top-level-const (非常量实参传给 top-level const 形参)或者从实参中 删除 top-level const (常量实参传给非常量形参);(在函数重载中,顶层 const 形参与非顶层 const 实参是一样的)
  1. 通过 底层 const 转换 实现的匹配(非常量对象传给 low-level const 的引用或指向非常量对象的指针传给 low-level const 的指针);
  2. 通过 类型提升 实现的匹配(整型提升);
  3. 通过 算术类型转换指针转换 (4.11.2)实现的匹配;
  4. 通过 类类型转换 (14.9)实现的匹配。
  1. 通过类型提升实现的匹配与通过算术类型转换实现的匹配的区别:
void ff(int);
void ff(short);
int main(){
	ff(2);
//此时会调用形参类型为 int 的函数
//因为将 char 类型提升为 int 类型的函数匹配比将 char 类型转换成 short 类型的匹配优先级要高
//第一种是类型提升的匹配,第二种是算术类型转换的匹配
}

因为所有通过算术类型转换实现的匹配优先级都是一样的,所以下面的函数调用会产生二义性错误:

void nn(double);
void nn(long);
int main(){
	nn(2);//会产生二义性错误,因为从 int 转换到 double 和 long 的匹配优先级是一样的
}

6.7 函数指针

  1. 函数指针指向的是某种特定类型的函数,而非对象。
  2. 函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
    例如:
//该函数的类型为 bool (const string&, const string&)
bool length(const string&, const string&);

如何声明一个可以指向该函数的指针,只需要用 *指针名替换指向的函数声明中的函数名:

//该函数指针为 pf 
//如果不用 () ,那么 pf 就是一个返回类型为 bool* ,参数列表为 (const string&, const string&) 的函数
bool (*pf)(const string&, const string&);
  1. 当我们把函数名作为值使用时,函数名会被隐式转换成函数指针。 在函数名前加 &表示该表达式的类型是一个指向该函数的指针。
//两个表达式都是一样的
pf = length;
pf = &length;

上面两个赋值表达式是等价的,但是 length&length的类型是不相同的,只是 length在这里隐式转换成了一个指向 length 函数的函数指针,也就是 &length

  1. 可以通过使用函数指针来调用函数。
    例如:
length("ss", "ss");
// pf 是指向 length 的函数指针
(*pf)("ss", "ss");//推荐第一种
pf("ss", "ss");//这两种方式是等价的
  1. 在指向不同函数类型的指针间不存在转换规则。但是可以用 nullptr或者值为 0 的整型常量表达式给所有类型的函数指针赋值,表示该指针没有指向任何一个函数。
    例如:
pf = 0;
pf = nullptr;
  1. 当我们使用重载函数时,上下文必须清晰地区分到底选用哪个重载函数。 编译器通过 指针类型 决定选用哪个重载函数,指针类型必须与重载函数中的某一个 精确匹配 (必须是精确匹配)。
  2. 和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。
//隐式声明, s3 是函数类型,它会自动转换成指向函数的指针
void useBigger(const string &s1, const string &s2, 
			   bool s3(const string &, const string &) );
//等价的声明:显示地将形参 s3 定义为指向函数的指针
void useBigger(const string &s1, const string &s2, 
			   bool (*s3)(const string &, const string &) );

调用方法如下,可以直接使用函数名,因为函数名会自动转换成指向函数类型的指针。

// lengthCompare 是对应类型的函数
useBigger(s1, s2, lengthCompare);
  1. 可以使用类型别名简化使用函数指针的代码,同时加深理解上文。
// 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; //等价的类型

decltype 关键字作用于函数名则返回函数类型,此时不会将函数类型自动转换成指针类型。
可以使用以下形式重新声明 useBigger 函数:

// useBigger 的两个等价声明,都使用了类型别名
//第一个函数中编译器自动将 Func2 形参类型转换成 Func2
void useBigger(const string &s1, const string &s2, Func2);
void useBigger(const string &s1, const string &s2, FuncP2);
  1. 和数组类似,虽然不能返回数组类型的值,但是可以返回指向函数类型的指针。 在不使用类型别名的情况下,如果想定义一个返回函数指针的函数,则函数头格式如下: type (*function(parameter_list1)) (parameter_list2);。其中 type 是指针所指向的函数的返回类型, function 是函数名, parameter_list1 是该函数的参数列表, parameter_list2 是指针所指向的函数的参数列表。例如
int (* f1(int)) (int*, int);

函数的声明是从外到内理解:函数名所在括号外有返回类型和参数列表,再加上函数名前有 * 符号证明该函数返回一个函数指针。
也可以使用类型别名简化并理清函数:

using F = int (int*, int); // F 是函数类型
using PF = int (int*, int); // PF 是指针类型
//等价声明
F* f1(int);
PF f1(int);

当然我们还可以 使用尾置返回类型的方式声明一个返回函数指针的函数:

//等价声明
auto f1(int) -> int (*)(int *, int);
  1. 当已知函数返回指针类型指向哪一个函数,则可以使用 decltype 关键字来作用该函数名,但是注意, decltype 取到的是函数类型而非指针类型,所以要在函数名前加上 * 。例如
decltype(useBigger) *f1(int);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值