张银峰的编程课堂

函数参数与栈

栈的概念

函数被调用时,会在一个名为栈的空间上,为形式参数与函数体内的局部变量划分存储空间;函数执行完成后,这些内存区域将被自动释放。从数据结构上讲,栈是一种具有 后进先出(LIFO, last in/first out) 性质的线性数据结构,也就是说最后入栈的数据最先被读取,插入数据的位置叫栈顶,栈的逻辑类似与一摞盘子,盘子总是放到最上面,最后放上去的最先被取出,。

与栈相关的几个操作分别为:

  • push:入栈,将元素放入栈的顶部
  • pop:出栈,弹出栈顶部的元素
  • is_empty:判断栈是否为空
  • clear:清空栈

示例程序以数组的方式演示了栈的基础操作。

#include <stdio.h>
#include <stdbool.h>
#include <assert.h>

typedef struct _IntStack
{
    int top;
    int c[30];
} IntStack;

void stack_clear(IntStack *s)
{
    s->top = -1;
}

bool stack_is_empty(IntStack *s)
{
    return s->top == -1;
}

bool stack_is_full(IntStack *s)
{
    return s->top == 9;
}

void stack_push(IntStack *s, int v)
{
    assert(!stack_is_full(s));
    s->c[++s->top] = v;
}

int stack_pop(IntStack *s)
{
    assert(!stack_is_empty(s));
    return s->c[s->top--];
}

int main()
{
    IntStack s;
    stack_clear(&s);

    for (int i = 0; i < 10; i++)
        stack_push(&s, i);

    stack_pop(&s);
    stack_pop(&s);

    while (!stack_is_empty(&s))
        printf("%d ", stack_pop(&s));

    assert(stack_is_empty(&s));
}

glimix.com

函数参数入栈

C语言调用约定,函数参数是从右向左压入栈中。这意味着函数fun被调用时,形式参数会按照 c、b、a 的顺序被放入栈中;这个栈空间有点特殊,通常栈底为高地址,栈顶为低地址。

#include <stdio.h>

void fun(int a, double b, char *c)
{
    printf("a: 0x%p\n", &a);
    printf("b: 0x%p\n", &b);
    printf("c: 0x%p\n", &c);
}

int main()
{
    fun(1, 2.0, "glimix.com");
}

glimix.com

注意,输出参数c的地址时使用了取地址操作符;因为我们要打印变量c的地址,而非c所指对象的地址。

函数调用时,栈的状态如下,其中栈地址的是从高到低。

参数入栈
===================↑===========
0x008FF000         |  <-栈顶
..                 |
0x008FF6C4  <-a    |
0x008FF6C8  <-b    |
0x008FF6D0  <-c    |  <-栈底
===================|===========

最先入栈的c的地址是0x008FF6D0,下一个数据b是double类型,占8个字节,因此起始地址是0x008FF6D0-8=0x008FF6C8。

结构体入栈

将结构体变量传递到函数时,会在栈上为结构创建所有成员的拷贝。

#include <stdio.h>

struct Bar
{
    int a;
    double b;
    char c[10];
};

void foo(struct Bar bar)
{    
    printf("bar.a: 0x%p\n", &bar.a);
    printf("bar.b: 0x%p\n", &bar.b);
    printf("bar.c: 0x%p\n", &bar.c[9]);
}

int main()
{
    printf("================================ main\n");
    struct Bar bar;
    printf("bar.a: 0x%p\n", &bar.a);
    printf("bar.b: 0x%p\n", &bar.b);
    printf("bar.c: 0x%p\n", &bar.c[9]);

    printf("================================ foo\n");
    foo(bar);
}

glimix.com

可以看到,结构体成员的地址,按照其声明顺序由低到高;main与foo中结构体的成员地址并不相同,当foo被调用时,main中的实参bar被拷贝到foo的形参bar中。在C语言中,函数参数传递都是一个值拷贝过程,这一点,对于较大的结构体,所有成员的拷贝操作可能是一个负担,这可以通过传递结构体指针来解决。

#include <stdio.h>

struct Bar
{
    int a;
    double b;
    char c[10];
};

void foo(int x, double y, const struct Bar *bar)
{
    printf("================================ foo\n");
    printf("&bar:  0x%p\n\n", &bar);
    printf("bar:   0x%p\n", bar);
    printf("bar.a: 0x%p\n", &bar->a);
    printf("bar.b: 0x%p\n", &bar->b);
    printf("bar.c: 0x%p\n", &bar->c[9]);
}

int main()
{
    struct Bar bar;

    printf("================================ main\n");
    printf("bar:   0x%p\n", &bar);
    printf("bar.a: 0x%p\n", &bar.a);
    printf("bar.b: 0x%p\n", &bar.b);
    printf("bar.c: 0x%p\n", &bar.c[9]);

    printf("\n");
    foo(1, 3.14, &bar);
}

参数传递时,main中的bar指针实参(0x012FFB58),被拷贝到foo的指针形参bar里(0x012FFA84)。因此foo中形参bar的地址就是&bar,而bar表示指针所指的对象,即main函数中的bar,这两者是相同的,指向0x012FFB58。可以看到,通过使用指针传递结构体,只需要拷贝一个指针的大小!

glimix.com

练习一下指针

在掌握了函数调用栈的知识后,我们就可以使用一个char*指针来访问所有参数了。

#include <stdio.h>

void foo(int a, double b, char *c)
{    
    // 函数参数压栈是由右向左的,即c, b, a;先入栈的存储在高位地址
    printf("&a = %p\n", &a);
    printf("&b = %p\n", &b);
    printf("&c = %p\n", &c);

    printf("\n");

    // 输出第一个参数,其地址在低位内存
    char *p = (char*)&a;
    printf("a = %d\n", *(int*)p);

    // 地址偏移第一个参数的大小
    p += sizeof(int);
    printf("b = %f\n", *(double*)p);

    // 第三个参数类型是个指针,因此p中存储的是一个地址值,即p是一个指针的指针。
    p += sizeof(double);
    printf("c = %s\n", *(const char**)p);
}

int main()
{
    foo(1, 3.14, "glimix.com");
}

glimix.com