首页 > 其他分享 >详细解释可变参数列表C语言

详细解释可变参数列表C语言

时间:2024-03-16 20:30:18浏览次数:25  
标签:va include return int 可变 列表 参数 arg C语言

目录

考研复习-函数栈帧(详解)

一.什么是可变参数列表?

1.1 求两个数据中的最大值

1.2求多个数据中的最大值

1.3逐步分析

 1.va_start

 2.va_arg

3.va_end 

4._INTSIZEOF(t)

第一步理解:4的倍数

第二步理解:最小4字节对齐数                

第三步理解:理解源代码中的宏

二.清晰的回顾一遍 

三.命令行参数:

 四.递归


可变参数列表

对函数栈帧有问题的可以看我的另外一篇博客

考研复习-函数栈帧(详解)

前提知识:

一.什么是可变参数列表?

使用

1.1 求两个数据中的最大值

demo 1:求两个数据中的最大值


#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
int FindMax(int x, int y)
{
if (x > y){
return x;
}
return y;
}
int main()
{
int x = 0;
int y = 0;
printf("Please Eneter Two Data# ");
scanf("%d %d", &x, &y);
int max = FindMax(x, y);
printf("max = %d\n", max);
system("pause");
return 0;
}

1.2求多个数据中的最大值

demo 2:求任意多个数据中的最大值(至少一个),要求不能使用数组
//因为目前参数个数不确定,那么函数编写的时候,参数个数也无法确定,换句话说,函数也就没法编写
//不过,C提供了满足该场景的解决方案:可变参数列表 

重点1:可变参数列表至少要给一个参数,不然不能往下找其他变量的栈帧


#include <stdio.h>
#include <windows.h>
//num:表示传入参数的个数
int FindMax(int num, ...)
{
va_list arg;     //定义可以访问可变参数部分的变量,其实是一个char*类型
va_start(arg, num); //使arg指向可变参数部分
int max = va_arg(arg, int); //根据类型,获取可变参数列表中的第一个数据
for (int i = 0; i < num - 1; i++){//获取并比较其他的
int curr = va_arg(arg, int);
if (max < curr){
max = curr;
}
}
va_end(arg); //arg使用完毕,收尾工作。本质就是讲arg指向NULL
return max;
}
int main()
{
int max = FindMax(5,11,22,33,44,55);
printf("max = %d\n", max);
system("pause");
return 0;
}

基本原理如图:数据放的时候在vs里面是连续的 

 

/demo 3: 如果将参数改成char类型,求char类型变量中的最大值,代码会有问题吗? 

答案:没有影响,因为char会整型提升!

通过查看汇编,我们看到,在可变参数场景下:
1. 实际传入的参数如果是char,short,float,编译器在编译的时候,会自动进行提升(通过查看汇编,我们都能看
到)
2. 函数内部使用的时候,根据类型提取数据,更多的是通过int或者double来进行 

#include <stdio.h>
#include <windows.h>
//num:表示传入参数的个数
int FindMax(int num, ...)
{
va_list arg;     //定义可以访问可变参数部分的变量,其实是一个char*类型
va_start(arg, num); //使arg指向可变参数部分
int max = va_arg(arg, int); //根据类型,获取可变参数列表中的第一个数据
for (int i = 0; i < num - 1; i++){//获取并比较其他的
int curr = va_arg(arg, int);
if (max < curr){
max = curr;
}
}
va_end(arg); //arg使用完毕,收尾工作。本质就是讲arg指向NULL
return max;
}
int main()
{
char a = '1'; //ascii值: 49
char b = '2'; //ascii值: 50
char c = '3'; //ascii值: 51
char d = '4'; //ascii值: 52
char e = '5'; //ascii值: 53
int max = FindMax(5, a, b, c, d, e);
printf("max = %d\n", max);
system("pause");
return 0;
}
//结果并未受影响,可是,我们解析的时候,是按照va_arg(arg, int)来解析的,这是为什么?

注意事项

