Структура программы. Скалярные типы данных. Выражения и присваивания цель: Изучить категории типов данных, виды выражений и операций и работу с ними на языке Си. Общие сведения
Вид материала | Лабораторная работа |
СодержаниеЧасть 3 операторы языка с++: операторы передачи управления Указатели и адреса объектов Массивы и указатели Общие сведения |
- Алгоритм и программа поиска минимального и максимального элементов в массиве. Задача, 17.73kb.
- Понятие типа данных. Переменные и константы. Основные типы данных в языке Си: общая, 143.1kb.
- Структура программы. Часть Структуры данных. 24. Классификация структур данных. Операции, 41.26kb.
- Курс за второй семестр. Абстрактные типы данных, 687.76kb.
- Программа дисциплины программирование на языке С++ для направления 080700. 62 «Бизнес-информатика», 131.2kb.
- Основы Pascal. Типы данных. Структура программы на языке Pascal, 3515.8kb.
- Лабораторная работа №4 Тема : Структурный тип данных в языке С++, 112.14kb.
- Практическая работа № «Создание базы данных», 21.96kb.
- Структура программы. Описание типов данных. Любая программа,все равно на каком языке, 611.52kb.
- Лекция Выражения. Операции в выражениях Построение выражений. Операции и их приоритеты., 271.62kb.
ЧАСТЬ 3
ОПЕРАТОРЫ ЯЗЫКА С++: ОПЕРАТОРЫ ПЕРЕДАЧИ
УПРАВЛЕНИЯ
Цель: Ознакомиться с операторами передачи управления и их возможностями.
Общие сведения
К операторам передачи управления относят оператор безусловного перехода (иначе – оператор безусловной передачи управления (goto)), оператор возврата из функции (return), оператор выхода из цикла или переключателя (break) и оператор перехода к следующей итерации цикла (continue).
Оператор безусловного перехода имеет вид
goto идентификатор;
где идентификатор – имя метки оператора, расположенного в той же функции, где используется оператор безусловного перехода.
Передача управления разрешена на любой помеченный оператор в теле функции. Однако существует одно важное ограничение: запрещено "перескакивать" через описания, содержащие инициализацию объектов. Это ограничение не распространяется на вложенные блоки, которые можно обходить целиком. Следующий фрагмент иллюстрирует сказанное:
33
…
goto b; // Ошибочный переход, минуя описание
float x = 0.0; // Инициализация не будет выполнена
goto В; // Допустимый переход, минуя блок
{ int n = 10; // Внутри блока определена переменная
х = n * х + х;
}
B: cout « "\tx = ” « x;
…
Все операторы блока достижимы для перехода к ним из внешних блоков. Однако при таких переходах необходимо соблюдать то же самое правило: нельзя передавать управление в блок, обходя инициализацию. Следовательно, будет ошибочным переход к операторам блока, перед которыми помещены описания с явной* или неявной инициализацией. Это же требование обязательного выполнения инициализации справедливо и при внутренних переходах в блоке. Следующий фрагмент содержит обе указанные ошибки:
{ ... // Внешний блок
goto АВС; // Во внутренний блок, минуя описание ii
…
{ int ii = 15; // Внутренний блок
…
ABC:
…
goto XYZ; // Обход описания CC
char CC = ‘’;
…
XYZ:
…
}
…
}
Принятая в настоящее время дисциплина программирования рекомендует либо вовсе отказаться от оператора goto, либо свести его применение к минимуму и строго придерживаться следующих рекомендаций:
- не входить внутрь блока извне;
-
59
не входить внутрь условного оператора, т. е. не передавать управление операторам, размещенным после служебных слов if или else;
- не входить извне внутрь переключателя (switch);
- не передавать управление внутрь цикла.
Следование перечисленным рекомендациям позволяет исключить возможные нежелательные последствия бессистемного использования оператора безусловного перехода. Полностью отказываться от оператора goto вряд ли стоит. Есть случаи, когда этот оператор обеспечивает наиболее простые и понятные решения. Один из них – это ситуация, когда в рамках текста одной функции необходимо из разных мест переходить к одному участку программы. Если по каким-либо причинам эту часть программы нельзя оформить в виде функции (например, это может быть текст на ассемблере), то наиболее простое решение – применение безусловного перехода с помощью оператора goto. Такое положение возникает, например, при необходимости обрабатывать ошибки, выявляемые в процессе выполнения программы.
Второй случай возникает, когда нужно выйти из нескольких вложенных друг в друга циклов или переключателей. Оператор break прерывания цикла и выхода из переключателя здесь не поможет, так как он обеспечивает выход только из самого внутреннего вложенного цикла или переключателя. Например, в задаче поиска в матрице хотя бы одного элемента с заданным значением для перебора элементов матрицы обычно используют два вложенных цикла. Как только элемент с заданным значением будет найден, нужно выйти сразу из двух циклов, что удобно сделать с помощью goto.
Оператор возврата из функции имеет вид:
return выражение;
или просто return;
Выражение (если оно присутствует) может быть только скалярным. Например, следующая функция вычисляет и возвращает куб значения своего аргумента:
float cube(float z) { return z * z * z; }
Выражение в операторе return не может присутствовать в том случае, если возвращаемое функцией значение имеет тип void. Например, следующая функция выводит на экран дисплея, связанный с потоком cout, значение третьей степени своего аргумента и не возвращает в точку вызова никакого значения:
void cube_print(float z)
{ cout « "\t cube = " « z * z * z;
return;
}
В данном примере оператор возврата из функции не содержит выражения.
Оператор break служит для принудительного выхода из цикла или переключателя. Определение "принудительный" подчеркивает безусловность перехода. Например, в случае цикла не проверяются и не учитываются условия дальнейшего продолжения итераций. Оператор break прекращает выполнение оператора цикла или переключателя и осуществляет передачу управления (переход) к следующему за циклом или переключателем оператору.
При этом, в отличие от перехода с помощью goto, оператор, к которому выполняется передача управления, не должен быть помечен. Оператор break нельзя использовать нигде, кроме циклов и переключателей.
Необходимость в использовании оператора break в теле цикла возникает тогда, когда условия продолжения итераций нужно проверять не в начале итерации (циклы for, while), не в конце итерации (цикл do), а в середине тела цикла. В этом случае тело цикла может иметь такую структуру:
{ операторы
if (условие) break;
операторы}
Например, если начальные значения целых переменных i, j таковы, что i < j, то следующий цикл определяет наименьшее целое, не меньшее их среднего арифметического:
while (i < j)
{i++;
if (i == j) break;
j--;
}
Для i == 0, j == 3 результат i == j === 2 достигается при выходе из цикла с помощью оператора break. Запись i == j == 2 не в тексте программы означает равенство значений переменных i, j и константы 2. Для i == 0, j == 2 результат i == j == 1 будет получен при естественном завершении цикла.
Оператор break практически незаменим в переключателях, когда с их помощью надо организовать разветвление. Например, следующая программа печатает название любой, но только одной, восьмеричной цифры:
//Оператор break в переключателе
#include
void.main()
{ int ic;
cout « "\n Введите восьмеричную цифру: ";
cin » ic;
cout « "\n" « ic;
switch (ic)
{ case 0 Cout « " - нуль"; break ;
case 1 Cout « " - один"; break;
case 2 Cout « " - два"; break;
case 3 Cout « " - три"; break;
case 4 Cout « " - четыре"; break;
case 5 Cout « " - пять"; break;
case 6 Cout « " - шесть"; break;
case 7 Cout « " - семь"; break;
default Cout « " - это не восьмеричная цифра!'
}
cout « "\nКонец выполнения программы.
}
Программа напечатает название только одной введенной цифры и прекратит работу. Если в ней удалить операторы break, то в переключателе будут последовательно выполнены все операторы, начиная с помеченного нужным (введенным) значением.
Циклы и переключатели могут быть многократно вложенными. Однако следует помнить, что оператор break позволяет выйти только из самого внутреннего цикла или переключателя. Например, в следующей программе, которая в символьном массиве подсчитывает количество нулей (k0) и единиц (kl), в цикл вложен переключатель:
//break при вложении переключателя в цикл
#include
void main(void)
37
{ char c[l = "АВС100111";
int k0 = 0, kl = 0;
for (int i = 0; c[i] = ‘\0’; i++)
switch (c[i])
{ case ‘0’: k0++ ; break ;
case ‘1’: kl++; break;
default: break;
}
cout « "\nB строке " « kO « " нуля, " «
k1 « " единицы";
}
Результат выполнения программы:
В строке 2 нуля, 4 единицы.
Оператор break в данном примере передает управление из переключателя, но не за пределы цикла. Цикл продолжается до естественного завершения. При многократном вложении циклов и переключателей оператор break не может вызвать передачу управления из самого внутреннего уровня непосредственно на самый внешний. Например, при решении задачи поиска в матрице хотя бы одного элемента с заданным значением удобнее всего пользоваться не оператором break, а оператором безусловной передачи управления (goto):
for (int i = 0; i < n; i++)
for (int j = 0; j < и; j++)
{ if (A[i][j] ==» x) goto success;
// Действия при отсутствии элемента в матрице
…
} // Конец цикла success:
cout « "\nЭлемент х найден. Строка i = " « i;
cout « ", столбец j = " « j;
В качестве примера, когда при вложении циклов целесообразно применение оператора break, рассмотрим задачу вычисления произведения элементов строки матрицы. В данном случае вычисление произведения элементов можно прервать, если один из сомножителей окажется равным 0. Возможный вариант реализации может быть таким:
…
for (i = 0; i < n; i++) // Перебор строк матрицы
// Перебор элементов строки:
for (j = 0, p[i] =1; j < a; j++)
if (A[i][jl == 0.0) // Обнаружен нулевой элемент
{ p[i] = 0.0; break; }
else p[i] *= A[i][j];
…
При появлении в строке нулевого элемента оператор break прерывает выполнение только внутреннего цикла, однако внешний цикл перебора строк всегда выполняется для всех значений i от 0 до n-1.
Оператор continue употребляется только в операторах цикла. С его помощью завершается текущая итерация и начинается проверка условия дальнейшего продолжения цикла, т. е. условий начала следующей итерации. Для объяснений действия оператора continue рекомендуется рассматривать следующие три формы основных операторов цикла:
while (foo) do for (;foo;)
{… {… {…
contin: contin: contin:
} } while (foo); }
В каждой из форм многоточием обозначены операторы тела цикла. Вслед за ними размещен пустой оператор с меткой contin. Если среди операторов тела цикла есть оператор continue и он выполняется, то его действие эквивалентно оператору безусловного перехода на метку contin.
Типичный пример использования оператора continue: подсчитать среднее значение только положительных элементов одномерного массива:
for (s = 0.0, k = 0, i - 0; i < n; i++)
{ if (x[i] <= 0.0) continue;
k++; // Количество положительных
s += x[i]; // Сумма положительных
}
39
if (k > О) э = s/k; // Среднее значение
Практические задания
1. Задать матрицу, найти необходимое число.
2. Задать строку из 0 и 1 и подсчитать количество 0 и 1.
3. Написать программу для выполнения следующих действий. При вводе цифры от 0 до 9 будет выводиться ее название. Использовать оператор break.
4. Задать одномерный массив и посчитать среднее значение положительных элементов.
5. Вычислить произведение элементов строки матрицы. При обнаружении 0 должно выдаваться сообщение «Обнаружен 0».
6. Вывести четные числа от 0 до 100 используя оператор continue.
7. Задать строку из произвольного количества цифр и посчитать количество одинаковых введенных цифр.
8. Вывести числа, делящиеся на 3 в интервале от 0 до 100, используя оператор continue.
9. Вывести числа от 0 до 100, удовлетворяющие условию х1=х2, x2=x12 и т. д. используя оператор continue.
10. Вычислить произведение положительных элементов строки матрицы. При обнаружении 0 должно выдаваться сообщение «Обнаружен 0».
11. Вычислить произведение отрицательных элементов строки матрицы. При обнаружении 0 должно выдаваться сообщение «Обнаружен 0».
12. Написать программу для выполнения следующих действий. При вводе в строку цифр буквы выдается сообщение «Это не цифра».
13. Вывести числа, делящиеся на К в интервале от 0 до 100, используя оператор continue.
14. Вывести нечетные числа от 0 до 100, используя оператор continue.
53
15. Написать программу для выполнения следующих действий. При вводе в строку цифры 0 выдается сообщение «Это 0, введите другую цифру». Ввод продолжается.
16. Написать программу для выполнения следующих действий. При вводе цифры от 0 до 9 будет выводиться ее название. Использовать оператор go to.
17. Задать строку из произвольного количества цифр и посчитать количество четных и нечетных введенных цифр.
18. Написать программу для выполнения следующих действий. При вводе в строку цифр нечетной цифры выдается сообщение «Это нечетная цифра, введите другую цифру», ввод продолжается.
71
23
25
67
65
29
31
19. y(x)=(ln|e+x|-ax)/(bx-a), x=3.9(0.2)4.7.
20. y(x)=(sin(x2)+ax)/(ex-b), x=2.9(0.3)4.1.
Лабораторная работа № 12
УКАЗАТЕЛИ И АДРЕСА ОБЪЕКТОВ
Цель: Изучить свойства указателей и методы адресации.
Общие сведения
Специальными объектами в программах на языках Си и Си++ являются указатели. О них уже кратко говорилось, например, в связи с операциями new и delete для динамического управления памятью. Различают указатели-переменные (именно их мы будем называть указателями) и указатели-константы. Значениями указателей служат адреса участков памяти, выделенных для объектов конкретных типов. Именно поэтому в определении и описании указателя всегда присутствует обозначение соответствующего ему типа. Эта информация позволяет в последующем с помощью указателя получить доступ ко всему сохраняемому объекту в целом.
Указатели делятся на две категории – указатели на объекты и указатели на функции. Выделение этих двух категорий связано с отличиями в свойствах и правилах использования. Например, указатели функций не допускают применения к ним арифметических операций, а указатели объектов разрешено использовать в некоторых арифметических выражениях. Начнем с указателей объектов.
В простейшем случае определение и описание указателя-переменной на некоторый объект имеют вид:
type *имя_указателя;
где type – обозначение типа; имя_указателя – это идентификатор; * – унарная операция раскрытия ссылки (операция разыменования; операция обращения по адресу; операция доступа по адресу), операндом которой должен быть указатель (именно в соответствии с этим правилом вслед за ней следует имя указателя).
Признаком указателя при лексическом разборе определения или описания служит символ '*', помещенный перед именем.
Таким образом, при необходимости определить несколько указателей на объекты одного и того же типа этот символ '*' помещают перед каждым именем. Например, определение int *ilp, *i2p, *i3p, i;
43
вводит три указателя на объекты целого типа ilp, i2p, i3p и одну переменную i целого типа. Переменной i будет отведено в памяти 2 байта (ТС++ или ВС++), а указатели ilp, i2p, i3p разместятся в участках памяти, размер которых также зависит от реализации, но которые только иногда имеют длину 2 байта.
В совокупности имя типа и символ '*' перед именем воспринимаются как обозначение особого типа данных – "указатель на объект данного типа".
При определении указателя в большинстве случаев целесообразно выполнить его инициализацию. Формат определения станет таким:
type *имя_указателя инициализатор;
Как упоминалось, инициализатор имеет две формы записи, поэтому допустимы следующие две формы определения указателей:
type *имя_указателя = инициализирующее_выражение;
type *имя_указателя (инициализирующее_выражение);
В качестве инициализирующего выражения должно использоваться константное выражение, частными случаями которого являются:
- явно заданный адрес участка памяти;
- указатель, уже имеющий значение;
- выражение, позволяющее получить адрес объекта с помощью операции '&'.
Если значение константного выражения равно нулю, то это нулевое значение преобразуется к пустому (иначе нулевому) указателю. Синтаксис языка гарантирует, что этот указатель отличен от указателя на любой объект. Кроме того, внутреннее (битовое) представление пустого указателя может отличаться от битового представления целого значения 0. В компиляторах ТС++ и ВС++ условное нулевое значение адреса, соответствующее значению пустого указателя, имеет специальное обозначение NULL. Примеры определений указателей:
char сс = 'd'; // Символьная переменная (типа char)
char *рс = &cс; // Инициализированный указатель на объект
// типа char
char *ptr(NULL); // Нулевой указатель на объект типа char
char *p; // Неинициализированный указатель на
// объект типа char
49
Переменная инициализирована значением символьной константы 'd'. После определения (с инициализацией) указателя рс доступ к значению переменной возможен как с помощью имени, так и с помощью адреса, являющегося значением указателя-переменной рс. В последнем случае должна применяться операция разыменования '*' (получение значения через указатель). Таким образом, при выполнении оператора
cout « "\n cc равно "« cc «" и *рс = "« *рс;
будет выведено:
cc равно d и *рс = d.
Указатели ptr и р, определенные в нашем примере, пользуются различными "правами". Указатель ptr получил нулевое начальное значение (пустой указатель), и попытка его разыменования будет бесперспективной.
Не нужно надеяться, что пустой указатель связан с участком памяти, имеющим нулевой адрес или хранящим нулевое значение. Синтаксис языка Си++ этого не гарантирует. Однако, присвоив затем ptr значение адреса уже существующего объекта, можно осмысленно применять операцию разыменования.
Например, любой из операторов присваивания ptr =&cc; или ptr =pc; свяжет ptr с участком памяти, выделенным для переменной ее, т. е. после их выполнения значением *ptr будет 'd'.
Присвоив указателю адрес конкретного участка памяти, можно с помощью операции разыменования не только получать, но и изменять содержимое этого участка памяти. Например, операторы присваивания *pc = ‘+’; или ptr = pс; *ptr = ‘+’; сделают значением переменной ее символ ' +'.
45
Унарное выражение *указатель обладает в некотором смысле правами имени переменной, т. е. *рс и *ptr служат синонимами (псевдонимами, другими именами) имени ее. Выражение *указатель может использоваться практически везде, где допустимо использование имен объектов того типа, к которому относится указатель. Однако это утверждение справедливо лишь в том случае, если указатель инициализирован при определении явным способом. В нашем примере не инициализирован указатель р. Поэтому попытки использовать выражение *р в левой части оператора присваивания или в операторе ввода неправомерны. Значение указателя р неизвестно, а результат занесения значения в неопределенный участок памяти непредсказуем и иногда может привести к аварийному событию.
*р = ‘%’; // Ошибочное применение неинициализированного р
Если присвоить указателю адрес конкретного объекта (р = *бес;) или значение уже инициализированного указателя (р = рс;), то это превратит *р в синоним (псевдоним) уже имеющегося имени объекта.
Чтобы связать неинициализированный указатель с новым участком памяти, еще не занятым никаким объектом программы, используется оператор new или присваивается указателю явный адрес:
р = new char; // Выделили память для переменной типа char
// и связали указатель р с этим участком
// памяти
р = (char *)Oxb8OOOOOO; // Начальный адрес видеопамяти
// ПЭВМ для цветного дисплея
// в текстовом режиме
Обратите внимание на необходимость преобразования числового значения к типу указателя (char *).
После любого из таких операторов можно использовать *р для записи в память нужных символьных значений. Например, станут допустимы операторы:
*р = '&';
или
cin » *р;
Числовое значение адреса может быть использовано не только во
47
время присваивания указателю значения в ходе выполнения программы, но и при инициализации указателя при его определении. Нужно только не забывать о необходимости явного преобразования типов. Например, в следующем определении указатель с именем Computer при инициализации получает значение адреса того байта, в котором содержатся сведения о типе компьютера, на котором выполняется программа (справедливо только для ЭВМ, совместимых с IBM PC):
char *Computer = (char *)OxFOOOFFFE;
Работая с указателями и применяя к ним операцию '*' (разыменования), стоит употреблять словесное описание ее действия. Операцию разыменования '*' вместе с указателем при их использовании в выражении можно объяснить как получение значения, размещенного по адресу, равному значению указателя. Если та же конструкция находится слева от знака операции присваивания или в операторе ввода данных, то действие таково: разместить значение по адресу, равному значению указателя.
В соответствии с соглашениями, принятыми в операционной системе MS-DOS, байт основной памяти, имеющий шестнадцатеричный адрес OxFOOOFFFE, может содержать следующие коды:
FF (ДЛЯ IBM PC);
FE (ДЛЯ IBM PC XT);
FD (для IBM PCjr);
FA (для IBM PC AT).
С помощью введенного выше указателя Computer несложно получить доступ к содержимому этого байта идентификации типа ПЭВМ. Следующая программа решает эту задачу:
//проверка типа компьютера (обращение к байту памяти)
#include
void main(void)
{ char *Con>puter = (char *)OxFOOOFFFE;
cout « "\nПрограмма выполняется на ";
switch (*Computer)
{ case (char)OxFF: cout « "ПЭВМ типа IBM PC.";
break ;
case (char)OxFE: cout « "ПЭВМ типа IBM PC XT.";
break; case (char)OxFD: cout « "ПЭВМ типа IBM PCjr.";
break ;
case (char)OxFC: cout « "ПЭВМ типа IBM PC AT.";
break ;
default: cout « "ПЭВМ неизвестного типа ”;
}
}
Результат выполнения на ПЭВМ с процессором 80386 при использовании модели памяти Large:
программа выполняется на ПЭВМ типа IBM PC AT.
В тексте программы обратите внимание на явные преобразования типов. Во-первых, целочисленный шестнадцатеричный код адреса преобразуется к типу char * определяемого указателя computer. Значением computer служит величина типа char, поэтому в метках переключателя после case также должны быть значения типа char. Явные преобразования типов (char) помещены перед шестнадцатеричными кодами.
При определении указателя как сам он, так и его значение могут быть объявлены константами. Для этого используется модификатор const:
type const * const имя_указателя инициализатор;
Модификаторы const – это необязательные элементы определения. Ближайший к имени указателя модификатор const относится собственно к указателю, а const перед символом '*' определяет "константность" начального значения, связанного с указателем. Мнемоника очевидна, так как выражение *имя_указателя есть обращение к содержимому соответствующего указателю участка памяти. Таким образом, определение неизменяемого (константного) указателя имеет следующий формат:
type * const имя_указателя инициализатор;
Для примера определим указатель-константу key_byte и свяжем его с байтом, отображающим текущее состояние клавиатуры ПЭВМ IBM PC:
char * const key byte = (char *)1047; case break ;
case (char)OxFC: cout « "ПЭВМ типа IBM PC AT.";
break;
default: cout « "ПЭВМ неизвестного типа.
}}
45
Унарное выражение *указатель обладает в некотором смысле правами имени переменной, т. е. *рс и *ptr служат синонимами (псевдонимами, другими именами) имени ее. Выражение *указатель может использоваться практически (char)OxFD: cout « "ПЭВМ типа IBM PCjr."; не допустит компилятор и выдаст сообщение об ошибке:
Error...: Cannot modify a const object.
Формат определения указателя на константу:
type const * имя указателя инициализатор;
Например, введем указатель на константу целого типа со значением 0:
const int zero =0; // Определение константы
int const *point_to_const= &zero; // Указатель на константу 0
Операторы вида *point_to_const = 1; cin » *point to const; недопустимы, т. к. каждый из них – это попытка изменить значение константы 0. Однако операторы point_to_const = &CC; point to const = NULL; вполне допустимы. Они разрывают связь указателя point_to_const с константой 0, однако не меняют значения этой константы, т. е. не изменяют ее изображение в фиксированном участке основной памяти.
Можно определить неизменяемый (постоянный) указатель на константу. Например, иногда полезен так называемый определенный указатель-константа на константное значение:
const float pi = 3.141593;
float const *const pointpi = π
Здесь невозможно изменить значение константы, обращаясь к ней с помощью выражения *pointpi. Нельзя изменить и значение указателя pointpi, т. е. он всегда "смотрит" на константу 3.141593.
Работая с указателями, постоянно используют операцию & – получение адреса объекта. Для нее существуют естественные ограничения:
- Нельзя определять адрес неименованной константы, т. е. недопустимы выражения &3.141593 или &'?';
- Нельзя определять адрес значения, получаемого при вычислении скалярных выражений, т. е. недопустимы конструкции
&(44 * х - z) или &(а + b) != 12;
- Нельзя определить адрес переменной, относящейся к классу памяти register. Следовательно, ошибочной будет последовательность операторов:
int register Numb = 1;
int *prt Numb = &Numb;
Цитируя проект стандарта языка и обобщая сказанное, можно сделать вывод, что операция & применима к объектам, имеющим имя и размещенным в памяти. Ее нельзя применять к выражениям, неименованным константам, битовым полям структур и объединений, к регистровым переменным и внешним объектам (файлам).
Однако допустимо получать адрес именованной константы, т. е. правомерна, например, такая последовательность определений:
const float Euler = 2.718282;
float *pEuler = (float *)&Euler;
Обратите внимание на необходимость явного приведения типов, так как &Euler имеет тип const float *, а не float *.
Практические задания:
1. Присвоить указателю адрес переменной.
2. Определить количество байтов, извлекаемых из памяти при выполнении операции разадресации.
3. Присвоить указателю адрес переменной типа float.
4. Создать указатель, который сможет указывать объект любого типа. Присвоить указателю типы данных (float, int).
5. Присвоить один указатель другому.
6. Сравнить два указателя. Выполнить сравнение два раза, когда указатели равны и когда указатели не равны.
7. Выбрать данные из памяти с помощью разных типов указателей (float, char).
8. Записать в память данные и просмотреть содержимое памяти.
9. Вычислить контрольную сумму последовательности байтов, ограниченной двумя указателями.
10. Увеличить указатель на размер адресуемой им структуры.
11. Преобразовать указатель одного типа в указатель другого.
51
12. Выбрать данные из памяти с помощью разных типов указателей (int, long).
13. Уменьшить указатель на размер адресуемой им структуры.
14. Создать указатель, который сможет указывать объект любого типа. Присвоить указателю типы данных (long, char).
15. Произвести вычитание указателей.
16. Сложить два указателя.
17. Изменить указатель на произвольную величину.
18. Отобразить адреса и длины указателей разных типов.
19. Спрограммировать цепочку из трех указателей.
20. Произвести вычитание адресов и указателей разных длин.
Лабораторная работа № 13
МАССИВЫ И УКАЗАТЕЛИ
Цель: Изучить способы доступа к произвольному элементу массива с помощью индексированных переменных (указателей).
Общие сведения
Теперь необходимо тщательно разобрать соотношение между массивами и указателями.
Самое загадочное в массивах языков Си и Си++ – это их различное "поведение" на этапах определения и использования.
При определении массива ему выделяется память так же, как и массивам других алгоритмических языков (например, ПЛ/1 или Паскаль). Но как только память для массива выделена, имя массива воспринимается как константный указатель того типа, к которому отнесены элементы массива. Существуют исключения, например, применение имени массива в операции sizeof. В этой операции массив "вспоминает" о своем отличии от обычного указателя, и результатом является размер в байтах участка памяти, выделенного не для указателя, а для массива в целом. Исключением является и применение операции & (получения адреса) к имени массива. Результат – адрес начального (с нулевым индексом) элемента массива. В остальных случаях значением имени массива является адрес первого элемента массива, и это значение невозможно изменить. Таким образом, для любого массива соблюдается равенство:
имя массива == &имя_массива == &имя_массива[0]
Итак, массив – это один из структурированных типов языка Си++. От других структурированных данных массив отличается тем, что все его элементы имеют один и тот же тип и что элементы массива расположены в памяти подряд. Определение одномерного массива типа type:
type имя массива[константное_выражение];
Здесь имя массива – идентификатор; константное_выражение, если оно присутствует, определяет размер массива, т. е. количество элементов в массиве. В некоторых случаях допустимо описание массива без указания количества его элементов, т. е. без константного выражения в квадратных скобках.
Например: extern unsigned long UL[]; суть описание внешнего массива, который определен в другой части программы, где ему выделена память и (возможно) присвоены начальные значения его элементам.
При определении массива может выполняться его инициализация, т. е. элементы массива получают конкретные значения. Инициализация выполняется по умолчанию (без вмешательства программиста), если массив статический или внешний. В этих случаях всем элементам массива компилятор автоматически присваивает нулевые значения:
void f(void)
( static float F[4]; // Внутренний статический массив
long double A[10]; // Массив автоматической памяти
}
void main()
{ extern int D[] ; //Описание массива
. . .
f();
. . .
int D[8]; // Внешний массив (определение)
Массивы D[8] и F[4] инициализированы нулевыми значениями. В основной программе main () массив D описан без указания количества его элементов. Массив А[10] не получает конкретных значений своих элементов при определении.
Явная инициализация элементов массива разрешена только при его определении и возможна двумя способами: либо с указанием размера массива в квадратных скобках, либо без явного указания (без конкретного выражения) в квадратных скобках:
char СН[] = { 'А', 'В', 'С', 'D'}; // Массив из 4 элементов
int IN[6] = { 10, 20, 30, 40 }; // Массив на 6 элементов
char STR[] = "ABCD"; // Массив из 5 элементов
Количество элементов массива СН компилятор определяет по числу начальных значений в списке инициализации, помещенном в фигурных скобках при определении массива. В массиве IN шесть элементов, но только первые четыре из них явно получают начальные значения. Элементы IN[4], IN[S] либо не определены, либо имеют нулевые значения, когда массив внешний или статический. В массиве STR элемент STR[4] равен ' \о', а всего в этом массиве 5 элементов.
При отсутствии константного выражения в квадратных скобках список начальных значений в определении массива обязателен. Если размер массива явно задан, то количество элементов в списке начальных значений не должно превышать размера массива. Ошибочные определения:
float А[]; // Ошибка в определении массива - нет размера
double В[4] = ( 1, 2, 3, 4, 5, 6 ); // Ошибка инициализации
В тех случаях, когда массив не определяется, а описывается, список начальных значений задавать нельзя. В описании массива может отсутствовать и его размер:
extern float E[]; // Правильное описание внешнего массива
Предполагается, что в месте определения массива Е для него выделена память и выполнена инициализация. Описание массива (без указания размера и без списка начальных значений) может использоваться в списке формальных параметров определения функции и в спецификации параметров прототипа функции. Примеры:
float MULTY(float G[], float F[]) // Определение функции MOLTY ( .. .
тело_функции
void print array(int I[]); // Прототип функции print_array
Доступ к элементам массива с помощью индексированных переменных мы уже несколько раз демонстрировали на примерах. Приведем еще один, но предварительно обратим внимание на полезный прием, позволяющий контролировать диапазон изменения индекса массива при его "просмотре", например, в цикле. С помощью операции sizeof (имя_массива) можно определить размер массива в байтах, т. е. размеры участка памяти, выделенного для массива. Так как все элементы массива имеют одинаковые размеры, то частное sizeof (имя_массива) / sizeof (имя_массива[0]) определяет количество элементов в массиве. Следующий фрагмент программы печатает значения всех элементов массива:
for (int i = 0; i < sizeof(m)/sizeof(m[0]); i++)
cout « "m[« i « "] = " « mi[i] « " ";
Результат на экране дисплея:
m[0] = 10 m[l] = 20 m[2] = 30 m[3] = 40
Еще раз отметим, что для первого элемента массива индекс равен 0. Цикл завершается при достижении i значения 4.
По определению имя массива является указателем-константой, значением которой служит адрес первого элемента массива (с индексом 0). Таким образом, в нашем примере &m == m. Раз имя массива есть указатель, то к нему применимы все правила адресной арифметики, связанной с указателями. Более того, запись имя_массива[индекс] является выражением с двумя операндами. Первый из них, т. е. имя массива, – это константный указатель (адрес начала массива в основной памяти), индекс – это выражение целого типа, определяющее смещение от начала массива.
Используя операцию обращения по адресу * (раскрытие ссылки, разыменование), действие бинарной операции [] можно объяснить так:
55
*(имя_массива + индекс)
Таким образом, операндами для операции [] служат имя массива и индекс. В языках Си и Си++ принято, что индексы массивов начинаются с нуля, т.е. массив int z[3] из трех элементов включает индексированные элементы z[0], z[l], z[2]. Это соглашение языка становится очевидным, если учесть, что индекс определяет не номер элемента, а его смещение относительно начала массива. Таким образом, *z – обращение к первому элементу z[0],*(z + 1) – обращение ко второму элементу z[l] и т.д. В следующей программе показано, как можно не использовать квадратные скобки при работе с элементами массива:
//работа с элементами массива без скобок []
#include
void main()
{ char x[] " "DIXI"; // "Я сказал (высказался)"
int i = 0;
while (*(x + i) ! = '\0') cout « "\n" « * (х + i++);
Результат выполнения программы: слово "DIXI", написанное в столбик.
В данном примере оператор цикла с заголовком while выполняется, пока верно выражение в скобках, т. е. пока очередной символ массива не равен '\о'. Это же условие можно проверять и при таком заголовке цикла:
while (*(x + i))
В цикле при каждом вычислении выражения х + i++ используется текущее значение i, которое затем увеличивается на 1. Тот же результат будет получен, если для вывода в цикле поместить оператор cout « '\n' « x[i++]; (квадратные скобки играют роль бинарной операции, а операндами служат имя массива х и индекс i++.). Индексированный элемент можно употребить и в заголовке цикла:
while(x[i]).
Обращение к элементу массива в языке Си++ относят к постфиксному выражению вида PE[IE]. Постфиксное выражение PE должно быть указателем на нужный тип, выражение IE в квадратных скобках должно быть целочисленного типа. Таким образом, если PE – указатель на массив, то pe[ie] – индексированный элемент этого массива. Другой путь доступа к тому же элементу массива * (ре + ie). Поскольку сложение коммутативно, то возможна эквивалентная запись *(ie + ре) и, следовательно, ie[pe] именует тот же элемент массива, что и ре [ie] .
Сказанное иллюстрирует следующая программа:
//коммутативность операции []
#include
void main()
{int m[] = {10, 20, 30, 40 );
int j=1;
cout << “ \ nm[j] = ” << m[j];
cout << “ *(m + j++) = ” << (m + j++);
cout << “\n*(++j + m) = ” << * (++j + m);
cout << “ j[m] = ” << j[m]
cout << “\n*( j-- + m) = ” << *(j-- + m);
cout << “ j-- [m] = ” << j-- [m]
cout << “\n*( --j + m) = ” << *(--j + m);
cout << “--j [m] = ” *--j [m];
cout << “\n3[m] = ” << 3[m] << “ 2 [m]m = ” << 2[m] <<
“ 1[m] = “ << 1[m] << “ 0[m] = “ << 0[m];
}
Впечатляющий результат на экране:
m[j] = 20 *(m + j++)=20
*(++j + m) =40 j[m] = 40
*( j-- + m) = 40 j--[m] = 30
*( --j + m) = 10 --j[m] = 9
3[m] = 40 2[m] = 30 1[m] = 20 0[m] = 9
Обратите внимание на порядок вычислений. В выражении j—[m] вычисляется j[m], а затем j--. В выражении –j[m] вычисляется j[m] и результат уменьшается на 1, т. е. --(j[m]).
В некоторых не совсем обычных конструкциях можно использовать постфиксное выражение PE[IE] с отрицательным значением индекса. В этом случае PE должен указывать не на начало массива, т. е. не на его нулевой элемент. Например, последовательность операторов:
char А[] = "СОН";
char *U = &A[2] ;
cout « "\n" « U[0] « U[-1] « U[-2];
приведет к выводу на экран слова НОС. Toт же результат будет получен при использовании оператора cout « "\n" « *U « *U-- « *U--;
То же самое слово будет выведено на экран при таком использовании вспомогательной переменной индекса:
int i = 2;
cout « "\n" « i[A] « i[A - 1] « i[A - 2];
Как видно из приведенных примеров, перемещение указателя от одного элемента к другому выполняется в естественном порядке, т. е. при увеличении индекса или указателя на 1 переходим к элементу с большим номером. Внутри массива нет проблемы "обратного" размещения в памяти последовательно определенных в программе объектов.
Так как имя массива есть не просто указатель, а указатель-константа, то значение имени массива невозможно изменить. Попытка получить доступ ко второму элементу массива int z[4] с помощью выражения *(++z) будет ошибочной. А выражение *(z+1) вполне допустимо.
Следующая программа иллюстрирует естественный порядок размещения в памяти элементов массива и обратный порядок расположения массивов, последовательно определенных в программе.
//адреса массивов и использование указателей для доступа
#include
void main(void)
{ int A[] = { 1, 2, 3, 4, 5, 6 );
int B[] = ( 1, 2, 3, 4, 5, 6 );
int *pA = А, *рВ = &В[5];
cout « "\nАдреса массивов: &А = "« &А «
" &В = " « &В « "\n";
while (*pA < *рВ)
cout « " *рА++ + *рВ-- = " « *рА++ + *рВ--;
cout « "\n Значения указателей после цикла:";
cout « "\n рА = " « рА « " рВ = " « рВ;
35
}
Результат выполнения программы:
Адреса массивов: &А = 0x8d8e0fec &B = 0x8d8e0fe0
*рА++ + *рВ-- = 7 *рА++ + рВ-- = 7 *рА++ + *рВ-- = 7
Значения указателей после цикла:
рА = 0x8d8e0ff2 рВ = 0x8d8e0fe4
Обратите внимание, что тот же результат будет получен, если определить указатели таким образом: int *рА = &А[0], *рВ = (В + 5); .
Как видно по значениям адресов &А, &в, массивы А и В размещены в памяти в обратном порядке по сравнению с их определением в программе. Внутри массивов элементы размещены в естественном порядке.
Инициализация символьных массивов может быть выполнена не только с помощью строк, но и с помощью списка инициализации, где последовательно указаны значения каждого отдельного элемента:
char stroka[] = { 'S', ‘I', 'С', '\0' ); .
При такой инициализации списком в конце символьного массива можно явно записать символ ' \0'. Только при этом одномерный массив (в данном случае stroka) получает свойства строки, которую можно использовать, например, в библиотечных функциях для работы со строками или при выводе строки на экран дисплея с помощью оператора cout << stroka;.
Продолжая изучать массивы и указатели, рассмотрим конструкцию:
type *имя;
В зависимости от контекста она описывает или определяет различные объекты типа type *. Если она размещена вне любой функции, то объект есть внешний указатель, инициализированный по умолчанию нулевым значением.
Внутри функции это тоже указатель, но не имеющий определенного значения. В обоих случаях его можно связать с массивом элементов типа type несколькими способами как во время определения, так и в процессе выполнения программы. В определениях существуют следующие возможности:
type *имя » имя_уже_определенного_массива_типа_tуре;
type *имя = new tуре[размер_массива];
type *имя = (type *)mаllос(размер * sizeof(type));
Например:
long arlong[] = ( 100, 200, 300, 400); // Определили массив
long *arlo = arlong; // Определили указатель,
// связали его с массивом
int *arint = new int[4]; // Определили указатель
// и выделили участок памяти float *arfloat = new float[4]; // Определили указатель
// и выделили участок памяти
double *ardouble = // Определили указатель и
(double *)malloc(4 * sizeof(double));// выделили участок
//памяти
В примерах определены четыре массива из четырех элементов в каждом.
Массив arlong инициализирован списком начальных значений в фигурных скобках. Массив, связанный с указателем arfloat, с помощью операции new получил участок памяти нужных размеров (16 байт), однако эта память явно не инициализирована. Без инициализации остается и массив, связанный с указателем arint. Память для элементов массива, связанного с указателем ardouble, выделена с помощью библиотечной функции ша11ос() языка Си. В ее параметре приходится указывать количество выделяемой памяти (в байтах). Так как эта функция возвращает значение указателя типа void *, то потребовалось явное преобразование типа (double *). Выделенная память явно не инициализирована.
В отличие от имени массива указатель, связанный с массивом, никогда не "вспоминает" об этом факте. Операция sizeof, применяемая к такому указателю, вернет количество байтов, занятых именно этим указателем, а вовсе не размер массива, связанного с указателем. Операция & указатель возвращает адрес указателя в основной памяти, а никак не адрес начала массива, на который настроен указатель. Таким образом, для наших примеров:
sizeof arint ==4 - длина указателя int *
sizeof *arint ==2- длина элемента arint [ 0 ]
sizeof arlong ==16
sizeof arlo ==4
Как и при обычном определении массивов типа char, указатели char* могут инициализироваться с помощью символьных строк:
char *имя_указателя = "символьная строка";
char *имя_указателя = { "символьная строка" } ;
char *имя_указателя ("символьная строка");
В этом случае количество элементов в символьном массиве, связанном с указателем, как обычно, на 1 больше, чем количество символов в инициализирующей строке. Примеры определения массивов типа char:
char *car1 = "строка-1";
char *car2 = { "строка-2" );
char *car3("строка-3");
Длины массивов, связанных с указателями сar1, car2, саr3, одинаковы. В последнем элементе каждого из этих массивов находится символ ' \0’.
Операция sizeof, примененная к указателю на символьный массив, возвращает длину не массива, а самого указателя, например, sizeof(carl) == 4.
Как и при обычном определении массивов, к элементам массивов, связанных с указателями, существует несколько путей доступа. Принципиально различных путей два: с помощью операции [] и с помощью операции разыменования. В качестве иллюстрации приведем пример программы, использующей оба способа доступа:
//Копирование массивов-строк
#include
#include
void main() // Для функции sir ten()
char * arch = "0123456789";// Массив из 11 элементов
int k = strlen(arch) + 1; // k - размер массива
char * newar = new char[k];
for (int i = 0; i < k;)
{ newar[i++] = *arch++;
if (!(i%3)) cout « "\narch = " « arch;
{
cout « "\nk = " « k « " newar =" « newar;
cout « "\nsizeof(arch) = " « sizeof(arch);
Результат выполнения программы:
arch = 3456789
arch = 6789
arch = 9
k = 11 newar = 0123456789
sizeof(arch) = 4
Для определения длины массива, не имеющего фиксированного имени, нельзя использовать операцию sizeof. Поэтому в заголовке программы включен файл string.h с прототипами функций для работы со строками. Одна из них, а именно функция strlen(), определяющая длину строки-параметра, использована для определения количества элементов в массиве, связанном с указателем arch. Функция strlen () возвращает количество "значащих" символов в строке без учета конечного нулевого символа.
Именно поэтому при определении значения k к результату strlen(arch) прибавляется 1.
В программе определен и инициализирован символьный массив-строка, связанный с указателем arch, и выделена память операцией new для такого же по типу и размерам, но динамического и неинициализированного массива, связанного с указателем newar. Длина каждого из массивов с учетом "невидимого" в строке инициализации символа '\0' равна 11.
"Перебор" элементов массивов в программе выполняется по-разному.
Доступ к компонентам массива, связанного с указателем newar, реализован с помощью операции [], к элементам второго массива – с помощью разыменования *. У массива, связанного с указателем newar, изменяется индекс. Указатель arch изменяется под действием операции ++. Такой возможности не существует для обычных массивов.
В программе использована еще одна возможность вывода с помощью операции « в стандартный поток cout – ему передается имя (указатель) массива, содержащего строку, а на экран выводятся значения всех элементов массива в естественном порядке, за исключением последнего символа ' \0'. При этом необязательно, чтобы указатель адресовал начало массива. Указатель arch "перемещается" по элементам массива, поэтому в цикле выводятся в поток cout разные "отрезки" исходной строки. Чтобы сократить количество печати, в цикл добавлен условный оператор, в котором проверяется значение модуля i%3.
Обратите внимание, что здесь выполнен вывод массива-строки. Если бы указатель newar был связан не со строкой, а с массивом произвольного типа, то вывод содержимого на экран дисплея с помощью cout « был бы невозможен.
Итак, в случае определения массива с использованием указателя этот указатель является переменной и доступен изменениям. Такими свойствами обладают arch и newar в нашей программе. Вот еще варианты циклов копирования:
for (; *newar'='\0'; *newar++ = *arch++);
while (*newar++ = *arch++);
Результат будет тем же самым. Однако указатель newar в обоих случаях сместится с начала массива, и его нельзя в дальнейшем использовать, например, для печати строки.
При определении указателя ему может быть присвоено значение другого указателя, уже связанного с массивом того же типа:
int pi1 [] = ( 1, 2, 3, 4 );
int *pi2 = pi1; // pi2 - другое имя" для pi1
double pd1 [] = { 10, 20, 30, 40, 50 );
double *pd2 = pd1; // pd2 - "другое имя" для pd1
После таких определений к элементам каждого из массивов возможен доступ с помощью двух разных имен. Например:
cout « pi2[0]; // Выводится 1
*pil =0; // Изменяется pi1[0]
cout « *pi2; // Выводится 0
cout « pd1[3]; // Выводится 40
*(pd2 + 3) = 77; // Изменяется pd2[3]
cout « pdl[3] // Выводится 77.
63
Такие же присваивания указателям допустимы и в процессе исполнения программы, т. е. последовательность операторов int *pi3; pi3 = pil; свяжет еще один указатель pi3 с тем же самым массивом int из четырех элементов.
Возможность доступа к элементам массива с помощью нескольких указателей не следует путать с продемонстрированной в программе схемой присваивания одному массиву значений элементов другого массива. Рассмотрим такой пример:
char str[] = "массив” // Определили массив с именем str
char *pstr = str; // Определили указатель patr и
// "настроили" его на массив str
pstr = "строка"; // Изменили значение указателя,
// но никак не изменили массив str.
Присваивание указателю pstr не переписывает символьную строку "строка" в массив str, вместо этого изменится значение самого указателя pstr. Если при определении он указывал на начало массива с именем str, то после присваивания его значением станет адрес того участка памяти, в котором размещена строковая константа "строка". Чтобы в процессе выполнения программы изменить значения элементов массива, необходимо, явно или опосредованно (с помощью указателей или средств ввода данных), выполнить присваивания. Например, заменить содержимое массива-строки str таким дополнительным оператором while (atr++ = pstr++); или его аналог с индексированными переменными for (int i = 0; str[i] = pstr[i]; i++);
При переписывании одного массива в другой длина заполняемого массива должна быть не меньше длины копируемого массива, т. к. никаких проверок предельных значений индексов язык Си++ не предусматривает, а выход за границу индекса часто приводит к аварийной ситуации. В обоих операторах учтено, что длины строк "массив" и "строка" одинаковы, а в конце строки всегда размещается нулевой символ, по достижении которого цикл завершается.
Примечание. Для копирования строк в стандартной библиотеке языков Си и Си++ имеется функция strcpy(), прототип которой находится в заголовочном файле string. h.
Возможно "настроить" на массив указатели других типов, однако при этом потребуются явные приведения типов:
char *pch = (char *) pil;
float *pfl = (float *) pil;
Так, определенные указатели позволят по-другому "перебирать" элементы массива. Выражения * (pch + 2) или рсh[2] обеспечивают доступ к байту с младшим адресом элемента pi1[l]. Индексированный элемент pfl[l] и выражение * (pfl + 1) соответствуют четырем байтам, входящим в элементы pil [2], pil [3]. Например, присваивание значения индексированному элементу pfl[l] изменит в общем случае как pil [2] = 3, так и pil [3] = 4. После выполнения операторов pfl[l] = 1.0/3.0; cout « "\npil[2].« " « pil[2] « " pil[3] = " « Pil [3]; На экране появится такой результат: pil[2] = -21845 pil[3] = 16042; что совсем не похоже на исходные значения pil[2] = 3 pil[3] – 4;
Итак, допустимо присваивать указателю адрес начала массива. Однако имя массива, являясь указателем, не обладает этим свойством, т. к. имя массива есть указатель-константа. Рассмотрим пример:
long arl[] = { 10, 20, 30, 40 };
long *pl = new long[4];
Определены два массива по 16 байт каждый. Операторы присваивания для имен этих массивов обладают разными правами:
arl = pi; // Недопустимый оператор
pi = arl; // Опасный оператор
Первый оператор недопустим, т. к. имя массива arl соответствует указателю-константе. Второй оператор синтаксически верен, однако приводит к опасным последствиям – участок памяти, выделенный операцией new long[4], становится недоступным. Его нельзя теперь не только использовать, но и освободить, т. к. в операции delete нужен адрес начала освобождаемой' памяти, а его значение потеряно.
Мы неоднократно отмечали особую роль символьных строковых констант в языках Си и Си++. В языке Си++ нет специального типа данных "строка". Вместо этого каждая символьная строка в памяти ЭВМ представляется в виде одномерного массива типа char, последним элементом которого является символ '\0'. Изображение строковой константы (последовательность символов, заключенная в двойные кавычки) может использоваться по-разному. Если строка используется для инициализации массива типа char, например, так: то адрес первого элемента строки становится значением указателя-константы (имени массива) array.
Если строка используется для инициализации указателя типа char* : char * pointer = "инициализирующая строка"; то адрес первого элемента строки становится значением указателя-переменной (pointer).
И, наконец, если использовать строку в выражении, где разрешено применять указатель, то используется адрес первого элемента строки:
char * string;
string = "строковый литерал";
В данном примере значением указателя string будет не вся строка "строковый литерал", а только адрес ее первого элемента.
Практические задания:
1. Ввести строку латинских и русских букв вперемешку. Заменить в ней все русские буквы латинскими.
27
2. Найти сумму положительных элементов массива J(10).
3. Ввести две символьные строки одинаковой длины. Сравнить их символ за символом до первого отличного. Напечатать номер первого несравнившегося символа.
4. Найти сумму отрицательных элементов массива J(10).
5. Ввести строку латинских и русских букв вперемешку. Заменить в ней все русские буквы пробелами. Вывести полученную строку.
6. Ввести строку символов. Подсчитать, сколько раз в ней встречается символ «А» .
7. Найти наибольший элемент вектора J(20).
8. Найти наименьший элемент вектора J(20).
9. Ввести строку символов. Сформировать строку вдвое большей длины, где каждый символ предворяется пробелом. Вывести полученную строку.
10. Передать массив I(10) в массив J(10) в обратной последовательности.
11. Ввести строку символов. Сформировать строку вдвое большей длины, где каждый символ повторяется дважды. Вывести полученную строку.
12. Передать в массив I(5) вторую половину массива J(10).
13. Ввести четное количество символов. Вывести вторую половину их в обратной последовательности.
14. Передать массив I(5) в первую половину массива J(10) в обратной последовательности.
15. Ввести четное количество символов. Поменять местами каждую пару символов. Вывести новую строку.
16. Сформировать массив J(10) из массива I(10) по следующему принципу: J(N)=I(N)+N.
17. Ввести четное количество символов. Раздвинуть строку в середине десятью пробелами. Вывести новую строку.
18. Найти сумму четных элементов массива J(10).
19. Ввести строку латинских и русских букв вперемешку в одинаковом количестве. Сформировать из нее отдельно две строки – латинских и русских букв.
20. Сложить поэлементно два массива I(10) и J(10). Результат в массиве I.