Упрощение моей программы для преобразования чисел с одной базы на другую

Я беру курс начинающего С++. Я получил задание, в котором мне сообщалось написать программу, которая преобразует произвольное число из любой базы между двоичной и шестнадцатеричной на другую базу между двоичным и шестнадцатеричным. Меня попросили использовать отдельные функции для преобразования в базу и из базы 10. Это должно было помочь нам привыкнуть к использованию массивов. (Мы уже рассматривали передачу по ссылке ранее в классе.) Я уже включил это, но я уверен, что это было не так, как я должен был это сделать:

#include <iostream>
#include <conio.h>
#include <cstring>
#include <cmath>

using std::cout;
using std::cin;
using std::endl;

int to_dec(char value[], int starting_base);
char* from_dec(int value, int ending_base);

int main() {
    char value[30];
    int starting_base;
    int ending_base;
    cout << "This program converts from one base to another, so long as the bases are" << endl
        << "between 2 and 16." << endl
        << endl;
input_numbers:
    cout << "Enter the number, then starting base, then ending base:" << endl;
    cin >> value >> starting_base >> ending_base;
    if (starting_base < 2 || starting_base > 16 || ending_base < 2 || ending_base > 16) {
        cout << "Invalid base(s). ";
        goto input_numbers;
    }
    for (int i=0; value[i]; i++) value[i] = toupper(value[i]);
    cout << "Base " << ending_base << ": " << from_dec(to_dec(value, starting_base), ending_base) << endl
        << "Press any key to exit.";
    getch();
    return 0;
}

int to_dec(char value[], int starting_base) {
    char hex[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
    long int return_value = 0;
    unsigned short int digit = 0;
    for (short int pos = strlen(value)-1; pos > -1; pos--) {
        for (int i=0; i<starting_base; i++) {
            if (hex[i] == value[pos]) {
                return_value+=i*pow((float)starting_base, digit++);
                break;
            }
        }
    }
    return return_value;
}

char* from_dec(int value, int ending_base) {
    char hex[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
    char *return_value = (char *)malloc(30);
    unsigned short int digit = (int)ceil(log10((double)(value+1))/log10((double)ending_base));
    return_value[digit] = 0;
    for (; value != 0; value/=ending_base) return_value[--digit] = hex[value%ending_base];
    return return_value;
}

Я уверен, что это более продвинутый, чем предполагалось. Как вы думаете, как я должен это делать?

Я ищу два варианта ответов:

  • Примерами того, что может быть простое решение, подобное тому, которое, вероятно, ожидал мой учитель, было бы.
  • Предложения по улучшению кода.

Ответ 1

Я не думаю, что вам нужен внутренний цикл:

for (int i=0; i<starting_base; i++) {

В чем его цель?

Скорее, вы должны получить символ в value[ pos ] и преобразовать его в целое число. Преобразование зависит от базы, поэтому лучше сделать это в отдельной функции.

Вы определяете char hex[ 16 ] дважды, один раз в каждой функции. Лучше сделать это только в одном месте.


РЕДАКТИРОВАТЬ 1: Поскольку это "домашнее задание" отмечено, я не могу дать вам полный ответ. Однако, вот пример того, как to_dec() должен работать. (В идеале вы должны были построить это!)

Input:

  char * value = 3012, 
  int base = 4, 

Математика:

Number = 3 * 4^3 + 0 * 4^2 + 1 * 4^1 + 2 * 4^0 = 192 + 0 + 4 + 2 = 198

Ожидаемая работа цикла:

  x = 0
  x = 4x + 3 = 3
  x = 4x + 0 = 12
  x = 4x + 1 = 49
  x = 4x + 2 = 198

  return x;

ИЗМЕНИТЬ 2:

Достаточно честный! Итак, вот еще несколько: -)

Вот эскиз кода. Не скомпилированы или не проверены. Это прямой перевод приведенного выше примера.

unsigned
to_dec( char * inputString, unsigned base )
{
  unsigned rv = 0; // return value
  unsigned c;      // character converted to integer

  for( char * p = inputString; *p; ++p ) // p iterates through the string
  {
    c = *p - hex[0];
    rv = base * rv + c;
  }

  return rv;
}

Ответ 2

Я бы держался подальше от заявлений GOTO, если они абсолютно необходимы. Заявления GOTO просты в использовании, но приведут к "коду спагетти".

Попробуйте использовать цикл. Что-то вроде этого:

bool base_is_invalid = true;
while ( base_is_invalid ) {

    cout << "Enter the number, then starting base, then ending base:" << endl;
    cin >> value >> starting_base >> ending_base;

    if (starting_base < 2 || starting_base > 16 || ending_base < 2 || ending_base > 16)
        cout << "Invalid number. ";
    else
        base_is_invalid = false;

}

Ответ 3

Вы можете инициализировать массивы строковыми литералами (обратите внимание, что завершающий \0 не включен, потому что размер массива не позволяет этого):

char const hex[16] = "0123456789ABCDEF";

Или просто используйте указатель на строковый литерал для того же эффекта:

char const* hex = "0123456789ABCDEF";

Ответ 4

to_dec() выглядит сложным, вот мой снимок:

int to_dec(char* value, int starting_base)
{
    int return_value = 0;
    for (char* cur = value + strlen(value) - 1; cur >= value; cur--) {
        // assuming chars are ascii/utf: 0-9=48-57, A-F=65-70
        // faster than loop
        int inval = *cur - 48;
        if (inval > 9) {
            inval = *cur - 55;
            if (inval > 15) {
                // throw input error
            }
        }
        if (inval < 0) {
            // throw input error
        }
        if (inval >= starting_base) {
            // throw input error
        }
        // now the simple calc
        return_value *= starting_base;
        return_value += inval;
    }
    return return_value;
}

Ответ 5

для начального преобразования из ascii в целое число, вы также можете использовать таблицу поиска (так же, как вы используете обратимое преобразование в обратном направлении), что намного быстрее, чем поиск по массиву для каждой цифры.

 int to_dec(char value[], int starting_base) 
 {
      char asc2BaseTab = {0,1,2,3,4,5,6,7,8,9,-1,-1,-1,-1,-1,-1,-1,10,11,12,13,14,15, //0-9 and A-F (big caps)
                         -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,  //unused ascii chars
                          10,11,12,13,14,15};  //a-f (small caps)

      srcIdx = strlen(value);
      int number=0;
      while((--srcIdx) >= 0)
      {
           number *= starting_base;          
           char asciiDigit = value[srcIdx];
           if(asciiDigit<'0' || asciiDigit>'f') 
           {
                //display input error
           } 
           char digit = asc2BaseTab[asciiDigit - '0'];
           if(digit == -1)
           {
                 //display input error
           }
           number += digit;
      }
      return number;
 }

p.s. оправдания, если в этом есть некоторые ошибки компиляции... Я не мог проверить это... но логика звучит.

Ответ 6

В вашем описании присваивания указано:

"Меня попросили использовать отдельные функции для преобразования в и из базы 10".

Если это действительно то, что учитель хотел и хотел, что сомнительно, ваш код этого не делает:

int to_dec(char value[], int starting_base)

возвращает int, который является двоичным числом.:-) Что, на мой взгляд, имеет больше смысла.

Учитель даже заметил это?

Ответ 7

C и С++ - разные языки и с разными стилями программирования. Лучше не смешивать их. (Где C и С++ отличаются)

Если вы пытаетесь использовать С++, то:

Используйте std::string вместо char * или char [].

int to_dec(string value, int starting_base);
string from_dec(int value, int ending_base);

Нет никаких mallocs, используйте new/delete. Но на самом деле С++ автоматически управляет памятью. Память освобождается, как только переменная выходит за рамки (если вы не имеете дело с указателями). И указатели - это последнее, с чем вам нужно иметь дело.

Здесь нам не нужны таблицы поиска, просто волшебная строка.

string hex = "0123456789ABCDEF";//The index of the letter is its decimal value. A is 10, F is 15.
//usage
char c = 'B';
int value = hex.find( c );//works only with uppercase;

Рефакторизованный to_dec может быть таким.

int to_dec(string value, int starting_base) {
  string hex = "0123456789ABCDEF";
  int result = 0;
  for (int power = 0; power < value.size(); ++power) {
    result += hex.find( value.at(value.size()-power-1) ) * pow((float)starting_base, power);
  }
  return result;
}

И есть более элегантный алгоритм преобразования из базы 10 в любой другой См. там, например. У вас есть возможность самостоятельно его закодировать:)

