C语言学习笔记:杂记

xeonds

2021.09.29 00:25:19

奇怪的赋值

先上两个赋值语句。

int a = 5;
//版本1
float c_1 = (a-9)*5/9;

//版本2
float c_2 = (a-9)*5/9.0f;

那么请问,c_1是否和c_2的值相等呢?

不相等。

因为这里发生了隐式类型转换。对于第一个语句,5/9的值实际上是整数,这是因为9和5都是整数。第二个语句则会得到符合我们直觉的结果。

虽然现如今很多编译器已经支持这种自动转换,但是为了确保兼容性,我们还是严格按照数据类型编写赋值语句吧。

这里再说一句,编译器遇到这样的表达式时,会进行自动类型转换,将所有操作数的类型转换为其中容纳范围最大的数据类型。顺序是short->int->unsigned int->long->unsigned long->float->double->long double

无限读取

先来看看这个语句。

int ch;

while((ch = getchar()) != '\n')
{
    //statements
}

在一些情况下,它的确可以正常运行。但是,在一些特殊情况下,它会无限输出。因为它的结束条件是读入的字符不是换行符。换句话来说,就是此处while的边界条件太少了,以至于循环很可能无限执行下去,导致输出超限。

之前我也不是很清楚,所以说得很抽象。实际上,这里需要使用while((ch=getchar())!=EOF),这才是正确的终止条件。

除此之外,ch务必声明为int类型。因为char类型实际上就是短整型,所以读取的字符如果ASCII码过大(超过256)就会发生溢出,从而有可能和EOF的值相等,从而异常退出。

指针?

请看下面的赋值语句,想想a,b的数据类型。

int* a,b;

答案是:a是指向int的指针,而b是int类型的变量。原因是什么?因为*是和a在一起的。所以通常我们不会这么写,我们一般会把*放在靠近变量的一侧来避免混淆。

再来看看这个。

#define INTPTR int*

......
INTPTR a,b,c;

其中,只有a是指向int的指针类型。b和c都是整数类型。所以,宏指令并不能很好地处理指针类型。因此我们通常会用typedef取而代之。

转义符

这是转义字符\。注意,对于printf()而言,未定义的转义字符会直接输出反斜杠后的字符。

另外,有一种三符号系统,用\??*来表示其他符号。所以连续使用问号时请务必注意。

单引号,双引号

在很多语言中,单引号和双引号是等效的(比如Python)。但是对于C语言而言,单引号内只能表示单个字符,而双引号只能表示字符串

奇怪的赋值2

来看看这个。

r=s+(t=u-v)/3;

这个表达式合法吗?合法。因为C语言中,赋值并不是语句,而是表达式。所以它可以出现在任何允许出现的地方。

既然是表达式,那么它就有返回值。 赋值表达式的值就是左操作数的新值。

再来看看这个语句。

int a;
(a=4)=3*4;

合法吗?不合法。括号项是表达式,它作为另一个赋值表达式的左值参与赋值运算。但是左值不能是常量,而(a=4)的值是4,显然不能被赋值。

逗号运算符

逗号运算符将几个表达式相连接,构成一个表达式。这个表达式的值就是最右边的子表达式的值

在这里有一个小技巧:

while(expression1)
    statement1,
statement2;

事实上这两条语句都会循环运行。此处的逗号运算符将两条语句合并成一条语句。

除了这里可以这么用,可以在循环条件中这么写:如果这么做能使程序更优秀的话。

for语句

C的forwhile的一种常用语句组合形式的简写法。语法如下所示:

for(expression1; expression2; expression3)
    statement;

其中statement称为循环体。expression1初始化部分,只在循环开始时执行一次。expression2称为条件部分,它在循环体每次执行前都要执行一次,和while语句中的表达式一样。expression3称为调整部分,它在循环体每次执行完毕,在条件部分即将执行前执行。

这三个表达式都是可省略的。若省略条件部分,表示测试的值始终为真。

表达式和语句

表达式可以出现在任何地方,而语句只能出现在单独的一行。C语言没有赋值语句,它只有赋值表达式。

所以嘛,表达式能出现的地方,都可以赋值,这就有了上面那个奇怪的赋值2。

代码块与声明

我们知道,变量是有作用域的。也就是说,它可以声明在最外层,或者是代码块开头。其实,函数的声明也一样。看看这个:

#include <stdio.h>

int main(void)
{
    int square(int a)
    {
        return a*a;
    }
    int num;

    scanf("%d",&num);
    printf("%d",square(num));

    return 0;
}

一样,它也可以使用函数原型。只需要在前面声明函数原型,在之后写上函数实现就行。不过,以这种形式只能在这个语句块中使用。所以,我们可以随便套娃我们可以声明任意多重的函数。不过注意作用域问题:内层声明会在当前语句块内覆盖重名的外层声明。

另外还有,关于代码块,它并非必须和for等一起出现。它也可以单独出现。和一起出现时一样,它形成了一个块作用域,可以划定更精细的作用域和生命周期。

关于main()函数

其实它真的和其他函数一样是平等的。编译器编译时并没有区别对待它,但是连接器在链接过程,会将一个中间文件链接过来,那个文件指明了程序的入口点:main()。 程序只是从main()开始执行,仅此而已。 既然如此,那么其他函数的操作,在main()函数,也可以使用了。比如递归(虽然这种用法极度罕见),被别的函数调用等各种操作。

可变参数列表的函数

需要用宏来实现。这些宏位于stdargs.h头文件,是C标准库的一部分。

#include <stdarg.h>

float avarage(int n_values, ...)
{
    va_list  var_arg;
    int count;
    float sum=0;

    va_start(var_arg, n_values);
    for(count=0;count<n_values;count++)
        sum+=va_arg(var_arg, int);
    va_end(var_arg);

    return sum/n_values;
}

灵活性

switch(cond) {
    do{
        case 1: i++; 
        case 2: i++; 
        case 3: i++; 
        case 4: i++; 
        case 5: i++; 
        case 6: i++; 
        case 7: i++; 
    }
    while(cond2);
}

没啥技巧,就是反映了C实现的灵活性。原作者为了减少转移次数来优化性能,就整了这么个写法来增加一次跳转后执行的指令数,同时借助switch控制非整数量来对齐结果。最重要的是,这用法反应了编译器/C标准对switch这个语法结构的描述,以及对于程序执行流控制的方式。