C++Primer第5版学习笔记(四)

 

C++Primer第5版学习笔记(四)
第六章的重难点内容
 
      你可以点击这里回顾第四/五章的内容
      第六章是和函数有关的知识,函数就是命名了的代码块,可以处理不同的情况,本章内容包括:
 
      1.函数的概念基础,包括函数的定义声明以及函数如何生成值和返回结果。
      2.函数重载,重载可以使函数接受不同种类或者数量不同的参数。
      3.函数指针,指向函数的一类特殊指针。
 
      下面是这一章的知识点:
      
      知识点1:P182,6.1,函数基础(英文版203页)
      在调用函数时,第一步编译器会隐式的定义并初始化它的形参。比如一个函数void f(int a);形参int a会被用户传入的实参初始化,此时形参是实参的一个副本。当有多个形参时,形参对应的实参的求值顺序是不一定的。实参的类型必须和形参的类型一致或能转化为形参声明的类型。
     函数可以返回空值。返回函数指针和数组的特殊函数类型将在之后提到。
 
      知识点2:P185,6.1.1,局部静态对象(英文版206页)
      一个对象的名字有作用域,对象本身也有生命周期。名字的作用域是我们可以通过名字访问对象的的区间。相对的,生命周期是指对象的产生和销毁的过程。
      定义在所有函数外部的变量叫做全局变量,在整个程序的执行过程中一直存在。这种对象在程序启动时被创建,直到程序结束才会被销毁。
      定义在函数体内的对象或者函数的形参都是局部变量。当函数执行路径经过该对象的定义语句时才会自动开始创建该对象,在对应的块结束时,这个对象会被销毁。
      有时候我们有必要使局部变量的生命周期贯穿函数调用及之后的时间,所以我们可以将局部对象定义成static对象,定义语句形如static int a=1;这样我们就可以在程序的别的地方(只要是在这个static对象的作用域内访问它)操作这个局部静态对象。
      在一个程序中多次定义局部静态对象仍然是不被允许的。但是当一个函数里的对象被定义为局部静态对象,多次调用这个函数并不会重置这个局部静态对象的值。它自己会记得上一次被函数调用之后的值并继承这个值,不被第二次函数调用的变量定义初始化,这就是它静态的特性。
 
     知识点3:P186,6.1.2,函数声明(英文版207页)
     函数声明要在使用这个函数之前。规范的形式是通常放在头文件里。函数声明可以不写形参的名字,只写形参的类型。
 
     知识点4:P189,6.2.2,传引用调用(英文版211页)
     当函数的形参是一个引用类型的时候,在使用函数时,这个函数的引用形参就绑定在了传入的实参上,这种函数调用就叫做传引用调用。在函数涉及到一些比较大的类型对象作为参数的时候,通常地我们使用传引用调用,这样就可以避免实参初始化形参带来的拷贝。在C语言里经常传入指针避免拷贝,在C++里,一般使用引用。
     大多数情况下函数只能有一个返回值,因此在我们需要的时候,我们可以传一个引用的参数在函数里面。这样函数体内就可以改变引用的值进而改变函数外部被引用连接的对象的值,从而返回多个数值。
 
     知识点5:P190,6.2.2,const形参和实参(英文版213页)
     函数形参的类型也可以是带const的类型。当我们接受函数的参数是为了完成比较或者判断等操作,而不需要改变参数的值,我们应该使用带const的参数来确保参数不会被更改。另外,const类型的形参能比普通类型的形参接受更多种类的参数。比如void fn(const string&);这个函数,字符串字面值是const char [ ]类型,因此fn这个函数接受字符串字面值。但是如果声明成了void fn(string&);,那么这个函数就没有办法接受字符串字面值(类似:“string”这样的值就是字符串字面值)。另外,带const的形参也接受带底层const的对象。
 
    知识点6:P193,6.2.4,想要传递数组作为参数(英文版215页)
    又是我们想要向函数传递一个数组,但是数组是不可拷贝的,因此我们不能够通过值传递的方式传递一个数组到函数里,另外,如果数组的内容很大,传递数组的每个元素会带来不必要的拷贝。因此正确的做法有以下四种:
    0.以下的方法都基于或类似传递数组的指针。一维数组的指针指向数组的第一个元素。我们可以声明类似void pri(int*);void pri(int []);或者void pri(int [10]);的形式传递一个数组指针到函数。这三者是等价的。传递之后形参的类型都是int *类型。
      但是正因为数组的信息是以指针的形式传递给函数的,所以函数只得到一个地址,并不知道数组的大小,因此也就很容易访问到未定义的内存区域,因此在传递数组指针的基础上,我们可以通过手动标志数组大小等方法保证函数访问的内存是合法的不越界的。因此衍生出以下几种方法。
     1.第一种方法:在数组的末尾加标记。
     这种方法类似于C风格字符串,末尾会自动加'\0'来告诉大家这个字符串结束了。在数组末尾加特殊的标记来使数组不越界是简单易用的方法。
     2.第二种方法:使用标准库规范中的begin和end函数。
     头文件iterator里有针对数组的begin和end函数,返回数组的首指针和尾后指针,指针指向数组元素的类型,这种方法也可以检测越界。
     3.第三种方法:传递一个表示数组大小的参数。
      这样构建函数时就知道数组有多大了。
     4.第四种方法:传递数组的引用。
      除了使用指针,我们还可以使用引用来得到一个完整数组的引用(别名)。
      声明格式类似下面这种:void fx(int (&arr)[10]);这里形参的名字是arr,arr前面的&符号代表它是引用类型,引用了一个实参数组,这个数组必须只有10个元素(因为arr后面的[10]也是构成引用声明的必要部分。)
     5.传递多维数组:有时我们也需要向一个函数传递多维数组。多维数组的实质是数组的数组,一维数组的名是指向数组元素的指针,二维数组是指向数组元素的指针的指针。因此想要一个函数传递多维数组的形参声明如下:
