C陷阱与缺陷(C Traps and Pitfalls)学习笔记

scanf

scanf("%d,%d,&a,&b") /输入格式必须和sacnf保持一致
//输入1,2
3.3 作为参数的数组声明

数组名是首元素地址

在C语言中,我们没有办法可以将一个数组作为函数参数直接传递。如果我们使用数组名作为参数,那么数组名会立刻被转换为指向该数组第1个元素的指针。

char hello[] = "hello";
//声明了hello是一个字符数组。如果将该数组作为参数传递给一个函数:
printf("%s\n", hello);
//实际上与将该数组第1个元素的地址作为参数传递给函数的作用完全等效,即:
printf("%s\n", &hello[1]);

因此,将数组作为函数参数毫无意义。所以,C语言中会自动地将作为参数的数组声明转换为相应的指针声明。

int strlen(char s[])
{
    /* 具体内容 */
}
//两者写法等效
int strlen(char *s)
{
    /* 具体内容 */
}

C 程序员经常错误地假设,在其他情形下也会有这种自动地转换。本书 4.5节详细地讨论了一个具体的例子

extern char *hello;
//这个语句与下面的语句有着天渊之别:
extern char hello[];

如果一个指针参数并不实际代表一个数组,即使从技术上而言是正确的,采用数组形式的记法经常会起到误导作用。如果一个指针参数代表一个数组,情况又是如何呢?一个常见的例子就是函数main的第二个参数:

main(int argc,char* argv[])  //数组里存放的是一个个指针地址
{
    /* 具体内容 */
}
//两者写法等价
main(int argc,char** argv)
{
    /* 具体内容 */
}

前一种写法在于argv是一个指向某数组起始元素的指针,该数组里的元素为字符指针类型

3.4 避免举隅法

举隅法:以偏概全

指针就是地址

char *p,*q;  //char 开辟4个字节空间
p="xyz"
    
//赋值
char *q=p;   // or q=p;

q[1]='Y'  //禁止修改内容

在这里插入图片描述
实际上,p的值是一个指向由’x’、‘y’、‘z"和’\0’这4 个字符组成的数组的起始元素的指针

复制指针是复制地址而不是数据
在这里插入图片描述

//以数组的形式存放
char p[]="xyz"

这种数组形式是真实开辟空间而非放在常量池里

注意:指针定义与数组定义的区别

3.5 空指针并非空字符串

除了一个重要的例外情况,在C语言中将一个整数转换为一个指针,最后得到的结果都取决于具体的C编译器实现。这个特殊情况就是常数0,编译器保证由 0 转换而来的指针不等于任何有效的指针。出于代码文档化的考虑,常数0这个值经常用一个符号来代替:

#define NULL 0

当然无论是直接用常数 0,还是用符号 NULL,效果都是相同的。需要记住的重要一点是,当常数 0 被转换为指针使用时,这个指针绝对不能被解除引用(dereference)。换句话说,当我们将 0 赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存中存储的内容。下面的写法是完全合法的:

if( p == (char *) 0)   //合法
    
if( strcmp(p,(char *) 0) == 0)   //非法

就是非法的了,原因在于库函数 strcmp的实现中会包括查看它的指针参数所指向内存中的内容的操作。

void main(){
    int *p=0; //避免被误认为赋值,一般写下面的
    //两者写法等价
    int *p=NULL; //不指向任何地址,未定义,空指针
}

空指针与空字符串

void main(){
    char *p=NULL;  //p
    //两者写法不等价
    char *q=" "; // \0
    printf("%d\n",strlen(q)); //0
}

如果p是一个空指针

printf(p);
printf("%s",p);

行为是未定义的,而且,与此类似的语句在不同的计算机上会有不同的效果

3.6 边界计算与不对称边界

在C语言中,这个数组的下标范围是从0到9。一个拥有10个元素的数组中,存在下标为0的元素,却不存在下标为10的元素。C语言中一个拥有n个元素的数组,却不存在下标为 n的元素,它的元素的下标范围是从0到n-1

例如,让我们仔细地来看看本书导读中的一段代码:

