函数参数与栈
栈的概念
函数被调用时,会在一个名为栈的空间上,为形式参数与函数体内的局部变量划分存储空间;函数执行完成后,这些内存区域将被自动释放。从数据结构上讲,栈是一种具有 后进先出(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));
}

函数参数入栈
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");
}

注意,输出参数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);
}

可以看到,结构体成员的地址,按照其声明顺序由低到高;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。可以看到,通过使用指针传递结构体,只需要拷贝一个指针的大小!

练习一下指针
在掌握了函数调用栈的知识后,我们就可以使用一个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");
}
