关于函数形参的一些讨论

形参的初始化与变量的初始化一样:如果形参具有非引用类型,则复制实参的值,如果形参为引用类型,则它只是实参的别名。

  • 非引用形参:

普通的非引用类型的参数通过复制对应的实参实现初始化。当用实参副本初始化形参时,函数并没有访问调用所传递的实参本身,因此不会修改实参的值

while循环体虽然修改了v1v2的值,但这些变化仅限于局部参数,而对调用gcd函数使用的实参没有任何影响。于是,如果有函数调用gcd(i, j),ij的值不受gcd内执行的赋值操作的影响。

#include <iostream>
#include <string>

using namespace std;

int gcd(int v1, int v2)
{
    while (v2) {
        int temp = v2;
        v2 = v1 % v2;
        v1 = temp;
    }
    cout << "v1:" << v1 << endl;
    cout << "v2:" << v2 << endl;
    return v1;
}


int main()
{

    int i = 100;
    int j = 3;
    gcd(i, j);
    cout << "i:" << i << endl;
    cout << "j:" << j << endl;
    system("pause");
    return 0;
}

输出结果:

关于函数形参的一些讨论

所以,非引用形参表示对应实参的局部副本。对这类形参的修改仅仅改变了局部副本的值。一旦函数执行结束,这些局部变量的值也就没有了。

  • 指针形参:

函数的形参可以是指针,此时将复制实参指针。与其他非引用类型的形参一样,该类形参的任何改变也仅作用于局部副本。如果函数将新指针赋给形参,主调函数使用的实参指针的值没有改变。但如果对局部副本的指针所指向的对象的值进行修改,则实参指针所指对象的值也会相应的被修改。

如果函数形参是非const类型的指针,则函数可通过指针实现赋值,修改指针所指向对象的值:

void reset(int *ip)
{
    *ip = 0; // changes the value of the object to which ip points
    ip = 0;   // changes only the local value of ip; the argument is unchanged
    cout << "local ip: " << ip << endl;
}



int main()
{

    int i = 42;
    int *p = &i;
    cout << "i: " << *p << '\n';   // prints i: 42
    cout << "p: " << p << endl;
    reset(p);                      // changes *p but not p
    cout << "i: " << *p << endl;   // ok: prints i: 0
    cout << "p: " << p << endl;//并没有改动

    system("pause");
    return 0;
}

关于函数形参的一些讨论

如果保护指针指向的值,则形参需定义为指向const对象的指针:

关于函数形参的一些讨论

指针形参是指向const类型还是非const类型,将影响函数调用所使用的实参。我们既可以用int也可以用const int类型的实参调用use_ptr函数;但仅能将int类型的实参传递给reset函数。这个差别来源于指针的初始化规则——可以将指向const对象的指针初始化为指向非const对象的指针,但不可以让指向非const对象的指针初始化为指向const对象的指针*。

  • const形参:

在调用函数时,如果该函数使用非引用的非const形参,则既可给该函数传递const实参也可传递非const的实参。例如,可以传递两个intconst对象调用gcd

int gcd(int v1, int v2)
{
    while (v2) {
        int temp = v2;
        v2 = v1 % v2;
        v1 = temp;
    }
    return v1;
}


int main()
{

    const int i = 3, j = 6;
    int k = gcd(3, 6);   

    system("pause");
    return 0;
}

这种行为源于const对象的标准初始化规则。因为初始化复制了初始化时的对象,仅仅是要用那个值,即便修改也是对拷贝后的对象进行修改,而不对原对象进行修改,所以可用const对象初始化非const对象,反之亦然。

如果将形参定义为非引用的const类型:

void fcn(const int i) {

}

则在函数中,不可以改变实参的局部副本。由于实参仍然是以副本的形式传递,因此传递给fcn的既可以是const对象也可以是非const对象。

尽管函数的形参是const,但是编译器却将fcn的定义视为其形码被声明为普通的int

void fcn(const int i) {  }
void fcn(int i) {  }

关于函数形参的一些讨论

这种用法是为了支持对 C 语言的兼容,因为在 C 语言中,具有const形参或非const形参的函数并无区别。

  • 复制实参的局限性:

当需要在函数中修改实参的值时。

