Tips for C++ Primer Chapter 6 函数

第6章 函数

函数基础

局部静态对象(local static object)

在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。

如果局部静态变量未被显式初始化,它将执行值初始化(内置类型的局部静态变量初始化为0)

 

函数声明

函数声明也称作函数原型(function prototype)

函数声明没有函数体,用一个分号表示声明语句结束。

函数声明无须形参的名字(也可以写上形参的名字)。

int f(int n); //ok

int f(int); //ok

 

在头文件中进行函数声明

函数应该在头文件中声明,而在源文件中定义;

含有函数声明的头文件应该被包含到定义函数的源文件中。

 

参数传递

const形参和实参

形参有顶层const时,传给它常量对象或非常量对象都是可以的。换句话说,形参的顶层const被忽略掉了

void f(const int i) { /*f能读取i不能向i写值*/}

void f(int i) { /*可以向i写值*/}

以上两个函数定义似乎是有差异的,实际上若二者同时存在属于重复定义。因为顶层const被忽略掉了,传入这两个函数的参数可以完全一样。

 

补充:

int calc(char* a, char* b) { /**/ }

int calc(char* const a, char* const b) { /**/ }

以上函数属于重复定义,因为这里的const是顶层const,形参的顶层const被忽略了,两个函数的所有参数类型都是char*。

 

补充:

int calc(char* a, char* b) { /**/ }

int calc(const char* a, const char* b) { /**/ }

以上函数可以同时定义,因为这里的const是底层const。

以上函数在函数匹配时不会发生二义性调用,如果形参是指向常量的指针,调用后者;如果实参是指向非常量的指针,虽然调用二者均可行,编译器会调用前者以达到精确匹配。

 

扩展:

void f(...) const {...}

void f(...) {...}

它们被认为是两个不同的函数,因为此处的const是函数签名的一部分。

 

数组形参

回顾数组的两个性质:

  不允许拷贝数组;

  使用数组时(通常)会将其转换成指针。

尽管不能以值传递的方式传递整个数组,但是可以把形参写成类似数组的形式。

  void print(const int*);

  void print(const int[]);

  void print(const in[10]); //这里的维度表示我们期望数组含有多少元素,实际不一定

以上三者完全等价,每个函数的唯一形参都是const int*类型。编译器处理对print函数的调用时,只检查传入的参数是否const int*类型。

例如:

  int i = 0, a[2] = {0,1};

  print(&i); //合法;&i的类型是int*

  print(a); //合法;a转换成int*并指向a[0]

 

数组引用形参

形参也可以是数组的引用,引用形参绑定到对应的实参上,也就是绑定到数组上。

  void print(int (&arr)[10]) {/**/}

但是,这一用法也无形中限制了print函数的可用性:我们只能将函数作用于大小为10的数组。

(通过函数模板,可以实现给引用类型的形参传递任意大小的数组,后面将会讨论)

 

PS:&arr两端的括号不能少

  void print(int &arr[10]) //非法;试图将arr声明成“引用的数组”;但是不存在引用的数组,数组元素应该是对象

  void print(int (&arr)[10]) //合法;arr是一个“含有10个整数的整型数组”的引用

 

传递多维数组

回顾:多维数组其实是数组的数组。将多维数组传递给函数,真正传递的是指向数组首元素的指针,而首元素本身就是一个数组,所以这个指针就是一个指向数组的指针。

数组第二维(以及后面的所有维度)的大小都是数组类型的一部分 ,不能省略。

  void print(int (*matrix)[10], int rowSize) { /**/ }

  //matrix是指向“含有10个整数的数组”的指针

 

PS:*matrix两端的括号必不可少

  void print(int *matrix[10], int rowSize) { /**/ }

  //合法(但不是我们所要的);matrix是“10个指向整数的指针”构成的数组

 

另一种等价定义方式(以二维数组为例):

  void print(int matrix[][10], int rowSize) { /**/ }

  //matrix的声明看起来是一个二维数组,实际上形参是一个指向“含有10个整数的数组”的指针(编译器会忽略掉第一个维度)。

 

main:处理命令行选项

  int main(int argc, char *argv[]) { ... }

  int main(int argc, char **argv) { ... } //上一条语句的等价写法;argv是一个指针,指向char*类型的对象

第二个形参是一个数组,它的元素是指向C风格字符串的指针

