如果argv是char以外的类型,C会怎么做

时间:2013-05-27 18:32:24

标签: c command-line arguments

我在C中乱搞,并且认为尝试将argv的类型从char *更改为int很酷,只是为了看看会发生什么。我写了这个:

#include <stdlib.h>
#include <stdio.h>
int main(int argc, int  argv )
{
        printf("arg is %d \n", argv);
}

我从这个程序得到了非常奇怪的输出。每当我运行它时,无论我运行什么参数,它似乎只是向我吐出随机数。这是输出:

[14:30:00][maksim]~/learnProg/cDance$ ./dink
arg is -2058142376 
[14:30:01][maksim]~/learnProg/cDance$ ./dink 2141
arg is 2111473256 
[14:30:04][maksim]~/learnProg/cDance$ ./dink 2141
arg is -8005928 

(该程序称为丁克)。这是怎么回事? C编译时做了什么?如果我使用除int之外的数据类型,如double或者结构或其他什么会发生什么?

7 个答案:

答案 0 :(得分:4)

正如其他人所说,行为未定义(因此可能发生任何事情)。

让我们来看看三个&#34;典型&#34;虽然行为。传递参数的三种常用方法是:

  • 在堆栈上
  • 在通用寄存器中
  • 在专用寄存器中

Intel x86系统大多使用第一种方法(但有时是第二种或第三种方法)。基于MIPS的处理器大多使用第二种处理器。

