《C专家编程》读书笔记(1)


 ***C的发展历程***

BCPL -> B -> New B -> 早期的C

注释:
BCPL(Basic Combined Programming Language) 基本组合编程语言
B 在BCPL的基础上进行了简化,但由于硬件系统的内存限制,它只允许放置解释器,而不是编译器。它同时保持了BCPL语言无类型的特点,仅有的操作数就是机器的字。
New B 能同时解决多种数据类型,采用了编译模式而不是解释模式,并引入了类型系统。
早期的C 有以下一些语言特性:(1)数组下标从0而不是1开始;(2)基本数据类型直接与底层硬件相对应(这一点带来了极高的效率和移植性);(3)数组名可看作指针;(4)不允许嵌套函数。

C编译器未实现的功能在运行时进行处理,既可出现在应用程序代码中,也可出现在运行时函数库中。

 

***ANSI C***

ANSI C标准的几个术语:
(1)不可移植的代码:
         (a)由编译器定义的:由编译器设计者决定采取何种行动,可能并不相同,但都是正确的。
                   例如:当整型数向右移位时,要不要扩展符号位
         (b)未确定的:在某些正确情况下的做法,标准并未明确规定应该怎样做。
                   例如:计算参数的顺序
(2)坏代码:
        (a)未定义的:在某些不正确情况下的做法,但标准并未规定应该怎么做,即你可采取任何行动。
                  例如:当一个有符号整数溢出时该采取什么行动
        (b)约束条件:必须遵守的限制或要求,若不遵守,则程序行为将变成未定义的。
                  例如:%操作符的操作数必须属于整型(属于约束条件),若不符合,必引发一条错误信息。
                  声明一个已有的malloc函数(不属于约束条件),则可能出现任何情况。

 

K&R C与ANSI C的一个重大区别:原型
原型的目的是当我们对函数作前向声明时,在形参类型中增加一些信息。这样,编译器就能够在编译时对函数调用中的实参和函数声明中的形参之间进行一致性检查。

 

                             使用原型之前                    使用原型之后
函数声明:         
char *  strcpy();                       char *  strcpy( char *  dst,  char *  src)
函数定义:         
char *  strcpy(dst, src)          char *  strcpy( char *  dst,  char *  src)
                         
char *  dst,  char *  src;              { ... }
                         
{ ... }

 

***指针赋值的规范***

规范:两个操作数都是指向有限定符或无限定符的相容类型的指针,左边指针所指向的类型必须具有右边指针所指向类型的全部限定符。

下面是相关示例:

(1)合法情况:

char *  cp;
const   char *  ccp;
ccp 
=  cp;

左操作数是一个指向有const限定符的char的指针,右操作数是一个指向无限定符的char的指针,char类型和char类型是相容的,左操作数所指向的类型具有右操作数所指向的类型的限定符(无),再加上自身的限定符(const),因而此赋值是合法的。

(2)非法情况:
char **  cp;
const   char **  ccp;
ccp 
=  cp;

左操作数是一个指向无限定符的char类型的指针的指针,右操作数是一个指向有const限定符的char类型的指针的指针,虽然其中cp指向的和ccp指向的都是无限定符的指针,且二者指向的指针是相容的(原因参见情况1),但由于相容性是不能传递的,因而此赋值是非法的。

 

***对无符号类型的建议***

尽量不要在代码中使用无符号类型,只有在使用位段和二进制掩码时,才可以用无符号数,但也要注意在表达式中使操作数均为无符号数或有符号数,这样就不必由编译器来选择结果的类型。

下面是一个错误示例:

#include  " iostream "

using   namespace  std;

double  array[]  =   23,34,12,17,204,99,16 } ;
#define  TOTAL_ELEMENTS (sizeof(array)/sizeof(array[0]))

int  main()
{
    
int d = -1;
    
double x;

    
if(d <= TOTAL_ELEMENTS - 2)
    
{
        x 
= array[d+1];
     }


    
return 0;
}

TOTAL_ELEMENTS的类型是unsigned int(因为sizeof()的返回类型是无符号数),if语句在int和unsigned int之间测试相等性,所以d被升级为无符号数,-1转化为unsigned int的结果是一个非常巨大的正整数,致使表达式的结果为假(与预期不符)。要修正这个问题,只需对TOTAL_ELEMENTS进行强制类型转换既可:

if (d  <=  ( int )TOTAL_ELEMENTS  -   2 )

 

***C语言的缺陷***

C语言的缺陷:

(1)多做之过:
         (a)由于存在fall through(默认在没有break时会顺序执行case),switch语句会带来麻烦;
         (b)将相邻的字符串常量自动合并成一个字符串;
         (c)太多的缺省可见性:在缺省情况下,函数的名字是全局可见的,如果要限制对这个函数的访问,就必须加个static关键字(在这个文件之外不可见)。范围过宽的问题常见于库中,一个库要让一个对象在另一个库中可见,唯一的办法是让它变得全局可见。但这样一来,它对于链接到该库的所有对象都是可见的了。

