第十一课

这一课将标志着我们所谓的 “C语言入门环节” 的结束,学完这一课之后,我们将开始介绍为什么 C++ 那么的强大,然后,我们就会真正进入 Haiku 系统编程的环节,这一课是关于数据结构的。尽管变量和数组已经很好使了,不过有时候还是有点不够用。让我们开始吧!

自定义类型和枚举类型

使用关键字 typedef 是可以在 CC++ 创建自己的类型的。我们在上一节课里介绍的 fread() 和 fwrite() 函数使用到的 size_t 类型就是这么创建的。以后当我们开始学习 Haiku API 的特殊点时,我们就会一直使用这些自定义类型了,因为内建的类型比如 long, int 的大小取决于你使用的操作系统。其他的类型则是定长的。如同下面我们从 Haiku 的头文件里截取的一部分一样,自定义类型的格式是 typedef baseType newTypeName :

// The type of data        The new name for the data
typedef signed char        __haiku_std_int8;
typedef unsigned char      __haiku_std_uint8;
typedef signed short       __haiku_std_int16;
typedef unsigned short     __haiku_std_uint16;
typedef signed int         __haiku_std_int32;
typedef unsigned int       __haiku_std_uint32;
typedef signed long long   __haiku_std_int64;
typedef unsigned long long __haiku_std_uint64;

signed 和 unsigned 关键字对我们来说是新的是因为我们之前从没需要用到它们。有符号类型 signed 可以保存负数,无符号类型 unsigned 就不行。有符号类型的最高比特位被定义成符号位来标识正或负。这个其实就影响到整形变量的取值范围,比如,有符号 char 的取值范围是 -128 到 127,无符号 char 的取值范围则是 0 到 255.

这些自定义类型实际上只是同样数据的另一个名字,所以 _haiku_std_uint32 只是 unsigned int 的另外一个名字。上面代码里面的类型声明是可以运行的,但不是非常友好,这样的写法一般不会用到 – 它们实际上被用来作为头文件里定义的更友好的类型名的一个中间名。

typedef __haiku_int8    int8;    // an 8-bit signed integer
typedef __haiku_uint8   uint8;   // an 8-bit unsigned integer
typedef __haiku_int16   int16;   // a 16-bit signed integer
typedef __haiku_uint16  uint16;  // a 16-bit unsigned integer
typedef __haiku_int32   int32;   // a 32-bit signed integer
typedef __haiku_uint32  uint32;  // a 32-bit unsigned integer
typedef __haiku_int64   int64;   // a 64-bit signed integer
typedef __haiku_uint64  uint64;  // a 64-bit unsigned integer

这样看上去就舒服多了吧,从现在开始我们就会一直使用上面这些类型来替换内置的标准类型,这样我们在接触 Haiku API 之前就能习惯它们了。

枚举类型

枚举类型是一种被限定在一个特定值集合内的类型。比如你在写一个纸牌游戏,你可以把纸牌的花色定义成枚举类型,这样做比随便使用某个整数来的更有意义,比如你可以写成这样:

enum card_suit
{
    SUIT_CLUBS,
    SUIT_DIAMONDS,
    SUIT_HEARTS,
    SUIT_SPADES
};

这样一来我们就可以定义一个取这个列表中的一项为值的变量 cardSuit 了,它的声明和初始化是这样的:

enum card_suit cardSuit = SUIT_HEARTS;

声明枚举类型的格式就和下面的代码一样,枚举类型的类型名和其内部项的默认值不是必须给定的:

enum enumName
{
    // 在枚举类型里的一个项可以指定一个整数对应它
    // 如果没有指定,则第一个项就默认是0
    enumValue1 = 5,
    // 如果枚举列表中的一个项被指定了一个对应的整数
    // 这个项后的每一个项对应的值则在这个整数的基础上依次加1
    // 除非特别指定,否则enumValue2对应的整数值就是6
    enumValue2,
    enumValue3 = 9,
    // enumValue4对应的整数值应该是10
    enumValue4
};

联合类型

