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是清明的茗
  • 相关阅读:
    染色法判定二分图
    Kruskal算法求最小生成树
    Prim算法求最小生成树
    Floyd算法求多源最短路
    spfa判断负环
    java 线程的使用
    java IO基础
    数据库 EXISTS与NOT EXISTS
    数据库 何为相关查询和不相关查询?
    数据库的基础知识
  • 原文地址:https://www.cnblogs.com/MinPage/p/13942279.html
Copyright © 2011-2022 走看看