《C++ Primer》读书笔记—第六章 函数

声明:

  • 文中内容收集整理自《C++ Primer 中文版 (第5版)》,版权归原书所有。
  • 学习一门程序设计语言最好的方法就是练习编程

一、函数基础

1、一个典型的函数定义包括以下内容:返回类型(return type)、函数名字、0个或者多个形参(parameter)组成的列表以及函数体。

2、求n的阶乘,即1-n所有数字的乘积。如5的阶乘是120.

 1 #include<iostream>
 2 
 3 using namespace std;
 4 
 5 int fact(int val)
 6 {
 7     int ret = 1;  //局部变量,用于存储计算结果
 8     while(val>1){
 9         ret *= val--;   //把ret和val的乘积赋值给ret,然后val-1
10     }
11     return ret;
12 }
13 int main()
14 {
15     int j = fact(5);  //返回fact(5)的结果
16     cout<<j<<endl;
17     return 0;
18 }

3、函数的调用完成两个工作:一是实参初始化函数对应的形参,二是将控制权转移给被调用函数。此时,主调函数执行被暂时中断,被调函数开始执行。

4、return语句完成两个工作:一是返回return语句中的值,二是将控制权从被调函数转移回主调函数。

5、实参是形参的初始值。实参的类型必须与形参相匹配。

6、函数的形参列表可以为空,但不能省略。形参列表中的每个形参都是含有一个声明符的声明。即使两个形参类型一样,也不能省略。  

  int fact (int f1, int f2){...}   //不能省略形参类型

7、任意两个形参不能同名,而且函数最外层作用域中的局部变量也不能使用与函数形参一样的名字。

8、函数的返回类型不能是数组类型或者函数类型,但可以是指向数组或函数的指针。

9、形参和函数体内部定义的变量统称为局部变量(local variable),仅在函数的作用域内部课件,同时局部变量还隐藏在外层作用域中同名的其他声明之外。

10、自动对象:只存在于块执行期间的对象称为自动对象。

  形参是一种自动对象。函数开始时为形参申请存储空间,因为形参定义于函数体作用域内,所以一旦函数终止,则形参被销毁。

  我们用传递给函数的实参初始化形参对应的自动对象。对于局部变量对应的自动对象来说,分两种:

    如果变量定义本身含有初始值,则用这个初始值作初始化。

    如果变量定义本身不含初始化,则执行默认初始化,这意味着内置类型的未初始化局部变量将产生未定义的值。

11、若要令局部变量的生命周期贯穿函数调用及以后的时间,则可以将局部变量定义为static类型。即局部静态对象

  如:下面的函数统计自己被执行多少次。将输出1-10的数字。

 1 #include<iostream>
 2 
 3 using namespace std;
 4 
 5 size_t count_calls()
 6 {
 7     static size_t ctr = 0; //调用结束后这个值仍然有效
 8     return ++ctr;
 9 }
10 
11 int main(){
12 
13     for(size_t i = 0;i != 10;++i){
14         cout<< count_calls()<<endl;
15     }
16 return 0;
17 }

12、函数只能定义一次,但可以声明多次。函数声明不需要函数体,用一个分号代替即可。应在头文件中声明函数而在源文件中定义函数。

二、参数传递

1、形参的类型决定了形参和实参的交互方式。如果形参是引用类型,将绑定在实参上,否则,将实参的值拷贝后赋值给形参。

  当形参是引用类型时,它对应的实参被引用传递(passed by reference)或者函数被传引用调用(call by reference)。引用形参也是绑定的对象或者别名。

  当实参是拷贝给形参时,形参和实参是两个互相独立的对象,这样的实参被称为值传递(passed by value)或者传值调用(called by value)。

2、指针形参:当执行指针拷贝时,拷贝的是指针的值,拷贝后是两个不同的指针。因此指针使我们可以间接的访问他所指的对象,所以通过指针可以修改它所指的对象的值。

 1 int n = 0,i = 42;
 2 int *p = &n, *q = &i; // p指向n,q指向i
 3 *p = 42; //n值改变而p不变
 4 p = q; // p指向了i ,但i和n的值都不变
 5 
 6 //该函数接受一个指针,然后指针所指的位置是0
 7 void reset(int *p)
 8 {
 9    *p = 0; //改变指针p所指对象的值
10     p = 0;  // 只改变了p的局部拷贝,实参未变
11 }
12 
13 //调用reset函数之后,实参所指的对象被置为0,但实参本身并没有变
14 int i  = 42;
15 reset(&i);  //改变i的值而非改变i的地址
16 cout<<"i =  "<<i<<endl; //输出i=0