第一个形参argc表示数组中字符串的数量

当实参传给main函数之后,argv[0]指向程序的名字或一个空字符串(argv[0]的值由系统设定的,而非用户输入);

从argv[1]开始,保存用户输入的可选实参

最后一个指针之后的元素值保证为0(这也是由系统设定的);

 

例如:

假定main函数位于可执行文件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;

 

含有可变形参的函数

(1)initializer_list形参

适用情况:函数的实参数量未知,但是全部实参的类型都相同。

initializer_list是一种标准库类型,它定义在同名的头文件中;initializer_list是一种模板类型。

initializer_list对象中的元素永远是常量值。

如果要向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内。

  void f(initializer_list lst) { ... }

  f({"string1", s2, s3}); //PS:s2和s3是string对象

initializer_list提供的操作:

  initializer_list<T> lst;  默认初始化;T类型元素的空列表

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

  lst2(lst)  lst2 = lst  拷贝构造、拷贝赋值;(实际上,不会拷贝列表中的元素,拷贝后,原始列表和副本共享元素)

  lst.size()

  lst.begin()

  lst.end()

 

(2)省略符形参

省略符形参仅仅用于C和C++通用的类型。特别要注意:大多数类类型的对象在传递给省略符形参时都无法正确拷贝。

省略符形参只能出现在形参列表的最后一个位置。

  void f(parm_list, ...); //此处的逗号是可选的

  void f(...);

省略符形参对应的实参无需类型检查。

 

(3)函数模板:可变参数模板

这里先不讨论。

 

返回类型和return语句

对于有返回值的函数,如果函数体内不包含return语句,有的编译器可能会检测到这个错误,有的也许不会

如果编译器没有发现这个错误,则运行时的行为是未定义的。(返回值不可预知)

 

不要返回局部对象的引用或指针

函数完成后。它所占用的存储空间也随之被释放掉。函数终止意味着局部变量的引用或指针将指向不再有效的内存区域。

注意:返回局部静态对象的引用或指针是安全的。

 

列表初始化返回值

C++11允许函数返回花括号包围的值的列表。

vector<string> f(string s)
{
  if(s.empty())
    return {}; //合法;返回一个空vector对象
  else
    return {"string1", "string2"}; //合法;返回列表初始化的vector对象
}

注意:如果函数是基本内置类型,也同样允许使用列表初始化返回值;但是,花括号包围的列表最多包含一个值,而且所占空间不应大于目标类型的空间。

int f() { return {} }; //合法;返回0

int f() { return {1} }; //合法

int f() { return {1.1} }; //非法;返回1;编译器给出warning(但程序还是能运行,并执行了隐式类型转换)

 

返回数组指针

(1)使用类型别名简化返回数组的指针或引用:

  typedef int arrT[10]; //arrT是一个类型别名,它表示的类型是“含有10个整数的数组”

  using arrT = int[10]; //上一条语句的等价写法

  arrT* func(int i); //func返回一个“指向含有10个整数的数组”指针

 

(2)如果不使用类型别名,则返回数组指针的函数形式如下:

  Type (*function(parameter_list)) [dimension]

注:(*function(parameter_list))两端的括号必须存在,否则,函数的返回类型将是“元素是指针的数组”。

 

以具体例子来理解该声明的含义:

  int (*func(int i)) [10];

可按以下顺序来理解:

  func(int i) 表示调用函数时需要一个int类型的实参;

  (*func(int i)) 意味着我们可以对函数的调用结果执行解引用操作(或者说:意味着函数的调用结果是一个某种类型的指针);

  (*func(int i)) [10] 表示解引用func的调用将得到一个大小是10的数组

  int (*func(int i)) [10] 表示数组中的元素是int类型

 

(3)使用尾置返回类型

C++支持尾置返回类型(trailing return type)。

任何函数都能使用尾置返回,但是尾置返回对于较复杂的函数返回类型最有效。

上例子:

  auto func(int i) -> int (*) [10]; //func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组。

注:尾置返回类型跟在形参列表后,并以一个->符号开头;而在本应出现返回类型的地方放置一个auto。

 

(4)使用decltype

当我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型。

例如,下面的函数返回一个指针,该指针根据参数i的不同指向两个已知数组中的某一个。

int odd[] = {1,3,5};

int even[] = {2,4,6};

decltype(odd) *arrPtr(int i)

