[C++ Primer] 函数

函数

函数基础

  1. 函数是一个命名了的代码块,一个典型的函数定义包括以下部分:返回类型、函数名字、由0个或多个形参组成的列表以及函数体。

  2. 函数的调用完成两项工作:一是实参初始化形参,二是将控制权转移给被调用函数。

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

  4. 局部静态对象:将局部变量定义成static类型

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

    若局部静态变量没有显示的初始值,将执行值初始化,内置类型局部静态变量初始化为0。

  5. 分离式编译:允许将程序分割到几个文件中去,每个文件独立编译。若修改了其中一个源文件,只需重新编译改动的文件。大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名是.obj(Windows).o(UNIX)的文件,后缀名的含义是该文件包含对象代码(object code)

参数传递

  1. 当形参是引用类型时,对应的实参被引用传递或者函数被传引用调用。当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。这样的实参被值传递或者函数被传值调用

  2. 指针形参:执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。

  3. 使用引用避免拷贝:拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型)不支持拷贝操作。

  4. 如果函数无须改变引用类型的值,最好将其声明为常量引用。

    // 比较两个string对象的长度
    bool isSHorter(const string &s1, const string &s2)
    {
        return s1.size() < s2.size();
    }
    
  5. 使用引用形参返回额外信息

    // 返回s中c第一次出现的位置索引
    // 引用形参occurs负责统计c出现的总次数
    string::size_type find_char(const string &s, char c, string::size_type &occurs)
    {
        // 具体实现
    }
    
  6. 管理指针形参的三种常用技术:

    • 使用标记指定数组长度

      要求数组本身包含一个结束标记,典型示例是C风格字符串,C风格字符串存储在字符数组中,并且在最后一个字符后面跟着一个空字符。

    • 使用标准库规范:传递指向数组首元素和尾后元素的指针

      void print(const int *beg, const int *end)
      {
          // 输出beg到end之间(不含end)的所有元素
          while(beg != end)
              cout << *beg++ << endl;	// 输出当前元素并将指针向前移动一个位置
      }
      
      int j[2] = {0,1};
      print(begin(j),end(j));
      
    • 显示传递一个表示数组大小的形参

      // const int ia[] 等价于const int* ia
      // size表示数组的大小,将其显式地传给函数用于控制对ia元素的访问
      void print(const int ia[], size_t size)
      {
          for(size_t i = 0; i != size; ++i){
              cout << ia[i] << endl;
          }
      }
      
      int j[] = {0,1};
      print(j,end(j) - begin(j));
      
  7. main:处理命令行选项
    假定main函数位于可执行文件prog之内,可以向程序传递下面的选项:

    prog -d -o ofile data0
    

    这些命令行选项通过两个(可选的)形参传递给main函数:

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

    第二个形参是一个数组,它的元素是指向C风格字符串的指针;第一个形参argc表示数组中字符串的数量。因为第二个形参是数组,所以main函数也可以写成:

    int main(int argc, char **argv){...}	// argv指向char*
    

    实参传递给main函数之后,argv第一个元素指向程序的名字或者空字符串,接下来的元素依次传递命令行提供的实参,最后一个指针之后的元素值保证为0。以上面提供的命令行为例,argc应该等于5,argv应该包含如下的C风格字符串:

    argv[0] = "prog";	// 或者argv[0]也可以指向一个空字符串""
    argv[1] = "-d";
    argv[2] = "-o";
    argv[3] = "ofile";
    argv[4] = "data0 ";
    argv[5] = 0;
    

    使用argv实参时,记得可选的实参从argv[1]开始;argv[0]保存程序的名字,而非用户输入。

  8. 含有可变形参的函数

    • initializer_list标准库类型

      函数实参数量未知但是类型相同,也是一种模板类型。initialize_list对象中的元素永远是常量值,无法改变对象中元素的值。

      void error_msg(initializer_list<string> il)
      {
          for(auto beg = il.begin(); beg != il.end(); ++beg )
              cout << *beg << " ";
          cout << endl;
      }
      
    • 可变参数模板

    • 省略符

      使用了varargs的C标准库