当需要以大型对象作为实参传递时。对实际的应用而言,复制对象所付出的时间和存储空间代价往往过大。

当没有办法实现对象的复制时。

  • 引用形参

考虑下面不适宜复制实参的例子,该函数希望交换两个实参的值:

void swap(int v1, int v2)
     {
         int tmp = v2;
         v2 = v1;    
         v1 = tmp;
     }

这个例子期望改变实参本身的值。但对于上述的函数定义,swap无法影响实参本身。执行swap时,只交换了其实参的局部副本,而传递swap的实参并没有修改:

int main()
{
    int i = 10;
    int j = 20;
    cout << "Before swap():\ti: "
        << i << "\tj: " << j << endl;
    swap(i, j);
    cout << "After swap():\ti: "
        << i << "\tj: " << j << endl;
    system("pause");
    return 0;
}

输出结果:

关于函数形参的一些讨论

为了使swap函数以期望的方式工作,交换实参的值,需要将形参定义为引用类型:

void swap(int &v1, int &v2)
{
    int tmp = v2;
    v2 = v1;
    v1 = tmp;
}


int main()
{
    int i = 10;
    int j = 20;
    cout << "Before swap():\ti: "
        << i << "\tj: " << j << endl;
    swap(i, j);
    cout << "After swap():\ti: "
        << i << "\tj: " << j << endl;
    system("pause");
    return 0;
}

输出结果:

关于函数形参的一些讨论

与所有引用一样,引用形参直接关联到其所绑定的实参,而并非这些对象的副本。定义引用时,必须用与该引用绑定的对象初始化该引用。引用形参完全以相同的方式工作。每次调用函数,引用形参被创建并与相应实参关联。此时,当调用swap(i, j); 形参v1只是对象i的另一个名字,而v2则是对象j的另一个名字。对v1的任何修改实际上也是对i的修改。同样地,v2上的任何修改实际上也是对j的修改。

从 C 语言背景转到 C++ 的程序员习惯通过传递指针来实现对实参的访问。在 C++ 中,使用引用形参则更安全和更自然。

  • 使用引用形参返回额外的信息

引用形参的另一种用法是向主调函数返回额外的结果。函数只能返回单个值,但有些时候,函数有不止一个的内容需要返回。

如何定义既返回一个迭代器又返回出现次数的函数?我们可以定义一种包含一个迭代器和一个计数器的新类型。而更简便的解决方案是给find_val传递一个额外的引用实参,用于返回出现次数的统计结果:

vector<int>::const_iterator find_val(
    vector<int>::const_iterator beg,             
    vector<int>::const_iterator end,             
    int value,                                   
    vector<int>::size_type &occurs)             
{
    vector<int>::const_iterator res_iter = end;
    occurs = 0; 
    for (; beg != end; ++beg)
        if (*beg == value) {
            if (res_iter == end)
                res_iter = beg;
            ++occurs; 
        }
    return res_iter;  
}

int main()
{
    vector<int> ivec = { 1, 42, 3, 42, 5, 6, 7, 8 };
    vector<int>::size_type ctr = 0;
    vector<int>::const_iterator it = find_val(ivec.begin(), ivec.end(), 42, ctr);
    cout << ctr << endl;

    system("pause");
    return 0;
}

输出结果:

关于函数形参的一些讨论

  • 利用const引用避免复制

在向函数传递大型对象时,需要使用引用形参,这是引用形参适用的另一种情况。虽然复制实参对于内置数据类型的对象或者规模较小的类类型对象来说没有什么问题,但是对于大部分的类类型或者大型数组,它的效率(通常)太低了;此外,某些类类型是无法复制的。使用引用形参,函数可以直接访问实参对象,而无须复制它。编写一个比较两个string对象长度的函数作为例子。这个函数需要访问每个string对象的size,但不必修改这些对象。由于string对象可能相当长,所以我们希望避免复制操作。使用const引用就可避免复制:

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

其每一个形参都是const string类型的引用。因为形参是引用,所以不复制实参。又因为形参是const引用,所以isShorter函数不能使用该引用来修改实参如果使用引用形参的唯一目的是避免复制实参,则应将形参定义为const引用

  • 更灵活的指向const的引用

