zoukankan      html  css  js  c++  java
  • 函数中的参数问题小结(&,*,传参与变参)

    数组传参

    数组作为参数传给函数时传的是指针而不是数组本身,那个指针存储的是数组的首地址,如:fun(char[8])和fun(char[])都等价于fun(char *)。

    //在C++里参数传递数组永远都是传递指向数组首元素的指针,编译器不知道数组的大小。
    //如果想在函数内知道数组的大小, 可以在进入函数后用memcpy拷贝出来,长度由另一个形参传进去
    fun(unsiged char *p1, int len) { unsigned char* buf = new unsigned char[len+1] memcpy(buf, p1, len); }

    &和*

    “&”这个符号有两个不同的含义:引用和取地址。

    引用的声明其实等于变量的别名:

    Type &newname = name; //newname是name的别名,在编译器看来对应同一片内存分配

    函数形参中的&(引用)和指针*

    函数形参使用&是为了改变实参的值。普通形参只是拷贝了值进行操作,实参不受影响(即值不变)。

    形参中的指针*同理,因为拷贝的是内存地址!!!所以对指针操作也可改变实参,因为是根据地址操作的。(函数调用时注意参数传递要用地址:fun(&name))

    函数形参中指针*和引用&的搭配使用

    此时引用&可改变的不是指针存储地址所指向的变量值(*ptr),而是可以对指针*的存储地址进行改变!即&可改变的是指针变量本身的值:存储的内存地址(ptr)!!!例如:在二叉树的类成员函数createNode中,为了在函数中进行指针指向变量的内存地址动态分配(即改变指针存储值),必须是Node* &ptr才行!!!此处&可理解为“取指针(ptr)本身的地址”,即可改变ptr本身的值(实现Node的动态内存分配)。

    二维数组传参

    形参为二维数组并给定第二维长度

    最简单最直观的方法,形参与实参一样,容易理解。

    #include <stdio.h>
    void subfun(int n, char subargs[][5]) {
        int i;
        for (i = 0; i < n; i++) {
            printf("subargs[%d] = %s
    ", i, subargs[i]);
        }
    }
     
    void main() {
        char args[][5] = {"abc", "def", "ghi"};
        subfun(3, args);
    }

    形参为指向数组的指针并给出数组长度

    #include <stdio.h>
    void subfun(int n, char (*subargs)[5]) {
        int i;
        for (i = 0; i < n; i++) {
            printf("subargs[%d] = %s
    ", i, subargs[i]);
        }
    }
     
    void main() {
        char args[][5] = {"abc", "cde", "ghi"};
        subfun(3, args);
    }

    形参为指针的指针

    此方法实参必须为指针,而不能为数组名。

    #include <stdio.h>
    void subfun(int n, char **subargs) {
        int i; 
        for (i = 0; i < n; i++) {
            printf("subargs[%d] = %s
    ", i, subargs[i]);
        }
    }
     
    void main() {
        char *a[3];
        char args[][5] = {"abc", "def", "ghi"};
        a[0] = args[0];  //equals with a[0] = &args[0][0];
        a[1] = args[1];
        a[2] = args[2];
        subfun(3, a);  //若此处为subfun(3, args);则会编译出错
    }

    上述代码等价于下面代码。当然我们这里只是讨论的二维数组传参问题,下面代码只起扩展作用。

    #include <stdio.h>
    void subfun(int n, char **subargs) {
        int i; 
        for (i = 0; i < n; i++) {
            printf("subargs[%d] = %s
    ", i, subargs[i]);
        }
    }
     
    void main() {
        char *args[] = {"abc", "def", "ghi"};//equals with char *args[3] = {"abc", "def", "ghi"};
        subfun(3, args);
    } 

    变参

    C语言中函数调用的原理

    函数是大多数编程语言都实现的编程要素,调用函数的实现原理就是:执行跳转+参数传递。对于执行跳转,所有的CPU都直接提供跳转指令;对于参数传递,CPU会提供多种方式,最常见的方式就是利用栈来传递参数。C语言标准实现了函数调用,但是却没有限定实现细节,不同的C编译器厂商可以根据底层硬件环境自行确定实现方式。

    函数调用的一般实现原理,请参考smstong的博文:C语言中利用setjmp和longjmp做异常处理中的第一段。

    变参实现思路

    1. 如何取得后续实参地址

    以X86架构上的VC++编译器为例进行举例说明:

    void f(int x, int y, int z) {
        printf("%p, %p, %p
    ", &x, &y, &z);
    }
    int main() {
        f(100, 200, 300);
        return 0;
    }
    //可能的执行结果:
    00FFF674, 00FFF678, 00FFF67C

    VC++中函数的参数是通过堆栈传递的,参数按照从右向左的顺序入栈。调用f时参数在堆栈中的情况如下图所示:

     可见,我们只要知道x的地址,就可以推算出y,z的地址,从而通过其地址取得参数y,z的值,而不用其参数名称取值。如下代码所示。

    void f(int x, int y, int z) {
        char* px = (char*)&x;
        char *py = px + sizeof(x);
        char *pz = py + sizeof(int);
        printf("x=%d, y=%d, z=%d
    ", x, *(int*)py, *(int*)pz);
    }
    
    int main(){
        f(100, 200, 300);
        return 0;
    }

    根据函数的第一个参数,以及后续参数的类型,就可以根据偏移量计算出后续参数的地址,从而取得后续参数值。
    于是可以把上述代码改写成可变参数的形式。

    void f(int x, ...) {
        char* px = (char*)&x;
        char *py = px + sizeof(x);
        char *pz = py + sizeof(int);
        printf("x=%d, y=%d, z=%d
    ", x, *(int*)py, *(int*)pz);
    }
    
    int main() {
        f(100, 200, 300);
        return 0;
    }

    2. 如何标识后续参数个数和类型

    虽然写成了可变参形式,但是函数如何判断后续实参的个数和类型呢?这就需要在固定参数中携带这些信息,如printf(char*, …)使用的格式化字符串方法,通过第一个参数来携带后续参数个数以及类型的信息。我们实现一个简单点的,只能识别%s,%d,%f三种标志。

    void f(char* fmt, ...) {
        char* p0 = (char*)&fmt;
        char* ap = p0 + sizeof(fmt);
        char* p = fmt;
        while (*p) {
            if (*p == '%' && *(p+1) == 'd') {
                printf("参数类型为int,值为 %d
    ", *((int*)ap));
                ap += sizeof(int);
            }
            else if (*p == '%' && *(p+1) == 'f') {
                printf("参数类型为double,值为 %f
    ", *((double*)ap));
                ap += sizeof(double);
            }
            else if (*p == '%' && *(p+1) == 's') {
                printf("参数类型为char*,值为 %s
    ", *((char**)ap));
                ap += sizeof(char*);
            }
            p++;
        }
    }
    
    int main() {
        f("%d,%f,%s", 100, 1.23, "hello world");
        return 0;
    }

    输出:

    参数类型为int,值为 100
    参数类型为double,值为 1.230000
    参数类型为char*,值为 hello world

    为简化分析参数代码,定义一些宏来简化,如下。

    #define va_list char*   /* 可变参数地址 */
    #define va_start(ap, x) ap=(char*)&x+sizeof(x) /* 初始化指针指向第一个可变参数 */
    #define va_arg(ap, t)   (ap+=sizeof(t),*((t*)(ap-sizeof(t)))) /* 取得参数值,同时移动指针指向后续参数 */
    #define va_end(ap)  ap=0 /* 结束参数处理 */
    
    void f(char* fmt, ...) {
        va_list ap;
        va_start(ap, fmt);
    
        char* p = fmt;
        while (*p) {
            if (*p == '%' && *(p+1) == 'd') {
                printf("参数类型为int,值为 %d
    ", va_arg(ap, int));
            }
            else if (*p == '%' && *(p+1) == 'f') {
                printf("参数类型为double,值为 %f
    ", va_arg(ap, double));
            }
            else if (*p == '%' && *(p+1) == 's') {
                printf("参数类型为char*,值为 %s
    ", va_arg(ap, char*));
            }
            p++;
        }
        va_end(ap);
    }
    
    int main() {
        f("%d,%f,%s,%d", 100, 1.23, "hello world", 200);
        return 0;
    }

    正确的变参函数实现方法

    上面的例子中,我们没有使用任何库函数就轻松实现了可变参数函数。别高兴太早,上述代码在X86平台的VC++编译器下可以顺利编译、正确执行。但是在gcc编译后,运行却是错误的。可见GCC对于可变参数的实参传递实现与VC++并不相同。

    gcc下编译运行:

    [smstong@cf-19 ~]$ ./a.out
    参数类型为int,值为 0
    参数类型为double,值为 0.000000
    Segmentation fault

    可见,上述代码是不可移植的。为了在使得可变参函数能够跨平台、跨编译器正确执行,必须使用C标准头文件stdarg.h中定义的宏,而不是我们自己定义的。(这些宏的名字和作用与我们自己定义的宏完全相同,这绝不是巧合!)每个不同的C编译器所附带的stdarg.h文件中对这些宏的定义都不相同。再次重申一下这几个宏的使用范式:

    va_list ap;
    va_start(ap, 固定参数名); /* 根据最后一个固定参数初始化 */
    可变参数1类型 x1 = va_arg(ap, 可变参数类型1); /* 根据参数类型,取得第一个可变参数值 */
    可变参数2类型 x2 = va_arg(ap, 可变参数类型2); /* 根据参数类型,取得第二个可变参数值 */
    ...
    va_end(ap);     /* 结束 */

    这次,把我们自己的宏定义去掉,换成#include

    #include <stdio.h>
    #include <stdarg.h>
    void f(char* fmt, ...) {
        va_list ap;
        va_start(ap, fmt);
        char* p = fmt;
        while (*p) {
            if (*p == '%' && *(p+1) == 'd') {
                printf("参数类型为int,值为 %d
    ", va_arg(ap, int));
            }
            else if (*p == '%' && *(p+1) == 'f') {
                printf("参数类型为double,值为 %f
    ", va_arg(ap, double));
            }
            else if (*p == '%' && *(p+1) == 's') {
                printf("参数类型为char*,值为 %s
    ", va_arg(ap, char*));
            }
            p++;
        }
        va_end(ap);
    }
    
    int main() {
        f("%d,%f,%s,%d", 100, 1.23, "hello world", 200);
        return 0;
    }

    代码在VC++和GCC下均可以正确执行了。

    关于C和C++的变参列表中宏的使用,详情见:C和C++中可变参数的宏使用

    几个需要注意的问题

    va_end(ap); 必须不能省略

    也许在有些编译器环境中,va_end(ap);确实没有什么作用,但是在其他编译器中却可能涉及到内存的回收,切不可省略。

    可变参数的默认类型提升

    《C语言程序设计》中提到:在没有函数原型的情况下,char与short类型都将被转换为int类型,float类型将被转换为double类型。实际上,用...标识的可变参数总是会执行这种类型提升。

    引用《C陷阱与缺陷》里的话:

    **va_arg宏的第2个参数不能被指定为char、short或者float类型**。因为char和short类型的参数会被转换为int类型,而float类型的参数会被转换为double类型 ……

    例如,这样写肯定是不对的:c = va_arg(ap,char); 因为我们无法传递一个char类型参数,如果传递了,它将会被自动转化为int类型。上面的式子应该写成:c = va_arg(ap,int);

    编译器无法进行参数类型检查

    对于可变参数,编译器无法进行任何检查,只能靠调用者的自觉来保证正确。

    可变参数函数必须提供一个或更多的固定参数

    可变参数必须靠固定参数来定位,所以函数中至少需要提供固定参数,f(固定参数,…)。
    当然,也可以提供更多的固定参数,如f(固定参数1,固定参数2,…)。注意的是,当提供2个或以上固定参数时,va_start(ap, x)宏中的x必须是最后一个固定参数的名字(也就是紧邻可变参数的那个固定参数)。

    C的可变参函数与C++的重载函数

    C++的函数重载特性,允许重复使用相同的名称来定义函数,只要同名函数的参数(类型或数量)不同。例如

    void f(int x);
    void f(int x, double d);
    void f(char* s);

    虽然源代码中函数名字相同,其实编译器处理后生成的是三个具有不同函数名的函数(名字改编name mangling)。虽然在使用上有些类似之处,但这显然与C的可变参数函数完全不是一个概念。

    (整理自网络)

    参考资料:

    https://blog.csdn.net/gqb_driver/article/details/8886687

    https://blog.csdn.net/smstong/article/details/50751121

    Min是清明的茗
  • 相关阅读:
    NOIP2011 D1T1 铺地毯
    NOIP2013 D1T3 货车运输 倍增LCA OR 并查集按秩合并
    POJ 2513 trie树+并查集判断无向图的欧拉路
    599. Minimum Index Sum of Two Lists
    594. Longest Harmonious Subsequence
    575. Distribute Candies
    554. Brick Wall
    535. Encode and Decode TinyURL(rand and srand)
    525. Contiguous Array
    500. Keyboard Row
  • 原文地址:https://www.cnblogs.com/MinPage/p/13942279.html
Copyright © 2011-2022 走看看