返回类型和return语句

  1. 返回的值用于初始化函数调用点的临时量。

  2. 声明一个返回数组指针的函数

    函数形式如下:

    Type (*function(parameter_list))[deimension]
    

    Type表示元素的类型,demision表示数组大小。(*function(parameter_list))两端的括号必须存在,如果没有这对括号,函数返回类型将是指针的数组。

    具体例子:

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

    可以按照以下的顺序来逐层理解该声明的含义:

    • func(int i)表示调用func函数时需要int类型实参。
    • (*func(int i))意味可以对函数调用的结果执行解引用操作。
    • (*func(int i))[10]表示解引用func的调用将得到一个大小为10的数组。
    • int (*func(int i))[10]表示数组中的元素是int类型。
  3. 尾置返回类型

    任何函数的定义都能用尾置返回,这种形式对于返回类型比较复杂的函数最有效。尾置返回类型跟在形参列表后面并以一个->符号开头。为了表示函数真正的返回类型跟在形参列表之后,在本应该出现返回类型的地方放置一个auto:

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

函数重载

特殊用途语言特性

默认实参

  1. 一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
  2. 当设计含有默认实参的函数时,合理设置形参顺序,尽量让不怎么使用默认值的形参出现在前面,让经常使用默认值的的形参出现在后面。

内联函数和constexpr函数

  1. 内联函数可避免函数调用的开销,在返回类型前加上关键字inline,就可以将其声明为内联函数。
inline const string &shorterString(const string &s1, const string &s2)
{
    return s1.size()<=s2.size() ? s1 : s2;
}

内联函数只是向编译器发出一个内联展开的请求,编译器可以选择忽略这个请求。

  1. constexpr函数

    函数的返回类型及所有形参类型都得是字面值类型,函数体中必须有且仅有一条return语句。

调试帮助

  1. assert预处理宏

    assert(expr);
    

    对expr求值,若表达式为假,assert输出信息并终止程序的执行。如果表达式为真,assert什么也不做。

  2. NDEBUG预处理变量

    assert的行为依赖于NDEBUG的预处理状态。若定义了NDEBUG,则assert什么也不做。

    定义NDEBUG能避免检查各种条件所需的运行时开销,当然此时根本就不会执行运行时检查。

    除了用于assert之外,也可以使用NDEBUG编写自己的条件调试代码。如果NDEBUG未定义,将执行#ifndef#endif之间的代码;如果定义了NDEBUG,这些代码将被忽略掉:

    void print(const int ia[], size_t size)
    {
        #ifndef NDEBUG
        // __func__是编译器定义的一个局部静态变量,用于存放函数的名字
        cerr << __func__ << ": array size is " << size <<endl;
        #endif
        //...
    }
    

    变量__func__输出当前调试的函数的名字。它是const char的静态数组。

    预处理器还定义了另外4个对于程序调试很有用的名字:

    __FILE__	// 存放文件名的字符串字面值
    __LINE__	// 存放当前行号的整型字面值
    __TIME__	// 存放文件编译时间的字符串字面值
    __DATE__	// 存放文件编译日期的字符串字面值
    

函数匹配

为了确定最佳匹配,编译器将实参类型到形参类型的转换分成几个等级,具体排序如下:

  1. 精确匹配,包括以下情况:
    • 实参类型和形参类型相同
    • 实参从数组类型或函数类型转换成对应的指针类型
    • 向实参添加顶层const或者从实参中删除顶层const
  2. 通过const转换实现的匹配
  3. 通过类型提升实现的匹配
  4. 通过算术类型转换或指针转换实现的匹配
  5. 通过类类型转换实现的匹配

函数指针

  1. 函数指针指向的是函数而非对象。函数指针指向某种特定类型,函数的类型由它的返回类型和形参类型哦共同决定,与函数名无关。例如:

    // 比较两个string对象的长度
    bool lengthCompare(const string &, const string &);
    

    该函数的类型是bool(const string &, const string &),该函数的指针只需用指针替换函数名即可:

    // pf指向一个函数
    bool (*pf)(const string &, const string &);
    
posted @ 2024-06-18 22:10  Invinc-Z  阅读(33)  评论(0编辑  收藏  举报