3、建议使用引用类型的形参代替指针。

4、引用操作实际上是作用在引用所引的对象上。

5、可以使用引用避免拷贝

  这个函数比较两个string的长度,因为string比较长,所以应尽量避免直接拷贝它们。比较长度无须改变string对象的内容,所以把形参定义成对常量的引用。

1 bool isShorter(const string &s1,const string &s2){
2     return s1.size()<s2.size;
3 }

  当函数无须修改引用形参的值时最好使用常量引用。(const)。

6、定义一个find_char函数,返回在string对象中某个指定字符第一次出现的位置,同时也返回该字符出现的总次数。

 1 string::size_type find_char(const string &s,char c,string::size_type &occurs)
 2 {
 3     auto ret = s.size();//第一次出现的位置
 4     occurs = 0;         //表示出现次数的形参
 5     for (decltype (ret) i = 0;i != s.size(); ++i){
 6         if(s[i] = c){
 7             if(ret == s.size())
 8                 ret = i;  //记录第一次出现c的位置
 9             +=occurs;   //出现次数+1
10         }
11     }
12     return ret;   //出现次数通过occurs隐式返回
13 }

7、允许定义若干具有相同名字的函数,不过前提是不同函数的形参列表应该有明显区别。

8、可以使用一个非常量初始化一个底层const对象,但反过来不行。同时一个普通的引用必须用同类型的对象初始化。

9、如果不需要改变引用形参的值,最好使用常量引用,它能接受的实参类型比普通引用多

  • 普通引用只接受同类型的对象作为初始值

  • 常量引用可以用同类型对象、表达式、字面值初始化

10、为了处理不同数量实参的函数,如果所有实参类型相同,可以传递一个initializer_list的标准库类型。如果实参类型不同,可以编写一种特殊函数,也就是可变参数模板。操作见P198

  initializer_list是一个标准类,表示的是一组花括号包围的类型相同的对象,对象之间以逗号隔开。

三、返回类型和return语句

1、无返回值函数:void,不要求有return,在函数最后一句会隐式执行return。但如果想提前退出也是可以。

例如,一个swap函数,使其在交换值相等的时候什么都不做直接退出。

1 void swap (int &v1, int &v2){
2 
3     if(v1==v2)
4         return;
5     int tmp = v2;
6     v2 = v1;
7     v1 = tmp;
8 }

2、返回一个值的方式和初始化一个变量或形参的方式完全一样

  返回值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。

3、不要返回局部对象的引用或指针。函数完成后,它所占用的存储空间随之被释放,函数终止意味着局部变量的引用将指向不再有效的内存区域。返回局部对象的指针也是错误的。

 1 //严重错误:这个函数试图返回局部对象的引用
 2 const string &manip()  
 3 {  
 4     string ret;  
 5     if( !ret.empty() )  
 6     {  
 7         return ret;  //错误,返回局部对象的引用
 8     }  
 9     else  
10     {  
11         return "Empty";  //错误,empty是一个局部临时量
12     }  
13 }


4、返回值是个引用,所以调用是个左值,和其他左值一样它也能出现在赋值运算符的左侧。如果返回类型是常量引用,则不能给调用的结果赋值。

  如:string s("a value");

    get_val(s,0)='A';//把s[0]的值修改为A    正确

    shorterString("Hi","bye") = 'X';  //错误  返回值是个常量

5、如果一个函数调用了它自身,则不管这个调用是直接还是间接的,都称为递归函数(recursive function)。

  如:递归实现阶乘 

1 //递归实现阶乘
2 int factorial (int val)
3 {
4    if(val>1)
5         return factorial(val-1)*val;
6    return 1;    
7 }

  一定会有个if,保证有一条路径是不包含递归调用的,不然就死循环了。

