1.5 读写二进制文件

严格来说所有的文件都是二进制格式,二进制文件一般会对应一些数据结构。可以直接将自定义的数据结构存储到文件中,读写都很方便,而且在读写的过程中,可以很轻松地添加加密解密的操作,安全性高,效率也快,不需要像前面的其他格式,逐个解析数据,而是直接取出内存,然后结构体强制转换,直接使用。由于是自定义的格式,所以可以自定义各种后缀,如以dat结尾,或者以sys结尾都可以。

首先来了解一下简单数据结构读写,现在定义一个数据结构,来存储玩家的名字、等级、金币、经验等信息。注意,数组必须是固定长度的,如果是不固定长度的数组,可以参考下面读取动态数据结构的方法。

        //定义玩家信息结构体
        struct PlayerInfo
        {
            char Name[32];
            int Level;
            int Money;
            int Exp;
        };

        //填充这个结构体
        PlayerInfo info;
        memset(&info, 0, sizeof(PlayerInfo));
        strncpy(info.Name, "BaoYe", sizeof(info.Name));
        info.Level = 10;
        info.Money = 888;
        info.Exp = 0;

        //注意这里是getWritablePath,获取一个可写的路径
        string path = FileUtils::getInstance()->getWritablePath();
        path.append("user.dat");

        //文件打开的方式是wb二进制方式写入
        FILE* fd = fopen(path.c_str(), "wb");
        if (NULL == fd)
        {
            return false;
        }

        //写入文件并关闭
        int count = fwrite((char*)&info, 1, sizeof(PlayerInfo), fd);
        fclose(fd);
        CCLOG("Write File %s\n Size %d", path.c_str(), count);

上面的代码将信息写入到文件保存起来了,接下来将其读出来。

        string path = FileUtils::getInstance()->getWritablePath();
        path.append("user.dat");
        PlayerInfo info;

        //文件打开的方式是rb二进制读取
        FILE* fd = fopen(path.c_str(), "rb");
        if (NULL ! = fd)
        {
            //取出来就可以用了
            fread(reinterpret_cast<char*>(&info), 1, MAX_BUFFER_SIZE, fd);
        }
        CCLOG("Read File %s\n name %s level %d money %d exp %d",
            path.c_str(), info.Name, info.Level, info.Money, info.Money);

接下来看一下动态数据结构读写,我们定义一个背包的数据结构,动态数据的读写会稍微麻烦一些,也比较容易出错,但还是可以轻松搞定的。因为是动态的,所以数据结构尽量简化一些。

        //物品信息结构体
        struct Item
        {
            int id;
            int count;
        };

        char buf[MAX_BUFFER_SIZE];
        //先把背包中物品的总数写入
        *(int*)(buf) = 10;
        //后面的内容是背包中所有物品的信息
        Item* bag = (Item*)(buf + sizeof(int));
        for (int i = 0; i < 10; ++i)
        {
            bag[i].id = i;
            bag[i].count = 3;
        }

        string path = FileUtils::getInstance()->getWritablePath();
        path.append("bag.dat");
        FILE* fd = fopen(path.c_str(), "wb");
        if (NULL == fd)
        {
            return false;
        }
        //写入文件并关闭,写入的长度是动态计算出的内存大小
        //一共写入了1个int和10个Item
        int count = fwrite(buf, 1, sizeof(int)+sizeof(Item)* 10, fd);
        fclose(fd);
        CCLOG("Write File %s\n Size %d", path.c_str(), count);

接下来就是把它读出来!其实在读的时候,应该做一个这样的判断,假设读取失败,说明存档异常,或者是没有存档,这时候应该创建一个默认的存档。

        char buf[MAX_BUFFER_SIZE];

        string path = FileUtils::getInstance()->getWritablePath();
        path.append("bag.dat");
        CCLOG("Read File %s", path.c_str());
        //文件打开的方式是rb二进制读取
        FILE* fd = fopen(path.c_str(), "rb");
        if (NULL ! = fd)
        {
            fread(buf, 1, MAX_BUFFER_SIZE, fd);
            //取出第一个字段,判断有多少个物品
            int count = *(int*)buf;
            CCLOG("Item Count %d", count);
            Item* items = (Item*)(buf + sizeof(int));
            for (int i = 0; i < count; ++i)
            {
                //遍历取出所有的物品
                Item item = items[i];
                CCLOG("Item %d is %d, count %d", i + 1, item.id, item.count);
            }
        }

需要特别注意的一点是,使用fopen()方法打开,必须使用fclose()方法关闭,特别是在需要保存文件时,如果忘记调用fclose()方法,在Windows下不会有问题,但是在iOS下却会导致文件保存失败。对于二进制文件的读写,完全是指针的操作,所以一定要把指针操作搞清楚才行。