void fx(int (*arr)[10]);这时arr指向有10个int型元素的数组。当我们把arr+1,它就又指向了新的10个元素,因此arr相当于二维数组的数组名(两者都是指向包含的一维数组首元素的指针)。
      也可以用int arr[][10]代替int (*arr)[10],因为它们是等价的,都是二维数组名。用int arr[][10]这种方式定义形参时,要标出除了第一个维度以外的每个维度。(假设有一个数组int b[2][3],就说明b有两列,每列3个元素,这里的2就是第一个维度。指向一维数组的指针不关心在这个维度上有几个元素,因此忽略)。
 
    知识点7:P196,6.2.5,main函数的命令行选项(英文版218页)
    最开始我们使用UNIX或LINUX系统编程时经常使用没有图形界面的编译器来把写好的代码编译成obj文件,这时候我们使用命令行来编译一份源代码文件,我们需要在终端里输入类似“prog -d -o oflie data0”的命令行来进行命令行控制。
     现在我们看到的main函数一般都是int main(),括号里面什么也不写,我们也可以给main传递上述的那个命令行参数。形如:int main(int argc,char *argv[])  main可以什么参数也不接受,也可以接受一个int和一个指向字符串的指针这两个参数。main没有第三种形式了。
   int argc是表示后面的argv一共指向几个字符串用的。char *argv[] 里面的每一个字符串都顺序对应着命令行的参数。这些参数的字符串数组的第一个元素应该是可执行文件的名字或者空参数,最后一个字符串的值必须为0。有关命令行的更多选项和argv参数的具体用法,可以参照对应的编译器文档。
 
   知识点7:P197,6.2.6,含有可变形参的函数(英文版220页)
   到现在我们定义的函数都是固定参数的,但是有时候我们无法预知向函数传递几个参数,又想使用一个函数接受这种变化,我们就可以使用C++指定的两种方法来定义含有可变形参的函数。
   第一种方法是当参数个数不一定,但是参数类型都相同时,我们可以传递一个initializer_list参数。这是标准库设施中的一部分。
   第二种方法在当我们想传递不确定个数的不同类型的实参时要使用的技术:可变参数模板。这个16章才介绍。
   其实还有一种方法使函数接受多种形参,不过这种方法多用于和C语言旧代码对接时使用。这个方法用省略符来传递可变数量的形参。
    
     知识点8:P197,6.2.6,initializer_list (英文版220页)
    下面是关于定义可变形参函数的第一种方法——initializer_list参数的介绍:
     initializer_list类似vector,是一种容器,接纳一种同样类型的元素。initializer_list定义在<initializer_list >中,我们可以把任意数量,同样类型的参数传递给这个容器使函数能够处理多个元素。
     initializer_list支持的操作包括:
     initializer_list<容器内元素的类型名> 容器名   //默认初始化一个 initializer_list空容器
     initializer_list<容器内元素的类型名> 容器名{元素值1,元素值2,元素值2...}   //大括号初始化
     initializer_list 容器名1(已被定义的initializer_list 容器的容器名2 //使容器1的内容和容器二一致,两个容器共享容器二里面的元素。不会形成拷贝,(也可以用initializer_list 容器名1=initializer_list 容器名2
     initializer_list 容器名.size()      //元素数量
     initializer_list 容器名.begin()   //指向首元素的迭代器
     initializer_list 容器名.end()      //尾后迭代器
     initializer_list里面元素的值永远是常量不能被更改。
    当我们声明一个接受 initializer_list类型的函数 void fa(initializer_list<int> list1);的时候,我们需要使用大括号来调用这个函数,形如fa({2,3,4});这样我们就向initializer_list传递了一个值的序列。
    我们也可以声明void fa(string b,initializer_list<int> list1);这种函数。
 
    
     知识点9:P199,6.3,省略符形参 (英文版222页)
    下面是关于定义可变形参函数的第二种方法——省略符形参的介绍
     省略符形参有下列两种形式:
     void foo ( parm_list , ...); 
     void foo ( ... );
     第一种形式为特定数目的形参提供了声明。在这种情况下,当函数被调用时,对于与显示声明的形参相对应的实参进行类型检查,而对于与省略符对应的实参则暂停类型检查。在第一种形式中,形参声明后面的逗号是可选的。如果没有逗号,相应地,就变成了第二种情况。
      你可以传递任意数量的参数给省略符形参。要注意省略号的优先级别最低,所以在函数解析时,只有当其它所有的函数都无法调用时,编译器才会考虑调用省略号函数的。 
      首先,如果要用省略符的方式处理不定参数的函数要包含头文件:#include <stdarg.h> (C语言中)或者#include <cstdarg>(C++中)。 然后利用va_list类型和va_start、va_arg、va_end 3个宏读取传递到函数中的参数值。用省略符处理不定参数的函数基于C语言的方法,在C++中不建议使用。(使用了C语言标准库功能varargs)。
 
     知识点10:P202,6.3,返回值(英文版226页)
     在void返回值的语句最后会隐式地有return;语句,这时函数什么也不返回。函数可以返回一个引用作为左值。也可以返回一个花括号括起来的列表,来初始化vector等类型。main函数的return语句可以不写,编译器会带为隐式补充。
    
     知识点11:P205,6.3.3,返回数组指针(英文版229页)
     虽然我们不能直接让函数返回一个数组,但是我们可以设定函数返回一个指针的类型。函数会返回数组的指针。返回数组指针的函数定义语句如下所示:数组元素类型 (*函数名 (参数列表))[ 数组大小 ]当然,返回一个临时量或者局部对象的引用/指针都是错误的行为,如果你在函数里普通地定义了一个数组,那么这个数组的生命周期在函数返回时就结束了,会被内存中释放,因此可能需要用static使这个数组静态。静态对象只是延长了对象的生命周期,但是无论如何在函数内部定义的对象在外部都无法访问,除非使用返回指针的方法。
     一条double (*func(int a))[10]这种语句来说明函数接受一个int a形参并且返回一个带有10个元素的double数组,这种语句在写法上比较乱,因此C++提供了尾置返回的方法让程序员不必要非要迁就编译器的理解能力,上一条语句等价于这样:auto func(int a)->double(*)[10]   我们使用->符号把返回值类型的描述放在了参数列表后面并和函数声明分离开让函数看起来不那么乱。
      我们也可以使用decltype语句返回数组指针。decltype后面的括号可以括起一个现有的数组推导数组类型。
我们再手动加*得到数组指针类型的返回值。在已有int  a[10];的情况下,我们可以使用decltype(a) *fn(int b)这种形式定义一个返回指向数组的指针的返回值类型。
 
     知识点11:P207,6.4,函数重载(英文版233页)
     我们可以定义一组功能类似,函数名一致,但是接受的参数类型或数量不同的函数。定义多个这种函数就叫做函数重载。函数重载可以提供给我们用一个函数名处理多种参数形式的情况。
     定义重载函数要能重传入的参数里区别出实质不同的重载函数,如函数A的定义为int fa(const int a);和函数B int fa(int b);这两个函数函数名一样,形参类型不同,但仍然无法作为重载函数。因为我们传入一个int值时,fa不知道应该执行第一种还是第二种。所以只有参数顶层const属性不同的几个函数不是重载函数。
     当然,对于底层const,比如参数列表为const int *a的函数和参数列表为int a的函数能被看出不同,因为对于一个传入的const常量指针,这个实参只能初始化const int *a,不能被初始化int a。当同时有这两种形式的重载函数时,当传入一个非常量,IDE会优先选择为它匹配形参为int a版本的普通变量形参函数。
 
     知识点12:P209,6.4,const_cast和函数(英文版234页)
     这里主要介绍const_cast类型强制转换是如何在函数中被使用的。在第四章第一次接触const_cast的时候我们提到过这个常被用于函数里。这里我们就看看怎么使用。
     之前说过,向函数传递参数时最好传递const型参数使其能够接受多种参数,这里我们可以在函数体内使用const再把参数变回普通的变量,这样就可以返回一个非const值了。
 
 
      
      知识点13:P210,6.4,重载和作用域(英文版234页)
     声明变量时,变量的作用域就在块里,声明函数也一样,而且里层的作用域会隐藏外部的作用域。例子如下:
    {
        int a=0;    //这里是a的外层作用域
        {
              double a=1.2;   //外边已经有a了,这里又声明了一个a,因此这个a的作用域覆盖了前面的int a;
              cout<<a<<endl;      //输出的会是1.2
        }
     }
     函数声明也一样,
    {
        int fa(int b);    //这里是函数fa的外层作用域
        double fa(double b);    //重载了函数fa使它能够接受double
        {
              double fa(string & c);  //外边已经有fa了,这里又声明了一个fa,因此这个fa的作用域覆盖了前面的;
              fa(2.3);    //错误,原型为double fa(double b)的函数声明作用域被double fa(string & c);覆盖,匹配不到函数
        }
     }
 
     知识点14:P211,6.5.1,默认实参(英文版236页)
     有时候一些函数我们每次调用它总会向它传递一些特殊的值。我们可以声明带有默认实参的函数。默认实参如果没有明确说明,默认实参会被自动当做函数的初始值传递进去。
     形如int fn(int a,int b=2,double c=3.3)这样定义函数头的方式就给了b和c默认的实参,注意,当一个形参被给了默认实参,它后面的所有参数都要有默认实参才行。
     当我们想使用默认实参的时候,只要调用函数的时候使用这种对应的实参就行了,默认实参会用来填补缺少的尾部实参,上面的定义的函数如果这么调用:fn(1,2);double c的值会被自动设为3.3。书写这种函数时要尽量保证要经常用到的默认实参放在参数列表的更后面一点,这样才合理。
     可以只在函数声明里标注默认实参不在函数定义里这样写,结果仍然将是正确的。void fn(int = 1, int = 2, int =3);这种函数声明语句省略了形参的名字,不过也是可以的。
     局部变量不能做默认实参,默认实参的定义在函数体之外。另外,默认实参是可以在名字的作用域内通过名字更改的。
 
     知识点15:P213,6.5.2,内联函数(英文版238页)
     有时我们要频繁调用一个优化规模小,流程直接,频繁被调用的函数,定义函数时我们可以在返回值类型前面加上关键字inline使它成为内联函数,减少运行时的开销。
 
     知识点16:P214,6.5.2,constexpr函数(英文版239页)
     这是一种能够被用在常量表达式的函数,但是函数的返回值类型和形参类型必须都是字面值。函数体中必须有且只有一条return语句,constexpr函数被隐式的指定为内连函数。const函数中也可以有类型别名,使用作用域声明等不执行操作的其他语句。这里没有赋值,没有构建对象。同时constexpr可以返回计算后的结果。如constexpr int fn(int a){return a+22;},这条定义是正确的,前提是调用函数这个函数fn时,传入的实参是一个常量。比如fn(3);
 
     知识点17:P215,6.5.3,调试帮助(英文版240页)
     程序员在写程序时可能涉及到一些调试中的代码,这些代码只在开发程序时使用,当即将发布程序的时候,要暂时屏蔽掉正在调试中的代码。C++提供了assert和NDEBUG两个预处理功能屏蔽测试代码。
    assert这个宏定义在cassert头文件中,assert使用一个表达式作为它的条件,形如assert(expr);首先对expr或者表达式求值,如果结果为真(非0),那么assert什么都不做。如果结果为假(表达式值为0),那么assert输出信息并且终止程序的执行。
    assert经常用于处理不能发生的条件,如果你写了一段代码,代码没测试越界,你就可以用assert,当它越界了我们就结束程序的执行。
    NDEBUG宏定义可以影响assert的行为,这个默认是没被定义的。当我们宏定义了NDEBUG,就屏蔽掉了assert的功能。
    此外,IDE还提供了__FILE__(这里是两个英文下划线,这个存放文件名) 、__func__(这个存放所在的函数名)  __LINE__(这个存放所在的行数)  __TIME__(这个存放调试的时间)  __DATE__(这个存放调试的日期)  这五种静态数组来提供错误信息。
 
    知识点18:P217,6.6,函数匹配(英文版242页)
     程序员定义重载函数之后就可以使用它们了,挑选到底使用哪个版本的函数是一个过程,这个过程叫做函数匹配。
      1、函数匹配的第一步是在调用时先找与与调用函数同名的函数名。且调用点在函数作用域内。这一步筛选出的函数叫做候选函数。
      2、函数匹配的第二步是从候选函数中选择出能够被本次函数调用的实参传入的函数,函数名一致的前提下还要求函数的形参个数和实参一致,实参能够转化成(或者就是)形参规定的类型。这一步筛选出的函数叫做可行函数。
      3、寻找最佳匹配。当有int fn(int a);int fn(double a,double b=1.0)时,我们调用函数fn形如fn(3.4);显然这两种函数都是可行函数,这是我们再寻找最佳的匹配,因为fn(3.4);对应fn(double,double=1.0);的话无需转化,因此是最佳匹配。当有多个最佳匹配的时候函数将停止调用。
 
      为了划分最佳匹配的各种情况,编译器将实参类型到形参类型的转换划分为几个等级,具体排序如下所示:
      1.精确匹配:
       精确匹配可以包含以下情况:数组名转化成数组指针的匹配,函数类型转换成函数指针的匹配,实参类型与形参类型相同。另外,像实参添加顶层const或者忽略实参赋值给形参的顶层const也属于精确匹配。
      2.通过指针的转换把非常量指针转换成常量指针。
      3.通过类型提升实现的匹配。
      4.通过算数类型转换或指针转换实现的匹配
      5.通过类类型转换实现匹配(类类型转换还没有讲)
      要注意小整数字面值会被自动转换成int,而带小数点的字面值会被默认转换成doube。
 
    知识点19:P221,6.7,函数指针(英文版248页)
    声明一条函数指针的语句如下: int (*PtrOfFunc)(参数列表),其中PtrOfFunc就是指向函数的指针。我们可以把函数名赋值给定义的函数指针的名字。
    返回函数指针的形参定义为 double(*fn(int a)) (int d,char b);这里声明的函数是fn,函数的形参是int a,返回值是函数指针类型的,返回的函数指针对应的函数的返回类型是double,参数是int d,char b
     和处理数组一样,我们也可以使用尾置返回来返回一个函数指针,尾置返回函数指针的声明是auto fn(int a)->double (*)(int d,char b);尾置返回适合用来返回复杂的类型比如数组,函数指针等等。
    遇double(*fn(int a)) (int d,char b);这种复杂的表达式,应该以定义的变量名为中心,从里往外一层层往外扩展。这个函数的定义语句里面,fn就是其中的变量名,看它右侧,有(int a),这(int a)是一个形参列表。因此得出结论fn的本质是一个函数,再看左侧,*代表这个函数返回一个指针,这个指针的类型在更外层(double (*) (int d,char b))型。当然这种声明/定义容易让人心累,所以这种情况下使用auto fn(int a)->double (*)(int d,char b)是不错的选择。如果这样还是觉得太长了,可以使用typdef,USING等重命名语句加上decltype推导。比如tpyedef double func (int d,char b);这样的语句之后,func就是一个函数类型。
也可以使用tpyedef decltype(fn) func2;这条语句等价于上面的语句。对于using语句,using Func2 = double  (int d,char b);即可。可见typedef和using的替换原则是不同的,在涉及到复杂类型的时候,类似数组,函数指针,tpyedef的替换名要和被替换的类型一起被声明。
posted @ 2016-02-16 09:28  诱兔  阅读(773)  评论(0编辑  收藏  举报