(2)误做之过:
         (a)符号重载带来的歧义;
         (b)有些运算符的优先级不符合常规理解;

优先级问题             表达式           常规理解                           实际结果
.的优先级高于
*     * p.f                 p所指对象的字段             对p取f偏移,作为指针再解引用
                                                     (
* p).f                                   * (p.f)
[]高于
*                   int *  ap[]         ap是一个指向int数组      ap是一个元素为int指针的数组
                                                     的指针                               
int *  (ap[])
                                                     
int  ( * ap)[]
函数()高于
*                int *  fp()         p是一个函数指针,      fp是个函数,返回int *
                                                       所指函数返回int               
int *  (fp())
                                                       
int  ( * fp)()
== != 高于赋值符     c = getchar()    (c = getchar()) != EOF       c = (getchar() != EOF)
                                 
!= EOF
逗号在所有运算符 i 
=   1 , 2             i  =  ( 1 , 2 )                           (i  =   1 ), 2
中优先级最低

          (c)例如gets()等文件I/O函数的缓冲区溢出问题;

(3)少做之过:
         (a)标准参数处理未区分选项和其它参数;
         (b)注释歧义;
         (c)自动变量引发的问题:在C语言中,自动变量在堆栈中分配内存,当包含自动变量的代码块或函数返回时,它们所占用的内存即被收回。
          最好的解决方案就是要求调用者分配内存来保护函数的返回值。为了提高安全性,调用者应该同时指定缓冲区的大小:

void  func( char *  result,  int  size)
{
     ...
    strncpy(result,
"That's right",size);
}

          
// 调用过程
buffer  =  malloc(size);
func(buffer,size);
free(buffer);

 

***C语言中的声明***

(1)如果想要把什么东西的类型强制转换为指向数组的指针,就不得不使用下面的语句来表示这个强制类型转 换:

char  ( * j)[ 20 ];     // j是一个指向数组的指针,数组内有20个char元素
=  ( char  ( * )[ 20 ])malloc( 20 );

(2)涉及指针和const的声明可能会出现几种不同的顺序:

const   int *  grape;         // 指针所指向的对象是只读的
int   const *  grape;         // 同上
int *   const  grape;         // 指针是只读的
const   int *   const  grape;   // 指针和指针所指向的对象都是只读的

(3)几种合法的声明形式:

          函数的返回值允许是一个函数指针,如:int (*fun())();
          函数的返回值允许是一个指向数组的指针,如:int (*fun())[];
          数组里面允许有函数指针,如int (*foo[])();

(4)最好不要将struct的声明和定义混合在一起:
// 不好的做法
struct  veg  int weight,price; }  onion, radish, turnip;
// 建议的做法
struct  veg  int weight,price; } ;     // 声明
struct  veg onion, radish, turnip;     // 定义

(5)联合又被称作变体记录,它的外表与结构相似,但在内存布局上存在关键性的区别。在结构中,每个成员依次存储,而在联合中,所有的成员都从偏移地址零开始存储,在某一时刻,只有一个成员真正存储于该地址;

联合通常用来节省空间,此外联合也可以把同一个数据解释成两种不同的东西,例如:

union bits32_tag
{
    
int whole;                                           //1个32位的值
    struct char c0, c1, c2, c3; } byte;    //4个8位的字节
}

这个联合允许程序员提取整个32位值(value.whole),也可以提取单独的字节字段如value.byte.c0等。

(6)枚举与#define作用相似,但具有一个优点:#define定义的名字一般在编译时被丢弃,而枚举名字一直可见;

enum  size  { small = 7, medium, large = 10 } ;

(7)C语言声明的优先级规则:
     (a)声明从它的名字开始读取,然后按照优先级顺序依次读取;
     (b)优先级从高到低依次是:
              (1)声明中被括号括起来的那部分;
              (2)后缀操作符(()表示这是一个函数,[]表示这是一个数组);
              (3)前缀操作符(*表示指向...的指针);
     (c)如果const或volatile关键字后面紧跟类型说明符,则它作用于该类型说明符;否则它作用于其左边紧邻的指针星号。

根据上述规则,可分析以下声明:
char *   const   * ( * next)();

解释:next是一个指针,它指向一个函数,该函数返回另一个指针,该指针指向一个类型为char的常量指针。

(8)typedef和宏文本替换之间的存在一个关键性的区别,即typedef是一种彻底的“封装”类型:

         (a)可以用其它类型说明符对宏类型名进行扩展,而typedef禁止这样做;

#define  peach int
unsigned peach i;    
// 没问题
typedef  int  banana;
unsigned banana i;   
// 非法

         (b)在连续几个变量的声明中,typedef能保证所有变量均为同一种类型;

#define  int_ptr int*;
int_ptr chalk, cheese;    
// 经扩展后,变为int* chalk, cheese;
typedef  int *  int_ptr;
int_ptr chalk, cheese;    
// chalk、cheese均是int*类型
(9)不要为了方便起见对结构使用typedef,这样做好处很小,且容易混淆;
 

你可能感兴趣的:(编程,c,struct,读书,语言,编译器)