§10 Адресуемость памяти. Указатели. Ссылки

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

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

Представлены данных в программе 10.1
Представление данных в программе 10.1


От разработчика скрыта информация какие адреса памяти будут использоваться для хранения данных во время выполнения программы (с каждым запуском программы адреса будут различаться). Известно, что эти адреса будут располагаться близко друг к другу – ОС резервирует для выполнения программы определенное адресное пространство. (см. Программа 10.1). При объявлении переменной с ее именем будет связан адрес байта с наименьшим значением адреса среди тех байтов, которые будут заняты значением этой переменной. Для хранения значения переменной отводится такое количество байтов, которое требуется для хранения соответствующего типа данных. Напомним, что для хранения в памяти значения символьной переменной (char) требуется один байт, а переменная типа int займет 4 байта (т. е. четыре ячейки, каждая из которых будет иметь свой собственный адрес). (Подробнее: учебник 10 кл., т. 1, гл. 5, п. 32).

Операция “взятия адреса”

Адрес обычной переменной можно получить с помощью операции &взятия адреса” (address of).

int a;
cout << &a;

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

#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

Указатели

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

int *a;        //указатель на целую переменную
const int *b;  //указатель на целую константу

Указатели можно объявлять, чтобы указывать на объекты любого типа.
Инициализировать указатель можно с помощью операции взятия адреса &:

int d = 10;
int *c = &d;

Здесь указатель с указывает на целую переменную d. С помощью указателя можно получить значение переменной на которую он указывает. Для этого применяется операция косвенной адресации (разыменования) так, как показано в программе 10.2, стр. 17 и 23.
Программа 10.2

#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

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

С указателями можно выполнять следующие операции:

  • разыменование (*)
  • Операция разыменования (*) позволяет получить доступ к значению находящемуся по адресу, который сохранен в указателе.

  • присваивание
  • арифметические операции (сложение с константой, вычитание, инкремент ++, декремент --)
  • Программа 10.3

    #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

Ссылки

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

int a = 10;
int &b = a;

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

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

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

При обращении к ссылке происходит обращение к значению того объекта, с которым связана данная ссылка.

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

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

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

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

#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;
    

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

Презентация к уроку

lesson-10

Литература
  1. Лафоре Р. Объектно-ориентированное программирование в C++ (4-е изд.). Питер: 2004
  2. Прата, Стивен. Язык программирования C++. Лекции и упражнения, 6-е изд.: Пер. с англ. — М.: ООО «И.Д. Вильяме», 2012
  3. Липпман Б. Стенли, Жози Лажойе, Барбара Э. Му. Язык программирования С++. Базовый курс. Изд. 5-е. М: ООО "И. Д. Вильямс", 2014
  4. Эллайн А. C++. От ламера до программера. СПб.: Питер, 2015
  5. Шилдт Г. С++: Базовый курс, 3-изд. М.: Вильямс, 2010


Print Friendly, PDF & Email

Добавить комментарий