C语言可变参数与函数参数的内存对齐详解

目录
  • 什么是可变参数?
  • 使用可变参数
  • 函数参数的内存对齐
  • 总结

什么是可变参数?

有时,您可能会碰到这样的情况,您希望函数带有可变数量的参数,而不是预定义数量的参数。

C 语言为这种情况提供了一个解决方案,它允许您定义一个函数,能根据具体的需求接受可变数量的参数。

比如我们最常用的printf函数,它的函数声明是:int printf(const char *format, ...);该函数就是一个典型的应用可变参数的实例,后面那三个...就是说明该函数是可变参数函数。

使用可变参数

要使用可变函数,得引用一个头文件#include <stdarg.h>该文件提供了实现可变参数功能的函数和宏。
使用可变参数的步骤如下:

1.定义一个函数,最后一个参数为省略号...,省略号前面可以设置自定义参数(至少得有一个固定参数)。如
int getSum(int num, ...)//定义可变参数的函数

2.在函数中定义va_list类型的变量list,该类型在stdarg.h中已定义。

3.使用宏函数va_start来初始化变量list,该宏函数在stdarg.h中已定义。

4.使用宏函数va_arglist来访问参数列表中的每个项。

5.使用宏函数va_end来清理赋予list变量的内存。

宏的声明

/** \brief      初始化 ap 变量,它与 va_arg 和 va_end 宏是一起使用的。
 *				last_arg 是最后一个传递给函数的已知的固定参数,即省略号之前的参数。
 *				这个宏必须在使用 va_arg 和 va_end 之前被调用。
 *
 * \param   	ap -- 这是一个 va_list 类型的对象,
 * 						它用来存储通过 va_arg 获取额外参数时所必需的信息。
 * \param   	last_arg -- 最后一个传递给函数的已知的固定参数(省略号前面的那个参数)。
 * \return      无
 *
 */
void va_start(va_list ap, last_arg)

/** \brief      检索函数参数列表中类型为 type 的下一个参数。它无法判断检索到的参数是否是传给函数的最后一个参数。
 *
 * \param   	ap -- 这是一个 va_list 类型的对象,存储了有关额外参数和检索状态的信息。
 * 					该对象应在第一次调用 va_arg 之前通过调用 va_start 进行初始化。
 * \param   	type -- 这是一个类型名称。该类型名称是作为扩展自该宏的表达式的类型来使用的。
 * \return      该宏返回下一个额外的参数,是一个类型为 type 的表达式。
 *
 */
type va_arg(va_list ap, type)

/** \brief  	该宏允许使用了 va_start 宏的带有可变参数的函数返回(释放内存)。如果在从函数返回之前没有调用 va_end,则结果为未定义。
 *
 * \param   	ap -- 这是之前由同一函数中的 va_start 初始化的 va_list 对象。
 * \return      无
 *
 */
void va_end(va_list ap)

实例1一个可变参数的函数,求和

#include <stdio.h>
#include <stdarg.h>//引用可变参数宏头文件

int getSum(int num, ...)//定义可变参数的函数
{
    int sum = 0;
    va_list list;//创建va_list类型的变量
    va_start(list, num);//初始化可变参数list
    for(int i = 0; i < num; i++)
    {
        sum += va_arg(list, int);//访问参数列表中的每个项
    }
    va_end(list);//释放内存
    return sum;
}

int main()
{
    printf("%d\n", getSum(4, 4, 5, 6, 7));
}

实例2输出字符串

#include <stdio.h>
#include <stdarg.h>//引用可变参数宏头文件

void func(char *demo, ...)
{
    char *pstr = NULL;
    va_list list;
    va_start(list, demo);
    while(1)
    {
        pstr = va_arg(list, char *);
        if(*pstr == '$')//以 '$' 代表结束
            break;
        printf("%s\n", pstr);
    }
    va_end(list);
}

int main()
{
    func("demo", "ABC", "123", "Hello Wolrd!", '$');
}