如果函数具有普通的非const引用形参,则显然不能通过const对象进行调用。毕竟,此时函数可以修改传递进来的对象,这样就违背了实参的const特性。

int incr(int &val)
{
    return ++val;
}

int main()
{
    const int v2 = 42;
    int v3 = incr(v2);   // error: v1 is not an int
}

会报错:

关于函数形参的一些讨论

但比较容易忽略的是,调用这样的函数时,传递一个右值或具有需要转换的类型的对象同样是不允许的

int main()
{
    short v1 = 0;
    const int v2 = 42;

    int v3 = incr(v1);   // error: v1 is not an int
     v3 = incr(0);        // error: literals are not lvalues
     v3 = incr(v1 + v2);  // error: addition doesn't yield an lvalue
     int v4 = incr(v3);   // ok: v3 is a non const object type int
    system("pause");
    return 0;
}

会提示错误:

关于函数形参的一些讨论

问题的关键是非const引用形参只能与完全同类型的非const对象关联

应该将不修改相应实参的形参定义为const引用。如果将这样的形参定义为非const引用,则毫无必要地限制了该函数的使用。例如,可编写下面的程序在一个string对象中查找一个指定的字符:

string::size_type find_char(string &s, char c)
{
    string::size_type i = 0;
    while (i != s.size() && s[i] != c)
        ++i;                   // not found, look at next character
    return i;
}

这个函数将其string类型的实参当作普通(非const)的引用,尽管函数并没有修改这个形参的值。这样的定义带来的问题是不能通过字符串字面值来调用这个函数:

if (find_char("Hello World", 'o'))

虽然字符串字面值可以转换为string对象,但上述调用仍然会导致编译失败:

关于函数形参的一些讨论

继续将这个问题延伸下去会发现,即使程序本身没有const对象,而且只使用string对象(而并非字符串字面值或产生string对象的表达式)调用find_char函数,编译阶段的问题依然会出现。例如,可能有另一个函数is_sentence调用find_char来判断一个string对象是否是句子:

bool is_sentence (const string &s)
     {
          return (find_char(s, '.') == s.size() - 1);
     }

如上代码,函数is_sentencefind_char的调用是一个编译错误。传递进is_sentence的形参是指向const string对象的引用,不能将这种类型的参数传递给find_char,因为后者期待得到一个指向非const string对象的引用。

应该将不需要修改的引用形参定义为const引用。普通的非const引用形参在使用时不太灵活。这样的形参既不能用const对象初始化,也不能用字面值或产生右值的表达式实参初始化

  • 传递指向指针的引用:

假设我们想编写一个与前面交换两个整数的swap类似的函数,实现两个指针的交换。已知需用*定义指针,用&定义引用。现在,问题在于如何将这两个操作符结合起来以获得指向指针的引用。这里给出一个例子:

void ptrswap(int *&v1, int *&v2)
{
    int *tmp = v2;
    v2 = v1;
    v1 = tmp;
}

形参int *&v1的定义应从右至左理解:v1是一个引用,与指向int型对象的指针相关联。也就是说,v1只是传递进ptrswap函数的任意指针的别名。调用ptrswap交换分别指向值 10 和 20 的指针:

int main()
{
    int i = 10;
    int j = 20;
    int *pi = &i;  // pi points to i
    int *pj = &j; // pj points to j
    cout << "Before ptrswap():\t*pi: "
        << *pi << "\t*pj: " << *pj << endl;
    ptrswap(pi, pj); // now pi points to j; pj points to i
    cout << "After ptrswap():\t*pi: "
        << *pi << "\t*pj: " << *pj << endl;
    system("pause");
    return 0;
}

输出结果:

关于函数形参的一些讨论

即指针的值被交换了。在调用ptrswap时,pi指向i,而pj则指向j。在ptrswap函数中,指针被交换,使得调用ptrswap结束后,pi指向了原来pj所指向的对象。换句话说,现在pi指向j,而pj则指向了i

  • vector和其它类型容器的形参

通常,函数不应该有vector或其他标准库容器类型的形参。调用含有普通的非引用vector形参的函数将会复制vector的每一个元素。从避免复制vector的角度出发,应考虑将形参声明为引用类型。然而,事实上,C++ 程序员倾向于通过传递指向容器中需要处理的元素的迭代器来传递容器

