Бритуля - Богиня
Привет!
решение домашнего задания с предыдущей лекцииУсловие:
Напишите программу, которая вычисляет студента, которого нужно послать на олимпиаду по физике.
Немного о вузе: в вузе есть всего 3 курса: физика, математика и английский. В Вузе - 100 бальная система оценок. Вуз заботится о своем имени и поэтому, он не посылает на такие мероприятия студентов, у которых средняя оценка по всем курсам ниже 85. На олимпиаду должны поехать студенты, у которых оценка по физике не меньше 90.
Создайте 3 студента с такими оценками:
1. физика 95, математика 90, английский 85.
2. физика 95, математика 60, английский 60.
3. физика 80, математика 90, английский 95.
Раздайте им произвольные имена.
Программа напишет на экран имена всех студентов, которые достойны поездки на олимпиаду по физике.
Примечание: Не обязательно получать данные с "внешнего источника", можно прописать их в коде. Цель задания - работа со структурами, а не с клавиатурой.
Совет: даже больше чем совет, скорее требование: разбейте код на функции, не пишите все в main, это не красиво.
решениеКод:









Результат:

Разбор полетов:
Начну с того, что скажу, что в моем решении есть ряд проблем. Я укажу их.
Итак, прежде всего - библиотека string.h - для некоторых функций, которые я использую в работе со стрингами. Во-вторых - энумератор. Он помогает мне с курсами. В-третьих - сама структура. Так как я не требовал номер ID, то я и не написал его.
Я создаю трех студентов и заполняю данные через функцию. Я передаю студента в функцию через ссылку, чтобы то, что я заполню в функции сохранилось в возврате. Кроме этого я "убиваю" еще "зайца": я бы мог вернуть студента из функции через возвращаемый параметр, но тут есть подводные камни, часть мы рассмотрим далее, а то, что пока на виду - я хотел избежать копирование структуры. Это не хорошо хотя бы потому, что структуры могут быть массивными и это занимает лишнее время.
Кроме того моя функция принимает стринг как const char * . Таким образом, я превращаю стринг в константу, потому что она мне нужна только для чтения. Таким образом, код будет следить за тем, чтобы я не изменил его содержимое.
Внутри функции create_student я динамически аллокирую память и делаю это убедившись, что параметры, которые я получил - мне подходят.
В функции print_good_student я проверяю условия и распечатываю только нужного студента.
Обратите внимание на преобразование типов в get_avarage.
Функция destroy_student освобождает память, которую я аллокировал под имена.
Теперь проблема: перед тем, как я освобожу память, я проверяю не является ли указатель нулем? НО! Его, изначально никто не обнулял... упс... создайте студента и сразу же передайте его в функцию destroy_student и вы увидите, как программа рухнет)))). Среди всего прочего, мы поговорим сегодня об автоматическом обнулении параметров, а так же увидим, что если правильно описать структуру, то многие проблемы решаются сами собой. Приятной лекции)))
лекцияНа прошлой лекции мы познакомились с возможностью создать собственные типы. Если с энумераторами все понятно, то со структурами мы только начали. На прошлой лекции я обещал: "Естественно, что на мою переменную распространяются ВСЕ законы обычных переменных: ее можно передать в функцию, ее можно получить из функции, можно создать указатель или ссылку на нее, можно создать массив таких переменных.... но обо всем этом мы поговорим в следующий раз, а пока - достаточно.", но на самом деле, конкретно в этом, говорить не о чем... то есть: все это можно)))
То есть:
допустим, есть структура Student, а значит можно:
Student student; // создать переменную в стеке
Student * p_student = & student; // создать указатель на нее
Student & r_student = student; // создать ссылку на нее
Student array [ 100 ] ; // создать массив в стеке
Student * new_student = new Student; // создать переменную в куче (динамическая аллокация памяти)
Student * new_student = new Student ( ); // некоторые создают так (пустые скобки в конце) - на данном этапе это тоже самое, хотя причина возникновения скобок есть, об этом позже.
delete new_student; // освободить переменную, (не забываем)
Student * din_arr = new Student [ size ]; // создать массив в куче с зараннее неизвестным размером
delete [ ] din_arr; // освободить массив
Student foo (Student student); // работа с функцией (передать / получить переменную)
Student * foo (Student * student); // работа с функцией (передать / получить указатель)
Student & foo (Student & student); // работа с функцией (передать / получить ссылку)
Все, что я показал выше можно делать, то есть: созданная нами переменная НИЧЕМ, с этой точки зрения, не отличается от примитивных переменных, с которыми мы уже очень хорошо знакомы.
Давайте остановимся на этом утверждении. Из него вытекает, что можно сделать вот так:
struct Passport {
int id;
int age;
};
struct Student {
int id;
int grades [ 5 ];
Passport passport; // обратите внимание! Я выше описал структуру "Паспорт", а теперь, у студента есть переменная "паспорт", то есть - паспорт конкретного студента
};
Вопрос: можно ли так делать? Да, безусловно можно!
Сразу следущий вопрос: допустим, мы создали структуру Студент и хотим заполнить ее. Мы присваиваем студенту номер id, пишем его оценки и подходим к моменту, когда надо заполнить данные паспорта. Как мы это делаем? Ну, во-первых, я напомню, как мы работаем с примитивными переменными, лежащими в структуре:
Student student;
student.id = 5; // запись
cout << "The ID: " << student.id << endl; // чтение
По той же логике мы работаем и со структурами, внутри структур:
// запишем и прочтем ID паспорта:
student.passport.id = 12;
cout << "Student ID: " << student.id << " has passport ID: " << student.passport.id << endl;
На экран будет выдано:
Student ID: 5 has passport ID: 12
А можно ли написать так:
struct Student {
int id;
Student s; // то есть в переменной "Студент" держать переменную "Студент"?
};
Если честно - ни разу не пробовал, не думаю, что это скомпилируется, но даже если и да, то уверен, что ничего хорошего из этого не выйдет..... почему? Давайте подумаем, что произойдет, как только мы в коде объявим переменную из типа "Студент":
Student student;
Компилятор начнет ее строить: выделит память под ID, а потом начнет выделять память под "Студента" лежащего внутри. Ему нужно создать переменную студент, в которой лежит: ID и Student. То есть внутри студента будет еще один студент, внутри которого еще один студент, содержащий внутри себя студента..................... думаю, что проблема ясна)))) Если это и скомпилируется, то как только код дойдет до места, в котором впервые создается студент, то скорее всего программа сразу же рухнет, потому что сожрет всю память. Будьте внимательны)))
Как же быть, если по какой-то причине нам нужен студент внутри студента? Мы можем выйти из положения следующим образом:
struct Student {
int id;
Student * s; // то есть в переменной "Студент" лежит УКАЗАТЕЛЬ на студента
};
В таком случае, компилятор, создавая структуру "Студент" создаст в ней указатель, на такую же структуру, но, так как указатель - НЕ сама структура, и чтобы указатель указывал на структуру ее надо создать отдельно, то есть, написать так:
Student student;
student.id = 5;
student.s = new Student;
И тогда, в структуре студент появится новый студент, в котором тоже будет лежать указатель на студента. Этот фокус имеет широчайшее применение и мы увидим как именно используют это фишку, но это будет на следующей лекции, а пока мы научимся работать с указателем на структуру и с данными, которые там лежат.
Я хочу кое что напомнить, а именно:
// у нас есть примитивная переменная:
int num; // объявление
num = 0; // запись
cout << "Number: " << num << endl; // чтение
// у нас есть указатель:
int * p_num; // объявление
p_num = & num; // запись в указатель
cout << "Address of num: " << p_num << endl; // чтение указателя
* p_num = 5; // запись в переменную, на которую указывает указатель
cout << "Number: " << * p_num << endl; // чтение из переменной, на которую указывает указатель
// а так же:
// вспомним, что мы можем, используя указатель, создать переменную в куче:
int * p_num = new int;
// то есть, указатель УЖЕ указывает на переменную! А значит, мы можем далее писать и читать:
* p_num = 5;
cout << "Number: " << * p_num << endl;
Как же работать, когда мы создали указатель на структуру? Допустим, мы сделали как в первом случае:
Student student;
Student * p_student = & student;
Или как во втором:
Student * p_student = new Student;
Как через указатель заполнить поле id, лежащее в студенте? Мы действуем по логике:
(*p_student).id = 5;
то есть, в скобках мы говорим: "дай мне то, на что указывает указатель p_student". А после этого мы обращаемся к тому, что получили через точку. НО! Такая запись неудобна, поэтому, придумали укороченную запись:
p_student->id = 5;
мы ставим значек "стрелочку". Она ставится так: в начале "минус" ( - ), а потом - закрывающая треугольная скобка, тот же символ, который мы пишем в сравнении "больше" ( x > 0 ). Эти 2 символа пишутся один за другим и зрительно получается "стрелочка".
Хочу подчеркнуть: обе записи СОВЕРШЕННО идентичны, но второй - более удобочитаемый, поэтому, почти все программисты (кроме задротов) используют второй способ. В нашем случае, когда в структуре студент есть указатель на студента, то после того, как аллокировали "внутреннего", мы можем заполнить его:
Student student;
student.s->id = 15; // присвоили "внутреннему" студенту, которого мы выше назвали s (плохое название, потому что оно не рассказывает назначение "внутреннего" студента), ID 15.
cout << student.s->id << endl; // прочли ID из "внутреннего" студента
Допустим, мы хотим создать какой-нибудь общий параметр для всех студентов в нашем коллежде. Ну, например, проходной балл. В нашем колледже есть 2 факультета: математический и философский. Чтобы оправдать создание двух разных типов, представим, что у них разный набор курсов (что вполне допустимо). Не будем сильно напрягаться, для примера хватит двух. У философского факультета: философия и математика, у математического - математика и физика.
У обоих факультетов есть математика, но так как философам она не так нужна, как математикам, то мы допустим, что проходной балл по математике у философов 40, а у математиков - 60. Наши две структуры определены так:
struct Philosopher {
int math_pass_score;
// другие данные студента
};
struct Mathematician {
int math_pass_score;
// другие данные студента
};
Давайте рассмотрим плюсы и минусы такого подхода. Плюс заключается в том, что "проходной балл по математике" находится в подходящей структуре, то есть во время выполнения программы можно написать код:
Philosopher curr_student;
// инициализация, заполнение данных
// какой-нибудь код
if ( curr_student.grades [MATH] >= curr_student.math_pass_score)
// тут действия которые происходят, когда студент прошел математику
else
// тут - не прошел
То есть, довольно читаемо, упорядочено и прекрасно. А минусы? Ну, во-первых, нужно на инициализации заполнять эту переменную (math_pass_score) значением КАЖДЫЙ раз, когда мы создаем студента. Во-вторых, ОДИНАКОВОЕ значение у всех студентов - копируется из структуры в структуру.... а если нужно поменять это значение? И, конечно же мы не можем не заметить еще один, существенный минус: чтобы, допустим, распечатать на экран значение "проходного балла по математике для философского факультета" - нужно, чтобы БЫЛА ПЕРЕМЕННАЯ этого типа! То есть - нужно конкретную переменную из типа "студент-философ"
Как же правильно и красиво решить эту задачу? И тут на помощь приходят статические переменные. Статическая переменная и ее значение будут принадлежать не КОНКРЕТНОЙ ПЕРЕМЕННОЙ, а ВСЕМУ ТИПУ! То есть, мы напишем ее значение один раз - и это значение будет У ВСЕХ переменных типа "студент-филосов"! Нужно будет поменять? Не проблема! Мы поменяем ОДИН РАЗ! Как описать и заполнить значение такой переменной? В начале, в самой структуре (в ее описании) мы добавим ключевое слово "static", то есть:
struct Philosopher {
static int math_pass_score;
// другие данные студента
};
struct Mathematician {
static int math_pass_score;
// другие данные студента
};
А ниже, перед описанием функций, то есть там, где мы объявляем глобальные переменные, мы напишем:
int Philosopher::math_pass_score = 40;
int Mathematician::math_pass_score = 60;
То есть - имя типа переменной внутри структуры, имя структуры, 2 раза символ "двоеточие", имя статической переменной. Таким образом, значение этой переменной будет известно еще ДО того, как мы создадим переменную типа "студент". А так как эта переменная НЕ принадлежит конкретному студенту (конкретной переменной), а ВСЕМУ типу, то обращаться мы будем к ней через имя типа, а не через имя конкретной переменной. И тоже - через двойное двоеточие:
Philosopher curr_student;
// инициализация, заполнение данных
// какой-нибудь код
if ( curr_student.grades [MATH] >= Philosopher::math_pass_score)
// тут действия которые происходят, когда студент прошел математику
else
// тут - не прошел
Ниже я приведу пример, обобщающий все знания, что мы получили на этой лекции, но пока еще есть о чем поговорить.
Понятие конструкторов и деструкторов, точно так же, как понятие статических переменных в структуре - это уже чистый С++ ! В С - это не работает. Что же это такое? Это, скажем так, функции, которые описаны в теле структуры. В момент, когда создается переменная из типа структуры, то есть, в момент, когда мы пишем в коде Student curr_student; вызывается функция, которая носит название "конструктор". В момент, когда наша структура выходит из области видимости и ее уничтожает система, ЛИБО, если это была динамическая аллокация и мы пишем delete curr_student;, вызывается функция, которая называется "деструктор".
До сих пор, во всех структурах которые я приводил в качестве примера, мы не видели этих функций. Это происходит потому, что если мы не описываем явно эти функции, компилятор "дарит" нам свои и они - абсолютно пустые. НО! Мы можем описать свои конструкторы и деструкторы и тогда "подарка" от компилятора не будет.
Зачем нам могут пригодиться конструкторы и деструкторы? Для того, чтобы правильно создать или уничтожить нашу структуру. Приведу пример: ну, то, что бросается сразу в глаза - обнуление переменных внутри структуры, то есть, в структуре студент, которую мы уже все хорошо знаем, есть переменная ID. Как мы уже поняли, до того, как мы напишем там значение - там лежит мусор. Мы всегда старались избегать такой ситуации, а тут я почему-то промолчал и ничего не сказал об этом....
Иногда наша структура не имеет смысла без определенного значения внутри. Допустим, не существует студента без номера студенческого билета и мы хотим обязать во время создания студента СРАЗУ ЖЕ присвоить ему такой номер.
Что же по поводу уничтожения? Давайте вспомним, что на предыдущей лекции мы создали студента и динамически аллокировали память под его имя и фамилию. Позже нам пришлось освобождать эту память, прежде чем уничтожить студента. А можно ли сделать это автоматически? Да, можно! Прописав процесс освобождения памяти в деструкторе.
Естественно, так как эти функции - особый вид функций, то они имеют немного отличающийся синтакс. Конструктор и деструктор ОБЯЗАНЫ:
1. Иметь название, совпадающее с названием структуры.
2. НЕ возвращать никаких параметров
3. Деструктор не принимает параметры.
К примеру:
struct Student {
Student( параметры ) { // конструктор
// тело функции
}
~Student ( void ) { // деструктор
// тело функции
}
};
Разберем: как я и обещал, наши "особые" функции не возвращают параметры, их имя совпадает с названием структуры, а деструктор - не принимает параметры. В конструкторе, в скобках, написано "параметры" и это может быть что угодно, включая void. Присмотримся к деструктору: перед ним стоит символ "тильда". Этот символ, на клавиатуре, находится слева от единицы и его вводят, удерживая клавишу "Shift".
Давайте остановимся на таком случае: мы создали конструктор, в студенте, который принимает параметр: ID студента. То, что я сейчас напишу надо усвоить как "отче наш", потому что если допустить эту ошибку, то компилятор откажет, написав причину, которую будет трудно понять, не зная почему она произошла. И тогда, начинающий программист почувствует себя в нехорошем положении, в котором он просто не знает что делать.
struct Student {
Student (long id) { // конструктор
student_id = id;
}
long student_id; // член структуры
};
Как мы видим, действие, которое происходит в конструкторе - мы присваиваем переменной student_id тот id, который мы получили в конструкторе. То есть в коде, переменную "студент" нужно создавать так:
Student student (15); // создаем переменную в стеке
Student * p_student = new Student ( 15 ); // создаем переменную в куче
Обратите внимание! Если написать:
Student student;
или
Student * p_student = new Student;
то это не скомпилируется, потому что конструктора, не принимающего параметры УЖЕ НЕТ. Раньше - он был, мы "получили его в подарок" от компилятора, потому что не написали свой, но как только мы определяем свой конструктор, подарка нет. В принципе можно описать И свой пустой конструктор. Конструкторов может быть неограниченное количество! Но с одним требованием: не должно быть 2 (и более) конструктора, получающих одни и те же типы параметров! В принципе, такой фокус можно сделать с любой функцией и мы увидим это на следующей лекции, на которой я запланировал закрыть образовавшиеся пробелы, но пока - знайте - можно)))
Пока нет пустого конструктора - так же не отработает создание массива вот таким образом:
Student array [ 3 ] ;
потому что в момент создания 3-х студентов в массиве код пытается вызвать пустой конструктор в структуре, которого, как мы уже поняли, нет.
Теперь еще о "подводных камнях":
Допустим структура описана так:
struct Student {
Student (long id) { // конструктор
id = id; // присвоение значения в переменную структуры
}
long id; // переменная структуры (член структуры, на "профессиональном" языке)
};
Первая проблема, которая сразу бросается в глаза, это строка id = id;. Где первый id - должен быть член структуры, а второй id - параметр, который получает конструктор. Как компилятор понимает, что из них - что? Никак не понимает. При условии, что имена совпадают, он выбирает параметр, который передали в конструктор. То есть - в этом коде, присвоение члену структуры не происходит. Как быть?
Есть 2 выхода.
1. Не называть член структуры так же, как мы назвали параметр в конструкторе. Программисты, которые выбирают этот подход, обычно делятся на 2 типа: первые пишут перед или после имени нижнее подчеркивание, то есть, член структуры объявлен так:
long id_;
И тогда, строчка присвоения выглядит так:
id_ = id;
вторые ставят букву m в начале имени, то есть: m_id; (от англ. слова "член" "member")
2. Можно называть параметр и член одинаково, и тогда, если мы имеем ввиду член структуры, то мы пишем перед ним слово this.
this - это указатель, который работает только в структурах и он говорит: "эта структура". Член структуры объявлен как обычно:
long id;
а присвоение происходит так:
this->id = id;
Обратим внимание: так как this это указатель, то после него ставится "стрелочка", а не "точка".
И последнее, что я хотел бы показать:
Более правильно в С++ производить присвоение ДО тела конструктора. То есть не так:
Student (long id) {
id_ = id;
}
А вот так:
Student (long id) : id_(id) {
}
После скобок, в которых перечислены параметры, ставится двоеточие, потом имя члена, скобки и значение, которое в него положить. Если есть несколько членов, которые нужно инициализировать, то они перечисляются через запятую. И тогда, язык гарантирует, что все переменные, которым присвоено значение таким образом, буду проинициализированы еще ДО того, как структура будет создана! Например, если член структуры - ссылка - то это ЕДИНСТВЕННЫЙ способ назначить ей значение!
Теперь о синтаксисе: я, в примерах выше, описывал конструктор и деструктор в теле структуры. НО, как правило так не делают (можно, но так не делают, это не удобочитаемо). Прибегают к такому трюку:
struct Student {
Student ( long id ) ; // только объявление конструктора
long id_;
};
А ниже в коде, среди других функций описывают конструктор:
Student::Student (long id) : id_(id)
{
// тело конструктора
}
То же самое делают и с деструктором.
Окей, я много рассказывал и скорее всего, немного запутал, поэтому, я решил закончить эту лекцию примером кода, в котором я наведу порядок и дам "пощупать" все, что рассказал сегодня:
1. Работа со структурами через указатель
2. Статические переменные в структуре
3. Использование конструктора и деструктора.
Код:







