решение домашнего задания с предыдущей лекцииУсловие:
Напишите 2 функции:
1. функция:
bool ascii_to_int (char * num_str, int & num);
Принимает стринг, в котором написано число и переводит его в тип "целое число" (int). Ответ "возвращается" через параметр num, а функция возвращает bool (правда - если перевод удался, ложь - если полученный стринг не является числом)
Примечание:
стринг "vasya123" - функция НЕ переведет в число, потому что стринг НЕ начинается с чисел (вернется ложь)
стринг "123vasya58" - функция переведет в число 123 и функция вернет "правда". (остаток "vasya58" - будет игнорирован)
стринг " 54" (с пробелами впереди) - будет успешно переведен в число 54
проверьте работу функции на следующих стрингах:
1. "-57" - удачно
2. "348str236" - удачно
3. "@12" - не удачно
4. "c++" - не удачно
5. " -238" - (с пробелами впереди) удачно
2. функция:
void int_to_ascii (int num, char * buf, unsigned int buf_len);
Принимает целое число через параметр num. Переводит его в стринг и ответ кладет в буфер buf.
Проверьте работу функции на числах:
1. -578
2. 33
3. 0
решениеКод:
Результат:
Пояснения:
Да..... ну во-первых, хочу признать: я немного отжег с домашним заданием))))) Сниму шляпу перед тем, у кого все получилось! Но... "не все коту масленица", пора расти)))
Задача была действительно сложной для этого уровня знаний, это правда. Особенно сложной была функция "int_to_ascii", думаю, что ее "сестра", "ascii_to_int" пошла НАМНОГО проще)))
Но, дело сделано и остался только "разбор полетов".
Для начала, внимание на операцию define. Теперь все константы я буду объявлять так. В этой константе прописан размер буфера, в который я пишу результаты из функции int_to_ascii.
В функции main нет ничего, что стоило бы объяснять, более того, писать ее было не обязательно. Я написал ее, чтобы просто проверить результаты. Праздник начинается ниже!
Разберем, в начале функцию ascii_to_int
Я получаю число, написанное в стринге и должен превратить его в int. Что я делаю? Допустим, я получил стринг: "-236", это выглядит так:
стринг из 5-ти ячеек, а именно:
ячейка 0 содержит знак "минус"
ячейка 1 содержит символ 2 (в ascii)
ячейка 2 содержит символ 3 (в ascii)
ячейка 3 содержит символ 6 (в ascii)
ячейка 4 содержит символ '\0' (конец строки)
Первым делом я обнулю ссылку, в которую я должен положить результат. Я делаю это потому, что мой алгоритм построен так, что числа прибавляются к существующим, поэтому, я обязан убедиться, что изначально там 0, иначе, если функция получила num равный 12, например.... и стринг, в котором написано "58", то результатом ее работы будет 1258, а НЕ 58, как было задумано. Почему это так, мы разберем ниже.
условие в блоке for построено так, что в блок зайдет только стринг, начинающийся с "легальных" символов, а именно: это символы цифр в ascii, пробел и знак минус. Как только в стринге появляется что-то другое - цикл заканчивается. То есть, если с самого начала там была буква, то цикл не сделает ни шагу! Аналогично, цикл закончится, если встретит символ '\0' (конец строки). Как бы "двух зайцев - одним выстрелом".
В самом начале я проверяю, не является ли конкретный символ пробелом? Если да, то я проверю, где она находится: если в начале строки, то это номально и я просто его промотаю, если в середине или в конце, то мое число на нем закончилось. Как я это проверю?
Если до сих пор я не встретил ни одну цифру, то я - в "первом" положении, иначе - я во "втором" положении. Как я знаю, встретил ли я цифру или нет? Если бы я встретил цифру, то ret был бы "правдой", так как только я встречаю цифру - я выставляю в него true, то есть: конвертация удалась.
В итоге, стринг "2 пробел 5" будет переведен как 2, а не как 25. Так как между этим стрингом и стрингом "2vasya5" нет никакой разницы.
Далее я проверяю не является ли символ знаком минус? Тут похожая логика: минус в середине числа не должен учитываться, то есть, стринг "23-567" должен быть переведенным как 23, а не как -23567. Однако, минус, поставленный в начале я запомню, чтобы сделать число негативным.
Если мы миновали эти два блока if, то мы однозначно смотрим на цифру в стринге. В нашем примере мы пришли к первой цифре и это "2". В первую очередь я выставляю true в переменную ret, так как стринг УЖЕ легальный. После, я получаю эту цифру и храню ее в переменной cur_ch. Далее мне нужно "перевести" ее из "2", написанное в ascii в реальную 2. Что я делаю? Я просто отнимаю от нее "0", написанный в ascii!
Взглянем на таблицу ascii. "0" - это 48, а "2" - это 50. То есть: 50 - 48 = 2 ))))
Почему я могу хранить результат в int? Я же раньше говорил, что char минус char будет char. НО! Почему так? Потому, что если мы пытаемся сохранить int в char, то будет проблема: int хранится в 2-х байтах или в 4-х, а вот char - меньше, ему достаточно одного. Поэтому, при переводе int в char есть опасность потерять информацию, однако - при переходе на оборот: char в int - такой опасности нет!
Отлично, далее я умножаю то, что было в num на 10. Сейчас - это лишнее действие, так как в num лежит ноль (помните, я выше говорил, что мне было очень важно обнулить num? Вот сейчас мы отчетливо видим причины этого), а потом прибавляю к num нашу цифру 2. В итоге в num сейчас лежит двойка.
Мы идем на новый шаг цикла! Так как следующий символ, на который мы смотрим - "3", то он проходит проверку для входа в блок. Он пропускает 2 первых if и попадает в код дальше. Выходит, что тут я совершаю лишнее действие и опять выставляю ret = true;, но так как подобные операции "дешевы" с точки зрения времени выполнения, то это не критично. Как и в прошлый раз, я получаю цифру, действием 51 - 48 = 3
Вот теперь становится более интересно! У нас в num лежит 2. НО! Так как за ней идет 3, то 2 - это не "единицы", это - "десятки" или больше! Я умножаю 2 на 10 и получаю 20, потом прибавляю 3 и получаю 23.
На следующем шаге, с цифрой "6" я умножу 23 на 10 и получу 230, потом прибавлю 6 и получу 236. Ну, а следующий символ уже остановит цикл, так как это символ '\0' и он не пройдет проверку для входа в блок.
Каков результат? У нас было число "-236" (отрицательное), но пока наш num равняется 236 (положительное). Но помните, что в блоке for, во втором if я запомнил: встречал ли я минус перед числом? Я положил "да, встречал" в переменную negative. Самое время это использовать! Я проверю переменную negative и если она - правда, то я сделаю положительное число отрицательным, умножив его на -1.
На этом работа закончена, функция вернет "правда", то есть - перевод удался и параметр num уже будет содержать нужное нам число!
Надеюсь, не взорвал вам мозг))) Ну, а если еще остались силы, все понятно и не надо попить чаю, чтобы остыть, то самое время браться за "вкусненькое"!
функция int_to_ascii
Как мы видим, я разделил работу функции на 2 основные части:
1. Обработка числа ноль
2. Обработка числа, отличного от нуля
С нулем все понятно: я ставлю на первое место символ "0", а на второе символ конца строки - '\0'.
Примечание: чтобы уж совсем по-хорошему, стоило возвращать из функции bool, на тот случай, чтобы вернуть false, если буфер не достаточного размера. Но я подумал об этом уже потом, когда успел написать задание. И поэтому, если есть такие читатели, кто выполняет все в режиме "реального времени", то есть сразу, как только я выкладываю задания, изменить условие было бы не честно. Когда мы будем использовать эту функцию, желающие смогут внести простые изменения, чтобы обезопасить себя.
Перейдем к случаю, когда я получил число, неравное нулю. Возьмем опять пример -236.
Я создаю стринг, который содержит цифры, совпадающие с номером ячейки. То есть, в нулевой ячейке лежит "0" (в ascii, так как это - стринг), в первой - "1" и так далее. Кроме этого, я создаю переменную, которая сохранит информацию "является ли число отрицательным" (negative) и переменную i, которая будет мне помогать как в циклах, так и вне их.
Примечание: во время своей учебы, я встречал людей, которые начинали писать код с того, что пытались угадать какие именно переменные им будут нужны. При этом, уходило много времени зря, так как переменную всегда можно добавить или убрать потом. Начинайте писать! Не тупите в монитор, вы зря теряете время! Какие переменные вам нужны, вы поймете в процессе написания программы.
Первым делом, я проверяю: если число негативное, то я сделаю его позитивным, но запомню этот факт! Перевод числа в позитивное мне необходим, так как цикл ниже не умеет работать с негативными результатами, потому что все завязано на массиве digits, а именно: на номере ячейки.
После я попадаю в цикл for. Мне нужно каким-то образом "вычленить" цифру из заданного числа. Как это можно сделать? Самый простой вариант, который пришел мне в голову - это сделать операцию "по модулю". Напомню что это такое (мы проходили):
Операция 58 по модулю 10 даст результат 8. Потому что операция возвращает ответ: чему равен остаток, при делении 58 на 10? 58 можно поделить 5 раз на целые 10 (то есть 50), а остаток будет равен 8, так как он на 10 целиком не делится. Ранее мы применяли операцию "по модулю" только когда хотели узнать: четное число или нет? То есть брали его по модулю 2.
Напомню, что число, которое мы взяли за пример: -236. На предыдущем шаге мы изменили ему знак и теперь оно равно: 236. То есть, 236 по модулю 10 даст мне цифру 6. Этот результат я использую как номер ячейки массива digits и получаю символ "6" уже в ascii!
Далее я ставлю проверку, является ли исходное число равным нулю? Пока что - эта проверка лишняя, так как мы пока только на первом шаге, а над нулем я поработал отдельно (теперь понятно, почему). Если бы это не было так, то тут цикл бы прервался и я бы не получил возможность обработать ноль.
После этого я делю 236 на 10 и получаю 23.6, а так, как результат хранится в int, то дробная часть: 0.6 - выбрасывается, то есть, num теперь равен 23. Теперь я просто сохраняю ранее полученное "6" в буфер, на нулевое место и захожу в цикл еще раз.
Получив похожим образом символ "3", я проверяю: является ли num равным нулю? Нет, он равен 23. Отлично. Делю, получаю 2.3, то есть 2, без дробной части. Запоминаю "3" в первом месте буфера и возвращаюсь в цикл с 2. Получаю символ "2", проверяю, что 2 не равно нулю и делю на 10. Мое число теперь равно 0.2, то есть - 0. Я сохраняю "2" во второй ячейке буфера, а при следующем заходе, во время проверки if ( num <= 0 ), я покину цикл.
Далее, если число было отрицательным, то я ставлю знак "минус" в последнюю ячейку. Обратите внимание на трюк! Я делаю buf [ i++ ], то есть: "верни мне ячейку из buf, равную i (по номеру ячейки), а после этого увеличь i на один. Если бы я написал buf [ ++i ], то результат был бы другой. Думаю, что все помнят причины этого.
В конце, я ставлю знак "конец строки".
Что мы имеем? У нас было число: -236, но в нашем буфере лежит его "зеркальное отражение", то есть: "632-". Вывод: нам осталось только перевернуть стринг! Этим я и займусь в следующем цикле.
Для этого, я объявляю дополнительную переменную, которая должна указывать на конец строки. Если i сейчас равен 4 и указывает на '\0', то мне нужно положить в переменную num_size 3, чтобы указывать на "-". После этого я обнулю i и он станет указателем на нулевую ячейку, то есть на "6".
Смысл такой: я буду менять местами первую и последнюю цифру, все время двигая свои "указатели" на встречу друг другу. И когда num_size станет меньше, чем i, я буду знать, что "указатели" встретились и работа закончена.
На первом шаге i равен 0 и указывает на "6", num_size равен 3 и указывает на "-". Поменяем их местами!
Я создаю дополнительную переменную tmp, в которую кладу "6", а на место "6", кладу знак "-". Теперь наш буфер выглядит так: "-32-". Самое время положить на первоначальное место знака "-" символ "6" из переменной tmp. Такой трюк называется "свопинг" (англ. "swap"). Результат: "-326".
Увеличим i и уменьшим num_size. Теперь i равен 1 и указывает на "3", num_size равен 2 и указывает на "2". Поменяем и их местами! После выполнения процедуры свопинга, наша строка выглядит так: "-236", то есть: мы добились результата! Поздравляю!
Увеличим i и уменьшим num_size. Теперь i равен 2, а num_size равен 1, то есть - цикл закончился!
Примечание: Если число было бы с нечетным количеством символов, то на последнем этапе i и num_size были бы равны и как результат мы бы поменяли символ "сам с собой", то есть - ничего бы не испортили. Но, это конечно лишнее действие и я только потом это заметил, но уже не стал менять, так как это дешевое и некритичное упущение.
Послесловие:
Это было тяжелым заданием и скорее всего - подавляющее большинство не справилось, НО! Это - не беда! Мы только учимся. Если вы пробовали и думали над проблемой, то вы уже сделали половину пути! Теперь, видя пример, нужно поступить так: нужно ОСНОВАТЕЛЬНО разобраться в решении, предложенным мною и постараться потом "по памяти" переписать это задание к себе.
Если вы смогли это сделать - вы поняли решение и в следующий раз вы сможете решить подобную задачу. Кроме того, поняв мое решение вы научились многим полезным трюкам, а значит - стали лучше уметь программировать! Неудачи не должны выбивать из колеи: на них учатся! Посветите разбору решения еще нужное количество времени и можно переходить к новому материалу. Удачи!
лекцияСуществует надобность создавать переменные в куче. Мы рассмотрим один такой случай, но для начала я напомню, что мы говорили про стек и кучу, (глава 6, Память) и там я пообещал, что научу этому позже. До этого момента, все переменные, которые мы создавали - были созданы в стеке.
Немного теории. Переменная, которую надо создать в куче, создают, используя указатель на нее и делают это через оператор new:
int * p = new int;
Я создал переменную в куче. Я как бы "занял" память у операционной системы "в долг". Как известно, долги нужно отдавать, поэтому, когда переменная мне уже не нужна, я должен сказать операционной системе: "вот, спасибо за переменную, я ею пользовался, но больше мне не нужно, забери себе". Делаю я это операцией delete:
delete p;
Как только мы выделяем переменную в куче, операционная система отдает нам на нее все права и больше за ней не смотрит. Она смотрит только за памятью в стеке. То есть, если я создам ее в блоке, а далее передам ее адрес, то покинув блок, место, на которое указывает этот адрес - освобождено НЕ будет. Мы можем пользоваться этой переменной, сколько захотим, пока ее не отдадим назад.
Точно также, мы можем создать массив:
int * arr = new int [ 15 ];
а вернуть системе (далее я буду употреблять слово "удалить", либо слово "освободить") нужно так:
delete [ ] n;
То есть при создании, мы в квадратные скобки помещаем число, чтобы сказать, сколько ячеек выделить, а при удалении, лишь "напоминаем" системе, что это был массив, чтобы она знала, как его правильно удалить.
Примечание:Помните, мы говорили, что нельзя создать массив так:
int n = 0; // создал переменную, в которую я получу размер массива
cin >> n; // получил размер
int arr [ n ] ; //не будет работать, так как в скобках должна быть константа!
Но здесь этого ограничения не существует! Поэтому, мы вдруг получаем возможность создавать массивы, у которых мы заранее не знаем размеров!
int n = 0; // создал переменную, в которую я получу размер массива
cin >> n; // получил размер
int * arr = new int [ n ]; // Сработает!
ВАЖНО: Если программист занимает память, то он ОБЯЗАН вернуть ее, когда она ему уже будет не нужна!
Давайте посмотрим маленький примерчик:
Окей, в принципе, все пояснения написаны в коде и добавить к этому мало что можно. Но все же:
1. процесс заема памяти в куче называется "Динамическая аллокация памяти".
2. обычно стараются писать так, чтобы аллокация и освобождение памяти происходило в одной функции
но если это нельзя сделать, то над функцией, которая аллокирует память, пишут комментарий:
"функция аллокирует память, которую нужно освободить"
3. В моем коде есть пару "трюков", один из которых - пустой for.
если подумать, то переменная ret используется в двух целях: считает буквы и "бежит" по массиву и дополнительный код просто не нужен. Но! Такой for - нетипичен, поэтому у программера, читающего код, либо у вас, через полгода, может возникнуть столбняк))) я предпочитаю комментировать такой код, чтобы ускорить понимание этого места.
Примечание: Существует готовая функция, которая считает количество букв в стринге. Я написал свою только ради того, чтобы напомнить еще раз как строится стринг. Однако, нет никакой необходимости так делать на самом деле. Если существуют готовые функции - пользуйтесь ими! Это значительно уменьшает количество кода, а так же ускоряет понимание программы! Вашу функцию никто не знает, а готовая - всем известна, плюс ко всему - в вашей могут быть баги, а готовая существует уже не одно десятилетие, поэтому все баги там уже исправлены)))
Эта функция называется strlen. Для ее использования нужно подключить библиотеку string.h, то есть:
#include <iostream>
#include <string.h> // подключили библиотеку string.h, где описана функция strlen
using namespace std;
int main (void) {
int str_size = 0;
char * str = NULL;
str = get_str(); // получили стринг (неважно откуда)
str_size = strlen(str); // получили размер стринга через стандартную функцию strlen
return 0;
}
Единственный минус: если указатель, который мы передадим указывает на NULL, то программа упадет. Дело в том, что стандартные функции были написаны так, чтобы занимать наименьшее количество времени. А так, как это еще и старая функция, когда мощность компьютеров была минимальной, экономили даже на проверках, сегодня концепция поменялась, поэтому мы, в своих функциях, проверяем указатели. А значит, если вы пользуетесь функцией strlen, вы либо уверены на миллион процентов, что указатель НЕ нулевой, либо, ставите проверку на вызове: if (str)
ВАЖНОЕ примечание:
когда программист пишет delete, операционная система освобождает память, НО! Она не обнуляет указатель! что это значит? Это значит, что он теперь указывает на "мусор" и поэтому при проверке if ( p == NULL ), где р - указатель, который освободили, программа ответит "нет", мы полезем к указателю и свалимся! Вывод? Если ваш указатель освобождается не в самом конце области его видимости, то следующая строка, которую ОЧЕНЬ желательно написать, выглядит так: p = NULL;
Примечание: если забывать освобождать память, то может возникнуть ситуация, при которой память, которую может вам выделить операционная система - закончится. В этом случае, при попытке аллокировать новый кусок памяти - операционная система убьет программу. Будьте ОЧЕНЬ внимательны!
Ошибка программиста, при которой в программе не освобождается память, называется "Утечка памяти" (англ. "memory leak"). Иногда - чрезвычайно сложно найти такое место в коде, где происходит утечка! Лучше - быть внимательным и не допускать такой ситуации.
Про программу, в которой есть утечка памяти, говорят: "программа течет". Лично я очень строго отношусь к программе, которая течет, я считаю такую программу не работающей, даже, если она выполняет свои функции.
Эта лекция не была очень длинной, но она была ОЧЕНЬ важной! Мы будем часто использовать динамическую аллокацию в будущем, поэтому, отнеситесь к этому серьезно.
Домашнее задание
Напишите программу, которая получает от пользователя 2 параметра:
1. стартовое число
2. количество чисел
Программа сохранит ряд чисел, начиная со стартового, а если количество чисел равно нулю, то программа напишет "Нечего делать" и выйдет. Предположите, что пользователь ввел правильное число, то есть не нужно проверять, не выходит ли число за рамки, установленные размером типа int
Пример:
1. Пользователь ввел:
стартовое число == 12
количество чисел == 3
Программа сохранит такие числа: 12, 13, 14
2. Пользователь ввел:
стартовое число == -2
количество чисел == 5
Программа сохранит такие числа: -2, -1, 0, 1, 2
В конце можно распечатать сохраненные числа, чтобы убедиться, что все сделано правильно.
Удачи!