void print(vector<int>::const_iterator beg,
                vector<int>::const_iterator end)
     {
         while (beg != end) {
             cout << *beg++;
             if (beg != end) cout << " "; // no space after last element
         }
         cout << endl;
     }

这个函数将输出从beg指向的元素开始到end指向的元素(不含)为止的范围内所有的元素。除了最后一个元素外,每个元素后面都输出一个空格。

  • 数组形参

数组有两个特殊的性质,影响我们定义和使用作用在数组上的函数:一是不能复制数组;二是使用数组名字时,数组名会自动转化为指向其第一个元素的指针。因为数组不能复制,所以无法编写使用数组类型形参的函数。因为数组会被自动转化为指针,所以处理数组的函数通常通过操纵指向数组指向数组中的元素的指针来处理数组。

如果要编写一个函数,输出int型数组的内容,可用下面三种方式指定数组形参:

void printValues(int*) { /* ... */ }
void printValues(int[]) { /* ... */ }
void printValues(int[10]) { /* ... */ }

虽然不能直接传递数组,但是函数的形参可以写成数组的形式。虽然形参表示方式不同,但可将使用数组语法定义的形参看作指向数组元素类型的指针。上面的三种定义是等价的,形参类型都是int*

形参的长度会引起误会。编译器忽略为任何数组形参指定的长度。根据数组长度(权且这样说),可将函数printValues编写为:

void printValues(const int ia[10])
     {
          for (size_t i = 0; i != 10; ++i)
          {
              cout << ia[i] << endl;
          }
     }

尽管上述代码假定所传递的数组至少含有 10 个元素,但 C++ 语言没有任何机制强制实现这个假设。下面的调用都是合法的:

int main()
     {
         int i = 0, j[2] = {0, 1};
         printValues(&i);      // ok: &i is int*; probable run-time error
         printValues(j);      // ok: j is converted to pointer to 0th
                              // element; argument has type int*;
                              // probable run-time error
         return 0;
     }

虽然编译没有问题,但是这两个调用都是错误的,可能导致运行失败。在这两个调用中,由于函数printValues假设传递进来的数组至少含有 10 个元素,因此造成数组内在的越界访问。程序的执行可能产生错误的输出,也可能崩溃,这取决于越界访问的内存中恰好存储的数值是什么。

当编译器检查数组形参关联的实参时,它只会检查实参是不是指针、指针的类型和数组元素的类型时是否匹配,而不会检查数组的长度

和其他类型一样,数组形参可定义为引用或非引用类型。大部分情况下,数组以普通的非引用类型传递,此时数组会悄悄地转换为指针。一般来说,非引用类型的形参会初始化为其相应实参的副本。而在传递数组时,实参是指向数组第一个元素的指针,形参复制的是这个指针的值,而不是数组元素本身。函数操纵的是指针的副本,因此不会修改实参指针的值。然而,函数可通过该指针改变它所指向的数组元素的值。通过指针形参做的任何改变都在修改数组元素本身。不需要修改数组形参的元素时,函数应该将形参定义为指向const对象的指针:

void f(const int*) { /* ... */ }
  • 通过引用传递数组

和其他类型一样,数组形参可声明为数组的引用。如果形参是数组的引用,编译器不会将数组实参转化为指针,而是传递数组的引用本身。在这种情况下,数组大小成为形参和实参类型的一部分。编译器检查数组的实参的大小与形参的大小是否匹配

