c专家编程笔记

本文深入探讨C语言的各种特性和难点,包括宏扩展、K&R C与ANSI C的区别、数组与指针的关系、内存管理等问题,并提供了丰富的示例代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第一章   C:穿越时空的迷雾

 

1)      宏扩展中空格对扩展结果的影响:

#define a(y)  a_expanded(y)

a(x)被扩展为: a_expanded(x)

#define a  (y)       a_expanded  (y)  

a(x) 被扩展为: (y)   a_expanded (y) (x)

 

2)      K&R CANSI C区别:

1 .第一类区别是指一些新的、非常不同的、并且很重要的东西。惟一属于这类区别的特性是原型― 把形参的类型作为函数声明的一部分。原型使得编译器很容易根据函数的定义检查函数的用法
2
.第二类区别是一些新的关键字。
ANSI C 正式增加了一些关键字:enum 代表枚举类型(最初出现于pcc 的后期版本), const volatile signed void 也有各自相关的语义,另外原先可能由于疏忽而加入到c 中的关键字entry则弃之不用
3
.第三类区别被称作“安静的改变”― 原先的有些语言特性仍然合法,但它的意思有了一些轻微的改变。这方面的例子很多,但都不是很重要,几乎可以被忽略。在你偶尔漫步于它们之上时,可能由于不注意而被其中一个绊了个趔趄,例如,现在的预处理规则定义得更加严格,有一条新规则就是相邻的字符串字面值会被自动连接在一起
4
.最后一类区别就是除上面3 类之外的所有区别,包括那些在语言的标准化过程中长期争论的东西,这些区别在现实中几乎不可能碰到,如符号粘贴(token-Pasting)和三字母词(trigraph)(三字母词就是用3 个字符表示一个单独的字符,如果该字符不存在于某种计算机的字符集中,就可以用这3 个字符来表示,比如两字母词(digraph) /t 表示“tab " ,而三字母词??<则表示“开放的花括号”)

3)      参数的传递过程类似赋值

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

4)      对无符号类型的建议

尽量不要在你的代码中使用无符号类型,以免增加不必要的复杂性。尤其是,不要仅仅因为无符号数不存在负值(如年龄、国债)而用它来表示数量。
  
尽量使用像int 那样的有符号类型,这样在涉及升级混合类型的复杂细节时,不必担心边界情况(如-1被翻译为非常大的正数)
  
只有在使用位段和二进制掩码时,才可以用无符号数。应该在表达式中使用强制类型转换,使操作数均为有符号数或者无符号数,这样就不必由编译器来选择结果的类型

第二章   这不是bug,而是语言特性

 

1) C语言运算符优先级存在的问题

  .的优先级高于*.-->操作符用于消除这个问题

  *p.f:对pf偏移,作为指针,然后进行解引用操作。*(p.f)

  [ ]高于*

  Int *ap[ ]ap是个元素为int指针的数组。Int *(ap[ ])

  函数()高于*

  Int *fp()fp是个函数,返回int指针。Int *(fp())

  = =!=高于位操作符

  (val & mask != 0) val & (mask != 0)

  = =!=高于赋值符

  C = getchar() != EOF c = (getchar() != EOF)

  逗号运算符在所有运算符中优先级最低

   I = 1,2; (I = 1),2

 

第三章 分析c语言的声明

1Char * const * (*next)();

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

2)枚举的一个优点:

#define定义的名字一般在编辑时被丢弃,而枚举名字则通常一直在编译器可见,可以在调试代码时使用它们

 

第四章 令人震惊的事实:数组和指针并不相同

1定义和声明:

    定义只能出现在一个地方,确定对象的类型并分配内存,用于创建新的对象。

    声明可以多次出现,描述对象的类型,用来指代其他地方定义的对象。

 

第五章 对链接的思考

1函数mktemp(现在已被ANSI C标准库函数tmpnam取代)

  : 建立唯一的文件名
  : char *mktemp(char *template);
程序例:

#include <dir.h>
#include <stdio.h>

int main(void)
{
   /* fname defines the template for the
     temporary file.  */

   char *fname = “TXXXXXX”, *ptr;

   ptr = mktemp(fname);
   printf(“%s/n”,ptr);
   return 0;
}

 

第六章 运行的诗章:运行时数据结构

1Setjmp&longjmp函数

通过操纵过程活动记录实现的。

 setjmp(jmp_buf j)必须首先被调用。它表示使用变量j记录现在的位置。函数返回零。虽然是一个函数,可是却可以返回两个不同的值。当第一次直接调用setjmp时,返回值为0。当从longjmp函数返回时,setjmp函数的返回值为longjmp的第二个参数的值。 
 longjmp(jmp_buf j,int i)
可以接着被调用。它表示回到j所记录的位置,让它看上去像是从原来的setjmp()函数返回一样。但是函数返回i,使代码知道它实际上是通过longjmp()返回的。
那么在什么地方调用setjmp呢?我们希望当从longjmp函数返回时,程序从哪里接着开始运行,我们就在哪里调用setjmp

 

第七章 对内存的思考

 

1Cache存储器

当数据从内存读入时,整”(一般1632个字节)的数据被装入cache,如果程序具有良好的地址引用局部性(如:它顺序浏览一个字符串),那么cpu以后对邻近数据的引用就可以从快速的cache读取,而不用从缓慢的内存读取。

 

