C++函数(一):C++函数参数必知必会

34 阅读6分钟

函数定义与声明

  • 与变量类似,函数只能定义一次,但是可以声明多次
  • 函数声明无函数体,用一个分号替代即可

    需要函数返回类型、函数名和形参类型

    [返回类型] [函数名]([形参列表]);

  • 函数定义就是在函数声明基础上加上函数体

     [返回类型] [函数名]([形参列表]) {
       [函数体]
     }
    

函数参数传递

每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化

形参初始化的机理和变量初始化一样。理解这句话,就理解了C++传参的本质

值传递

实参的值会拷贝给形参,形参和实参是两个相互独立的对象

  • 基本类型形参在函数调用时,实参都是值传递
  • 指针形参

    执行指针的拷贝时,拷贝的是指针的值。拷贝后,两个指针是不同的指针。但是通过指针间接访问的对象是同一个

     //函数接收一个指针
     void reset(int *ip) {
       //改编指针ip所指向对象的值
       *ip = 0;
       //改变ip的局部拷贝,实参实际并未修改
       ip = 0;
     }
    

引用传递

当形参是引用类型时,对应的实参被引用传递

和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名

那么改变形参的值也就是在改变实参的值

  • 函数接收引用

     //函数接收引用
     void reset(int &i) {
       //改变了i所引用对象的值
       i = 0;
     }
    

    在函数体内改变了形参引用的值,也就是改变了实参引用的值。

  • 拷贝大的类型对象比较低效,甚至有些不支持拷贝操作,这时就只能使用引用传参

  • 函数传出参数,除了使用指针,也可以使用引用

    在C语言中,当有传出参数时,除了可以使用返回值,也可以定义传递指针,在函数内对指针指向的对象赋值

    在C++中,也可以通过引用实现相同的功能,在函数体内改变形参引用的值即可

const形参和实参

  • 基本类型和const

    基本类型只有顶层const,顶层const作用于对象本身

    和变量初始化一样,即常量或者非常量都可以用来初始化顶层const形参,实参初始化形参时会忽略顶层const

     void fcn(const int i) {
       ...
     }
     ​
     const int x = 10;
     int y = 20;
     ​
     //都是正确的
     fcn(x);
     fcn(y);
     ​
     ​
    

    所以函数重载上,区别只有形参是const或者非const。这样是错误的,会编译报错

  • 指针和引用和const

    指针和引用有顶层const和底层const之分

    • 顶层const和基本类型规则相同,重载编译器都会报错

    • 底层const

      1. 底层const是可以重载的,具体调用时的匹配逻辑见函数匹配

        区别在于const形参,函数内是不能修改参数的

      2. 使用常量或者非常量实参调用底层const形参函数都可以

      3. 常量实参不能用来调用非const形参函数

      所以尽量使用常量引用形参

 //引用类型形参
 void reset(int &i) {
 }
 ​
 int i = 0;
 const int &x = i;
 int &y = i;
 ​
 //错误,const实参不能初始化非const引用实参
 reset(x);
 ​
 //正确,都是const引用
 reset(y);
 //底层const形参
 void reset(const int &i) {
 ​
 }
 ​
 int i = 0;
 const int &x = i;
 int &y = i;
 ​
 //正确,可以使用非const引用实参初始化const引用形参
 reset(x);
 //正确,都是const引用
 reset(y);
 ​

数组形参

  • 数组形参

    数组两个特殊性质:

    1. 不允许拷贝数组 所以不能使用值传递方式使用数组

    2. 使用数组时通常会将其转换成指针 所以传数组实参时,实际传递的是指向数组首元素的指针

       //这两种形式是等价的
       void print(char[] c){}
       void print(char * c){}
      
  • 因为数组是以指针传递给函数的,那么就需要额外的参数知道数组的长度

    1. 使用标记指定数组的长度

      适用于数组包含一个明确的结束标记且不会和普通数组混淆,比如C语言的字符串,最后存在一个空字符。但对于类似int数组就不适合了

       void print(const char* p) {
         if (p) {
           while(*cp) {
             //操作数组元素
           }
         }
       }
      
    2. 使用标准库规范

      传入需要操作的数组的首指针和尾后指针

       void print(const int *begin, const int  *end) {
         ...
       }
      

      这样可以使用标准库提供的迭代器,begin()和end()获取到首指针和尾后指针

    1. 显示传递数组的大小

      添加一个形参,接收数组的大小,比较常用的做法

       void print(const int *p, const int size) {
         ...
       }
      
  • 数组形参和const

    1. 前面关于引用和指针和const的规则同样适用于const数组
    1. C++还能定义数组的引用

      形式:<const> type (&param) [size];

      两边括号不可少,因为不存在引用数组;

      数组的大小是数组的一部分,所以size不可少,这样就限制了函数的通用性

  • 多维数组

    C++实际没有真正的多维数组,只是数组的数组, 所以多维数组的形参写成:数组类型形参,加上数组的长度

     //p指向数组的首元素,一共有rowSize个元素,
     //该数组的每个元素都是由10个整数构成的数组
     void f (const int (*p)[10], int rowSize) {}
     ​
     等价于
     //编译器也会忽略掉第一个维度
     void f(const int p, int rowSize) {}
    

可变形参

  • 使用initializer_list标准库类型

    initializer_list提供的操作.png

    编写可以处理不同数量实参的函数。但是实参的类型可要一致

    initializer_list也是一种模版类型,定义时,必须带上元素类型

     initializer_list<string> ls;
    
    1. initializer_list对象中的元素永远是常量值,无法改变元素的值
    1. initializer_list也可以使用迭代器访问元素
     //输出错误信息
     void error_msg(initializer_list<string> li)
     {
     //begin()指向首元素,end()指向尾后元素
         for (auto begin = li.begin(); begin != li.end(); ++begin)
         {
             cout<<*begin<<" ";
         }
         cout<<endl;
     }
     ​
     //调用,元素必须是常量
     error_msg({"funX","error","xxx"});
     error_msg({"funX","ok"});
    
  • 使用省略符类型,一般用于与C函数交互的接口

    形参中使用...省略符,在函数内部使用varargs访问可变参数

     //省略符只能出现在形参列表的最后位置
     void foo(params,...);
     void foo(...);
    

    省略符形参应该仅仅用于C和C++通用的类型。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