§4 Поток ввода. Переменные. Константы. Указатели. Ссылки

Поток ввода. Потоковый объект cin

Для ввода данных используется поток ввода. Поток ввода начинается с определения потокового объекта ввода – cin (читается “си-ин”). По аналогии с потоком вывода, поток ввода использует операцию извлечения >> (или ввода) для чтения данных в поток со стандартного устройства (клавиатуры) или из файла (для файловых потоков определяются свои потоковые объекты ввода). Переменные, входящие в поток должны быть определены в программе раньше. Например:

int var_A;
cin >> var_A;

1
Можно вводить значения нескольких переменных за раз, но тогда, при вводе, в окне терминала, значения переменных должны разделяться пробелами (или иными пробельными символами). Пример ввода нескольких переменных:

int a = 10, b, c;
cin >> a >> b >> c;

Здесь вводятся переменные одного типа, но в потоке cin можно вводить значения переменных разного типа. В этом случае нужно следить за тем, чтобы вводимые значения переменных соответствовали требованиям предъявляемым к литералам данных типов. Если переменная имела уже какое-то значение (a), то оно будет заменено на новое. Данные в поток отправляются нажатием клавиши Enter на клавиатуре.

Примечание. Для потоков ввода также могут быть использованы манипуляторы, например, setw() для ограничения количества вводимых символов.

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

Программа cpp-4.1
#include <iostream>
#include <string>
using namespace std;

int main() {
	int Y;
	string S;
	cout << "Как тебя зовут?\n";
	cin >> S;
	cout << "Сколько тебе лет?\n";
	cin >> Y;
	cout << "Здравствуй, " << S
	     << "! Тебе всего " << Y
	     << " лет." << endl;
	return 0;
}

Обратите внимание, что поток вывода, например, в стр. 10 не завершается endl. Дело в том, что последующий поток ввода автоматически завершает предыдущий поток с установкой признака завершения строки.

Примечание. В этой программе подразумевается, что имя не будет содержать пробелов. В противном случае, строка должна вводится с помощью функции класса stringgetline(cin, S), т. к. cin не игнорирует пробелы в строке! Если функция getline() следует за потоком ввода, то необходимо предусмотреть также очистку потока ввода от нулевого символа с помощью методов потокового класса, например, cin.get().

Переменные

Адресуемость памяти

Программы вместе с данными, к которым они имеют доступ, находятся в оперативной памяти. Оперативная память состоит из двоичных запоминающих элементов – битов. Соседние биты объединены в группы (ячейки), которые называются байтами. Хотя название ячеек и совпадает с единицами измерения информации – байт (т. е. 8 бит), реально имеют размер от 8 до 64 бит, в зависимости от архитектуры. Все байты пронумерованы, начиная с нуля, т. е. имеют адреса. Номер ячейки памяти называется адресом.
От разработчика скрыта информация какие адреса памяти будут использоваться для хранения данных во время выполнения программы (с каждым запуском программы адреса будут различаться). Известно, что эти адреса будут располагаться близко друг к другу – ОС резервирует для выполнения программы определенное адресное пространство.

Переменные

Компьютерная память устроена так, что данные разного типа хранятся в ней различным образом. Существует множество различных типов данных: целые числа, действительные числа, символы, строки. Каждый из этих типов занимает в памяти различное количество байтов. Отсюда следует, что для каждого типа определен свой диапазон возможных принимаемых значений данных этого типа и возможный набор операций с этими данными. Например, для типа целое число (тип int, занимает в памяти 4 байта) диапазон принимаемых значений от -2147483648 до 2147483647. С данными этого типа могут выполняться такие операции: +, -, *, /, %, <<, >>. Для действительного типа (тип double, занимает в памяти 8 байтов) диапазон значительно "шире" (до максимального положительного значения 1.797693134862316e+308), но количество операций (из приведенных выше) будет значительно меньше, только: +, -, * и /.
18-1Для записи данных в память и извлечения их из памяти используются переменные. Переменные — это именованная область памяти, к которой можно обращаться из программы по имени. С каждой переменной связан адрес памяти (адрес первого байта с наименьшим значением). Для программиста - это не имеет значения, так как от него скрыта информация в каком адресном пространстве будет работать программа. Для программиста важно - какой тип данных будет связан с именем переменной.

Определение переменных

