14:08 

Учимся программировать на С++. Лекция 13. Закрываем пробелы

ToxicSpider
Бритуля - Богиня
Привет!

Условие:

Напишите программу, которая просит у пользователя 5 чисел, от 1 до 50.
После того, как пользователь введет эти 5 чисел, высветите на экран меню, в котором будут такие возможности:
если пользователь ввел 1 - программа распечатывает весь массив
если пользователь ввел 2 - распечатывает минимальное из введенных им чисел
если пользователь ввел 3 - сумму всех чисел.

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

Удачи!


Код:









Примечания:

1. Я объявил 2 переменные min и sum. Таким образом я "оптимизировал" работу программы. Так как массив задается только один раз, а спросить сумму или минимальное число можно бесконечное количество раз, то глупо высчитывать это каждый раз заново. Поэтому, программа посчитает эти значения только при первом запросе и сохранит результат. При следующем подобном запросе, она покажет уже ранее посчитанный результат. Привыкайте искать места в программе, которые потенциально ускорят ее выполнение. Это - умный подход к задаче, а значит - хорошее программирование.

2. Функция get_input_from_user написана с использованием бесконечного цикла для того, чтобы показать инструкцию break. В реальной задаче я бы написал ее так, как написал на одной из лекций. Но! Это тоже решение, оно тоже работает и я не вижу минусов в таком решении, оно - вполне легитимное.

3. В функции main, блок switch-case: присутствует пустой case (на ноль). Если бы я не написал его, то код, получив ноль - попал бы в default и отработал как "ошибочный ввод", а это - баг, так как ноль - легитимный выбор и он означает конец работы.

4. Во всех функциях, которые получают указатель - присутствует проверка на NULL. Это - правильный подход.




Я обещал, что на этой лекции разговор пойдет далее про функции. Однако, кроме этого, я бы хотел рассказать еще про кое-что. Дело в том, что выбирая темы, я старался быть последовательным и поднимать важные вещи. Но, есть пару понятий, которые на лекцию не тянут, а знать их надо, поэтому, в рамках этой лекции, я расскажу и про них.



Инструкция typedef


Я когда-то говорил, что в С++ есть возможность определить собственные типы. Есть несколько способов это сделать и они зависят от сложности типа, который мы определяем. Самый простой вариант - инструкция typedef. Эта инструкция позволяет "обучить" компилятор новому типу, который, на самом деле, новым не является. Мы просто даем типу новое имя. В С эта инструкция имела более широкое применение, однако, на мой взгляд, многое было лишним. Как видно, не я один так считал, потому что в С++ исчезла надобность пользоваться этой инструкцией в ряде случаев.

На сегодняшний день, в С++ она используется, в основном для удобства. Приведу пример. Допустим, в коде нам нужно использовать тип unsigned short int, однако, каждый раз писать такое длинное словосочетание - лень. Можно прибегнуть к трюку и в начале текста, сразу после фразы using namespace std; написать следующее:

typedef unsigned short int USHORT;

теперь, везде по коду, где нам необходимо было объявить:

unsigned short int a = 0;

можно написать:

USHORT a = 0;

И компилятор будет знать, что именно мы объявили. Мы как бы "обучили" его новому типу. Но это же, конечно, не единственное применение. Часто, такой прием применяется, когда программист пользуется более сложными типами переменных, чем эти. Такие типы умеют хранить в себе огромное количество данных и по названию типа невозможно однозначно сказать, что он хранит. И вот тогда, инструкция typedef очень сильно помогает! Программист просто переименовывает тип, чтобы его применение было более понятным.

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



Указатель на функцию


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

Но начну я не с примера, а с теории. Точно также, как можно создать переменную, а потом создать указатель на нее, так же можно описать функцию, и присвоить ее указателю на подобный тип. Но в этом случае, все не так однозначно. Почему? Потому что, указатель на переменную должен совпадать, в определенном смысле, с типом переменной.... а есть ли типы в функциях? Да, есть. Чем отличается функция одного типа, от функции другого типа? Они отличаются параметрами, которые принимает и возвращает функция. Правда только в теории это сложно объяснить, поэтому я напишу пример и мы его разберем.


