Семинар 3 (18.09.2015)
Как "правильно" писать программы на C++ (часть 1)
Один из наиболее важных принципов при программировании на C++ можно сформулировать следующим образом:
не смешивайте C и C++.
Несмотря на то, что язык C++ позволяет так делать, и то, что программа на C почти всегда является валидной программой на C++, на практике такое смешивание приводит к запутыванию кода и ошибкам.
Как понять, что вы смешиваете два языка?
-
Использование библиотечных функций C. Если в коде подключены библиотеки
<stdio.h>
,<stdlib.h>
,<string.h>
, то это повод провести рефакторинг и использовать C++ аналоги. (Хотя есть некоторые исключения, например). -
Как следствие предыдущего пункта: использование для ввода/вывода функций
printf
,scanf
и других. Вместо них предпочтительнее использовать потоки ввода/вывода (a.k.a. streams, не путать с threads!). -
Строки. Используйте std::string вместо
char*
. -
Использование "сырых" указателей. Старайтесь использовать либо непосредственно объекты, либо ссылки. Конечно, совсем без указателей не обойтись (например, как в случае со связным списком), поэтому старайтесь придерживаться следующих правил:
-
"Спрячьте" всю работу с указателями внутрь реализации класса - пользователь класса не должен думать о том, что делать с указателями, например, нужно ли удалять пямять, после работы с указателем.
-
Если работы с указателями не избежать, используйте "умные" указатели: unique_ptr и shared_ptr. Но надо помнить, что работа с ними имеет свои "особенности".
-
Небольшой пример, демонстрирующий сложности при работе с указателями:
void doSomeWork() {
// Никогда так не пишите!
// 1. оператор new - надо не забыть в конце сделать delete
std::string * str = new std::string("hello world");
if (str->find('o') != std::string::npos) {
// 2. Ещё одна точка выхода из функции - нужно удалить str:
delete str;
return
}
// Вызов функции, что будет если она выбросит исключение?
// Можно конечно обернуть её в try-catch (или обернуть всё в try-catch)
// Но выглядеть будет не очень аккуратно
doSomeOtherWork();
if (str->size() > 10) {
// Ещё одна точка выхода. Это начинает напоминать тот хаос,
// который был с управлением ресурсами в C.
delete str;
return;
}
delete str;
}
Ещё один пример:
char * getSomeString();
// ...
char * str = getSomeString();
// do something with str...
// Нужно ли удалять память при помощи free или это указатель на статически
// аллоцированный участок памяти? Может быть нужно удалять при помощи какой-то другой функции?
// free(str);
// Лучше так:
std::string getSomeString();
-
Вместо
malloc
иfree
нужно использоватьnew
иdelete
(в случае необходимости). -
Использование "традиционных" массивов. Стоит отметить, что в C++ есть форма оператора
new
, которая позволяет создавать массивы:
int * list = new int[5]{1, 2, 3};
for (int i=0; i<5; i++) {
std::cout << list[i] << std::endl;
}
delete[] list;
однако предпочтительнее (то есть, используйте всегда :) ) использовать std::vector.
-
Ключевые преимущества вектора:
-
метод
size()
, позволяющий узнать длину контейнера -
оператор доступа к элементу (
[]
) внутри себя проверяет выход за границы массива -
У вектора много других полезных методов
-
С вектором работает "foreach-like for":
-
std::vector<int> v = {1, 2, 4, 8};
for (int i=0; i<v.size(); i++) {
std::cout << v[i] << std::endl;
}
В целом же, использование вектора мало отличается от использования массива:
std::vector<int> v(5); // вектор длины 5
v[3] = 13;
for (int i=0; i<v.size(); i++) {
std::cout << v[i] << std::endl;
}
- Помимо вектора в стандартной бииблиотеке есть и другие контейнеры.