Результат:

Разбор полетов:
Вот мы видим всю сегодняшнюю теорию в практике. Тут есть о чем поговорить и я начну разбирать код.
Начнем сверху. Во-первых, из-за того, что я ниже пользуюсь функциями strlen и strcpy, мне необходимо подключить string.h.
Ниже я описываю структуру, в которой я отмечаю, что есть конструктор и деструктор. Это объявление, код этих функций я опишу ниже. Далее я объявляю "вспомогательные" функции, а после этого - выставляю начальный параметр в статическую переменную структуры. Она получит параметр 0 еще ДО старта программы.
Потом я прошу у пользователя проходной балл по математике. Я сделал так для того, чтобы показать запись в статическую переменную. Я бы мог изначально вместо нуля прописать, например, 50. Но для наглядности я получаю данные с клавиатуры и кладу в переменную, чтобы показать как с ней работать. Я не проверяю правильность ввода пользователя только потому, чтобы не "загружать" код лишней информацией. Но в реальной программе это делать необходимо.
Отлично. Ниже я описываю main. В нем я создаю 2 переменные из типа "Студент", одну в стеке, другую в куче. Обратим внимание, что перед созданием переменных я пишу на экран сообщение. Ниже в конструкторе, я тоже распечатаю на экран сообщения, чтобы было видно, когда отрабатывает конструктор.
После создания структуры я сразу заполняю переменные math_grade в обоих структурах. С первой я работаю через точку, так как это сама переменная, а со второй - через стрелочку, потому что это указатель на переменную.
Потом, я вызываю функцию, которая распечатывает ответ на вопрос "прошел ли студент математику". Посмотрите, что там все ясно.
Примечание: я передаю переменные в функцию через указатель. Почему? Как мы уже поняли, структуры могут быть очень массивные, поэтому, если передать переменную по имени, то во время приема ее в функцию, структура будет копироваться. И в функции мы будем работать с копией, это расточительно. Поэтому лучше передавать структуры либо через указатель, либо через ссылку. А чтобы гарантировать, что в функции данные не будут изменены, можно добавить слово const, превратив переменную в константу (только для этой функции).
В конце я освобождаю динамическую аллокацию, распечатав при этом, строку сообщения. Из деструктора я тоже буду печатать на экран, чтобы показать, что он сработал. Обратите внимание, что деструктор сработал 2 раза (см. на "Результат"). Первый раз он сработал для переменной в куче, когда я написал слово delete, а второй раз - для переменной в стеке. Второй деструктор вызвался "неявно", как только переменная, созданная в стеке покинула область видимости (пределы функции main).
Ниже я описал 2 вспомогательные функции, в которых нет ничего необычного, кроме, пожалуй одного маленького момента: способа написания распечатки из is_math_pass. Так как для компилятора конец строки это точка с запятой, то в принципе, это все - одна строка. Я разбил ее на строки, чтобы удобней было читать.
Конструктор. Первое, что я делаю в конструкторе, я обнуляю члены структуры. Посмотрите, как я это делаю. Это происходит через двоеточие. Это гарантирует то, что эти члены будут "обнулены" еще до того, как начнет работать конструктор. Так делать правильно.
В самом конструкторе я получаю переменные, которые совпадают с именами членов структуры, поэтому, к членам я обращаюсь через this и стрелочку (потому что this - это указатель). На самом деле, обычно я так не делаю, я отношусь к той группе программистов, которые любят называть члены структур с нижним подчеркиванием, то есть: first_name_ и last_name_. Скорее всего, далее в примерах я буду делать так. Сейчас я специально назвал это одинаково, чтобы познакомить вас с указателем this
Я динамически аллокирую места под имена и заполняю их. При этом я пользуюсь стандартными функциями.
В деструкторе мне нужно освободить имена перед тем, как структура будет уничтожена. Если этого не сделать - будет утечка памяти! Но обратите внимание, что в деструкторе я уже не пишу this перед членами! НЕ будет ошибкой, если написать это, но просто в этом случае нет необходимости, так как других кандидатов на эти имена нет (в конструкторе имена членов совпадали с именами параметров, полученных в конструктор).
Хорошенько пройдитесь по коду и убедитесь, что в нем все досконально понятно. Если остаются неясности - нужно переписать код себе и поиграться с ним, меняя его. Обычно, когда щупаешь все сам - становится понятней)))
Это была длинная лекция, которая включала в себя много нового. Не спешите продолжать, прежде чем не усвоите эти знания. Они ВАЖНЫ!
Перепишите программу, заданную в качестве домашнего задания на предыдущей лекции, с такими изменениями.
Структура имеет имя, фамилию, оценки и ID-номер студента. ID номер назначается автоматически при создании студента. Предположите, что количество студентов не может выходить за рамки числа, которое может быть сохранено в переменной long. Потрудитесь сделать так, чтобы ID был уникальным для каждого студента. Имя и фамилию студент получает в конструктор. (То есть: при создании переменной студент необходимо указать имя и фамилию)
Введите студентам те же оценки, которые были в прошлом задании и распечатайте имена И ID-номера тех, кто достоин поездки на олимпиаду.
Требование: все студенты должны быть созданы в куче.
Удачи!
решение домашнего задания с предыдущей лекцииУсловие:
Напишите программу, которая вычисляет студента, которого нужно послать на олимпиаду по физике.
Немного о вузе: в вузе есть всего 3 курса: физика, математика и английский. В Вузе - 100 бальная система оценок. Вуз заботится о своем имени и поэтому, он не посылает на такие мероприятия студентов, у которых средняя оценка по всем курсам ниже 85. На олимпиаду должны поехать студенты, у которых оценка по физике не меньше 90.
Создайте 3 студента с такими оценками:
1. физика 95, математика 90, английский 85.
2. физика 95, математика 60, английский 60.
3. физика 80, математика 90, английский 95.
Раздайте им произвольные имена.
Программа напишет на экран имена всех студентов, которые достойны поездки на олимпиаду по физике.
Примечание: Не обязательно получать данные с "внешнего источника", можно прописать их в коде. Цель задания - работа со структурами, а не с клавиатурой.
Совет: даже больше чем совет, скорее требование: разбейте код на функции, не пишите все в main, это не красиво.
решениеКод:









Результат:

Разбор полетов:
Начну с того, что скажу, что в моем решении есть ряд проблем. Я укажу их.
Итак, прежде всего - библиотека string.h - для некоторых функций, которые я использую в работе со стрингами. Во-вторых - энумератор. Он помогает мне с курсами. В-третьих - сама структура. Так как я не требовал номер ID, то я и не написал его.
Я создаю трех студентов и заполняю данные через функцию. Я передаю студента в функцию через ссылку, чтобы то, что я заполню в функции сохранилось в возврате. Кроме этого я "убиваю" еще "зайца": я бы мог вернуть студента из функции через возвращаемый параметр, но тут есть подводные камни, часть мы рассмотрим далее, а то, что пока на виду - я хотел избежать копирование структуры. Это не хорошо хотя бы потому, что структуры могут быть массивными и это занимает лишнее время.
Кроме того моя функция принимает стринг как const char * . Таким образом, я превращаю стринг в константу, потому что она мне нужна только для чтения. Таким образом, код будет следить за тем, чтобы я не изменил его содержимое.
Внутри функции create_student я динамически аллокирую память и делаю это убедившись, что параметры, которые я получил - мне подходят.
В функции print_good_student я проверяю условия и распечатываю только нужного студента.
Обратите внимание на преобразование типов в get_avarage.
Функция destroy_student освобождает память, которую я аллокировал под имена.
Теперь проблема: перед тем, как я освобожу память, я проверяю не является ли указатель нулем? НО! Его, изначально никто не обнулял... упс... создайте студента и сразу же передайте его в функцию destroy_student и вы увидите, как программа рухнет)))). Среди всего прочего, мы поговорим сегодня об автоматическом обнулении параметров, а так же увидим, что если правильно описать структуру, то многие проблемы решаются сами собой. Приятной лекции)))
лекцияНа прошлой лекции мы познакомились с возможностью создать собственные типы. Если с энумераторами все понятно, то со структурами мы только начали. На прошлой лекции я обещал: "Естественно, что на мою переменную распространяются ВСЕ законы обычных переменных: ее можно передать в функцию, ее можно получить из функции, можно создать указатель или ссылку на нее, можно создать массив таких переменных.... но обо всем этом мы поговорим в следующий раз, а пока - достаточно.", но на самом деле, конкретно в этом, говорить не о чем... то есть: все это можно)))
То есть:
допустим, есть структура Student, а значит можно:
Student student; // создать переменную в стеке
Student * p_student = & student; // создать указатель на нее
Student & r_student = student; // создать ссылку на нее
Student array [ 100 ] ; // создать массив в стеке
Student * new_student = new Student; // создать переменную в куче (динамическая аллокация памяти)
Student * new_student = new Student ( ); // некоторые создают так (пустые скобки в конце) - на данном этапе это тоже самое, хотя причина возникновения скобок есть, об этом позже.
delete new_student; // освободить переменную, (не забываем)
Student * din_arr = new Student [ size ]; // создать массив в куче с зараннее неизвестным размером
delete [ ] din_arr; // освободить массив
Student foo (Student student); // работа с функцией (передать / получить переменную)
Student * foo (Student * student); // работа с функцией (передать / получить указатель)
Student & foo (Student & student); // работа с функцией (передать / получить ссылку)
Все, что я показал выше можно делать, то есть: созданная нами переменная НИЧЕМ, с этой точки зрения, не отличается от примитивных переменных, с которыми мы уже очень хорошо знакомы.
Давайте остановимся на этом утверждении. Из него вытекает, что можно сделать вот так:
struct Passport {
int id;
int age;
};
struct Student {
int id;
int grades [ 5 ];
Passport passport; // обратите внимание! Я выше описал структуру "Паспорт", а теперь, у студента есть переменная "паспорт", то есть - паспорт конкретного студента
};
Вопрос: можно ли так делать? Да, безусловно можно!
Сразу следущий вопрос: допустим, мы создали структуру Студент и хотим заполнить ее. Мы присваиваем студенту номер id, пишем его оценки и подходим к моменту, когда надо заполнить данные паспорта. Как мы это делаем? Ну, во-первых, я напомню, как мы работаем с примитивными переменными, лежащими в структуре:
Student student;
student.id = 5; // запись
cout << "The ID: " << student.id << endl; // чтение
По той же логике мы работаем и со структурами, внутри структур:
// запишем и прочтем ID паспорта:
student.passport.id = 12;
cout << "Student ID: " << student.id << " has passport ID: " << student.passport.id << endl;
На экран будет выдано:
Student ID: 5 has passport ID: 12
А можно ли написать так:
struct Student {
int id;
Student s; // то есть в переменной "Студент" держать переменную "Студент"?
};
Если честно - ни разу не пробовал, не думаю, что это скомпилируется, но даже если и да, то уверен, что ничего хорошего из этого не выйдет..... почему? Давайте подумаем, что произойдет, как только мы в коде объявим переменную из типа "Студент":
Student student;
Компилятор начнет ее строить: выделит память под ID, а потом начнет выделять память под "Студента" лежащего внутри. Ему нужно создать переменную студент, в которой лежит: ID и Student. То есть внутри студента будет еще один студент, внутри которого еще один студент, содержащий внутри себя студента..................... думаю, что проблема ясна)))) Если это и скомпилируется, то как только код дойдет до места, в котором впервые создается студент, то скорее всего программа сразу же рухнет, потому что сожрет всю память. Будьте внимательны)))
Как же быть, если по какой-то причине нам нужен студент внутри студента? Мы можем выйти из положения следующим образом:
struct Student {
int id;
Student * s; // то есть в переменной "Студент" лежит УКАЗАТЕЛЬ на студента
};
В таком случае, компилятор, создавая структуру "Студент" создаст в ней указатель, на такую же структуру, но, так как указатель - НЕ сама структура, и чтобы указатель указывал на структуру ее надо создать отдельно, то есть, написать так:
Student student;
student.id = 5;
student.s = new Student;
И тогда, в структуре студент появится новый студент, в котором тоже будет лежать указатель на студента. Этот фокус имеет широчайшее применение и мы увидим как именно используют это фишку, но это будет на следующей лекции, а пока мы научимся работать с указателем на структуру и с данными, которые там лежат.
Работа со структурами через указатель
Я хочу кое что напомнить, а именно:
// у нас есть примитивная переменная:
int num; // объявление
num = 0; // запись
cout << "Number: " << num << endl; // чтение
// у нас есть указатель:
int * p_num; // объявление
p_num = & num; // запись в указатель
cout << "Address of num: " << p_num << endl; // чтение указателя
* p_num = 5; // запись в переменную, на которую указывает указатель
cout << "Number: " << * p_num << endl; // чтение из переменной, на которую указывает указатель
// а так же:
// вспомним, что мы можем, используя указатель, создать переменную в куче:
int * p_num = new int;
// то есть, указатель УЖЕ указывает на переменную! А значит, мы можем далее писать и читать:
* p_num = 5;
cout << "Number: " << * p_num << endl;
Как же работать, когда мы создали указатель на структуру? Допустим, мы сделали как в первом случае:
Student student;
Student * p_student = & student;
Или как во втором:
Student * p_student = new Student;
Как через указатель заполнить поле id, лежащее в студенте? Мы действуем по логике:
(*p_student).id = 5;
то есть, в скобках мы говорим: "дай мне то, на что указывает указатель p_student". А после этого мы обращаемся к тому, что получили через точку. НО! Такая запись неудобна, поэтому, придумали укороченную запись:
p_student->id = 5;
мы ставим значек "стрелочку". Она ставится так: в начале "минус" ( - ), а потом - закрывающая треугольная скобка, тот же символ, который мы пишем в сравнении "больше" ( x > 0 ). Эти 2 символа пишутся один за другим и зрительно получается "стрелочка".
Хочу подчеркнуть: обе записи СОВЕРШЕННО идентичны, но второй - более удобочитаемый, поэтому, почти все программисты (кроме задротов) используют второй способ. В нашем случае, когда в структуре студент есть указатель на студента, то после того, как аллокировали "внутреннего", мы можем заполнить его:
Student student;
student.s->id = 15; // присвоили "внутреннему" студенту, которого мы выше назвали s (плохое название, потому что оно не рассказывает назначение "внутреннего" студента), ID 15.
cout << student.s->id << endl; // прочли ID из "внутреннего" студента
Статические переменные в структурах
Допустим, мы хотим создать какой-нибудь общий параметр для всех студентов в нашем коллежде. Ну, например, проходной балл. В нашем колледже есть 2 факультета: математический и философский. Чтобы оправдать создание двух разных типов, представим, что у них разный набор курсов (что вполне допустимо). Не будем сильно напрягаться, для примера хватит двух. У философского факультета: философия и математика, у математического - математика и физика.
У обоих факультетов есть математика, но так как философам она не так нужна, как математикам, то мы допустим, что проходной балл по математике у философов 40, а у математиков - 60. Наши две структуры определены так:
struct Philosopher {
int math_pass_score;
// другие данные студента
};
struct Mathematician {
int math_pass_score;
// другие данные студента
};
Давайте рассмотрим плюсы и минусы такого подхода. Плюс заключается в том, что "проходной балл по математике" находится в подходящей структуре, то есть во время выполнения программы можно написать код:
Philosopher curr_student;
// инициализация, заполнение данных
// какой-нибудь код
if ( curr_student.grades [MATH] >= curr_student.math_pass_score)
// тут действия которые происходят, когда студент прошел математику
else
// тут - не прошел
То есть, довольно читаемо, упорядочено и прекрасно. А минусы? Ну, во-первых, нужно на инициализации заполнять эту переменную (math_pass_score) значением КАЖДЫЙ раз, когда мы создаем студента. Во-вторых, ОДИНАКОВОЕ значение у всех студентов - копируется из структуры в структуру.... а если нужно поменять это значение? И, конечно же мы не можем не заметить еще один, существенный минус: чтобы, допустим, распечатать на экран значение "проходного балла по математике для философского факультета" - нужно, чтобы БЫЛА ПЕРЕМЕННАЯ этого типа! То есть - нужно конкретную переменную из типа "студент-философ"
Как же правильно и красиво решить эту задачу? И тут на помощь приходят статические переменные. Статическая переменная и ее значение будут принадлежать не КОНКРЕТНОЙ ПЕРЕМЕННОЙ, а ВСЕМУ ТИПУ! То есть, мы напишем ее значение один раз - и это значение будет У ВСЕХ переменных типа "студент-филосов"! Нужно будет поменять? Не проблема! Мы поменяем ОДИН РАЗ! Как описать и заполнить значение такой переменной? В начале, в самой структуре (в ее описании) мы добавим ключевое слово "static", то есть:
struct Philosopher {
static int math_pass_score;
// другие данные студента
};
struct Mathematician {
static int math_pass_score;
// другие данные студента
};
А ниже, перед описанием функций, то есть там, где мы объявляем глобальные переменные, мы напишем:
int Philosopher::math_pass_score = 40;
int Mathematician::math_pass_score = 60;
То есть - имя типа переменной внутри структуры, имя структуры, 2 раза символ "двоеточие", имя статической переменной. Таким образом, значение этой переменной будет известно еще ДО того, как мы создадим переменную типа "студент". А так как эта переменная НЕ принадлежит конкретному студенту (конкретной переменной), а ВСЕМУ типу, то обращаться мы будем к ней через имя типа, а не через имя конкретной переменной. И тоже - через двойное двоеточие:
Philosopher curr_student;
// инициализация, заполнение данных
// какой-нибудь код
if ( curr_student.grades [MATH] >= Philosopher::math_pass_score)
// тут действия которые происходят, когда студент прошел математику
else
// тут - не прошел
Ниже я приведу пример, обобщающий все знания, что мы получили на этой лекции, но пока еще есть о чем поговорить.
Конструкторы и деструкторы
Понятие конструкторов и деструкторов, точно так же, как понятие статических переменных в структуре - это уже чистый С++ ! В С - это не работает. Что же это такое? Это, скажем так, функции, которые описаны в теле структуры. В момент, когда создается переменная из типа структуры, то есть, в момент, когда мы пишем в коде Student curr_student; вызывается функция, которая носит название "конструктор". В момент, когда наша структура выходит из области видимости и ее уничтожает система, ЛИБО, если это была динамическая аллокация и мы пишем delete curr_student;, вызывается функция, которая называется "деструктор".
До сих пор, во всех структурах которые я приводил в качестве примера, мы не видели этих функций. Это происходит потому, что если мы не описываем явно эти функции, компилятор "дарит" нам свои и они - абсолютно пустые. НО! Мы можем описать свои конструкторы и деструкторы и тогда "подарка" от компилятора не будет.
Зачем нам могут пригодиться конструкторы и деструкторы? Для того, чтобы правильно создать или уничтожить нашу структуру. Приведу пример: ну, то, что бросается сразу в глаза - обнуление переменных внутри структуры, то есть, в структуре студент, которую мы уже все хорошо знаем, есть переменная ID. Как мы уже поняли, до того, как мы напишем там значение - там лежит мусор. Мы всегда старались избегать такой ситуации, а тут я почему-то промолчал и ничего не сказал об этом....
Иногда наша структура не имеет смысла без определенного значения внутри. Допустим, не существует студента без номера студенческого билета и мы хотим обязать во время создания студента СРАЗУ ЖЕ присвоить ему такой номер.
Что же по поводу уничтожения? Давайте вспомним, что на предыдущей лекции мы создали студента и динамически аллокировали память под его имя и фамилию. Позже нам пришлось освобождать эту память, прежде чем уничтожить студента. А можно ли сделать это автоматически? Да, можно! Прописав процесс освобождения памяти в деструкторе.
Естественно, так как эти функции - особый вид функций, то они имеют немного отличающийся синтакс. Конструктор и деструктор ОБЯЗАНЫ:
1. Иметь название, совпадающее с названием структуры.
2. НЕ возвращать никаких параметров
3. Деструктор не принимает параметры.
К примеру:
struct Student {
Student( параметры ) { // конструктор
// тело функции
}
~Student ( void ) { // деструктор
// тело функции
}
};
Разберем: как я и обещал, наши "особые" функции не возвращают параметры, их имя совпадает с названием структуры, а деструктор - не принимает параметры. В конструкторе, в скобках, написано "параметры" и это может быть что угодно, включая void. Присмотримся к деструктору: перед ним стоит символ "тильда". Этот символ, на клавиатуре, находится слева от единицы и его вводят, удерживая клавишу "Shift".
Давайте остановимся на таком случае: мы создали конструктор, в студенте, который принимает параметр: ID студента. То, что я сейчас напишу надо усвоить как "отче наш", потому что если допустить эту ошибку, то компилятор откажет, написав причину, которую будет трудно понять, не зная почему она произошла. И тогда, начинающий программист почувствует себя в нехорошем положении, в котором он просто не знает что делать.
struct Student {
Student (long id) { // конструктор
student_id = id;
}
long student_id; // член структуры
};
Как мы видим, действие, которое происходит в конструкторе - мы присваиваем переменной student_id тот id, который мы получили в конструкторе. То есть в коде, переменную "студент" нужно создавать так:
Student student (15); // создаем переменную в стеке
Student * p_student = new Student ( 15 ); // создаем переменную в куче
Обратите внимание! Если написать:
Student student;
или
Student * p_student = new Student;
то это не скомпилируется, потому что конструктора, не принимающего параметры УЖЕ НЕТ. Раньше - он был, мы "получили его в подарок" от компилятора, потому что не написали свой, но как только мы определяем свой конструктор, подарка нет. В принципе можно описать И свой пустой конструктор. Конструкторов может быть неограниченное количество! Но с одним требованием: не должно быть 2 (и более) конструктора, получающих одни и те же типы параметров! В принципе, такой фокус можно сделать с любой функцией и мы увидим это на следующей лекции, на которой я запланировал закрыть образовавшиеся пробелы, но пока - знайте - можно)))
Пока нет пустого конструктора - так же не отработает создание массива вот таким образом:
Student array [ 3 ] ;
потому что в момент создания 3-х студентов в массиве код пытается вызвать пустой конструктор в структуре, которого, как мы уже поняли, нет.
Теперь еще о "подводных камнях":
Допустим структура описана так:
struct Student {
Student (long id) { // конструктор
id = id; // присвоение значения в переменную структуры
}
long id; // переменная структуры (член структуры, на "профессиональном" языке)
};
Первая проблема, которая сразу бросается в глаза, это строка id = id;. Где первый id - должен быть член структуры, а второй id - параметр, который получает конструктор. Как компилятор понимает, что из них - что? Никак не понимает. При условии, что имена совпадают, он выбирает параметр, который передали в конструктор. То есть - в этом коде, присвоение члену структуры не происходит. Как быть?
Есть 2 выхода.
1. Не называть член структуры так же, как мы назвали параметр в конструкторе. Программисты, которые выбирают этот подход, обычно делятся на 2 типа: первые пишут перед или после имени нижнее подчеркивание, то есть, член структуры объявлен так:
long id_;
И тогда, строчка присвоения выглядит так:
id_ = id;
вторые ставят букву m в начале имени, то есть: m_id; (от англ. слова "член" "member")
2. Можно называть параметр и член одинаково, и тогда, если мы имеем ввиду член структуры, то мы пишем перед ним слово this.
this - это указатель, который работает только в структурах и он говорит: "эта структура". Член структуры объявлен как обычно:
long id;
а присвоение происходит так:
this->id = id;
Обратим внимание: так как this это указатель, то после него ставится "стрелочка", а не "точка".
И последнее, что я хотел бы показать:
Более правильно в С++ производить присвоение ДО тела конструктора. То есть не так:
Student (long id) {
id_ = id;
}
А вот так:
Student (long id) : id_(id) {
}
После скобок, в которых перечислены параметры, ставится двоеточие, потом имя члена, скобки и значение, которое в него положить. Если есть несколько членов, которые нужно инициализировать, то они перечисляются через запятую. И тогда, язык гарантирует, что все переменные, которым присвоено значение таким образом, буду проинициализированы еще ДО того, как структура будет создана! Например, если член структуры - ссылка - то это ЕДИНСТВЕННЫЙ способ назначить ей значение!
Теперь о синтаксисе: я, в примерах выше, описывал конструктор и деструктор в теле структуры. НО, как правило так не делают (можно, но так не делают, это не удобочитаемо). Прибегают к такому трюку:
struct Student {
Student ( long id ) ; // только объявление конструктора
long id_;
};
А ниже в коде, среди других функций описывают конструктор:
Student::Student (long id) : id_(id)
{
// тело конструктора
}
То же самое делают и с деструктором.
Окей, я много рассказывал и скорее всего, немного запутал, поэтому, я решил закончить эту лекцию примером кода, в котором я наведу порядок и дам "пощупать" все, что рассказал сегодня:
1. Работа со структурами через указатель
2. Статические переменные в структуре
3. Использование конструктора и деструктора.
Код:







Результат:

Разбор полетов:
Вот мы видим всю сегодняшнюю теорию в практике. Тут есть о чем поговорить и я начну разбирать код.
Начнем сверху. Во-первых, из-за того, что я ниже пользуюсь функциями strlen и strcpy, мне необходимо подключить string.h.
Ниже я описываю структуру, в которой я отмечаю, что есть конструктор и деструктор. Это объявление, код этих функций я опишу ниже. Далее я объявляю "вспомогательные" функции, а после этого - выставляю начальный параметр в статическую переменную структуры. Она получит параметр 0 еще ДО старта программы.
Потом я прошу у пользователя проходной балл по математике. Я сделал так для того, чтобы показать запись в статическую переменную. Я бы мог изначально вместо нуля прописать, например, 50. Но для наглядности я получаю данные с клавиатуры и кладу в переменную, чтобы показать как с ней работать. Я не проверяю правильность ввода пользователя только потому, чтобы не "загружать" код лишней информацией. Но в реальной программе это делать необходимо.
Отлично. Ниже я описываю main. В нем я создаю 2 переменные из типа "Студент", одну в стеке, другую в куче. Обратим внимание, что перед созданием переменных я пишу на экран сообщение. Ниже в конструкторе, я тоже распечатаю на экран сообщения, чтобы было видно, когда отрабатывает конструктор.
После создания структуры я сразу заполняю переменные math_grade в обоих структурах. С первой я работаю через точку, так как это сама переменная, а со второй - через стрелочку, потому что это указатель на переменную.
Потом, я вызываю функцию, которая распечатывает ответ на вопрос "прошел ли студент математику". Посмотрите, что там все ясно.
Примечание: я передаю переменные в функцию через указатель. Почему? Как мы уже поняли, структуры могут быть очень массивные, поэтому, если передать переменную по имени, то во время приема ее в функцию, структура будет копироваться. И в функции мы будем работать с копией, это расточительно. Поэтому лучше передавать структуры либо через указатель, либо через ссылку. А чтобы гарантировать, что в функции данные не будут изменены, можно добавить слово const, превратив переменную в константу (только для этой функции).
В конце я освобождаю динамическую аллокацию, распечатав при этом, строку сообщения. Из деструктора я тоже буду печатать на экран, чтобы показать, что он сработал. Обратите внимание, что деструктор сработал 2 раза (см. на "Результат"). Первый раз он сработал для переменной в куче, когда я написал слово delete, а второй раз - для переменной в стеке. Второй деструктор вызвался "неявно", как только переменная, созданная в стеке покинула область видимости (пределы функции main).
Ниже я описал 2 вспомогательные функции, в которых нет ничего необычного, кроме, пожалуй одного маленького момента: способа написания распечатки из is_math_pass. Так как для компилятора конец строки это точка с запятой, то в принципе, это все - одна строка. Я разбил ее на строки, чтобы удобней было читать.
Конструктор. Первое, что я делаю в конструкторе, я обнуляю члены структуры. Посмотрите, как я это делаю. Это происходит через двоеточие. Это гарантирует то, что эти члены будут "обнулены" еще до того, как начнет работать конструктор. Так делать правильно.
В самом конструкторе я получаю переменные, которые совпадают с именами членов структуры, поэтому, к членам я обращаюсь через this и стрелочку (потому что this - это указатель). На самом деле, обычно я так не делаю, я отношусь к той группе программистов, которые любят называть члены структур с нижним подчеркиванием, то есть: first_name_ и last_name_. Скорее всего, далее в примерах я буду делать так. Сейчас я специально назвал это одинаково, чтобы познакомить вас с указателем this
Я динамически аллокирую места под имена и заполняю их. При этом я пользуюсь стандартными функциями.
В деструкторе мне нужно освободить имена перед тем, как структура будет уничтожена. Если этого не сделать - будет утечка памяти! Но обратите внимание, что в деструкторе я уже не пишу this перед членами! НЕ будет ошибкой, если написать это, но просто в этом случае нет необходимости, так как других кандидатов на эти имена нет (в конструкторе имена членов совпадали с именами параметров, полученных в конструктор).
Хорошенько пройдитесь по коду и убедитесь, что в нем все досконально понятно. Если остаются неясности - нужно переписать код себе и поиграться с ним, меняя его. Обычно, когда щупаешь все сам - становится понятней)))
Это была длинная лекция, которая включала в себя много нового. Не спешите продолжать, прежде чем не усвоите эти знания. Они ВАЖНЫ!
Домашнее задание
Перепишите программу, заданную в качестве домашнего задания на предыдущей лекции, с такими изменениями.
Структура имеет имя, фамилию, оценки и ID-номер студента. ID номер назначается автоматически при создании студента. Предположите, что количество студентов не может выходить за рамки числа, которое может быть сохранено в переменной long. Потрудитесь сделать так, чтобы ID был уникальным для каждого студента. Имя и фамилию студент получает в конструктор. (То есть: при создании переменной студент необходимо указать имя и фамилию)
Введите студентам те же оценки, которые были в прошлом задании и распечатайте имена И ID-номера тех, кто достоин поездки на олимпиаду.
Требование: все студенты должны быть созданы в куче.
Удачи!
@темы: C++