Перед использованием переменных их нужно определить (definition). Определение состоит из спецификатора типа (для целой переменной это, например, int) и следующего за ним идентификатора. Определение производится в любом месте программы, но до использования самой переменной. Определение переменной следует производить в месте её использования, дабы избежать случайного появления в программе неиспользуемых переменных (современные компиляторы способны предупреждать их появление на этапе сборки, но не на этапе работы самой программы).
При определении переменной с ее именем будет связан адрес байта с наименьшим значением адреса среди тех байтов, которые будут заняты значением этой переменной. Для хранения значения переменной отводится такое количество байтов, которое требуется для хранения соответствующего типа данных. Напомним, что для хранения в памяти значения символьной переменной (char) требуется один байт, а переменная типа int займет 4 байта (т. е. четыре ячейки, каждая из которых будет иметь свой собственный адрес).
Допустим, мы будем использовать переменную целого типа с именем a. Тогда определение будет иметь следующий вид:

int a;

Тип int является стандартным типом для целых переменных, так как имеет размер равный машинному слову (4 байта, вне зависимости от архитектуры). Если для целых данных нет веских причин для использования какого-либо иного целого типа, то необходимо использовать именно этот тип.
Если используется несколько переменных одного типа, то они могут быть перечислены через запятую в одной инструкции:

int a, b, c, other;

Определение переменной может сопровождаться инициализацией (т. е. указанием начального значения):

int a = 10;
int b = a;
int c = 0, d, e, other = 1000;

Если переменная определяется (но не инициализируется) в теле главной функции main(), то она не будет иметь начального значения, её значением будет мусор (т. е. неопределенное состояние памяти).
Следует различать определение и объявление (declaration). Объявления вводят имена в программу (или повторно вводят). По существу, определения - это тоже объявления. Если для работы с объектом достаточно его определения в данном месте программы, то ограничиваются определением. Если переменная определяется вне тела главной функции, то говорят, что она объявлена. При объявлении переменная будет иметь значение по умолчанию (например, для целочисленных типов - это значение "0").

Программа cpp-4.2
#include <iostream>
using namespace std;
int A;

int main() {
	int a;
	cout << A << endl;
	cout << a << endl; // Ошибка!
	return 0;
}

Компилятор выдаст предупреждение:

../src/test.cpp: В функции «int main()»:
../src/test.cpp:16:10: предупреждение: «a» используется без инициализации в данной функции [-Wuninitialized]
  cout << a << endl; // Ошибка!
          ^

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

Другие способы инициализации переменных

Выше мы показали как можно инициализировать переменную с помощью операции =. Но это не единственная возможность. В C++11 появилась списочная инициализация с помощью фигурных скобок. Этот способ имеет преимущества, так как контролирует потерю точности при инициализации. К тому же, он позволяет инициализировать переменную значением по умолчанию (первая строка в примере ниже), а это, в свою очередь, может предупредить использование неинициализированного объекта.

int a {};
int b = {10};
int c(0);
Если вместо типа стоит auto, то список инициализации без использования операции = создавать нельзя:

auto x1 = {3};   // std::initializer_list
auto x2 {1, 2};  // ошибка
auto x3 {3};     // переменная типа int

Инициализатором переменной может быть выражение любой сложности. Например, вот такая инструкция с арифметическим выражением:

int b = 10, c = 5;
int a = (2 * b - 4) / c - 1;

Константы

Если перед именем переменной используется ключевое слово const - то это означает, что описана константа. const называется квалификатором (qualifier), поскольку он уточняет, что означает это объявление. Константа инициализируется значением в момент описания. Попытка присвоить ей новое значение приведет к ошибке. Существует практика давать имена константам с заглавной буквы. Квалификатор const принято использовать до спецификатора типа:

[квалификатор const][спецификатор типа][Name_const] = [значение];

Например:

const int Buff = 255;
const double Alpha = 0.0072973506;

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

Указатели

В С++ существует еще один необычный вид переменных - это указатель (pointer). Указатель - это специальный вид переменной, который хранит адрес памяти (см. рисунок выше) другого объекта, точнее байта с наименьшим значением из тех байтов, что выделены для хранения этого объекта. Поскольку указатель - это переменная, то, в этой связи, она тоже имеет свой адрес, который тоже можно получить. Указатели могут иметь разные типы (и даже абстрактные), а также вовсе не иметь типа (void). С указателями можно производить различные операции, но перечень таких операций невелик. Поскольку задачи решаются на достаточно низком уровне - это требует от разработчика большей внимательности, поскольку использование указателей небезопасно и может привести к повреждению данных.

Объявление и инициализация указателей

