Не обращайтесь к памяти с помощью удаленного указателя.
int* p = new int(7);
// ...
delete p;
// ...
*p = 13; // Ой!
Инструкция
delete p
или код, размещенный после нее, может неосторожно обратиться к значению
*p
или использовать его косвенно. Все эти ситуации совершенно недопустимы. Наиболее эффективной защитой против этого является запрет на использование “голых” операторов
new
, требующих выполнения “голых”
операторов
delete
: выполняйте операторы
new
и
delete
в конструкторах и деструкторах или используйте контейнеры, такие как
Vector_ref
(раздел Д.4).
Не возвращайте указатель на локальную переменную.
int* f
{
int x = 7;
// .. .
return &x;
}
// ...
int* p = f;
// ...
*p = 15; // Ой!
Возврат из функции
f
или код, размещенный после него, может неосторожно обратиться к значению
*p
или использовать его косвенно. Причина заключается в том, что локальные переменные, объявленные в функции, размещаются в стеке перед вызовом функции и удаляются из него при выходе. В частности, если локальной переменной является объект класса, то вызывается его деструктор (см. раздел 17.5.1). Компиляторы не способны распознать большинство проблем, связанных с возвращением указателей на локальные переменные, но некоторые из них они все же выявляют.
Рассмотрим эквивалентный пример.
vector& ff
{
vector x(7);
// ...
return x;
} // здесь вектор х был уничтожен
// ...
vector& p = ff;
// ...
p[4] = 15; // Ой!
Только некоторые компиляторы распознают такую разновидность проблемы, связанной с возвращением указателя на локальную переменную. Обычно программисты недооценивают эти проблемы. Однако многие опытные программисты терпели неудачи, сталкиваясь с бесчисленными вариациями и комбинациями проблем, порожденных использованием простых массивов и указателей. Решение очевидно — не замусоривайте свою программу указателями, массивами, операторами
new
и
delete
. Если же вы поступаете так, то просто быть осторожным в реальной жизни недостаточно. Полагайтесь на векторы, концепцию RAII (“Resource Acquisition Is Initialization” — “Получение ресурса — это инициализация”; см. раздел 19.5), а также на другие систематические подходы к управлению памятью и другими ресурсами.
18.6. Примеры: палиндром
Довольно технических примеров! Попробуем решить маленькую головоломку. Палиндром (palindrome) — это слово, которое одинаково читается как слева направо так и справа налево. Например, слова anna, petep и malayalam являются палиндромами, а слова ida и homesick — нет. Есть два основных способа определить, является ли слово палиндромом.
• Создать копию букв, расположенных в противоположном порядке, и сравнить ее с оригиналом.
• Проверить, совпадает ли первая буква с последней, вторая — с предпоследней, и так далее до середины.
Мы выбираем второй подход. Существует много способов выразить эту идею в коде. Они зависят от представления слова и от способа отслеживания букв в слове. Мы напишем небольшую программу, которая будет по-разному проверять, является ли слово палиндромом. Это просто позволит нам выяснить, как разные особенности языка программирования влияют на внешний вид и работу программы.
18.6.1.
Палиндромы, созданные с помощью класса string
Прежде всего напишем вариант программы, используя стандартный класс
string
, в котором индексы сравниваемых букв задаются переменной типа
int
.
bool is_palindrome(const string& s)
{
int first = 0; // индекс первой буквы
int last = s.length–1; // индекс последней буквы
while (first < last) { // мы еще не достигли середины слова
if (s[first]!=s[last]) return false;
++first; // вперед
––last; // назад
}
return true;
}
Мы возвращаем значение true, если достигли середины слова, не обнаружив разницы между буквами. Предлагаем вам просмотреть этот код и самим убедиться, что он работает правильно, когда в строке вообще нет букв, когда строка состоит только из одной буквы, когда в строке содержится четное количество букв и когда в строке содержится нечетное количество букв. Разумеется, мы не должны полагаться только на логику, стараясь убедиться, что программа работает правильно. Попробуем выполнить функцию
is_palindrome
.
int main
{
string s;
while (cin>>s) {
cout << s << " is";
if (!is_palindrome(s)) cout << " not";
cout << " a palindrome\n";
}
}
По существу, причина, по которой мы используем класс
string
, заключается в том, что объекты класса
string
хорошо работают со словами. Они достаточно просто считывают слова, разделенные пробелами, и знают свой размер. Если бы мы хотели применить функцию
is_palindrome
к строкам, содержащим пробелы, то просто считывали бы их с помощью функции
getline
(см. раздел 11.5). Это можно было бы продемонстрировать на примере строк ah ha и as df fd sa.
18.6.2. Палиндромы, созданные с помощью массива
А если бы у нас не было класса
string
(или
vector
) и нам пришлось бы хранить символы в массиве? Посмотрим.
bool is_palindrome(const char s[], int n)
// указатель s ссылается на первый символ массива из n символов
{
int first = 0; // индекс первой буквы
int last = n–1; // индекс последней буквы
while (first < last) { // мы еще не достигли середины слова
if (s[first]!=s[last]) return false;
++first; // вперед
––last; // назад
}
return true;
}
Для того чтобы выполнить функцию
is_palindrome
, сначала необходимо записать символы в массив. Один из безопасных способов (без риска переполнения массива) выглядит так:
istream& read_word(istream& is, char* buffer, int max)
// считывает не более max–1 символов в массив buffer
{
is.width(max); // при выполнении следующего оператора >>
// будет считано не более max–1 символов
is >> buffer; // читаем слово, разделенное пробелами,