这里特别注意一下,宏va_arg无法判断检索到的参数是否是传给函数的最后一个参数,所以我们需要告诉该参数是不是最后一个参数,有2个方法,一是在使用一个函数参数来说明可变参数的数量,一是定义一个结束标志符。

可变参数的另外的一种使用方式

#include <stdio.h>

int getSum(int num, ...)
{
    int sum = 0;
    char *p = NULL;
    p = (char*)&num;
    p += 8;
    for(int i = 0; i < num; i++)
    {
        sum += *((int*)p);
        p += 8;
    }
    return sum;
}
int main()
{
    int a = 1;
    int b = 2;
    int c = 3;
    printf("sum = %d\n", getSum(3, a, b, c));
}
/*
输出结果
sum = 6;
*/

为什么这样也可以访问可变参数呢?为什么指针p要加8呢?
因为这与函数参数的入栈出栈及函数参数的内存对齐有关。

函数参数的内存对齐

首先我们来看函数void func(int a, int b, int c)各个参数在栈中的位置

c 高地址
b
a 低地址

函数参数的传递存储在栈中,从右至左压入栈中,压栈过程为递减;出栈过程为递增。

所以我们只需要知道a的地址,在a的地址上加上偏移量就可以访问b或者c了。

那应该加上多少偏移量呢?

#include <stdio.h>

void func(int a, int b, int c)
{
    printf("a = %p\n", &a);
    printf("b = %p\n", &b);
    printf("c = %p\n", &c);
}
int main()
{
	int a,b,c;
	func(a, b, c);
}
/*
输出结果
a = 000000000061FDF0
b = 000000000061FDF8
c = 000000000061FE00
*/

通过上例,发现它们之间相差8,为什么是8呢?

因为我是在Window64位上运行的,故需要按照8字节对齐。

综上,函数参数的传递存储在栈中,从右至左压入栈中,压栈过程为递减,出栈过程为递增;并且需要进行内存对齐,Window64位为8字节对齐,32位为4字节对齐。

下面是我做的一些实验,更改了函数参数类型。

短整型

#include <stdio.h>

void func(char a, short b, long long c)
{
    printf("a = %p\n", &a);
    printf("b = %p\n", &b);
    printf("c = %p\n", &c);
}

int main()
{
    char a = 1;
    short b = 2;
    long long c = 3;
    func(a, b, c);
}
/*
输出结果
a = 000000000061FDF0
b = 000000000061FDF8
c = 000000000061FE00
*/

浮点型

#include <stdio.h>

void func(double a, double b, double c)
{
    printf("a = %p\n", &a);
    printf("b = %p\n", &b);
    printf("c = %p\n", &c);
}

int main()
{
    double a = 1;
    double b = 2;
    double c = 3;
    func(a, b, c);
}
/*
输出结果
a = 000000000061FDF0
b = 000000000061FDF8
c = 000000000061FE00
*/

结构体1

#include <stdio.h>
typedef struct
{
    char c[7];
}str_t;

void func(str_t a, str_t b, str_t c)
{
    printf("a = %p\n", &a);
    printf("b = %p\n", &b);
    printf("c = %p\n", &c);
}

int main()
{
    str_t a;
    str_t b;
    str_t c;
    func(a, b, c);
}
/*
输出结果
a = 000000000061FDF0
b = 000000000061FDE0
c = 000000000061FDD0
*/

结构体2

#include <stdio.h>
typedef struct
{
    char c[4];
}str_t;

void func(str_t a, str_t b, str_t c)
{
    printf("a = %p\n", &a);
    printf("b = %p\n", &b);
    printf("c = %p\n", &c);
}

int main()
{
    str_t a;
    str_t b;
    str_t c;
    func(a, b, c);
}
/*
输出结果
a = 000000000061FDF0
b = 000000000061FDF8
c = 000000000061FE00
*/

结构体1出问题了,具体没搞明白,欢迎大佬指导。