可变参数必须从头到尾逐个访问。如果你在访问了几个可变参数之后想半途终止,这是可以的,但是,如果你
想一开始就访问参数列表中间的参数,那是不行的。
参数列表中至少有一个命名参数。如果连一个命名参数都没有,就无法使用 va_start 。
这些宏是无法直接判断实际存在参数的数量。
这些宏无法判断每个参数的是类型。
如果在 va_arg 中指定了错误的类型,那么其后果是不可预测的。

原理:

1. 可变参数列表对应的函数,最终调用也是函数调用,也要形成栈帧
2. 栈帧形成前,临时变量是要先入栈的,根据之前所学,参数之间位置关系是固定的
3. 通过汇编的学习,发现了短整型在可变参数部分,会默认进行整形提升,那么函数内部在提取该数据的时候,就要考虑提升之后的值,如果不加考虑,获取数据可能会报错或者结果不正确

1.3逐步分析

先看看这几个宏的含义:

//va_list其实就是char*类型,方便后续按照字节进行指针移动
typedef char * va_list;
#define va_start _crt_va_start
#define va_arg _crt_va_arg
#define va_end _crt_va_end

 1.va_start

 我们先看    #define va_start _crt_va_start

定义:#define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )

 再看_ADDRESSOF(v): 

 //取参数的地址,也很好理解
#define _ADDRESSOF(v)  ( &(v) )

还有 _INTSIZEOF(v) :作用是4字节对齐(向上取整)

#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

所以:va_start(arg,num)可以翻译为: 

也就是在可变参数的函数中

int FindMax(int num, ...) 

找到了num后面栈帧的第一个可变参数 

 2.va_arg

再看:#define va_arg _crt_va_arg

定义:

#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) 

使用过程是 va_arg(arg,int)

ap += _INTSIZEOF(t),也就是ap(也是上面提到的arg),指向了下一个元素的位置。

( *(t *)是通用强制转化,提取出指针指向元素类型符合大小的数据

(ap += _INTSIZEOF(t)) - _INTSIZEOF(t),就是指针又回到了原来指的位置,但是!!arg已经指向下一个元素了。现在指针又回来( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )解引用,就是把上一个元素给找到又解引用了;

//这个设计特别巧妙,先让ap指向下个元素,然后使用相对位置-偏移量,访问当前元素。
//访问了当前数据的同时,还让ap指向了后续元素,一举两得。

3.va_end 

#define va_end _crt_va_end

定义:

#define _crt_va_end(ap) ( ap = (va_list)0 ) 

将指针归零,防止野指针 

4._INTSIZEOF(t)

回过头来再看这个:#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

_INTSIZEOF(n)的意思:计算一个最小数字x,满足 x>=n && x%4==0,其实就是一种4字节对齐的方式
是什么:
 比如n是:1,2,3,4 对n进行向 sizeof(int) 的最小整数倍取整的问题 就是 4
 比如n是:5,6,7,8 对n进行向 sizeof(int) 的最小整数倍取整的问题 就是 8

举个例子:

第一步理解:4的倍数


 既然是4的最小整数倍取整,那么本质是:x=4*m,m是具体几倍。

 对7来讲,m就是2,对齐的结果就是8
 而m具体是多少,取决于n是多少
 如果n能整除4,那么m就是n/4

 如果n不能整除4,那么m就是(n/4)+1
 上面是两种情况,如何合并成为一种写法呢?
 
 常见对齐int最小倍数做法是 ( n+sizeof(int)-1) )/sizeof(int) -> (n+4-1)/4
 
 如果n能整除4,那么m就是(n+4-1)/4->(n+3)/4, +3的值无意义,会因取整自动消除,等价于 n/4
 如果n不能整除4,那么n=最大能整除4部分+r,1<=r<4 那么m就是 (n+4-1)/4->(能整除4部分+r+3)/4,其中4<=r+3<7 -> 能整除4部分/4 + (r+3)/4 -> n/4+1 

