Компания ALT Linux
Опубликован: 07.03.2015 | Доступ: свободный | Студентов: 2134 / 486 | Длительность: 24:14:00
Лекция 2:

Общие сведения о языке С++

< Лекция 1 || Лекция 2: 123456 || Лекция 3 >

2.5 Указатели

Указатели широко применяются в С++. Можно сказать, что именно наличие указателей сделало этот язык удобным для программирования. С другой стороны это одна из наиболее сложных для освоения возможностей С++. Идея работы с указателями состоит в том, что пользователь работает с адресом ячейки памяти.

Как правило, при обработке оператора объявления переменной

тип имя_переменной;
		

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

char C; //Выделена память под символьную переменную C
		

Доступ к объявленной переменной осуществляется по её имени. При этом все обращения к переменной заменяются на адрес ячейки памяти, в которой хранится её значение. При завершении программы или функции, в которой была описана переменная, память автоматически освобождается.

Доступ к значению переменной можно получить иным способом — определить собственные переменные для хранения адресов памяти. Такие переменные называют указателями. С помощью указателей можно обрабатывать массивы, строки и структуры, создавать новые переменные в процессе выполнения программы, передавать адреса фактических параметров функциям и адреса функций в качестве параметров.

Итак, указатель это переменная, значением которой является адрес памяти, по которому хранится объект определённого типа (другая переменная). Например, если С это переменная типа char, а Р — указатель на С, значит в Р находится адрес по которому в памяти компьютера хранится значение переменной С.

Как и любая переменная, указатель должен быть объявлен. При объявлении указателей всегда указывается тип объекта, который будет храниться по данному адресу:

тип *имя_переменной;
		

Например:

int *p; //По адресу, записанному в переменной p, будет храниться переменная типа int
		

Звёздочка в описании указателя, относится непосредственно к имени, поэтому чтобы объявить несколько указателей, её ставят перед именем каждого из них:

float *x, y, *z; //Описаны указатели на вещественные числа x и z (сами вещественные
//значения — *x, *z), а так же вещественная переменная y, её адрес — &y.
		

2.6 Операции и выражения

Выражение задаёт порядок выполнения действий над данными и состоит из операндов (констант, переменных, обращений к функциям), круглых скобок и знаков операций. Операции делятся на унарные (например, ) и бинарные (например, а+b). В таблице 2.4 представлены основные операции языка С++.

Таблица 2.4. Основные операции языка С++
Операция Описание
Унарные операции
++ увеличение значения на единицу
-- уменьшение значения на единицу
~ поразрядное отрицание
! логическое отрицание
- арифметическое отрицание (унарный минус)
+ унарный плюс
& получение адреса
* обращение по адресу
(type) преобразование типа
Бинарные операции
+ сложение
- вычитание
* умножение
/ деление
% остаток от деления
<< сдвиг влево
>> сдвиг вправо
< меньше
> больше
<= меньше или равно
>= больше или равно
== равно
!= не равно
& поразрядная конъюнкция (И)
^ поразрядное исключающее ИЛИ
| поразрядная дизъюнкция (ИЛИ)
&& логическое И
|| логическое ИЛИ
= присваивание
*= умножение с присваиванием
/= деление с присваиванием
+= сложение с присваиванием
-= вычитание с присваиванием
%= остаток от деления с присваиванием
<<= сдвиг влево с присваиванием
>>= сдвиг вправо с присваиванием
&= поразрядная конъюнкция с присваиванием
|= поразрядная дизъюнкция с присваиванием
^= поразрядное исключающее ИЛИ с присваиванием
Другие операции
? условная операция
, последовательное вычисление
sizeof определение размера
(тип) преобразование типа

Перейдём к подробному рассмотрению основных операций языка.

2.6.1 Операции присваивания

Обычная операция присваивания имеет вид:

имя_переменной=значение;
			

где значение это выражение, переменная, константа или функция. Выполняется операция так. Сначала вычисляется значение выражения указанного в правой части оператора, а затем его результат записывается в область памяти, имя которой указано слева.

Например,

b=3; //Переменной b присваивается значение, равное трём.
a=b; //Переменной а присваивается значение b.
c=a+2*b; //Переменной c присваивается значение выражения.
c=c +1; //Значение переменой с увеличивается на единицу.
a=a *3; //Значение переменой а увеличивается в три раза.
			

Задача 2.1. Пусть в переменной а хранится значение, равное 3, а в переменную b записали число 5. Поменять местами значения переменных а и b.

Для решения задачи понадобится дополнительная переменная c(см. рис. 2.1). В ней временно сохраняется значение переменной а. Затем, значение переменной bзаписывается в переменную a, а значение переменной c в переменную b.

c=a; //Шаг 1. с=3
a=b; //Шаг 2. a=5
b=c; //Шаг 3. b=3
			