{

  return (i%2) ? &odd : &even; //返回一个指向数组的指针

}

arrPtr返回一个“指向含有3个整数的数组”指针

注意:之前讨论过,decltype并不会把数组类型转换成对应的指针,所以decltype(odd)的结果表示的是数组;要想表示arrPtr返回的是指针,则还必须在函数声明时加一个*。

 

函数重载

函数重载:同一作用域内;函数名相同;形参列表不同。

不允许两个函数除了返回类型外其它所有要素都相同。

 

重载和const形参

顶层const不影响传入函数的对象;一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。

  Record lookup(Phone);

  Record lookup(const Phone); //重复定义

 

如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象,可以实现函数重载;此时const是底层的。

  Record lookup(Account&); //函数作用于Account的引用

  Record lookup(const Account&); //新函数;作用于常量引用

  Record lookup(Account*); //新函数;作用于指向Account的指针

  Record lookup(const Account*); //新函数;作用于指向常量的指针

因为const不能转换成非常量,所以只能把const对象(或指向const的指针)传递给const形参;

因为非常量可以转换成const,所以以上4个函数都能作用于非常量对象(或指向非常量对象的指针)。(不过,编译器会优先选择非常量版本的函数)

 

特殊用途语言特性

默认实参

一旦某个形参被赋予了默认值,它后面的所有形参都必须指定默认值。

默认实参负责填补函数调用缺少的尾部实参

 

用作默认实参的名字在函数声明所在的作用域内解析,而这些名字的求值过程发生在函数调用时

 1 int i = 10; //A
 2 char c = '.'; //B
 3 
 4 void f(int sz = i, char ch = c)
 5 {
 6   cout<< sz << " " << ch <<endl;
 7 }
 8 
 9 int main()
10 {
11   c = '*'; //C
12   int i = 20; //D
13   f(); //使用默认实参
14   return 0;
15 }

输出结果是: 10 *

注解:调用f时,使用的i是函数声明所在的作用域内的那个i(A处);使用的c的值是函数调用时c的最新值(B处的变量c,C处的最新值'*')。

虽然我们意图声明一个局部变量i(D处)用于隐藏外层的i(A处),但是该局部变量与传递给f的默认实参没有任何联系。

 

内联函数和constexpr函数

内联函数:函数将在每个调用点上“内联地”展开,从而避免了函数调用的开销。(函数调用开销:包括保存和恢复寄存器、拷贝实参等。)

注:在函数的返回类型前加上inline关键字,以建议编译器将其做成内联函数,编译器可以忽略这个请求。

 

constexpr函数:能用于常量表达式的函数。

定义constexpr函数的约定:函数的返回类型及所有形参的类型都得是字面值类型(参见第2章Tips);函数体中有且仅有一条return语句。

constexpr函数体内也可以包含其它语句,但这些语句应该在运行时不执行任何操作。(例如:空语句、类型别名、using声明等)

constexpr函数并不一定返回常量表达式(我们允许其返回值并非一个常量):

  注解:对func(arg),当实参是常量表达式时,它的返回结果也是常量表达式,反之不然。

  constexpr int f(int cnt) { return f2() * cnt; }

  f(10); //10是字面值,是常量表达式,故返回值也是常量表达式

  int i = 10;

  f(i); //i是一个非常量表达式,则返回值是一个非常量表达式

PS:编译器会把对constexpr函数的调用替换成其结果值,为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数

 

PS:与其它函数不同,内联函数和constexpr函数可在程序中多次定义;不过,对某个给定的内联函数或constexpr函数来说,它的多个定义必须完全一致

基于此原因,内联函数和constexpr函数通常定义头文件中。

 

预处理时的调试:assert和NDEBUG

assert:是一种预处理宏,定义在cassert头文件中。

  assert(expr);

assert以一个表达式作为它的条件,如果表达式为,assert输出信息并终止程序,如果表达式为,assert什么也不做

 

NDEBUG预处理变量:assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做