6、函数可以返回数组的指针或引用。最直接的方法是使用类型别名。

1    typedef int arr[10];  //arr是个类型别名,表示的类型时含有10个整数的数组
2 
3   using arr = int[10]; //arr的等价声明
4 
5   arr* func(int i);  //func返回一个指向含有10个整数的数组的指针

  func 函数接受一个int形参,返回一个指向包含10个整数的数组指针。

1   int arr[10];  //arr是一个含有10个整数的数组
2 
3   int *p1[10];  //p1是一个含有10个指针的数组
4 
5   int (*p2)[10]=&arr;  //p2是一个指针,指向含有10个整数的数组

7、尾置返回类型:

  func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组

  auto func (int i)->int(*)[10];

8、使用decltype。如果知道函数返回的指针指向哪个数组,则使用decltype声明返回类型。

1 int odd[] = { 1, 3 };  
2 int even[] = { 2, 4 };  
3 //返回一个指针,该指针指向含有5个整数的数组
4 decltype(odd) *arrPtr( int i )  
5 {  
6     return (i%2)? &odd : &even; //返回一个指向数组的指针 
7 }

  arrPtr使用关键字decltype表示它的返回类型是一个指针,并且该指针所指的对象与odd类型一样。因为odd是个数组,所以arrPtr返回一个指向含有5个整数的数组的指针。且注意:decltype并不负责把数组类型转换成对应的指针,所以decltype的结果是个数组,要想表示arrPtr返回指针还要在函数声明时加一个*。

四、函数重载

1、重载函数:如果同一作用域内几个函数名字相同但形参列表不一样(形参数量或类型不一样),则称之为重载函数(overloaded)。main函数不能重载。

2、const_cast 和重载:

  如果函数的形参和返回值等是底层const,我们可以利用const_cast和重载创建出函数的非const版本。

  在写重载函数时注意修改形参的const,否则无法构成重载。

1 const string &Test( const string &s1 )  
2 {  
3     return s1;  
4 }
5 string &Test( string &s1 )  
6 {  
7     auto &sResult = Test( const_cast<const string&>( s1 ) );  
8     return const_cast<string &>( sResult );  
9 }

3、函数匹配(function matching)即重载确定(overload resolution),在这个过程中我们把函数调用和一组重载函数中某一个关联起来。编译器首先把调用的实参与重载集合中每一个函数的形参比较,然后根据比较结果决定到底调用哪个函数。

4、重载对作用域的一般性质并没有改变,如果我们在内层作用域声明名字,他将隐藏外层作用域中声明的同名实体。

五、特殊用途语言特性

1、某些函数中的一些形参,在函数的多次调用中它们被赋予了一个相同的值,此时,这个相同的值被称为函数的默认实参(default argument)。调用含有默认实参的函数时,可以包含该实参也可以省略之。

2、默认实参作为形参的初始值出现在形参列表中。我们可以为一个或多个形参定义的默认值,不过,一旦某个形参被赋予了默认值,则它后面的形参都必须有默认值。

3、要使用默认实参时,只要在调用时省略该实参就行了。调用时实参按照位置解析,默认实参负责填补函数调用缺少的某部分实参。

4、局部变量不能作为默认实参。

5、用作实参的名字在函数声明所在的作用域内解析。

   求值过程发生在函数调用时。

 1 int wd = 80;  
 2 char def = ' ';  
 3 int ht();  
 4 string screen( int width = ht(), int height = wd, char title = def );  
 5 
 6 void f2()  
 7 {  
 8     def = '*';  
 9     //def改变了默认实参的值  
10     int wd = 100;  
11     //wd隐藏了外层定义的wd,但没有改变默认值  
12     string window = screen();  
13 }

  (1)screen只会找同一作用域内的变量作为实参,所以后来定义的局部变量wd根本是不可见的。

  (2)默认实参只是代替了实参,但是初始化形参的时机没有变,还是发生在函数调用时。因此改变全局变量def的值后,默认实参也发生了改变。

6、一次函数的调用包含一系列工作:

  • 调用前要先保存寄存器,并在返回时恢复
  • 可能要拷贝实参
  • 程序转向一个新的位置继续执行

  内联函数可以避免函数调用的开销。

  将函数指定为内联函数(inline),通常就是将它在每个调用点上“内联地”展开。

