C陷阱与缺陷(更新中,总结了全书中核心内容,读者可以根据核心决定是否需要学习该书)

本文总结了《C陷阱与缺陷》中的核心内容,包括初值与初始化、运算符和赋值的区别、函数声明、指针与数组、内存管理等方面的问题。强调了初始化变量、理解函数声明和指针操作的重要性,同时提醒开发者注意如运算符优先级、缓冲输出、errno错误检测等方面的潜在陷阱。
摘要由CSDN通过智能技术生成

1、初值与初始化

该部分解释到,我们在定义变量时最好作初始化操作,比如int a[N];定义数组时,N也被定义了,但是没有初始化,编译器不会报错,后续代码使用N时会陷入死循环。如果经常写代码,从经验上也不会这样定义,我一般会直接定义一个确定的值比如a[100]。

2、== 与= 、&& 与&  、-- - 与- --、+=与=+区别

这类问题不需要看C陷阱与缺陷也会知道吧,代码写多了,自然就知道了,平时的积累,而且很多奇怪而容易出错的用法,一般我们避免使用。

3、函数声明是个大问题

这部分很经典,我们试着理解下这行代码( *( void(*) () ) 0 ) ();如果你感到害怕,那你很有必要看看这章节,并去学会理解,这块融入了指针的精髓,后续你可以再去理解containerof宏与offset宏,说明你对指针有了深刻的理解。