默认情况下没有定义NDEBUG,此时assert将执行运行时检查。(#define NEBUG则关闭调试状态)

 

函数指针

函数指针指向的是函数,而非对象。

函数指针指向某种特定类型。

函数的类型由它的返回类型和形参类型共同决定,而与函数名无关

例如函数:

  bool compare(const string &, const string &);

该函数的类型是:

  bool (const string &, const string &)

要想声明一个指向该函数的指针,只需要用指针替换函数名

  bool (*pf)(const string &, const string &); //注意:指针未初始化

  //pf指向一个函数,该函数的参数是两个const string的引用,该函数的返回值是bool类型

 

可以这样理解指针pf的声明语句:

pf前面有个*,说明pf是指针;

右侧是形参列表(参数是两个const string的引用),表示pf指向的是函数;

再观察左侧,发现函数的返回类型是bool;

综上,pf是一个指向函数的指针,该函数的参数是两个const string的引用,该函数数的返回值是bool类型。

 

PS:*pf两端的括号必不可少。假若不写这对括号:

  bool *pf(const string &, const string &); //声明一个名为pf的函数,该函数返回bool*

 

使用函数指针

当我们把函数名作为一个值使用时,该函数自动地转换成指针。

  pf = compare; //pf指向名为compare的函数

  pf = &compare; //与上一条语句等价:取地址符是可选的

扩展:对数组名,没有取地址符的写法。

例如:

  int a[] = {1,2};

  int *p = a; //ok

  int *p = &a; //非法

  int *p = &a[0]; //ok

 

允许直接使用指向函数的指针调用该函数,无须解引用指针。

  bool b1 = pf("hi", "bye"); //ok

  bool b2 = (*pf)("hi", "bye"); //ok

  bool b3 = compare("hi", "bye"); //ok

  //以上三者等价

 

指向不同函数类型的指针不存在转换规则。

  int sumLength(const string &, const string &);

  bool cstringCompare(const char*, const char*);

  pf = sumLength; //错误;返回类型不匹配

  pf = cstringCompare; //错误;形参类型不匹配

  pf = compare; //ok

  pf = 0; //ok;表示指针没有指向任何一个函数

 

函数指针形参

和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针使用。

  void useBigger(const string &s1, const string &s2, bool pf(const string &, cosnt string &));

  //第三个形参是函数类型,它会自动地转换成指向函数的指针

  void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, cosnt string &));

  //等价的声明;显式地将形参定义成指向函数的指针

可以直接把函数作为形参使用,此时它会自动转换成指针:

  useBigger(s1, s2, compare);

 

使用类型别名和decltype简化使用了函数指针的代码

  //Func和Func2是某种函数类型

  typedef bool Func(const string &, const string &);

  typedef decltype(compare) Func2; //等价的类型

  //FuncP和FuncP2是指向函数的指针

  typedef bool (*FuncP)(const string &, const string &);

  typedef decltype(compare) *FuncP2; //等价的类型

注:与decltype使用数组类似,decltype使用函数也不会将函数转换成指针。因为decltype返回函数类型,所以再加上一个*才能得到指向函数的指针。

此时前述useBigger可声明如下:

  void useBigger(const string &s1, const string &s2, Func); //或Func2;编译器会自动地将Func表示的函数类型转换成指针

  void useBigger(const string &s1, const string &s2, FuncP); //或FuncP2

 

返回指向函数的指针

和数组类似,虽然不能返回一个函数,但是可以返回指向函数类型的指针。

(1)使用类型别名:

  using F = int(int*, int); //F是函数类型,不是指针

  using PF = int (*) (int*, int); //PF是指针类型

注意:和函数类型的形参不同,返回类型不会自动地转换成指针,必须显式地将返回类型指定为指针。

  PF f1(int); //正确;PF是指向函数的指针,f1返回指向函数的指针

  F f1(int); //错误;F是函数类型,f1不能返回一个函数

  F *f1(int); //正确;显式地指定返回类型是指向函数的指针

 

(2)使用一般的方法直接声明:

  int (*f1(int)) (int*, int);

注解:由内向外,看到f1有形参列表,所以f1是个函数;

f1前面有*,所以f1返回一个指针;

观察右边发现,指针的类型本身也包含形参列表,因此指针指向函数,再看最左边,知道该函数的返回类型是int。

综上,f1是一个形参类型为一个int,返回类型为“指向int(int*, int)类型的函数的指针”的函数。

或者说,f1是一个形参类型为一个int,返回类型为“int (*) (int*, int)的函数。

 

(3)使用尾置返回类型:

  auto f1(int) -> int (*) (int*, int);

 

posted @ 2017-09-28 22:26  junjie_x  阅读(413)  评论(2编辑  收藏  举报