void main(){
    int i,a[10];
    for (i=1;i<=10;i++)
    {
        a[i]=0;
    }       
}

这段代码本意是要设置数组a中所有元素为0,却产生了一个出人意料的”副效果“——无限循环

在这里插入图片描述

3.7 求值顺序
a < b && c < d

C语言的定义中说明a<b应当首先被求值。如果a确实小于b,此时必须进一步对 c<d 求值,以确定整个表达式的值。但是,如果 a 大于或等于 b,则无需对c<d求值,表达式肯定为假

void main(){
    int a = 0;
    int b = 1;
    int c = a && b++  //a=0,表达式为假,b++不会参与执行
    int d = a || b++  //a=0,表达式为真,b++不会参与执行
	printf("b=%d\n",b);
}

另外,要对 a< b 求值,编译器可能先对 a 求值,也可能先对 b 求值,在某些机器上甚至有可能对它们问时并行求值。

C 语言中其他所有运算符对其操作数求值的顺序是未定义的。特别地,赋值运算符并不保证任何求值顺序。

下面这种从数组ar中复制前n个元素到数组br中的做法是不正确的,因为它对求值顺序作了太多的假设:

void main(){
    int ar[10] = {1,2,3,4,5,6,7,8,9,10};
    int br[10];
    int i = 0;
    while(i < 10)
        br[i]=ar[i++]
}

不同编译器执行顺序不同,可能自增之后再赋值,也可能相反

3.8 运算符&&,|| 和 !

按位运算符&,|和 ~ 对操作数的处理方式是将其视作一个二进制的位序列,分别对其每个位进行操作。例如,10&12 的结果是8(二进制表示为1000)

逻辑运算符&&,|| 和 !,结果只有0或者1

void main()
{
    int a=10; //1010
    int b=12; //1100
    int c;
    //逻辑与
    c=a && b; //1 0
    printf("c=%d\n",c) //1
    //二进制与
    c=a & b; //1000
    printf("c=%d\n",c) //8
    
}
3.9 整数溢出

C 语言中存在两类整数算术运算,有符号运算与无符号运算。在无符号算术运算中,没有所谓的“溢出”一说:所有的无符号运算都是以2的n次方为模,这里 n 是结果中的位数。如果算术运算符的一个操作数是有符号整数,另一个是无符号整数,那么有符号整数会被转换为无符号整数,“溢出”也不可能发生。但是,当两个操作数都是有符号整数时,“溢出”就有可能发生,而且“溢出”的结
果是未定义的。当一个运算的结果发生“溢出”时,作出任何假设都是不安全的。

void main(){
    int a = 2147483647;
    int b = 1;
	a += b;
    printf("a = %d\n",a); // 输出 -2147483648
}

例如,假定a和b是两个非负整型变量,我们需要检查a+b是否会“溢出”。一种想当然的方式是这样:

void main(){
    int a = 2147483647;
    int b = 1;
	if (a+b < 0)
    {
         printf("OK!\n"); 
    }
}

这并不能正常运行。当 a+b 确实发生“溢出”时,所有关于结果如何的假设都不再可靠。例如,在某些机器上,加法运算将设置一个内部寄存器为四种状态之一:正、负、零和溢出。在这种机器上,C编译器完全有理由这样来实现上面的例子,即 a与b相加,然后检查该内部寄存器的标志是否为“负”。当加法操作发生“溢出”时,这个内部寄存器的状态是溢出而不是负,那么 if 的语句的检查就会失败。

一种正确的方式是将a和b都强制转换为无符号整数

#define IN_MAX 2147483647
void main(){
    int a = 2147483647;
    int b = 1;
	if ((unsigned int)a + (unsigned int)b > IN_MAX)
    {
         printf("OK!\n"); 
    }
}
//此处的 INT_MAX 是一个已定义常量,代表可能的最大整数值

在这里插入图片描述

声明与定义陷阱
int a;  // 定义 a

extern int a;  // 声明 a  没有定义 引入外部变量会报错

//区别定义与声明在于有没有开辟空间

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值