Указатель может быть константой или переменной, а также указывать на константу или переменную.
Чтобы описать переменную-указатель используется операция * - разыменования (или косвенной адресации):

int *a;        //указатель на целую переменную
const int *b;  //указатель на целую константу
Операция взятия адреса (&)

Также как и обычные переменные, указатели можно инициализировать, в процессе описания, адресом переменной, которая должна существовать к этому моменту:

int a = 10;
int *p = &a;

Операция & называется операцией взятия адреса. После описания указателям присваивается значение (адреса) подобно присваиванию значений другим объектам:

int a, *p, *c;
p = &a;
c = p;

Адреса в программе представлены в виде шестнадцатеричных чисел с префиксом '0x' в начале, поскольку такое представление (по сравнению с двоичным) выглядит более компактным. Например, 0xbfd62cac в двоичном представлении будет выглядеть как:
10111111110101100010110010101100
Всего - 32 бита, что соответствует типу int, который занимает в памяти 4 байта.

Программа cpp-4.3
#include <iostream>
using namespace std;

int main() {
    char a[]("#");	
	cout <<  a << '\t' << &a << endl;
	short b(1000);	
	cout <<  b << '\t' << &b << endl;
	int c(1000000);	
	cout <<  c << '\t' << &c << endl;
	double d(0.02);	
	cout <<  d << '\t' << &d << endl;
    return 0;
}

Вывод

#	0xbf8e24de
1000	0xbf8e24dc
1000000	0xbf8e24d8
0.02	0xbf8e24d0
Указатели и переменные

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

Программа cpp-4.4
#include <iostream>
using namespace std;

int main() {
	int d = 10, k = 20;
	int *p; // объявление целого указателя
	// объявление указателя c инициализацией
	int *t = &d;
	p = &d;	// присваивание указателю адреса переменной d

	cout << " d => " << d << endl
		 << " p => " << p << endl;

	*p = 5; // изменяется значение переменной d
	cout << " d => " << d << endl;

	p = &k; // теперь указатель указывает на переменную k
	cout << " p => " << p << endl;

	d = *p; // d присваивается значение переменной k
	cout << "*p => " << d << endl;

	p = t; // Присваивание значения одного указателя другому
	cout << " p => " << p << endl
			// Указатель тоже имеет адрес:
		 << "&p => " << &p << endl;
	return 0;
}

Вывод

 d => 10
 p => 0xbfe59b58
 d => 5
 p => 0xbfe59b54
*p => 20
 p => 0xbfe59b58
&p => 0xbfe59b50
18_2
Указатель p хранит адрес переменной d (или указывает на переменную d), но, как переменная, имеет свой адрес


На схеме ниже показано как получить значение и адрес переменной.

links_2

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

Помимо операций рассмотренных в программе cpp-4.4, c указателями можно выполнять также следующие операции:

  • арифметические операции (сложение с константой, вычитание, инкремент ++, декремент --)
  • Программа cpp-4.5
    #include <iostream>
    using namespace std;
    
    int main() {
    	int a = 5, b = 10, c = 15, d = 20;
    	int *p;
    	cout << &a << "\n"
    		 << &b << "\n"
    		 << &c << "\n"
    		 << &d << "\n" << endl;
    	p = &d;
    	// получаем значения переменных
    	// разыменованием указателя
    	// с последующей его инкрементацией
    	cout << *p++ << "\n"
    		 << *p++ << "\n"
    		 << *p++ << "\n"
    		 << *p++ << endl;
    	// eще одно обращение к данным приведет
    	// к неопределенным результатам!
    	cout << *p << endl;
    	return 0;
    }
    
    0xbfd64cf8
    0xbfd64cf4
    0xbfd64cf0
    0xbfd64cec
    
    5
    10
    15
    20
    -1076474628
    

    Инкремент (декремент) увеличивает (уменьшает) значение указателя на величину sizeof(тип). Разность двух указателей – это разность их значений, деленная на размер типа в байтах. Иными словами – это расстояние между блоками памяти.
    Суммирование двух указателей не допускается! Можно суммировать указатель и константу, что равносильно прибавлению константы помноженной на sizeof(тип).

  • сравнение
  • приведение типов.
    Например:

    char *a;
    int  *b;
    a = reinterpret_cast<char *>(b);
    
  • Здесь целый указатель приводится к типу char

  • присваивание значения nullptr (нулевой указатель). Например, int *p = nullptr
Динамическое выделение памяти

