常量字符串数组

将一组常量字符串存储到数组中,就具备了迭代与随机访问的优势,这是一种非常典型的应用,如存储一周7天的名称,可以方便的在实现某个计划任务程序中输出表头。

const char *week_names[] =
{
    "Monday",
    "Tuesday",
    "Wednesday",
    "Thursady",
    "Friday",
    "Saturday",
    "Sunday"
};

对于像"Monday"之类的字符串字面量,它是不可修改的,如果需要使用一个名称引用这个字面量,可以定义 const char* 指针指向它。

const char *monday = "Monday";

现在week_names存储的每一个元素都是像"Monday"这样的不可写字面量,所以week_names的定义需要const修饰符。

我的生肖

下面我们借助于常量字符串数组,编写一个根据年份计算生肖的小程序。

#include <stdio.h>

int main()
{
    const char *zodiacs[] =
    {
        "猴", "鸡", "狗", "猪", "鼠", "牛",
        "虎", "兔", "龙", "蛇", "马", "羊"
    };

    const int years[] =
    {
        1954, 1955, 1983, 1987, 1998, 2001, 2010, 2020, 2021
    };

    for (int i = 0; i < sizeof(years) / sizeof(int); i++)
    {
        printf("%d -- %s\n", years[i], zodiacs[years[i] % 12]);
    }

    return 0;
}

glimix.com

数组zodiacs中的排列是有依据的,这对应到年份与12求余的结果;代码 zodiacs[years[i] % 12] 展示了数组套用数组的情形:years[i] % 12计算的结果作为数组zodiacs的索引,以取得对应生肖名称。

ZYF

消息描述

假如我们使用了一个第三方库,当调用库函数时,会返回状态值表示调用的结果,状态码由#define定义的常量组成。

#define ERR_NO          100
#define ERR_COMMAND     101
#define ERR_PERMISSION  102
#define ERR_FATAL       103
#define ERR_UNKNOWN     104

作为应用层,我们希望能给用户显示一个直观的状态提示,如显示消息框提示出错,消息框的内容就是这些状态码的文字描述,为此我们可以定义一个消息数组,与这些状态码逐一对应。

const char *err_msg[] =
{
    "No error",
    "Bad command or file name",
    "No permission",
    "Fatal error",
    "Unknown error"
};

接下来,我们模拟一个该库的函数实现,函数前导名lib指示这个函数位于第三方库中。

int lib_open_file(const char *fname)
{
    int file_exist = 0;

    if (!file_exist)
        return ERR_COMMAND;

    return ERR_NO;
}

然后在我们的客户端应用程序中,使用此函数。

int my_app_task()
{
    // 调用库函数
    int result = lib_open_file("glimix.com");

    // 如果发生错误,就输出文本信息。
    if (result != ERR_NO)
    {
        printf("%s",  err_msg[result - 100]);
        return -1;
    }

    return 0;
}

int main()
{
    my_app_task();
    return 0;
}

glimix.com

嘀嘀嘀嘀 嘀嘀(.... ..)

摩斯电码(Morse code)是莫斯于1844年发明的,由点(.)、划(-)两种符号组成。关于摩斯码的具体知识,我们可以通过搜索引擎获得,下面这张表展示了字母、数字、符号对应的编码。

glimix.com

我们的任务是编写程序将字符串以摩斯码输出,如:

Cat        -.-.  .-  -
2021/9/12  ..---  -----  ..---  .----  -..-.  ----.  -..-.  .----  ..---

首先,我们定义一个全局变量morse_code保存编码,这里依据数字、字母、符号分为三个区。

// 摩斯密码表
const char *morse_code[] =
{
    //===== 数字区
    "-----",        // 0
    ".----",        // 1
    "..---",        // 2
    "...--",        // 3
    "....-",        // 4
    ".....",        // 5
    "-....",        // 6
    "--...",        // 7
    "---..",        // 8
    "----.",        // 9

    //===== 字母区
    ".-",           // A
    "-...",         // B
    "-.-.",         // C
    "-..",          // D
    ".",            // E
    "..-.",         // F
    "--.",          // G
    "....",         // H
    "..",           // I
    ".---",         // J
    "-.-",          // K
    ".-..",         // L
    "--",           // M
    "-.",           // N
    "---",          // O
    ".--.",         // P
    "--.-",         // Q
    ".-.",          // R
    "...",          // S
    "-",            // T
    "..-",          // U
    "...-",         // V
    ".--",          // W
    "-..-",         // X
    "-.--",         // Y
    "--..",         // Z

    //===== 符号区
    ".-.-.-",       // .
    "---...",       // :
    "--..--",       // ,
    "-.-.-.",       // ;
    "..--..",       // ?
    "-...-",        // =
    ".----",        // `
    "-..-.",        // /
    "-.-.--",       // !
    "-....-",       // -
    "..--.-",       // _
    ".-..-.",       // "
    "-.--.",        // (
    "-.--.-",       // )
    "...-..-",      // $
    "....",         // &
    ".--.-.",       // @
};