联合(union)是以前当内存空间非常紧张时 C 语言的一个遗留产物。除了在一些古老的代码中,现在已经非常少见了。它的定义看上去是一个结构体,但它实际的内存空间只是它内部最大变量所占用的内存空间。对它内部成员的访问和访问结构体的成员是一样的。联合和结构体的一个关键不同在于,如果你改变了联合内一个成员的值,那其他成员的值也会跟着一起改变。联合内部的每个成员其实就是对同一块内存空间里的数据的不同解释而已。恩,是不是有点绕?不过不用太关心它啦。

union myUnion
{
    int32 data32;
    int8 data8;
};

myUnion onion;
onion.data32 = 5000;

结构体

结构体(struct)是一组变量。它们可以是任何类型的变量,虽然在实际使用中结构体内成员的个数有一定限制,但理论上其实是没有的。结构体用来把有紧密联系的变量组在一起,它们是如此定义的:

// 和枚举类型一样,类型名不是必须的。
// 不能在结构体内部初始化成员。
struct myStructName
{
    int8   someInt;
    int16  someOtherInt;
    float  someFloat;
    bool   aFlagOfSomeKind;
};

现在我们就有了一个结构体,我们像使用其他数据类型一样的使用它

myStructName someVar;
myStructName aSecondVar;

要访问结构体内部的成员,我们使用结构体名加一个“.”再加结构体内部成员的名字就可以了。比如我们想设置 someVar 结构体内的 someInt 的值是5,我们就可以这么写:

// 把 someVar 内的 someInt 的值设为 5
someVar.someInt = 5;

如果你想为结构体在堆上申请空间,也不比其他普通数据类型来的麻烦:

myStructName *ptrStruct
                 = (myStructName*)malloc(sizeof(myStructName));

这么做的话,当你想要访问结构体内部的成员的时候是和之前的做法有区别的。我们使用一个箭头来代替之前的”.”。箭头就是一个减号加一个大于号 “->”。

ptrStruct->someInt = 5;

最后,结构体的内部还能嵌套结构体,访问它内部的值就是多用几个点或者多有几个箭头的事。

这一课我们已经一口气学了很多东西了。现在让我们来一些可以加升大家理解的代码吧。下面的代码是我们迄今为止看过最长的。所以请慢慢的仔细看,确保在弄明白每一行都在干什么了之后再看下一行。如果需要的时候也可能去复习下前面课程的内容。和一些案例代码相比这个更像真正的 Haiku 代码,其中包括一些我们会在之后课程普遍使用的小花招。不要担心你会不会忘记这些小花招,因为以后我们会越来越多的用到它们。

#include <stdio.h>
#include <malloc.h>
#include <math.h>
// 一个新的头文件!这个头文件是为我们的rand()函数服务的
// 这个函数让我们可以生成一些随机数
#include <stdlib.h>
// 另一个新的头文件,不过我不太记得这个有啥用。。晕。。。
#include <time.h>
// 这是一个Haiku特有的头文件
// 它提供了uint8,uint32和其他一些类型的短小,特殊的名字
// 我们之前就看过的
#include <SupportDefs.h>
// 我们将使用一个枚举类来为我们的代码定义一些有意义的变量
// 我们可以使用#defines,不过这样做可能会导致一些奇怪的错误
// 记住当把SUIT_HEARTS当整数使用的时候它的值是0
// SUIT_SPADES的值是3。我们在这个例子的后期会用到它们的整数值
enum card_suit
{
    SUIT_HEARTS,
    SUIT_CLUBS,
    SUIT_DIAMONDS,
    SUIT_SPADES,
    SUIT_NONE //这个是为了大小怪定义的花色.
};

// 这个字符数组是一个查找表,它让打印花色变的非常简单
// 我们可以使用card_suit内部成员的整数值来充当这个列表的索性
// 相比使用switch()语句这种做法节省了输入工作,同时速度也会快一点
static char sSuitCharList[] = { 'h', 'c', 'd', 's', ' ', '\0' };

// 这个枚举类型包含了所有可能的纸牌的值
// 注意到我们把每个成员的整数值和它对应的纸牌的值都对应起来了
// 所以纸牌10对应的整数值也是10
enum card_value
{
    CARD_2 = 2,
    CARD_3,
    CARD_4,
    CARD_5,
    CARD_6,
    CARD_7,
    CARD_8,
    CARD_9,
    CARD_10,
    CARD_JACK,
    CARD_QUEEN,
    CARD_KING,
    CARD_ACE,
    CARD_JOKER
};

