Главная страница
Навигация по странице:

  • 1.8. Массивы, указатели и ссылки 1.8.1. Массивы

  • {1.0, 2.0, 3.0}

  • Использование стандартных контейнеров

  • Использование интеллектуальных указателей

  • 1.8.3. Интеллектуальные указатели

  • 1.8.3.1. unique_ptr

  • Основы C Моим детям. Никогда не смейтесь, помогая мне осваивать


    Скачать 1.68 Mb.
    НазваниеОсновы C Моим детям. Никогда не смейтесь, помогая мне осваивать
    Дата12.10.2022
    Размер1.68 Mb.
    Формат файлаpdf
    Имя файлаsovremennyy-cpp-piter.pdf
    ТипГлава
    #729589
    страница7 из 9
    1   2   3   4   5   6   7   8   9
    77
    int main ()
    { std::ofstream outfile; with_io_exceptions(outfile); outfile.open("f.txt"); double o1= 5.2, o2= 6.2; outfile << o1 << o2 << std::endl; // Нет разделителя outfile.close(); std::ifstream infile; with_io_exceptions(infile); infile.open("f.txt"); int i1, i2; char c; infile >> i1 >> c >> i2; // Несоответствие типов std::cout << "i1 = " << i1 << ", i2 = " << i2 << "\n";
    }
    Тем не менее генерации исключений не происходит, а программа выводит на экран следующий результат:
    i1 = 5, i2 = 26
    Как мы все знаем, тестирование не доказывает правильность программы.
    Это еще более очевидно для ввода-вывода. Входной поток считывает входящие символы и передает их в качестве значения переменной соответствующего типа, например int для i1. Он останавливается на первом же символе, который не мо- жет быть частью значения: сначала — на точке для значения i1 типа int. Если мы читаем после этого еще один int, происходит ошибка, поскольку пустая стро- ка не может рассматриваться как значение типа int. Но ведь мы считываем char, которому вполне соответствует точка. В ходе анализа ввода для i2 мы считываем сначала дробную часть от o1, а затем — целую часть от o2, прежде чем получим символ, который не может принадлежать значению типа int.
    К сожалению, не каждое нарушение грамматических правил на практике вызывает исключение.
    0.3 в ходе анализа в качестве int дает нуль (в то вре- мя как при считывании следующего значения, вероятно, произойдет ошибка);
    -5 в ходе анализа в качестве 32-битового беззнакового целого дает 4 294 967 291.
    Похоже, в потоках ввода-вывода пока не применяется принцип сужения (если он вообще когда-либо будет применяться — из-за обеспечения обратной сов- местимости).
    В любом случае часть приложения, связанная с вводом-выводом, требует осо- бого внимания. Числа должны быть корректно разделены, например, пробелами и считываться в переменные тех же типов, из которых были записаны. Когда вывод осуществляется с ветвлением, так что формат файла может варьироваться, код чтения входных данных оказывается значительно более сложным и даже может быть неоднозначным.
    02_ch01.indd 77 14.07.2016 10:46:41

    Основы C++
    78
    Имеются еще две разновидности ввода-вывода, которые мы хотим отметить: бинарный и ввод-вывод в стиле C. Заинтересовавшийся читатель найдет их в раз- делах A.2.7 и A.2.8 соответственно. Вы можете обратиться к ним позже, когда у вас возникнет такая необходимость.
    1.8. Массивы, указатели и ссылки
    1.8.1. Массивы
    Поддержка встроенных массивов C++ имеет определенные ограничения и кое в чем странное поведение. Тем не менее мы считаем, что каждый программист
    C++ должен из знать и быть осведомлен о возможных проблемах.
    Массив объявляется следующим образом:
    int x[10];
    Переменная x представляет собой массив с десятью элементами типа int.
    В стандарте C++ размер массива должен быть константой и известен во время компиляции. Некоторые компиляторы (например, gcc) поддерживают размеры времени выполнения.
    Обращение к элементам массива осуществляется с помощью квадратных ско- бок. x[i] является ссылкой на i-й элемент массива x. Первым элементом массива является x[0]; последним — x[9]. Массивы могут быть инициализированы при определении:
    float v[] = {1.0, 2.0, 3.0}, w[] = {7.0, 8.0, 9.0};
    В этом случае размер массива выводится компилятором.
    Список инициализации в C++11 не может быть подвергнут сужающему преоб- разованию. На практике это редко вызывает проблемы. Например, код int v[] = {1.0, 2.0, 3.0}; // Ошибка в C++11: сужение корректен в C++03, но не в C++11, поскольку преобразование литерала с плаваю- щей точкой в int потенциально ведет к потере точности. Впрочем, мы в любом случае не будем писать такой уродливый код.
    Операции над массивами обычно выполняются в циклах; например, вычис- ление x = v – 3w как векторная операция выполняется с помощью следующего кода:
    float x[3]; for(int i= 0; i < 3; ++i) x[i] = v[i] - 3.0*w[i];
    Можно определять массивы и более высоких размерностей:
    float A[7][9]; // Матрица 7×9 int q[3][2][3]; // Массив 3×2×3 02_ch01.indd 78 14.07.2016 10:46:42

    1.8. Массивы, указатели и ссылки
    79
    Язык не предоставляет операции линейной алгебры над массивами. Реали- зации, основанные на массивах, безвкусны и подвержены ошибкам. Например, функция для векторного сложения будет выглядеть следующим образом:
    void vector_add(unsigned size, const double v1[],
    const double v2[], double s[])
    { for(unsigned i = 0; i < size; ++i) s[i]= v1[i] + v2[i];
    }
    Обратите внимание, что мы передаем размер массивов в качестве первого па- раметра функции, так как параметры-массивы не содержат информации о разме- ре
    12
    . В этом случае вызывающая функция отвечает за передачу правильного раз- мера массивов:
    int main ()
    { double x[] = {2, 3, 4}, y[] = {4, 2, 0}, sum[3]; vector_add(3, x, y, sum);
    }
    Поскольку размер массива известен во время компиляции, мы можем вычис- лить его путем деления размера массива в байтах на размер одного элемента:
    vector_add(sizeof x / sizeof x[0], x, y, sum);
    При таком старом интерфейсе мы также не в состоянии проверить соответс- твие размеров наших массивов. К сожалению, библиотеки C и Fortran с такими интерфейсами, в которых сведения о размере передаются как аргументы функции, по-прежнему используются и сегодня. Малейшая ошибка пользователя приводит к большим неприятностям, и могут потребоваться огромные усилия для того, что- бы отследить причину сбоев. Поэтому в этой книге мы покажем, как можно реа- лизовать собственное математическое программное обеспечение, более простое в использовании и менее подверженное ошибкам. Хочется верить, что в будущие стандарты C++ будет включено больше высшей математики, в особенности — библиотека для решения задач линейной алгебры.
    Массивы обладают двумя основными недостатками.
    При обращении к массиву не выполняется проверка индексов, так что можно

    легко выйти за его пределы, и программа завершит работу с ошибкой нару- шения сегментации памяти. Это далеко не худший случай; по крайней мере, при этом мы видим, что что-то пошло не так. Неверное обращение к масси- ву может также испортить наши данные. Программа при этом продолжает работать и давать совершенно неправильные результаты — с последствиями,
    12
    При передаче массивов более высоких размерностей только первое измерение может быть от- крытым, в то время как все остальные должны быть известны во время компиляции. Однако такие программы попросту опасны (и уродливы), и C++ предлагает куда лучшие решения.
    02_ch01.indd 79 14.07.2016 10:46:42

    Основы C++
    80
    которые вы можете себе представить сами. Мы даже могли бы перезаписать сам программный код. Тогда данные интерпретируются компьютером как машинные команды, что может привести к любой бессмыслице.
    Размер массива должен быть известен во время компиляции

    13
    . Например, пусть у нас есть массив, сохраненный в файл, и нам надо считать его обрат- но в память:
    ifstream ifs("some_array.dat"); ifs >> size; float v[size]; // Ошибка: размер не известен во время компиляции
    Этот код не будет работать, так как размер массива должен быть известен во время компиляции.
    Первая проблема может быть решена только с помощью массивов нового типа, а вторая — с помощью динамического выделения памяти. Это приводит нас к указателям.
    1.8.2. Указатели
    Указатель представляет собой переменную, которая содержит адрес памяти.
    Этот адрес может быть адресом другой переменной, который мы получаем с по- мощью оператора получения адреса (например,
    &x) или динамически выделенной памяти. Давайте начнем с последнего случая и рассмотрим создание массива ди- намического размера.
    int* y = new int[10];
    Здесь выделена память для массива из десяти элементов типа int. Размер мас- сива может быть выбран во время выполнения. Мы можем также реализовать пример с чтением вектора из предыдущего раздела:
    ifstream ifs("some_array.dat"); int size; ifs >> size;
    float * v = new float[size]; for(int i = 0; i < size; ++i) ifs >> v[i];
    Указатели так же опасны, как и массивы: доступ к данным за пределами диа- пазона может вызвать сбой программы или тихую порчу данных. При работе с динамически выделенными массивами за хранение размера массива отвечает про- граммист.
    Кроме того, программист несет ответственность за освобождение памяти, когда она больше не нужна. Это делается следующим образом:
    13
    Некоторые компиляторы поддерживают значения времени выполнения в качестве размеров массивов. Поскольку такое поведение другими компиляторами не гарантируется, в переносимом про- граммном обеспечении его следует избегать. Рассматривалось включение этой возможности в стан- дарт C++14, но оно было отложено, так как не удалось полностью прояснить все тонкие моменты.
    02_ch01.indd 80 14.07.2016 10:46:42

    1.8. Массивы, указатели и ссылки
    81
    delete[] v;
    В ряде современных языков явное освобождение памяти не требуется, так как при применяемой в них технологии сборки мусора для освобождения памяти достаточно просто сбросить переменную-указатель, присвоив ей нулевое значе- ние (память освобождается и когда переменная выходит из области видимости и уничтожается). В разделе A.2.9 мы даем некоторые комментарии по сборке мусо- ра, которые сводятся к тому, что можно неплохо работать и без нее.
    Поскольку массивы как параметры функции внутренне рассматриваются как указатели, функция vector_add из раздела 1.8.1 может работать и с указателями:
    int main (int argc, char * argv [])
    { double *x = new double[3],
    *y = new double[3],
    *sum = new double[3]; for(unsigned i = 0; i < 3; ++i) x[i] = i+2, y[i] = 4-2*i; vector_add(3,x,y,sum);
    }
    С указателями мы не можем использовать трюк с sizeof; он даст нам лишь размер в байтах самого указателя, который, конечно, не зависит от количества за- писей. Во всех остальных отношениях указатели и массивы являются взаимозаме- няемыми в большинстве ситуаций: как указатель может быть передан в качестве аргумента-массива (как в предыдущем листинге), так и массив может быть пере- дан в качестве аргумента-указателя. Единственное место, где они действительно различаются — это определение. В то время как определение массива размера n резервирует память для n записей, определение указателя оставляет за собой мес- то только для хранения адреса.
    Так как мы начинали с массивов, выполним еще один шаг перед тем как перей- ти к использованию указателей. Простейшее применение указателя — выделение памяти для одного элемента данных:
    int* ip = new int;
    Освобождение памяти в этом случае имеет следующий вид:
    delete ip;
    Обратите внимание на двойственность выделения и освобождения памяти. Вы- деление одного объекта требует освобождения одного объекта, а выделение мас- сива требует освобождения массива. В противном случае система времени выпол- нения будет обрабатывать освобождение памяти неправильно, и, скорее всего, это приведет к аварийному завершению. Указатели также могут ссылаться на другие переменные:
    int i = 3; int* ip2 = &i;
    02_ch01.indd 81 14.07.2016 10:46:42

    Основы C++
    82
    Оператор
    & принимает объект и возвращает его адрес. Обратный оператор * принимает адрес и возвращает объект:
    int j = *ip2;
    Эта операция называется разыменованием. Учитывая приоритеты операторов и правила грамматики, смысл символа
    * как разыменования или умножения невоз- можно спутать — по крайней мере, компилятору.
    Неинициализированные указатели содержат случайные значения (набор битов, содержащийся в соответствующем месте памяти). Применение неинициализированных указателей может привести к самым разнооб- разным ошибкам. Чтобы явно показать, что указатель ни на что не ука- зывает, ему надо при своить значение nullptr:
    int* ip3 = nullptr; // >= C++11 int* ip4 {}; // >= C++11
    или (в старых компиляторах)
    int* ip3 = 0; // В C++11 и более поздних лучше не использовать int* ip4 = NULL; // То же самое
    Гарантируется, что адрес 0 никогда не будет использован в приложени- ях, так что его можно безопасно использовать для обозначения того, что этот указатель является пустым (не указывает ни на что). Тем не менее литерал
    0 не совсем ясно выражает это намерение и может привести к неоднозначности при перегрузке функций. Макрос
    NULL ничуть не луч- ше. Он просто возвращает значение
    0. В C++11 вводится новое ключевое слово nullptr, являющееся литералом указателя. Это значение может присваиваться или сравниваться с указателями любого типа. Поскольку его нельзя спутать с другими типами, а его имя говорит само за себя, его применение предпочтительнее, чем использование любых других обоз- начений. Инициализация с помощью пустого списка в фигурных скоб- ках также устанавливает указатель равным nullptr.
    Наибольшая опасность указателей — утечки памяти. Например, наш массив y оказывается слишком маленьким, и мы хотим присвоить этой переменной но- вый массив:
    int* y = new int[15];
    Теперь мы можем использовать больше памяти с помощью y. Отлично! Но что происходит с памятью, выделенной ранее? Она все еще выделена, но у нас больше нет к ней доступа. Мы не можем даже освободить ее, потому что для этого нужно знать ее адрес. Эта память оказывается потерянной для нашей программы. Только тогда, когда программа завершится, операционная система сможет ее освободить.
    В нашем примере мы потеряли только 40 байт из нескольких гигабайтов, кото- рые мы можем использовать. Но если такая потеря происходит в итеративном
    C++11
    C++11 02_ch01.indd 82 14.07.2016 10:46:43

    1.8. Массивы, указатели и ссылки
    83
    процессе, объем неиспользуемой памяти непрерывно увеличивается до тех пор, пока в какой-то момент не будет исчерпана вся (виртуальная) память.
    Даже если трата памяти впустую не является критической для разрабатывае- мого приложения, когда мы пишем научное программное обеспечение высокого качества, утечки памяти совершенно неприемлемы. Если ваше программное обес- печение предназначено для использования многими пользователями, рано или поздно утечка будет обнаружена, и даже если она не приведет к сбоям програм- мы, она приведет к потере доверия к ее качеству, а в конечном счете — к отказу пользователей от вашего программного обеспечения. К счастью, имеются инстру- менты, которые могут помочь найти утечки памяти (см. раздел Б.3).
    Описанные проблемы, возникающие при работе с указателями, показаны не для того, чтобы вас запугать. И мы вовсе не рекомендуем отказываться от указа- телей. Многое можно сделать только с помощью указателей — списки, очереди, деревья, графы и т.д. Однако указатели должны использоваться с осторожностью, чтобы полностью избежать всех серьезных проблем, упомянутых выше.
    Минимизировать ошибки, связанные с указателями, можно с помощью трех стратегий.
    Использование стандартных контейнеров из стандартной библиотеки или других проверенных библиотек. std::vector из стандартной библиотеки предоставляет нам всю функциональность динамических массивов, вклю- чая изменение размеров и проверку выхода за границы диапазона, а также автоматическое освобождение памяти.
    Инкапсуляция управления динамической памятью в классах. В таком случае мы должны справиться со всеми проблемами только один раз — в классе
    14
    Если вся память, выделенная объектом, освобождается при уничтожении объекта, то не важно, как часто мы выделяем память. Если у нас есть 738 объек тов с динамической памятью, то она будет освобождена 738 раз. Память должна выделяться при создании объекта и освобождаться при его уничтоже- нии. Этот принцип называется Захват ресурса есть инициализация (Resource acquisition is initialization — RAII). И наоборот, если мы вызвали new 738 раз, частично в цикле и ветвях, то как мы можем быть уверены, что мы вызовем delete тоже точно 738 раз? Мы знаем, что имеется соответствующий инстру- ментарий, но ошибки легче не допускать, чем исправлять
    15
    . Конечно, идея ин- капсуляции тоже не “защищена от дурака”, но она требует гораздо меньших усилий для корректной работы, чем разбрасывание обычных указателей по всей программе. Идиому RAII мы обсудим более подробно в разделе 2.4.2.1.
    Использование интеллектуальных указателей, о которых мы поговорим в следующем разделе, 1.8.3.
    14
    Обычно объектов в программе гораздо больше, чем классов; в противном случае что-то не так в самом дизайне программы.
    15
    Кроме того, соответствующий инструментарий может указать на отсутствие утечки только для данного конкретного выполнения программы, но она может проявиться для других входных данных.
    02_ch01.indd 83 14.07.2016 10:46:43

    Основы C++
    84
    Указатели служат двум целям:
    указание на объекты,

    управление динамической памятью.

    Проблема с обычными указателями заключается в том, что мы не знаем, ука- зывает ли указатель на некоторые данные или он также отвечает за освобождение памяти, когда та больше не нужна. Чтобы иметь возможность такого явного раз- личия на уровне типов, мы можем использовать интеллектуальные указатели.
    1.8.3. Интеллектуальные указатели
    В C++11 введены три новых типа интеллектуальных указателей: unique_ptr, shared_ptr и weak_ptr. Имеющийся в C++03 интеллектуальный указатель auto_ptr обычно считается неудачной попыткой на пути к unique_ptr, посколь- ку язык в то время еще не был к этому готов. Использовать интеллектуальный указатель auto_ptr больше не следует. Все интеллектуальные указатели опреде- ляются в заголовочном файле
    . Если вы не можете использовать на сво- ей платформе возможности C++11, примите к сведению, что достойной заменой являются интеллектуальные указатели из библиотеки Boost.
    1.8.3.1.
    unique_ptr
    Имя этого указателя говорит о том, что он является единственным указателем на свои данные. Он может быть использован, по сути, так же, как и обычный ука- затель:
    #include int main ()
    {
    1   2   3   4   5   6   7   8   9


    написать администратору сайта