Ответ 8

В вашей функции from_dec вы преобразуете цифры слева направо. Альтернативой является преобразование справа налево. То есть

std::string from_dec(int n, int base)
{
    std::string result;
    bool is_negative = n < 0;

    if (is_negative)
    {
       n = - n;
    }

    while (n != 0)
    {
        result = DIGITS[n % base] + result;
        n /= base;
    }

    if (is_negative)
    {
        result = '-' + result;
    }

    return result;
}

Таким образом, вам не понадобится функция журнала.

(BTW, to_dec и from_dec являются неточными именами. Ваш компьютер не сохраняет числа в базе 10.)

Ответ 9

Получил этот вопрос на собеседовании один раз и мозг и вращал колеса на некоторое время. Идите фигуру. В любом случае, через пару лет я прохожу через математику и физику для программистов, чтобы расчистить позиции, которые более интенсифицированы по математике, чем то, что я делал. CH1 "назначение" имеет

// Write a function ConvertBase(Number, Base1, Base2) which takes a 
// string or array representing an integer in Base1 and converts it
// into base Base2, returning the new string.

Итак, я принял упомянутый выше подход: я конвертирую строку в произвольную базу в UINT64, а затем конвертирую UINT64 обратно в произвольную базу:

CString ConvertBase(const CString& strNumber, int base1, int base2)
{
   return ValueToBaseString(BaseStringToValue(strNumber, base1), base2);
}

Каждая из подфункций имеет рекурсивное решение. Здесь один пример:

UINT64 BaseStringToValue(const CString& strNumber, int base)
{
    if (strNumber.IsEmpty())
    {
        return 0;
    }

    CString outDigit = strNumber.Right(1);
    UINT64 output = DigitToInt(outDigit[0]);
    CString strRemaining = strNumber.Left(strNumber.GetLength() - 1);
    UINT64 val = BaseStringToValue(strRemaining, base);
    output += val * base;
    return output;
}

Я нахожу другого немного сложнее понять мысленно, но он работает примерно так же.

Я также реализовал DigitToInt и IntToDigit, которые работают так, как они звучат. Кстати, вы можете сделать некоторые быстрые ярлыки, если вы понимаете, что символы - это ints, тогда вам не нужны огромные инструкции switch:

int DigitToInt(wchar_t cDigit)
{
    cDigit = toupper(cDigit);
    if (cDigit >= '0' && cDigit <= '9')
    {
        return cDigit - '0';
    }
    return cDigit - 'A' + 10;
}

и модульные тесты действительно ваш друг здесь:

typedef struct 
{
    CString number;
    int base1;
    int base2;
    CString answer;
} Input;

Input input[] =
{ 
    { "345678", 10, 16, "5464E"},
    { "FAE211", 16, 8, "76561021" },
    { "FAE211", 16, 2, "111110101110001000010001"},
    { "110110111", 2, 10, "439" } 
}; 

(snip)