// 这是另外一个查找表。就好像我们刚才对花色做的那样
// 我们要使用card_value的整形值来查找纸牌的友好的字符串名
// 这里有个注意点,因为第一张纸牌的整形值是2
// 所以我们必须将纸牌的整形值减去2才能得到对应的索引值
static char sValueNameList[14][3] = {
            "2","3","4","5","6","7","8",
            "9","10","J","Q","K","A","Jo"
};

// 使用结构体可以很简单的将纸牌的值和花色结合在一起
struct card
{
    card_value value;
    card_suit suit;
};

// 这个函数初始化一副标准的54张纸牌。
// 注意这幅牌没有洗过哦,它从2开始一直到Ace, 接着是大小怪
void
InitStandardDeck(card *deck)
{
    // 这个索引变量被用来在我们处理整副纸牌的时候保存我们所在的位置。
    uint8 deckIndex = 0;

    // 枚举类型值可以被当作整形使用。
    // 我们不使用我们经常使用的i,而给索引变量使用更有意义的值以免我们搞混了
    for (uint8 suitValue = SUIT_HEARTS; suitValue < SUIT_NONE; suitValue++)
    {
        // 这两个循环依次给每张卡都赋上花色值和牌值,顺序是每个花色从2开始到A
        for (uint8 cardValue = CARD_2; cardValue < CARD_JOKER; cardValue++)
        {
            deck[deckIndex].value = (card_value)cardValue;
            deck[deckIndex].suit = (card_suit)suitValue;
            deckIndex++;
        }
    }

    // 我们已经初始化完所有的普通牌,现在是大小怪
    deck[deckIndex].value = CARD_JOKER;
    deck[deckIndex].suit = SUIT_NONE;

    deckIndex++;

    deck[deckIndex].value = CARD_JOKER;
    deck[deckIndex].suit = SUIT_NONE;
}

// 这个函数的参数是指针,不过我们将把它当数组来使用
void
ShuffleDeck(card *deck, const uint8 &numCards, const uint8 &shuffleCount)
{
    //一副牌有不同数量的牌,比如:Canasta有104张
    // 我们需要一个适用与不同纸牌游戏而不用重写的生成函数
    // 指定洗牌的次数也能提供额外的灵活性来控制一副牌的混和程度
    // 这个循环是为了把牌洗指定的次数
    // 我们这里可以使用i,j作为索引变量名因为我们不会在循环内代码中使用它们
    // 它们只决定循环内的代码会循环几次,没有其他作用
    for (uint8 i = 0; i < shuffleCount; i++)
    {
        // 我们通过在数组中交换元素来洗牌
        // 这有点像第7课里的ReverseString函数
        // 不过我们是选择数组中的随机项来交换

        // 我们交换的次数越多,洗的就越彻底,尤其对于牌多的纸牌
        // 所以交换的次数取决与牌的数量
        // 浮点型外的ceil()函数向上舍入,不管小数点部分的值是多少
        // 它返回一个double型,所以我们需要把它强制转换成uint16
        // 把float或者double强制转化为整形会丢失小数部分
        // 不过由于我们已经用ceil()向上舍入了,所以我们不会丢失任何东西
        uint16 swapCount = uint16(ceil(numCards * 1.25));

        for (uint16 j = 0; j < swapCount; j++)
        {
            // rand()生成一个从0到RAND_MAX之间的随机数
            // RAND_MAX至少是32767
            // 为了生成一个随机书,我们用下面的公式
            // randomValue = rand() % rangeOfValues + minimumValue;
            // 所以生成一个5-12之间的随机数就是 rand() % 7 + 5;
            // 下面我们为洗牌先生成两个随机数
            uint8 firstIndex = uint8(rand() % numCards);
            uint8 secondIndex = uint8(rand() % numCards);

            if (firstIndex == secondIndex)
            {
                // 如果两个数一样,那就不要费力交换了
                // 由于每次我们返回循环顶部的时候j会自增
                // 所以我们在这里先让j变小
                j--;
                continue;
            }

            // 交换纸牌
            card tempCard;
            tempCard.value = deck[firstIndex].value;
            tempCard.suit = deck[firstIndex].suit;
            deck[firstIndex].value = deck[secondIndex].value;
            deck[firstIndex].suit = deck[secondIndex].suit;
            deck[secondIndex].value = tempCard.value;
            deck[secondIndex].suit = tempCard.suit;
        }
    }
}

