精华内容
下载资源
问答
  • C语言资料大全1.0

    2013-08-03 15:22:18
    12.看懂的书,请仔细看;看不懂的书,请硬着头皮看; 13.别指望看第一遍书就能记住和掌握什么——请看第二遍、第三遍; 14.请把书上的例子亲手到电脑上实践,即使配套光盘中有源文件; 15.把在书中看到的有意义的...
  • 你必须知道的495个C语言问题

    千次下载 热门讨论 2015-05-08 11:09:25
    *2.5 在C语言中是否有模拟继承等面向对象程序设计特性的方法? 2.6 为什么声明externf(structx*p);给我报了一个晦涩难懂的警告信息? 2.7 我遇到这样声明结构的代码:structname{intnamelen;charnamestr[1];}...
  • 难道在C语言中一个结构不能包含指向自己的指针吗? o 2.7 怎样建立和理解非常复杂的声明?例如定义一个包含 N 个指向返回指向字符的指针的函数的指针的数组? o 2.8 函数只定义了一次, 调用了一次, 但编译器提示...
  • # # # # 我觉得我打游戏(不知道算不算一技之长)毕竟从小学一年级就接触到了各种形形色色的游戏,道理其实我的游戏天赋毕竟还是很大的,从意识到感觉我觉得都比大多数人好一些,其实吧打游戏打得好也是很不容易的...

    # # # #
    我觉得我打游戏(不知道算不算一技之长)毕竟从小学一年级就接触到了各种形形色色的游戏,讲道理其实我的游戏天赋毕竟还是很大的,从意识到感觉我觉得都比大多数人好一些,其实吧打游戏打得好也是很不容易的,要学的东西也不少。经验其实就是我经常上网看攻略,而且也经常看一些大神的视频操作,而且我也经常刻苦练习这些东西,最重要的吧我觉得我还是比较感兴趣(不感兴趣谁愿意花时间。。。)下学期就要开始Java的,其实我也不知道Java到底学起来怎么样。。自己到底会不会感兴趣。对下学期的学习还有些。。算得上迷茫吧,不过内心还是有些紧张和激动的。
    - 有关C语言的调查
    1. 怎么学习C语言的:其实这个问题还是很简单的,我学习的时候就是从头到尾把书本看了一遍,然后对于书上典型的例子我会专门敲敲代码,然后配合着老师发的练习,只挑一些看着比较典型的做一遍(毕竟不可能都全部做完,我又不是神仙)
    2. 写过多少行我想不起来的,可能有几千行吧,这种东西都是要多做才能有效果的,打代码打的多了好多套路都是差不多的自己深有体会
    3. 数组指针跟指针数组听着差不多但是对象却是不一样的,前者本质是指针后者是数组,函数指针跟指针函数也是如此的。
    4. 文件和流的区别:实话实说,我真的不知道这俩是啥玩意。。。可能是我的程度不算高,完全不记得了。文本文件跟二进制文件我也没有听说过2333
    5. 我知道Java是面向对象的,就是说要先确定好你的目的是什么,要做什么,怎么去做,然后把过程表示出来。
    6. 模板就是源,没有写过多个源文件的程序...
    7. 高内聚就是说模块间聚合度比较高,相互牵制相互影响,低耦合的意思就正好相反了
    8.
    ```
    #include<stdio.h>
    int main(void0)
    {
    int a[5],b[5],i;
    for ( i=0;i<5;i++ )
    {
    scanf("%d",a[i]);
    b[i] = a[i];
    }
    for ( i=0;i<5;i++ )
    printf("%d",b[i]);
    return 0;
    }

     

    int m,n,a;
    scanf("%d%d%d",&m,&n,&a);
    if(n > m){
    int temp = m;
    m = n;
    n = temp;
    }
    for(int i = n;i<=m;i++){
    int flag = i;
    while(flag){
    if(flag % 10 == a){
    printf("%d\n", i);
    break;
    }
    flag /= 10;
    }
    }


    for(int i = 0;i < arrays.Length - 1; i++)

    {

    for(int j = i + 1;j < arrays.Length;i++)

    {

    if (arrays[i] > arrays[j])

    {

    int temp = arrays[i];

    arrays[i] = arrays[j];

    arrays[j] = temp;

    }

    }

    }
    自己并不知道写了多少行代码,也没有计算过。。下次多注意这方面的知识。
    断点知识并不熟悉。。。。。


    ```
    9.首先,我个人认为C语言跟java有许多相似之处,我比较喜欢通过实践来学习代码知识,我还是比较希望老师能多关注我们每个学生,这样子效率会大大提高的。平时上课听讲是一定的,课下多练习才是王道,希望我能够真正体会到其中的规律,期待~
    ```

    ```

    转载于:https://www.cnblogs.com/brs6666/p/8401694.html

    展开全文
  • 《你必须知道的495个C语言问题》

    热门讨论 2010-03-20 16:41:18
    《你必须知道的495个C语言问题》结构清晰,讲解透彻,是各高校相关专业C语言课程很的教学参考书,也是各层次C程序员的优秀实践指南。 -----------------------------------------------------------------------...
  • *2.5 在C语言中是否有模拟继承等面向对象程序设计特性的方法? 22 2.6 为什么声明extern f(struct x *p); 给我报了一个晦涩难懂的警告信息? 23 2.7 我遇到这样声明结构的代码:struct name {int namelen; ...
  • *2.5 在C语言中是否有模拟继承等面向对象程序设计特性的方法? 2.6 为什么声明externf(structx*p);给我报了一个晦涩难懂的警告信息?  2.7 我遇到这样声明结构的代码:structname{intnamelen;charnamestr[1];}...
  •  *2.5 在C语言中是否有模拟继承等面向对象程序设计特性的方法? 2.6 为什么声明externf(structx*p);给我报了一个晦涩难懂的警告信息? 2.7 我遇到这样声明结构的代码:structname{intnamelen;charnamestr[1];...
  • 你必须知道的495个C语言问题(PDF)

    热门讨论 2009-09-15 10:25:47
    难道在C语言中一个结构不能包含指向自己的指针吗? . . . . 3 1.7 怎样建立和理解非常复杂的声明?例如定义一个包含N 个指向返 回指向字符的指针的函数的指针的数组? . . . . . . . . . . . . . . 3 1.8 函数只定义...
  • 本书结构清晰,讲解透彻,是各高校相关专业C语言课程很的教学参考书,也是各层次C程序员的优秀实践指南。 第1章 声明和初始化 30 基本类型 30 1.1 我该如何决定使用哪种整数类型? 30 1.2 为什么不精确定义...
  •  本书结构清晰,讲解透彻,是各高校相关专业C语言课程很的教学参考书,也是各层次C程序员的优秀实践指南。 第1章 声明和初始化 30 基本类型 30 1.1 我该如何决定使用哪种整数类型? 30 1.2 为什么不精确定义...
  • 现在,终于来到第一部分的最后一个知识点了,也是C++的基础部分的最后一个题。之后进入第二部分,就会开始面向对象之旅。因此,这一课也注定不平凡。系安全带吧,因为马力要加足了! 指针这个C系语言的难点...



    内容简介

    1、第一部分第十二课:指针一出,谁与争锋

    2、第一部分第十三课预告:第一部分小测验



    指针一出,谁与争锋


    上一课《【C++探索之旅】第一部分第十一课:小练习,猜单词》中,我们用一个小游戏来总结了之前几课学习的知识点。


    现在,终于来到第一部分的最后一个知识点了,也是C++的基础部分的最后一个讲题。之后进入第二部分,就会开始面向对象之旅。因此,这一课也注定不平凡。系好安全带吧,因为马力要加足了!


    指针这个C系语言的难点(著名的C语言里也有指针),令无数英雄"尽折腰",也是这个系列课程里最难的课之一。不过不用怕,只要耐心地跟着小编走,哪能不湿鞋... 噢,不是,是肯定能治好您的"腰间盘突出"!


    只要腰间盘一康复,你会发现,很多其他的知识点都会变得异常清晰和容易。而且,腰不酸了,腿不疼了,一口气都能上五楼了... 小编你够了...


    指针在C++的程序中被广泛使用。在之前的课程中我们已经在使用了,只是你没意识到罢了。只要学好了指针,对于内存的掌控水准就又上了一个台阶。


    好了,不吊胃口了。我们出发吧~



    内存地址的问题


    你是否还记得之前讲到内存的那一课?也就是《【C++探索之旅】第一部分第四课:内存,变量和引用》。希望你能够再温习一下那一课,特别是那几张内存的图示。


    那一课中说到,当我们声明一个变量时,操作系统会把内存的一小块空间借给我们使用,并且给这一块空间起一个名字(变量的名字)。就好像借给我们一个抽屉使用,并且在抽屉外面贴一个标签。例如:


    int main()
    {
        int userAge(16);
        return 0;
    }
    


    我们可以用内存图示来说明上面的代码:




    上图既简单又清楚,不是吗?不过,上图简化了不少事情。


    你会慢慢发现,在电脑中,所有都是井然有序,符合逻辑的。我们之前把内存中的地址空间比喻为一个个的抽屉,而这些抽屉上会被贴标签(如果被分配给了变量的话),也就是变量的名字。实际的情况可比这个复杂。


    电脑的内存确实是由多个"抽屉"组成的,在这一点上我们之前并没有说错。而且,现代的电脑的内存里有多达数十亿个"抽屉"!


    因此,我们需要一个体系来帮助我们在茫茫抽屉海中找到心仪的那个抽屉。内存中的每一个抽屉都有一个独一无二的编号,也就是内存地址。如下图所示:




    上图中,我们看到了在内存中的那些"抽屉",每个抽屉对应一个内存地址。


    我们之前的程序只使用了其中的一个"抽屉",它的内存地址是53768,这个抽屉里存放了我们的变量userAge。


    注意:每一个变量都有唯一的内存地址,每个内存地址上一次也只能存放一个变量。


    在我们之前的课程中,为了访问一个变量,我们使用变量的名称,例如:


    userAge = 18; // 用变量名来访问变量,对变量重新赋值
    


    但是,我们也可以用变量的地址作为访问变量的媒介。例如,我们可以对电脑"说":乖,给我显示内存地址53768上的内容。


    也许你会说:既然有了第一种用变量名来访问变量的方法,既简单又有效率,那为什么还要用第二种方法呢?


    确实,不过我们马上会看到,有时候利用内存地址来访问变量的方法是必要的。


    在这之前,我们先来学习如何得知变量的内存地址。


    显示内存地址


    在C++中,和C语言一样,用于获取一个变量的内存地址,我们需要用到&这个符号。&也被称为"取地址符"。


    假如我想要获取变量userAge的内存地址,很简单,我只要这样写:


    &userAge
    


    写一个程序试一下:


    #include <iostream>
    using namespace std;
    
    int main()
    {
       int userAge(16);
       
       cout << "变量userAge的内存地址是 " << &userAge << endl;
       
       return 0;
    }
    


    运行后,在我的电脑上显示的是:


    变量userAge的内存地址是 0x22ff0c





    你运行自己的程序时,显示的内存地址一般来说和我的不一样,毕竟这个内存地址是操作系统分配的。


    虽然上面显示的内存地址中包含了字母,但其实它是一个数字。


    因为内存地址打印出来的时候默认是以十六进制的格式来显示的(开头的0x表示后面的数字是十六进制的,因此其实是十六进制数22ff0c)。我们很熟悉十进制,也就是我们一般用的数制,电脑最底层其实只知道0和1组成的二进制。


    不过十进制,二进制,十六进制之间都是可以相互转换的。至于如何转换,一般学校里都教过了。如果没有,那百度一下也不是难事。或者你用电脑里自带的计算器程序就可以实现进制间的转换了。


    例如,上面的这个十六进制的数(0x22ff0c),转化成十进制是2293516,转换成二进制是1000101111111100001100 。


    我们的&符号,在这里是用于显示变量地址。之前讲到引用时,我们也用到了&符号。因此,不要搞混了两种用法。


    接下来,我们就来看看拿这些地址能干什么。



    指针,指哪打哪


    内存地址其实就是以数字来表示的。


    我们已经知道,在C++中,有多种数据类型可以储存数字:int,unsigned int,double,等。因此,我们可以将内存地址储存在变量中吗?


    回答是肯定的。


    不过,要储存内存地址,我们不使用之前见过的那些普通变量类型,而要用一种特殊的类型:指针。


    指针,简而言之就是:储存别的变量的内存地址的一种变量。


    请记住这句话。


    声明一个指针


    声明一个指针,就和以前我们声明一个普通变量是类似的,需要两样东西:


    • 类型

    • 名字


    至于名字,就和以前一样,只要符合规则,随便你取什么名字都行。


    不过,指针的类型却有些特别。这个类型须要指明我们要存储的地址上的变量的类型,还要加上一个*号。例如:


    int *pointer;
    


    看上去有点特别是吗?比之前我们声明一个int型的变量时多加了一个星号(*)。正是这个*号标明了这是一个指针变量。


    上面的代码声明了一个指针,名字是pointer,其内容是int型变量的内存地址。也可以说成:pointer指向一个int型的变量(之所以叫"指针"的原因)。


    我们也可以写成:


    int* pointer;
    


    这次我们让*和int相邻,之前我们是将*紧邻pointer来写的。这两种写法的效果是一样的,但是我们推荐前一种。


    为什么呢?因为如果是写成int* pointer这样的形式,很容易让我们认为int*是一个整体,如果我们在一行上同时声明多个指针,很容易就会这么写:


    int* pointer1, pointer2, pointer3;
    


    我们的初衷是:声明三个指针变量pointer1, pointer2, pointer3,都指向int型变量。但事实上,只有pointer1成功地被声明为了指针变量,后面的pointer2和pointer3只是int型变量!


    这是初学需要注意的。因此,我们一般这么写:


    int *pointer1, *pointer2, *pointer3;   // 声明三个指针变量
    


    同样地,我们可以声明指向其他变量类型的指针。例如:


    double *pointerA;
    //声明一个指针,其中可以储存double类型的变量的地址
    
    unsigned int *pointerB;
    //声明一个指针,其中可以储存unsigned int类型的变量的地址
    
    string *pointerC;
    //声明一个指针,其中可以储存string类型的变量的地址
    
    vector<int> *pointerD;
    //声明一个指针,其中可以储存vector<int>类型的变量的地址
    
    int const *pointerE;
    //声明一个指针,其中可以储存int const类型的变量的地址
    


    注意:指针是一种变量类型,这种变量类型在每一个操作系统上的大小是固定的,就好像int型,double型这样。不要认为指针可以储存int类型的变量的地址,这个指针就是int型指针,这样的说法是不准确的。


    你可以测试一下,用sizeof操作符来获取指针类型所占的字节数。


    例如:


    cout << "指针的大小是 " << sizeof(pointer) << endl;
    


    在我的电脑上,打印的结果是


    指针的大小是 4


    也就是说指针的大小是4个字节,是不随其所指向的内存地址上的变量的类型而改变的。


    暂时,上面那些指针还只是声明了,没有被赋值。


    这是很危险的!


    因为此时指针里面包含的可以是任意的内存地址。假如你使用这样未赋值的指针的话,你并不知道自己在操作内存中的哪块地址。有可能这块内存地址上保存着极为重要的数据。


    因此,永远记得:声明指针之后,在使用前一定要先对其赋值。


    因此,一个不错的习惯就是声明的同时给指针赋初值,就算是初始化啦。通常习惯赋0,例如:


    int *pointer(0);
    
    double *pointerA(0); 
    
    unsigned int *pointerB(0);
    
    string *pointerC(0);
    
    vector<int> *pointerD(0);
    
    int const *pointerE(0);
    


    还记得这一课的开始处我们给的一幅内存图吗?内存的第一个可用的抽屉的标号是1而不是0,地址为0的内存空间一般不可用。


    因此,当我们为一个指针变量赋初始值0时,意味着它不指向任何内存地址。就好像用一根缰绳把一匹会乱跑的悍马栓在0这个木桩上("套马的汉子,你威武雄壮...")。


    因此,假如你声明一个指针变量时还未决定让其指向什么地址,那么给其赋初值0是很必要的。


    储存一个内存地址


    现在我们已经会声明指针变量了,接下来要学习的就是如何把另一个变量的内存地址储存到这个指针变量中。


    我们已经知道,要获得变量的内存地址,需要用到&符号。那么就很简单了,例如:


    int main()
    {    
        int userAge(16);    //一个int型的变量
        
        int *ptr(0);    //一个指针变量,其中可以储存一个int型变量的内存地址    
        
        ptr = &userAge;    //把int型变量userAge的地址存放到ptr这个指针变量里    
        
        return 0;
    }
    


    以上程序中,最关键的一句就是


    ptr = &userAge;
    


    执行完这句指令之后,指针变量ptr里面的内容就变成了userAge这个变量的地址,我们说:ptr指向userAge。


    用一张内存图示来说明:




    上图中,我们见到了我们的老朋友userAge,此变量存放在内存地址53768处,变量的值是16。


    新增的内容当然就是指针啦。在内存地址14566上(当然这些内存地址都是举个例子)存放着一个指针变量,名字是ptr,注意看:ptr的值就是53768,也就是我们的userAge的地址。


    好了,现在你差不多理解了吧。


    当然了,也许你还是有疑问:为什么要把一个变量的内存地址存放到另一个变量里呢?有什么好处呢?


    相信我,你会"守得云开见月明"的。


    假如你理解了上图的话,那么是时候深入学习咯。


    显示内存地址


    指针也是一种变量类型,因此,我们可以显示其值。


    写个程序:


    #include <iostream>
    using namespace std;
    
    int main()
    {
        int userAge(16);
        int *ptr(0);
        
        ptr = &userAge;
        
        cout << "变量userAge的内存地址是 " << &userAge << endl;
        cout << "指针变量ptr的值是 " << ptr << endl;
        
        return 0;
    }
    


    运行以上程序,显示:


    变量userAge的内存地址是 0x22ff0c

    指针变量ptr的值是 0x22ff0c




    看到了吗?我们的userAge变量的内存地址和指针变量ptr的值是一样的。


    访问指针指向的内容


    还记得指针的作用吗?它使我们可以不通过变量名就访问一个变量。


    那么怎么做呢?需要使用*号,它可以获取指针指向的变量的值。


    例如:


    int main()
    {
       int userAge(16);
       int *ptr(0);  
       
       ptr= &userAge;
        
       cout << "指针变量ptr所指向的变量的值是  " << *ptr << endl; 
         
       return 0;
    }
    


    程序执行到 cout << *ptr 时依次做了以下的操作:


    1. 找到名字是ptr的内存地址空间

    2. 读取ptr中的内容

    3. "跟着"指针指向的地址(也就是ptr的值),找到所在地址的内存空间

    4. 读取其中的内容(变量userAge的值)

    5. 打印这个值(此处是16)


    此处我们又一次使用了星号(*),术语称为:"解引用"一个指针。


    还记得之前我们用星号来声明指针变量吗?因此,同一个符号在不同情况下作用也许不一样。


    符号小结


    我承认,这些符号是有点让人头晕。目前为止,星号(*)有两个作用,而且&在这一课之前是用于引用的。


    这可不能怪我,你们说对吧,要怪也得怪C++之父。


    好吧,我们来小结一下:


    假如我们有如下代码:


    int number = 16;
    int *pointer = &number;
    


    那么:


    对于int型变量 number 来说


    • number:number的值

    • &number:number的内存地址


    对于指针 pointer 来说


    • pointer:指针的值,也就是其所指向的另一个变量的内存地址。也就是number的地址,即&number

    • *pointer:指针所指向的内存的值,也就是number的值


    如果不太理解,可以画一些图来帮助掌握。



    动态分配


    你想要知道指针到底可以用来做什么吗?那好,我们先来学习第一种用法:动态分配。


    内存的自动管理


    在我们以前关于变量的课程里,我们已经学过:当变量被定义时,大致说来程序其实做了两步:


    1. 程序请求操作系统分配给它一块内存空间。用专业术语说就是:内存分配。

    2. 用一个数值来填充这块内存空间,用术语说就是:变量的初始化。


    上面的两个步骤是自动完成的,程序会替我们打理。而且,当我们的程序执行到函数的末尾时,就会把操作系统分配的内存空间自动归还。专业术语叫做:内存释放。在这种情况下,内存释放也是自动的。


    我们现在就来学习不是自动的方式,也就是手动的。是的,聪明如你应该猜到了,我们要使用指针。


    分配内存空间


    为了手动申请内存空间,我们需要使用运算符new。


    new在英语中时"新的"的意思。


    new会申请一个内存空间,如果成功,则返回指向这块内存空间的指针。所以嘛,就轮到我们指针上场啦。例如:


    int *pointer(0);
    pointer = new int;
    


    上面的两行程序中的第二行向操作系统申请一块内存地址,为了储存int型变量。这块内存地址的地址值将储存在pointer这个指针里。原理如下图所示:





    从上图中可以看到,我们一共使用了两个内存空间:


    • 第一块内存空间的地址是14563,其中存放的是一个还没被赋初值的int型变量,而且此变量也没有名字。

    • 第二块内存空间的地址是53771,其中存放的是我们的指针pointer,指针的值是14563。


    记得:在内存地址14563上的变量是没有变量名的,只能通过指针pointer来访问。


    因此,如果我们修改指针pointer的值,我们就失去了唯一访问那块没有标签的内存的机会。你将不能再使用那块内存,也不能删掉它。这块内存就好像迷失了一般,不过又占用着内存,术语称为:内存泄漏。因此必须当心!


    一旦手动分配成功,这个变量就和平时的变量一样使用。只不过我们是通过指向这个变量的指针来操作它的,因为我们并没有给这个变量起名字。需要用到解引用(*)。


    int *pointer(0);
    pointer = new int;
    *pointer = 2;  //通过指针访问内存,以改写其中的内容


    那块没有标签的内存空间(相当于没有变量名的变量)现在被填充了数值,是2。因此,内存里的情况如下:




    使用完此内存地址后,我们需要向操作系统归还这块内存地址(指针指向的那块内存地址)。


    释放内存


    我们用delete运算符来释放内存。


    delete在英语中是"删除,清除"的意思。例如:


    int *pointer(0);
    pointer = new int;
    
    delete pointer;  //释放内存。注意:是释放了指针指向的那块内存


    执行上面的操作后,指针pointer所指向的那块内存就被归还给操作系统了。不过,内存却还一直存在,而且还是指向那块内存,不过,我们没有权利再使用这个指针了。如下图所示:




    上图还是比较形象的。如果我们循着指针的所指的箭头找去,我们会达到一块已经不属于我们的内存。因此我们不能再使用这块内存了。


    "伊人已嫁,吾将何去何从?何以解忧,唯有稀粥"


    因此,为了避免我们之后又误操作这块内存,须要在delete操作之后,再删去这个指向已不属于我们的内存的箭头。聪慧如你应该想到了,就是将指针的值置为0(无效的内存地址)。如果没有置0这一步,那么程序往往会奔溃,即使一时没奔溃,也是存在隐患的。


    int *pointer(0);
    pointer = new int;
    
    delete pointer;    //释放内存
    pointer = 0;       //把指针指向一个无效的地址


    记得:手动分配了内存之后,使用完一定要释放这块内存。不然,你的内存会越来越少。一旦内存不够用了,你的程序就会奔溃。


    一个完整的例子


    我们用一个完整的例子来结束这一小节吧:询问用户的年龄,并借助指针来显示年龄


    #include <iostream>
    using namespace std;
    
    int main()
    {   
        int* pointer(0);   
        pointer = new int;   
        
        cout << "您的年龄是 ? ";   
        cin >> *pointer;   
        //借助指针变量pointer,我们改写pointer指向的内存地址上的内容
        
        cout << "您 " << *pointer << " 岁了." << endl;
        //再次使用pointer指针
        ​
        ​delete pointer;   //别忘记释放内存   
        ​pointer = 0;   //并且把指针指向一个无效的地址   
        ​
        ​return 0;
    }


    通过这个例子,我们掌握了如何通过指针进行内存的分配和释放。


    之后,我们会学习使用Qt来开发图形界面的程序。我们会经常使用new和delete这对组合,例如在创建窗体和销毁窗体的时候。



    到底啥时用指针好呢?


    到了这一课的最后一小节,我们需要解释一下:何时使用指针呢?


    通常在以下三种情况下应该使用指针:


    • 想要自己控制内存空间的分配和释放

    • 在多个代码块中共享一个变量

    • 在多个元素间选择


    其他的情况,我们可以不必使用指针。


    第一种情况我们已经学习了。来看看后两种吧。


    共享一个变量


    对于指针的这种用法,暂时我还不给你完整的代码示例。当之后第二部分讲到面向对象的编程时,我们自然就会有实例了。不过,我会给出一个更加视觉化的例子。


    你玩过策略游戏吗?如果没玩过,也许听说过吧。


    举个例子,这类游戏中有一个很著名的游戏:Warcraft III,暴雪公司的作品。截图如下:



    要编写这样一个大型的RPG游戏,是非常复杂的。我们此处不讨论游戏,而是借此来思考一下一些用例。


    在上图中,我们可以看到红色的人族和蓝色的兽族在交战。游戏中的每一个角色都有一个攻击目标。


    例如,兽族的几乎所有兵力都在攻击被暗影猎手变成小动物的山丘之王(就是鼠标点中的那个"小动物")。人族的兵力在攻击剑圣。


    那么,在C++中,如何指明红色的人族的攻击目标呢?当然了,暂时你还不知道怎么用代码实现,但是你应该有点主意了吧?回想一下这一课的主题。


    是的,就是用指针。游戏中每一个角色都有一个指针,指向他们攻击的目标。这样,每个角色才会知道在某一刻该攻击谁。例如,我们可以写这样的代码:


    Personage *target;  //指向攻击目标的一个指针


    当双方没有交战之前,target这个指针指向地址0,也就是没有攻击目标。一旦两军兵刃相接,target指针就指向攻击的目标,而且随着游戏进行,target指向的目标是可以切换的。


    因此,在这样的游戏中,一个指针就可以将一个角色和他的攻击目标联系起来了。


    之后我们会学习怎么写这样的代码。而且在第二部分我们也可以写一个小型的角色扮演游戏(RPG游戏)。


    在多个元素间选择


    指针的第三种用途是可以依据用户的不同选择来作出不同的反应。


    举个例子,我们给用户出一个多项选择题,有三个选项。一旦用户选择答案之后,我们用指针来显示他选择了哪个答案。代码如下:


    #include <iostream>
    #include <string>
    using namespace std;
    
    int main()
    {    
        string responseA, responseB, responseC;    
        
        responseA = "幽闭空间恐惧症";    
        responseB = "老年痴呆症";    
        responseC = "妄想症";    
        
        cout << "阿尔茨海默病是指什么 ? " << endl;  //提问题
        
        //显示答案
        cout << "A) " << responseA << endl;    
        cout << "B) " << responseB << endl;    
        cout << "C) " << responseC << endl;    
        
        char response;    
        cout << "您的答案是 (填写A, B 或 C) : ";    
        cin >> response; //提取用户的答案    
        
        string *userResponse(0); //指向所选答案的指针    
        
        switch(response)    
        {    
            case 'A':        
                userResponse = &responseA;        
                break;    
            case 'B':        
                userResponse = &responseB;        
                break;    
            case 'C':        
                userResponse = &responseC;        
                break;    
        }    
        
        //用指针来显示我们选择的答案    
        cout << "您选择了答案 " << *userResponse << endl;    
        
        return 0;
    }


    当然了,指针还有很多用处,以后我们会慢慢学习的。



    总结


    1. 每一个变量是存储在内存的不同地址空间上。

    2. 每一个地址上一次只能存放一个变量。

    3. 可以借助&符号来获得一个变量的地址,用法是:&variable

    4. 指针是一种特殊的变量类型,这种变量中存储的是另一个变量的地址。

    5. 一个指针是这样声明的:int *pointer; (这种情况下这个指针指向的是一个int型的变量)

    6. 如果我们打印一个指针的内容,那么得到的是其中储存的内存地址。如果我们用*符号来获得指针指向的内存(*pointer),那么打印的是指针指向的地址上存放的变量值:

    7. 我们可以手动申请一块内存地址,用new关键字。如果用了new关键字进行了所谓动态内存分配,之后不用此变量了,我们需要用delete关键字来手动释放这块内存(因为不会被自动释放)。

    8. 初学指针可能会觉得比较难理解。不过不必担心,大可再读一遍这一课。读书百遍,其义自见。不过也要配合练习。不用怕,以后我们会经常使用指针,所以你会慢慢熟练的。



    第一部分第十三课预告


    今天的课就到这里,一起加油吧!

    下一课我们学习:第一部分小测验

    展开全文
  • 009-彩色的显示器

    2017-03-09 00:02:10
    上次到了引用C语言开发的原因以及如何使用C语言与之前的汇编头结合,这次,正式开始C语言的开发。  由于我们已经指定了C语言编写的入口函数为OSMain(),那么,就从这个函数开始写吧。既然要写一个函数,我们首先...

           上次讲到了引用C语言开发的原因以及如何使用C语言与之前的汇编头结合,这次,正式开始C语言的开发。

           由于我们已经指定了C语言编写的入口函数为OSMain(),那么,就从这个函数开始写吧。既然要写一个函数,我们首先得确定这个函数的参数和返回值类型。如何确定呢?那得看谁会来调用这个函数,又想从这个函数里获得点什么。由于我们是将编译好的这个程序和之前写的汇编头相拼接的,那么,调用这个函数的一定就是我们之前写的汇编指令了,更准确的来说,是一条JMP语句。但是,汇编语言之间的函数参数传递都是通过寄存器来实现的,因为对于汇编语言来说,不存在什么局部变量,所有的程序公用寄存器和内存,所有,它们之间可以随意使用,自然就可以很顺畅的传递参数。但是这一点在C语言不可以,由于C语言在编译时会给内存来划分相对应的堆区栈区,函数之间的数据传递一定要通过接口,这里的接口就体现为参数和返回值。我们在写一个纯C程序时不必考虑这些问题,因为只要提供好接口,并且合适地调用就万事大吉了,至于接口处数据如何传递,这是编译器的事。但是现在不行,现在我们的C函数是要用汇编语言来调用的,我们就必须明白这些参数到底是存放在了哪里。

           我们可以写一段简单的C程序,通过反汇编来查看里面的数据传递逻辑。通过研究我们可以知道,C语言会在函数调用的时候,将函数指针传入esp寄存器中,而在实际内存中,函数指针的后面紧跟着的就是参数的指针,由于我们在32位环境下编写代码,所有的指针都应该是32位的,也就是4字节,所以,esp+4, esp+8……就分别是第一个,第二个……参数的指针。写过变参函数的程序员都知道,变参函数中函数参数的类型是未知的,个数也是无限制的,之所以会这样就是由于C语言函数将参数依次放在了内存中,当你写一个普通函数的时候,我们通过函数指针类型可以知道这个范围到哪里,所以不允许参数越界(这完全是编译器的限制),但是当你写一个变参函数的时候,这个限制就被放开,自然也就无法确定参数个数了。那么,函数的返回值会放在哪里呢?会放在EAX中(当然,这会根据你实际声明的变量类型来决定究竟是读取EAX还是AX还是AL)。所以总结一下就是,当我们在C语言中调用一个函数表达式时,编译器要做的事情是这样的,首先,根据函数指针找到函数对应的内存位置,并且把这个地址放到esp中,其次,以esp为基址依次向后读取参数,然后执行函数主体,最后会读取EAX中的值作为函数返回值,替换函数调用在C语言表达式中的位置。这就是为什么如果你声明了函数返回值,但是在函数中没有写return语句,它就会返回一个任意值的原因,因为EAX中的值是随机的。了解了这个以后再来考虑,OSMain函数的参数和返回值问题。我们在调用这个函数的时候,将需要的信息都保存在了内存的固定位置,因此只要知道这些位置就好,暂时还不需要通过参数传递(当然,有序组织起来直接传参不妨视为一个更好的方式,目前暂时搁置,以后在优化代码的时候会重新考虑),而至于返回值,好的习惯是,在C语言主体程序结束后,给寄存器里留下一个返回值,用于表示程序是否正常运行,如果非正常退出,可以给出错误码,我们在之后还可以去进行相关处理。暂时还不需要特别关注这个错误码,先留出一个接口就好,因此决定,OSMain函数是一个int返回值的无参数函数,并且设定正常退出的返回值为0,这样,程序框架就出来了,是我们非常熟悉的形式。

    int OSMain() {
    
        return 0;
    }

           接下来的任务就是在屏幕上显示出内容来了。我们知道在进入OSMain函数之前,我们就已经开启了VGA彩色显示模式,这是一个320×200分辨率的256色。换句话说,我们的屏幕上现在有640,000个像素点,每个像素点你可以在256种颜色中选择一种来填充,由此来绘制我们的界面。在VGA模式下,显卡支持RGB配色,红、绿、蓝三种颜色的配比各有256个等级,通常情况下我们用两位十六进制表示一个颜色分量的比例。例如纯白色就表示为#FFFFFF,纯黄色就表示为#00FFFF。但是这样的话我们来算一下,所有能够显示得颜色应该有256×256×256=16,777,216种颜色,这显然已经远远超出了256种颜色,因此,我们就需要从这一千六百多万种颜色里选出256种颜色作为我们要使用的颜色。这256种颜色慢慢选,慢慢试,机械性的选美工作就让大家萝卜青菜了。这里为了把操作系统界面差不多显示出来,就只选择了16种颜色,我们先来做一个16色的操作系统,至于其他颜色,以后慢慢加就好了。

           所以,我们选择了16种配色,写出了以下basicDisplay.h文件:

    #ifndef basicDisplay_h
    #define basicDisplay_h
    
    #include "asmfunc.h"
    #include "global.h"
    
    #define PORTID_PALETTE 	        ((void *)0x03c8)
    #define PORTID_RGB		((void *)0x03c9)
    #define VGA_MEM_END		((void *)0xafa00)
    
    typedef struct {
    	unsigned char red, green, blue;
    } colorRGB;
    colorRGB colorRGBMake(const char r, const char g, const char b);
    
    #define COLOR_RGB(rgb) (colorRGBMake(rgb))
    #define BLACK		0x00, 0x00, 0x00
    #define RED 		0xff, 0x00, 0x00
    #define GREEN		0x00, 0xff, 0x00
    #define YELLOW		0xff, 0xff, 0x00
    #define BLUE		0x00, 0x00, 0xff
    #define PERPLE		0xff, 0x00, 0xff
    #define CYAN		0x00, 0xff, 0xff
    #define WHITE		0xff, 0xff, 0xff
    #define GRAY		0xc6, 0xc6, 0xc6
    #define DARK_RED 	0x85, 0x00, 0x00
    #define DARK_GREEN	0x00, 0x85, 0x00
    #define DARK_YELLOW	0x85, 0x85, 0x00
    #define DARK_BLUE	0x00, 0x00, 0x85
    #define DARK_PERPLE	0x85, 0x00, 0x85
    #define DARK_CYAN	0x00, 0x85, 0x85
    #define DARK_GREY	0x85, 0x85, 0x85
    
    enum {
    	id_black = 0,
    	id_red = 1,
    	id_green = 2,
    	id_yellow = 3,
    	id_blue = 4,
    	id_perple = 5,
    	id_cyan = 6,
    	id_white = 7,
    	id_gray = 8,
    	id_darkRed = 9,
    	id_darkGreen = 10,
    	id_darkYellow = 11,
    	id_darkBlue = 12,
    	id_darkPerple = 13,
    	id_darkCyan = 14,
    	id_darkGray = 15
    };
    
    void setPalette();
    #endif
    


           解释一下这个代码,4-8行暂时略过,一会会解释。第9行这里定义了一个指针常量,指向显存。在CPU的寻址空间中(目前是32位字长4GB寻址空间),绝大部分由内存条提供,但是还有一部分是由显卡来提供的,这部分RAM的作用就是用于存储关于显示的数据,就叫做显存。如果你使用独立显卡,那这部分存储空间就在你的显卡上,这时对于CPU来说,虽然你有内存和显存这两个物理起见,但由于它们都被接在了CPU的直接寻址线上,因此对于CPU来说它们是一样的。(这里我们考虑的是两种物理设备的情况,而在真实的计算机设备上,通常不只这两种,CPU还会将一根寻址线接到硬盘,通过虚拟内存技术将外存的一部分作为内存来使用,从而提升内存空间,缓解内存压力)这也就是当年量产了4GB内存条以后,大家都吵着浪费内存空间的原因,因为CPU要去访问显存,自然就要有一部分内存永远无法被访问到了,大致上只能用到3.2GB-3.6GB左右。而如果你使用的是集成显卡,那么显存就得让内存来充当了,这时候虽然你可以访问4GB的内存空间,但是你就必须划分出来一部分内存空间来作为显存使用。但是无论你使用哪种具体的物理设备作为显存,它都在CPU的寻址空间中,因此我们在编写程序的时候就不需要考虑这个问题了,我们只需要知道显存的地址是多少就可以了。在早期黑白显示的时候,对于屏幕上每一个像素只能有两种状态,亮或者灭,所以一个像素只需要一个二进制位的显存就可以了,这个时候一个地址的显存会对应屏幕上8个像素点的亮暗信息。(这里顺带一提,计算机刚加电的时候,屏幕会被初始化为80×25的文本模式,利用320KB的显存可以显示2000个字符。)不过我们由于进入了VGA模式,每一个像素点都需要表示一个8位色,所以,一个像素点就需要对应8位,也就是1字节的显存。那么我们有640,000个像素点,所以0xafa00-0xbf400的地址空间就是我们的显存了,在每一个像素里添一个颜色代号就可以显示对应的颜色了。

          11-14行定义了RGB色结构体,并声明了与之配套的结构体构造函数。16-32行用于配合RGB构造函数,定义了16种颜色。34-51行分别定义了这些颜色的代号。53行定义的函数,接下来就要重点进行说明。下面给出basicDisplay.c的代码:

    #include "basicDIsplay.h"
    
    colorRGB colorRGBMake(const char r, const char g, const char b) {
    	colorRGB temp;
    	temp.red = r;
    	temp.green = g;
    	temp.blue = b;
    	return temp;
    }
    
    void setColor(const int colorId, const colorRGB color) {
    	outportB(PORTID_PALETTE, colorId);
    	outportB(PORTID_RGB, color.red / 4);
    	outportB(PORTID_RGB, color.green / 4);
    	outportB(PORTID_RGB, color.blue / 4);
    }
    
    void setPalette() {
    	int nEflags = loadEflags();
    	cleanInterrupt();
    	setColor(0, COLOR_RGB(BLACK));
    	setColor(1, COLOR_RGB(RED));
    	setColor(2, COLOR_RGB(GREEN));
    	setColor(3, COLOR_RGB(YELLOW));
    	setColor(4, COLOR_RGB(BLUE));
    	setColor(5, COLOR_RGB(PERPLE));
    	setColor(6, COLOR_RGB(CYAN));
    	setColor(7, COLOR_RGB(WHITE));
    	setColor(8, COLOR_RGB(GRAY));
    	setColor(9, COLOR_RGB(DARK_RED));
    	setColor(10, COLOR_RGB(DARK_GREEN));
    	setColor(11, COLOR_RGB(DARK_YELLOW));
    	setColor(12, COLOR_RGB(DARK_BLUE));
    	setColor(13, COLOR_RGB(DARK_PERPLE));
    	setColor(14, COLOR_RGB(DARK_CYAN));
    	setColor(15, COLOR_RGB(DARK_GREY));
    	storeEflags(nEflags);
    	setInterrupt();
    }
    

          
           3-9行用于创建结构体变量创建的构造函数,不再解释。重点解释11-16行的setColor()函数。我们刚才说到,需要在很多颜色中选出256中颜色,对于我们这里来说,就是要选出16中颜色,给它表上0-15号,这样,你给显存里存放0-15中的数字的时候,显卡就能知道到底把哪个颜色显示出去了。那如此说来,就应该有一个“东西”,能够让我们写一个表格(就有点类似于GDT和IDT那样),里面要放着色号和颜色的对应关系,我们把这个“东西”就叫做调色盘。那么究竟什么是调色盘,它其实是显卡上的一个部件,类似于寄存器又类似于内存,这么纠结是因为它的存储空间要比寄存器大得多,但是性质上来说又很像寄存器。具体的电子芯片结构的问题在这里就没有讨论的意义的,总之这是显卡中的一部分。那么既然它不再内存中,我们也就不能通过寻址的方式来访问里面的数据了,因为它并不在CPU的寻址空间当中。那么如何访问到它呢?CPU要想访问内存之外的东西,就必须通过I/O总线,这是CPU与外界输入输出设备连接的接口,在I/O总线上的每个设备都有一个自己的编号,CPU正是通过这个设备编号找到对应的设备的。setColor()函数的任务就是向调色盘对应的编号发送数据。向0x03c8发送数据表示将要设定的颜色编号,然后再向0x03c9发送三次数据来分辨表示R、G、B的分量。这样就设置好了一种颜色。为什么要除以4呢?这是因为,虽然RGB用了6位十六进制表示一个颜色,照理说应该能够表示16,777,216中颜色,但是其实VGA显卡还支持不了这么多中颜色,而是每种颜色分量只支持64种,也就是一共64×64×64=262,144种颜色,那么这与真正的RGB色号的对应关系是什么呢?就是每4个一跳,也就是说(0x00, 0x00, 0x00)表示#000000,(0x01, 0x01, 0x01)表示#040404, (0x02, 0x03, 0x40)表示#080CFF,以此类推,所以,我们把RGB的颜色分量除以4就可以得到实际的编码。21-36行就是通过调用16次setClolor()函数来完成16种颜色的编选。

           接下来介绍刚才没有介绍到的代码,比如19,20,37,38行代码的含义以及outportB函数的由来。这些莫名其妙出现的函数是哪里来的呢?这里需要注意的是,由于我们在编写非常底层的操作系统代码,这是没有任何集成开发环境给我们使用的,更不会有什么函数库给我们用,所有的一切都是我们自己写上去的。所谓C语言编程,也不过是仅仅使用了C语言语法特性而已,那些我们所熟悉的标准C语言库是一概没有的,因为谁也不会知道我们即将开发出的操作系统是什么样的,自然也就不会有与之相匹配的库文件了。那么,这些函数并不是库函数,它一定是我们自己来实现的。outportB函数是想给I/O设备发送信息,这种功能C语言是做不到的,所以,得靠外部力量来实现。这个功能用汇编语言来写非常容易,一句OUT指令就可以搞定,所以,我们创建了asmfunc.asm文件,专门用于写C语言无法实现的函数的函数体。

    [format "WCOFF"]		; 由于该文件编译后还需要链接,所以声明文件格式
    [instrset "i486p"]
    [bits 32]				; 生成32位模式机器码
    
    [file "asmfunc.asm"]	; 文件名
    	global _halt		; 包含的函数
    	global _cleanInterrupt, _setInterrupt
    	global _inportB, _inportW, _inportD
    	global _outportB, _outportW, _outportD
    	global _loadEflags, _storeEflags
    	global _loadGDTR, _loadIDTR
    	global _preInterruptHandler21, _preInterruptHandler2c, _preInterruptHandler27
    	extern _interruptHandler21, _interruptHandler2c, _interruptHandler27
    	[section .text]			; 实际的函数
    _halt:
    	hlt
    	ret
    
    _cleanInterrupt:
    	cli
    	ret
    	
    _setInterrupt:
    	sti
    	ret
    	
    _inportB:
    	mov edx, [esp + 4]
    	mov eax, 0
    	in al, dx
    	ret
    	
    _inportW:
    	mov edx, [esp + 4]
    	mov eax, 0
    	in ax, dx
    	ret
    	
    _inportD:
    	mov edx, [esp + 4]
    	mov eax, 0
    	in eax, dx
    	ret
    	
    _outportB:
    	mov edx, [esp + 4]
    	mov al, [esp + 8]
    	out dx, al
    	ret
    	
    _outportW:
    	mov edx, [esp + 4]
    	mov ax, [esp + 8]
    	out dx, ax
    	ret
    	
    _outportD:
    	mov edx, [esp + 4]
    	mov eax, [esp + 8]
    	out dx, eax
    	ret
    	
    _loadEflags:
    	pushfd
    	pop eax
    	ret
    	
    _storeEflags:
    	mov eax, [esp + 4]
    	push eax
    	popfd
    	ret
    	
    _loadGDTR:
    	mov ax, [esp + 4]
    	mov [esp + 6], ax
    	lgdt [esp + 6]
    ;	mov eax, [esp + 8]
    ;	mov [esp + 6], eax
    ;	lgdt [esp + 4]
    	ret
    	
    _loadIDTR:
    	mov ax, [esp + 4]
    	mov [esp + 6], ax
    	lidt [esp + 6]
    	ret
    	
    _preInterruptHandler21:
    	push es
    	push ds
    	pushad
    	mov eax, esp
    	push eax
    	mov ax, ss
    	mov ds, ax
    	mov es, ax
    	call _interruptHandler21
    	pop eax
    	popad
    	pop ds
    	pop es
    	iretd
    	
    _preInterruptHandler2c:
    	push es
    	push ds
    	pushad
    	mov eax, esp
    	push eax
    	mov ax, ss
    	mov ds, ax
    	mov es, ax
    	call _interruptHandler2c
    	pop eax
    	popad
    	pop ds
    	pop es
    	iretd
    	
    _preInterruptHandler27:
    	push es
    	push ds
    	pushad
    	mov eax, esp
    	push eax
    	mov ax, ss
    	mov ds, ax
    	mov es, ax
    	call _interruptHandler27
    	pop eax
    	popad
    	pop ds
    	pop es
    	iretd

           现在给出的这个文件里有很多函数是以后要用的,先给出,以后还会来讲解。这里主要讲解outportB函数。46行接收传来的第一个参数,放入edx中,显然这应该是32位的数据,47行接收传来的第二个参数,放入al中,显然这应该是一个8位的数据。在48行执行out指令,edx中的数据作为设备编号,al中的数据作为发送的数据。如此看来,给这个函数传递的参数,应该是一个指针(用于指向设备),还有一个数据(用于向设备发送),函数名中的B的意思就是发送的这个数据应该是一个字节的。由于最后没有向eax里保存什么数据,所以说明这个函数不需要返回值。那么我们由此就可以写出它在C语言中的函数声明:

    void outportB(void *pPort, char data);

           这样就解释清楚了outportB()函数的来历。下面给出完整的asmfunc.h文件:

    #ifndef asmfunc_h
    #define asmfunc_h
    
    void halt();
    
    void cleanInterrupt();
    void setInterrupt();
    
    char inportB(void *pPort);
    short inportW(void *pPort);
    long inportD(void *pPort);
    
    void outportB(void *pPort, char data);
    void outportW(void *pPort, short data);
    void outPortD(void *pPort, long data);
    
    long loadEflags();
    void storeEflags(long Eflag);
    
    void loadGDTR(long limit, void *addr);
    void loadIDTR(long limit, void *addr);
    
    void preInterruptHandler21();
    void preInterruptHandler2c();
    void preInterruptHandler27();
    
    #endif
    

           这样我们也能参照看出loadEflags(),storeEflags(nEflags),cleanInterrupt()和setInterrupt()函数的实现。这4个函数又是干什么的呢?之前介绍过CPU的中断机制,中断信号在什么时候发过来是不知道的,在接收到中断指令后CPU会停下手中的活转去执行中断,但是,就像之前进行模式转换时候那样,如果在某些重要步骤中发生了中断,是不能够直接去处理的,否则将会发生严重错误,我们把这样的操作叫做原子操作,意思就是这些操作不可分,你必须一次性处理完毕,不能因为中断就把它拆开处理。设定调色盘的过程也是原子操作,所以需要在之前关闭中断,之后再开启中断。cleanInterrupt()其实就是执行了CTI,setInterrupt()其实就是执行了STI,而另外那两个函数的目的就在于,关闭中断前保存一些数据,开启中断后再恢复,因为有些中断发出的数据会在原子操作中被改掉,等再去执行中断的时候就会发生错误,为了保险起见,我们把当前的状态先保存下来,等恢复中断以后再恢复它。

           这样,我们只需要在OSMain()函数中调用initPalette()函数就可以完成调色盘的初始化,接下来的任务就是给显存里写东西了,但是如果你每一个像素点都专门写一遍的话,那真的就类似了,所以我们略微封装一下用于显示各种界面的函数,比如说最容易想到的就是画出一个矩形了。
           要想画出一个矩形,我们应该了解矩形的位置以及大小。那么,既然我们的研究单位是屏幕上的每一个像素点,所以,我们这里的坐标也好,长度也好,都是以像素为单位。下面给出用于绘制图形的代码draw.h:

    #ifndef draw_h
    #define draw_h
    
    #include "basicDisplay.h"
    
    #define SCR_CENTER (posMake(bootInfo -> scr_x / 2, bootInfo -> scr_y / 2))
    
    typedef struct {
    	unsigned x, y;
    } Position;
    Position posMake(const unsigned x, const unsigned y);
    
    typedef struct {
    	unsigned x, y, height, width;
    } Rect;
    Rect rectMake(const unsigned x, const unsigned y, const unsigned width, const unsigned height);
    
    void drawRect(Rect rect, unsigned char colorId);
    #endif

            两个结构体分别用于定义位置和矩形,当然有与之配套的结构体构造函数。而16行的函数用于绘制矩形,下面给出其实现文件draw.c:

    #include "draw.h"
    
    Position posMake(const unsigned x, const unsigned y) {
    	Position temp;
    	temp.x = x;
    	temp.y = y;
    	return temp;
    }
    
    Rect rectMake(const unsigned x, const unsigned y, const unsigned width, const unsigned height) {
    	Rect temp;
    	temp.x = x;
    	temp.y = y;
    	temp.height = height;
    	temp.width = width;
    	return temp;
    }
    
    void drawRect(Rect rect, unsigned char colorId) {
    	int x, y;
    	for (x = rect.x; x <= rect.x + rect.width; x++) {
    		for (y = rect.y; y < rect.y + rect.height; y++) {
    			int position = y * bootInfo -> scr_x + x;
    			*(unsigned char *)(bootInfo -> vga_mem_strat + position) = colorId;
    		}
    	}
    }
    


           两个结构体构造函数不再解释,62-71行的函数就是根据rect的信息,计算出需要更改的左边点,然后把与之对应的颜色信息写到相应的显存中,非常简单。有了这个工具以后,我们就可以绘制出一个差不多的图形了,下面是initDesktop.h

    #ifndef initDesktop_h
    #define initDesktop_h
    
    #include "basicDisplay.h"
    #include "draw.h"
    
    void initDesktop();
    
    #endif
    


           一下是initDesktop.c:

    #include "initDesktop.h"
    
    void initDesktop() {
    	setPalette();
    	drawRect(rectMake(0, 0, bootInfo -> scr_x, bootInfo -> scr_y - 29), id_darkCyan);
    	drawRect(rectMake(0, bootInfo -> scr_y - 28, bootInfo -> scr_x, 1), id_gray);
    	drawRect(rectMake(0, bootInfo -> scr_y - 28, 1, 28), id_gray);
    	drawRect(rectMake(0, bootInfo -> scr_y - 1, bootInfo -> scr_x, 1), id_gray);
    	drawRect(rectMake(bootInfo -> scr_x - 2, bootInfo -> scr_y - 28, 1, 28), id_gray);
    	drawRect(rectMake(1, bootInfo -> scr_y - 27, bootInfo -> scr_x - 3, 26), id_darkGray);
    }
    


           其实就是绘制了一个看起来比较像桌面的界面而已,不再过多解释。最后再OSMain()里调用initDesktop()就大功告成。附上效果图:

    展开全文
  • open和fopen的区别

    2020-07-28 12:57:57
    觉得讲得还是很全面的。 对于这两个名字很类似的函数,对于很多初学者来说,不容易搞清楚它们有什么不同,只知道按照函数用法使用。如果能很的区分两者,相信大家对于C语言和UNIX系统(包括LINUX)有更深入的了解...
    看到了一篇比较好的文章,是说关于open和fopen的区别。觉得讲得还是很全面的。
    

    对于这两个名字很类似的函数,对于很多初学者来说,不容易搞清楚它们有什么不同,只知道按照函数用法使用。如果能很好的区分两者,相信大家对于C语言和UNIX系统(包括LINUX)有更深入的了解。

    在网上查找了一些资料,但是感觉不够全面,一些答案只是从某个角度阐述,所以让人觉得,这个也对,那个也对。但到底谁的表述更正确呢?其实都是对的,只是解释的视角不同罢了。下面结合个人的理解做一些梳理。

    1. 来源

    从来源的角度看,两者能很好的区分开,这也是两者最显而易见的区别:

    open是UNIX系统调用函数(包括LINUX等),返回的是文件描述符(File Descriptor),它是文件在文件描述符表里的索引。
    fopen是ANSIC标准中的C语言库函数,在不同的系统中应该调用不同的内核api。返回的是一个指向文件结构的指针。

    PS:从来源来看,两者是有千丝万缕的联系的,毕竟C语言的库函数还是需要调用系统API实现的。

    1. 移植性

    这一点从上面的来源就可以推断出来,fopen是C标准函数,因此拥有良好的移植性;而open是UNIX系统调用,移植性有限。如windows下相似的功能使用API函数CreateFile

    1. 适用范围

    open返回文件描述符,而文件描述符是UNIX系统下的一个重要概念,UNIX下的一切设备都是以文件的形式操作。如网络套接字、硬件设备等。当然包括操作普通正规文件(Regular File)。
    fopen是用来操纵普通正规文件(Regular File)的。

    1. 文件IO层次

    如果从文件IO的角度来看,前者属于低级IO函数,后者属于高级IO函数。低级和高级的简单区分标准是:谁离系统内核更近。低级文件IO运行在内核态,高级文件IO运行在用户态。

    1. 缓冲

    缓冲文件系统

    缓冲文件系统的特点是:在内存开辟一个“缓冲区”,为程序中的每一个文件使用;当执行读文件的操作时,从磁盘文件将数据先读入内存“缓冲区”,装满后再从内存“缓冲区”依此读出需要的数据。执行写文件的操作时,先将数据写入内存“缓冲区”,待内存“缓冲区”装满后再写入文件。由此可以看出,内存“缓冲区”的大小,影响着实际操作外存的次数,内存“缓冲区”越大,则操作外存的次数就少,执行速度就快、效率高。一般来说,文件“缓冲区”的大小随机器 而定。fopen, fclose, fread, fwrite, fgetc, fgets, fputc, fputs, freopen, fseek, ftell, rewind等。
    非缓冲文件系统

    缓冲文件系统是借助文件结构体指针来对文件进行管理,通过文件指针来对文件进行访问,既可以读写字符、字符串、格式化数据,也可以读写二进制数据。非缓冲文件系统依赖于操作系统,通过操作系统的功能对文件进行读写,是系统级的输入输出,它不设文件结构体指针,只能读写二进制文件,但效率高、速度快,由于ANSI标准不再包括非缓冲文件系统,因此建议大家最好不要选择它。open, close, read, write, getc, getchar, putc, putchar等。

    一句话总结一下,就是open无缓冲,fopen有缓冲。前者与read, write等配合使用, 后者与fread,fwrite等配合使用。

    使用fopen函数,由于在用户态下就有了缓冲,因此进行文件读写操作的时候就减少了用户态和内核态的切换(切换到内核态调用还是需要调用系统调用API:read,write);而使用open函数,在文件读写时则每次都需要进行内核态和用户态的切换;表现为,如果顺序访问文件,fopen系列的函数要比直接调用open系列的函数快;如果随机访问文件则相反。

    这样一总结梳理,相信大家对于两个函数及系列函数有了一个更全面清晰的认识,也应该知道在什么场合下使用什么样的函数更合适,效率更高。

    网址:https://www.zybuluo.com/yiltoncent/note/87461
    
    展开全文
  • 学习Python编程语言难吗?... 学习编程是一定需要老师的,我不信能无师自通把Python学。至少着急就业的人肯定不会,没人指导很难学成。那么学习Python编程语言难吗?其实学Python不难,比起C语言、C#、
  • 二线城市的程序员活得好吗? 自学编程的八大误区!克服它! 我的本科回忆录:从迷茫自卑到保送华科 聊一聊加班多该如何有效地自我提升 到底要不要考研? 昨夜,我梦回武汉 . . . 转行的路,走起来其实有点累... 来B...
  • 疯狂的程序员

    热门讨论 2012-07-18 18:05:32
    这个时候如果有电脑,在他面前这么一坐,嗒嗒嗒往键盘上这么一敲,屏幕网上一翻滚,肯定成偶像,叫他们什么也不懂,做就要做别人都不懂的。不过这年头,懂“DOS”的还真没几个。“Windows”害死人咧! 没条件,...
  • 英特尔面试专项准备

    2020-12-09 13:46:46
    同时对这些信号处理进程设计的数据结构,他说这个问题可能太复杂,就没让我继续回答。 <ul><li>线程&进程讨论</li></ul> 线程&进程的区别以及优缺点?最重要的是线程间共享用户空间&#...
  • 原因在于,我希望这本书是用来填补空白的,而不是重复去做别人已经做的事情。所谓填补空白,具体说就是让像我一样的操作系统爱好者在读完本书之后,能够有信心去读其他比较流行的开源的操作系统代码,有能力从...
  • 原因在于,我希望这本书是用来填补空白的,而不是重复去做别人已经做的事情。所谓填补空白,具体说就是让像我一样的操作系统爱好者在读完本书之后,能够有信心去读其他比较流行的开源的操作系统代码,有能力从...
  • C++MFC教程

    热门讨论 2013-05-21 13:37:15
    我很难说清楚,也很难下一个定义(在嘘我),我下面从不同的几个方面讲解一下,希望大家看了后有一点了解。 1、消息的组成:一个消息由一个消息名称(UINT),和两个参数(WPARAM,LPARAM)。当用户进行了输入...

空空如也

空空如也

1 2
收藏数 31
精华内容 12
关键字:

c语言谁讲得好

c语言 订阅