С помощью указателей можно получать доступ к памяти и использовать динамические переменные, которые создаются на время, для выполнения каких-либо задач, с последующим их уничтожением. Для выделения памяти используется операция new, а для уничтожения delete. Например:

int *p = new int(10);
int *c = new int(40);
*p = *c / 2;
cout << "*c = " << *c << endl;
cout << "*p = " << *p << endl;
delete p;
delete c;

Указатели, описанные выше, являются незащищенными. Это означает, что непосредственное их использование в программе чревато негативными последствиями, так как непродуманных алгоритм может привести к повреждению данных или утечкам памяти. В std языка С++ имеются безопасные типы указателей (iterator, smart pointer).

Ссылки

Ссылки (reference), как и указатели, являются реализацией ссылочного типа. Для описания ссылки используется операция &. Ссылка должна быть проинициализирована в момент описания именем переменной, на которую она будет ссылаться.

int a = 10;
int &b = a;

При описании ссылки происходит не инициализация копированием (как в случае с переменной a), а связывание её с объектом (т. е. с этой переменной). Таким образом, ссылка является псевдонимом объекта. В этом ключе некорректно именовать ссылку - ссылочной переменной.

    Правила использования ссылок в программе:

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

Связь указателей и ссылок

Ссылка — это, по существу, указатель, который жестко привязан к области памяти, на которую он указывает, и который автоматически разыменовывается, когда происходит обращение по имени ссылки. Ссылка, в отличие от указателей, не занимает места в памяти.
Связь между указателями и ссылками можно установить следующим образом: операция взятия адреса (&) получает указатель, ссылающийся на тот же самый объект при переходе по ссылке, а ссылка, которая инициализирована при разыменовании (*) указателя, будет указывать на тот же объект, что и указатель:

int b = 10;
int *p = &b;
int &r = *p;
cout << "*p = " << *p << "\n"
     << " r = " << r << endl;

Использование ссылок в программе демонстрирует следующая программа.

Программа cpp-4.6
#include <iostream>
using namespace std;

int main() {
	int a = 10, b = 20;
	cout << "a => " << a << endl;
	cout << "b => " << b << endl;
	int *p = &a;
	int &c = b; // связывание ссылки
	cout << "c => " << c << endl;
	c = *p; // присваивание значения разыменовывания
			// объекта на который ссылается ссылка
	cout << "c => " << c << endl;
	c = 20; // присваивание объекту с которым связана ссылка
	cout << "c => " << c << endl;
	return 0;
}
a => 10
b => 20
c => 20
c => 10
c => 20
Важно отличать операцию взятия адреса от ссылки. Операция взятия адреса используется для уже созданного объекта с целью получения его адреса, а ссылка - это только задание альтернативного имени объекта.

Ссылки, в отличие от указателей, предоставляют более компактное, понятное, безопасное и удобное средство работы с данными. Поэтому, использование ссылок (вместо указателей) является хорошим тоном в программировании на С++.
Причиной введения ссылок в язык С++ (в языке С они отсутствуют) явилась необходимость перегрузки операторов, применяемых к объектам пользовательских типов (или классов).

Примеры решения задач
Вопросы
  1. В чем отличие ссылки от указателя?
  2. Почему ссылку нельзя назвать переменной, а указатель можно?
  3. Почему в программе 10.3 переменные получают адреса именно в таком порядке?
  4. Почему в этой программе переменные выводятся в обратном порядке? Как изменить это?
  5. Переменная a, типа int имеет адрес 0xbfe59b50. К указателю указывающему на эту переменную применили операцию p++. На какой адрес памяти будет указывать указатель?
  6. Возможны ли такие описания:
    int p = *d;
    int *p = d;
    int &p = *d;
    int &p = d
    int *p = &d;
    int &p = *d
    

    p – указатель на переменную типа int, d – имеет тип int

  7. Возможны ли такие описания:
    int r = *d;
    int *r = d;
    int &r = *d;
    int &r = d
    int *r = &d;
    int &r = *d
    

    r – ccылка на переменную типа int, d – имеет тип int

  8. Дан фрагмент программы:
    int a;
    int &b = a;
    a = 10;
    b = 5;
    cout << a << '\n' << b << endl;
    

    Что будет выведено?

Темы сообщений
Задания А
Задания Б
Задания С
Ссылки
1 Звезда2 Звезды3 Звезды4 Звезды5 Звезд (3 оценок, среднее: 5,00 из 5)
Загрузка...

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Print Friendly, PDF & Email

Обсуждение закрыто.