void
PrintDeck(card *deck, const uint8 &numCards)
{
    // 用格式'2h 5c Jc'这样的格式来打印纸牌堆里的牌
    // 它们被全部打印在一行里
    // 如果终端窗口太小可能会自动跳转到第二行来打印
    for (uint8 i = 0; i < numCards; i++)
    {
        // 这里就是我们用牌值和花色来快速查找友好的名字的地方
        // 查阅这个例子开始card_value和card_suit的声明处
        // 可以得到详细的解释
        printf("%s%c ",sValueNameList[deck[i].value - 2],
                       sSuitCharList[deck[i].suit]);
    }

    printf("\n");
}

int
main(void)
{
    // rand()函数只提供“伪随机数”。
    // 我们通过当前的时间来初始化随机数生成器
    // 这样可以提供足够的随机性
    // 以后我们会更多的用到time()函数,现在忽略它就行了
    srand(time(NULL));

    // 我们的纸牌
    card deck[54];

    // 初始化纸牌
    InitStandardDeck(deck);

    // 洗牌钱先打印一边纸牌
    printf("Our deck of cards before shuffling:\n");
    PrintDeck(deck,54);

    // 好好洗一遍,5次应该差不多了把
    ShuffleDeck(deck,54,5);

    // 打印洗好的牌
    printf("Our deck of cards after shuffling:\n");
    PrintDeck(deck,54);

    return 0;
}

路在何方?

啊,终于搞定了。这个例子使用了很多我们之前花了很多时间讲解的内容,或许把它们整在一起也得花点时间。这次不用找 Bug 啦,只是要回顾下上一节课最后的项目。花点时间想想这一节课的问题回顾和另外两节课的。从下一节课开始我们要进入 C++ 这个非常给力的领域了。

第十课项目回顾

上一节课我们给出了一个项目,输入至少一个文件名,把这个文件内容打印出来,在 for 循环内的程序步骤是:

  • 尝试将参数当文件打开用于读取
  • 如果打开错误,跳到下一个循环
  • 如果打开成功,尝试读取文件的一块,把读取的字节数储存在一个变量中
  • 使用 while 循环读取文件内的数据,当读取的字节数大于 0 的时候不停的循环
  • 把读取的字节数打印在标准输出里
  • 尝试从文件流中读取更多的数据,并保存实际读取的字节数
  • 关闭文件流

代码部分如下:

#include <stdio.h>
#include <malloc.h>

int
main(int argc, char **argv)
{
    for (int i = 1; i < argc; i++)
    {
        // 为了从argv[i]中读取数据打开一个文件流
        FILE *fileHandle = fopen(argv[i],"r");

        // 如果文件流为NULL或者有错误,则进入下一个循环
        if (!fileHandle || ferror(fileHandle))
            continue;

        // 创建一个数据缓冲 -- 一个存放我们数据的数组,大小不是很重要
        // 但它至少应该有几百字节大,但不要大过4000字节
        // 你可以把它创建在栈上
        // 或者使用malloc,你爱怎么做都可以
        char buffer[1024];

        // 创建一个变量来存储真正读出的字节的个数
        int bytesRead;

        // 从文件流里读取数据并存储读出的字节个数
        // 读取我们刚建立的变量的值
        bytesRead = fread(buffer,sizeof(char),1024,fileHandle);

        // 开始while循环,如果读出的字节的个数大于0,
        // 并且文件流没有出现ferror错误,则一直循环
        while (bytesRead > 0 && !ferror(fileHandle))
        {
            // 把读取的字节个数输出到标准输出stdout里
                fwrite(buffer,sizeof(char),bytesRead,stdout);

            // 读取更多的数据并更新读取的字节个数
            // 到我们上面创建的那个变量里
                bytesRead = fread(buffer,sizeof(char),1024,fileHandle);
        }
        // 如果你使用了malloc,则释放缓存
        // 如果你使用的是栈空间,就不要管了
        // 关闭文件流
        fclose(fileHandle);
    }
    return 0;
}