Код:




Результаты 3-х разных запусков:




Ну что, начнем разбирать? Помолясь приступим)))

Итак, для начала, обратите внимание, что я объявил 3 функции, которые описал ниже main: foo1, foo2 и foo3. Как мы видим они не сильно отличаются своей имплементацией, но это лишь для яркости примера, чтобы не распылять внимание на ненужные детали.

Есть всего лишь одно ограничение: все эти функции должны быть с одинаковой подписью, то есть - принимать и возвращать одни и те же параметры.

Как мы видим, foo1, foo2 и foo3 - обладают такой схожестью.

Окей, пойдем далее. Я пишу слово typedef, которое как бы "говорит" компилятору: "я создаю новый тип, выучи его". После слова typedef я пишу параметр, который функции возвращают, потом в скобках я пишу звездочку, что означает "указатель", после звездочки - я пишу имя нового ТИПА, а потом, в других скобках - параметры, которые функции принимают.

Для примера: если бы я написал

typedef float (*NAME) (char a, bool b);

это означало бы, что указатель типа NAME способен указывать на такие функции:

float foo (char a, bool b);
float bar (char c, bool d);


Взглянем на строку в коде, возле которой стоит комментарий "объявление указателя на функцию". Как мы видим, я создал переменную. Ее тип FUNC, ее имя - func. В начале я присвоил ей NULL (обнулил указатель). После, я присвою ей одну из функций, в зависимости от желания пользователя. Эта переменная может содержать адрес любой функции, которая совпадает по типу. Далее, я вызову функцию через этот указатель. Все остальное в этом коде нам уже понятно, поэтому - нет смысла разбирать. Остановитесь на примере, чтобы убедиться, что все абсолютно ясно.

Примечание: Необязательно писать так:

typedef void (*FUNC)(int num);

можно так:

typedef void (*FUNC)(int); // без имени переменной num

Если есть несколько типов, то их перечисляют через запятую, можно без имен.


Примечание: Объявить указатель на функцию без трюка с typedef можно. Тогда строка:

FUNC func = NULL;

выглядела бы так:

void (*func)(int) = NULL;

Все остальное - без изменений.

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



Команды пре-процессора или "макросы"


В начале - теория. Прежде, чем компилятор начинает компилировать код в единицы и нули, в дело вступает процесс, который называется "пре-процессор". Он редактирует код в зависимости от наших нужд и распоряжений.

Представьте себе такую ситуацию: приходит заказчик и объясняет задачу. Вы приходите к выводу, что в этой задаче вам нужен массив, размером 15 ячеек. Отлично, говорите вы... пишете очень много кода, а ваш массив, как известно, передаете в функции так: указатель и его размер. И вот вы с радостью, через месяц, приносите готовый проект, а заказчик и говорит: "задачка немного поменялась.... но совсем чуть-чуть, не серьезно...." и в процессе объяснения, вы понимаете, что массива в 15 ячеек не хватит, нужно 18....

Для заказчика - это "дешевое" изменение, а для вас? Идите, пройдитесь по всем местам в коде, где вы написали "15" и поменяйте на "18"..... уф.... а с другой стороны, вы не можете объявить массив, используя глобальную переменную (не константу), так как это нельзя делать по стандартам языка.... как быть?

На помощь приходит макро. Вы, используя команду для препроцессора, говорите: "везде, где я напишу слово SIZE, в коде, поменяй это на 15". То есть пишете 15 только ОДИН раз, а везде, где этот размер нужен, пишете "SIZE". И тогда, когда заказчик говорит "поменяй на 18" - вы меняете ТОЛЬКО в одном месте! В остальных это произойдет автоматом! Идея ясна?

Поговорим о синтаксисе. Макро начинается с символа "решетка" - "#" то есть:

#define SIZE 15