第二步理解:最小4字节对齐数                


 搞清楚了满足条件最小是几倍问题,那么,计算一个最小数字x,满足 x>=n && x%4==0,就变成了
 
 ((n+sizeof(int)-1)/sizeof(int))[最小几倍] * sizeof(int)[单位大小] -> ((n+4-1)/4)*4


 这样就能求出来4字节对齐的数据了,其实上面的写法,在功能上,已经和源代码中的宏等价了。

第三步理解:理解源代码中的宏


 拿出简洁写法:((n+4-1)/4)* 4,设w=n+4-1, 那么表达式可以变化成为 (w/4)*4,而4就是2^2,w/4,不就相当于右移两位吗?,再次*4不就相当左移两位吗?先右移两位,在左移两位,最终结果就是,最后2个比特位被清空为0!
 需要这么费劲吗?
 w & ~3 不香吗?

 所以,简洁版:(n+4-1) & ~(4-1)
 原码版:( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ),无需先/,在* 

现在整个过程再看,就会非常顺畅了!

二.清晰的回顾一遍 

//
#include <stdio.h>
#include <windows.h>
//num:表示传入参数的个数
int FindMax(int num, ...)
{
va_list arg;     //定义可以访问可变参数部分的变量,其实是一个char*类型
va_start(arg, num); //使arg指向可变参数部分
int max = va_arg(arg, int); //根据类型,获取可变参数列表中的第一个数据
for (int i = 0; i < num - 1; i++){//获取并比较其他的
int curr = va_arg(arg, int);
if (max < curr){
max = curr;
}
}
va_end(arg); //arg使用完毕,收尾工作。本质就是讲arg指向NULL
return max;
}
int main()
{
  //为了方便查看,我们参数换成直观的参数
int max = FindMax(5, 0x11, 0x22, 0x33, 0x44, 0x55);
printf("max = %d\n", max);
system("pause");
return 0;
}

 

三.命令行参数:

main函数也是一个函数,其实也可以携带参数的
 

int main( int argc, char *argv[ ], char *envp[ ] )
{
program-statements
}

那这里是有三个参数的。

第一个参数: argc 是个整型变量,表示命令行参数的个数(含第一个参数)。

第二个参数: argv 是个字符指针的数组,每个元素是一个字符指针,指向一个字符串。这些字符串就是命令行中的每一个参数(字符串)。

第三个参数: envp 是字符指针的数组,数组的每一个原元素是一个指向一个环境变量(字符串)的字符指针。

main函数的argc参数是由操作系统在程序启动时根据命令行参数自动赋值的 

#include <stdio.h>
int main(int argc, char* argv[], char* envp[])
{
int i = 0;
for(i=0; i<argc; i++)
{
printf("%s\n", argv[i]);
}
return 0;
}

 vs中:

 

注:现场绘制命令行参数图,注意:argv数组的最后一个元素存放了一个 NULL 的指针。 

在linux系统中:

 

了解:本质其实是获取系统相关环境变量内容,这个了解一下

作用:可以根据不同的命令行参数表达出不同的内容

envp

#include <stdio.h>
int main(int argc, char* argv[], char* envp[])
{
int i = 0;
while(envp[i] != NULL)
{
printf("%s\n", envp[i]);
i++;
}
return 0;
}

 

 

注: envp 数组的最后一个元素也存放 NULL 指针 

 四.递归

//基本认识
//1. 递归本质也是函数调用,是函数调用,本质就要形成和释放栈帧
//2. 根据栈帧的学习,调用函数是有成本的,这个成本就体现在形成和释放栈帧上:时间+空间
//3. 所以,递归就是不断形成栈帧的过程
//理论认识
//1. 内存和CPU的资源是有限的,也就决定了,合理的递归是绝对不能无限递归下去
//2. 递归不是什么时候都能用,而是要满足自身的应用场景,即:目标问题的子问题,也可以采用相同的算法解决,本质
就是分治的思想
//3. 核心思想:大事化小+递归出口

 

