浅谈c语言的知识体系

来源:互联网 发布:淘宝上下架时间设置 编辑:程序博客网 时间:2024/05/17 09:21

    学习完c语言一直想写一个知识体系的东西,总感觉自己能力不够。终于鼓起勇气,来和大家聊聊我学习c语言的感悟。当然,要在这几千字中将c语言的所有知识点都遍历一遍是不现实的,本人也不具备这样的能力。我会在下面写到我学习c语言的时候感觉重要的东西。

     一.选择结构程序设计以及循环结构设计

    1.if语句的使用

    1).bool变量与“零值”比较

    bool i = FALSE;

    那么下面初始化什么比较好?

    if( i == 0);  if(i == 1)  // 1    if( i == TURE); if( i == FALSE);  //2    if(i); if(!i);  //3

    注意:大家知道if语句是靠其后面括号里表达式的值来进行分支跳转。表达式为真,则执行if语句后面紧跟的代码;否则不执行。那显然,第三组写法很好。不会引起误会,也不会因为TRUE,FALSE的不同定义值而出错。

    当然,还有float变量和“零值”比较。指针变量与“零值”比较。大家可以自己研究。我在这里就不再讲述了。

    2.while循环使用

    1).讲述一个例子来剖析一下吧

    

           #include<stdio.h>        int main()    {    int a = 1;    int b = 2;    int c = 2;    int t;    while (a < b < c)    {    t = a;    a = b;    b = t;    c--;    }    printf("%d %d", a, c);    system("pause");    return 0;    }

    分析一下结果会是什么?a的值为1,c的值为0;while循环也关注的是括号里面的值是否为真,那么a<b<c怎么分析,首先第一次进行判断,a为1,b为2,a<b为真,那么这个表达式的值为真(真为1,假为0),那么1<c,进入循环,同理进行第二次循环,最后算出结果。while循环需要注意的就是括号中的表达式中的值,如果写成while(1),那就是无限循环,读者可以尝试在while(1)中动态开辟内存,试试我说的话对不对。当然前提也可以使用break语句跳出或者return语句跳出循环。

    3.for循环的使用

    同样用一个例子来分析一下

            #include<stdio.h>        int main()    {    int i = 0;    int arr[10] = {0};    for (i = 0; i < 10; i++)    {    arr[i] = i;    }    for (i = 0; i < 10; i++)    {    printf("%d\n", arr[i]);    }        system("pause");        return 0;    }

    当然这个程序是非常简单,但足以说明问题。当i = 10的时候会跳出循环。那么就很简单明了了。当然for循环也可以用while循环改写。

     二.数组

    数组是储存同一数据类型的一组有序的数据的集合。

    数组中需要注意的点:

    1.数组用循环语句赋值。同样是上面的for循环,可以自己试试将条件 i<10改为 i<11试试。程序会无条件的奔溃。因为你操作越界了,访问了不属于你自己的内存。

    2.数组下标的操作。记住一点的就是,数组的下标是从0开始,并不是1!!!

    3.数组和逗号表达式一起迷惑你。

            #include<stdio.h>        int main()    {    int a[3][2] = { (0, 1), (2, 3), (4, 5) };    int *p = a[0];    printf("%d", p[0]);    system("pause");    return 0;    }

    这个时候的p[0]等于1;为什么,不是应该是0?这就考到了逗号表达式,逗号表达式最后一个值才是整个表达式的值,那么上面的代码就变成了 int a[3][2] = {1,3,5};如果这样说,你是不是就会做对呢?

    4.二维数组在内存中的存储

    假如二维数组

    

      int arr[3][3] = {0};

    二维数组在内存中存储是线性的。我们可以用一维数组的方式来理解二维数组,上述代码代表的二维数组有三个元素,arr[0],arr[1],arr[2],同时每一个元素分别有三个元素,arr[0][0],arr[0][1],arr[0][2]...,那么可以这样理解arr[0],arr[1],arr[2],就是一维数组的数组名,只不过这三个一维数组是有联系的并且在内存中是连续开辟的。

    同理我们可以推到三维数组直到n维数组。只要你有时间并且有毅力和决心(意义不大)。哈哈!!

    5.字符数组

    字符数组的赋值方式,为什么要提字符数组的赋值方式,本人认为这和字符串的操作息息相关,看看两种不同的赋值方式。

    1).

    char arr[]={'i',' ','a','m',' ','h','a','c','k','e','r'};

    可以预见的是,没有‘\0’;那么这个字符数组所储存的字符串是不会使用一系列的字符串操作函数。如果对常见的字符串操作函数不太了解,那么访问下面的链接,http://10799170.blog.51cto.com/10789170/1715083 帮助你了解常见的字符串操作函数的实现与注意的事项。言归正传,第二种赋值方式。

    2).

    char arr[] = "i am hacker";

    这种赋值方式,是最常见也是经的起考验,建议大家就这样使用字符数组的赋值。这种赋值方式不会丢掉“\0”。那么,问题来了,可不可以这样赋值?

    char arr[] = {"i am hacker"};    char arr[] { = "i am hacker"};

    答案是肯定的,有兴趣的读者可以在《c语言深度剖析》第二章第6小节看到。

    6.总结一下:

    1).自己给数组在内存中赋值的时候,千万注意内存越界的问题。

    2).数组的下标操作。

    3).字符数组的“\0”。

    4).二维数组的赋值方式,理解方式以及使用方式。

     三.指针

    指针是地址。我在初学指针的时候,总是分不清楚,指针和指针变量和它指向的地址中的内容的区别,所幸,我在钻研了一下午理解了指针这个东西。得益于一本叫做《c和指针》的书,里面讲述的清楚易懂。

    1.一级指针

    定义一个一级指针

        int *p = NULL;

    首先要说的是,指针就是地址,指针变量就是用来保存指针指向的那块空间的地址,“*”就是打开这块内存的钥匙。其次,指针的定义和赋值是两码事,不可以混为一谈。最后我也曾经写过自己对指针,数组指针的理解。读者有兴趣可以打开下面的链接

    http://10799170.blog.51cto.com/10789170/1717081 

    http://10799170.blog.51cto.com/10789170/1715728 

    1).指针的笑话(在vc6.0中实现)

    

        int *p = (int *)ox12ff7c;  //这个地址必须是内存开始给变量分配地址的第一个,要不是                                   //不会有作用!           *p = NULL;           p = NULL;

    读者可以自己试试结果是什么。

    2).谈谈指针的安全

    指针是可以指向内存中的任何地址的,但是在一个程序中,总是只有自己的栈帧,或者储存在静态去和堆上的内存。如果一不小心访问到不属于自己的内存,并改掉其中的值。对于一个大型程序来说,调试都是一个非常庞大的工作。所以,在使用指针的时候千万要小心。杜绝野指针!!

    3).const和assert修饰指针

    不需要修改指针指向的值,请必须加上const修饰

    

    const int *p;    int const *p;    int * const p;    const int * const p;

    读者可以尝试着分析上面的语句代表着什么。

    assert只在debug版本生效,在release版本没有意义。在debug版本可以帮助我们检验指针的有效性。大公司面试的时候很注重这一定。

    4).指针与字符串

    指针和字符串的代表就是实现字符串反转,应该是入门级的经典。一起来实现字符串反转吧。

        void reverse(int *left, int *right)        {            assert(left);            assert(right);            while(left<right)            {                int tmp = *left;                *left = *right;                *right = tmp;                left++;                right--;            }        }

    请读者不要小瞧这个字符串翻转函数,不信你可以看看《剑指offer》中有一道左旋字符串的面试题就用到上面这个函数。

    总结一下:

    a.如果是同一个字符串操作,那么就会使用到指针偏移,通常这个时候会和循环打交道并且会和字符串的长度打交道。为什么要和长度打交道,原因很简单,怕越界啊!另一种情况,就是指针变量中所储存的地址比较,例子就是字符串的翻转。

    b.如果是不同字符串操作,通常是通过指针偏移实现的。想想看strcpy,strcmp,strcat等等的实现。

    c.指针的偏移,是偏移那个类型乘以偏移数的,这个还是很重要的!来个例子吧。

    char *p = "abcdef";
    printf("%c\n",*(p+1));

    p+1代表的是p向后偏移p+1*(sizeof(char));p向后偏移了两个字节。如果是p+2呢?等等..只有搞明白其中的道理,指针偏移对你来说就是小菜一碟。

    5).动态开辟内存

    

        #include<stdio.h>        char *getmemory(int *p)    {    p = (int *)malloc(10 * sizeof(int));    if(p != NULL)    {    return p;    }    return NULL;    }        int main()    {    int *p = NULL;    getmemory(p);    //...;    free(p);    p = NULL;    return 0;    }

    a.内存开辟成功你没有?

    b.拿到开辟的内存后,*p是否可以使用这块内存?

    c.free函数有没有将内存释放掉,换句话说,有没有内存泄漏?

    d.如果要改怎么改?

    2.函数指针

    指针函数作为函数的可扩展性应用很广阔。

    1).回调函数

    如果要实现一个冒泡排序既可以排字符串也可以排数字该如何设计函数?

    a.首先要实现一个冒泡排序的主逻辑函数。

    b.实现一个可以比较数字的函数,不管是int,double,float。

    c.实现一个字符串比较函数。

    d.交换函数

    那么问题来了该如何三个函数联系起来。看看代码吧。

    

    void sort(void *base, int len, int width, int(*cmp)(const void*, const void*))        {            assert(base);            int i = 0;            int j = 0;            for (i = 0; i < len; i++)            {                for (j = 0; j < len - 1 - j; j++)                {                    if (cmp((char *)base + width*j, (char *)base + width*(j + 1))>0)                    {                        swap((char *)base + width*j, (char *)base + width*(j + 1), width);                    }                }            }        }   //实现冒牌排序的主逻辑

    void类型是回调函数的一个特点,另一个是(int width)表示类型所占的字节数(大小),最重要的是参数中函数的声明,声明声明为一个函数指针,两个参数,接受两个void类型的数据,那么当需要比较整数的时候只需要将比较整数函数的地址传进来就好了。当然,不得不提的是在比较函数内部需要强制类型转换。读者可以自己实现整个冒泡排序,当然可以加上冒泡排序的优化。上面的代码是没有优化的。如果你还是不能理解,可以看看库函数qsort的实现。

    2)使用函数指针让你直接拿offer

    《剑指offer》里面第14道面试题--调整数组顺序使奇数位于偶数的前面。在这里我们是讨论c语言的体系,所以我就不代码的实现加进来了。读者需要自己去看。

    自己在学习的时候看的书是《c语言深度剖析》和《c和指针》。读者可以自己进一步了解。

     四.内存操作

    说到内存操作,不得不提到栈帧。读者可以参考我原来写过的一片博文 附上url

    http://10799170.blog.51cto.com/10789170/1715186 

     1.我们在这里就主要讨论讨论malloc开辟内存时应该注意的问题。

      1).动态开辟内存后,需要判断内存是否开辟成功。

    如果内存没有开辟成功,我们直接使用会造成访问越界;

      2).在使用完成后必须使用free函数释放掉内存,其原理是将指针与内存切断联系。

    会造成内存泄漏。很危险!

      3).使用完成后指针应赋为NULL;杜绝野指针。

    没有及时将指针赋值为NULL,指针会指向“垃圾”内存!

    2.一个经典的例子

    

        void fun()    {    int tmp = 10;    int *p = (int *)(*(&tmp + 1));    *(p - 1) = 20;    }    int main()    {    int a = 0;    fun();    printf("a = %d\n", a);    return 0;    }

    答案是20。为什么呢?当创建fun函数的栈帧的时候,首先 push ebp,这个ebp中保存着main函数栈底的地址,那么就很好理解了,在fun内部创建局部变量tmp,tmp被保存在ebp这个地址的上面,那么取到tmp的地址之后加1强制类型转换为(int *),解引用之后其实访问到了main函数的栈底,*(p - 1) = 20;之后访问到main函数ebp的上面的四个字节,碰巧的是a的内容在这四个字节中保存,呀!赋值为20。在fun函数的内部居然改掉了a的内容。是不是很神奇。

    c语言的神奇之处也是难点之处就是操作内存。相信上面的例子已经很好的佐证了。读者可以在学习c语言的同时,多关注内存,有利于以后的求职。

    五.函数

    函数这一部分,本来是不愿意拿出来说的,毕竟每一个学习c语言的人,几乎都在写函数。

    1.说说函数的设计思路

    1).专业的函数名必不可少。

    2).参数设计,在实现功能的同时尽量减少参数的个数。为什么这样说?都知道函数在调用的时候参数是需要压栈。可以想象一下实现一个函数有100个参数,那么在压栈的时候会有100个实参压到栈的空间中,那么你所浪费的栈空间是非常大的。比较一下数组的传参,和字符串指针传参。数组传参的最少的话,需要将数组首地址传进来和数组大小传进来吧。因为在被调用函数的内部是不能使用sizeof来求数组的大小的,早都降级成为数组的首元素的地址了。反观,字符串指针的传参就只需要将指针传过去就好,在被调用函数内部同样可以求得字符串的长度。

    3).进入函数内部之后,第一件事就是参数有效性的检查。不要忙着设计函数的实现,慢下来,将所有可能性都考虑到,会事半功倍的。

    4).之后就是函数的设计,设计有很多种方法,本人才疏学浅也不能面面俱到,读者应多看书。

    5).函数收尾阶段,请注意问操作系统借的内存是否还回去,另一方面,指针是否为野指针。函数有没有返回值,返回值的类型又是什么。

    2.使用函数的好处

    1).降低复杂性

    2).避免重复代码

    3).限制改动带来的影响。

    4).隐含顺序

    5).改进性能

    6).进行集中控制

    读者需要了解请参照《c语言深度剖析》。

    六.结构体

     1.结构体内存对齐以及位段的知识

    内存对齐 url:http://10799170.blog.51cto.com/10789170/1718679 

    位段:按bit位填充。

     2.说说我对结构体的感悟吧,不再谈语法。

    1).结构体是一个自定义的类型,可以用这个类型定义结构体变量。

    2).结构体不同于数组,它可以将集中不同的数据类型集中在一起。

    3).是实现链表的结点。

    4).本人用结构体实现过一个简易的电话本,附上url

    http://10799170.blog.51cto.com/10789170/1718562 

    5).结构体需要大量的练习以及开发才能熟练掌握,光懂语法是不顶用的。实践是检验真理的唯一标准,同时也是学习新知识的必由之路。


    以上就是本人在学习过程中的一些经验总结。当然,本人能力有限,难免会有纰漏,希望大家可以指正。

本文出自 “做一个小小小司机” 博客,请务必保留此出处http://10799170.blog.51cto.com/10789170/1720538

0 0
原创粉丝点击