MyException - 我的异常网
当前位置:我的异常网» C++ » Tips for C++ Primer Chapter 六

Tips for C++ Primer Chapter 六

www.MyException.Cn  网友分享于:2013-10-08  浏览:0次
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);

 

1楼命格无双w
。。。。没啦?
Re: junjie_x
@命格无双w,有的,还没更完呢

文章评论

 程序员的样子
程序员的样子
程序员都该阅读的书
程序员都该阅读的书
为啥Android手机总会越用越慢?
为啥Android手机总会越用越慢?
Web开发者需具备的8个好习惯
Web开发者需具备的8个好习惯
总结2014中国互联网十大段子
总结2014中国互联网十大段子
Java程序员必看电影
Java程序员必看电影
程序员的一天:一寸光阴一寸金
程序员的一天:一寸光阴一寸金
程序员必看的十大电影
程序员必看的十大电影
为什么程序员都是夜猫子
为什么程序员都是夜猫子
程序员的鄙视链
程序员的鄙视链
十大编程算法助程序员走上高手之路
十大编程算法助程序员走上高手之路
科技史上最臭名昭著的13大罪犯
科技史上最臭名昭著的13大罪犯
代码女神横空出世
代码女神横空出世
那些争议最大的编程观点
那些争议最大的编程观点
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
程序员眼里IE浏览器是什么样的
程序员眼里IE浏览器是什么样的
程序猿的崛起——Growth Hacker
程序猿的崛起——Growth Hacker
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
每天工作4小时的程序员
每天工作4小时的程序员
“肮脏的”IT工作排行榜
“肮脏的”IT工作排行榜
团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
如何成为一名黑客
如何成为一名黑客
我的丈夫是个程序员
我的丈夫是个程序员
当下全球最炙手可热的八位少年创业者
当下全球最炙手可热的八位少年创业者
初级 vs 高级开发者 哪个性价比更高?
初级 vs 高级开发者 哪个性价比更高?
Google伦敦新总部 犹如星级庄园
Google伦敦新总部 犹如星级庄园
程序员应该关注的一些事儿
程序员应该关注的一些事儿
编程语言是女人
编程语言是女人
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
旅行,写作,编程
旅行,写作,编程
5款最佳正则表达式编辑调试器
5款最佳正则表达式编辑调试器
2013年中国软件开发者薪资调查报告
2013年中国软件开发者薪资调查报告
漫画:程序员的工作
漫画:程序员的工作
“懒”出效率是程序员的美德
“懒”出效率是程序员的美德
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
那些性感的让人尖叫的程序员
那些性感的让人尖叫的程序员
老程序员的下场
老程序员的下场
鲜为人知的编程真相
鲜为人知的编程真相
10个调试和排错的小建议
10个调试和排错的小建议
做程序猿的老婆应该注意的一些事情
做程序猿的老婆应该注意的一些事情
亲爱的项目经理,我恨你
亲爱的项目经理,我恨你
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
如何区分一个程序员是“老手“还是“新手“?
如何区分一个程序员是“老手“还是“新手“?
10个帮程序员减压放松的网站
10个帮程序员减压放松的网站
写给自己也写给你 自己到底该何去何从
写给自己也写给你 自己到底该何去何从
程序员最害怕的5件事 你中招了吗?
程序员最害怕的5件事 你中招了吗?
老美怎么看待阿里赴美上市
老美怎么看待阿里赴美上市
Java 与 .NET 的平台发展之争
Java 与 .NET 的平台发展之争
中美印日四国程序员比较
中美印日四国程序员比较
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有