Указатели в C++. Введение

от автора

Хотелось бы с самого начала прояснить одну вещь — я не отношу себя к категории true-кодеров, сам учусь по специальности, не связанной с разработкой ПО, и это мой первый пост. Прошу судить по всей строгости. Итак, в свое время то ли по причине того, что я спал на лекциях, то ли я не особо вникал в эту тему, но у меня возникали некоторые сложности при работе с указателями в плюсах. Теперь же ни одна моя даже самая крохотная быдлокодерская программа не обходится без указателей. В данной статье я попытаюсь рассказать базовые вещи: что такое указатели, как с ними работать и где их можно применять. Повторюсь, изложенный ниже материал предназначен для новичков.

1. Общие сведения

Итак, что же такое указатель? Указатель — это та же переменная, только инициализируется она не значением одного из множества типов данных в C++, а адресом, адресом некоторой переменной, которая была объявлена в коде ранее. Разберем на примере:

void main(){     int i_val = 7; } 

# Здесь ниже, конечно, я ребятки вам соврал. Переменная i_val — статическая, она явно будет размещена в стеке. В куче место выделяется под динамические объекты. Это важные вещи! Но в данном контексте, я, сделав сам себе замечание, позволю оставить себе все как есть, так что сильно не ругайтесь.

Мы объявили переменную типа int и здесь же ее проинициализировали. Что же произойдет при компиляции программы? В оперативной памяти, в куче, будет выделено свободное место такого размера, что там можно будет беспрепятственно разместить значение нашей переменной i_val. Переменная займет некоторый участок памяти, разместившись в нескольких ячейках в зависимости от своего типа; учитывая, что каждая такая ячейка имеет адрес, мы можем узнать диапазон адресов, в пределах которого разместилось значение переменной. В данном случае, при работе с указателями нам нужен лишь один адрес — адрес первой ячейки, именно он и послужит значением, которым мы проинициализируем указатель. Итак:

void main(){     // 1     int  i_val = 7;     int* i_ptr = &i_val;     // 2     void* v_ptr = (int *)&i_val } 

Используя унарную операцию взятия адреса &, мы извлекаем адрес переменной i_val и присваиваем ее указателю. Здесь стоит обратить внимание на следующие вещи:

  1. Тип, используемый при объявлении указателя в точности должен соответствовать типу переменной, адрес которой мы присваиваем указателю.
  2. В качестве типа, который используется при объявлении указателя, можно выбрать тип void. Но в этом случае при инициализации указателя придется приводить его к типу переменной, на которую он указывает.
  3. Не следует путать оператор взятия адреса со ссылкой на некоторое значение, которое так же визуально отображается символом &.

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

#include <iostream>  using namespace std;  void main(){     int  i_val = 7;     int* i_ptr = &i_val;          // выведем на экран значение переменной i_val     cout <<  i_val << endl; // C1     cout << *i_ptr << endl; // C2 } 

  1. Здесь все ясно — используем саму переменную.
  2. Во втором случае — мы обращаемся к значению переменной i_val через указатель. Но, как вы заметили, мы не просто используем имя указателя — здесь используется операция разыменования: она позволяет перейти от адреса к значению.

В предыдущем примере был организован только вывод значения переменной на экран. Можем ли мы непосредственно через указатель оперировать с значением переменной, на которую он указывает? Да, конечно, для этого они и реализованы (однако, не только для этого — но об этом чуть позже). Все, что нужно — сделать разыменование указателя:

    *i_ptr++; // результат эквивалентен операции инкремента самой переменной: i_val++               // т.е. в данном случае в i_val сейчас хранится значение не 7, а 8. 

2. Массивы

Сразу перейдем к примеру — рассмотрим статичный одномерный массив определенной длинны и инициализируем его элементы:

void main(){ 	const int size = 7; 	// объявление  	int i_array[size]; 	// инициализация элементов массива 	for (int i = 0; i != size; i++){ 		i_array[i] = i; 	} } 

А теперь будем обращаться к элементам массива, используя указатели:

int* arr_ptr = i_array; 	for (int i = 0; i != size; i++){ 		cout << *(arr_ptr + i) << endl; 	} 

Что здесь происходит: мы инициализируем указатель arr_ptr адресом начала массива i_array. Затем, в цикле мы выводим элементы, обращаясь к каждому с помощью начального адреса и смещения. То есть:

*(arr_ptr + 0) 

это тот же самый нулевой элемент, смещение нулевое (i = 0),

*(arr_ptr + 1) 

— первый (i = 1), и так далее.

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

int* arr_ptr_null = &i_array[0]; 	for (int i = 0; i != size; i++){ 		cout << *(arr_ptr_null + i) << endl; 	} 

Пройдем по элементам с конца массива:

int* arr_ptr_end = &i_array[size - 1]; 	for (int i = 0; i != size; i++){ 		cout << *(arr_ptr_end - i) << endl; 	} 

Замечания:

  1. Запись array[i] эквивалентна записи *(array + i). Никто не запрещает использовать их комбинированно: (array + i)[1] — в этом случае смещение идет на i, и еще на единичку. Однако, в данном случае перед выражением (array + i) ставить * не нужно. Наличие скобок это «компенсирует.
  2. Следите за вашими „перемещениями“ по элементам массива — особенно если вам захочется использовать порнографический такой метод записи, как (array + i)[j].

3. Динамическое выделение памяти

Вот та замечательная плюшка, из-за которой я использую указатели. Начнем с динамических массивов. Зачастую при решении какой-либо задачи возникает потребность в использовании массива неопределенного размера, то есть размер этот заранее неизвестен. Здесь нам на помощь приходят динамические массивы — память под них выделяется в процессе выполнения программы. Пример:

int size = -1; // здесь происходят какие - то  // действия, которые изменяют // значение переменной size int* dyn_arr = new int[size]; 

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

Что такое указатель на указатель? Это та же переменная, которая хранит адрес другого указателя „более низкого порядка“. Зачем он нужен? Для инициализации двумерного динамического массива, например:

const int size = 7; // двумерный массив размером 7x7 int** i_arr = new int*[size]; for(int i = 0; i != size; i++){     i_arr[i] = new int[size]; } 

А тройной указатель? Трехмерный динамический массив. Неинтересно, скажите вы, так можно продолжать до бесконечности. Ну хорошо. Тогда давайте представим себе ситуацию, когда нам нужно разместить динамические объекты какого-нибудь класса MyClass в двумерном динамическом массиве. Как это выглядит (пример иллюстрирует исключительно использование указателей, приведенный в примере класс никакой смысловой нагрузки не несет):

class MyClass{     public:         int a;     public:         MyClass(int v){ this->a = v; };        ~MyClass(){}; };  void main(){     MyClass*** v = new MyClass**[7]; 	for (int i = 0; i != 7; i++){ 		v[i] = new MyClass*[3]; 		for (int j = 0; j != 3; j++){ 			v[i][j] = new MyClass(i*j); 		} 	} } 

Здесь два указателя нужны для формирования матрицы, в которой будут располагаться объекты, третий — собственно для размещения там динамических объектов (не MyClass a, а MyClass* a). Это не единственный пример использования указателей такого рода, чуть ниже будут рассмотрены еще примеры.

4. Указатель как аргумент функции

Для начала создадим два динамических массива размером 4×4 и проинициализируем их элементы некоторыми значениями:

void f1(int**, int); void main(){ 	const int size = 4;         // объявление и выделение памяти          // под другие указатели 	int** a = new int*[size]; 	int** b = new int*[size];         // выделение памяти под числовые значения 	for (int i = 0; i != size; i++){ 		a[i] = new int[size];  		b[i] = new int[size];                 // собственно инициализация 		for (int j = 0; j != size; j++){ 			a[i][j] = i * j + 1; 			b[i][j] = i * j - 1; 		} 	} } void f1(int** a, int c){ 	for (int i = 0; i != c; i++){ 		for (int j = 0; j != c; j++){ 			cout.width(3); 			cout << a[i][j]; 		} 		cout << endl; 	} 	cout << endl; } 

Функция f1 выводит значения массивов на экран: первый ее аргумент указатель на двумерный массив, второй — его размерность (указывается одно значение, потому как мы условились для простоты работать с массивами, где количество строк совпадает с количеством столбцов).

Задача: заменить значения элементов массива a соответствующими элементами из массива b, учитывая, что это должно произойти в некоторой функции, которая так или иначе занимается обработкой массивов. Цель: разобраться в способе передачи указателей для их дальнейшей модификации.

  1. Вариант первый. Передаем собственно указатели a и b в качестве параметров функции:
    void f2(int** a, int** b, int c){ 	for (int i = 0; i != c; i++){ 		for (int j = 0; j != c; j++){ 			a[i][j] = b[i][j]; 		} 	} } 

    После вызова данной функции в теле mainf2(a, b, 4) содержимое массивов a и b станет одинаковым.

  2. Вариант второй. Заменить значение указателя: просто присвоить значение указателя b указателю a.
    void main(){ 	const int size = 4;         // объявление и выделение памяти          // под другие указатели 	int** a = new int*[size]; 	int** b = new int*[size];         // выделение памяти под числовые значения 	for (int i = 0; i != size; i++){ 		a[i] = new int[size];  		b[i] = new int[size];                 // собственно инициализация 		for (int j = 0; j != size; j++){ 			a[i][j] = i * j + 1; 			b[i][j] = i * j - 1; 		} 	}         // Здесь это сработает         a = b; } 

    Однако, нам интересен случай, когда массивы обрабатываются в некоторой функции. Что первое приходит на ум? Передать указатели в качестве параметров нашей функции и там сделать то же самое: присвоить указателю a значение указателя b. То есть реализовать следующую функцию:

    void f3(int** a, int** b){ 	a = b; } 

    Сработает ли она? Если мы внутри функции f3 вызовем функцию f1(a, 4), то увидим, что значения массива действительно поменялись. НО: если мы посмотрим содержимое массива a в main — то обнаружим обратное — ничего не изменилось. Так в чем же причина? Все предельно просто: в функции f3 мы работали не с самим указателем a, а с его локальной копией! Все изменения, которые произошли в функции f3 — затронули только локальную копию указателя, но никак не сам указатель a. Давайте посмотрим на следующий пример:

    void false_eqv(int, int); void main(){     int a = 3, b = 5;     false_eqv(a, b);     // Поменялось значение a?      // Конечно же, нет } false_eqv(int a, int b){     a = b; } 

    Итак, я думаю, вы поняли, к чему я веду. Переменной a нельзя присвоить таким образом значение переменной b — ведь мы передавали их значения напрямую, а не по ссылке. То же самое и с указателями — используя их в качестве аргументов таким образом, мы заведомо лишаем их возможности изменения значения.
    Вариант третий, или работа над ошибками по второму варианту:

    void f4(int***, int**); void main(){ 	const int size = 4; 	int** a = new int*[4]; 	int** b = new int*[4]; 	for (int i = 0; i != 4; i++){ 		a[i] = new int[4];  		b[i] = new int[4]; 		for (int j = 0; j != 4; j++){ 			a[i][j] = i * j + 1; 			b[i][j] = i * j - 1; 		} 	} 	int*** d = &a;         f4(d, b); } void f4(int*** a, int** b){ 	*a = b; } 

    Таким образом, в main’е мы создаем указатель d на указатель a, и именно его передаем в качестве аргумента в функцию замены. Теперь, разыменовав d внутри f4 и приравняв ему значение указателя b, мы заменили значение настоящего указателя a, а не его локальной копии, на значение указателя b.

    Кстати, а чего это мы создаем динамические объекты? Ну ладно размер массива не знали, а экземпляры классов мы зачем динамическими делали? Да потому что зачастую, созданный нами объекты свое — они генерились, порождали новые данные/объекты для дальнейшей работы, а теперь пришло им время…умереть [фу, как грубо] уйти со сцены. И как мы это сделаем? Просто:

    delete(a); delete(b); // Вот и кончились наши двумерные массивы delete(v); // Вот и нет больше двумерного массива с динамическими объектами delete(dyn_array); // Вот и удалился одномерный массив 

  3. На данной ноте я хотел бы закончить свое повествование. Если найдется хотя бы пара ребят, которым понравится стиль изложения материала, то я постараюсь продолжить… ой, да кого я обманываю, мне нужен инвайт и все на этом, дайте инвайт и вашим глазам больше не придется видеть это околесицу. Шучу, конечно. Ругайте, комментируйте.

ссылка на оригинал статьи http://habrahabr.ru/post/256443/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *