zoukankan      html  css  js  c++  java
  • 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);

  • 相关阅读:
    You Don't Know JS: Async & Performance(第2章,Callbacks)
    You Don't Know JS: this & Object Prototypes (第6章 Behavior Delegation)附加的ES6 class未读
    C#抓取网页内容
    输出jq对象
    ASP.NET MVC 常用内置验证特性
    MVC不错的学习资料
    让Entity Framework启动不再效验__MigrationHistory表
    多线程
    递归
    序列化
  • 原文地址:https://www.cnblogs.com/junjie_x/p/7608964.html
Copyright © 2011-2022 走看看