int main() {

int arr [ SIZE ] = {0};

// код ....

foo(arr, SIZE); // вызов функции, принимающей массив и его размер

return 0;
}


void foo ( int * arr, int size_of_arr ){
// код
}

Итак... препроцессор, начав свою работу ДО компилятора, проходит по тексту и ВЕЗДЕ, где он видит текст "SIZE" меняет этот текст на "15". В итоге, когда компилятор приступает к работе - текст уже поменян и ему не нужно ломать голову, что такое "SIZE".

Примечание: в конце макро-комманд НЕ ставится точка с запятой! К макро-комманде относится ВСЯ строчка, написанная после символа "решетка". Если программисту, для удобства чтения нужно перенести строку (макросы бывают длинными), то в конце строки ставится знак \ (обратный слеш или бек-слеш). Это как бы говорит пре-процессору: "строка не закончилась, я лишь продолжаю ее с новой строки, чтобы было удобно читать".

Примечание: принято при определении константы через #define, писать ее имя большими буквами. Это правила хорошего тона. Объяснение простое: когда программер видит, что в функцию передается SIZE, то он понимает, что это не переменная, определенная в блоке, а глобальная константа, определенная, к примеру, через #define. Естественно, что глобальную константу можно определить через const int SIZE = 15;, вынеся ее из блоков. То есть написав ее НЕ в какой-либо функции или блоке. (про глобальные переменные и область видимости говорилось ранее). При таком определении, обычно, тоже пишут имя большими буквами.

На самом деле, с помощью макро-команд можно писать программы! Но мы сейчас не будем рассматривать все возможности этой прикольной штуки, потому что в этом нет большой необходимости. Однако, я все же обращу внимание на один факт. Мы все хорошо помним, что программы на С++, которые мы писали до сих пор, начинаются со строки:

#include <iostream>

Как мы теперь уже поняли, это - макро-комманда. Что же она означает? Слово, после решетки, означает "добавь". То есть написано: добавь какой-то iostream. iostream - это файл (назовем это пока так), в котором написан код. То есть где-то, на компьютере лежит файл, который называется iostream. В нем имплементированы разные функции. Зачем это сделано?

Ну, есть ряд функций, которые нам нужны практически в любой программе. Например: функция, которая пишет на экран (cout) или функция, которая принимает с клавиатуры (cin). Две эти конкретные функции работают немного не привычно пока для нас.... мы ведь как привыкли видеть вызов функции? Вот так:

cout ( "Hello World") ;

а вот эта работает так:

cout << "Hello World" ;

НО! Хочу сразу уточнить.... это просто так совпало, что эти конкретные функции, из-за которых мы добавляем iostream, такие "необычные". Просто совпадение.... там вполне могут быть и "обычные" функции. И они там есть! В iostream много интересных функций, кроме этих двух, просто они пока не были нам нужны)))

Итак, что же происходит, когда препроцессор встречает инструкцию #include <iostream> ? А происходит следующее: он идет туда, где этот файл лежит (он знает дорогу), берет от туда ВЕСЬ текст, ВСЕ функции, что там есть и ПЕРЕПИСЫВАЕТ эти функции в наш код, вместо команды #include <iostream> . То есть, до того, как начинает работать компилятор, у него эти функции переписаны ВЫШЕ нашего кода, а значит, встретив в нашем коде:

cout << "Hello World" ;

он уже знает, что это такое.


Скажу вам больше: мы скоро будем писать наши программы в нескольких текстовых файлах, подключая через команду #include один к другому. Это все впереди! Только свои мы будем брать в кавычки, а те, что взяты в треугольные скобки - "стандартные". То есть:

#include <name>// подключили стандартный набор функций

#include "name" // подключили написанный нами набор функций

Такой набор функций (стандартный или собственный) называют "библиотека". Советуя написать #include <iostream> - говорят: "подключи библиотеку iostream"



Еще немного об области видимости переменной


Давайте посмотрим на код и на результат выполнения программы:


Код:




Результат:




Отличненько))) Видите ли вы то, что вижу я? Мы создали переменную n и положили туда 5. Потом, мы вызвали функцию, в которую это n передали, и изменили ее значение. Однако, возвратившись из функции мы вдруг обнаруживаем, что изменение не произошло. Я специально везде распечатываю значение n, чтобы это было наглядно видно.

Как объяснить этот феномен? Нужно подробно рассмотреть, что же произошло на самом деле. Итак, когда мы написали:

int n = 5;

В памяти создалась ячейка и в нее положили 5. Это все понятно. Что произошло, когда мы положили это в функцию? Произошло следующее: создалась ДРУГАЯ ячейка, которую мы видим ТОЛЬКО в функции и которая по завершению блока функции - умрет. В эту другую ячейку мы СКОПИРОВАЛИ ЗНАЧЕНИЕ из первой.

То есть, другими словами, мы создали другую переменную и ей присвоили значение первой. В момент, когда мы только вошли в функцию, переменная n, тоже содержала 5. Однако, в первой строке мы поменяли это значение на 7 и распечатали. После того, как блок был покинут и мы вернулись в main, переменная, созданная в функции и измененная там - умерла. А с нашей, "первой" переменной, ничего не произошло, потому что все действия происходили с совершенно другой переменной.

Как быть, если в функции нужно поменять значение переменной так, чтобы оно вернулось в main? Ну, самый естественный подход, это изменить код так:


Код:




Результат:



Мы просто вернем новое значение из функции, через инструкцию return (при этом не забыв поменять описание функции - она теперь возвращает int), и присвоим ее значение - первой переменной. Однако, у этого способа есть 2 минуса:

1. Как все уже поняли из функции можно вернуть не более ОДНОЙ переменной. А если нужно изменить и вернуть 2? 3? и так далее....

2. Что произошло? Мы скопировали значение 2 раза. Когда мы работаем с примитивными типами - это не страшно, они занимают считанные биты и процедура происходит быстро. НО! Скоро мы научимся создавать свои типы, а "свой" тип может "весить" НАМНОГО больше. И что же? Мы будем все это копировать туда-сюда? Это замедляет работу программы!

Примечание:Представьте ситуацию, когда вам нужно вернуть строку из функции. Если вы сделаете так:

int main(){

char * p = NULL;


//код

p = foo(); //вызов функции
cout << p << endl; //распечатка строки на экран

//код

return 0;
}

char * foo(void){
char str = "vasya";
return str;

}


программе нужно получить строку из функции. Мы объявляем указатель на char (то есть - потенциальный стринг) и принимаем то, что даст нам функция на выходе. Как только мы полезем на следующей строке распечатывать полученную строку: программа с большой вероятность - грохнется. Почему?

Что мы написали? Мы объявили стринг, который по всем законам будет жить, пока код не покинет блок. Выйдя из функции, операционная система уничтожит все буквы, которые он содержал и ВОЗМОЖНО займет это место чем-нибудь другим, а в return, то есть в указатель на место, вернет адрес первой ячейки, которая, возможно, Уже строкой не является, то есть: УЖЕ нет строки!

Мы ведь возвращаем адрес!

Поэтому поступают так:

int main(){

char * p = NULL;


//код

p = foo(p); //вызов функции
cout << p << endl; //распечатка строки на экран

//код

return 0;
}

char * foo(char * p){
//тут, пока не известным нам образом получают строку в p, и возвращают назад. и таким образом,
//операционная не убивает эту строку.
return р;

}



Примечание: обычно, если функция возвращает примитивную переменную, и нет больше кандидатов на ее место как, например, в функции "get_min()", то применяют такой способ, как я показал выше, но бывают ситуации, когда нужно получить из функции несколько параметров... о том, как обойти это ограничение - далее.



Какой выход? Как обойти эти 2 минуса? Может быть самое время вспомнить про указатели?


Код:




Результат:




