在我观看的YouTube视频中,发言人说,了解如何在不同基数之间进行计算非常重要;即2、10和16。然后,我受到启发编写了一个小型C程序,以确保我了解如何完成此类转换。然后,我编写了一个简短的Python2脚本来测试程序。我希望获得有关编码风格,布局,效率以及其他任何突出方面的建设性反馈。调试行已注释掉。

calc.c

#include <math.h>
#include <stdio.h>
#include <string.h>

typedef enum {
    AND = '&',
    OR = '|',
    XOR = '^',
    NONE = 0
} operator;

typedef enum {
    B02 = 'b',
    B10 = 'd',
    B16 = 'x'
} type;

const char zero = '0';
const char _A = 'A';
const char _a = 'a';

int main(int argc, char **argv) {
    int i, offs, len;
    char *nend;
    long result = 0, tmp, optmp;
    operator op = NONE;
    type ntype;

    for (i = 1; i < argc; i++) {
        len = strlen(argv[i]);
        nend = argv[i] + len - 1;
        if (len == 1) {
            op = *nend;
            continue;
        }
        ntype = *nend;
        nend--;
        offs = 0;
        optmp = 0;
        while (offs < len - 1) {
            switch (ntype) {
            case B02:
                tmp = (*(nend - offs) - zero) * pow(2, offs);
                break;
            case B10:
                tmp = (*(nend - offs) - zero) * pow(10, offs);
                break;
            case B16:
                tmp = *(nend - offs);
                if (tmp >= _a) tmp -= _a - 10;
                else if (tmp >= _A) tmp -= _A - 10;
                else tmp -= zero;
                tmp *= pow(16, offs);
                break;
            }
            optmp += tmp;
            offs++;

        }
//printf("%s: offs: %i tmp: %li optmp: %li len: %i result: %li\n", argv[i], offs, tmp, optmp, len, result);
        switch (op) {
        case AND:
            result &= optmp;
            break;
        case OR:
            result |= optmp;
            break;
        case XOR:
            result ^= optmp;
            break;
        case NONE:
            result += optmp;
            break;
        }
//printf("\n");
    }
    printf("%li\n", result);
    return 0;
}


makecalc.sh

gcc -Wall calc.c -o calc


testcalc.py

import subprocess as subp
import time

_bin = lambda x: bin(x)[2:] + 'b'
_dec = lambda x: str(x) + 'd'
_hex = lambda x: hex(x)[2:] + 'x'

ops = ('&', '|', '^')
types = (_bin, _dec, _hex)
RANGE = 20

tmp = None

def test(op, t, x, y):
    call = ["./calc", t(x), op, t(y)]
    print call
    proc = subp.Popen(call, stdout=subp.PIPE, stdin=subp.PIPE)
    data = proc.stdout.readline().replace("\n", "")
    exec("tmp = %s %c %s;" % (str(x), op, str(y)))
    if tmp != int(data):
        print ("fail", tmp, data)
        quit()

for op in ops:
    for t in types:
        for x in range(RANGE):
            for y in range(RANGE):
                test(op, t, x, y)


#1 楼

我想您不会缩进printf调试行以使它们更容易发现。这是一个明智的想法(尽管我以前从未见过)。但是,我将“代码审查”答案视为“就在签入之前”,因此建议在这一点上删除这些答案。

您需要预先声明变量。不要这样

将它们写在尽可能接近使用点的位置。

您的NONE op似乎没什么用,您可以直接使用OR。因此,将其删除。

您的

const char zero = '0';
const char _A = 'A';
const char _a = 'a';


变量似乎是对“无魔法常数”的良好依从性,但实际上它们可以有效地没有有用的目的。如果要更新代码以支持其他字符集,则无论如何都要重新设计整个解析器。恕我直言,内联这些代码更简单。

现代C语言中不需要return 0;,而且在某些情况下通常不需要。

您的整数解析器应该在基础。我建议将其提取到类似int parse_digit(char character, int base, int place)的函数中:

int parse_digit(char character, int base, int place) {
    int value;

    if (character >= 'a') {
        value = character - 'a' + 10;
    }
    else if (character >= 'A') {
        value = character - 'A' + 10;
    }
    else {
        value = character - '0';
    }

    return value * pow(base, place);
}

while (offs < len - 1) {
    optmp += parse_digit(*(nend - offs), base, offs);
    offs++;
}


base可以通过在ntype上进行简单查找来计算: >
尽管这使type看起来很傻:毕竟这只是一个整数!

int type_to_base(type ntype) {
    switch (ntype) {
    case B02: return 2;
    case B10: return 10;
    case B16: return 16;
    }
}


请注意,这是一种愚蠢的事情不过。首先,enum需要pow s!如果可能,我宁愿避免这种情况。此外,这是需要完成的工作。相反,可以执行以下操作:

int flag_to_base(char flag) {
    switch (flag) {
    case 'b': return 2;
    case 'd': return 10;
    case 'x': return 16;
    }
}


这可以通过以下方式完成:

  1*base^4 + 0*base^3 + 1*base^2 + 0*base^1 + 1*base^0  // how you're currently doing it
= (((1 * base + 0) * base + 1) * base + 0) * base + 1          // simpler way to do it


解析整数应该在另一个函数中,可以稍微简化一下:

int parse_digit(char character) {
    if (character >= 'a') {
        return character - 'a' + 10;
    }
    else if (character >= 'A') {
        return character - 'A' + 10;
    }
    else {
        return character - '0';
    }
}

while (offs < len - 1) {
    optmp *= base;
    optmp += parse_digit(*(nend - offs));
    offs++;
}


我还将把double的应用程序放到一个函数中,并停止调用optmp(通常,大多数变量是临时的-说明这毫无意义):

long parse_int(char *start, char *end) {
    int base = flag_to_base(*end);

    long ret = 0;
    for (char *digit = start; digit < end; digit++) {
        ret *= base;
        ret += parse_digit(*digit);
    }

    return ret;
}


然后,我会(通常)将optmp更改为long,将uint64_t更改为int(甚至uint32_t)-在这种时代,没有必要为这些类型使用可变长度整数,并且您不需要带符号的值。当您确实想要可变长度值时,您可能宁愿坚持使用uint8_t。 (如果发现这些问题比较笨拙,请size_t替换为typedefu64u32。只是不要使用u8int,因为它们更漂亮。)
long apply(operator op, long left, long right) {
    switch (op) {
    case AND: return left & right;
    case OR:  return left | right;
    case XOR: return left ^ right;
    }
}


这是因为新方法可以进行更好的案例分析-我们忘记了检查此类操作是否有效!

calc.c: In function ‘apply’:
calc.c:18:1: warning: control reaches end of non-void function [-Wreturn-type]
 }
 ^
calc.c: In function ‘flag_to_base’:
calc.c:26:1: warning: control reaches end of non-void function [-Wreturn-type]
 }
 ^


请注意,在将枚举转换为可能导致“无效”枚举的枚举时,我会有所警惕,但至少它是定义良好的行为。


对于Python ,您的

#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

typedef enum {
    AND = '&',
    OR = '|',
    XOR = '^',
} operator;

int apply(operator op, uint64_t *left, uint64_t right) {
    switch (op) {
    case AND: *left &= right; break;
    case OR:  *left |= right; break;
    case XOR: *left ^= right; break;
    default: return -1;
    }
    return 0;
}

int flag_to_base(char flag, uint8_t *base) {
    switch (flag) {
    case 'b': *base = 2; break;
    case 'd': *base = 10; break;
    case 'x': *base = 16; break;
    default: return -1;
    }
    return 0;
}

int parse_digit(char digit, uint8_t *value) {
    if ('a' <= digit && digit <= 'z') {
        *value = digit - 'a' + 10;
    }
    else if ('A' <= digit && digit <= 'Z') {
        *value = digit - 'A' + 10;
    }
    else if ('0' <= digit && digit <= '9') {
        *value = digit - '0';
    }
    else {
        return -1;
    }

    return 0;
}

int parse_int(uint64_t *ret, char *start, char *end) {
    uint8_t base;
    if (flag_to_base(*end, &base)) {
        return -1;
    }

    *ret = 0;
    for (char *digit = start; digit < end; digit++) {
        uint8_t value;
        if (parse_digit(*digit, &value) || value >= base) {
            return -1;
        }

        *ret *= base;
        *ret += value;
    }

    return 0;
}

int main(int argc, char **argv) {
    uint64_t result = 0;
    operator op = OR;

    for (int i = 1; i < argc; i++) {
        size_t len = strlen(argv[i]);
        char *end = argv[i] + len - 1;

        if (len == 1) {
            op = *end;
            continue;
        }

        uint64_t operand;
        if (parse_int(&operand, argv[i], end)) {
            printf("Can't parse argument '%s'\n", argv[i]);
            return -1;
        }

        if (apply(op, &result, operand)) {
            printf("Can't parse argument '%c'\n", op);
            return -1;
        }
    }

    printf("%li\n", result);
}


就可以了

_bin = lambda x: bin(x)[2:] + 'b'
_dec = lambda x: str(x) + 'd'
_hex = lambda x: hex(x)[2:] + 'x'


您的嵌入式循环可以简化为

_bin = "{:b}b".format
_dec = "{:d}d".format
_hex = "{:x}x".format


您不使用long-将其删除。我将直接导入timesubprocess而不是将subp重命名为Popen。请使用新的格式设置!另外,请使用PIPE而不是eval-最好不要使用exec,尽管在这种情况下使用它是可以理解的(即使仍然令人不安)。解释器的用法。在这种情况下,我实际上建议仅quit

您的子流程调用仅需要输出,因此删除assert。此外,您可以致电stdin=PIPE

from itertools import product
for args in product(ops, types, range(RANGE), range(RANGE)):
    test(*args)


评论


\ $ \ begingroup \ $
好答案!您能否详细说明不使用可变长度类型?听起来很有趣,但是我什么都没看完。
\ $ \ endgroup \ $
–杰瓦
15年6月14日在20:48

\ $ \ begingroup \ $
@jacwah int和long的大小(以及它们可以容纳的值)取决于平台。另一方面,int32_t始终为32位长。使用int只是意味着您的代码在不同平台上的工作方式不同,通常这不是您想要的。
\ $ \ endgroup \ $
–Veedrac
15年6月14日在21:05

\ $ \ begingroup \ $
“您预先声明了变量。请不要这样做...”您能否详细说明这种偏好?
\ $ \ endgroup \ $
–motoku
15年6月14日在22:45

\ $ \ begingroup \ $
是的。我在问为什么选择它是首选,因为我的教练以其他方式教了我。
\ $ \ endgroup \ $
–motoku
15年6月14日在22:57

\ $ \ begingroup \ $
@MotokoKusanagi的想法是最小化变量的范围,这也可以最大程度地减少实时变量的数量。通过减少交叉变量的交互作用,可以提高灵活性,并且通常更易于阅读。互联网对此也有一些疑问。
\ $ \ endgroup \ $
–Veedrac
15年6月15日在1:10

#2 楼

这写得很好,很高兴阅读。
我的大部分评论都将涉及可用性和可读性。

NONE运算符


typedef enum {
    AND = '&',
    OR = '|',
    XOR = '^',
    NONE = 0
} operator;



NONE有点在那里:它不是运算符。
让我想知道它的用法。
结果,
如果运算符是NONE
然后计算器用+进行加法。

因此,最好将其更改为ADD = '+'
这样,一切都会变得很清楚,
为什么将加法默认设置为
,但这不是一个大问题。

输入验证

分配运算符时,
验证输入。例如,如果您运行以下命令:


./calc 5d x 4d 6d



在我的计算机上,我得到5作为输出,没有任何意义。
当然会发生什么,
默认的运算符是加法运算,就像我们之前看到的那样,
所以5被添加到q43120 79q,
之后将不再进行任何操作,因为result的值为op,它将与任何'x'语句都不匹配。

用户友好的操作类似地,操作数也不会得到验证,
也可能导致意外结果。
再次,崩溃将导致错误。更好。


case语句应具有switch大小写

通常建议default语句应具有switch大小写。
您的两个default语句不具有如果没有,
它会使读者猜测如果没有大小写匹配的情况,程序将会发生什么。

命名比原始图片稍长:

typedef enum {
    BASE_2 = 'b',
    BASE_10 = 'd',
    BASE_16 = 'x'
} type;


或者:

typedef enum {
    BINARY = 'b',
    DECIMAL = 'd',  // or DENARY
    HEXADECIMAL = 'x'
} type;



一个其中的一个与众不同:


const char zero = '0';
const char _A = 'A';
const char _a = 'a';



我正在看switch
您可以轻松地使它看起来像其他样式:

const char _0 = '0';


另一件事,
我跳过了阅读zero元素,
/>,然后在代码的中间,当我看到enumzero_a时,
并不清楚它们是什么。
这可能是个问题,
但是我实际上会更喜欢_A'0''a'而不是命名的常量。
*(nend - offs)



我会在'A'之前给它起一个名字,然后在以下情况下重新使用它:

char c = *(nend - offs);


易用性

我对此有一个小小的可用性抱怨,那就是在shell中运行程序时,我必须转义一些运算符,例如:

./calc 5d '&' 9d


虽然不确定什么是一个好的解决方案。只是让您知道。

另一件事,
我注意到在Python代码中,您将caseswitch表示法转换为自己的0xNUM0bNUM表示法。
如何重用Python的?至少您会遵循一些标准,而不是发明新的东西。
使用用户熟悉的现有东西的好处。
我做的第一件事是测试了您的代码而没有阅读,
而当NUMx没有提供我期望的结果时,我有点沮丧。

Python 2?

为什么Python 2?为什么不使用Python 3?

顺便说一句,您的NUMb语句之一使用括号(因此可以用于Python 3),而另一个则不需要。同时为Python 3做好准备将是一件好事。

评论


\ $ \ begingroup \ $
NONE的“运算符”可以在参数序列中放置第一个数字。感谢您的评论。
\ $ \ endgroup \ $
–motoku
2015年6月14日在18:47



\ $ \ begingroup \ $
您在这里指出的大多数问题都是我考虑过的;但没有这么详细。我的想法是我不想弄乱代码。但是您已经明确了有效性检查的必要性。我也在考虑将strcmp与运算符一起使用,例如AND或XOR(不再有转义字符)。
\ $ \ endgroup \ $
–motoku
15年6月14日在19:08

#3 楼

我肯定会将主要功能分解为几个较小的功能,并为这些功能找到最好的名称。 int convert(const char* str, type in_type)bin2hex(...)hex2bin(...)dec2bin(...)int operator_apply(int a, int b, operator op)

这将使main()更加易于浏览,并且可以通过不同步骤进行自我记录(具有良好的标识符)。现在,所有参数解析,转换,操作都交织在一起,难以阅读。