精华内容
下载资源
问答
  • C语言未定义行为

    千次阅读 2018-04-08 16:58:16
    写在前面的话: ...感谢博主,是真的结合自己的感受写出来的,而且练习题哈哈哈和我遇上的也...简单地说,未定义行为是指C语言标准未做规定的行为。编译器可能不会报错,但是这些行为编译器会自行处理,所以不同的编译...

    写在前面的话:
    原文链接为:https://blog.csdn.net/qq_29169813/article/details/51416281。感谢博主,是真的结合自己的感受写出来的,而且练习题哈哈哈和我遇上的也是一样的。我申请过啦,博主同意我转载的哦~

    1.什么是未定义行为

    简单地说,未定义行为是指C语言标准未做规定的行为。编译器可能不会报错,但是这些行为编译器会自行处理,所以不同的编译器会出现不同的结果,什么都有可能发生,这是一个极大的隐患,所以我们应该尽量避免这种情况的发生。

    1.1特征

    1. 首先定义不确定副作用行为:

      粗略而言:是指在同一个表达式中同一对象修改一次以上的行为。
      诸如:同一变量被修改以后又进行了引用的自增, 自减和赋值

    2. 未定义行为是指:包含多个不确定的副作用的代码的行为

    在网上了解了一番,发现未定义行为有很多,而我初出茅庐,遇到的情况不多,只有借鉴前人的经验。总结了一些前人遇到的问题。下面三种未定义行为是前人总结的,我只是加上了一点自己的理解。有错误望指出。
    附原文地址:http://www.itoldme.net/archives/904

    1.1.1第一例(同一个表达式中有多种运算符)

    同一个表达式中多种运算符一起计算的时候,即使我们知道各符号都有自己的优先级或者是人为的加上括号限制计算顺序,但是我们却不知道编译器会先计算哪一段,计算顺序完全取决于编译器,所以结果并不一定按照我们预想中的输出。

    代码段一:

    int i=7; 
    printf(“%d”, i++*i++); 

    编译器可能选择变量的旧值相乘以后再对二者进行自增运算,但是无法确保自增或自减一定会在1.输出变量原值之后,2.对表达式的其它部分计算之前立即进行。

    代码段二

    int a=5,b;
    b=++a*–a;

    b的值不能确定

    代码段三

     int i = 5;
     int j = (++i) + (++i) + (++i);

    j j <script type="math/tex" id="MathJax-Element-9">j</script> 的值不能确定

    代码段四

    #include <stdio.h>
    int main(){
        int i = 0;
        int a[] = {10,20,30};
    
        int r = 1 * a[i++] + 2 * a[i++] + 3 * a[i++];
        printf("%d\n", r);
        return 0;
    }

    这段代码也并不是我们想象中的那样按照优先级来计算,编译器选择了他自己的一种套路,
    此段代码详细实现情况请戳链接:http://blog.jobbole.com/53211/

    1.1.2 第二例(同一语句中各参数的求值顺序)

    在同一语句中,有多个表达式,我们不能确定编译器先调用哪一个表达式进行运算,运算之后又会对另一个表达式产生影响,因为他不一定是按照我们想象中自左向右进行调用的。
    代码段一

     printf("%d,%d\n",++n,power(2,n)); 

    代码段二

    int f(int a, int b);
    int i = 5;
    f(++i, ++i);

    1.1.3第三例(通过指针修改const常量的值)

    编译器对于向常量所在内存赋值这件事的处理是未定义的。即在对常量的内存操作也许并不是我们想象的那样。
    代码段一

    int main()
    {
        const int a = 1;
        int *b = (int*)&a;
        *b = 21;
    
        printf("%d, %d", a, *b);
        return 0;
    }

    该段代码的详细实现请戳链接:http://www.cnblogs.com/wghost/p/3280074.html

    练习题

    这里写图片描述

    解析:根据上面的总结,A选项,我的理解是我们不知道编译器会怎么选择自增和赋值的顺序,所以这是由编译器决定的,属于未定义行为。B选项,”hello“这个字符串属于一个字符串常量了,指针p指向了这个字符串常量,下一语句通过这个指针来直接修改常量第二个字符,这也属于未定义行为。选项C,只是通过指针找到第二个字符并将它赋值给一个字符变量,并没有改变这个字符串常量,所以不属于未定义行为。选项D,在printf语句中,i++和i–谁先执行由编译器决定,这是未定义行为。故此题选C。

    展开全文
  • C语言未定义行为一览

    2021-05-21 04:58:53
    最近我们一直在互相考问C语言的知识,所以我微笑着鼓起勇气面对无疑即将到来的地狱。他在白板上写了几行代码,并问这个程序会输出什么?#include int main(){int i = 0;int a[] = {10,20,30};int r = 1 * a[i++] + 2...

    编者注:文章中的"我"是指原文作者

    几周前,我的一位同事带着一个编程问题来到我桌前。最近我们一直在互相考问C语言的知识,所以我微笑着鼓起勇气面对无疑即将到来的地狱。

    他在白板上写了几行代码,并问这个程序会输出什么?

    #include

    int main(){

    int i = 0;

    int a[] = {10,20,30};

    int r = 1 * a[i++] + 2 * a[i++] + 3 * a[i++];

    printf("%d\n", r);

    return 0;

    }

    看上去相当简单明了。我解释了操作符的优先顺序——后缀操作比乘法先计算、乘法比加法先计算,并且乘法和加法的结合性都是从左到右,于是我抓出运算符号并开始写出算式。

    int r = 1 * a[i++] + 2 * a[i++] + 3 * a[i++];

    // = a[0] + 2 * a[1] + 3 * a[2];

    // = 10 + 40 + 90;

    // = 140

    我自鸣得意地写下答案后,我的同事回应了一个简单的“不”。我想了几分钟后,还是被难住了。我不太记得后缀操作符的结合顺序了。此外,我知道那个顺序甚至不会改变这里的值计算的顺序,因为结合规则只会应用于同级的操作符之间。但我想到了应该根据后缀操作符都从右到左求值的规则,尝试算一遍这条算式。看上去相当简单明了。

    int r = 1 * a[i++] + 2 * a[i++] + 3 * a[i++];

    // = a[2] + 2 * a[1] + 3 * a[0];

    // = 30 + 40 + 30;

    // = 100

    我的同事再一次回答说,答案仍是错的。这时候我只好认输了,问他答案是什么。这段短小的样例代码原来是从他写过的更大的代码段里删减出来的。为了验证他的问题,他编译并且运行了那个更大的代码样例,但是惊奇地发现那段代码没有按照他预想的运行。他删减了不需要的步骤后得到了上面的样例代码,用gcc 4.7.3编译了这段样例代码,结果输出了令人吃惊的结果:“60”。

    这时我被迷住了。我记得,C语言里,函数参数的计算求值顺序是未定义的,所以我们以为后缀操作符只是遵照某个随机的、而非从左至右的顺序,计算的。我们仍然确信后缀比加法和乘法拥有更高的操作优先级,所以很快证明我们自己,不存在我们可以计算i++的顺序,使得这三个数组元素一起加起来、乘起来得到60。

    现在我已对此入迷了。我的第一个想法是,查看这段代码的反汇编代码,然后尝试查出它实际上发生了什么。我用调试符号(debugging symbols)编译了这段样例代码,用了objdump后很快得到了带注释的x86_64反汇编代码。

    Disassembly of section .text:

    0000000000000000 :

    #include

    int main(){

    0: 55 push %rbp

    1: 48 89 e5 mov %rsp,%rbp

    4: 48 83 ec 20 sub $0x20,%rsp

    int i = 0;

    8: c7 45 e8 00 00 00 00 movl $0x0,-0x18(%rbp)

    int a[] = {10,20,30};

    f: c7 45 f0 0a 00 00 00 movl $0xa,-0x10(%rbp)

    16: c7 45 f4 14 00 00 00 movl $0x14,-0xc(%rbp)

    1d: c7 45 f8 1e 00 00 00 movl $0x1e,-0x8(%rbp)

    int r = 1 * a[i++] + 2 * a[i++] + 3 * a[i++];

    24: 8b 45 e8 mov -0x18(%rbp),%eax

    27: 48 98 cltq

    29: 8b 54 85 f0 mov -0x10(%rbp,%rax,4),%edx

    2d: 8b 45 e8 mov -0x18(%rbp),%eax

    30: 48 98 cltq

    32: 8b 44 85 f0 mov -0x10(%rbp,%rax,4),%eax

    36: 01 c0 add %eax,%eax

    38: 8d 0c 02 lea (%rdx,%rax,1),%ecx

    3b: 8b 45 e8 mov -0x18(%rbp),%eax

    3e: 48 98 cltq

    40: 8b 54 85 f0 mov -0x10(%rbp,%rax,4),%edx

    44: 89 d0 mov %edx,%eax

    46: 01 c0 add %eax,%eax

    48: 01 d0 add %edx,%eax

    4a: 01 c8 add %ecx,%eax

    4c: 89 45 ec mov %eax,-0x14(%rbp)

    4f: 83 45 e8 01 addl $0x1,-0x18(%rbp)

    53: 83 45 e8 01 addl $0x1,-0x18(%rbp)

    57: 83 45 e8 01 addl $0x1,-0x18(%rbp)

    printf("%d\n", r);

    5b: 8b 45 ec mov -0x14(%rbp),%eax

    5e: 89 c6 mov %eax,%esi

    60: bf 00 00 00 00 mov $0x0,%edi

    65: b8 00 00 00 00 mov $0x0,%eax

    6a: e8 00 00 00 00 callq 6f

    return 0;

    6f: b8 00 00 00 00 mov $0x0,%eax

    }

    74: c9 leaveq

    75: c3 retq

    最先和最后的几个指令只建立了堆栈结构,初始化变量的值,调用printf函数,还从main函数返回。所以我们实际上只需要关心从0×24到0×57之间的指令。那是令人关注的行为发生的地方。让我们每次查看几个指令。

    24: 8b 45 e8 mov -0x18(%rbp),%eax

    27: 48 98 cltq

    29: 8b 54 85 f0 mov -0x10(%rbp,%rax,4),%edx

    最先的三个指令与我们预期的一致。首先,它把i(0)的值加载到eax寄存器,带符号扩展到64位,然后加载a[0]到edx寄存器。这里的乘以1的运算(1*)显然被编译器优化后去除了,但是一切看起来都正常。接下来的几个指令开始时也大致相同。

    2d: 8b 45 e8 mov -0x18(%rbp),%eax

    30: 48 98 cltq

    32: 8b 44 85 f0 mov -0x10(%rbp,%rax,4),%eax

    36: 01 c0 add %eax,%eax

    38: 8d 0c 02 lea (%rdx,%rax,1),%ecx

    第一个mov指令把i的值(仍然是0)加载进eax寄存器,带符号扩展到64位,然后加载a[0]进eax寄存器。有意思的事情发生了——我们再次期待i++在这三条指令之前已经运行过了,但也许最后两条指令会用某种汇编的魔法来得到预期的结果(2a[1])。这两条指令把eax寄存器的值自加了一次,实际上执行了2a[0]的操作,然后把结果加到前面的计算结果上,并存进ecx寄存器。此时指令已经求得了a[0] + 2 * a[0]的值。事情开始看起来有一些奇怪了,然而再一次,也许某个编译器魔法在发生。

    3b: 8b 45 e8 mov -0x18(%rbp),%eax

    3e: 48 98 cltq

    40: 8b 54 85 f0 mov -0x10(%rbp,%rax,4),%edx

    44: 89 d0 mov %edx,%eax

    接下来这些指令开始看上去相当熟悉。他们家在i的值(仍然是0),带符号扩展至64位,加载a[0]?到edx寄存器,然后拷贝edx里的值到eax。嗯,好吧,让我们在多看一些:

    46: 01 c0 add %eax,%eax

    48: 01 d0 add %edx,%eax

    4a: 01 c8 add %ecx,%eax

    4c: 89 45 ec mov %eax,-0x14(%rbp)

    在这里把a[0]自加了3次,再加上之前的计算结果,然后存入到变量“r”。现在不可思议的事情——我们的变量r现在包含了a[0] + 2 * a[0] + 3 * a[0]。足够肯定的是,那就是程序的输出:“60”。但是那些后缀操作符上发生了什么?他们都在最后:

    4f: 83 45 e8 01 addl $0x1,-0x18(%rbp)

    53: 83 45 e8 01 addl $0x1,-0x18(%rbp)

    57: 83 45 e8 01 addl $0x1,-0x18(%rbp)

    看上去我们编译版本的代码完全错了!为什么后缀操作符被扔到最底下、所有任务已经完成之后?随着我对现实的信仰减少,我决定直接去看源代码。不,不是编译器的源代码——那只是实现——我抓起了C11语言规范。

    这个问题处在后缀操作符的细节。在我们的案例中,我们在单个表达式里对数组下标执行了三次后缀自增。当计算后缀操作符时,它返回变量的初始值。把新的值再分配回变量是一个副作用。结果是,那个副作用只被定义为只被付诸于各顺序点之间。参照标准的5.1.2.3章节,那里定义了顺序点的细节。但在我们的例子中,我们的表达式展示了未定义行为。它完全取决于编译器对于 什么时候 给变量分配新值的副作用会执行 相对于表达式的其他部分。

    最终,我俩都学到了一点新的C语言知识。众所周知,最好的应用是避免构造复杂的前缀后缀表达式,这就是一个关于为什么要这样的极好例子。

    展开全文
  • 传递两个指向同一int对象的指针时,将产生未定义行为。 我猜想,引入这一特性最初的动机之一是想让C语言在数值计算时可以Fortran一样快。在Fortran 中,默认假定数组不会重叠,因此只有你通过 restrict  限定...

    译注:本文摘编自 Quora 的一个热门问答贴。 请在linux系统下测试本文中出现的代码

    Andrew Weimholt 的回复:

    switch语句中的case 关键词可以放在if-else或者是循环当中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    switch (a)
    {
         case 1:;
           // ...
           if (b==2)
           {
             case 2:;
             // ...
           }
           else case 3:
           {
             // ...
             for (b=0;b<10;b++)
             {
               case 5:;
               // ...
             }
           }
           break ;
     
         case 4:

    Brian Bi 的回复:

    1. 声明紧随用途之后

    理解声明有一条很简单的法则,不过不是什么“从左向右”这种没道理却到处宣传的法则。这一法则的观点是,一个声明是要告诉你,你所声明的对象要如何使用。例如:

    1
    2
    3
    4
    int *p; /* *p是int类型的, 因此p是指向int类型的指针 */
    int a[5]; /* a[0], ..., a[4] 是int类型的, 因此a是int类型的数组 */
    int *ap[5]; /* *ap[0], .., *ap[4] 是int类型的, 因此ap是包含指向int类型指针的指针数组 */
    int (*pa)[5]; /* (*pa)[0], ..., (*pa)[4] 是int类型的, 因此pa是指向一个int类型数组的指针 */

    更多详情请看这里: Brian Bi’s answer to C (programming language): Why doesn’t C use better notation for pointers?

    2. 指定初始化:

    在C99之前,你只能按顺序初始化一个结构体。在C99中你可以这样做:

    1
    2
    3
    4
    5
    6
    struct Foo {
         int x;
         int y;
         int z;
    };
    Foo foo = {.z = 3, .x = 5};

    这段代码首先初始化了foo.z,然后初始化了foo.xfoo.y 没有被初始化,所以被置为0。
    这一语法同样可以被用在数组中。以下三行代码是等价的:

    1
    2
    3
    int a[5] = {[1] = 2, [4] = 5};
    int a[] = {[1] = 2, [4] = 5};
    int a[5] = {0, 2, 0, 0, 5};

    3. 受限指针(C99):

    restrict关键词是一个限定词,可以被用在指针上。它向编译器保证,在这个指针的生命周期内,任何通过该指针访问的内存,都只能被这个指针改变。比如,在

    1
    2
    3
    4
    5
    6
    int f( const int * restrict x, int * y) {
         (*y)++;
         int z = *x;
         (*y)--;
         return z;
    }

    编译器可能会假设,xy 所指的并不是同一个int对象,因为如果它们指向了同一个对象,则x的值将可以通过y修改,这正是你保证不会发生的。因此,将允许编译器来优化f,就好像函数原本被写做如下这样:

    1
    2
    3
    int f( const int * restrict x, int * y) {
         return *x;
    }

    如果你违反协议向f传递两个指向同一int对象的指针时,将产生未定义行为。

    我猜想,引入这一特性最初的动机之一是想让C语言在数值计算时可以Fortran一样快。在Fortran 中,默认假定数组不会重叠,因此只有你通过restrict 限定词来显式的告诉编译器数组不能重叠,编译器才能在C语言中进行这样的优化。

    4. 静态数组索引(C99)

    1
    2
    3
    void f( int a[ static 10]) {
         /* ... */
    }

    中,你向编译器保证,你传递给f 的指针指向一个具有至少10个int 类型元素的数组的首个元素。我猜这也是为了优化;例如,编译器将会假定a 非空。编译器还会在你尝试要将一个可以被静态确定为null的指针传入或是一个数组太小的时候发出警告。

    1
    2
    3
    void f( int a[ const ]) {
         /* ... */
    }

    你不能修改指针a.,这和说明符int * const a.作用是一样的。然而,当你结合上一段中提到的static 使用,比如在int a[static const 10] 中,你可以获得一些使用指针风格无法得到的东西。

    5. 泛型表达式(C11)

    这个表达式会在编译期间根据控制表达式的类型,在一个含有一个或多个备选方案的集合中做出选择。下面这个例子可以很好的说明这一切:

    1
    2
    3
    4
    5
    #define cbrt(X) _Generic((X), \
                             long double : cbrtl, \
                             default : cbrt, \
                             float : cbrtf \
                             )(X)

    因此,如果expr 是long double类型的, cbrt(expr) 被转换为cbrtl(expr),如果是float类型 则转换为cbrtf(expr) ,或是转换为cbrt(expr),如果是其他不同的类型(比如说double )。注意,_Generic 可以用在宏以外的地方,但是用在宏里面最好因为C不允许你进行函数重载。

    6. wint_t (C99)

    我相信大家都知道wint_t 但是 wint_t 到底是个什么鬼东西呢?

    好吧,记住fgetc 实际上并不会返回 char 。它会返回int。显然这是因为fgetc 必须返回返回一个与其他char 都不同的值,也就是EOF,表示到达文件末尾。基于相同的原因,fgetwc 并不返回wchar_t。它会返回一个类型,叫做wint_t 可以表示所有无效wchar_t 类型,包括WEOF,来表示到达文件末尾。

    Michal Forišek

    下面这段C程序可以准确的打印2的747次方而不产生误差。这是为什么呢?

    程序:

    1
    2
    3
    4
    5
    6
    #include <stdio.h>
    #include <math.h>
    int main() {
         printf ( "%.0f\n" , pow (2,747));
         return 0;
    }

    输出结果:

    1
    740298315191606967520227188330889966610377319868419938630605715764070011466206019559325413145373572325939050053182159998975553533608824916574615132828322000124194610605645134711392062011527273571616649243219599128195212771328

    答案:

    这个问题包含两个部分。
    其一,2的次方可以在double 中被准确的保存而不产生任何精度上的损失(这一结论直到2^1023都是对的,再往后就会产生上溢,得到一个正无穷的值)

    另外一部分,很多人猜测是语言实现中的某些特殊情况导致的,但是实际上并非如此。的确,当输入的数据可以被2的某高次方整除时,有一部分代码被执行了,但是本质上这只是通常实现工作时的一个副作用。基本上,printf 在打印数字(任何类型)的时候只是做了从二进制到十进制的转换。并且由于结果对于浮点数可能会过大,printf 的内部实现包含和使用一个大整型实现,尽管在C中并没有大整型这种变量(在gcc源代码中,vfprintf.c 和dtoa.c 中包含了很多转换,如果你想要了解可以一看。)

    如果你尝试打印3^474,

    程序:

    1
    2
    3
    4
    5
    6
    #include <stdio.h>
    #include <math.h>
    int main() {
         printf ( "%.0f\n" , pow (3,474));
         return 0;
    }

    输出结果:

    14304567688284661153278974752312031583901259203711201647725006924333106634519194823303091330277684776547167093155518867557708479462413116497799842448027156309852771422896137582164841870381535840058702788340257784498862132559872

    结果仍然是一个很大的数且位数也正确,但是这一次却不够精确。这里会产生一个相对误差,因为3^474不能以双精度浮点数准确的表示。准确的数应该是这样的143045676882846603471

    译注:在linux系统上是可以的,在windows 64位上后面会有很多0

    Utkal Sinha

    我发现一些C语言特性或者是小技巧,我觉得只有很少的人知道。

    1. 不使用加号来使数字相加

    因为printf() 函数返回它所打印的字符的个数,我们可以利用这一点来使数字相加,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include<stdio.h>;
     
    int add( int a, int b){
         if ( if (a!=0&&b!=0))
             return printf ( "%*c%*c" ,a, '\r' ,b, '\r' );
         else return a!=0?a:b;
    }
     
    int main(){
         int A = 0, B = 0;
         printf ( "Enter the two numbers to add\n" );
         scanf ( "%d %d" ,&A,&B);
         printf ( "Required sum is %d" ,add(A,B));
     
         return 0;
    }

    利用位操作同样也可以做到:

    1
    2
    3
    4
    5
    6
    7
    int Add( int x, int y)
    {
         if (y == 0)
             return x;
         else
             return Add( x ^ y, (x & y) << 1);
    }

    2. 条件运算符的用法

    通常我们都这样使用它:
    x = (y < 0) ? 10 : 20;
    但是同样也可以这样用:
    (y < 0 ? x : y) = 20;

    3. 在一个返回值为void 的函数中写一个return 语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    static void foo ( void ) { }
    static void bar ( void ) {
    return foo(); // 注意这里的返回语句.
    }
     
    int main ( void ) {
    bar();
    return 0;
    }

    4. 逗号表达式的使用

    通常逗号表达式会这样使用:

    1
    2
    3
    4
    for ( int i=0; i<10; i++, doSomethingElse())
    {
       /* whatever */
    }

    但是你可以在其他任何地方使用逗号表达式:

    1
    int j = ( printf ( "Assigning variable j\n" ), getValueFromSomewhere());

    每条语句都进行了求值,但是表达式的值是最后一个语句的值。

    5. 将结构体初始化为0

    struct mystruct a = {0};

    这将把结构体中全部元素初始化为0

    6. 多字符常量

    int x = 'ABCD';

    这会把x的值设置为0×41424344(或者0×44434241,取决于架构)

    7. printf 允许你使用变量来格式化格式说明符本身

    1
    2
    3
    4
    5
    6
    7
    8
    #include <stdio.h>
     
    int main() {
         int a = 3;
         float b = 6.412355;
         printf ( "%.*f\n" ,a,b);
         return 0;
    }

    * 符号可以达到这一目的

    希望这些可以帮助到大家
    此致敬礼

    Vivek Nagarajan

    你可以在奇怪的地方使用#include

    如果你写:

    1
    2
    3
    4
    5
    6
    7
    #include <stdio.h>
     
    void main()
    {
         printf
    #include "fragment.c"       
    }

    fragment.c 包含:

    1
    ( "dayum!\n" );

    这完全没有问题。只要#include 包含完整可解析的C表达式,预处理器并不在意它放在什么位置。

    Vipul Mehta

    1. printf 格式限定符中指定(POSIX扩展语法)

    printf("%4$d %3$d %2$d %1$d", 1, 2, 3, 9); //将会打印9 3 2 1

    2. 在scanf 中忽略输入输入

    scanf("%*d%d", &a);// 如果输入1 2,则只会得到2

    3. 在switch 中使用范围(gcc扩展语法)

    1
    2
    3
    4
    5
    switch (c) {
       case 'A' ... 'Z' : //do something
       break ;
       case 1 ... 5 : //do something
    }

    4. 使用前缀ob 来限定常数,使其被当做二进制数(gcc扩展语法)

    1
    printf ( "%d" ,0b1101); // prints 13

    5.完全正确的最短的C语言程序

    1
    main;

    译注:虽然编译没有error但是却不能执行

    Karan Bansal

    scanf()的力量

    假定我们有一个数组char a[100]
    读取一个字符串:
    scanf("%[^\n]\n", a);//表示一直读取直到遇到'\n',并且忽略掉'\n'

    读取字符串直到遇到逗号:
    scanf("%[^,]", a);//但是这次不会忽略逗号

    如果你想忽略掉某个输入,使用在% 后使用*,如果你想要得到John Smith 的姓:

    1
    scanf ( "%s %s" , temp, last_name); //典型答案,使用一个临时变量
    1
    2
    3
    scanf ( "%s" , last_name);
    scanf ( "%s" , last_name);
    // 另一种答案,使用一个变量但是调用两次 `scanf()`
    1
    2
    scanf ( "%*s %s" , last);
    //最佳答案,因为你不需要额外的变量或是调用两次`scanf()`

    顺便提一句,你应该非常小心的使用scanf 因为它可能会是你的输入缓冲溢出!通常你应该使用fgets 和sscanf 而不是仅仅使用scanf,使用fgets 来读取一行,然后用sscanf 来解析这一行,就像上面演示的一样。

    Afif Ahmed

    ~-n 等于n-1
    -~n 等于n+1

    原因:
    当我们写-n时,实际上是以补码形式储存,所以-n 可以写成~n + 1,吧整个式子放在上面表达式的前面你就能明白原因了。

    几周前,我的一位同事带着一个编程问题来到我桌前。最近我们一直在互相考问C语言的知识,所以我微笑着鼓起勇气面对无疑即将到来的地狱。

    他在白板上写了几行代码,并问这个程序会输出什么?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <stdio.h>
     
    int main(){
         int i = 0;
         int a[] = {10,20,30};
     
         int r = 1 * a[i++] + 2 * a[i++] + 3 * a[i++];
         printf ( "%d\n" , r);
         return 0;
    }

    看上去相当简单明了。我解释了操作符的优先顺序——后缀操作比乘法先计算、乘法比加法先计算,并且乘法和加法的结合性都是从左到右,于是我抓出运算符号并开始写出算式。

    1
    2
    3
    4
    int r = 1 * a[i++] + 2 * a[i++] + 3 * a[i++];
    //    =    a[0]    + 2 * a[1]  + 3 * a[2];
    //    =     10     +     40    +    90;
    //    = 140

    我自鸣得意地写下答案后,我的同事回应了一个简单的“不”。我想了几分钟后,还是被难住了。我不太记得后缀操作符的结合顺序了。此外,我知道那个顺序甚至不会改变这里的值计算的顺序,因为结合规则只会应用于同级的操作符之间。但我想到了应该根据后缀操作符都从右到左求值的规则,尝试算一遍这条算式。看上去相当简单明了。

    1
    2
    3
    4
    int r = 1 * a[i++] + 2 * a[i++] + 3 * a[i++];
    //    =    a[2]    + 2 * a[1]  + 3 * a[0];
    //    =     30     +     40    +    30;
    //    = 100

    我的同事再一次回答说,答案仍是错的。这时候我只好认输了,问他答案是什么。这段短小的样例代码原来是从他写过的更大的代码段里删减出来的。为了验证他的问题,他编译并且运行了那个更大的代码样例,但是惊奇地发现那段代码没有按照他预想的运行。他删减了不需要的步骤后得到了上面的样例代码,用gcc 4.7.3编译了这段样例代码,结果输出了令人吃惊的结果:“60”。

    这时我被迷住了。我记得,C语言里,函数参数的计算求值顺序是未定义的,所以我们以为后缀操作符只是遵照某个随机的、而非从左至右的顺序,计算的。我们仍然确信后缀比加法和乘法拥有更高的操作优先级,所以很快证明我们自己,不存在我们可以计算i++的顺序,使得这三个数组元素一起加起来、乘起来得到60。

    现在我已对此入迷了。我的第一个想法是,查看这段代码的反汇编代码,然后尝试查出它实际上发生了什么。我用调试符号(debugging symbols)编译了这段样例代码,用了objdump后很快得到了带注释的x86_64反汇编代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    Disassembly of section .text:
     
    0000000000000000 <main>:
    #include <stdio.h>
     
    int main(){
        0:   55                      push   %rbp
        1:   48 89 e5                mov    %rsp,%rbp
        4:   48 83 ec 20             sub    $0x20,%rsp
         int i = 0;
        8:   c7 45 e8 00 00 00 00    movl   $0x0,-0x18(%rbp)
         int a[] = {10,20,30};
        f:   c7 45 f0 0a 00 00 00    movl   $0xa,-0x10(%rbp)
       16:   c7 45 f4 14 00 00 00    movl   $0x14,-0xc(%rbp)
       1d:   c7 45 f8 1e 00 00 00    movl   $0x1e,-0x8(%rbp)
         int r = 1 * a[i++] + 2 * a[i++] + 3 * a[i++];
       24:   8b 45 e8                mov    -0x18(%rbp),%eax
       27:   48 98                   cltq 
       29:   8b 54 85 f0             mov    -0x10(%rbp,%rax,4),%edx
       2d:   8b 45 e8                mov    -0x18(%rbp),%eax
       30:   48 98                   cltq 
       32:   8b 44 85 f0             mov    -0x10(%rbp,%rax,4),%eax
       36:   01 c0                   add    %eax,%eax
       38:   8d 0c 02                lea    (%rdx,%rax,1),%ecx
       3b:   8b 45 e8                mov    -0x18(%rbp),%eax
       3e:   48 98                   cltq 
       40:   8b 54 85 f0             mov    -0x10(%rbp,%rax,4),%edx
       44:   89 d0                   mov    %edx,%eax
       46:   01 c0                   add    %eax,%eax
       48:   01 d0                   add    %edx,%eax
       4a:   01 c8                   add    %ecx,%eax
       4c:   89 45 ec                mov    %eax,-0x14(%rbp)
       4f:   83 45 e8 01             addl   $0x1,-0x18(%rbp)
       53:   83 45 e8 01             addl   $0x1,-0x18(%rbp)
       57:   83 45 e8 01             addl   $0x1,-0x18(%rbp)
         printf ( "%d\n" , r);
       5b:   8b 45 ec                mov    -0x14(%rbp),%eax
       5e:   89 c6                   mov    %eax,%esi
       60:   bf 00 00 00 00          mov    $0x0,%edi
       65:   b8 00 00 00 00          mov    $0x0,%eax
       6a:   e8 00 00 00 00          callq  6f <main+0x6f>
         return 0;
       6f:   b8 00 00 00 00          mov    $0x0,%eax
    }
       74:   c9                      leaveq
       75:   c3                      retq

    最先和最后的几个指令只建立了堆栈结构,初始化变量的值,调用printf函数,还从main函数返回。所以我们实际上只需要关心从0×24到0×57之间的指令。那是令人关注的行为发生的地方。让我们每次查看几个指令。

    1
    2
    3
    24:   8b 45 e8                mov    -0x18(%rbp),%eax
    27:   48 98                   cltq 
    29:   8b 54 85 f0             mov    -0x10(%rbp,%rax,4),%edx

    最先的三个指令与我们预期的一致。首先,它把i(0)的值加载到eax寄存器,带符号扩展到64位,然后加载a[0]到edx寄存器。这里的乘以1的运算(1*)显然被编译器优化后去除了,但是一切看起来都正常。接下来的几个指令开始时也大致相同。

    1
    2
    3
    4
    5
    2d:   8b 45 e8                mov    -0x18(%rbp),%eax
    30:   48 98                   cltq 
    32:   8b 44 85 f0             mov    -0x10(%rbp,%rax,4),%eax
    36:   01 c0                   add    %eax,%eax
    38:   8d 0c 02                lea    (%rdx,%rax,1),%ecx

    第一个mov指令把i的值(仍然是0)加载进eax寄存器,带符号扩展到64位,然后加载a[0]进eax寄存器。有意思的事情发生了——我们再次期待i++在这三条指令之前已经运行过了,但也许最后两条指令会用某种汇编的魔法来得到预期的结果(2*a[1])。这两条指令把eax寄存器的值自加了一次,实际上执行了2*a[0]的操作,然后把结果加到前面的计算结果上,并存进ecx寄存器。此时指令已经求得了a[0] + 2 * a[0]的值。事情开始看起来有一些奇怪了,然而再一次,也许某个编译器魔法在发生。

    1
    2
    3
    4
    3b:   8b 45 e8                mov    -0x18(%rbp),%eax
    3e:   48 98                   cltq 
    40:   8b 54 85 f0             mov    -0x10(%rbp,%rax,4),%edx
    44:   89 d0                   mov    %edx,%eax

    接下来这些指令开始看上去相当熟悉。他们加载i的值(仍然是0),带符号扩展至64位,加载a[0]到edx寄存器,然后拷贝edx里的值到eax。嗯,好吧,让我们在多看一些:

    1
    2
    3
    4
    46:   01 c0                   add    %eax,%eax
    48:   01 d0                   add    %edx,%eax
    4a:   01 c8                   add    %ecx,%eax
    4c:   89 45 ec                mov    %eax,-0x14(%rbp)

    在这里把a[0]自加了3次,再加上之前的计算结果,然后存入到变量“r”。现在不可思议的事情——我们的变量r现在包含了a[0] + 2 * a[0] + 3 * a[0]。足够肯定的是,那就是程序的输出:“60”。但是那些后缀操作符上发生了什么?他们都在最后:

    1
    2
    3
    4f:   83 45 e8 01             addl   $0x1,-0x18(%rbp)
    53:   83 45 e8 01             addl   $0x1,-0x18(%rbp)
    57:   83 45 e8 01             addl   $0x1,-0x18(%rbp)

    看上去我们编译版本的代码完全错了!为什么后缀操作符被扔到最底下、所有任务已经完成之后?随着我对现实的信仰减少,我决定直接去找本源。不,不是编译器的源代码——那只是实现——我抓起了C11语言规范。

    这个问题处在后缀操作符的细节。在我们的案例中,我们在单个表达式里对数组下标执行了三次后缀自增。当计算后缀操作符时,它返回变量的初始值。把新的值再分配回变量是一个副作用。结果是,那个副作用只被定义为只被付诸于各顺序点之间。参照标准的5.1.2.3章节,那里定义了顺序点的细节。但在我们的例子中,我们的表达式展示了未定义行为。它完全取决于编译器对于 什么时候 给变量分配新值的副作用会执行 相对于表达式的其他部分。

    最终,我俩都学到了一点新的C语言知识。众所周知,最好的应用是避免构造复杂的前缀后缀表达式,这就是一个关于为什么要这样的极好例子。



    展开全文
  • C语言中的未定义行为

    千次阅读 2015-08-10 22:41:59
    C语言中的未定义行为(Undefined Behavior)是指C语言标准未做规定的行为。同时,标准也从没要求编译器判断未定义行为,所以这些行为有编译器自行处理,在不同的编译器可能会产生不同的结果,又或者如果程序调用...

     C语言中的未定义行为(Undefined Behavior)是指C语言标准未做规定的行为。同时,标准也从没要求编译器判断未定义行为,所以这些行为有编译器自行处理,在不同的编译器可能会产生不同的结果,又或者如果程序调用未定义的行为,可能会成功编译,甚至一开始运行时没有错误,只会在另一个系统上,甚至是在另一个日期运行失败。当一个未定义行为的实例发生时,正如语言标准所说,“什么事情都可能发生”,也许什么都没有发生。

      所以,避免未定义行为,是个明智的决定。本文将介绍几种未定义行为,同时欢迎读者纠错和补充。

    1.同一运算符中多个操作数的计算顺序(&&、||、?和,运算符除外)

      例如:x = f()+g(); //错误

      f()和g()谁先计算由编译器决定,如果函数f或g改变了另一个函数所使用变量的值,那么x的结果可能依赖于这两个函数的计算顺序。

    参考: 《C程序设计语言(第2版)》 P43

    2.函数各参数的求值顺序

      例如: printf("%d,%d\n",++n,power(2,n)); //错误

      在不同的编译器可能产生不同的结果,这取决于n的自增运算和power调用谁在前谁在后。

    需要注意的是,不要和逗号表达式弄混,都好表达式可以参考这篇文章:c语言中逗号运算符和逗号表达式

    参考: 《C程序设计语言(第2版)》 P43

    3.通过指针直接修改 const 常量的值

      直接通过赋值修改const变量的值,编译器会报错,但通过指针修改则不会,例如:

    a输出值也由编译器决定。

    展开全文
  • 原文链接: Christopher Cole 翻译: 伯乐在线 - cjpan ...最近我们一直在互相考问C语言的知识,所以我微笑着鼓起勇气面对无疑即将到来的地狱。 他在白板上写了几行代码,并问这个程序会输出什么? 1 2
  • C语言未定义行为(undefined behaviour)

    万次阅读 多人点赞 2016-05-15 11:21:03
    什么是未定义行为简单地说,未定义行为是指C语言标准未做规定的行为。编译器可能不会报错,但是这些行为编译器会自行处理,所以不同的编译器会出现不同的结果,什么都有可能发生,这是一个极大的隐患,所以我们应该...
  • 英文出处:Christopher Cole: a glimpse of undefined behavior in c...最近我们一直在互相考问C语言的知识,所以我微笑着鼓起勇气面对无疑即将到来的地狱。他在白板上写了几行代码,并问这个程序会输出什么?#include
  • C语言中的未定义行为(Undefined Behavior) C语言中的未定义行为(Undefined Behavior)是指C语言标准未做规定的行为。同时,标准也从没要求编译器判断未定义行为,所以这些行为有编译器自行处理,在不同的编译器...
  • c语言中的未定义行为

    千次阅读 2016-04-14 08:56:28
    下列 C 代码中,不属于未定义行为的有_A.Int i=0;i=(i++); B.char *p=”hello”;p[1]=’E’; C.char *p=”hello”;char ch=*p++; D.int i=0;printf(“%d%d\n”,i++,i–); E.都是未定义行为 F.都不是未定义行为...
  • C语言未定义行为

    2017-07-07 12:33:59
    下列 C 代码中,不属于未定义行为的有___ 正确答案: C int i=0;i=(i++); char *p=”hello”;p[1]=’E’; char *p=”hello”;char ch=*p++; int i=0;printf(“%d%d\n”,i++,i--); 都...
  • 试谈C语言未定义行为.pdf
  • C/C++未定义行为

    2017-03-29 09:50:15
    下列 C 代码中,不属于未定义行为的有:______。 int i=0;i=(i++); char *p=”hello”;p[1]=’E’ char *p=”hello”;char ch=*p++ int i=0;printf(“%d%d\n”,i++ i--) 都是未定义行为 都不是...
  • C++中引发异常和处理异常是松耦合的,这使得在 C++中避免错误是十分容易的,但却使得保证程序永远不会出现未定义行为变得基本不可能。在C语言中,引发错误和处理错误的部分是紧耦合的,它们在源代码中处于同一个位置...
  • 摘 要:自增自减运算符是C语言的一个特色,本文通过表达式中对一个变量进行多次自增或自减运算时产生的未定义行为进行了详细介绍,望帮助大家正确使用自增自减运算符。关键词:C;自增自减运算符;未定义行为中图...
  • 简单地说,未定义行为是指C语言标准未做规定的行为。编译器可能不会报错,但是这些行为编译器会自行处理,所以不同的编译器会出现不同的结果,什么都有可能发生,这是一个极大的隐患,所以我们应该尽量避免这种情况...
  • 对于未定义行为,C++标准没有明确规定编译器们应该怎么做,那么执行的结果就是不可预料的。下面我们来详细探讨下
  • 什么叫做未定义行为

    2019-08-24 09:51:37
    C语言中的未定义行为(Undefined Behavior)是指C语言标准未做规定的行为。 同时,标准也从没要求编译器判断未定义行为,所以这些行为有编译器自行处理,在不同的编译器可能会产生不同的结果,又或者如果程序调用...
  • C语言中自增自减表达式的未定义行为.pdf
  • 这些未定义行为往往会导致一些程序的错误问题的出现,作为一名合格的程序员,应该极力避免这些未定义行为的出现,让程序的运行结果不会说出现不确定的情况。 总结 在C++中的常见的未定义行为主要有以下几种: 数组...
  • 我在以下几行代码中重现了我在一个更大的项目中所经历的行为.我遗漏了#ifndef守卫和#include指令,试图提高可读性.调用make时会产生链接器错误. makefile包含在问题的末尾.C类继承自B的继承自A的O.O是完全不同的类....
  • 3.11 人们总是说i=i++的行为未定义的。可我刚刚在一个ANSI编译器上尝试过,其结果正如我所期望的。 3.12 我不想学习那些复杂的规则,怎样才能避免这些未定义的求值顺序问题呢? 其他的表达式问题 *3.13 ++i和i+...
  • 嵌入式C语言面试题(附答案)1 读程序段,回答问题int main(int argc,char *argv[]){int c=9,d=0;c=c++%5;d=c;printf("d=%d\n",d);return 0;}a) 写出程序输出b) 在一个可移植的系统中这种表达式是否存在风险?why?#...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 10,410
精华内容 4,164
关键字:

c语言未定义行为

c语言 订阅