void printValues(int(&arr)[10]) 
{

}
int main()
{
    int i = 0, j[2] = { 0, 1 };
    int k[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    printValues(&i); // error: argument is not an array of 10 ints
    printValues(j);  // error: argument is not an array of 10 ints
    printValues(k);  // ok: argument is an array of 10 ints
    system("pause");
    return 0;
}

报错:

关于函数形参的一些讨论

这个版本的printValues函数只严格地接受含有 10 个int型数值的数组,这限制了哪些数组可以传递。

  • 多维数组的传递

所谓多维数组实际是指数组的数组。和其他数组一样,多维数组以指向 0 号元素的指针方式传递。多维数组的元素本身就是数组。除了第一维以外的所有维的长度都是元素类型的一部分,必须明确指定

void printValues(int (matrix*)[10], int rowSize);

上面的语句将matrix声明为指向含有 10 个int型元素的数组的指针//如果传递的数组时int a[10],那么形参应该是void printValues(int locala[10], int rowSize);,locala就是指向int类型的指针。传递数组,形参类型就是数组元素的指针类型。因为int a[10]是整型数组,所以形参是整形指针,因为这里要传递的是数组的数组,所以形参应该是数组的指针

我们也可以用数组语法定义多维数组。与一维数组一样,编译器忽略第一维的长度,所以最好不要把它包括在形参表内:

void printValues(int matrix[][10], int rowSize);

这条语句把matrix声明为二维数组的形式。实际上,形参是一个指针,指向数组的数组中的元素。数组中的每个元素本身就是含有 10 个int型对象的数组。

举例:

int matrix[3][10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
        11, 12, 13, 14, 15, 16, 17, 18, 18, 20,
        21, 22, 23, 24, 25, 26, 27, 28, 29, 30 };

其实二维数组matrix中的数据是现行排放的,而matrix则是首元素的地址:

关于函数形参的一些讨论

关于函数形参的一些讨论

非引用数组形参的类型检查只是确保实参是和数组元素具有同样类型的指针,而不会检查实参实际上是否指向指定大小的数组。任何处理数组的程序都要确保程序停留在数组的边界内。

有三种常见的编程技巧确保函数的操作不超出数组实参的边界。第一种方法是在数组本身放置一个标记来检测数组的结束。C 风格字符串就是采用这种方法的一个例子,它是一种字符数组,并且以空字符 null 作为结束的标记。处理 C 风格字符串的程序就是使用这个标记停止数组元素的处理。

第二种方法是传递指向数组第一个和最后一个元素的下一个位置的指针。如:

void printValues(const int *beg, const int *end)
     {
         while (beg != end) {
             cout << *beg++ << endl;
          }
     }
     int main()
     {
         int j[2] = {0, 1};
         // ok: j is converted to pointer to 0th element in j
         //     j + 2 refers one past the end of j
         printValues(j, j + 2);
         return 0;
     }

第三种方法是将第二个形参定义为表示数组的大小,这种用法在 C 程序和标准化之前的 C++ 程序中十分普遍。

  • main处理命令行选项

传统上,主函数的实参是可选的,用来确定程序要执行的操作。比如,假设我们的主函数main位于名为prog的可执行文件中,可如下将实参选项传递给程序:

prog -d -o ofile data0

这种用法的处理方法实际上是在主函数main中定义了两个形参:

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

第二个形参argv是一个 C 风格字符串数组。第一个形参argc则用于传递该数组中字符串的个数。由于第二个参数是一个数组,主函数main也可以这样定义:

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

表示argv是指向char的指针。//形参是char,其原型可以是char类型的数组,argv是char类型数组的指针,那么原型可以是char类型数组的数组。简化思考,可以发现,参数中有几个“”,传递的就可以是几维数组。*

以前面的命令行为例,argc应设为 5,argv会保存下面几个 C 风格字符串:

argv[0] = "prog";
     argv[1] = "-d";
     argv[2] = "-o";
     argv[3] = "ofile";
     argv[4] = "data0";
  • 含有可变形参的函数

在无法列举出传递给函数的所有实参的类型和数目时,可以使用省略符形参。省略符暂停了类型检查机制。它们的出现告知编译器,当调用函数时,可以有 0 或多个实参,而实参的类型未知。省略符形参有下列两种形式:

void foo(parm_list, ...);
 void foo(...);

第一种形式为特定数目的形参提供了声明。在这种情况下,当函数被调用时,对于与显示声明的形参相对应的实参进行类型检查,而对于与省略符对应的实参则暂停类型检查。在第一种形式中,形参声明后面的逗号是可选的。大部分带有省略符形参的函数都利用显式声明的参数中的一些信息,来获取函数调用中提供的其他可选实参的类型和数目。因此带有省略符的第一种形式的函数声明是最常用的。
原文链接: https://www.cnblogs.com/predator-wang/p/5195830.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/228846

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月13日 下午2:06
下一篇 2023年2月13日 下午2:06

相关推荐