Давайте вместе скажем: "ух ты! работает!" Да, работает. Но почему? Проведем анализ?
Итак, у нас есть переменная n и у нее есть адрес. В момент, когда мы попали в функцию, создалась переменная типа "указатель на int". В нее мы положили адрес n (обратите внимание, на вызов функции, а именно - как я передаю переменную).

Примечание: так как функция принимает указатель, стоит проверить, не указывает ли он на NULL, дабы избежать крушения программы. Я написал это так: if ( !p ) NULL - это ноль. Если р указывает на ноль, то if ( p ) будет ложной и код не попадет в блок if НО! Это значит что "не р" - правда (если р - ложь, то не р - правда, помним, да?) А значит, что если р указывает на ноль, то в блоке if ( !p ) - код выполнится, так как - правда.

После этого я обращаюсь к указателю через звездочку, то есть, я обращаюсь не к указателю, а к ячейке памяти, на которую он указывает, другими словами, я написал: "положи в ячейку памяти, на которую указывает р, значение 7" Программа обращается к р, смотрит куда он указывает, лезет по этому адресу (который является адресом переменной n, объявленной в main) и туда кладет значение. То есть - меняет значение нашей "первой переменной" напрямую, по адресу.

Примечание: если в функцию передать массив и там его изменить, то изменения отразятся на "настоящем" массиве. Потому что передача массива происходит через указатель!

Если что-то непонятно - перечитайте с начала главы. Это ОЧЕНЬ важно понять! Если понятно - я продолжу)))



Ссылки


Наравне с типом переменных "указатель" существует еще один тип, способный хранить адрес. Этот тип называется "ссылка" (англ. "reference"). В С такого типа не было, но за время существования языка С стали понятны минусы указателей:

1. Работа с ними потенциально опасна, так как указатель может указывать на ноль и нужно все время его проверять

2. Предположим, что мы поменяли функцию из foo (int n) на foo(int * n), чтобы работать напрямую с переменной, которую я обозвал "первой". Это значит, что не достаточно поменять только "заглавие" функции, потому что потом надо пройти по коду и везде, где написано что-то типа n = 7; написать *n = 7; (добавить звездочку)

Поэтому в С++ создали еще один тип, "лечащий" оба этих недуга. Объявляется ссылка так:

int n = 5; // создали переменную

int & r = n; // создали ссылку, присвоив адрес

Теперь, везде в коде, где мы будем писать:

r = 7;

мы будем менять значение n

То есть: нам не нужно дописывать звездочку! Более того: ссылка НЕ МОЖЕТ указывать на ноль! Мы получили тип обладающий всеми преимуществами переменных и указателей! С одной стороны она имеет свойство указателя: работает "напрямую" с переменной, а не с ее копией. НО - она безопасна. С другой стороны мы обращаемся к ней так же, как и к переменной, без дополнительного "геммороя" со звездочками.

Примечание: присвоить адрес переменной, в ссылке, можно только один раз! И ТОЛЬКО в момент ее создания!

Код, который я приводил выше, можно написать, оперируя новым типом "ссылка":


Код:




Результат:




Оки. Пока - достаточно)))

Сегодня мы выучили много вещей:

1. Мы выучили инструкцию typedef, через которую можно объявлять собственный тип. Пока что - это "не по-настоящему" свой, а только "эмуляция", но такой трюк тоже иногда нужен, как правило, для удобства чтения.

2. Мы узнали, что можно создавать указатели на функцию и научились с ними работать.

3. Мы узнали что такое пре-процессор и как им командовать.

4. Мы узнали несколько трюков в работе с функциями

5. Выучили новый тип - "ссылка" и рассмотрели его преимущества.



Домашнее задание


Напишите 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

Совет: сохраните эти функции, возможно далее мы будем их использовать. В С++ есть готовые функции, выполняющие подобную работу, но "на всем готовеньком" жить скучно)))) Обещаю, что в последствии мы рассмотрим эти готовые функции!

Удачи!

@темы: C++

URL
   

Godney

главная