2总线错误

有时编译程序时会遇到两个运行时错误:
 bus error(core dumped) //
总线错误(信息已转储)
 segmentation fault(core dumped)
段错误(信息已转储)
总线错误几乎都是由于未对齐的读或者写引起的。之所以称之为总线错误,是因为出现未对齐的内存访问请求时,被阻塞的组件就是地址总线。

例:

union { char a[10];
  int i;
 }u;
int *p = (int *)&(u.a[1]);
*p = 17; /*p
中未对齐的地址会引起一个总线错误!*/
这将导致一个总线错误, 数组和int 的联合确保数组a是按照int4字节对齐的,所以“a+1”的地址肯定未按int对齐。然后我们试图往这个地址存储4个字节的数据,但这个访问只是按照单字节的char对齐,这就违反了规则。

在指针的强制类型转换:ptr1=(TYPE*)ptr2中,如果sizeof(ptr2的类型)大于sizeof(ptr1的类型),那么在使用指针ptr1来访问ptr2所指向的存储区时是安全的。如果sizeof(ptr2的类型)小于sizeof(ptr1的类型),那么在使用指针ptr1来访问ptr2所指向的存储区时是不安全的。

例:
chars='a';
int*ptr;
ptr=(int*)&s;

*ptr=1234;
指针ptr是一个int*类型的指针,它指向的类型是int。它指向的地址就是s的首地址。在32位程序中,s占一个字节,int类型占四个字节。最后一条语句不但改变了s所占的一个字节,还把和s相临的高地址方向的三个字节也改变了。这三个字节是干什么的?只有编译程序知道,而写程序的人是不太可能知道的。也许这三个字节里存储了非常重要的数据,也许这三个字节里正好是程序的一条代码,而由于你对指针的马虎应用,这三个字节的值被改变了!这会造成崩溃性的错误。

 

3段错误:

段错误是由于内存管理单元的异常所致,该异常通常是由于解除引用一个未初始化或非法值的指针引起的。

例:

int *p = 0;
 *p = 17;/*
引起一个段错误*/

通常导致段错误的几个直接原因:

Ø        解除引用一个包含非法值的指针

Ø        解除引用一个空指针(常常由于从系统程序中返回空指针,未经检查就使用)

Ø        在未得到正确的权限时进行访问。例,试图往一个只读的文本段存储值就会引起段错误

Ø        用完了堆栈或堆空间

 

第八章 为什么程序员无法分清万圣节和圣诞节

 

第九章 再论数组

 

1)什么时候数组和指针相同

规则一:表达式中的数组名就是指针

           对数组下标的引用总是可以写成一个指向数组起始地址的指针加上偏移量,对数组的引用如a[i]在编译时总是被编译器改写成*(a+1)的形式。在表达式中,指针和数组可以互换,因为它们在编译器里的最终形式都是指针,并且都可以进行取下标操作。

规则二:c语言把数组下标作为指针的偏移量

           无论是指针还是数组,在连续的内存地址上移动时,编译器都必须计算每次移动的步长,计算的方法是偏移量乘以每个数组元素占用的字节数。C语言把数组下标改写成指针偏移量的根本原因是指针和偏移量是底层硬件所使用的基本类型。

规则三:作为函数参数的数组名等同于指针

 

2)数组和指针可交换性的总结

1 .用a[i]这样的形式对数组进行访问总是被编译器“改写”或解释为像*(a+i)这样的指针访问。
2
.指针始终就是指针。它绝不可以改写成数组。你可以用下标形式访问指针,一般都是指针作为函数参数时,而且你知道实际传递给函数的是个数组
3
.在特定的上下文中,也就是它作为函数的参数(也只有这种情况),一个数组的声明可以看作是一个指针。作为函数参数的数组(就是在一个函数调用中)始终会被编译器修改成为指向数组第一个元素的指针。
4
.因此,当把一个数组定义为函数的参数时,可以选择把它定义为数组,也可以定义指针,不管选择哪种方法,在函数内部事实上获得的都是一个指针。
5
.在其他所有情况中,定义和声明必须匹配。如果定义了一个数组,在其他文件对它进行声明时也必须把它声明为数组,指针也是如此。

第十章 再论指针

 

1)数组和指针参数是如何被编译器修改的:

数组名被改写成一个指针参数规则并不是递归定义的。数组的数组会被改写成数组的指针,而不是指针的指针

实参                                所匹配的形式参数

数组的数组 char c[8][10]               char (*c)[10];数组指针

指针数组  char *c[15]                   char **c;     指针的指针

数组指针(行指针)  char (*c)[64]      char (*c)[64]    不改变

指针的指针 char **c                     char **c     不改变

 

另:

数组a &a

a:是数组的首地址即&a[0]
&a:
以整个数组为单位取其地址.
若分别相同类型加1,一个的长度是a[0]的长度,一个是a[]的长度。

例:

#include <stdio.h>
int main(int argc, char* argv[])
{
    int   a[5]={1,2,3,4,5}; 
    int   *ptr1=(int*)(&a+1); 
    int   *ptr2=(int*)(&a[0] + 1);

    printf("%x,%x,%x/n",ptr1[-1],*ptr2); 
    getchar();
    return   0; 
    
}

将打印:52.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值