zoukankan      html  css  js  c++  java
  • C语言深度解剖读书笔记

      开始本节学习笔记之前,先说几句题外话。其实对于C语言深度解剖这本书来说,看完了有一段时间了,一直没有时间来写这篇博客。正巧还刚刚看完了国嵌唐老师的C语言视频,觉得两者是异曲同工,所以就把两者一起记录下来。等更新完这七章的学习笔记,再打算粗略的看看剩下的一些C语言的书籍。

    本节知识:

    1.c语言中一共有32个关键字,分别是:auto、int、double、long、char、short、float、unsigned、signed、sizeof、extern、static、goto、if、else、struct、typedef、union、enum、switch、case、break、default、do、while、const、register、volatile、return、void、for、continue。注意:define、include这些带#号的都不是关键字,是预处理指令。
    2.定义与声明
    定义   是创建一个对象并为止分配内存。  如:int   a;
    声明   是告诉编译器在程序中有这么一个对象,并没有分配内存。   如: extern   int    a;
    3.对于register这个关键字定义的变量,不能进行取地址运算(&),因为对于x86架构来说,地址都是在内存中的,不是在寄存器中的,所以对寄存器进行取地址是没有意义的。并且应该注意的是给register定义的变量,应该赋一个比寄存器大小 要小的值。注意:register只是请求寄存器变量,但是不一定申请成功。
    4.关键字static:=
       对于static有两种用法:
       a.修饰变量:对于静态全局变量和静态局部变量,都有一个特点就是不能被作用域外面,或外文件调用(即使是使用了extern也没用)。原因就是它是存储在静态存储区中的。对于函数中的静态局部变量还有一个问题,就是它是存在静态存储区的,即使函数结束栈区收回,这个变量的值也不改变。static int i=0;  这是一条初始化语句  而不是一条赋值语句  所以跟i=0不一样的。
       b.修饰函数 :是定义为静态函数,使函数只能在文件内部使用,这样不同文件中的函数名就不怕重名了。原因也是相同的,就是static修饰的一切都是在静态存储区中的。
    -
    static代码如下:
    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <stdlib.h>  
    3.   
    4. int main(void)   
    5. {  
    6.     static int j=0;  
    7.     int k;  
    8.     void fun1()  
    9.     {  
    10.         j=0;  
    11.         j++;  
    12.         printf("fun1 %d\n",j);  
    13.     }  
    14.     void fun2()  
    15.     {  
    16.   
    17.         static int i=0;  
    18.         //i=0;  
    19.         printf("fun2 %d\n",i);  
    20.         i++;  
    21.     }  
    22.     for(k=0;k<10;k++)  
    23.     {  
    24.             fun1();  
    25.             fun2();  
    26.     }   
    27.     return 1;    
    28. }  


    5.关键字sizeof:
    怎么说明sizeof是关键字不是函数,这里有两个例子:
    a. int i;    printf("%d\n",sizeof i); 可见 sizeof是关键字
    b. sizeof(fun());  不调用fun函数 因为sizeof是在预编译期间完成的  说明是关键字
    sizeof的代码:
    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <stdlib.h>  
    3.   
    4. void fun(int b[100])  
    5. {  
    6.     printf("sizeof(b) is %d\n",sizeof(b));  
    7. }  
    8.   
    9. int main(void)   
    10. {  
    11.     int *p=NULL;  
    12.     int a[100];  
    13.     int b[100];  
    14.     printf("sizeof(p) is %d\n",sizeof(p));  
    15.     printf("sizeof(*p) is %d\n",sizeof(*p));  
    16.     printf("sizeof(a[100]) is %d\n",sizeof(a[100]));  
    17.     printf("sizeof(a) is %d\n",sizeof(a));  
    18.     printf("sizeof(&a) is %d\n",sizeof(&a));  
    19.     printf("sizeof(&a[0] is %d\n",sizeof(&a[0]));  
    20.       
    21.     fun(b);  
    22.     return 1;  
    23. }  
    6.关键字if:
    a.对于bool类型的比较:FLASE都是0  TRUE不一定是1   所以应该用if(bool_num);    if(!bool_num);
    对于浮点型与0比较要是否注意:不能直接比较,要定义精度,其实浮点型与浮点型比较也要注意这个问题,就是不能直接比较,要设定精度,如图:
    原因跟浮点型的存储格式有关,因为float的有效位是6位,超出6位就未知了,所以不能直接进行比较。同样的原因,也不能用一个很大的浮点数去加一个很小的浮点数。这个加法可能体现不出来。
    b.对于if后面的分号问题 ,一定要注意, 会被解析成if后面有一个空语句, 所以使用空语句的时候最好使用NULL;
    c.在使用if else的时候,应该if里面先处理正常情况(出现概率大的情况),else里面处理异常情况,这是一个好习惯看着代码舒服。
    7.关键字switch、case:
    注意case后面应该是整型或者字符型的常量及常量表达式,case后面最好是应该安装字母或数字顺序排列,先处理正常情况,后处理异常情况。
    8.关键字void:
    void *的一般用途是, 接收任何类型的指针 ,如当传入函数的指针类型不确定的时候,一般用 void*接收任何类型的指针。
    void* 指针作为右值赋值给其他指针的时候一定要强制类型转换,因为void* 指针类型不定。
    GNU中void *p p++跟char *p p++是一样的 。
    注意:strcpy跟memcpy的区别 就是 strcpy是char *   memcpy是void *  。所以说strcpy是给字符串赋值,memset是给整块内存赋值。
    9.关键字extern:
     extern就有两种用法:一种是声明外部定义的变量或函数、另一种是extern c告诉编译器以标准c语言方式编译
    10.关键字return:
    使用return的时候,要注意不能返回栈内指针,因为在函数体结束后,栈是会被收回的,其实是不能期望返回一个指针,来返回一块内存。因为返回一个指针或者地址没有问题,因为return是copy然后返回的,但是那个指针指向的内存如果是在函数栈中的话,就很有可能在函数结束后被收回了!!!
    return  ; 一般返回的值是1,根据编译器而定。
    11.关键字const:
    a.const是用来定义只读变量的,切忌它定义的是变量,不是常量,真的常量是#define的和enum。
    b.在陈正冲老师的这本书中的第35页,有说编译器不为普通const只读变量分配内存空间,而是将它们保存在符号表中,这使得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高,节省空间。具体的没怎么看懂,本次学习也不打算看懂了(因为它说const修饰的全局只读变量是在静态区的,我太认同)~~~嘿嘿
    c.其实const就是修饰变量,然后这个变量就不能当作左值了,当作左值,编译器就报错!!!
    d. 其实const中最不好区分的知识点是,如图:
    其实对于这四个情况的记忆很简单,就是看const跟谁近,是const *p   ,还是  * const  p,还是const  *  const  p,这样就很容易看出来const是修饰谁的了吧。
    e.但是const修饰的变量可以通过,指针将其改变。
    f.const修饰函数参数表示在函数体内不希望改变参数的值,比如说在strcmp等函数中,用的都是const  char*
    g.const修饰函数返回值表示返回值不可以改变,多用于返回指针的情况:
    [cpp] view plaincopy
     
     
    1. cosnt int* func()  
    2. {  
    3.       static int  count  =  0;  
    4.       count++;  
    5.       return &count;  
    6. }  

    h.在看const修饰谁,谁不变的问题上,可以把类型去掉再看,代码如下:

    [cpp] view plaincopy
     
     
    1. struct student  
    2. {  
    3.           
    4. }*str;  
    5. const str stu3;  
    6. str const stu4;  

    str是一个类型 ,所以在去掉类型的时候,应该都变成const stu3和const stu4了,所以说应该是stu4和stu3这个指针不能被赋值。
    12.关键字volatile:
    volatile搞嵌入式的,一定都特别属性这个关键字,记得第一使用这个关键字的时候是在韦东山老师的,Arm裸机视频的时候。volatile是告诉编译不要对这个变量进行任何优化,直接在内存中进行取值。一般用在对寄存器进行赋值的时候,或修饰可能被多个线程访问的变量。

    注意:const  volatile  int  i;  应该是定义了一个只读寄存器。
    13.关键字struct:
    a.对于空结构体的大小问题 ,vc和gcc的输出是不一样的,vc是1 、gcc是0 ,而且vc对于结构体的定义也和gcc不一样 ,vc中有c++的标准扩展了struct的作用,而gcc中是纯c的标准,就是按照标准c语言来的。
    b.struct这里还有一个很有用的东西,就是柔性数组,这个东西很有意思,我已经在数据结构的静态链表中进行了阐述,这里就仅仅记录一下,不详细说明了。
    14.关键字union: 
    union有一个作用就是判断,pc是大端存储还是小端存储的,x86是小端存储的,这个东西是有cpu决定的。arm(由存储器控制器决定)和x86一样都是小端的。
    下面的是一个大端小端的一个例子,代码如下:
    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <stdlib.h>  
    3. int main(void)   
    4. {  
    5.     int a[5]={1,2,3,4,5};  
    6.     int *p=(int *)(&a+1);  //数组指针 加一  进行正常的指针运算 走到数  
    7.   
    8. 组尾   
    9.     int *d=(int *)((int)a+1);//地址加一  不是指针运算  
    10.     //printf("%x\n",*((char *)((int)a+1)-1));  
    11.        
    12.     /*因为是小端存储  高地址  0x00  0x00  0x00  0x02  0x00  0x00  0x00  0x01 低地址*/  
    13.     /*变成了 0x02  0x00  0x00  0x00 */   
    14.     printf("%x,%x",p[-1],*d);  /*  第二个值就是这么存储的0x02  0x00  0x00  0x00  低地址处  所以就是2000000*/  
    15.     int a=0x11223344;  
    16.     char *p=(char *)((int)&a);  
    17.     printf("%x\n%x\n",*(p+0),p+0);   
    18.     printf("%x\n%x\n",*(p+1),p+1);  
    19.     return 0;  
    20. }  
    下面是一个利用union判断PC是大端小端的例子,代码如下:
    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <stdlib.h>  
    3. union  
    4. {  
    5.     int i;  
    6.     char a[2];  
    7. }*p,u;  
    8.   
    9. int main(void)   
    10. {  
    11.     p=&u;  
    12.     p->i=0x3839;  
    13.     printf("%x\n",p->i);  
    14.     printf("a0p=%x,a1p=%x\n",&(p->a[0]),&(p->a[1]));  
    15.     printf("a0=%x,a1=%x\n",p->a[0],p->a[1]);    
    16.     return 0;  
    17. }  
    15.enum关键字:
    枚举enum其实就是int类型,用来保存枚举常量的。enum枚举类型,这个才是真正的常量,定义常量一般用enum 。#define是宏定义是在预编译期间单纯的替换。#define宏定义无法调试,枚举常量是可以调试的。#define宏定义是无类型信息的,枚举类型是有类型信息的常量,是int型的。
    16.typedef关键字:
    a.typedef用于给一个已经存在的数据类型重新命名。
    b.typedef并没有产生新的数据类型
    c.typedef重定义的类型不能进行unsigned和signed进行扩展
    原因在于typedef 定义新类型的时候 应该定义全了,unsigned int是一个类型  不能拆开的。
    [cpp] view plaincopy
     
     
    1. typedef  unsigned  int   int32;  
    d.typedef 和 #define的区别:typedef是给已有的类型取别名,而#define只是简单的字符替换。区别如下图:
    #define PCHAR char*             PCHAR p3,p4;  //p3是char*型 p4是char型
    typedef char* PCHAR;             PCHAR p1,p2;    //p1和p2都是 char*型
    e.有一个知识点忘记了,嘿嘿,程序如下:
    [cpp] view plaincopy
     
     
    1. typedef struct student  
    2. {  
    3. }str,*str1;  

    str1 abc;  就是定义一个struct student *类型
    str abc;   就是定义一个struct student 类型
    f.对于const和typedef还有两个问题遗漏了,在< c++学习笔记(1.c到c++的升级)>这篇文章中的最后 (8.补充) 中进行了阐述。
     
    17.关键字for
    a.长循环应该在最内层,这样可以减少各个层直接的切换
    b.看看如下两段代码有什么区别:
    [cpp] view plaincopy
     
     
    1. 程序一:  
    2. for(i=0; i<m; i++)  
    3. {  
    4.     for(j=0; j<n; j++)  
    5.     {  
    6.         for(k=0; k<p; k++)  
    7.         {  
    8.             c[i][j] = a[i][k] * b[k][j];  
    9.         }  
    10.     }  
    11. }  
    12.   
    13. 程序二:  
    14. for(i=0; i<m; i++)  
    15. {  
    16.     for(k=0; k<p; k++)  
    17.     {  
    18.         for(j=0; j<n; j++)  
    19.         {  
    20.             c[i][j] = a[i][k] * b[k][j];  
    21.         }  
    22.     }  
    23. }  
    从程序来看,两者实现了同样的功能,区别只是第二层和第三层循环交换了位置。但是他们的差距却是巨大的 ,这个需要从CPU的cache来说了, cpu每次访问内存的时候都会先从内存将数据读入cache ,然后以后都从cache取数据。但是cache的大小是有限的 ,因此只会有部分进入cache。我们来看这个程序 c[i][j] = a[i][k] * b[k][j];  我们都知道C中二维数组是在内存中一维排列的,如果我们把k循环放在第三层 ,那么cache基本没有用了, 每次都需要重新到内存取数据,交换后每次取到cache的数据都可以复用多次 。所以说第二种写法效率高。
    18.关键字char(本节最重要的知识点char越界的问题):
    对于char有两种类型,分别是:unsigned  char(范围是0~255)和  signed  char(范围是-128~127)  一个是有符号的,一个是没有符号的。
    在计算机中数据都是以数据的补码形式进行存储的,所以如图:
    对于无符号类型(unsigned  int):就是不考虑最高位的问题,都是原码与补码相等的情况。
         然后我们说说越界的问题,对于一个unsigned  char  i;  我们给 i = 256;这很明显越界了,i是0到255的,那256的补码是什么再在它补码中取低八位就是i的值了。256的补码是1  0000  0000,所以printf ("%d\n",i);的值会是0。如果i = -1;-1的补码是1111 1111 所以会打印出255。
         对于一个char类型的越界又是什么样的呢?
         char  i; 我们给 i  =  129; 129是一个正数,它的补码就是原码:是1000 0001,但是它是char型,在char型中1000  0001是什么,如图是-127。所以printf("%d\n",i);  得到的是-127。如果i  =  -129,它的补码是0111  1111,所以它打印出来的是127。如果是i  =  259,我们就把它的补码取低八位来看。259的补码是1  0000  0011  所以说打印出来的是3。最后一个例子,如果i  =  385,它的补码是1   1000    0001  ,取低八位是1000   0001,所以打印的应该是-127。
         其实不管是有符号的还是没符号的,原则就一个,把数据转换成为补码,取低八位,然后在上面的图中去比较,就ok了。
    给一个练习,代码如下:
    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <stdlib.h>  
    3. #include <string.h>  
    4. int main()  
    5. {  
    6.     char a[1000];  
    7.     int i;  
    8.     for(i=0; i<1000; i++)  
    9.     {  
    10.         a[i] = (-1-i);  
    11.     }     
    12.          while(a[i])  
    13.     {  
    14.         printf("%d\n",a[i]);  
    15.         i++;  
    16.     }  
    17.     printf("%d\n",strlen(a));  
    18.     return 0;  
    19. }  
    打印结构是什么:答案是255   分析步骤跟上面是一样的,自己算算吧!!!
    其实int的越界原理跟char是一样的。
    19.一个关于tab键的问题:
    不同编辑器的tab键的字符数是不一样的,一般是4个字符,也有两个字节的,要注意一下,为了代码格式的整齐,建议设置一下tab或者使用空格。

    本节遗留问题:

    1.printf的实现问题,其实就是可变参数的问题,看linux源码,还有一个问题就是转移字符的问题,char p = '\'' 这样一个问题。
    2.浮点型的存储格式,为什么有效位是6位,小数是怎么保存的。
     
    二、

    本节接触了,C语言中的三大蛋疼:符号优先级  ++i顺序点  贪心法  (其实这里面好多都是跟编译器有关的,而且有好多问题都是可以通过良好的编程习惯避免的)

    本节知识点:

    1.注释问题:

        注释不能把关键字弄断,如:in/*注释*/t

        注释不是简单的剔除,而是使用空格替换

        编译器认为双引号括起来的内容都是字符串,双斜杠也不例外。如:char *p = "heh//jfeafe"   //不起注释作用

    2.接续符:

        接续符\  ,常用于宏定义中 

    [cpp] view plaincopy
     
     
    1. #define SWAP(a,b) \  
    2. {                 \  
    3.     int temp = a; \  
    4.     a = b;        \  
    5.     b = temp;     \  
    6. }  

        反斜杠同时有接续符和转义符两个用途,当接续符使用的时候,可以直接在程序中出现。当转义符使用的时候,必须是出现在字符串中。

        接续符,也用与接续一个关键字,代码如下,  注意: 但是直接连接\两边不能有空格。

    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <stdlib.h>  
    3.   
    4. int main()  
    5. {  
    6.     cha\  
    7. r a = 12;  
    8.     return 0;  
    9. }  

    3.逻辑运算符:有一个短路规则
    4.最容易忘记规则的两个运算符:

        三目运算符:(a?b:c)   当a的值为真的时候   返回b的值,否则返回c的值

        逗号表达式:a,b    表达式的值为b的值

    5.位运算:

        对于左移和右移<<  >>问题 :无符号的,和有符号左移,都是补0 ,对于有符号的在右移动的时候,正数补零,负数补什么跟编译器有关系。并且左移和右移的大小不能大于数据的长度,也不能小于0。

        交换两个数,有一种不借助中间变量的方法,就是异或,代码如下:

    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2.   
    3. #define SWAP1(a,b) \  
    4. {                  \  
    5.     int temp = a;  \  
    6.     a = b;         \  
    7.     b = temp;      \  
    8. }  
    9.   
    10. #define SWAP2(a,b) \  
    11. {                  \  
    12.     a = a + b;     \  
    13.     b = a - b;     \  
    14.     a = a - b;     \  
    15. }  
    16.   
    17. #define SWAP3(a,b) \  
    18. {                  \  
    19.     a = a ^ b;     \  
    20.     b = a ^ b;     \  
    21.     a = a ^ b;     \  
    22. }  
    23.   
    24. int main()  
    25. {  
    26.     int a = 1;  
    27.     int b = 2;  
    28.       
    29.     SWAP1(a,b);  
    30.     SWAP2(a,b);  
    31.     SWAP3(a,b);  
    32.       
    33.     return 0;  
    34. }  

    6.i++,i--顺序点:

            只有 i++ i--才有顺序点  就是什么时候开始加,什么时候开始减。真心对于顺序点 是搞不懂啊~~~ (++i)+(++i)+(++i) ,在gcc中是5+5+6(DEV C++) ,在vc中是6+6+6(vc++6.0) ,不同编译器顺序点不一样。这个例子的顺序点 在; 前。
            a=((++i),(++i),(++i))  它的顺序点在每个逗号前面完成计算。我觉得特殊的顺序点 是可以通过合理的顺序布局来避免的。

    7.贪心法:

            每一个符号应该尽可能多的包含字符
    8.符号运算优先级问题:

            个人觉得优先级不用记,好好的写括号吧~~~

             给一个易错优先级表,如图:

    9.c语言中的类型转换:

        c语言中有两种转换类型,分别是:隐式转换和显示转换(强制类型转换)

        隐式转换的规则:

        a.算术运算中,低类型转换为高类型

        b.赋值运算中,表达式的类型转换为左边变量的类型

        c.函数调用时,实参转换成形参的类型

        d.函数返回值,return表达式转换为返回值的类型

    隐式转换的例子,代码如下:

    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2.   
    3. int main()  
    4. {  
    5.     int i = -2;  
    6.     unsigned int j = 1;  
    7.       
    8.     if( (i + j) >= 0 )  
    9.     {  
    10.         printf("i+j>=0\n");  
    11.     }  
    12.     else  
    13.     {  
    14.         printf("i+j<0\n");  
    15.     }  
    16.       
    17.     printf("i+j=%d\n", i + j);  
    18.       
    19.     return 0;  
    20. }  

    注意:在使用C语言的时候,应该特别注意数据的类型是否相同,尽量避免隐式转换带来的不必要的麻烦~~~

       三、

    本节知识点:

    1.编译过程的简介:

       预编译:

    a.处理所有的注释,以空格代替。

    b.将所以#define删除,并展开所有的宏定义,字符串替换。

    c.处理条件编译指令#if,#ifdef,#elif,#else,#endif

    d.处理#include,并展开被包含的文件,把头文件中的声明,全部拷贝到文件中。

    e.保留编译器需要使用的#pragma指令、

    怎么样观察这些变化呢?最好的方法就是在GCC中,输入预处理指令,可以看看不同文件经过预处理后变成什么样了,预处理指令:gcc -E file.c -o file.i   注意:-C -E一起使用是预编译的时候保留注释。

       编译:

    a.对预处理文件进行一系列词法分析,语法分析和语义分析

                    词法分析:主要分析关键字,标示符,立即数等是否合法

                    语法分析:主要分析表达式是否遵循语法规则

                    语义分析:在语法分析的基础上进一步分析表达式是否合法

    b.分析结束后进行代码优化生成相应的汇编代码文件               编译指令:gcc -S  file.c  -o  file.s

       汇编:

    汇编器将汇编代码转变为机器可以执行的指令,每个汇编语句几乎都对应一条机器指令,其实机器指令就是机器码,就是2进制码。汇编指令:gcc  -c  file.c  -o file.o  注意:-c是编译汇编不连接。

       链接:

    再把产生的.o文件,进行链接就可以生成可执行文件。连接指令:gcc  file.o  file1.o  -o  file  这句指令是链接file.o和file1.o两个编译并汇编的文件,并生成可执行文件file。

    链接分两种:静态链接和动态链接,静态链接是在编译器完成的,动态链接是在运行期完成的。静态链接的指令是:gcc -static file.c -o file对于一些没有动态库的嵌入式系统,这是常用的。

    一般要想通过一条指令生成可执行文件的指令是:   gcc file.c  -o  file

       资料:这里面说到了很多关于gcc的使用的问题,我提供一个gcc的学习资料,个人觉得还不错,也不长,就是一个txt文档,很全面。资源下载地址http://download.csdn.net/detail/qq418674358/6041183   Ps:嘿嘿,设了一个下载积分,因为真的是没分用了!希望大家见谅哈!

    2.c语言中的预处理指令:#define、#undef(撤销已定义过的宏名)、#include、#if、#else、#elif、#endif、#ifdef、#ifndef、#line、#error、#pragma。还有一些ANSI标准C定义的宏:__LINE__、__FILE__、__DATA__、__TIME__、__STDC__。这样使用printf("%s\n",__TIME__);     printf(__DATE__);

    一个#undef的例子:

    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <stdlib.h>  
    3. #include <string.h>  
    4.   
    5.   
    6. #define X 2  
    7. #define Y X*2  
    8. #undef X  
    9. #define X 3  
    10.   
    11.   
    12. int main()  
    13. {  
    14.     printf("%d\n",Y);  
    15.     return 0;  
    16. }  

    这个输出的是6,说明了#undef的作用

    3.宏定义字符串的时候:应该是 #define HELLO "hello world"  记住是双引号。还有就是一切宏都是不能有分号的,这个一定要切忌!!!

    4.宏与函数的比较:

       a.宏表达式在预编译期被处理,编译器不知道有宏表达式存在

       b.宏表达式没有任何的"调用"开销

       c.宏表达式中不能出现递归定义

    5.为什么不在头文件中定义全局变量:

    如果一个全局变量,想要在两个文件中,同时使用,那这两个文件中都应该#include这个头文件,这样的话就会出现重复定义的问题。其实是重名的问题,因为#include是分别在两个文件中展开的,试想一下,如果在两个文件中的开始部分,都写上int  a = 10;  是不是也会报错。可能你会说那个#ifndef不是防止重复定义吗?是的 ,那是防止在同一个文件中,同时出现两次这个头文件。现在是两个文件中,所以都要展开的。全局变量就重名了!!!所以 对于全局变量,最好是定义在.c文件中,不要定义在头文件中。

    6.#pargma pack 设置字符对齐,看后面一节专门写字符对齐问题的!!!

    7.#运算符(转换成字符串):

        假如你希望在字符串中包含宏参数,那我们就用#号,它把语言符号转换成字符串。

        #define SQR(x) printf("the "#x"lait %d\n",((x)*(x)));
        SQR(8)
        输出结果是:the 8 lait 64   这个#号必须使用在带参宏中

    有个小例子:

    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <stdlib.h>  
    3. #include <string.h>  
    4. /*在字符串中  加入宏参用的*/  
    5. #define SCAN(N,String) scanf("%"#N"s",String);  //N是截取的个数  String是存储的字符串   
    6. int main()  
    7. {  
    8.     char dd[256];  
    9.     SCAN(3,dd) //记得没有分号哈  自定义 任意格式输入的scanf  截取输入的前三个   
    10.     printf("%s\n",dd);  
    11.     return 1;  
    12. }  

    8.##运算符(粘合剂)

        一般用于粘贴两个东西,一般是用作在给变量或函数命名的时候使用。如#define XNAME(n) x##n

        XNAME(8)为8n   这个##号可以使用在带参宏或无参宏中

    下面是一个##运算符的小例子,代码如下:

    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <stdlib.h>  
    3. #include <string.h>  
    4.   
    5. #define BL1 bb##ll##1  
    6.   
    7. #define BL(N) bbll##N  
    8. int main()  
    9. {  
    10.     int BL1=10;  
    11.   
    12.     int BL(4)=15;  
    13.     printf("%d\n",bbll1);  
    14.       
    15.     printf("%d\n",bbll4);  
    16.     return 1;  
    17. }  

    注意:#号和##号都必须只能在宏定义中使用,不能使用在其他地方
    9.其实预编译这块还有一些,不常用到的预编译指令,也是盲点,但是不难理解,用到的时候查查就好。比如说#line、#error、#warning等。

     
     
    四、
     
     
     

    很多人都觉得内存对齐这个问题很难,很不好算,总算错,其实我想说只要你画一画就没那么难了。好了,进入正题。

    本节知识点:

    1.结构体为什么要内存对齐(也叫字节对齐):

    其实我们都知道,结构体只是一些数据的集合,它本身什么都没有。我们所谓的结构体地址,其实就是结构体第一个元素的地址。这样,如果结构体各个元素之间不存在内存对齐问题,他们都挨着排放的。对于32位机,32位编译器(这是目前常见的环境,其他环境也会有内存对齐问题),就很可能操作一个问题,就是当你想要去访问结构体中的一个数据的时候,需要你操作两次数据总线,因为这个数据卡在中间,如图:

    在上图中,对于第2个short数据进行访问的时候,在32位机器上就要操作两次数据总线。这样会非常影响数据读写的效率,所以就引入了内存对齐的问题。

    另外一层不太重要的原因是:某些硬件平台只能从规定的地址处取某些特定类型的数据,否则会抛出硬件异常。

    2.内存对齐的规则:

        a.第一个成员起始于0偏移处

        b.每个成员按其类型大小和指定对齐参数n中较小的一个进行对齐

        c.结构体总长度必须为所有对齐参数的整数倍

        d.对于数组,可以拆开看做n个数组元素

    3.来几个小例子,画画图,有助于理解:

    第一个例子,代码如下:

    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. struct _tag_str1  
    3. {  
    4.     char a;  
    5.     int b;  
    6.     short c;  
    7. }str1;  
    8.   
    9. struct _tag_str2  
    10. {  
    11.     char a;  
    12.     short c;  
    13.     int b;  
    14. }str2;  
    15.   
    16. int main()  
    17. {  
    18.     printf("sizeof str1 %d\n",sizeof(str1));  
    19.     printf("sizeof str2 %d\n",sizeof(str2));  
    20.     return 0;  
    21. }   
    输出的结果分别是:str1为12    str2为8,分析的过程如下图:

    看图很自然就知道了str1为12个字节,str2为8个字节。

    第二个例子,上面的那个例子有好多问题还没有考虑到,比如说上面的那个例子在8字节对齐,和4字节对齐的情况都是一样的。结构体中嵌套结构体的内存对齐怎么算,所以就有了这个例子,代码如下:

    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2.   
    3. #pragma pack(8)  
    4. //#pragma pack(4)  
    5. struct S1  
    6. {  
    7.     short a;  
    8.     long b;  
    9. };  
    10.   
    11. struct S2  
    12. {  
    13.     char c;  
    14.     struct S1 d;  
    15.     double e;  
    16. };  
    17.   
    18. #pragma pack()  
    19.   
    20. int main()  
    21. {  
    22.     struct S2 s2;  
    23.       
    24.     printf("%d\n", sizeof(struct S1));  
    25.     printf("%d\n", sizeof(struct S2));  
    26.     printf("%d\n", (int)&(s2.d) - (int)&(s2.c));  
    27.   
    28.     return 0;  
    29. }  
    在Dev c++中,默认的是8字节对齐。我们分析下在4字节对齐的情况下输出的是,S2是20,S1是8,分析如图:

    在4字节对齐的情况中,有一个问题值得注意:就是图中画1的地方。这里面本应short是可以上去的。但是对于结构体中的结构体一定要十分警惕,S1是一体的,short已经由于long进行了内存对齐,后面还空了两个字节的内存,其实此时的short已经变成了4个字节了!!!即结构体不可拆,不管是多少字节对齐,他们都是一体的。所有的圈都变成了叉。所以说结构体只能往前篡位置,不能改变整体。

    我们在分析一些8字节对齐的情况,如图:

    同样,到这里又有一个字节对齐的原则要好好重申一下:就是以什么为对齐参数,首先我们要知道编译器或者自己定义的是多少字节对齐的,这个数为n。然后我们要看这个结构体中的各个数据类型,找到所占字节数最大的类型,为m。如果n大于m,就以m为对齐参数,比如说一个4字节对齐的结构体中都是short,那这个结构体以什么为对齐参数,当然是2了,如果m大于n,就以n为对齐参数,比如说在4字节对齐的情况下的double类型。

     以上就是我对内存对齐的小总结,最最想要说明的就是两大段红色的部分。    

     

    1.int a=9,b=10,d=9;是可以的。

    2.%*d ,在scanf中使用的时候,是1整数但不赋给任何变量,有个小代码:

    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <malloc.h>  
    3.   
    4. int main()  
    5. {  
    6.     int a=23,b=5,c=9;  
    7.     scanf("%*d%d%d",&a,&b,&c);  
    8.     printf("%d,%d,%d",a,b,c);  
    9.     return 0;  
    10. }  

    a的值,你是赋值不进去的,仅仅占位用的。

    3.对于冒泡排序,怎么在不完全执行完循环前就预先判断,已经排序结束了:

    在一次内层循环的时候,一次都没有进行数据交换,就说名冒泡排序已经排序ok了。

    4.不要总记得scanf,同样还存在getchar()和gets()函数,gets能接收含有空格的字符串,这个是scanf不能做到的。

    scanf("%ls",a);  //接收有效字符串的第一个字符

    scanf("%ns",a);   //这个是格式化输入,接收字符串的从头开始的n个字符

    其实我想说,scanf函数真心没有什么用,很不好的一个函数。

    5.堆区分配内存是从两头开始增长的,不是单向增长的。

    6.typedef int [10]   其实[10]就是int了,个人觉得这个代码风格,很不好,千万不能写成这样,可读性很差!

    7.要记住函数在传递参数的时候,其实是数据的拷贝,直接对形参进行改变或者赋值,是毫无意义的,实参是不会改变的。对于指针也是一样的。只有通过指针,取得了当前这个指针指向的内容的时候,改变了这个内容,这样实参才会被改变。因为是直接改变了内存地址中保存的数值。

    举个例子就是:在数据结构那节中的链表,creat函数就是一个典型的例子。仔细想想为什么不能在main函数中定义一个头结点,再把这个头结点的地址传给creat函数呢?一定要通过creat返回一个头结点指针呢?再想想,为什么在想通过形参获得子函数中数据的时候,一定要传入地址或者指针呢?然后再把想要获得数据,写入这个地址或者指针中去?

    给一段代码,帮助理解这个问题:

    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <malloc.h>  
    3. typedef struct _tag_str  
    4. {  
    5.     int a;  
    6.     int b;  
    7. }str;  
    8. void fun(str* str1)  
    9. {  
    10.     str1 = (str* )malloc(sizeof(str));  
    11.     str1->a = 12;  
    12.     str1->b = 34;  
    13. }  
    14. int main()  
    15. {  
    16.     /*str* strp; 
    17.     fun(strp); 
    18.     printf("%d\n",strp->a); 
    19.     printf("%d\n",strp->b);*/  
    20.     str str1;  
    21.     fun(&str1);  
    22.     printf("%d\n",str1.a);  
    23.     printf("%d\n",str1.b);  
    24. }  

    想想,为什么子函数中赋值,在main中打印出来是不一样的!!!
          对于fun(strp)的过程是这样的:在函数传递参数的时候,strp的值 赋值给了子函数的str1,这个过程就是函数参数拷贝的过程,然后str1的值在malloc的时候不幸被malloc改变了,所以在main中打印出来的不一样。

          对于fun(&str1)的过程是这样的:在函数传递参数的时候,&str1的值  赋值给了子函数的str1,后面的过程跟上面一样。所以在main中打印的也是不一样的。

    对于这种情况,最好的解决办法就是利用函数返回值,把str1返回 回来就ok了!!!

    注意:可能你会问了,那怎样通过参数获得子函数传递的值啊,其实很简单,你在main中开辟好一段内存,然后把这个内存地址传递到子函数中去,然后对这个内存进行赋值,不要去改变这个指针的指向(即指针的值),仅仅改变指针指向的内存(即指针指向的内容),自然就获得了你想要的值!

    8.c语言文件操作的一个问题:

       c语言中打开文件有两种方式,一种是二进制方式,另一种是文本方式(ASCII码方式)。这两种方式有什么区别?(对于Linux这种只有一种文件类型的操作系统来说是没有区别的)

       我们就以windows为例说说区别:

       a.以文本方式打开文件,若将数据写入文件,如果遇到换行符'\n'(ASII 值为10,0A),则会转换为回车—换行'\r\n'(ASCII值为13,10,0D0A)存入到文件中,同样读取的时候,若遇到回车—换行,即连续的ASCII值13,10,则自动转换为换行符。

          而以二进制方式打开文件时,不会进行这样的处理。

       b.还有如果以文本方式打开文件时,若读取到ASCII码为26(^Z)的字符即0x1a,则停止对文件的读取,会默认为文件已结束,而以二进制方式读取时不会发生这样的情况。由于正常情况下我们手动编辑完成的文件是不可能出现ASCII码为26的字符,所以可以用feof函数去检测文件是否结束。

       所以,由于存在上面的两个区别,我们在明确文件类型的时候,最好使用相对应的方式对文件进行打开。对于那些不明确文件类型的时候,最好使用二进制方式打开文件。

     

    指针这一节是本书中最难的一节,尤其是二级指针和二维数组直接的关系。

    本节知识点:

    1.指针基础,一张图说明什么是指针:
    2.跨过指针,直接去访问一块内存:
        只要你能保证这个地址是有效的 ,就可以这样去访问一个地址的内存*((unsigned int *)(0x0022ff4c))=10;  但是前提是 0x0022ff4c是有效地址。对于不同的编译器这样的用法还不一样,一些严格的编译器,当你定义一个指针,把这个指针赋值为一个这样的地址的时候,当检测到地址无效,编译的时候就会报错!!!如果一些不太严格的编译器,不管地址有效无效都会编译通过,但是对于无效地址,当你访问这块地址的时候,程序就会运行停止!
    3.a     &a    &a[0]三者的区别:
    首先说三者的值是一样的,但是意义是不一样的。(这里仅仅粗略的说说,详细见文章<c语言中数组名a和&a>)
         &a[0]:这个是数组首元素的地址
         a : 的第一个意义是 数组首元素的地址,此时与&a[0]完全相同
                    第二个意义是 数组名  sizeof(a)  为整体数组有多少个字节
        &a :这个是数组的地址 。跟a的区别就是,a是一个 int* 的指针(在第一种意义的时候) ,而&a是一个 int (*p)[5]类型的数组指针,指针运算的结果不一样。(此处的int* 仅仅是为了举例子,具体应该视情况而定)
    4.指针运算(本节最重要的知识点,但并不是最难的,所以的问题都来源于这儿):   
       对于指针的运算,首先要清楚的是指针类型(在C语言中,数据的类型决定数据的行为),然后对于加减其实就是对这个指针的大小加上或者减去,n*sizeof(这个指针指向的数据的类型)。即:一个类型为T的指针的移动,是以sizeof(T)为单位移动的。如:int* p;  p+1就是p这个指针的值加上sizeof(int)*1,即:(unsigned int)p + sizeof(int)*1。对于什么typedef的,struct的,数组的都是一样的。
    这个有一个例子,代码如下:
    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <stdlib.h>  
    3.   
    4. int main(int argc, char *argv[])   
    5. {  
    6. /*  int a[20]={1,2,4}; 
    7.     printf("%d\n",sizeof(a)); 
    8.     printf("%p\n",a); 
    9.     printf("%p\n",&a); 
    10.     printf("%p\n",&a[0]);    
    11. */  
    12.   
    13.   
    14. /*  int a[5]={1,2,3,4,5}; 
    15.     int (*p)[5]=&a; 
    16.     printf("%d\n",*((int *)(p+1)-1)); 
    17. */    
    18.       
    19.     int a[5]={1,2,3,4,5};  
    20.     int* p=(int *)(&a+1);  
    21. //  int *p=&a+1;  //这个条语句是  把&a这个数组指针 进行了指针运算后  的那个地址  强制类型转换成了 int *指针   
    22.     printf("%d\n",*(p-1));  
    23.     return 0;  
    24.       
    25. }  
    5.访问指针和访问数组的两种方式:
        分别是以下标方式访问和以指针的方式访问,我觉得没有任何区别,*(p+4)和p[4]是一样的 ,其实都可以理解成指针运算。如果非要说出区别,我觉得指针的方式会快些,但是在当前的硬件和编译器角度看,不会太明显。同样下标的方式可读性可能会高些。
    6.切记数组不是指针:
        数组是数组,指针是指针,根本就是两个完全不一样的东西。当然要是在宏观的内存角度看,那一段相同类型的连续空间,可以说的上是数组。但是你可以尝试下,定义一个指针,在其他地方把他声明成数组,看看编译器会不会把两者混为一谈,反过来也不会。
        但是为什么我们会经常弄混呢?第一,我们常常利用指针的方式去访问数组。第二,数组作为函数参数的时候,编译器会把它退化成为指针,因为函数的参数是拷贝,如果是一个很大的数组,拷贝是很浪费内存的,所以数组会被退化成指针(这里一定要理解好,退化的是数组成员的类型指针,不一定是数组指针的哈)。
    7.弄清数组的类型:
       数组类型是由数组元素类型和数组长度两个因素决定的,这一点在数组中体现的不明显,在数组指针的使用中体现的很好。
    [cpp] view plaincopy
     
     
    1. char a[5]={'a','b','c','d','e'};  
    2. char (*p)[3]=&a;  
       上面的代码是错误的,为什么?因为数组指针和数组不是一个类型,数组指针是指向一个数组元素为char 长度为3的类型的数组的,而这个数组的类型是数组元素是char长度是5,类型不匹配,所以是错的。
    8.字符串问题:
       a.C语言中没有真正的字符串,是用字符数组模拟的,即:字符串就是以'\0'结束的字符数组。
       b.要注意下strlen,strcmp等这个几个函数的返回值,是有符号的还是无符号的,这里很容易忽略返回值类型,造成操作错误。
       c.使用一条语句实现strlen,代码如下(此处注意assert函数的使用,安全性检测很重要):
    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <assert.h>  
    3.   
    4. int strlen(const char* s)  
    5. {  
    6.     return ( assert(s), (*s ? (strlen(s+1) + 1) : 0) );  
    7. }  
    8.   
    9. int main()  
    10. {  
    11.     printf("%d\n", strlen( NULL));  
    12.       
    13.     return 0;  
    14. }  
        d.自己动手实现strcpy,代码如下:
    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <assert.h>  
    3.   
    4. char* strcpy(char* dst, const char* src)  
    5. {  
    6.     char* ret = dst;  
    7.       
    8.     assert(dst && src);  
    9.       
    10.     while( (*dst++ = *src++) != '\0' );  
    11.       
    12.     return ret;  
    13. }  
    14.   
    15. int main()  
    16. {  
    17.     char dst[20];  
    18.   
    19.     printf("%s\n", strcpy(dst, "hello!"));  
    20.       
    21.     return 0;  
    22. }  
         e.推荐使用strncpy、strncat、strncmp这类长度受限的函数(这些函数还能在字符串后面自动补充'\0'),不太推荐使用strcpy、strcmpy、strcat等长度不受限仅仅依赖于'\0'进行操作的一系列函数,安全性较低。
         f.补充问题,为什么对于字符串char a[256] = "hello";,在printf和scanf函数中,使用a行,使用&a也行?代码如下:
    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. int main()  
    3. {  
    4.     char* p ="phello";  
    5.     char a[256] = "aworld";  
    6.     char b[25] = {'b','b','c','d'};  
    7.     char (*q)[256]=&a;  
    8.       
    9.     printf("%p\n",a);  //0022fe48  
    10.     //printf("%p\n",&a);  
    11.     //printf("%p\n",&a[0]);  
    12.       
    13.       
    14.     printf("tian %s\n",(0x22fe48));   
    15.     printf("%s\n",q);    //q就是&a   
    16.     printf("%s\n",*q);   //q就是a   
    17.       
    18.     printf("%s\n",p);  
    19.       
    20.     printf("%s\n",a);  
    21.     printf("%s\n",&a);  
    22.     printf("%s\n",&a[0]);  
    23.       
    24.     printf("%s\n",b);  
    25.     printf("%s\n",&b);  
    26.     printf("%s\n",&b[0]);     
    27. }  
    对于上面的代码:中的0x22fe48是根据打印a的值获得的。
    printf("tian %s\n",(0x22fe48));这条语句,可以看出来printf真的是不区分类型啊,完全是根据%s来判断类型。后面只需要一个值,就是字符串的首地址。a、&a、&a[0]三者的值还恰巧相等,所以说三个都行,因为printf根本就不判断指针类型。虽然都行但是我觉得要写有意义的代码,所以最好使用a和*p。还有一个问题就是,char* p = "hello"这是一个char*指针指向hello字符串。所以对于这种方式只能使用p。因为*p是hello字符串的第一个元素,即:‘h’,&p是char* 指针的地址,只有p是保存的hello字符串的首地址,所以只有p可以,其他都不可以。scanf同理,因为&a和a的值相同,且都是数组地址。
    9.二维数组(本节最重要的知识点):
          a.对于二维数组来说,二维数组就是一个一维数组 数组,每一个数组成员还是一个数组,比如int a[3][3],可以看做3个一维数组,数组名分别是a[0]  a[1]   a[2]   sizeof(a[0])就是一维数组的大小  ,*a[0]是一维数组首元素的值,&a[0]是 一维数组的数组指针。
          b.也可以通过另一个角度看这个问题。a是二维数组的数组名,数组元素分别是数组名为a[0]、a[1]、a[2]的三个一维数组。对a[0]这个数组来说,它的数组元素分别是a[0][0]  a[0][1]  、 a[0][2]三个元素。a和a[0]都是数组名,但是是两个级别的,a作为数组首元素地址的时候等价于&a[0](最容易出问题的地方在这里,这里一定要弄清此时的a[0]是什么,此时的a[0]是数组名,不是数组首元素的地址,不可以继续等价下去了,千万不能这样想 a是&a[0]    a[0]是&a[0][0]     a就是&&a[0][0] 然后再弄个2级指针出来,自己就蒙了!!!这是一个典型的错误,首先&&a[0][0]就没有任何意义,跟2级指针一点关系都没有,然后a[0]此时不代表数组首元素地址,所以这个等价是不成立的。Ps:一定要搞清概念,很重要!!! ),a[0]作为数组首元素地址的时候等价于&a[0][0]。但是二维数组的数组头有很多讲究,就是a(二维数组名)、&a(二维数组的数组地址)、&a[0](二维数组首元素地址  即a[0]一维数组的数组地址 a有的时候也表示这个意思)、a[0](二维数组的第一个元素 即a[0]一维数组的数组名)、&a[0][0](a[0]一维数组的数组首元素的地址 a[0]有的时候也表示这个意思),这些值都是相等,但是他们类型不相同,行为也就不相同,意义也不相同。分析他们一定要先搞清,他们分别代表什么。
    下面是一个,二维数组中指针运算的练习(指针运算的规则不变,类型决定行为):
    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <stdlib.h>  
    3. #include <stdbool.h>  
    4.   
    5. int main(int argc, char *argv[])   
    6. {  
    7.     int a[3][3]={1,2,3,4,5,6,7,8,9};  
    8.     printf("%d\n",sizeof(a[0]));  
    9.     printf("%d\n",*a[2]);  
    10.     printf("%d\n",*(a[0]+1));  
    11.       
    12.     printf("%p\n",a[0]);  
    13.     printf("%p\n",a[1]);  
    14.     printf("%p\n",&a[0]+1); //&a[0]+1 跟 a[1]不一样  指针类型不一样   &a[0]+1这个是数组指针  a[1]是&a[1][0] 是int*指针   
    15.       
    16.     printf("%d\n",*((int *)(&a[0]+1)));  
    17.       
    18.     printf("%d\n",*(a[1]+1));  
    19.       
    20.     printf("%p\n",a);  
    21.     printf("%p\n",&a);  
    22.     printf("%p\n",&a[0]);  
    23.       
    24.     printf("%d\n",sizeof(a));   //这是a当作数组名的时候  
    25.       
    26.     printf("%d\n",*((int *)(a+1))); //此时 a是数组首元素的地址  数组首元素是a[0]    
    27.                  //首元素地址是&a[0]  恰巧a[0]是数组名 &a[0]就变成了数组指针   
    28.     return 0;  
    29. }  
    总结:对于a和a[0]、a[1]等这些即当作数组名,又当作数组首元素地址,有时候还当作数组元素(即使当作数组元素,也无非就是当数组名,当数组首元素地址两种),这种特殊的变量,一定要先搞清它现在是当作什么用的
          c.二维数组中一定要注意,大括号,还是小括号,意义不一样的。
    10.二维数组和二级指针:
         很多人看到二维数组,都回想到二级指针,首先我要说二级指针跟二维数组毫无关系,真的是一点关系都没有。通过指针类型的分析,就可以看出来两者毫无关系。不要在这个问题上纠结。二级指针只跟指针数组有关系,如果这个二维数组是一个二维的指针数组,那自然就跟二级指针有关系了,其他类型的数组则毫无关系。切记!!!还有就是二级指针与数组指针也毫无关系!!
    11.二维数组的访问:
         二维数组有以下的几种访问方式:
         int   a[3][3];对于一个这样的二位数组
         a.方式一:printf("%d\n",a[2][2]); 
         b.方式二:printf("%d\n",*(a[1]+1));
         c.方式三:printf("%d\n",*(*(a+1)+1));
         d.方式四:其实二维数组在内存中也是连续的,这么看也是一个一维数组,所以就可以使用这个方式,利用数组成员类型的指针。
    [cpp] view plaincopy
     
     
    1. int *q;  
    2. q = (int *)a;  
    3. printf("%d\n",*(q+6));  
         e.方式五:二维数组中是由多个一维数组组成的,所以就可以利用数组指针来访问二维数组。
    [cpp] view plaincopy
     
     
    1. int (*p)[3];  
    2. p = a;  
    3. printf("%d\n",*(*(p+1)+1));  
    给一个整体的程序代码:
    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <stdlib.h>  
    3. #include <string.h>  
    4. int main()  
    5. {  
    6.     int a[3][3]={1,2,3,4,5,6,7,8,9};  
    7.     int (*p)[3];  
    8.     int *q;   
    9.     printf("%d\n",*(*(a+1)+1));   //a        *(&a[0]+1)  
    10.     p = a;  
    11.     q = (int *)a;  
    12.     printf("%d\n",*(*(p+1)+1));  
    13.     printf("%d\n",*(a[1]+1));  
    14.     printf("%d\n",a[1][1]);  
    15.     printf("%d\n",*(q+6));  
    16. }  
    17. <span style="font-family:Arial;BACKGROUND-COLOR: #ffffff"></span>  
     
    总结:对于二位数组int a[3][3]  要想定义一个指针指向这个二维数组的数组元素(即a[0]等一维数组),就要使用数组指针,这个数组指针要跟数组类型相同。a[0]等数组类型是元素类型是int,长度是3,所以数组指针就要定义成int (*p)[3]。后面的这个维度一定要匹配上,不然的话类型是不相同的。
    这里有一个程序,要记得在c编译器中编译,这个程序能看出类型相同的重要性:
    [cpp] view plaincopy
     
     
    1. <span style="color:#000000;">#include <stdio.h>  
    2.   
    3. int main()  
    4. {  
    5.     int a[5][5];  
    6.     int(*p)[4];  
    7.       
    8.     p = a;  
    9.       
    10.     printf("%d\n", &p[4][2] - &a[4][2]);  
    11. }</span>  
    12.二级指针:
        a.因为指针同样存在传值调用和传址调用,并且还有指针数组这个东西的存在,所以二级指针还是有它的存在价值的。
        b.常使用二级指针的地方:
              (1)函数中想要改变指针指向的情况,其实也就是函数中指针的传址调用,如:重置动态空间大小,代码如下:
    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <malloc.h>  
    3.   
    4. int reset(char**p, int size, int new_size)  
    5. {  
    6.     int ret = 1;  
    7.     int i = 0;  
    8.     int len = 0;  
    9.     char* pt = NULL;  
    10.     char* tmp = NULL;  
    11.     char* pp = *p;  
    12.       
    13.     if( (p != NULL) && (new_size > 0) )  
    14.     {  
    15.         pt = (char*)malloc(new_size);  
    16.           
    17.         tmp = pt;  
    18.           
    19.         len = (size < new_size) ? size : new_size;  
    20.           
    21.         for(i=0; i<len; i++)  
    22.         {  
    23.             *tmp++ = *pp++;        
    24.         }  
    25.           
    26.         free(*p);  
    27.         *p = pt;  
    28.     }  
    29.     else  
    30.     {  
    31.         ret = 0;  
    32.     }  
    33.       
    34.     return ret;  
    35. }  
    36.   
    37. int main()  
    38. {  
    39.     char* p = (char*)malloc(5);  
    40.       
    41.     printf("%0X\n", p);  
    42.       
    43.     if( reset(&p, 5, 3) )  
    44.     {  
    45.         printf("%0X\n", p);  
    46.     }  
    47.       
    48.     return 0;  
    49. }  

                 (2)函数中传递指针数组的时候,实参(指针数组)要退化成形参(二级指针)。
                 (3)定义一个指针指向指针数组的元素的时候,要使用二级指针。
          c.指针数组:char* p[4]={"afje","bab","ewrw"};  这是一个指针数组,数组中有4个char*型的指针,分别保存的是"afje"、"bab"、"ewrw"3个字符串的地址。p是数组首元素的地址即保存"afje"字符串char*指针的地址。
    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <stdlib.h>  
    3. #include <stdbool.h>  
    4.   
    5. int main(int argc, char *argv[])   
    6. {     
    7.     char* p[4]={"afje","bab","ewrw"};  
    8.     char* *d=p;   
    9.     printf("%s\n",*(p+1));    
    10.     printf("%s\n",*(d+1));  //d  &p[0] p[0]是"afje"的地址,所以&p[0]是保存"afje"字符串的char*指针的地址      
    11.     return 0;  
    12. }  

           d.子函数malloc,主函数free,这是可以的(有两种办法,第一种是利用return 把malloc的地址返回。第二种是利用二级指针,传递一个指针的地址,然后把malloc的地址保存出来)。记住不管函数参数是,指针还是数组, 当改变了指针的指向的时候,就会出问题,因为子函数中的指针就跟主函数的指针不一样了,他只是一个复制品,但可以改变指针指向的内容。这个知识点可以看<在某培训机构的听课笔记>这篇文章。

    13.数组作为函数参数:数组作为函数的实参的时候,往往会退化成数组元素类型的指针。如:int a[5],会退化成int*   ;指针数组会退化成二级指针;二维数组会退化成一维数组指针;三维数组会退化成二维数组指针(三维数组的这个是我猜得,如果说错了,希望大家帮我指出来,谢谢)。如图:

    二维数组作为实参的例子:

    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <stdlib.h>  
    3. #include <stdbool.h>  
    4.   
    5. int fun(int (*b)[3])  //此时的b为  &a[0]   
    6. {  
    7.     printf("%d\n",*(*(b+1)+0));  
    8.     printf("%d\n",b[2][2]);// b[2][2] 就是  (*(*(b+2)+2))  
    9.     printf("%d\n",*(b[1]+2));  
    10. }  
    11.   
    12. int main(int argc, char *argv[])   
    13. {  
    14.     int a[3][3]={1,2,3,4,5,6,7,8,9};  
    15.      fun(a);//与下句话等价  
    16.      fun(&a[0]);      
    17.     return 0;  
    18. }  

           数组当作实参的时候,会退化成指针。指针当做实参的时候,就是单纯的拷贝了!

    14.函数指针与指针函数:
          a.对于函数名来说,它是函数的入口,其实函数的入口就是一个地址,这个函数名也就是这个地址。这一点用汇编语言的思想很容易理解。下面一段代码说明函数名其实就是一个地址,代码如下:

    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <stdlib.h>  
    3. #include <stdbool.h>  
    4.   
    5. void abc()  
    6. {  
    7.     printf("hello fun\n");  
    8. }  
    9. int main(int argc, char *argv[])   
    10. {  
    11.     void (*d)();  
    12.     void (*p)();  
    13.     p = abc;  
    14.     abc();  
    15.     printf("%p\n",abc);  
    16.     printf("%p\n",&abc);//函数abc的地址0x40138c  
    17.     p();  
    18.     (*p)();       
    19.     d = ((unsigned int*)0x40138c);  //其实就算d= 0x40138c这么给赋值也没问题   
    20.     d();  
    21.     return 0;  
    22. }     
    23.    

    可见函数名就是一个地址,所以函数名abc与&abc没有区别,所以p和*p也没有区别。

        b.我觉得函数指针最重要的是它的应用环境,如回调函数(其实就是利用函数指针,把函数当作参数进行传递)代码如下,还有中断处理函数(同理)详细见<

    ok6410学习笔记(16.按键中断控制led)>中的 中断注册函数,request_irq。还有就是函数指针数组,第一次见到函数指针数组是在zigbee协议栈中。

    回调函数原理代码:

    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2.   
    3. typedef int(*FUNCTION)(int);  
    4.   
    5. int g(int n, FUNCTION f)  
    6. {  
    7.     int i = 0;  
    8.     int ret = 0;  
    9.       
    10.     for(i=1; i<=n; i++)  
    11.     {  
    12.         ret += i*f(i);  
    13.     }  
    14.       
    15.     return ret;  
    16. }  
    17.   
    18. int f1(int x)  
    19. {  
    20.     return x + 1;  
    21. }  
    22.   
    23. int f2(int x)  
    24. {  
    25.     return 2*x - 1;  
    26. }  
    27.   
    28. int f3(int x)  
    29. {  
    30.     return -x;  
    31. }  
    32.   
    33. int main()  
    34. {  
    35.     printf("x * f1(x): %d\n", g(3, f1));  
    36.     printf("x * f2(x): %d\n", g(3, &f2));  
    37.     printf("x * f3(x): %d\n", g(3, f3));  
    38. }  

    注意:可以使用函数名f2,函数名取地址&f2都可以,但是不能有括号。

           c.所谓指针函数其实真的没什么好说的,就是一个返回值为指针的函数而已。

    15.赋值指针的阅读:

           a.char* (*p[3])(char* d); 这是定义一个函数指针数组,一个数组,数组元素都是指针,这个指针是指向函数的,什么样的函数参数为char*  返回值为char*的函数。

    分析过程:char (*p)[3] 这是一个数组指针、char* p[3] 这是一个指针数组  char* 是数组元素类型、char* p(char* d) 这个是一个函数返回值类型是char* 、char (*p)(char* d)这个是一个 函数指针。可见char* (*p[3])(char* d)是一个数组  数组中元素类型是 指向函数的指针,char* (* )(char* d) 这是函数指针类型,char* (* )(char* d) p[3] 函数指针数组 这个不好看 就放里面了。(PS:这个看看就好了~~~当娱乐吧)

          b.函数指针数组的指针:char* (*(*pf)[3])(char* p) //这个就看看吧  我觉得意义也不大 因为这个逻辑要是一直下去 就递归循环了。

    分析过程:char* (* )(char *p) 函数指针类型,char* (*)(char *p) (*p)[3]  函数指针 数组指针  也不好看 就放里面了。

     

    本节知识点:

    1.栈的知识(我觉得栈是本节很头疼的一个问题):
        对于栈的问题,首先我们通过几个不同的角度来看(因为思维有些小乱所以我们通过分总的形式进行阐述):
        a.sp堆栈指针,相信学过51单片机,学过arm裸机的人都知道这个堆栈指针。我们现在从51单片机的角度来看这个堆栈指针寄存器。这个堆栈指针的目的是什么?是用来保护现场(子函数的调用)和保护断点(中断的处理)的,所以在处理中断前,调用子函数前,都应该把现场和返回地址压入栈中。而且堆栈还会用于一些临时数据的存放。51中的sp指针再单片机复位的时候初值为0x07。常常我们会把这个sp指针指向0x30处,因为0x30~0x7f是用户RAM区(专门为堆栈准备的存储区)。然后要引入一个栈顶栈底的概念。栈操作的一段叫栈顶(这里是sp指针移动的那个位置,sp也叫栈顶指针),sp指针被赋初值的那个地址叫栈底(这里是0x30是栈底,因为栈顶永远会只在0x30栈底的一侧进行移动,不会在两层移动)。而且51单片机的sp是向上增长的,叫做向上增长型堆栈(栈顶指针sp向高地址处进行增长)。因为PUSH压栈操作,是sp指针先加1(指向的地址就增大一个),再压入一个字节,POP弹出操作,先弹出一个字节,sp再减1(指向的地址就减少一个)。看PUSH和POP的过程,可见是一个满堆栈(满堆栈的介绍在后面)。小结一下:51的堆栈是一个向上增长型的满堆栈
        b.对于arm来说,大量的分析过程都与上面相同。只是堆栈不再仅仅局限于处理中断了,而是处理异常。arm的堆栈有四种增长方式(具体见d)注意:在arm写裸机的时候,那个ldr sp, =8*1024   其实是在初始化栈底。sp是栈顶指针,当没有使用堆栈的时候,栈顶指针是指向栈底的。当数据来的时候,每次都是从栈顶进入的(因为栈的操作入口在栈顶),然后sp栈顶指针指向栈顶,慢慢远离栈底。说这些是想好好理解下什么是栈顶,什么是栈底。
        c.对于8086来说,它的栈的生长方向也是从高地址到低地址,每次栈操作都是以字(两个字节)为单位的。压栈的时候,sp先减2,出栈的时候,sp再加2。可见8086的堆栈是一个向下增长型的满堆栈
        d.总结下:
                        (1).当堆栈指针sp指向,最后一个压入堆栈的数据的时候,叫满堆栈。
                        (2).当堆栈指针sp指向,下一个要放入数据的空位置的时候,叫空堆栈。如下图:
                      (3).当堆栈由低地址向高地址生长的时候,叫向上生长型堆栈即递增堆栈。
                      (4).当堆栈由高地址向低地址生长的时候,叫向下生长型堆栈即递减堆栈。如图:
                     (5). 所以说arm堆栈支持四种增长方式:满递减栈(常用的ARM,Thumb c/c++编译器都使用这个方式,也就是说如果你的程序中不是纯汇编写的,有c语言就得使用这种堆栈形式)、满递增栈、空递减栈、空递增栈。这四种方式分别有各自的压栈指令,出栈指针,如下图:  
               e.对于裸机驱动程序(51、ARM)没有操作系统的,编译器(keil、arm-linux-gcc等),会给sp指针寄存器一个地址。然后一切的函数调用,中断处理,这些需要的现场保护啊,数据啊都压入这个sp指向的栈空间。(arm的.s文件是自己写的,sp是自己指定的,编译器会根据这个sp寄存器的值进行压栈和出栈,但是压栈和出栈的规则是满递减栈的规则,因为arm-linux-gcc是这个方式的,所以在汇编调用c函数的时候,汇编代码必须使用满递减栈的那套压栈出栈指令)。这种没有操作系统的裸机驱动程序,只有一个栈空间,就是sp指针指向的那个栈空间。
               f.对于在操作系统上面的程序,里面涉及内存管理、虚拟内存、编译原理的问题。首先说不管是linux还是windows的进程的内存空间都是独立的,linux是前3G,windows是4G,这都是虚拟内存的功劳。那编译器给程序分配的栈空间,在程序运行时也是独立的。每一个进程中的栈空间,应该都是在使用sp指针(但是在进程切换的过程中,sp指针是怎么切换的我就不清楚了,这个应该去看看操作系统原理类的书)Ps:对于x86的32位机来说不再是sp和bp指针了,而是esp和ebp两个指针。有人说程序中的栈是怎么生长的,是由编译器决定的,有人说是由操作系统决定的!!!我觉得都不对,应该是由硬件决定的,因为cpu已经决定了sp指针的压栈出栈方式。只要你操作系统在进程运行的过程中,使用的这个栈是sp栈指针指向的(即使用了sp指针),而不是自己定义的一块内存(与sp指针无关的话)  Ps:实际中进程使用的是esp和ebp两个指针,这里仅仅用sp是想说明那个意思而已!  操作系统使用的栈空间就必须符合sp指针的压栈和出栈方式,也就是遵循了cpu决定的栈的生长方式。编译器要想编译出能在这个操作系统平台上使用的程序,也必须要遵守这个规则,所以来看这个栈的生长方式是由cpu决定的。这也是为什么我用那么长的篇幅来解释sp指针是怎么工作的原因!
              g.要记住,由于操作系统有虚拟内存这个东东,所以不要再纠结编译器分配的空间在操作系统中,进程执行的时候空间是怎么用的了。编译器分配的是什么地址,进程中使用这个变量的虚拟地址就是什么!是对应的。当然有的时候,编译器也会耍些小聪明。不同编译器对栈空间上的变量分配的地址可能不一样,但方向一定是一样的(因为这个方向是cpu决定,编译器是无权决定的,是sp指针压栈的方向),如图:
    图1和图2的共同点是:都是从高地址处到低地址处,因为sp指针把A、B、C变量压入栈的方向就是从高到低地址的。这个是什么编译器都不会变的。
    图1和图2的不同点是:图2进行了编译器的小聪明,它在给A,B,C开辟空间的时候,不是连续开辟的空间,有空闲(其实依然进行了压栈操作只是压入的是0或者是ff),这样变量直接有间隙就避免了,数组越界,内存越界造成的问题。切记在获取A、B、C变量的时候,不是通过sp指针,而是通过变量的地址获得的啊,sp只负责把他们压入栈中,即给他们分配内存。
                   h.说了那么多栈的原理,现在我们说说栈在函数中究竟起到什么作用:保存活动记录!!!如图:
    注意:活动记录是什么上面的这个图已经说的很清楚了,如果再调用函数,这个活动记录会变成什么样呢?会在这个活动记录后面继续添加活动记录(这个活动记录是子函数的活动记录),增加栈空间,当子函数结束后,子函数的活动记录清除,栈空间继续回到上图状态!
    Ps:活动记录如下:
             i.函数的调用行为
    函数的调用行为中有一个很重要的东西,叫做调用约定。调用约定包含两个约定。
    第一个是:参数的传递顺序(这个不是固定的,是在编译器中约定好的),从左到右依次入栈:__stdcall、__cdecl、__thiscall   (这些指令,直接写在函数名的前面就可以,但是跟编译器有点关系,可能会有的编译器不支持会报错)
                                                                                                                           从右到左依次入栈:__pascal、__fastcall
    第二个是:堆栈的清理(这段代码也是编译器自己添加上的):调用者清理
                                                                                                        被调用者函数返回后清理
    注意:一般我们都在同一个编译器下编译不会出这个问题。 但是如果是调用动态链接库,恰巧编译动态链接库的编译器跟你的编译器的默认约定不一样,那就惨了!!!或者说如果动态链接库的编写语言跟你的语言都不一样呢?                 
              j.这里要声明一个问题:就是栈的增长方向是固定的,是cpu决定的。但是不代表说你定义的局部变量也一定是先定义的在高地址,后定义的在低地址,局部变量之间都是连续的(这个在上面已经说过了是编译器决定的),还有就是栈的增长方向也决定不了参数的传递顺序(这个是调用约定,通过编译器的手处理的)。下面让我们探索下再dev c++中,局部变量的地址问题。
    1. #include <stdio.h>  
    2.   
    3. void fun()  
    4. {  
    5.     int a;  
    6.     int b;  
    7.     int c;  
    8.     printf("funa  %p\n",&a);  
    9.     printf("funb  %p\n",&b);  
    10.     printf("func  %p\n",&c);  
    11. }  
    12. void main()  
    13. {  
    14.     int a;  
    15.     int b;  
    16.     int c;  
    17.     int d;  
    18.     int e;  
    19.     int f;  
    20.     int p[100];  
    21.       
    22.     printf("a  %p\n",&a);  
    23.     printf("b  %p\n",&b);  
    24.     printf("c  %p\n",&c);  
    25.     printf("d  %p\n",&d);  
    26.     printf("e  %p\n",&e);  
    27.     printf("f  %p\n",&f);  
    28.     printf("p0    %p\n",&p[0]);  
    29.     printf("p1    %p\n",&p[1]);  
    30.     printf("p2    %p\n",&p[2]);  
    31.     printf("p3    %p\n",&p[3]);  
    32.     printf("p4    %p\n",&p[4]);  
    33.                       
    34.     printf("p10    %p\n",&p[10]);  
    35.     printf("p20    %p\n",&p[20]);  
    36.     printf("p30    %p\n",&p[30]);  
    37.     printf("p80    %p\n",&p[80]);  
    38.     printf("p90    %p\n",&p[90]);  
    39.     printf("p100    %p\n",&p[100]);  
    40.                   
    41.       
    42.     fun();  
    43.       
    44. }  
    运行结果如下(不同编译器的运行结果是不一样的):

    通过上面的运行结果,可以分析得出:在同一个函数中,先定义的变量在高地址处,后定义的变量在低地址处,且他们的地址是相连的中间没有空隙。定义的数组是下标大的在高地址处,下标小的在低地址处(由此可以推断出malloc开辟出的推空间,也应该是下标大的在高地址处,下标小的在低地址处)。子函数中的变量,跟父函数中的变量的地址之间有很大的一块空间,这块空间应该是两个函数的其他活动记录,且父函数中变量在高地址处,子函数中的变量在低地址处。
                 k.下面来一个栈空间数组越界的问题,让大家理解一下,越界的危害,代码如下(猜猜输出结构):
    1. #include<stdio.h>  
    2. /*这是一个死循环*/  
    3. /*这里面有数组越界的问题*/  
    4. /*有栈空间分配的问题*/  
    5. int main()  
    6. {  
    7.       
    8.     int i;  
    9. //  int c;  
    10.     int a[5];  
    11.     int c;  
    12.     printf("i %p,a[5] %p\n",&i,&a[5]); //观察栈空间是怎么分配的  这跟编译器有关系的  
    13.     printf("c %p,a[0] %p\n",&c,&a[0]);  
    14.     for(i=0;i<=5;i++)  
    15.     {  
    16.         a[i]=-i;  
    17.         printf("%d,%d",a[i],i);  
    18.     }  
    19.     return 1;  
    20. }  
    注意:不同编译器可能结果不一样,比如说vs2008就不会死循环,那是因为vs2008耍了我上面说的那个小聪明(就是局部变量和数组直接有间隙不是相连的,就避开了越界问题,但是如果越界多了也不行),建议在vc6和dev c++中编译看结果。
                l.最后说说数据结构中的栈,其实数据结构中的栈就是一个线性表,且这个线性表只有一个入口和出口叫做栈顶,还是LIFO(后进先出的)结构而已。
                对栈的总结:之前就说过了那么多种栈的细节,现在在宏观的角度来看,其实栈就是一种线性的后进先出的结构,只是不同场合用处不同而已!
    2.堆空间:堆空间弥补了栈空间在函数返回后,内存就不能使用的缺陷。是需要程序员自行跟操作系统申请的。
    3.静态存储区:程序在编译期,静态存储区的大小就确定了   
    4.对于程序中的内存分布:请看这篇文章<c语言中的内存布局>
    5.对于内存对齐的问题:请看这篇文章<C语言深度解剖读书笔记(3.结构体中内存对齐问题)>
    6.使用内存的好习惯:
        a.定义指针变量的时候,最好是初始化为NULL,用完指针后,最好也赋值为NULL。
        b.在函数中使用指针尽可能的,去检测指针的有效性
        c.malloc分配的时候,注意判断是否分配内存成功。
        d.malloc后记得free,防止内存泄漏!
        e.free(p)后应该p=NULL
        f.不要进行多次free
        g.不要使用free后的指针
        h.牢记数组的长度,防止数组越界
    7.内存常见的六个问题:
        a.野指针问题 :一个指针没有指向一个合法的地址
        b.为指针分配的内存太小
        c.内存分配成功,但忘记初始化,memset的妙用
        e.内存越界
        f.内存泄漏
        g.内存已经被释放 还仍然在使用(栈返回值问题)
     
     
     
     

     对于本节的函数内容其实就没什么难点了,但是对于函数这节又涉及到了顺序点的问题,我觉得可以还是忽略吧。

    本节知识点:

    1.函数中的顺序点:f(k,k++);  这样的问题大多跟编译器有关,不要去刻意追求。  这里给下顺序点的定义:顺序点是执行过程中修改变量值的最后时刻。在程序到达顺序点的时候,之前所做的一切操作都必须反应到后续的访问中。

    2.函数参数:函数的参数是存储在这个函数的栈上面的(对于栈可以看上篇文章<内存管理的艺术>),是实参的拷贝。

    3.函数的可变参数:

          a.对于可变参数要包含starg.h头文件。需要va_list变量,va_start函数,va_arg函数,va_end函数。对于其他函数没什么可说的,只有va_arg函数记得一定是按顺序的接收。这里有一个可变参数使用的小例子,代码如下:

    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <stdarg.h>  
    3.   
    4. float average(char c,int n, ...)  
    5. {  
    6.     va_list args;  
    7.     int i = 0;  
    8.     float sum = 0;  
    9.       
    10.     va_start(args, n);  
    11.       
    12.     for(i=0; i<n; i++)  
    13.     {  
    14.         sum += va_arg(args, int);  
    15.     }  
    16.       
    17.     va_end(args);  
    18.     printf("%c\n",c);  
    19.       
    20.     return sum / n;  
    21. }  
    22.   
    23. int main()  
    24. {  
    25.     char c = 'b';  
    26.     printf("%f\n", average(c,5, 1, 2, 3, 4, 5));  
    27.     printf("%f\n", average(c,4, 1, 2, 3, 4));  
    28.       
    29.     return 0;  
    30. }  

            b.可变参数的缺点:

                     (1).必须要从头到尾按照顺序逐个访问。

                     (2).参数列表中至少要存在一个确定的命名参数。

                     (3).可变参数宏无法判断实际存在的参数的数量。

                     (4).可变参数宏无法判断参数的实际类型。

                     (5).如果函数中想调用除了可变参数以外的参数,一定要放在可变参数前面。

    注意:va_arg中如果指定了错误的类型,那么结果是不可预期的。

    Ps:可变参数就说到这里,可变参数最经典的应用就是printf,等分析printf实现的时候,再好好写写。
    4.函数与宏的比较:

    注意:宏有一个函数不可取替的功能,宏的参数可以是类型名,这个是函数做不到的!代码如下:

    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2. #include <malloc.h>  
    3.   
    4. #define MALLOC(type, n) (type*)malloc(n * sizeof(type))  
    5.   
    6. int main()  
    7. {  
    8.     int* p = MALLOC(int, 5);  
    9.       
    10.     int i = 0;  
    11.       
    12.     for(i=0; i<5; i++)  
    13.     {  
    14.         p[i] = i + 1;  
    15.           
    16.         printf("%d\n", p[i]);  
    17.     }  
    18.       
    19.     free(p);  
    20.       
    21.     return 0;  
    22. }  

    5.函数调用中的活动记录问题:包含参数入栈、调用约定等问题。见上篇文章<内存管理的艺术>

    6.递归函数:递归函数有两个组成部分,一是递归点(以不同参数调用自身),另一个是出口(不再递归的终止条件)。

          对于递归函数要有一下几点注意:

           a.一定要有一个清晰的出口,不然递归就无限了。

           b.尽量不要进行太多层次的递归,因为递归是在不断调用函数,要不断的使用栈空间的,很容易造成栈空间溢出的,然后程序就会崩溃的。比如说:对一个已经排好序的结构进行快速排序(因为快排需要使用递归,且对排好顺序的结构排序是最坏情况,递归层数最多),就很容易造成栈空间溢出。一般不同的编译器分配的栈空间大小是不一样的,所以允许递归的层数也是不一样的!

            c.利用递归函数,实现不利用参数的strlen函数。代码如下:

    [cpp] view plaincopy
     
     
    1. /*这是自己实现  strlen*/  
    2. /*  
    3. #include <stdio.h> 
    4. #include <stdlib.h> 
    5. #include <assert.h> 
    6.  
    7. int my_strlen(const char *str) 
    8.     int num=0; 
    9.     assert(NULL!=str); 
    10.     while(*str++) 
    11.     { 
    12.         num++; 
    13.     } 
    14.     return num; 
    15. int main(int argc, char *argv[]) 
    16.     char *a="hello world"; 
    17.     printf("%d\n",my_strlen(a)); 
    18.     return 0; 
    19. }*/  
    20.   
    21. /*这是不用变量 实现strlen  使用递归*/  
    22. #include <stdio.h>  
    23. #include <stdlib.h>  
    24. #include <assert.h>  
    25.   
    26. int my_strlen(const char *str)  
    27. {  
    28.     assert(NULL!=str);  
    29.     return ('\0'!=*str)?(1+my_strlen(str+1)):0; //这里之所以 是加1 不是++ 我是担心顺序点的问题   
    30. }  
    31.   
    32. int main(int argc, char *argv[])  
    33. {  
    34.     char *a="hello world";  
    35.     printf("%d\n",my_strlen(a));  
    36.     return 0;  
    37. }  

    7.使用函数时应该注意的好习惯:

       a.如果函数参数是指针,且仅作为输入参数用的时候,应该加上const防止指针在函数体内被以外改变,如:

    [cpp] view plaincopy
     
     
    1. void str_copy(char *strDestination,const char *strSource);  

       b.在函数的入口处,应尽可能使用assert宏对指针进行有效性检查,函数参数的有效性检查是十分必要的。不用assert也行,if(NULL == p)也可以。

       c.函数不能返回指向栈内存的指针

       d.函数不仅仅要对输入的参数,进行有效性的检查 。还要对通过其他途径进入函数体的数据进行有效性的检查 ,如全局变量,文件句柄等。

       e.不要在函数中使用全局变量,尽量让函数从意义上是一个独立的模块

       f.尽量避免编写带有记忆性的函数。函数的规模要小,控制在80行。函数的参数不要太多,控制在4个以内,过多就使用结构体。

       g.函数名与返回值类型在语言上不可以冲突,这里有一个经典的例子getchar,getchar的返回值是int型,会隐藏这么一个问题:

    [cpp] view plaincopy
     
     
    1.  char c;  
    2.  c=getchar();  
    3.  if(XXX==c)  
    4.  {  
    5. /*code*/  
    6.  }  

          如果XXX的值不在char的范围之内, 那c中存储的就是XXX的低8位 ,if就永远不会成立。但是getchar当然不会惹这个祸了,因为getchar获得的值是从键盘中的输入的,是满足ASCII码的范围的,ASCII码是从0~127的,是在char的范围里面的,就算是用char去接getchar的值也不会有问题,getchar还是相对安全的。可是对于fgetc和fgetchar就没这么幸运了,他们的返回值类型同样是int,如果你还用char去接收,那文件中的一些大于127的字符,就会造成越界了,然后导致你从文件中接收的数据错误。这里面就有隐藏的危险了!!!对于字符越界问题可以看看这篇文章<c语言深度解剖读书笔记(1.关键字的秘密)>
    8.陈正冲老师还有一个第七章是讲文件的我觉得总结不多,就写在这里了:

       a.每个头文件和源文件的头部 ,都应该包含文件的说明和修改记录 。

       b.需要对外公开的常量放在头文件中 ,不需要对外公开的常量放在定义文件的头部。

    9.最终的胜利,进军c++(唐老师的最后一课,讲了些c++的知识,总结如下):

        a.类与对象:

        b.c++中类有三种访问权限:

               (1).public  类外部可以自由访问

               (2).protected   类自身和子类中可以访问

               (3).private     类自身中可以访问

    小例子:

    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2.   
    3. struct Student  
    4. {  
    5. protected:  
    6.     const char* name;  
    7.     int number;  
    8. public:  
    9.     void set(const char* n, int i)  
    10.     {  
    11.         name = n;  
    12.         number = i;  
    13.     }  
    14.       
    15.     void info()  
    16.     {  
    17.         printf("Name = %s, Number = %d\n", name, number);  
    18.     }  
    19. };  
    20.   
    21. int main()  
    22. {  
    23.     Student s;  
    24.       
    25.     s.set("Delphi", 100);  
    26.     s.info();  
    27.       
    28.     return 0;  
    29. }  

    注意:上面这段代码要在c++的编译器中进行编译,在gcc中会报错的,因为c标准中是不允许struct中有函数的。
            c.继承的使用,如图:

    小例子:

    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2.   
    3. struct Student  
    4. {  
    5. protected:  
    6.     const char* name;  
    7.     int number;  
    8. public:  
    9.     void set(const char* n, int i)  
    10.     {  
    11.         name = n;  
    12.         number = i;  
    13.     }  
    14.       
    15.     void info()  
    16.     {  
    17.         printf("Name = %s, Number = %d\n", name, number);  
    18.     }  
    19. };  
    20.   
    21. class Master : public Student  
    22. {  
    23. protected:  
    24.     const char* domain;  
    25. public:  
    26.     void setDomain(const char* d)  
    27.     {  
    28.         domain = d;  
    29.     }  
    30.       
    31.     const char* getDomain()  
    32.     {  
    33.         return domain;  
    34.     }  
    35. };  
    36.   
    37. int main()  
    38. {  
    39.     Master s;  
    40.       
    41.     s.set("Delphi", 100);  
    42.     s.setDomain("Software");  
    43.     s.info();  
    44.       
    45.     printf("Domain = %s\n", s.getDomain());  
    46.       
    47.     return 0;  
    48. }  

    Ps:以上6篇文章终于更新完了,是我对陈正冲老师的<c语言深度解剖>一书和国嵌唐老师c语言课程的一些总结和理解,针对c语言,后面的一点c++仅仅是做个笔记而已,望大牛莫喷~~~

    本节知识点:

    1.可以利用这个宏 #define OFFSET(type,number)  (int)(&(((type*)0)->number))  求出结构体中成员的偏移量
    2.对于assert的使用是:
    可以这样
    [cpp] view plaincopy
     
     
    1. assert(dst && src);   
    也可以这样
    [cpp] view plaincopy
     
     
    1. assert((NULL != dst) && (NULL != src));   
    上面两种方式都行!
    3.给一个考指针运算的面试题吧:
    [cpp] view plaincopy
     
     
    1. #include <stdio.h>  
    2.   
    3. void main()  
    4. {  
    5.     int TestArray[5][5] = { {11,12,13,14,15},  
    6.                             {16,17,18,19,20},  
    7.                             {21,22,23,24,25},  
    8.                             {26,27,28,29,30},  
    9.                             {31,32,33,34,35}  
    10.                           };  
    11.     int* p1 = (int*)(&TestArray + 1);  
    12.     int* p2 = (int*)(*(TestArray + 1) + 6);  
    13.   
    14.     printf("Result: %d; %d; %d; %d; %d\n", *(*TestArray), *(*(TestArray + 1)),   
    15.                                            *(*(TestArray + 3) + 3), p1[-8],   
    16.                                            p2[4]);  
    17. }  
    自己算算吧,记住一个前提就好,就是在对指针进行运算的时候一定要先弄清这个指针的类型!
    4.看看下面的代码,感受下安全编程的重要性:
    [cpp] view plaincopy
     
     
    1. #include<stdio.h>   
    2.   
    3. int main(int argc, char *argv[])   
    4. {   
    5.     int flag = 0;   
    6.   
    7.     char passwd[10];   
    8.   
    9.     memset(passwd,0,sizeof(passwd));   
    10.   
    11.     strcpy(passwd, argv[1]);   
    12.   
    13.     if(0 == strcmp("LinuxGeek", passwd))   
    14.     {   
    15.         flag = 1;   
    16.     }   
    17.   
    18.     if( flag )   
    19.     {   
    20.         printf("\n Password cracked \n");   
    21.   
    22.     }   
    23.     else   
    24.     {   
    25.         printf("\n Incorrect passwd \n");   
    26.     }   
    27.   
    28.     return 0;   
    29.   
    30. }  
    看看上面的代码有没有什么问题?如果把命令行输入的文字当作密码的话,会不会存在问题?答案是会,因为这里面有两个知识点:1.是数组越界   2.是strcpy安全性的问题。
    首先如果我输入11个字符且最后一个字符是大于0的话,就惨了,strcpy是要copy到'/0'的。他会一直把这11个字符都copy到passwd数组中,此时数组越界了,最后一个字符就把flag标志位个赋值了,if条件就满足了,密码就被破解了!
    所以应该使用安全性更高的strncpy:
    [cpp] view plaincopy
     
     
    1. strncpy(passwd,argv[1],9);  

    最近对c语言的总结学习可以告一段落了!觉得这种边学边思考边总结的方式,还不错,还是有一定的进步的!但是对于日后的c语言学习还远远没有停止。所以写了这篇文章来督促自己对c语言的学习,告诉自己还有很多不错的书没有去读。过一段时间,再回头看看。

            1.对于c语言描述的数据结构的学习。

            2.林锐老师的<高质量程序设计指南>,听说他的<大学十年>也很不错,有时间应该读一读。

            3.<c和指针>     <c陷阱与缺陷>    <c专家编程>    <c++沉思录>

            4.仔细研读 <c primer plus>这本书,这本书中有很多细节,很多标准(c99标准)值得学习,应该好好看看!

            5.还有就是找一本介绍c 表库函数的书(像字典一样),看看c库函数都有什么,c库中有多少头文件等。

            6.还有就是一些当作补充的书籍:<C语言的科学和艺术>    <你必须知道的495个c语言问题>    <c语言进阶:重点、难点与疑点解析>  <攻破C语言笔试与机试难点V0.3>

            7.最后在回头看看,带我最初接触c语言的,谭浩强的<c语言程序设计>

  • 相关阅读:
    使用JdbcTemplate访问数据库
    解决为什么每次打开Eclipse新的workspace需要更新nexus-maven-repository-index问题
    java内存设置
    Eclipse如何解决启动慢
    eclipse的包的加减号展开方式
    maven总结5
    maven总结4
    maven总结3
    maven总结2
    maven总结1
  • 原文地址:https://www.cnblogs.com/lijunlin/p/4434663.html
Copyright © 2011-2022 走看看