for (int i = 0 ; i < sizeof(input) / sizeof(input[0]) ; i++)
{
    CString result = ConvertBase(input[i].number, input[i].base1, input[i].base2);
    printf("%S in base %d is %S in base %d (%S expected - %s)\n", (const WCHAR*)input[i].number, 
                                               input[i].base1, 
                                               (const WCHAR*) result, 
                                               input[i].base2,
                                               (const WCHAR*) input[i].answer,
                                               result == input[i].answer ? "CORRECT" : "WRONG");
}

И вот вывод:

345678 in base 10 is 5464E in base 16 (5464E expected - CORRECT)
FAE211 in base 16 is 76561021 in base 8 (76561021 expected - CORRECT)
FAE211 in base 16 is 111110101110001000010001 in base 2 (111110101110001000010001 expected - CORRECT)
110110111 in base 2 is 439 in base 10 (439 expected - CORRECT)

Теперь я взял несколько ярлыков в кодировании с использованием типов CString и т.д. Я не обращал внимания на эффективность или производительность, я просто хотел решить алгоритм с самым простым кодированием.

Это может помочь понять, как эти алгоритмы являются рекурсивными, если вы пишете их так: Скажем, вы хотите определить "значение" "строки" B4A3, которая находится в базе 13. Вы знаете это 3 + 13 (A ) + 13 (13) (4) + 13 (13) (13) (B) Другой способ записи: 0 + 3 + 13 (A + 13 (4 + 13 (B))) - и вуаля! Рекурсия.

Ответ 10

Помимо упомянутых вещей, я предлагаю использовать вместо оператора новый оператор. Преимущества нового в том, что он также вызывает конструкторы вызовов, что здесь не имеет значения, поскольку вы используете тип POD, но важно, когда речь заходит о таких объектах, как std::string или ваших собственных пользовательских классах, и что вы можете перегрузить новый оператора в соответствии с вашими конкретными потребностями (что тоже не имеет значения: p). Но не следует использовать malloc для POD и новый для классов, поскольку их смешивание считается плохим.

Но хорошо, у вас есть куча памяти в from_dec... но где он снова освобождается? Основное правило: память, которую вы malloc (или calloc и т.д.), Должна быть передана бесплатно в какой-то момент. Это же правило применяется к новому оператору, так как оператор release называется delete. Обратите внимание, что для массивов вам нужно новое [] и удалить []. НЕ назначайте новые и освобождайте с помощью delete [] или наоборот, так как память не будет выпущена правильно.

Ничего плохого не произойдет, когда ваша игрушечная программа не выпустит память... Я думаю, ваш компьютер получил достаточное количество оперативной памяти, чтобы справиться с ней, и когда вы завершаете свою программу, ОС освобождает память в любом случае.. но не все программы (а) крошечные и (б) часто закрываются.

Также я бы избегал conio.h, так как это не переносимо. Вы не используете самый сложный IO, поэтому должны использоваться стандартные заголовки (iostream и т.д.).

Аналогично, я думаю, что большинство программистов, использующих современные языки, следуют правилу "Используйте только goto, если другие решения действительно искалечены или больше работы". Это ситуация, которую можно легко решить, используя циклы, как показано emceefly. В вашей программе goto прост в обращении, но вы не будете писать такие небольшие программы навсегда, не так ли?;) Я, например, недавно был представлен некоторым устаревшим кодом.. 2000 строк goto-littered code, yay! Попытка следовать логическому потоку кода была почти невозможна ( "О, прыгай вперед на 200 строк, отлично... кому нужен контекст в любом случае" ), еще труднее было переписать эту проклятую вещь.

Так хорошо, ваш goto здесь не болит, но где польза? 2-3 линии короче? Совсем неважно, в целом (если вам платят строки кода, это также может быть серьезным недостатком;)). Лично я считаю, что версия цикла более читабельна и чиста.

Как вы видите, большинство пунктов здесь можно легко игнорировать для вашей программы, так как это игрушечная программа. Но когда вы думаете о более крупных программах, они имеют больше смысла (надеюсь);)