同时定义三个分区的起始索引量,方便计算字符在morse_code中的索引。

#define SECT_DIGIT      0       // 数字段起始索引
#define SECT_CHAR       10      // 字母段起始索引
#define SECT_SYMBOL     36      // 符号段起始索引

比如,对于字符串"9Hi!",我们按字符分别计算索引:

字符    对应的索引
'9':    index = SECT_DIGIT + ('9' - '0');
'H':    index = SECT_CHAR + ('H' - 'A');

由于摩斯码仅编码了大写字母,而大小写字母的ASCII码值不一样,因此对于小写字母'i',要先将其转换成大写,再计算索引,

char ch = 'i';
ch = 'A' + (ch - 'a');  // 利用ASCII码中字符的关系,将小写转换为大写。
ch = toupper(ch);       // 利用库函数将小写字符转换为大写(优先选用库函数)
index = SECT_CHAR + (ch - 'A');

与数字、字母不同,符号的ASCII码并不连续,之前的方法不再适用;一种方案就是采用分支语句硬编码。

switch (ch)
{
    case '.':   return SECT_SYMBOL + 0;
    case ':':   return SECT_SYMBOL + 1;
    case ',':   return SECT_SYMBOL + 2;
    // ...
}

也可以使用查找策略,每遇到一个符号字符,就调用find_symbol_offset()一次,假定符号字符串的长度是n,那查找某个符号的最坏情况就是循环执行n次,所以这种方案的效率并不高;相对于硬编码,其优点就是,morse_code符号定义的顺序改变,这里改动最小。

int find_symbol_offset(char c)
{
    const char *symbols = ".:,;?=`/!-_\"()$&@";

    for (int i = 0; i < strlen(symbols); i++)
    {
        if (symbols[i] == c)
            return i;
    }

    return -1;
}

也可以将两者结合起来,在效率与维护上达到最优解,这也是我们采用的方案。原理是创建一张符号映射表,当需要知道某个符号的索引时,直接查表即可。

char tab[128]; // 定义一张足以容纳标准ASCII码表的所有字符

void make_symbol_tab()
{
    // 符号顺序与morse_code中定义的一致
    const char *symbol = ".:,;?=`/!-_\"()$&@";

    // 先将表清空,-1代表此处索引无效
    for (int i = 0; i < 128; i++)
        tab[i] = -1;

    // 填充每个符号对应的索引
    for (int i = 0; i < strlen(symbol); i++)
    {
        int index = *(symbol + i); // 取得当前符号的ASCII码
        tab[index] = i;            // 为表中的符号设置索引
    }

    // 这里的循环相当于
    // tab['.'] = 0;
    // tab[':'] = 1;
    // tab[','] = 2;
    // ...
}

最后,你会发现我们忽略了一个问题,那就是如何判断当前字符所属分区,这可以通过 <ctype.h> 提供的库函数解决。

int isdigit(int c): 检查所传的字符是否是十进制数字字符。
void isalpha(int c): 检查所传的字符是否是字母。

将这些组合起来,我们的摩斯编码机就运行起来了。

void print_morse_str(const char *str)
{
    printf("%s \t\t", str);

    for (int i = 0, len = strlen(str); i < len; i++)
    {
        char c = str[i];
        int index = -1;

        if (isdigit(c))
        {
            index = SECT_DIGIT + (c - '0');
        }
        else if (isalpha(c))
        {
            index = SECT_CHAR + (toupper(c) - 'A');
        }
        else
        {
            index = SECT_SYMBOL + tab[c];
        }

        assert(index != -1);

        printf((i != len - 1) ? "%s  " : "%s", morse_code[index]);
    }

    printf("\n");
}

int main()
{
    make_symbol_tab();
    print_morse_str("Hi!");
    print_morse_str("SOS");
    print_morse_str("9/12");
    return 0;
}

glimix.com

ZYF

陕ICP备2025078817号-1 陕公网安备61011202001108号