1 //内联版本:寻找两个string对象中较短的那个
2 inline const string & 
3 shorterString(const string &s1,const string s2)
4 {
5    return s1.size() <= s2.size() ? s1 : s2;  
6 }

  内联机制用于优化规模较小、流程直接、频繁调用的函数。

7、constexpr函数:指能用于常量表达式的函数。定义的方法遵循:函数的返回类型及所有形参的类型都是字面值类型,而且函数体中必须有且只有一条return语句。

  constexpr函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行。

  constexpr函数不一定返回常量表达式。

  constexpr函数被隐式地声明成内联函数。

  对于某个给定的内联函数或者constexpr函数,可以多次定义,但多个定义必须完全一致。通常定义在头文件中。

8、assert是个预处理宏,作用于一条表示条件的表达式。当未定义预处理变量NDEBUG时,assert对条件求值。如果条件为假,返回一个错误信息并终止当前程序的执行。

六、函数匹配

1、函数匹配过程:

  (1)找出调用点可见的所有同名函数作为候选函数,候选函数两个特征:一是与被调函数同名,而是其声明在调用点可见。

  (2)考察本次调用提供的实参,然后选出能被这组实参调用的函数,成为可行函数,两个特点:一是其形参数量与本次调用提供的实参数量相同,二是每个实参的类型与对应的形参类型相同或者可转换成形参类型。

  (3)寻找最佳匹配。精确匹配比需要类型转换的匹配更好。 

2、调用重载函数时应尽量避免强制类型转换。

3、前面提到实参和形参的类型越接近,匹配的越好。有以下的排序规则

  (1)精确匹配

    实参和形参类型相同

    实参从数组或函数类型转换成对应的指针

    向实参添加顶层const或从实参删除顶层const

  (2)通过const转换实现的匹配

  (3)通过类型提升实现的匹配

  (4)通过算术类型转换或指针转换实现的匹配

  (5)通过类类型转换实现的匹配

七、函数指针

1、函数指针指向的是函数,是某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。

1 bool lengthCompare( const string&, const string& );
2 //该函数的类型是
3 
4 bool ( const string&, const string& )
5 //那么可以用下面的形式声明函数指针
6 
7 bool (*pf)( const string&, const string& );
8 //即使用指针替换函数名即可

2、使用重载函数指针时,上下文必须清晰地界定到底该选择哪个函数。指针类型必须与重载函数中某一个精确匹配。

3、尾置返回类型声明一个返回函数指针的函数:

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

4、如果我们明确知道返回的函数是哪个,则使用decltype简化书写函数指针返回类型的过程。例如,有两个函数,返回类型都是string::size_type,并且各有两个const string&类型的形参,此时我们可以写第三个函数,它接受一个string类型的参数,返回一个指针,该指针指向前两个函数中的一个。

  string::size_type sumLength(const string&, const string&);

  string::size_type largeLength(const string&, const string&);

  //根据其形参的取值,getFcn函数返回指向sumLength或者largeLength的指针

  decltype(sumLength) *getFcn(const string &);

  牢记:当我们将decltype作用于某个函数时,它返回函数类型而非指针类型。因此,必须显式地加上*表明我们需要返回指针。

  3.1室友早上说,师兄好牛逼,大神好厉害,总是能carry,项目比赛一看就会,一顿讲架构,一顿撸代码,自己就是躺赢的,什么都不会的小菜鸡,吧啦吧啦。其实不用太在意别人怎么样,做好自己的事。每天的任务完成了,每天都有进展,就行了。也不用刻意的赶时间要求几点到实验室几点走,没什么卵用。每天认真看书的时间也就那么几个小时,坐着抠手机的话在哪里都一样。还有室友所谓的开会,半年的项目一点进展都没有,从老师到学生谁都不做,怎么能有进展。老师天天就知道说要学习新知识,学习新技能,然而,雷声大雨点小。笑死。玩蛇皮去吧。第六章花费时间比较长了,后面的章节应该越来越难。且随疾风前行。

  

原文地址:https://www.cnblogs.com/zlz099/p/6489031.html