//demo:
//不使用临时变量,求字符串长度

#include <stdio.h>
#include <windows.h>
int MyStrlen(const char *s)
{
if ('\0' == *s){
return 0;
}
return MyStrlen(++s) + 1;
}
int main()
{
const char *str = "abcdefg123456";
int len = MyStrlen(str);
printf("len: %d\n", len);
  system("pause");
return 0;
}
#include <stdio.h>
#include <windows.h>
#include <assert.h>
int MyStrlen(const char *s)
{
assert(s);
return *s ? (MyStrlen(++s) + 1) : 0;
}
int main()
{
const char *str = "abcdefg123456";
int len = MyStrlen(str);
printf("len: %d\n", len);
  system("pause");
return 0;
}

 备注:
assert就是一个判断条件是否成立的宏,了解一下即可
#define assert(_Expression) (void)( (!!(_Expression)) || (_wassert(_CRT_WIDE(#_Expression),
_CRT_WIDE(__FILE__), __LINE__), 0) )

斐波那契数列:

 

//demo1
#include <stdio.h>
#include <windows.h>
 
int Fib(int n)
{
if (1 == n || 2 == n){
return 1;
}
return Fib(n - 1) + Fib(n - 2);
}
int main()
{
int n = 5;
int x = Fib(n);
printf("fib(%d): %d\n", n, x);
 
  system("pause");
  return 0;
}
//通过做实验我们发现,fib递归版在个数超过几十(我们这里是40)的时候,就已经相当慢了
//demo 2
#include <stdio.h>
#include <windows.h> //包含该头文件,才能使用win提供的GetTickCount()函数,来获取开机到现在的累计时间,
此处用它,是因为简单
int Fib(int n)
{
if (1 == n || 2 == n){
return 1;
}
return Fib(n - 1) + Fib(n - 2);
}
int main()
{
int n = 42;
double start = GetTickCount();
int x = Fib(n);
double end = GetTickCount();
printf("fib(%d): %d\n", n, x);
printf("%lf ms\n", (end - start)/1000); //单位是毫秒(ms),转化成为s
  system("pause");
  return 0;
}

//运行结果
fib(42): 267914296
4.844000 ms
请按任意键继续. . .
 
//如果数字再大,就非常非常慢
//为何会这么慢呢?根本原因是因为大量的重复计算

 统计一下调用了多少次fib()

//demo3
#include <stdio.h>
#include <windows.h>  
int count = 0;
int Fib(int n)
{
if (1 == n || 2 == n){
return 1;
}
if (n == 3){
count++; //不影响运算逻辑,就单纯想统计一下n=3的时候,被重复计算了多少次
}
return Fib(n - 1) + Fib(n - 2);
}
int main()
{
int n = 42;
double start = GetTickCount();
int x = Fib(n);
double end = GetTickCount();
printf("fib(%d): %d\n", n, x);
printf("%lf ms\n", (end - start)/1000);
printf("count = %d\n", count);
 
  system("pause");
 
  return 0;
}

 

重复计算,意味着重复调用,重复调用意味着重复形成栈帧,创建与释放栈帧,是有成本的。 

//如何解决?

//这其实是一种动态规划的算法哦,看起来很神秘,其实这些问题也可以使用改方法解决哦

#include <stdio.h>
#include <windows.h>
int Fib(int n)
{
int *dp = (int*)malloc(sizeof(int)*(n+1)); //暂时不做返回值判定了
//[0]不用(当然,也可以用,不过这里我们从1,1开始,为了后续方便)
dp[1] = 1;
dp[2] = 1;
for (int i = 3; i <= n; i++){
dp[i] = dp[i - 1] + dp[i - 2];
}
int ret = dp[n];
free(dp);
return ret;
}
int main()
{
int n = 42;
double start = GetTickCount();
int x = Fib(n);
double end = GetTickCount();
printf("fib(%d): %d\n", n, x);
printf("%lf ms\n", (end - start)/1000);
 
  system("pause");
  return 0;
}

 //还能更进一步吗?任何一个数字,只和前两个数据相关