Использование буферной переменной

Рис. 2.1. Использование буферной переменной

Если в операторе присваивания левая и правая часть это переменные разных типов, то происходит преобразование: значение переменной в правой части преобразуется к типу переменной в левой части. Следует учитывать, что при этом можно потерять информацию или получить другое значение.

В С++ существует возможность присваивания нескольким переменным одного и того же значения. Такая операция называется множественным присваиванием и в общем виде может быть записана так:

имя_1 = имя_2 = ... = имя_N = значение;
			

Запись a=b=c=3.14159/6; означает, что переменным a, b и c было присвоено одно и то же значение 3.14159/6.

Операции + =, -=, *=, / = называют составным присваиванием. В таких операциях при вычислении выражения стоящего справа используется значение переменной из левой части, например так:

x+=p; //Увеличение x на p, то же что и x=x+p.
x-=p; //Уменьшения x на p, то же что и x=x-p.
x*=p; //Умножение x на p, то же что и x=x*p.
x/=p; //Деление x на p, то же что и x=x/p.
			

2.6.2 Арифметические операции

Операции +, -, *, / относят к арифметическим операциям. Их назначение понятно и не требует дополнительных пояснений. При программировании арифметических выражений следует придерживаться простых правил. Соблюдать очерёдность выполнения арифметических операций. Сначала выполняются операции умножения и деления (1-й уровень), а затем сложения и вычитания (2-й уровень). Операции одного уровня выполняются последовательно друг за другом. Для изменения очерёдности выполнения операций используют скобки. Таблица 2.5 содержит примеры записи алгебраических выражений.

Таблица 2.5. Примеры записи алгебраических выражений
Математическая запись Запись на языке С++
2\cdot a+b\cdot(c+d) 2*a+b*(c+d)
3\cdot\frac{a+b}{c+d} 3*(a+b)/(c+d)
\frac{3\cdot a-2\cdot b}{c\cdot d} (3*a-2*b)/(c*d) или (3*a-2*b)/c/d
\frac{(b-a)^2}{c+\frac{1}{d-2}}-\frac{a^2+1}{b^2+cd} (b-a)*(b-a)/(c+1/(d-2))-(a*a+1)/(b*b+c*d)

Операции инкремента ++ и декремента -- так же причисляют к арифметическим, так как они выполняют увеличение и уменьшение на единицу значения переменной. Эти операции имеют две формы записи: префиксную (операция записывается перед операндом) и постфиксную (операция записывается после операнда). Так, например оператор p=p+1; можно представить в префиксной форме ++p; и в постфиксной p++;.Эти формы отличаются при использовании их в выражении. Если знак декремента (инкремента) предшествует операнду, то сначала выполняется увеличение (уменьшение) значения операнда, а затем операнд участвует в выражении. Например,

x=12;
y=++x; //В переменных x и y будет храниться значение 13.
			

Если знак декремента (инкремента) следует после операнда, то сначала операнд участвует в выражении, а затем выполняется увеличение (уменьшение) значения операнда:

x=12;
y=x++; //Результат — число 12 в переменной y, а в x — 13.
			

Остановимся на операциях целочисленной арифметики.

Операция целочисленного деления / возвращает целую часть частного (дробная часть отбрасывается) в том случае, если она применяется к целочисленным операндам, в противном случае выполняется обычное деление: 11/4 = 2 или 11.0/4 = 2.75.

Операция остаток от деления % применяется только к целочисленным операндам: 11%4 = 3.

К операциям битовой арифметики относятся следующие операции: &, |, ^, ~, <<, >>. В операциях битовой арифметики действия происходят над двоичным представлением целых чисел.

Арифметическое И (&). Оба операнда переводятся в двоичную систему, затем над ними происходит логическое поразрядное умножение операндов по следующим правилам:

1&1=1, 1&0=0, 0&1=0, 0&0=0.
			

Например, если А=14 и В=24, то их двоичное представление — А=0000000000001110 и В=0000000000011000. В результате логического умножения A и B получим 0000000000001000 или 8 в десятичной системе счисления (рис. 2.2). Таким образом, A&B=14&24=8.

Пример логического умножения

Рис. 2.2. Пример логического умножения
Пример логического сложения

Рис. 2.3. Пример логического сложения

Арифметическое ИЛИ (|). Здесь также оба операнда переводятся в двоичную систему, после чего над ними происходит логическое поразрядное сложение операндов по следующим правилам:

1|1=1, 1|0=1, 0|1=1, 0|0=0.
			

Например, результат логического сложения чисел А=14 и В=24 будет равен A |B=30 (рис. 2.3).

Арифметическое исключающее ИЛИ (^). Оба операнда переводятся в двоичную систему, после чего над ними происходит логическая поразрядная операция ^ по следующим правилам:

1^1=0, 1^0=1, 0^1=1, 0^0=0.
			

Арифметическое отрицание (~). Эта операция выполняется над одним операндом. Применение операции ~ вызывает побитную инверсию двоичного представления числа (рис. 2.4).

Пример арифметического отрицания

Рис. 2.4. Пример арифметического отрицания

Сдвиг влево (M<<L). Число M, представленное в двоичной системе, сдвигается влево на Lпозиций. Рассмотрим операцию 15 << 3. Число 15 в двоичной системе имеет вид 1111. При сдвиге его на 3 позиции влево получим 1111000. В десятичной системе это двоичное число равно 120. Итак, 15 << 3 =120(рис. 2.5). Заметим, что сдвиг на один разряд влево соответствует умножению на два, на два разряда — умножению на четыре, на три — умножению на восемь. Таким образом, операция M << Lэквивалентна умножению числа M на 2 в степени L.

Пример операции "Сдвиг влево"

Рис. 2.5. Пример операции "Сдвиг влево"
Пример операции "Сдвиг вправо"

Рис. 2.6. Пример операции "Сдвиг вправо"

Сдвиг вправо (M>>L). Число M, представленное в двоичной системе, сдвигается вправо на L позиций, что эквивалентно целочисленному делению числа M на 2 в степени L. Например, 15 >> 1=7 (рис. 2.6), 15 >> 3=1.

2.6.3 Логические операции

В С++ определены следующие логические операции || (или), && (и), ! (не). Логические операции выполняются над логическими значениями true (истина) и false (ложь). В языке С ложь — 0, истина — любое значение \ne0. В таблице 2.6 приведены результаты логических операций.

Таблица 2.6. Логические операции
A B !A A&&B A||B
0 0 1 0 0
0 1 1 0 1
1 0 0 0 1
1 1 0 1 1

2.6.4 Операции отношения

Операции отношения возвращают в качестве результата логическое значение. Таких операций шесть: >, >=, <, <=, ==, !=. Результат операции отношения — логическое значение true (истина) или false (ложь).

2.6.5 Условная операция

Для организации разветвлений в простейшем случае можно использовать условную операцию ? :. Эта операция имеет три операнда и в общем виде может быть представлена так:

условие ? выражение1 : выражение2;
			

Работает операция следующим образом. Если условие истинно (не равно 0), то результатом будет выражение1, в противном случае выражение2. Например, операция y=x<0 ? -x : x; записывает в переменную y модуль числа х.

2.6.6 Операция преобразования типа

Для приведения выражения к другому типу данных в С++ существует операция преобразования типа:

(тип) выражение;
			

Например, в результате действий x=5; y=x/2; z=(float) x/2; переменная y примет значение равное 2 (результат целочисленного деления), а переменная z = 2.5.

2.6.7 Операция определения размера

Вычислить размер объекта или типа в байтах можно с помощью операции определения размера, которая имеет две формы записи:

sizeof (тип); или sizeof выражение;
			

Например, предположим, что была описана целочисленная переменная int k=3;.Исходя из того, что тип int занимает в памяти 4 байта, в переменную m=sizeof k; будет записано число 4.

В результате работы команд double z=123.456; p=sizeof (k+z); значение переменной p стало равно 8, т. к. вещественный тип double более длинный (8 байтов) по сравнению с типом int(4 байта) и значение результата было преобразовано к более длинному типу. В записи операции sizeof (k+z) были использованы скобки. Это связано с тем, что операция определения типа имеет более высокий приоритет, чем операция сложения. При заданном значении z=123.456; та же команда, но без скобок p=sizeof k+z; вычислит p=4+123.456=127.456.

Команда s = sizeof "Hello"; определит, что под заданную строку в памяти было выделено s=6 байтов, т. к. объект состоит из 5 символов и один байт на символ окончания строки.

2.6.8 Операции с указателями

При работе с указателями часто используют операции получения адреса & и разадресации * (табл. 2.7).

Таблица 2.7. Операции получения адреса & и разадресации *
Описание Адрес Значение, хранящееся по адресу
тип *p p *p
тип p &p p

Операция получения адреса & возвращает адрес своего операнда. Например:

float a; //Объявлена вещественная переменная а
float *adr_a; //Объявлен указатель на тип float
adr_a=&a; //Оператор записывает в переменную adr_a адрес переменной a
			

Операция разадресации * возвращает значение переменной, хранящееся по заданному адресу, т.е. выполняет действие, обратное операции &:

float a;	//Объявлена вещественная переменная а.
float *adr_a;	//Объявлен указатель на тип float.
a=*adr_a;	//Оператор записывает в переменную a вещественное значение,
//хранящееся по адресу adr_a.
			

К указателям применяют операцию присваивания. Это значит, что значение одного указателя можно присвоить другому. Если указатели одного типа, то для этого применяют обычную операцию присваивания:

//Описана вещественная переменная и два указателя.
float PI =3.14159,*p1, *p2;
//В указатели p1 и p2 записывается адрес переменной PI.
p1=p2=&PI;
			

Если указатели ссылаются на различные типы, то при присваивании значения одного указателя другому, необходимо использовать преобразование типов. Без преобразования можно присваивать любому указателю указатель void*.

Рассмотрим пример работы с указателями различных типов:

float PI = 3.14159;	//Объявлена вещественная переменная.
float *p1;	//Объявлен указатель на float.
double *p2;	//Объявлен указатель на double.
p1=&PI;	//Переменной p1 присваивается значение адреса PI.
p2=(double *) p1;	//Указателю на double присваивается значение,
//которое ссылается на тип float.
			

В указателях p1 и p2 хранится один и тот же адрес (p1=0012FF7C), но значения, на которые они ссылаются, разные (*p1=3.14159, *p2=2.642140e-308). Это связано с тем, указатель типа *float адресует 4 байта, а указатель *double — 8 байт. После присваивания p2=(double *)p1; при обращении к *p2 происходит следующее: к переменной, хранящейся по адресу p1, дописывается ещё следующих 4 байта из памяти. В результате значение *p2 не совпадает со значением *p1.

Таким образом, при преобразовании указателей разного типа приведение типов разрешает только синтаксическую проблему присваивания. Следует помнить, что операция * над указателями различного типа, ссылающимися на один и тот же адрес, возвращает различные значения.

Над адресами С++ определены следующие арифметические операции:

  • сложение и вычитание указателей с константой;
  • вычитание одного указателя из другого;
  • инкремент;
  • декремент.

Сложение и вычитание указателей с константой n означает, что указатель перемещается по ячейкам памяти на столько байт, сколько занимает n переменных того типа, на который он указывает. Например, пусть указатель имеет символьный тип, и его значение равно 100. Результат сложения этого указателя с единицей — 101, так как для хранения переменной типа char требуется один байт. Если же значение указателя равно 100, но он имеет целочисленный тип, то результат его сложения с единицей будет составлять 104, так как для переменной типа int отводится четыре байта.

Эти операции применимы только к указателям одного типа и имеют смысл в основном при работе со структурными типами данных, например массивами.

Фактически получается, что значение указателя изменяется на величину sizeof(тип).Если указатель на определённый тип увеличивается или уменьшается на константу, то его значение изменяется на величину этой константы, умноженную на размер объекта данного типа. Например:

//Объявление массива из 10 элементов.
double mas [10] = { 1.29,3.23,7.98,5.54,8.32,2.48,7.1 };
double *p1;	//Объявление указателя на double
p1=&mas [ 0 ];	//Присвоение указателю адреса нулевого элемента массива.
p1=p1+3;	//Увеличение значения адреса на 3*8=24 (размер типа double), в результате указатель
//сместится на три ячейки, размером double каждая.
			

Вычитание двух указателей определяет, сколько переменных данного типа размещается между указанными ячейками. Разность двух указателей это разность их значений, делённая на размер типа в байтах. Так разность указателей на третий и нулевой элементы массива равна трём, а на третий и девятый — шести. Суммирование двух указателей не допускается.

Операции инкремента и декремента, соответственно, увеличивают или уменьшают значение адреса:

double *p1;
float *p2;
int *i;
p1++; //Увеличение значения адреса на 8.
p2++; //Увеличение значения адреса на 4.
i ++; //Увеличение значения адреса на 4.
			

К указателям так же применимы операции отношения ==, !=, <, >, <=, >=. Иными словами, указатели можно сравнивать. Например, если i указывает на пятый элемент массива, а j на первый, то отношение i > j истинно. Кроме того, любой указатель всегда можно сравнить на равенство с константой нулевого указателя (NULL)6Константу нулевого указателя можно присвоить любому указателю и такой указатель при сравнении не будет равен любому реальному указателю.. Однако все эти утверждения верны, если речь идёт об указателях, ссылающихся на один массив. В противном случае результат арифметических операций и операций отношения будет не определён.

< Лекция 1 || Лекция 2: 123456 || Лекция 3 >
Сергей Радыгин
Сергей Радыгин

Символы кириллицы выводит некорректно. Как сделать чтобы выводился читабельный текст на русском языке?

Тип приложения - не Qt,

Qt Creator 4.5.0 основан на Qt 5.10.0. Win7.

 

Юрий Герко
Юрий Герко

Кому удалось собрать пример из раздела 13.2 Компоновка (Layouts)? Если создавать проект по изложенному алгоритму, автоматически не создается  файл mainwindow.cpp. Если создавать этот файл вручную и добавлять в проект, сборка не получается - компилятор сообщает об отсутствии класса MainWindow. Как правильно выполнить пример?