接下来解释这段话,( void(*) () )这个可以理解为(int)强制类型转换,即将常量0转换成( void(*) () )类型的函数指针,比如fp是一个指向返回值为void类型的函数指针,声明为void (*fp)(),然后我们可以使用fp指向其他函数,而其他函数也必须是一个void(*)()类型,使用(void(*)())0就是将0地址处按照(void(*)()类型格式化,这个函数体实体占用多大内存,从0地址开始往上也就是多大,调用0地址处的代码时,也会按照(void(*)()类型格式执行完函数体;定义一个该类型的函数指针时可以用fp =(void(*)()0 ,将fp指向0地址,或者直接用( *( void(*) () ) 0 ) ()定义。

4、运算符的优先级问题

这部分常用的我们都知道,不常用的一般也不会用,真用到了在看书。谭浩强版本也讲过。

5、语句结束标志分号

(1)if(),多写一个分号就等效于if(){}

if(x > big);
    big = x;

(2)return漏掉;编译器会默认为if() return a = 1;将a的值返回。如果该函数返回值与a一致或者为空,编译器则不会报错,这是很难发现的bug,如果函数定义的返回值与a不一致报错后容易发现并解决。

if(n<3)
    return
a = 1;
b = 2;

(3)结构体遗漏;这样会导致main的返回值是结构体a类型,这样main函数在实际返回时函数体内可能为int 或空等。

struct a
{
    int date;
}
main()
{
    ...
}

6、switch、else引发问题

这部分其实对于老手也不是问题,只有代码规范,主要if else的层次就ok。switch注意break;

7、指针与数组

这一块其实在很多blog与谭浩强以及一些视频中都讲过,包括数组指针,指针数组,*a 与a[ ]的表达,而导致的一些问题,这部分陷阱就是你很难去理解,对于熟练的程序员不算陷阱

8、非数组的指针(重点)这部分可以说是我写博客的真正原因

(1)陷阱1:我们这里只是定义了一个指针r,不确定指向何处,从函数strcpy中我们可以看到,指针r应该是一个输出型参数,不会加上const(题外话),这里传入指针前,该指针需要指向一个实体,函数内部并没有使用statci分配内存,所以出了函数体后,栈上的内存会被释放。

char *r;
strcpy(r,s);//将s内容复制到r
strcat(r,t);//连接两个字符串

修改上面代码 如下:似乎可以了,只要s和t指向的字符串小于r就ok,但是如果大于呢?而我们有必须给定一个确定大小的数组r,这是C语言的规定。

char r[100];
strcpy(r,s);//将s内容复制到r
strcat(r,t);//连接两个字符串

修改上面代码如下:所以你觉得ok了吗,其一:r申请的堆内存使用完没有释放,这个很容易忘记,而导致内存泄漏,其二,strlen函数计算字符串大小没有加上一个字符串的最后一个‘\0’字符,而实际这个字符是占用内存的。

char *r,*malloc();
r = malloc(strlen(s) + strlen(t));
strcpy(r,s);//将s内容复制到r
strcat(r,t);//连接两个字符串

修改上面代码如下:加1并释放r

char *r,*malloc();
r = malloc(strlen(s) + strlen(t) + 1);
strcpy(r,s);//将s内容复制到r
strcat(r,t);//连接两个字符串
free(r);

9、指针类型0与数字0

第一行是合法的,如果p之前指向了0地址指针,而0地址存放的是它指向变量的地址值,所以p的值实际为变量的地址。第二行是错误的,因为0被转换为指针使用时,这个指针绝不能被解引用,也就是当我们将0赋值给一个指针变量时,绝不能企图使用或操作该指针所指向的内存中存储的内容。而strcmp操作了0地址存储的内容,将其进行对比等操作。

if(p == (char *) 0)
if(strcmp(p, (char *)0) == 0)

10、边界计算

这部分讲的挺多的,我只说我比较关注的问题,如下代码:数字下标不可能为N,所以注意。

if(bufptr == &buffer[N])

if(bufptr > &buffer[N - 1])

11、缓冲输出与内存分配

C库函数的缓冲机制实现原理。当一个程序生成输出时,是否有必要将输出立即显示

程序输出有两种方式:一种是即时处理方式,另一种是先暂存起来,然后再大块写入的方式,前者往往造成较高的系统负担。因此,c语言实现通常都允许程序员进行实际的写操作之前控制产生的输出数据量。

这种控制能力一般是通过库函数setbuf实现的。如果buf是一个大小适当的字符数组,那么:setbuf(stdout,buf);

语句将通知输入/输出库,所有写入到stdout的输出都应该使用buf作为输出缓冲区,直到buf缓冲区被填满或者程序员直接调用fflush(译注:对于由写操作打开的文件,调用fflush将导致输出缓冲区的内容被实际地写入该文件),buf缓冲区中的内容才实际写入到stdout中。这段话很关键,如下代码:puts放入的字符超过了10,被填满时,系统将这15个字节直接写入stdout,这才打印出来(但是也要等主函数结束后才会打印,不知道为什么,按理说超过了应该直接与fflush效果一样才对),也可以通过fflush函数直接写入stdout(这个不用等主函数结束)。要知道使用setbuf后,执行了puts并不会直接写入stdout,而是放在缓冲区。

#include <stdio.h>
#include <string.h>
#include <unistd.h>

void main()
{
	int c;
	char buf[10];
	setbuf(stdout,buf);
	puts("this is my book");

	sleep(3);	
//	fflush(stdout);
}

所以你以为这样就OK了?加上setbuf机制后,必须等主函数结束后,系统才会去setbuf中读取并自动打印出来,而buf定义在main函数内部,所以main函数返回时,buf会被释放,所以这里要申请静态内存或堆内存,也可以放在主函数外,代码如下:

#include <stdio.h>
#include <string.h>
#include <unistd.h>

char buf[10];
void main()
{
	int c;
	setbuf(stdout,buf);
	puts("this is my book");

	sleep(3);	
//	fflush(stdout);
}

这样,缓冲区数据一直存在,最后主函数结束后也会打印出this is my book,这样一来,你写入的信息,都会被存放在buf,等函数结束后才会打印,而不是写入一个打印一个。

12、errno检测错误

errno 是记录系统的最后一次错误代码。代码是一个int型的值,在errno.h中定义。查看错误代码errno是调试程序的一个重要方法。当linux C api函数发生异常时,一般会将errno变量(需include errno.h)赋一个整数值,不同的值表示不同的含义,可以通过查看该值推测出错的原因。在实际编程中用这一招解决了不少原本看来莫名其妙的问题。

注意:只有当一个库函数失败时,errno才会被设置。当函数成功运行时,errno的值不会被修改。这意味着我们不能通过测试errno的值来判断是否有错误存在。反之,只有当被调用的函数提示有错误发生时检查errno的值才有意义。

如下代码:这样的例子并不能得到somecall这个函数的运行所产生的错误代码,因为很可能是printf这个函数产生的。

if (somecall() == -1) 
{
    printf("somecall() failed\n");
    if (errno == ...) 
    { 
        ...
    }
}

如下改正: 这样才能真正得到运行somecall函数所带来的错误代码

if (somecall() == -1)
{
    int errsv = errno;
    printf("somecall() failed\n");
    if (errsv == ...)
     { 
        ... 
     }
}

13、signal陷阱

这部分内容太多,太经典,在另一篇博客中详细介绍

https://blog.csdn.net/qq_40334837/article/details/96423711

14、内存位置0

C++中定义为0,C中定义为空类型指针,空类型指针,可以赋值给任何类型指针。

#undef NULL
#if defined(__cplusplus)
#define NULL 0
#else
#define NULL ((void *)0)
#endif

NULL指针不指向任何对象,因此,除了用于赋值(p=NULL)或比较运算(p == NULL),出去其他任何目的使用NULL指针都是非法的。某些平台C语言实现实现了对内存0的读或者写。但是这部分代码不具有移植性,对于一些无法对NULL进行其他操作的平台,将会出现致命错误。

要检查是否能对NULL进行操作很容易,定义一个指针p,将NULL赋值给它,然后printf(“%d”,*p),如果出现段错误,说明该系统下不能对NULL进行其他操作,如果读取到值,说明内存地址0可被操作。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值