如果系统使用一个或多个堆栈,通常的调用方法是:

    调用者中的
  • (一些OS提供的调用main的例程),推送参数,通常从右到左,即以相反的顺序。堆栈通常(但不总是)在C中看起来像*--sp = value;,堆栈指针从某个高地址开始下降。
  • 调用目标函数(main
  • 在目标函数中,检索参数&#34;堆栈&#34;或&#34;参数堆栈&#34;或者&#34;当前的线程堆栈&#34;或者系统使用的任何东西。因为它们按相反顺序推送,所以它们位于sp[0]sp[1]等地址。如果调用机制使用相同的堆栈作为参数传递机制,则索引可以从1或2开始甚至更多(例如sp[2]是第一个参数,sp[3]是第二个参数。

在这种情况下,argc可能会正确显示,但argv会错误地解释调用者所推送的内容,从而产生一种奇怪的int。如果底层系统足够花哨(检查类型),它可能会检测到调用者推送了char **类型的值,但您正在访问int类型的值,并为您提供某种类型的运行时错误。尽管如此,大多数系统都希望尽快给出错误的答案,但不要使用类型检查。所以你会得到一个看起来很奇怪的int,但它实际上是基于调用者试图通过的实际指针值(至少部分见下文)。

如果系统使用通用寄存器(代替或使用使用GPR的堆栈系统之前,如果使用许多参数,则通常会回退到堆栈上,有时将它们用于所有可变参数函数,即使用<stdarg.h>设施),然后调用方法看起来更像这样:

  • 在调用者中,将参数(int argc值和char **argv值)移动到前两个参数寄存器中(例如,SPARC上的%o0%o1,或{{ MIPS上的1}}和$a0)。
  • 调用目标函数
  • 在目标函数中,访问参数寄存器
  • 中的值

在这种情况下,代码通常与基于堆栈的系统上的行为相同。它运行得更快,因为寄存器中的参数往往比内存中的参数需要更少的CPU周期。 (这就是为什么有些英特尔编译器有时会在寄存器中传递一两个参数。)

如果系统使用专用寄存器,我们会得到一个新的明显行为。让我们说浮点值进入$a1寄存器(某些SPARC系统是真的; x86代替MMX和SSE寄存器);指针值进入f寄存器(a 680x0 CPU);整数值进入a寄存器(680x0,再次 - 尽管实际上大多数680x0系统只使用&#34;堆栈&#34;但是让我们假设我们有一个使用寄存器)。这一次,调用d的东西需要传递一个整数main和一个指针argc,所以它会这样做:

  • 将整数参数argv移动到数据寄存器argc
  • 将指针参数d0移动到指针寄存器argv
  • 致电a0

现在,在main中,您告诉编译器期望两个整数参数,它们分别到达寄存器main()d0。 CPU注册d1中的内容是什么?谁知道,调用d1之前没有在调用之前设置它。它拥有它拥有的任何价值,无论是谁最后在其中获得了一些价值。该值不再与预期的main相关联,因为它位于注册argv中。

现在,即使你有一个基于堆栈或基于GPR的呼叫系统,还需要考虑另外几个问题:

  • 如果指针是64位而普通a0 s只有32位怎么办?在这种情况下,调用者按下64位值,或将64位值写入参数寄存器;但int只看32位。你会看到实际给出的一半。
  • 如果指针是32位而普通main是64位怎么办?这是一个不寻常的实现,当然,但现在你要查看只提供32的值的所有64位。&#34; extra&#34; 32位可能全为零(这对于GPR中的参数是典型的),或者可能是某些无关值的32位,类似于int时检查寄存器d1的情况。来电者填写了注册号main
  • 当然,没有什么说32和64位是唯一可能的尺寸。在IBM AS / 400系统上,指针是一个高达128位的指针(16字节标记指针),并且有大量的运行时类型检查。这些机器确保代码正确,而不仅仅是快速。

还有一个值得注意的可能性。如果您构建类似的C ++代码(使用a0以外的函数),它通常无法链接。原因是C ++编译器经常使用一种名为&#34; name mangling&#34;处理重载的函数。名为main的函数接受一个f和一个int参数并返回char **,生成链接时符号int。名为Z1fiPPC的函数需要两个f并返回int,而是生成链接时符号int。我没有看到C编译器这样做,但他们可以做到这一点。在这种情况下,编译器会在链接时检查您的程序是否定义了Z1fii - Z4mainippC - 如果是,则在提供这些参数的调用者中进行链接;或者它会检查int main(int, char **) - Z4mainv - 并且在那种情况下,调用者中的链接不提供任何参数。如果找不到任何函数,链接器可能会检测到您写了不正确的int main(void)并且根本没有生成可执行文件!

答案 1 :(得分:3)

您将获得未定义的行为,这意味着任何发生是合法的。 main必须声明为:

int main(void)

或作为:

int main(int arg, char** argv)

或您的实施所指定的某种形式。

从ISO C99标准的J.2节开始:

  

在以下情况下,行为未定义:

     

...

     
      
  • 托管环境中的程序未使用其中一个指定的表单(5.1.2.2.1)定义名为main的函数。
  •   

答案 2 :(得分:2)

argv作为指向字符串指针数组的指针传递给您的程序。

如果说谎并告诉编译器它是int,指针的字节将被解释为int,你将获得一个内存地址。 (在64位系统上,你可能会崩溃)

如果假装它是float,编译器可能会将这些字节/位解释为IEE-754编码的浮点值,从而产生不同的奇怪数字。 (究竟发生了什么取决于调用约定)

如果你假装它的任何类型与指针的宽度不同,你可能会崩溃。

故事的寓意是

C完全按照你所说的去做。由你来告诉它如何解释事物。

答案 3 :(得分:0)

嗯...

Argv是一个数组。在C中,数组只是指针。指针内部只是内存位置的整数。所以,你看到的数字是内存中的位置。 (我猜这些否定因为它不是无符号的)

答案 4 :(得分:0)

C main()函数接收参数计数的整数和指向char数组的指针。

您的输出只是该指针包含的内存地址。 如果将其强制转换为其他变量类型,它们也将包含“垃圾”。

在正常情况下,如果可能的话,应该避免使用指针。

答案 5 :(得分:0)

让我们首先了解argv究竟是什么。

考虑标准main()格式。这是int main(int argc, char *argv[]) 这里argv是一个字符指针数组。由于数组的名称是指向它的第一个成员的常量指针,我们会说argv是指向它的第一个成员的指针。即argv是指向字符指针的指针。

现在请注意名字在这里无关紧要。它可以是argv旁边的任何内容。重要的是main()的第二个参数是指向字符指针的指针。即第二个参数是指向字符的指针。

因此,当程序开始执行时,内存地址作为第二个参数传递给main(),这是另一个指针的地址。而'另一个'指针是第一个参数的第一个字符的内存地址。而这个论点恰好是程序的名称。

因此,当您说int main(int argc, int argv )时,您正在使用int值投射地址。如果sizeof(int) == sizeof(int *)那么那根本不是问题。在这种情况下,该值不会降级。

现在,当您说printf("arg is %d \n", argv);时,您只是打印该地址。而已!无论你给一个程序的参数是什么,地址都是一个随机值。这就是为什么你得到随机的no.s,它实际上是argv数组的第一个成员的地址。即没有。 print是程序名称的地址,而程序名称的地址又是它的第一个char的地址。 (因为程序名称又是一个数组,因此它是指向它的第一个成员的常量指针。即第一个字符)

要验证这一点,请将此行添加到您的代码段:

printf("%c\n", **(char **)argv);  

你会看到.正在打印,这确实是第一个参数的第一个字符./dink

答案 6 :(得分:0)

我并不反对大多数其他答案,正如jamesdlin所说,如果main未正确声明,C99将行为指定为未定义。我想那时你的问题就变成了这个所谓的未定义行为。我说“所谓的未定义行为”,因为它实际上非常精确地定义为平台/系统应用程序二进制接口(ABI)的一部分。虽然ABI可能没有专门解决您将指针作为int传递的情况,但它确实定义了参数的传递方式,因此一些研究将准确揭示您的特定情况中发生的情况。

由于ABI回答了所有关于“如果我将其作为int,double或结构传递会发生什么”的问题,那么您的下一个问题可能是“我的系统的ABI是什么”。 ABI是特定于系统/平台的,它可能在Windows和Linux之间,PowerPC和X86之间,不同编译器之间,甚至不同版本的编译器之间有所不同。您没有提供必要的平台/系统信息来回答“哪个ABI”问题,但是,即使您提供了它,我也无意回答它,因为我需要进行研究(我不是专家) 。此外,这是您的实验,因此您将很好地学习和理解系统的ABI。

有很多好的信息,包括提问what is the ABI的问题,Linux ABI的简要概述,当然还有wikipedia page。 ABI问题提供了System V ABI PDF的链接,很可能涵盖了您的系统ABI,因此可能是最好的起点。

总而言之,您的实验根据C99导致未定义的行为,但实际行为由系统ABI定义,但系统ABI是系统特定的。换句话说,C99没有指定实验中的行为,因为它是C99之外的系统特定行为。另一方面,系统特定的ABI将行为定义为参数传递定义的一部分。通过了解系统的ABI,您将能够理解(即定义)您所看到的行为。很可能这个定义有点不起眼,例如,int参数和指针参数不兼容,因此您收到的int参数确实是随机垃圾,恰好位于某个寄存器或记忆位置。或者它可能是64位指针的上部或下部32位。