//优化代码;滚动数组

#include <stdio.h>
#include <windows.h>
int Fib(int n)
{
int first = 1;
int second = 1;
int third = 1;
while (n > 2){
third = second + first;
first = second;
second = third;
n--;
}
return third;
}
int main()
{
int n = 42;
double start = GetTickCount();
int x = Fib(n);
double end = GetTickCount();
printf("fib(%d): %d\n", n, x);
printf("%lf ms\n", (end - start)/1000);
system("pause");
  return 0;
}
//这种方法就是一种典型的迭代方法

标签:va,include,return,int,可变,列表,参数,arg,C语言
From: https://blog.csdn.net/qq_73814284/article/details/136764497

相关文章

  • 【C语言】结构体
    ......
  • 实验1 C语言开发环境使用和数据类型,运算符,表达式
    #include<stdio.h>intmain(){printf("O\n");printf("<H>\n");printf("II\n");return0;}#include<stdio.h>intmain(){printf("O\n");printf("<H>\n");print......
  • C语言基础-2、字符类型
    一、字符类型char是一种整数,也是一种特殊的类型:字符。这是因为:用单引号表示的字符字面量:'a','1'''也是一个字符printf和scanf里用%c来输入输出字符1、字符的输入输出#include<stdio.h>intmain(){ charc,d; c=1; d='1'; if(c==d){ printf("相等\n"); }......
  • c语言实验一
    #include<stdio.h>#include<stdlib.h>intmain(){printf("o\to\n");printf("<H>\t<H>\n");printf("II\tII\n");system("pause");return0;}#include&......
  • 0基础 三个月掌握C语言(11)
    字符函数和字符串函数为了方便操作字符和字符串C语言标准库中提供了一系列库函数接下来我们学习一下这些函数字符分类函数C语言提供了一系列用于字符分类的函数,这些函数定义在ctype.h头文件中。这些函数通常用于检查字符是否属于特定的类别,例如大写字母、小写字母、数字......
  • C语言葵花宝典之——文件操作
    前言:在之前的学习中,我们所写的C语言程序总是在运行结束之后,就会自动销毁,那如果我们想将一个结果进行长期存储应该如何操作呢?这时候就需要我们用文件来操作。目录1、什么是文件?1.1程序文件1.2数据文件1.3文件名2、二进制文件和文本文件2.1文本文件:2.2二进制文......
  • C语言基础-1、指针
    一、取地址运算运算符&scanf("%d",&i);中的&是获得变量的地址,它的操作对象必须是变量&不能对没有地址的东西取地址:&(a+b),&(a++)二、指针就是保存地址的变量inti;int*p=&i;int*p,q//p是一个指针,是一个指向int型的指针变量,q则是一个单纯的int型变量1、指针变量变量......
  • pyCharm oj 习题 列表合并、去重、排序
    列表合并、去重、排序ProblemDescription从键盘输入两个数列,构成两个列表list1、list2,合并这两个列表为list3,将list3去掉重复元素、降序排序后生成list4.InputDescription输入两个数列,以英文逗号分隔OutputDescription输出列表list1、list2、list3、list4SampleInpu......
  • C语言-动态内存管理
    动态内存管理为什么存在动态内存分配动态内存函数介绍malloc和freecallocrealloc常见动态内存错误1对NULL指针的解引用操作2对动态开辟空间的越界访问3对非动态开辟内存使用free释放4使用free释放一块动态开辟内存的一部分5.对同一块动态内存多次释放6.动态开辟......
  • c语言程序设计——实验报告一
    c语言程序设计——实验报告一实验项目名称:实验一熟悉C语言运行环境实验项目类型:验证性实验日期:2023年3月14日一、实验目的下载安装Devc6.0程序。了解在该系统上如何进行编辑、编译、连接和运行一个C程序。通过运行简单的C程序了解C程序的特点。二、实验硬、软件环境W......