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

  • Упражнение 3.33. Что будет, если не инициализировать массив scores в программе оценок из данного раздела

  • Язык программирования C Пятое издание


    Скачать 1.85 Mb.
    НазваниеЯзык программирования C Пятое издание
    Дата15.07.2019
    Размер1.85 Mb.
    Формат файлаpdf
    Имя файла620354-www.libfox.ru.pdf
    ТипДокументы
    #84130
    страница9 из 54
    1   ...   5   6   7   8   9   10   11   12   ...   54
    Упражнение 3.26. Почему в программе двоичного поиска использован код mid = beg + (end - beg) / 2;, а не mid = (beg + end) / 2;?
    3.5. Массивы
    Массив (array) — это структура данных, подобная библиотечному типу vector (см. раздел 3.3),
    но с другим соотношением между производительностью и гибкостью. Как и вектор, массив является контейнером безымянных объектов одинакового типа, к которым обращаются по позиции. В отличие от вектора, массивы имеют фиксированный размер; добавлять элементы к массиву нельзя. Поскольку размеры массивов постоянны, они иногда обеспечивают лучшую производительность во время выполнения приложений. Но это преимущество приобретается за счет потери гибкости.
    Если вы не знаете точно, сколько элементов необходимо, используйте вектор.
    3.5.1. Определение и инициализация встроенных массивов
    Массив является составным типом (см. раздел 2.3). Оператор объявления массива имеет форму a[d], где а — имя; d — размерность определяемого массива. Размерность задает количество элементов массива, она должна быть больше нуля. Количество элементов — это часть типа массива, поэтому она должна быть известна на момент компиляции.
    Page 145/1103

    Следовательно, размерность должна быть константным выражением (см. раздел 2.4.4). unsigned cnt = 42; // неконстантное выражение constexpr unsigned sz = 42; // константное выражение
    // constexpr см. p. 2.4.4 int arr[10]; // массив десяти целых чисел int *parr[sz]; // массив 42 указателей на int string bad[cnt]; // ошибка: cnt неконстантное выражение string strs[get_size()]; // ok, если get_size - constexpr,
    // в противном случае - ошибка
    По умолчанию элементы массива инициализируются значением по умолчанию (раздел 2.2.1).
    Подобно переменным встроенного типа, инициализированный по умолчанию массив встроенного типа, определенный в функции, будет содержать неопределенные значения.
    При определении массива необходимо указать тип его элементов. Нельзя использовать спецификатор auto для вывода типа из списка инициализаторов. Подобно вектору, массив содержит объекты. Таким образом, невозможен массив ссылок. Явная инициализация элементов массива
    Массив допускает списочную инициализацию (см. раздел 3.3.1) элементов. В этом случае размерность можно опустить. Если размерность отсутствует, компилятор выводит ее из количества инициализаторов. Если размерность определена, количество инициализаторов не должно превышать ее.
    Если размерность больше количества инициализаторов, то инициализаторы используются для первых элементов, а остальные инициализируются по умолчанию (см. раздел 3.3.1): const unsigned sz = 3; int ia1[sz] = {0, 1, 2}; // массив из трех целых чисел со
    Page 146/1103

    // значениями 0, 1, 2 int a2[] = {0, 1, 2}; // массив размером 3 элемента int a3[5] = {0, 1, 2}; // эквивалент a3[] = {0, 1, 2, 0, 0} string a4[3] = {"hi", "bye"}; // эквивалент a4[] = {"hi", "bye", ""} int a5[2] = {0, 1, 2}; // ошибка: слишком много инициализаторов Особенности символьных массивов
    У символьных массивов есть дополнительная форма инициализации: строковым литералом
    (см. раздел 2.1.3). Используя эту форму инициализации, следует помнить, что строковые литералы заканчиваются нулевым символом. Этот нулевой символ копируется в массив наряду с символами литерала. char a1[] = {'C', '+', '+'}; // списочная инициализация без
    // нулевого символа char а2[] = {'C', '+', '+', '\0'}; // списочная инициализация с явным
    // нулевым символом char a3[] = "С++"; // нулевой символ добавляется
    // автоматически const char a4[6] = "Daniel"; // ошибка: нет места для нулевого
    // символа!
    Массив a1 имеет размерность 3; массивы а2 и a3 — размерности 4. Определение массива a4
    ошибочно. Хотя литерал содержит только шесть явных символов, массив a4 должен иметь по
    Page 147/1103
    крайней мере семь элементов, т.е. шесть для самого литерала и один для нулевого символа.
    Не допускается ни копирование, ни присвоение
    Нельзя инициализировать массив как копию другого массива, не допустимо также присвоение одного массива другому. int a[] = {0, 1, 2}; // массив из трех целых чисел int a2[] = a; // ошибка: нельзя инициализировать один массив
    // другим а2 = a; // ошибка: нельзя присваивать один массив другому
    Некоторые компиляторы допускают присвоение массивов при применении расширения компилятора (compiler extension). Как правило, использования нестандартных средств следует избегать, поскольку они не будут работать на других компиляторах.Понятие сложных объявлений массива
    Как и векторы, массивы способны содержать объекты большинства типов. Например, может быть массив указателей. Поскольку массив — это объект, можно определять и указатели, и ссылки на массивы. Определение массива, содержащего указатели, довольно просто,
    определение указателя или ссылки на массив немного сложней. int *ptrs[10]; // ptrs массив десяти указателей на int int &refs[10] = /* ? */; // ошибка: массив ссылок невозможен int (*Parray)[10] = &arr; //
    Parray указывает на массив из десяти int int (&arrRef)[10] = arr; // arrRef ссылается на массив из десяти ints
    Обычно модификаторы типа читают справа налево. Читаем определение ptrs справа налево
    (см. раздел 2.3.3): определить массив размером 10 по имени ptrs для хранения указателей на тип int.
    Определение Parray также стоит читать справа налево. Поскольку размерность массива следует за объявляемым именем, объявление массива может быть легче читать изнутри наружу, а не справа налево. Так намного проще понять тип Parray. Объявление начинается с круглых скобок вокруг части *Parray, означающей, что Parray — указатель. Глядя направо,
    можно заметить, что указатель Parray указывает на массив размером 10. Глядя влево, можно
    Page 148/1103
    заметить, что элементами этого массива являются целые числа. Таким образом, Parray —
    это указатель на массив из десяти целых чисел. Точно так же часть (&arrRef) означает,
    что arrRef — это ссылка, а типом, на который она ссылается, является массив размером 10,
    хранящий элементы типа int.
    Конечно, нет никаких ограничений на количество применяемых модификаторов типа. int *(&arry)[10]=ptrs; // arry - ссылка на массив из десяти указателей
    Читая это объявление изнутри наружу, можно заметить, что arry — это ссылка. Глядя направо, можно заметить, что объект, на который ссылается arry, является массивом размером 10. Глядя влево, можно заметить, что типом элемента является указатель на тип int. Таким образом, arry — это ссылка на массив десяти указателей.
    Зачастую объявление массива может быть проще понять, начав его чтение с имени массива и продолжив его изнутри наружу. Упражнения раздела 3.5.1
    Упражнение 3.27. Предположим, что функция txt_size() на получает никаких аргументов и возвращают значение типа int. Объясните, какие из следующих определений недопустимы и почему? unsigned buf_size = 1024;
    (a) int ia[buf_size]; (b) int ia[4 * 7 - 14];
    (c) int ia[txt_size()]; (d) char st[11] = "fundamental";
    Упражнение 3.28. Какие значения содержатся в следующих массивах? string sa[10]; int ia[10]; int main() { string sa2[10]; int ia2[10];
    }
    Упражнение 3.29. Перечислите некоторые из недостатков использования массива вместо вектора.
    3.5.2. Доступ к элементам массива
    Подобно библиотечным типам vector и string, для доступа к элементам массива можно использовать серийный оператор for или оператор индексирования ([]) (subscript). Как обычно, индексы начинаются с 0. Для массива из десяти элементов используются индексы от 0 до 9, а не от 1 до 10.
    Page 149/1103

    При использовании переменной для индексирования массива ее обычно определяют как имеющую тип size_t. Тип size_t — это машинозависимый беззнаковый тип, гарантированно достаточно большой для содержания размера любого объекта в памяти. Тип size_t определен в заголовке cstddef, который является версией С++ заголовка stddef.h библиотеки
    С.
    За исключением фиксированного размера, массивы используются подобно векторам.
    Например, можно повторно реализовать программу оценок из раздела 3.3.3, используя для хранения счетчиков кластеров массив.
    // подсчет количества оценок в кластере по десять: 0--9,
    //
    10--19, ... 90--99, 100 unsigned scores[11] = {}; //
    11 ячеек, все со значением 0 unsigned grade; while (cin >> grade) { if (grade <= 100)
    ++scores[grade/10]; // приращение счетчика текущего кластера
    }
    Единственное очевидное различие между этой программой и приведенной в разделе 3.3.3 в объявлении массива scores. В данной программе это массив из 11 элементов типа unsigned.
    Не столь очевидно то различие, что оператор индексирования в данной программе тот,
    который определен как часть языка. Этот оператор применяется с операндами типа массива.
    Оператор индексирования, используемый в программе в разделе 3.3.3, был определен библиотечным шаблоном vector и применялся к операндам типа vector.
    Как и в случае строк или векторов, для перебора всего массива лучше использовать серийный оператор for. Например, все содержимое массива scores можно отобразить следующим образом: for (auto i : scores) // для каждого счетчика в scores cout << i << " "; // отобразить его значение cout << endl;
    Поскольку размерность является частью типа каждого массива, системе известно количество элементов в массиве scores. Используя средства серийного оператора for, перебором можно управлять и не самостоятельно. Проверка значений индекса
    Page 150/1103

    Как и в случае со строкой и вектором, ответственность за невыход индекса за пределы массива лежит на самом программисте. Он сам должен гарантировать, что значение индекса будет больше или равно нулю, но не больше размера массива. Ничто не мешает программе перешагнуть границу массива, кроме осторожности и внимания разработчика, а также полной проверки кода. В противном случае программа будет компилироваться и выполняться правильно, но все же содержать скрытую ошибку, способную проявиться в наименее подходящий момент.
    Наиболее распространенным источником проблем защиты приложений является ошибка переполнения буфера. Причиной такой ошибки является отсутствие в программе проверки индекса, в результате чего программа ошибочно использует память вне диапазона массива или подобной структуры данных. Упражнения раздела 3.5.2
    Упражнение 3.30. Выявите ошибки индексации в следующем коде constexpr size_t array size = 10; int ia[array_size]; for (size_t ix = 1; ix <= array size; ++ix) ia[ix] = ix;
    Упражнение 3.31. Напишите программу, где определен массив из десяти целых чисел,
    каждому элементу которого присвоено значение, соответствующее его позиции в массиве.
    Упражнение 3.32. Скопируйте массив, определенный в предыдущем упражнении, в другой массив. Перезапишите эту программу так, чтобы использовались векторы.

    Упражнение 3.33. Что будет, если не инициализировать массив scores в программе оценок из данного раздела?
    3.5.3. Указатели и массивы
    Указатели и массивы в языке С++ тесно связаны. В частности, как будет продемонстрировано вскоре, при использовании массивов компилятор обычно преобразует их в указатель.
    Обычно указатель на объект получают при помощи оператора обращения к адресу (см.
    раздел 2.3.2). По правде говоря, оператор обращения к адресу может быть применен к любому объекту, а элементы в массиве — объекты. При индексировании массива результатом является объект в этой области массива. Подобно любым другим объектам,
    указатель на элемент массива можно получить из адреса этого элемента: string nums[] = {"one", "two", "three"}; // массив строк string *p = &nums[0]; // p указывает на первый элемент массива nums
    Однако у массивов есть одна особенность — места их использования компилятор автоматически заменяет указателем на первый элемент.
    Page 151/1103
    string *p2 = nums; // эквивалент p2 = &nums[0]
    В большинстве выражений, где используется объект типа массива, в действительности используется указатель на первый элемент в этом массиве.
    Существует множество свидетельств того факта, что операции с массивами зачастую являются операциями с указателями. Одно из них — при использовании массива как инициализатора переменной, определенной с использованием спецификатора auto (см.
    раздел 2.5.2), выводится тип указателя, а не массива. int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia - массив из десяти целых чисел auto ia2(ia); // ia2 - это int*, указывающий на первый элемент в ia ia2 = 42; // ошибка: ia2 - указатель, нельзя присвоить указателю
    // значение типа int
    Хотя ia является массивом из десяти целых чисел, при его использовании в качестве инициализатора компилятор рассматривает это как следующий код: auto ia2(&ia[0]); // теперь ясно, что ia2 имеет тип int*
    Следует заметить, что это преобразование не происходит, если используется спецификатор decltype (см. раздел 2.5.3). Выражение decltype(ia) возвращает массив из десяти целых чисел:
    // ia3 - массив из десяти целых чисел decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9}; ia3 = p; // ошибка: невозможно присвоить int* массиву ia3[4] = i; // ok: присвоить значение i элементу в массиве ia3 Указатели — это итераторы
    Указатели, содержащие адреса элементов в массиве, обладают дополнительными возможностями, кроме описанных в разделе 2.3.2. В частности, указатели на элементы массивов поддерживают те же операции, что и итераторы векторов или строк (см. раздел
    3.4). Например, можно использовать оператор инкремента для перемещения с одного элемента массива на следующий:
    Page 152/1103
    int arr[] = {0,1,2,3,4,5,6,7,8,9}; int *p = arr; // p указывает на первый элемент в arr
    ++p; // p указывает на arr[1]
    Подобно тому, как итераторы можно использовать для перебора элементов вектора,
    указатели можно использовать для перебора элементов массива. Конечно, для этого нужно получить указатели на первый элемент и элемент, следующий после последнего. Как упоминалось только что, указатель на первый элемент можно получить при помощи самого массива или при обращении к адресу первого элемента. Получить указатель на следующий элемент после последнего можно при помощи другого специального свойства массива.
    Последний элемент массива arr находится в позиции 9, а адрес несуществующего элемента массива, следующего после него, можно получить так: int *е = &arr[10]; // указатель на элемент после
    // последнего в массиве arr
    Единственное, что можно сделать с этим элементом, так это получить его адрес, чтобы инициализировать указатель е. Как и итератор на элемент после конца (см. раздел 3.4.1),
    указатель на элемент после конца не указывает ни на какой элемент. Поэтому нельзя ни обратиться к его значению, ни прирастить.
    Используя эти указатели, можно написать цикл, выводящий элементы массива arr. for (int *b = arr; b != e; ++b) cout << *b << endl; // вывод элементов arr Библиотечные функции begin() и end()
    Указатель на элемент после конца можно вычислить, но этот подход подвержен ошибкам.
    Чтобы облегчить и обезопасить использование указателей, новая библиотека предоставляет две функции: begin() и end(). Эти функции действуют подобно одноименным функциям-членам контейнеров (см. раздел 3.4.1). Однако массивы — не классы, и данные функции не могут быть функциями-членами. Поэтому для работы они получают массив в качестве аргумента. int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia - массив из десяти целых чисел int *beg = begin(ia); // указатель на первый элемент массива ia int *last = end(ia); // указатель на следующий элемент ia за последним
    Page 153/1103

    Функция begin() возвращает указатель на первый, а функция end() на следующий после последнего элемент данного массива. Эти функции определены в заголовке iterator.
    Используя функции begin() и end(), довольно просто написать цикл обработки элементов массива. Предположим, например, что массив arr содержит значения типа int. Первое отрицательное значение в массиве arr можно найти следующим образом:
    // pbeg указывает на первый, a pend на следующий после последнего
    // элемент массива arr int *pbeg = begin(arr), *pend = end(arr);
    // найти первый отрицательный элемент, остановиться, если просмотрены
    // все элементы while (pbeg != pend && *pbeg >= 0)
    ++pbeg;
    Код начинается с определения двух указателей типа int по имени pbeg и pend. Указатель pbeg устанавливается на первый элемент массива arr, a pend — на следующий элемент после последнего. Условие цикла while использует указатель pend, чтобы узнать, безопасно ли обращаться к значению указателя pbeg. Если указатель pbeg действительно указывает на элемент, выполняется проверка результата обращения к его значению на наличие отрицательного значения. Если это так, то условие ложно и цикл завершается. В противном случае указатель переводится на следующий элемент.
    Указатель на элемент "после последнего" у встроенного массива ведет себя так же, как итератор, возвращенный функцией end() вектора. В частности, нельзя ни обратиться к значению такого указателя, ни осуществить его приращение. Арифметические действия с указателями
    Указатели на элементы массива позволяют использовать все операции с итераторами,
    перечисленные в табл. 3.6 и 3.7. Эти операции, обращения к значению, инкремента,
    сравнения, добавления целочисленного значения, вычитания двух указателей, имеют для указателей на элементы встроенного массива то же значение, что и для итераторов.
    Результатом добавления (или вычитания) целочисленного значения к указателю (или из него)
    является новый указатель, указывающий на элемент, расположенный на заданное количество позиций вперед (или назад) от исходного указателя. constexpr size_t sz = 5; int arr[sz] = {1,2,3,4,5}; int *ip = arr; // эквивалент int *ip = &arr[0]
    Page 154/1103
    int *ip2 = ip + 4; // ip2 указывает на arr[4], последний элемент в arr
    Результатом добавления 4 к указателю ip будет указатель на элемент, расположенный в массиве на четыре позиции далее от того, на который в настоящее время указывает ip.
    Результатом добавления целочисленного значения к указателю должен быть указатель на элемент (или следующую позицию после конца) в том же массиве:
    // ok: arr преобразуется в указатель на его первый элемент;
    // p указывает на позицию после конца arr int *p = arr + sz; // использовать осмотрительно - не обращаться
    // к значению! int *p2 = arr + 10; // ошибка: arr имеет только 5 элементов;
    // значение p2 неопределенно
    При сложении arr и sz компилятор преобразует arr в указатель на первый элемент массива arr. При добавлении sz к этому указателю получается указатель на позицию sz (т.е. на позицию 5) этого массива. Таким образом, он указывает на следующую позицию после конца массива arr. Вычисление указателя на более чем одну позицию после последнего элемента является ошибкой, хотя компилятор таких ошибок не обнаруживает.
    Подобно итераторам, вычитание двух указателей дает дистанцию между ними. Указатели должны указывать на элементы в том же массиве: auto n = end(arr) - begin(arr); // n - 5, количество элементов
    // массива arr
    Результат вычитания двух указателей имеет библиотечный тип ptrdiff_t. Как и тип size_t, тип ptrdiff_t является машинозависимым типом, определенным в заголовке cstddef. Поскольку вычитание способно возвратить отрицательное значение, тип ptrdiff_t — знаковый целочисленный.
    Для сравнения указателей на элементы (или позицию за концом) массива можно использовать операторы сравнения. Например, элементы массива arr можно перебрать
    Page 155/1103
    следующим образом: int *b = arr, *е = arr + sz; while (b < e) {
    // используется *b
    ++b;
    }
    Нельзя использовать операторы сравнения для указателей на два несвязанных объекта. int i = 0, sz = 42; int *p = &i, *е = &sz;
    // неопределенно: p и е не связаны; сравнение бессмысленно! while (p < е)
    Хотя на настоящий момент смысл может быть и неясен, но следует заметить, что арифметические действия с указателями допустимы также для нулевых указателей (см.
    раздел 2.3.2) и для указателей на объекты, не являющиеся массивом. В последнем случае указатели должны указывать на тот же объект или следующий после него. Если p — нулевой указатель, то к нему можно добавить (или вычесть) целочисленное константное выражение
    (см. раздел 2.4.4) со значением 0. Можно также вычесть два нулевых указателя из друг друга,
    и результатом будет 0. Взаимодействие обращения к значению с арифметическими действиями с указателями
    Результатом добавления целочисленного значения к указателю является указатель. Если полученный указатель указывает на элемент, то к его значению можно обратиться: int ia[] = {0,2,4,6,8}; // массив из 5 элементов типа int int last = *(ia + 4); // ok: инициализирует last значением
    // ia[4], т.е. 8
    Выражение *(ia + 4) вычисляет адрес четвертого элемента после ia и обращается к значению полученного указателя. Это выражение эквивалентно выражению ia[4].
    Помните, в разделе 3.4.1 обращалось внимание на необходимость круглых скобок в выражениях, содержащих оператор обращения к значению и точечный оператор. Аналогично необходимы круглые скобки вокруг части сложения указателей: last = *ia + 4; //
    Page 156/1103
    ok: last = 4, эквивалент ia[0] + 4
    Этот код обращается к значению ia и добавляет 4 к полученному значению. Причины подобного поведения рассматриваются в разделе 4.1.2. Индексирование и указатели
    Как уже упоминалось, в большинстве мест, где используется имя массива, в действительности используется указатель на первый элемент этого массива. Одним из мест,
    где компилятор осуществляет это преобразование, является индексирование массива. int ia[] = {0,2,4,6,8}; // массив из 5 элементов типа int
    Рассмотрим выражение ia[0], использующее имя массива. При индексировании массива в действительности индексируется указатель на элемент в этом массиве. int i = ia[2]; // ia преобразуется в указатель на первый элемент ia
    // ia[2] выбирает элемент, на который указывает (ia + 2) int *p = ia; // p указывает на первый элемент в массиве ia i = *(p + 2); // эквивалент i = ia[2]
    Оператор индексирования можно использовать для любого указателя, пока он указывает на элемент (или позицию после конца) в массиве. int *p = &ia[2]; // p указывает на элемент с индексом 2 int j = p[1]; // p[1] - эквивалент *(p + 1),
    // p[1] тот же элемент, что и ia[3] int k = p[-2]; // p[-2] тот же элемент, что и ia[0]
    Последний пример указывает на важное отличие между массивами и такими библиотечными типами, как vector и string, у которых есть операторы индексирования. Библиотечные типы требуют, чтобы используемый индекс был беззнаковым значением. Встроенный оператор индексирования этого не требует. Индекс, используемый со встроенным оператором индексирования, может быть отрицательным значением. Конечно, полученный адрес должен указывать на элемент (или позицию после конца) массива, на который указывает первоначальный указатель.
    Page 157/1103

    В отличие от индексов для векторов и строк, индекс встроенного массива не является беззнаковым. Упражнения раздела 3.5.3
    Упражнение 3.34. С учетом, что указатели p1 и p2 указывают на элементы в том же массиве,
    что делает следующий код? Какие значения p1 или p2 делают этот код недопустимым? p1 += p2 - p1;
    Упражнение 3.35. Напишите программу, которая использует указатели для обнуления элементов массива.
    Упражнение 3.36. Напишите программу, сравнивающую два массива на равенство. Напишите подобную программу для сравнения двух векторов.
    3.5.4. Символьные строки в стиле С
    Хотя язык С++ поддерживает строки в стиле С, использовать их в программах С++ не следует. Строки в стиле С — на удивление богатый источник разнообразных ошибок и наиболее распространенная причина проблем защиты.
    Символьный строковый литерал — это экземпляр более общей конструкции, которую язык
    С++ унаследовал от языка С: символьной строки в стиле С (C-style character string). Строка в стиле С не является типом данных, скорее это соглашение о представлении и использовании символьных строк.
    Следующие этому соглашению строки хранятся в символьных массивах и являются строкой с завершающим нулевым символом (null-terminated string). Под завершающим нулевым символом подразумевается, что последний видимый символ в строке сопровождается нулевым символом ('\0'). Для манипулирования этими строками обычно используются указатели.Строковые функции библиотеки С
    Стандартная библиотека языка С предоставляет набор функций, перечисленных в табл. 3.8,
    для работы со строками в стиле С. Эти функции определены в заголовке cstring, являющемся версией С++ заголовка языка С string.h.
    Функции из табл. 3.8 не проверяют свои строковые параметры
    Указатель (указатели), передаваемый этим функциям, должен указывать на массив (массивы)
    с нулевым символом в конце. char ca[] = {'C', '+', '+'}; // без нулевого символа в конце cout << strlen(ca) << endl; // катастрофа: ca не завершается нулевым
    // символом
    Page 158/1103

    В данном случае ca — это массив элементов типа char, но он не завершается нулевым символом. Результат непредсказуем. Вероятней всего, функция strlen() продолжит просматривать память уже за пределами массива ca, пока не встретит нулевой символ.
    Таблица 3.8. Функции для символьных строк в стиле С strlen(p) Возвращает длину строки p без учета нулевого символа strcmp(p1, p2) Проверяет равенство строк p1 и p2. Возвращает 0,
    если p1 == p2, положительное значение, если p1 > p2, и отрицательное значение, если p1
    < p2 strcat(p1, p2) Добавляет строку p2 к p1. Результат возвращает в строку p1 strcpy(p1, p2)
    Копирует строку p2 в строку p1. Результат возвращает в строку p1 Сравнение строк
    Сравнение двух строк в стиле С осуществляется совсем не так, как сравнение строк библиотечного типа string. При сравнении библиотечных строк используются обычные операторы равенства или сравнения: string s1 = "A string example"; string s2 = "A different string"; if (s1 < s2) // ложно: s2 меньше s1
    Использование этих же операторов для подобным образом определенных строк в стиле С
    приведет к сравнению значений указателей, а не самих строк. const char ca1[] = "A string example"; const char ca2[] = "A different string"; if (ca1 < ca2) // непредсказуемо: сравниваются два адреса
    Помните, что при использовании массива в действительности используются указатели на их первый элемент (см. раздел 3.5.3). Следовательно, это условие фактически сравнивает два значения const char*. Эти указатели содержат адреса разных объектов, поэтому результат такого сравнения непредсказуем.
    Чтобы сравнить строки, а не значения указателей, можем использовать функцию strcmp().
    Она возвращает значение 0, если строки равны, положительное или отрицательное значение, в зависимости от того, больше ли первая строка второй или меньше. if (strcmp(ca1, ca2) < 0) // то же, что и сравнение строк s1 < s2 За размер строки отвечает вызывающая сторона
    Конкатенация и копирование строк в стиле С также весьма отличается от таких же операций с библиотечным типом string. Например, если необходима конкатенация строк s1 и s2,
    определенных выше, то это можно сделать так:
    // инициализировать largeStr результатом конкатенации строки s1,
    //
    Page 159/1103
    пробела и строки s2 string largeStr = s1 + " " + s2;
    Подобное с двумя массивами, ca1 и ca2, было бы ошибкой. Выражение ca1 + ca2 попытается сложить два указателя, что некорректно и бессмысленно.
    Вместо этого можно использовать функции strcat() и strcpy(). Но чтобы использовать эти функции, им необходимо передать массив для хранения результирующей строки.
    Передаваемый массив должен быть достаточно большим, чтобы содержать созданную строку, включая нулевой символ в конце. Хотя представленный здесь код следует традиционной схеме, потенциально он может стать причиной серьезной ошибки.
    // катастрофа, если размер largeStr вычислен ошибочно strcpy(largeStr, ca1); // копирует ca1 в largeStr strcat(largeStr, " "); // добавляет пробел в конец largeStr strcat(largeStr, ca2); // конкатенирует ca2 с largeStr
    Проблема в том, что можно легко ошибиться в расчете необходимого размера largeStr. Кроме того, при каждом изменении значения, которые следует сохранить в largeStr, необходимо перепроверить правильность вычисления его размера. К сожалению, код, подобный этому,
    широко распространен в программах. Такие программы подвержены ошибкам и часто приводят к серьезным проблемам защиты.
    Для большинства приложений не только безопасней, но и эффективней использовать библиотечный тип string, а не строки в стиле С. Упражнения раздела 3.5.4
    Упражнение 3.37. Что делает следующая программа? const char ca[] = {'h', 'e', 'l', 'l', 'o'}; const char *cp = ca; while (*cp) { cout << *cp << endl;
    ++cp;
    }
    Упражнение 3.38. В этом разделе упоминалось, что не только некорректно, но и бессмысленно пытаться сложить два указателя. Почему сложение двух указателей бессмысленно?
    Page 160/1103

    Упражнение 3.39. Напишите программу, сравнивающую две строки. Затем напишите программу, сравнивающую значения двух символьных строк в стиле С.
    Упражнение 3.40. Напишите программу, определяющую два символьных массива,
    инициализированных строковыми литералами. Теперь определите третий символьный массив для содержания результата конкатенации этих двух массивов. Используйте функции strcpy() и strcat() для копирования этих двух массивов в третий.
    3.5.5. Взаимодействие с устаревшим кодом
    Множество программ С++ было написано до появления стандартной библиотеки, поэтому они не используют библиотечные типы string и vector. Кроме того, многие программы С++
    взаимодействуют с программами, написанными на языке С или других языках, которые не могут использовать библиотеку С++. Следовательно, программам, написанным на современном языке С++, вероятно, придется взаимодействовать с кодом, который использует символьные строки в стиле С и/или массивы. Библиотека С++ предоставляет средства,
    облегчающие такое взаимодействие. Совместное использование библиотечных строки строк в стиле С
    В разделе 3.2.1 была продемонстрирована возможность инициализации строки класса string строковым литералом: string s("Hello World"); // s содержит Hello World
    В общем, символьный массив с нулевым символом в конце можно использовать везде, где используется строковый литерал.
    • Символьный массив с нулевым символом в конце можно использовать для инициализации строки класса string или присвоения ей.
    • Символьный массив с нулевым символом в конце можно использовать как один из операндов (но не оба) в операторе суммы класса string или как правый операнд в составном операторе присвоения (+=) класса string.
    Однако нет никакого простого способа использовать библиотечную строку там, где требуется строка в стиле С. Например, невозможно инициализировать символьный указатель объектом класса string. Тем не менее класс string обладает функцией-членом c_str(), зачастую позволяющей выполнить желаемое. char *str = s; // ошибка: нельзя инициализировать char* из string const char *str = s.c_str(); // ok
    Имя функции c_str() означает, что она возвращает символьную строку в стиле С. Таким образом, она возвращает указатель на начало символьного массива с нулевым символом в конце, содержащим те же символы, что и строка. Тип указателя const char* не позволяет изменять содержимое массива.
    Page 161/1103

    Допустимость массива, возвращенного функцией c_str(), не гарантируется. Любое последующее использование указателя s, способное изменить его значение, может сделать этот массив недопустимым.
    Если программа нуждается в продолжительном доступе к содержимому массива,
    возвращенного функцией c_str(), то следует создать его копию. Использование массива для инициализации вектора
    В разделе 3.5.1 упоминалось о том, что нельзя инициализировать встроенный массив другим массивом. Инициализировать массив из вектора также нельзя. Однако можно использовать массив для инициализации вектора. Для этого необходимо определить адрес первого подлежащего копированию элемента и элемента, следующего за последним. int int_arr[] = {0, 1, 2, 3, 4, 5};
    // вектор ivec содержит 6 элементов, каждый из которых является
    // копией соответствующего элемента массива int_arr vector<int> ivec(begin(int_arr), end(int_arr));
    Два указателя, используемые при создании вектора ivec, отмечают диапазон значений,
    используемых для инициализации его элементов. Второй указатель указывает на следующий элемент после последнего копируемого. В данном случае для передачи указателей на первый и следующий после последнего элементы массива int_arr использовались библиотечные функции begin() и end() (см. раздел 3.5.3). В результате вектор ivec содержит шесть элементов, значения которых совпадают со значениями соответствующих элементов массива int_arr.
    Определяемый диапазон может быть также подмножеством массива:
    // скопировать 3 элемента: int_arr[1], int_arr[2], int_arr[3] vector<int> subVec(int_arr + 1, int_arr + 4);
    Этот код создает вектор subVec с тремя элементами, значения которых являются копиями значений элементов от intarr[1] до intarr[3]. Совет. Используйте вместо массивов библиотечные типы
    Указатели и массивы на удивление сильно подвержены ошибкам. Частично проблема в концепции: указатели используются для низкоуровневых манипуляций, в них очень просто сделать тривиальные ошибки. Другие проблемы возникают из-за используемого синтаксиса,
    особенно синтаксиса объявлений. Упражнения раздела 3.5.5
    Упражнение 3.41. Напишите программу, инициализирующую вектор значениями из массива целых чисел.
    Упражнение 3.42. Напишите программу, копирующую вектор целых чисел в массив целых чисел.
    Page 162/1103

    3.6. Многомерные массивы
    Строго говоря, никаких многомерных массивов (multidimensioned array) в языке С++ нет. То, что обычно упоминают как многомерный массив, фактически является массивом массивов. Не забывайте об этом факте, когда будете использовать то, что называют многомерным массивом.
    При определении массива, элементы которого являются массивами, указываются две размерности: размерность самого массива и размерность его элементов. int ia[3][4]; // массив из 3 элементов; каждый из которых является
    // массивом из 4 целых чисел
    // массив из 10 элементов, каждый из которых является массивом из 20
    // элементов, каждый из которых является массивом из 30 целых чисел int arr[10][20][30] = {0}; // инициализировать все элементы значением 0
    Как уже упоминалось в разделе 3.5.1, может быть легче понять эти определения, читая их изнутри наружу. Сначала можно заметить определяемое имя, ia, далее видно, что это массив размером 3. Продолжая вправо, видим, что у элементов массива ia также есть размерность.
    Таким образом, элементы массива ia сами являются массивами размером 4. Глядя влево,
    видно, что типом этих элементов является int. Так, ia является массивом из трех элементов,
    каждый из которых является массивом из четырех целых чисел.
    Прочитаем определение массива arr таким же образом. Сначала увидим, что arr — это массив размером 10 элементов. Элементы этого массива сами являются массивами размером 20 элементов. У каждого из этих массивов по 30 элементов типа int. Нет предела количеству используемых индексирований. Поэтому вполне может быть массив, элементы которого являются массивами массив, массив, массив и т.д.
    В двумерном массиве первую размерность зачастую называют рядом (row), а вторую — столбцом (column).Инициализация элементов многомерного массива
    Подобно любым массивам, элементы многомерного массива можно инициализировать,
    предоставив в фигурных скобках список инициализаторов. Многомерные массивы могут быть инициализированы списками значений в фигурных скобках для каждого ряда. int ia[3][4] = { //
    Page 163/1103
    три элемента; каждый - массив размером 4
    {0, 1, 2, 3}, // инициализаторы ряда 0
    {4, 5, 6, 7}, // инициализаторы ряда 1
    {8, 9, 10, 11} // инициализаторы ряда 2
    };
    Вложенные фигурные скобки необязательны. Следующая инициализация эквивалентна, хотя и значительно менее очевидна:
    // эквивалентная инициализация без необязательных вложенных фигурных
    // скобок для каждого ряда int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
    Как и в случае одномерных массивов, элементы списка инициализации могут быть пропущены. Следующим образом можно инициализировать только первый элемент каждого ряда:
    // явная инициализация только нулевого элемента в каждом ряду int ia[3][4] = {{ 0 }, { 4 }, { 8 } };
    Остальные элементы инициализируются значением по умолчанию, как и обычные одномерные массивы (см. раздел 3.5.1). Но если опустить вложенные фигурные скобки, то результаты были бы совсем иными:
    // явная инициализация нулевого ряда;
    // остальные элементы инициализируются
    // по умолчанию int ix[3][4] = {0, 3, 6, 9};
    Этот код инициализирует элементы первого ряда. Остальные элементы инициализируются значением 0. Индексация многомерных массивов
    Page 164/1103

    Подобно любому другому массиву, для доступа к элементам многомерного массива можно использовать индексирование. При этом для каждой размерности используется отдельный индекс.
    Если выражение предоставляет столько же индексов, сколько у массива размерностей,
    получается элемент с определенным типом. Если предоставить меньше индексов, чем есть размерностей, то результатом будет элемент внутреннего массива по указанному индексу:
    // присваивает первый элемент массива arr последнему элементу
    // в последнем ряду массива ia ia[2][3] = arr[0][0][0]; int (&row)[4] = ia[1]; // связывает ряд второго массива с четырьмя
    // элементами массива ia
    В первом примере предоставляются индексы для всех размерностей обоих массивов. Левая часть, ia[2], возвращает последний ряд массива ia. Она возвращает не отдельный элемент массива, а сам массив. Индексируем массив, выбирая элемент [3], являющийся последним элементом данного массива.
    Точно так же, правый операнд имеет три размерности. Сначала выбирается массив по индексу 0 из наиболее удаленного массива. Результат этой операции — массив
    (многомерный) размером 20. Используя массив размером 30, извлекаем из этого массива с
    20 элементами первый элемент. Затем выбирается первый элемент из полученного массива.
    Во втором примере row определяется как ссылка на массив из четырех целых чисел. Эта ссылка связывается со вторым рядом массива ia. constexpr size_t rowCnt = 3, colCnt = 4; int ia[rowCnt][colCnt]; //
    12 неинициализированных элементов
    // для каждого ряда for (size_t i = 0; i != rowCnt; ++i) {
    // для каждого столбца в ряду for (size_t j = 0; j != colCnt; ++j) {
    //
    Page 165/1103
    присвоить элементу его индекс как значение ia[i][j] = i * colCnt + j;
    }
    }
    Внешний цикл for перебирает каждый элемент массива ia. Внутренний цикл for перебирает элементы внутренних массивов. В данном случае каждому элементу присваивается значение его индекса в общем массиве. Использование серийного оператора for с многомерными массивами
    По новому стандарту предыдущий цикл можно упростить с помощью серийного оператора for:
    size_t cnt = 0; for (auto &row : ia) // для каждого элемента во внешнем массиве for (auto &col : row) { // для каждого элемента во внутреннем массиве col = cnt; // присвоить значение текущему элементу
    ++cnt; // инкремент cnt
    }
    Этот цикл присваивает элементам массива ia те же значения, что и предыдущий цикл, но на сей раз управление индексами берет на себя система. Значения элементов необходимо изменить, поэтому объявляем управляющие переменные row и col как ссылки (см. раздел
    3.2.3). Первый оператор for перебирает элементы массива ia, являющиеся массивами из 4
    элементов. Таким образом, типом row будет ссылка на массив из четырех целых чисел.
    Второй цикл for перебирает каждый из этих массивов по 4 элемента. Следовательно, col имеет тип int&. На каждой итерации значение cnt присваивается следующему элементу массива ia, а затем осуществляется инкремент переменной cnt.
    В предыдущем примере как управляющие переменные цикла использовались ссылки,
    поскольку элементы массива необходимо было изменять. Однако есть и более серьезная причина для использования ссылок. Рассмотрим в качестве примера следующий цикл: for (const auto &row : ia) // для каждого элемента во внешнем массиве for (auto col : row) // для каждого элемента во внутреннем массиве cout << col << endl;
    Page 166/1103

    Этому циклу запись в элементы не нужна, но все же управляющая переменная внешнего цикла определена как ссылка. Это сделано для того, чтобы избежать преобразования обычного массива в указатель (см. раздел 3.5.3). Если пренебречь ссылкой и написать эти циклы так, то компиляция потерпит неудачу: for (auto row : ia) for (auto col : row)
    Как и прежде, первый цикл for перебирает элементы массива ia, являющиеся массивами по 4
    элемента. Поскольку row не ссылка, при его инициализации компилятор преобразует каждый элемент массива (как и любой другой объект типа массива) в указатель на первый элемент этого массива. В результате типом row в этом цикле будет int*. Внутренний цикл for некорректен. Несмотря на намерения разработчика, этот цикл пытается перебрать указатель типа int*.
    Чтобы использовать многомерный массив в серийном операторе for, управляющие переменные всех циклов, кроме самого внутреннего, должны быть ссылками. Указатели и многомерные массивы
    Подобно любым другим массивам, имя многомерного массива автоматически преобразуется в указатель на первый его элемент.
    Определяя указатель на многомерный массив, помните, что на самом деле он является массивом массивов.
    Поскольку многомерный массив в действительности является массивом массивов, тип указателя, в который преобразуется массив, является типом первого внутреннего массива. int ia[3][4]; // массив размером 3 элемента; каждый элемент - массив
    // из 4 целых чисел int (*p)[4] = ia; // p указывает на массив из четырех целых чисел p = &ia[2]; // теперь p указывает на последний элемент ia
    Применяя стратегию из раздела 3.5.1, начнем рассмотрение с части (*p), гласящей, что p —
    указатель. Глядя вправо, замечаем, что объект, на который указывает указатель p, имеет размер 4 элемента, а глядя влево, видим, что типом элемента является int. Следовательно, p
    — это указатель на массив из четырех целых чисел.
    Круглые скобки в этом объявлении необходимы. int *ip[4]; // массив указателей на int int (*ip)[4]; //
    Page 167/1103
    указатель на массив из четырех целых чисел
    Новый стандарт зачастую позволяет избежать необходимости указывать тип указателя на массив за счет использования спецификаторов auto и decltype (см. раздел 2.5.2).
    // вывести значение каждого элемента ia; каждый внутренний массив
    // отображается в отдельной строке
    // p указывает на массив из четырех целых чисел for (auto p = ia; p != ia + 3; ++p) {
    // q указывает на первый элемент массива из четырех целых чисел;
    // т.е. q указывает на int for (auto q = *p; q != *p + 4; ++q) cout << *q << ' '; cout << endl;
    }
    Внешний цикл for начинается с инициализации указателя p адресом первого массива в массиве ia. Этот цикл продолжается, пока не будут обработаны все три ряда массива ia.
    Инкремент ++p перемещает указатель p на следующий ряд (т.е. следующий элемент)
    массива ia.
    Внутренний цикл for выводит значения внутренних массивов. Он начинается с создания указателя q на первый элемент в массиве, на который указывает указатель p. Результатом *p будет массив из четырех целых чисел. Как обычно, при использовании имени массива оно автоматически преобразуется в указатель на его первый элемент. Внутренний цикл for выполняется до тех пор, пока не будет обработан каждый элемент во внутреннем массиве.
    Чтобы получить указатель на элемент сразу за концом внутреннего массива, мы снова обращаемся к значению указателя p, чтобы получить указатель на первый элемент в этом массиве. Затем добавляем к нему 4, чтобы обработать четыре элемента в каждом внутреннем массиве.
    Конечно, используя библиотечные функции begin() и end() (см. раздел 3.5.3), этот цикл можно существенно упростить:
    // p указывает на первый массив в ia for (auto p = begin(ia); p != end(ia); ++p) {
    //
    Page 168/1103
    q указывает на первый элемент во внутреннем массиве for (auto q = begin(*p); q != end(*p); ++q) cout << *q << ' '; // выводит значение, указываемое q cout << endl;
    }
    Спецификатор auto позволяет библиотеке самостоятельно определить конечный указатель и избавить от необходимости писать тип, значение которого возвращает функция begin(). Во внешнем цикле этот тип — указатель на массив из четырех целых чисел. Во внутреннем цикле этот тип — указатель на тип int. Псевдонимы типов упрощают указатели на многомерные массивы
    Псевдоним типа (см. раздел 2.5.1) может еще больше облегчить чтение, написание и понимание указателей на многомерные массивы. Рассмотрим пример. using int_array = int[4]; // объявление псевдонима типа нового стиля;
    // см. раздел 2.5.1 typedef int int_array[4]; // эквивалентное объявление typedef;
    // см. раздел 2.5.1
    // вывести значение каждого элемента ia; каждый внутренний массив
    // отображается в отдельной строке for (int_array *p = ia; p != ia + 3; ++p) { for (int *q = *p; q != *p + 4; ++q) cout << *q << ' '; cout << endl;
    }
    Код начинается с определения int_array как имени для типа "массив из четырех целых чисел".
    Это имя типа используется для определения управляющей переменной внешнего цикла for.
    Упражнения раздела 3.6
    Page 169/1103

    Упражнение 3.43. Напишите три разных версии программы для вывода элементов массива ia.
    Одна версия должна использовать для управления перебором серийный оператор for, а другие две — обычный цикл for, но в одном случае использовать индексирование, а в другом
    — указатели. Во всех трех программах пишите все типы явно, т.е. не используйте псевдонимы типов и спецификаторы auto или decltype для упрощения кода.
    Упражнение 3.44. Перепишите программы из предыдущего упражнения, используя псевдоним для типа управляющих переменных цикла.
    Упражнение 3.45. Перепишите программы снова, на сей раз используя спецификатор auto.
    Резюме
    Одними из важнейших библиотечных типов являются vector и string. Строка — это последовательность символов переменной длины, а вектор — контейнер объектов единого типа.
    Итераторы обеспечивают косвенный доступ к хранящимся в контейнере объектам. Итераторы используются для доступа и перемещения между элементами в строках и векторах.
    Массивы и указатели на элементы массива обеспечивают низкоуровневые аналоги библиотечных типов vector и string. Как правило, предпочтительней использовать библиотечные классы, а не их низкоуровневые альтернативы, массивы и указатели,
    встроенные в язык.
    Термины
    Арифметические действия с итераторами (iterator arithmetic). Операции с итераторами векторов и строк. Добавление и вычитание целого числа из итератора приводит к изменению позиции итератора на соответствующее количество элементов вперед или назад от исходного. Вычитание двух итераторов позволяет вычислить дистанцию между ними.
    Арифметические действия допустимы лишь для итераторов, относящихся к элементам того же контейнера.
    Арифметические действия с указателями (pointer arithmetic). Арифметические операции,
    допустимые для указателей. Указатели на массивы поддерживают те же операции, что и арифметические действия с итераторами.
    Индекс (index). Значение, используемое в операторе индексирования для указания элемента,
    возвращаемого из строки, вектора или массива.
    Инициализация значения (value initialization). Инициализация, в ходе которой объекты встроенного типа инициализируются нулем, а объекты класса — при помощи стандартного конструктора класса. Объекты типа класса могут быть инициализированы значением, только если у класса есть стандартный конструктор. Используется при инициализации элементов контейнера, когда указан его размер, но не указан инициализирующий элемент. Элементы инициализируются копией значения, созданного компилятором.
    Инициализация копией (copy initialization). Форма инициализации, использующая знак =.
    Page 170/1103

    Вновь созданный объект является копией предоставленного инициализатора.
    Итератор после конца (off-the-end iterator). Итератор, возвращаемый функцией end(). Он указывает не на последний существующий элемент контейнера, а на позицию за его концом,
    т.е. на несуществующий элемент.
    Контейнер (container). Тип, объекты которого способны содержать коллекцию объектов определенного типа. К контейнерным относится тип vector.
    Объявление using. Позволяет сделать имя, определенное в пространстве имен, доступным непосредственно в коде. using пространствоимен :: имя ;. Теперь имя можно использовать без префикса пространствоимен ::.
    Оператор !. Оператор логического NOT. Возвращает инверсное значение своего операнда типа bool. Результат true, если операнд false, и наоборот.
    Оператор &&. Оператор логического AND. Результат true, если оба операнда true.
    Правый операнд обрабатывается, только если левый операнд true.
    Оператор []. Оператор индексирования. Оператор obj[i] возвращает элемент в позиции i объекта контейнера obj. Счет индексов начинается с нуля: первый элемент имеет индекс 0, а последний — obj.size() - 1. Индексирование возвращает объект. Если p — указатель, a n —
    целое число, то p[n] является синонимом для *(p+n).
    Оператор ||. Оператор логического OR. Результат true, если любой операнд true. Правый операнд обрабатывается, только если левый операнд false.
    Оператор ++. Для типов итераторов и указателей определен оператор инкремента, который "добавляет единицу", перемещая итератор или указатель на следующий элемент.
    Оператор <<. Библиотечный тип string определяет оператор вывода, читающий символы в строку.
    Оператор ->. Оператор стрелка. Объединяет оператор обращения к значению и точечный оператор: a->b — синоним для (*a).b.
    Оператор >>. Библиотечный тип string определяет оператор ввода, читающий разграниченные пробелами последовательности символов и сохраняющий их в строковой переменной, указанной правым операндом.
    Серийный оператор for (range for). Управляющий оператор, перебирающий значения указанной коллекции и выполняющий некую операцию с каждым из них.
    Переполнение буфера (buffer overflow). Грубая ошибка программирования, результат использования индекса, выходящего из диапазона элементов контейнера, такого как string,
    vector или массив.
    Page 171/1103

    Прямая инициализация (direct initialization). Форма инициализации, не использующая знак =.
    Расширение компилятора (compiler extension). Дополнительный компонент языка,
    предлагаемый некоторыми компиляторами. Код, применяющий расширение компилятора,
    может не подлежать переносу на другие компиляторы.
    Создание экземпляра (instantiation). Процесс, в ходе которого компилятор создает специфический экземпляр шаблона класса или функции.
    Строка в стиле С (C-style string). Символьный массив с нулевым символом в конце.
    Строковые литералы являются строками в стиле С. Строки в стиле С могут стать причиной ошибок.
    Строка с завершающим нулевым символом (null-terminated string). Строка, последний символ которой сопровождается нулевым символом ('\0').
    Тип difference_type. Целочисленный знаковый тип, определенный в классах vector и string,
    способный содержать дистанцию между любыми двумя итераторами.
    Тип iterator (итератор). Тип, используемый при переборе элементов контейнера и обращении к ним.
    Тип ptrdiff_t. Машинозависимый знаковый целочисленный тип, определенный в заголовке cstddef. Является достаточно большим, чтобы содержать разницу между двумя указателями в самом большом массиве.
    Тип size_t. Машинозависимый беззнаковый целочисленный тип, определенный в заголовке cstddef. Является достаточно большим, чтобы содержать размер самого большого возможного массива.
    Тип size_type. Имя типа, определенного для классов vector и string, способного содержать размер любой строки или вектора соответственно. Библиотечные классы, определяющие тип size_type, относят его к типу unsigned.
    Тип string. Библиотечный тип, представлявший последовательность символов.
    Тип vector. Библиотечный тип, содержащий коллекцию элементов определенного типа.
    Функция begin(). Функция-член классов vector и string, возвращающая итератор на первый элемент. Кроме того, автономная библиотечная функция, получающая массив и возвращающая указатель на первый элемент в массиве.
    Функция empty(). Функция-член классов vector и string. Возвращает логическое значение (типа bool) true, если размер нулевой, или значение false в противном случае.
    Функция end(). Функция-член классов vector и string, возвращающая итератор на элемент после последнего элемента контейнера. Кроме того, автономная библиотечная функция,
    получающая массив и возвращающая указатель на элемент после последнего в массиве.
    Функция getline(). Определенная в заголовке string функция, которой передают поток istream и строковую переменную. Функция читает данные из потока до тех пор, пока не встретится символ новой строки, а прочитанное сохраняет в строковой переменной. Функция возвращает поток istream. Символ новой строки в прочитанных данных отбрасывается.
    Функция push_back(). Функция-член класса vector, добавляющая элементы в его конец.
    Функция size(). Функция-член классов vector и string возвращает количество символов или
    Page 172/1103
    элементов соответственно. Возвращаемое значение имеет тип size_type для данного типа.
    Шаблон класса (class template). Проект, согласно которому может быть создано множество специализированных классов. Чтобы применить шаблон класса, необходимо указать дополнительную информацию. Например, чтобы определить вектор, указывают тип его элемента: vector<int> содержит целые числа.
    Глава 4
    Выражения
    Язык С++ предоставляет богатый набор операторов, а также определяет их назначение и применение к операндам встроенного типа. Он позволяет также определять назначение большинства операторов, операндами которых являются объекты классов. Эта глава посвящена операторам, определенным в самом языке и применяемым к операндам встроенных типов. Будут описаны также некоторые из операторов, определенных библиотекой. Определение операторов для собственных типов рассматривается в главе 14.
    Выражение (expression) состоит из одного или нескольких операторов (operator) и возвращает результат (result) вычисления. Самая простая форма выражения — это одиночной литерал или переменная. Более сложные выражения формируются из оператора и одного или нескольких операндов (operand).
    4.1. Основы
    Существует несколько фундаментальных концепций, определяющих то, как обрабатываются выражения. Начнем с краткого обсуждении концепций, относящихся к большинству (если не ко всем) выражений. В последующих разделах эти темы рассматриваются подробней.
    4.1.1. Фундаментальные концепции
    Существуют унарные операторы (unary operator) и парные операторы (binary operator). Унарные операторы, такие как обращение к адресу
    (&) и обращение к значению (*), воздействуют на один операнд. Парные операторы,
    такие как равенство (==) и умножение (*), воздействуют на два операнда. Существует также
    (всего один) тройственный оператор (ternary operator), который использует три операнда, а также
    Page 173/1103
    оператор вызова функции (function call), который получает неограниченное количество операндов.
    Некоторые символы (symbol), например *, используются для обозначения как унарных (обращение к значению), так и парных (умножение) операторов. Представляет ли символ унарный оператор или парный, определяет контекст, в котором он используется. В использовании таких символов нет никакой взаимосвязи, поэтому их можно считать двумя разными символами.Группировка операторов и операндов
    Чтобы лучше понять порядок выполнения выражений с несколькими операторами, следует рассмотреть концепцию приоритета (precedence), порядка (associativity) и порядка вычисления (order of evaluation) операторов. Например, в следующем выражении используются сложение, умножение и деление:
    5 + 10 * 20/2;
    Операндами оператора * могли бы быть числа 10 и 20, либо 10 и 20/2, либо 15 и 20, либо 15 и
    20/2. Понимание таких выражений и является темой следующего раздела. Преобразование операндов
    В ходе вычисления выражения операнды нередко преобразуются из одного типа в другой.
    Например, парные операторы обычно ожидают операндов одинакового типа. Но операторы применимы и к операндам с разными типами, если они допускают преобразование (см.
    раздел 2.1.2) в общий тип.
    Хотя правила преобразования довольно сложны, по большей части они очевидны. Например,
    целое число можно преобразовать в число с плавающей запятой, и наоборот, но преобразовать тип указателя в число с плавающей точкой нельзя. Немного неочевидным может быть то, что операнды меньших целочисленных типов (например, bool, char, short и т.д.) обычно преобразуются (promotion) в больший целочисленный тип, как правило int. Более подробная информация о преобразованиях приведена в разделе 4.11.Перегруженные операторы
    Значение операторов для встроенных и составных типов определяет сам язык. Значение большинства операторов типов классов мы можем определить самостоятельно. Поскольку такие определения придают альтернативное значение существующему символу оператора,
    они называются перегруженными операторами (overloaded operator). Операторы >> и << библиотеки ввода и вывода, а также операторы, использовавшиеся с объектами строк, векторов и итераторов, являются перегруженными операторами.
    При использовании перегруженного оператора его смысл, а также тип операндов и результата зависят от того, как определен оператор. Однако количество операндов, их приоритет и порядок не могут быть изменены. L- и r-значения
    Каждое выражение в языке С++ является либо r-значением (r-value), либо
    Page 174/1103
    l-значением (l-value). Эти названия унаследованы от языка С и первоначально имели простую мнемоническую цель: l-значения могли стоять слева от оператора присвоения, а r-значения не могли.
    В языке С++ различие не так просто. В языке С++ выражение l-значения возвращает объект или функцию. Однако некоторые l-значения, такие как константные объекты, не могут быть левым операндом присвоения. Кроме того, некоторые выражения возвращают объекты, но возвращают их как r-, а не l-значения. Короче говоря, при применении объекта в качестве r-значения используется его значение (т.е. его содержимое). При применении объекта в качестве l-значения используется его идентификатор (т.е. его область в памяти).
    Операторы различаются по тому, требуют ли они операндов l- или r-значения, а также по тому, возвращают ли они l- или r-значения. Важный момент здесь в том, что (за одним исключением, рассматриваемым в разделе 13.6) l-значение можно использовать там, где требуется r-значение, однако нельзя использовать r-значение там, где требуется l-значение
    (т.е. область). Когда l-значение применяется вместо r-значения, используется содержимое объекта (его значение). Мы уже использовали несколько операторов, которые задействовали l-значения.
    • В качестве своего левого операнда оператор присвоения требует (неконстантного)
    l-значения и возвращает свой левый операнд как l-значение.
    • Оператор обращения к адресу (см. раздел 2.3.2) требует в качестве операнда l-значение и возвращает указатель на свой операнд как r-значение.
    • Встроенные операторы обращения к значению и индексирования (см. раздел 2.3.2 и раздел
    3.5.2), а также обращение к значению итератора и операторы индексирования строк и векторов (см. раздел 3.4.1, раздел 3.2.3 и раздел 3.3.3) возвращают l-значения.
    • Операторы инкремента и декремента, как встроенные, так и итератора (см. раздел 1.4.1 и раздел 3.4.1), требуют l-значения в качестве операндов. Их префиксные версии (которые использовались до сих пор) также возвращают l-значения.
    Рассматривая операторы, следует обратить внимание на то, должен ли операнд быть l-значением и возвращает ли он l-значение.
    L- и r-значения также различаются при использовании спецификатора decltype (см. раздел
    2.5.3). При применении спецификатора decltype к выражению (отличному от переменной)
    результатом будет ссылочный тип, если выражение возвращает l-значение. Предположим,
    например, что указатель p имеет тип int*. Поскольку обращение к значению возвращает l-значение, выражение decltype(*p) имеет тип int&. С другой стороны, поскольку оператор обращения к адресу возвращает r-значение, выражение decltype(&p) имеет тип int**, т.е.
    указатель на указатель на тип int.
    4.1.2. Приоритет и порядок
    Выражения с двумя или несколькими операторами называются составными (compound expression). Результат составного выражения определяет способ группировки операндов в отдельных операторах. Группировку операндов определяют приоритет и порядок. Таким образом, они определяют, какие части выражения будут операндами для каждого из операторов в выражении. При помощи скобок программисты могут изменять эти правила,
    обеспечивая необходимую группировку.
    Page 175/1103

    Обычно значение выражения зависит от того, как группируются его части. Операнды операторов с более высоким приоритетом группируются прежде операндов операторов с более низким приоритетом. Порядок определяет то, как группируются операнды с тем же приоритетом. Например, операторы умножения и деления имеют одинаковый приоритет относительно друг друга, но их приоритет выше, чем у операторов сложения и вычитания.
    Поэтому операнды операторов умножения и деления группируются прежде операндов операторов сложения и вычитания. Арифметические операторы имеют левосторонний порядок, т.е. они группируются слева направо.
    • Благодаря приоритету результатом выражения 3 + 4*5 будет 23, а не 35.
    • Благодаря порядку результатом выражения 20-15-3 будет 2, а не 8.
    В более сложном примере при вычислении слева направо следующего выражения получается 20:
    6 + 3 * 4 / 2 + 2
    Вполне возможны и другие результаты: 9, 14 и 36. В языке С++ результат составит 14,
    поскольку это выражение эквивалентно следующему:
    // здесь круглые скобки соответствуют стандартному приоритету и порядку
    ((6 + ((3 * 4) / 2)) + 2) Круглые скобки переопределяют приоритет и порядок
    Круглые скобки позволяют переопределить обычную группировку. Выражения в круглых скобках обрабатываются как отдельные модули, а во всех остальных случаях применяются обычные правила приоритета. Например, используя круглые скобки в предыдущем выражении, можно принудительно получить любой из четырех возможных вариантов:
    // круглые скобки обеспечивают альтернативные группировки cout << (6 + 3) * (4 / 2 + 2) << endl; // выводит 36 cout << ((6 + 3) * 4) / 2 + 2 << endl; // выводит 20 cout << 6 + 3 * 4 / (2 + 2) << endl; // выводит 9 Когда важны приоритет и порядок
    Мы уже видели примеры, где приоритет влияет на правильность наших программ.
    Рассмотрим обсуждавшийся в разделе 3.5.3 пример обращения к значению и арифметических действий с указателями. int ia[] = {0,2,4,6,8}; // массив из 5 элементов типа int int last = *(ia + 4); //
    Page 176/1103
    ok: инициализирует last значением
    // ia[4], т.е. 8 last = *ia + 4; // last = 4, эквивалент ia[0] + 4
    Если необходим доступ к элементу в области ia+4, то круглые скобки вокруг сложения необходимы. Без круглых скобок сначала группируется часть *ia, а к полученному значению добавляется 4.
    Наиболее популярный случай, когда порядок имеет значение, — это выражения ввода и вывода. Как будет продемонстрировано в разделе 4.8, операторы ввода и вывода имеют левосторонний порядок. Этот порядок означает, что можно объединить несколько операций ввода и вывода в одном выражении. cin >> v1 >> v2; // читать в v1, а затем в v2
    В таблице раздела 4.12 перечислены все операторы, организованные по сегментам. У
    операторов в каждом сегменте одинаковый приоритет, причем сегменты с более высоким приоритетом расположены выше. Например, префиксный оператор инкремента и оператор обращения к значению имеют одинаковый приоритет, который выше, чем таковой у арифметических операторов. Таблица содержит ссылки на разделы, где описан каждый из операторов. Многие из этих операторов уже применялось, а большинство из остальных рассматривается в данной главе. Подробней некоторые из операторов рассматриваются позже. Упражнения раздела 4.1.2

    1   ...   5   6   7   8   9   10   11   12   ...   54


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