建议可变参数使用引用头文件stdarg.h得方式较好。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • C语言中结构体、联合体的成员内存对齐情况

    前言 最近项目进行中,遇到一个小问题,在数据协议传输过程中,我为了方便解析,就定义了一个结构体,在数据的指针传入函数的时候,我用定义好的结构体进行强制转化,没想到一直解析失败,调试很久,终于反应过来,在用结构体指针对数据强制转换时,定义结构体我没有注意到数据对齐,因为在底层实现中,我传入的数据buffer是排列整齐的,而强制转化的结构体格式中,我定义的时候没有使用__attribute__((__packed__))或者__packed强制数据对齐,导致结构体成员真实排列会按照成员中最大的变量的

  • C语言中结构体与内存对齐实例解析

    1.结构体类型 C语言中的2种类型:原生类型和自定义类型,结构体类型是一种自定义类型. 2.结构体使用时先定义结构体类型再用类型定义变量 -> 结构体定义时需要先定义结构体类型,然后再用类型来定义变量. -> 也可以在定义结构体类型的同时定义结构体变量. // 定义类型 struct people { char name[20]; int age; }; // 定义类型的同时定义变量. struct student { char name[20]; int age; }s1; // 将类型st

  • C语言重难点之内存对齐和位段

    一:结构体内存对齐 (1)为什么要存在内存对齐 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的:某些平台只能在某些地址处取得某些特定类型的数据,否则抛出硬件异常. 比如,当一个平台要取一个整型数据时只能在地址为4的倍数的位置取得,那么这时就需要内存对齐,否则无法访问到该整型数据. 性能原因: 数据结构(尤其是栈)应该尽可能的在自然边界上对齐.原因在于,为了访问未对齐内存,处理器需要作两次内存访问:而对齐的内存访问仅需一次. 核心思想就是:以空间换取时间 举个例子:对于有

  • C语言、C++内存对齐问题详解

    这也可以? 复制代码 代码如下: #include <iostream> using namespace std;   struct Test_A {      char a;      char b;      int c; };   struct Test_B {      char a;      int c;      char b; };   struct Test_C {      int c;      char a;      char b; };   int main() {

  • 一篇文章带你了解C语言内存对齐

    目录 内存对齐 三.在内存对齐话题下的sizeof与offsetof宏 3.1.sizeof 3.2.offsetof宏 3.3.Debug 总结 内存对齐 先看如下代码: 结构体Test1占用了多少字节?如果事先不知道内存对齐的话,答案肯定是:1个字节(char)+ 4个字节(int)+ 1个字节(char) = 6个字节. 事实上,Test1结构体占用了12个字节,从DEBUG模式下Watch1观察: OK,不就猜少了6个字节吗?有什么影响吗?先不说影响吧,咱们先来看看单片机内存里的实际情况

  • C语言程序中结构体的内存对齐详解

    目录 一.为什么存在内存对齐 二.结构体的内存对齐四规则 三.举例 一.为什么存在内存对齐 1.平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的:某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常. 2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐. 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问:而对齐的内存访问仅需要一次访问. 总的来说结构体的内存对齐是拿空间来换取时间的做法. 二.结构体的内存对齐四规则 默认情况:默认的对

  • C语言可变参数与函数参数的内存对齐详解

    目录 什么是可变参数? 使用可变参数 函数参数的内存对齐 总结 什么是可变参数? 有时,您可能会碰到这样的情况,您希望函数带有可变数量的参数,而不是预定义数量的参数. C 语言为这种情况提供了一个解决方案,它允许您定义一个函数,能根据具体的需求接受可变数量的参数. 比如我们最常用的printf函数,它的函数声明是:int printf(const char *format, ...);该函数就是一个典型的应用可变参数的实例,后面那三个...就是说明该函数是可变参数函数. 使用可变参数 要使用可变

  • Go语言的变量、函数、Socks5代理服务器示例详解

    Go语言中变量的声明和JavaScript很像,使用var关键字,变量的声明.定义有好几种形式 1. 变量和常量 // 声明并初始化一个变量 var m int = 10 // 声明初始化多个变量 var i, j, k = 1, 2, 3 // 多个变量的声明(注意小括号的使用) var( no int name string ) // 声明时不指明类型,通过初始化值来推导 var b = true // bool型 // := 隐含声明变量并赋值 str := "mimvp.com"

  • C语言热门考点结构体与内存对齐详解

    目录 一.引例 1.结构体的第一个成员永远放在结构体起始位置偏移量为0的位置 2.从第二个成员开始,总是放在偏移量为一个对齐数的整数处,对齐数=编译器默认的对齐数和变量自身大小的较小值 3.结构体的总大小必须是各个成员的对齐数中最大的那个对齐数的整数倍 二.小试牛刀 三.嵌套结构体的特殊情况 四.关于为什么存在内存对齐 1.平台原因(移植原因): 2.性能原因: 总结 一.引例 到底什么是结构体内存对齐,我们用一段代码来介绍一下 struct S1 { char c1;//1字节 int a;/

  • C语言 指针变量作为函数参数详解

    在C语言中,函数的参数不仅可以是整数.小数.字符等具体的数据,还可以是指向它们的指针.用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁. 像数组.字符串.动态分配的内存等都是一系列数据的集合,没有办法通过一个参数全部传入函数内部,只能传递它们的指针,在函数内部通过指针来影响这些数据集合. 有的时候,对于整数.小数.字符等基本类型数据的操作也必须要借助指针,一个典型的例子就是交换两个变量的值. 有些初学者可能会使用

  • Go语言基础函数基本用法及示例详解

    目录 概述 语法 函数定义 一.函数参数 无参数无返回 有参数有返回 函数值传递 函数引用传递 可变参数列表 无默认参数 函数作为参数 二.返回值 多个返回值 跳过返回值 匿名函数 匿名函数可以赋值给一个变量 为函数类型添加方法 总结 示例 概述 函数是基本的代码块,用于执行一个任务 语法 函数定义 func 函数名称( 参数列表] ) (返回值列表]){ 执行语句 } 一.函数参数 无参数无返回 func add() 有参数有返回 func add(a, b int) int 函数值传递 fu

  • C语言strlen函数实现读取字符串长度详解

    目录 前言 1.函数strlen 2.使用指针 3.指针改进 4.使用递归 5.my_strlen函数的参数改进—常量指针 总结 前言 读取字符串的长度,使用函数 strlen.这里我们写一个函数,来读取字符串的长度,本文内容主要包括: 使用strlen 使用指针:是常规的方法,但是要创建变量 指针改进:不需要创建变量,要求高,不易掌握 使用递归:不需要创建变量,要求高,不易掌握 介绍常量指针—— const char* str 1.函数strlen int main() { char arr[

  • Go语言学习之函数的定义与使用详解

    目录 1.函数定义 2.多值返回 3.引用传递 4.函数作为实参使用 5.匿名函数 1.函数定义 函数的定义和java一样,使用{}进行包裹,并且要明确入参类型以及返回类型. 样例代码如下: func min(num1, num2 int) int { if num1 <= num2 { return num1 } else { return num2 } } func main() { fmt.Printf("max = %d\n", min(10, 12)) } 执行结果 m

  • Vue实例的对象参数options的几个常用选项详解

    一. 新建一个Vue实例可以有下列两种方式: 1.new一个实例 var app= new Vue({ el:'#todo-app', // 挂载元素 data:{ // 在.vue组件中data是一个函数,要写成data () {}这种方式 items:['item 1','item 2','item 3'], todo:'' }, methods:{ // 方法成员 rm:function(i){ this.items.splice(i,1) } } }) // 之后再 export def

  • js中获取URL参数的共用方法getRequest()方法实例详解

    下面通过一段代码给大家介绍js中获取URL参数的共用方法getRequest()方法,具体代码如下所示: getRequest : function() { var url = location.search; //获取url中"?"符后的字串 var theRequest = new Object(); if (url.indexOf("?") != -1) { var str = url.substr(1); strs = str.split("&am

随机推荐