Хотелось бы с самого начала прояснить одну вещь — я не отношу себя к категории 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. Указатель как аргумент функции


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

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

Комментарии (59)


  1. Suvitruf
    23.04.2015 11:57
    +21

    Шёл 2015 год, а я всё ещё наблюдаю статьи по азам работы с указателями в C++


    1. NeoCode
      23.04.2015 12:19
      +6

      Люди же новые рождаются:) У каждого из нас был момент, когда указатели в С++ были чем-то новым и необычным… а для кого-то этот момент еще не настал.


      1. Nikiti4
        23.04.2015 12:53
        +2

        для меня, например=)
        Автор, спасибо за статью


        1. is_0xBh_139 Автор
          23.04.2015 13:12
          +1

          Вам спасибо ) Это очень приятно быть кому — то полезным )


  1. datacompboy
    23.04.2015 12:09
    +11

    Если пользуетесь оператором new[], то и удалять надо через delete[].

    да и удалять надо все уровни, а то сейчас у вас утекла пачка памяти, на которую ссылались массивы a и b


    1. is_0xBh_139 Автор
      23.04.2015 12:10

      Спасибо, учту.


    1. is_0xBh_139 Автор
      23.04.2015 12:14

      Можно немного подробнее?


      1. fsmorygo
        23.04.2015 12:47
        +3

        Вы создали два двумерных массива 4х4, выделив 2 * sizeof(int) * 4 * 4 памяти.

        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];
                ...
            }
            ...
        

        Но, вызывая
        delete a;
        delete b;
        

        Освободится лишь 2 * sizeof(int*) * 4 памяти, а та память, которая выделялась по a[i] = new int[4] утечет.

        Для того, чтобы корректно освободить все ресурсы, необходимо будет делать так:
        for (int i = 0; i < 4; i++) {
            delete[] a[i];
            delete[] b[i];
        }
        delete[] a;
        delete[] b;
        

        Почему при создании массивов через new[] нужно использовать delete[], будет понятно после изучения наследования и механизма работы виртуальных деструкторов. Если забыть [] после delete, можно снова получить утечку ресурсов. По счастливому стечению обстоятельств, для массивов из примитивов разницы между delete и delete[] нет, но это не значит, что можно этим пренебрегать.

        Также, не стоит забывать про то, что может выскочить исключение std::bad_alloc.

        Вообще, для управления памятью лучше всего использовать подход RAII.


        1. is_0xBh_139 Автор
          23.04.2015 12:50

          Спасибо)


        1. FoxCanFly
          01.05.2015 16:07

          Просто таким образом вообще не стоит создавать массивы никогда



    1. AxisPod
      23.04.2015 14:30
      -8

      Не утекла, а словили непредвиденное поведение.


  1. GarryC
    23.04.2015 12:09
    +1

    Переменная i_val — статическая, она явно будет размещена в стеке

    Вспоминается анекдот про Ходжу Насреддина.
    Однажды султан прочитал в книге фразу о том, что извинение бывает хуже проступка, и пристал к Ходже в требованием объяснить суть.
    Тот долго пытался, но султан оставался недоволен. Решили отложить вопрос на потом.
    Вечером, когда он поднимались по лестнице на минарет, и султан, естественно, шел первым, Ходжа шлепнул его по заду.
    Удивленный султан повернулся и спросил «Что случилось ?».
    Ходжа ответил: «Ваш зад, о мой султан, напомнил мне зад моей любимой жены, и почему бы нам не заняться любовью ?».
    Возмущенный султан воскликнул «Ты что, совсем ох.ел с ума сошел !».
    На что Ходжа ответил: «Вот теперь, мой повелитель, Вы поняли, как извинение может быть хуже проступка ?».

    По моему, здесь тот самый случай.


    1. is_0xBh_139 Автор
      23.04.2015 12:12
      +1

      Спасибо за замечание. Впредь постараюсь не делать подобных упущений)


      1. withoutuniverse
        24.04.2015 11:42

        Суть в том, что указатель

        int* i_ptr = &i_val;
        тоже будет размещен в стеке.


        1. is_0xBh_139 Автор
          26.04.2015 20:33

          потому что мы сразу его проинициализировали? а если бы мы объявили указатель на объект типа int, а инициализировали его позже, тогда бы размещение произошло в куче, я правильно понимаю? т.е. если бы мы, например, выделили память на массив из N элементов c помощью оператора new.


          1. is_0xBh_139 Автор
            26.04.2015 21:01

            причем после инициализации массива — значения элементов хранились бы в куче, а значение указателя — все-таки в стеке?


            1. withoutuniverse
              26.04.2015 23:55

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

              Пример кода и его output для понимания того, о чем я написал
              int main() {
                  int a = 5;
                  int *p = new int[10000];
                  printf("&a = %p\n", &a);
                  printf("&p = %p\n", &p);
                  printf("p = %p\n", p);
                  delete[] p;
                  return 0;
              }
              /// ... output
              &a = 0xbfb7e6f8 // стэк
              &p = 0xbfb7e6fc // стэк, в нем хранится 4 байта на любой участок памяти в куче
               p = 0x8887008 // куча, указатель ссылается на этот адрес


  1. icc
    23.04.2015 13:55
    +3

    Что мешает прочитать книги Кернигана и Ритчи «Язык программирования C» и Страуструпа «Язык программирования C++»? По-моему в них четко и ясно изложена концепция указателей. Также можно почитать C++11 FAQ, в том числе раздел про умные указатели.


  1. brn
    23.04.2015 13:59
    +11

    А с каких пор переменные размещенные на стеке стали называться статическими? Статические переменные, это те что помечены модификатором static и это совсем другое.


  1. Fil
    23.04.2015 14:07
    +1

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

    Не совсем. Ведь указатели незаменимы для реализации наследования и полиморфизма:
    class Base {
    };
    
    class Derived : public Base {
    };
    
    int main() {
      Derived d;
      Base* b = &d;
    }
    


    1. is_0xBh_139 Автор
      23.04.2015 14:18

      Спасибо, учту )


  1. saluev
    23.04.2015 15:06
    +8

    В свете всех найденных ошибок и неточностей у меня возникает вопрос: зачем вообще писать статью по теме, в которой разбираешься так плохо?


    1. is_0xBh_139 Автор
      23.04.2015 15:11
      -1

      За ошибки извиняюсь, впредь буду тщательнее проверять то, что написал. И спасибо тем ребятам, которые прочитали этот пост и сделали вполне корректные, важные замечания. Ну и для меня это — как некоторая форма обучения )))


      1. FoxCanFly
        01.05.2015 16:09

        Для обучения можно писать это своему преподавателю, а не в публичном доступе, а то один напишет, другой прочитает и будет так делать.


  1. MaximChistov
    23.04.2015 15:44
    +2

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

    Вы это серьёзно? Указатели и так не самая простая тема для новичков(вы вот в ней до сих пор не разобрались даже наполовину), а Вы им такой жутью хотите мозги загрузить…


    1. is_0xBh_139 Автор
      23.04.2015 16:01

      Учту, спасибо за замечание. Перепишу. Я пытался подвести к этому, раз не получилось, буду исправляться )


  1. grechnik
    23.04.2015 16:44

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

    Почитайте про приоритеты операций. *i_ptr++ означает i_ptr++.


    1. is_0xBh_139 Автор
      23.04.2015 16:58

      Спасибо, ознакомлюсь. Однако моя VS скомпилировала это так, как я предполагал. Все равно спасибо, учту.


  1. SOLON7
    23.04.2015 17:06

    эх роднинкие. что бы без вас делал!


  1. Flex25
    23.04.2015 19:50

    В продолжение темы указателей C++: всем новичкам, кто хоть немного дружит с английским, настоятельно рекомендую к просмотру серию великолепных коротких обучающих видео: www.youtube.com/playlist?list=PL2_aWCzGMAwLZp6LMUKI3cc7pgGsasm2_


    1. Xitsa
      24.04.2015 09:02

      Если уж человек понимает английский, то можно советовать книгу Richard M. Reese — Understanding and Using C Pointers.
      На мой взгляд она покрывает тему целиком.


  1. stack_trace
    24.04.2015 01:52

    Хм, я всегда воспринимал указатель, как просто тонкую абстракцию над адресом в памяти. По-моему, если их рассматривать так, то вообще никаких сложностей нет.


    1. withoutuniverse
      24.04.2015 11:38

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

      0x00004 - адрес нашего указателя, занимает 4 байта (корректно для 32 битных систем), 0x00008 - его значение (тоже 4 байта, очевидно)
      0x00008 - адрес некой переменной, занимающей 4 байта, 12345 - ее значение.

      Для ссылок ваше утверждение будет верно (хотя даже тут я усомнюсь в его корректности).


      1. withoutuniverse
        24.04.2015 11:45

        Немного поправлю себя

        Корректнее назвать 0x0004 началом адреса, так как 4 байта будут занимать 0x0004, 0x0005, 0x0006, 0x0007. 


        1. stack_trace
          24.04.2015 12:49
          +1

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

          Видимо, у нас просто проблема с терминалогией ). Поясню, слово «абстракция» я здесь употребляю не в смысле нечто неосязаемое, а в смысле объект, обладающий функциональностью отличной от объекта, на основе которого он выполнен. В данном случае, указатель обладает свойствами, отличными от просто адреса в памяти. С точки зрения данных — это именно адрес, но операции, определённые для указателя и адреса немного различаются. Надеюсь, объяснение получилось не слишком путаным.


          1. withoutuniverse
            24.04.2015 14:29

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

            ссылка — тонкая абстракция над адресом в памяти
            указатель — абстракция над адресом в памяти


            1. brn
              24.04.2015 16:31

              С этим я не совсем согласен. Сcылка такая же абстракция с одним исключением. Это указатель, никогда не указывающий на NULL.


              1. withoutuniverse
                24.04.2015 17:11

                Ссылка не занимает места и не имеет адреса в памяти, какой же это указатель?
                Это всего лишь алиас для переменной, на которую она ссылается.
                Другое дело, когда ссылка передается в качестве аргумента функции — там действительно на стеке будет выделена память, соразмерная занимаемой указателем памяти (4 байта для x86).
                Если я не прав, пусть специалисты по C++ меня поправят, предоставив пруфы.

                int a=5; // на стеке выделится место для переменной "a"
                int &b=a; // ничего не произойдет, "b" это алиас для "a"
                


                1. stack_trace
                  24.04.2015 21:17
                  +1

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


                  1. withoutuniverse
                    25.04.2015 15:27

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

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

                    Под споилером несколько ответов из stackoverflow, почему так происходит
                    The standard is pretty clear on sizeof (C++11, 5.3.3/4):

                    When applied to a reference or a reference type, the result is the size of the referenced type.
                    So if you really are taking sizeof(double&), the compiler is telling you that sizeof(double) is 4.

                    Update: So, what you really are doing is applying sizeof to a class type. In that case,

                    When applied to a class, the result is the number of bytes in an object of that class [...]
                    So we know that the presence of the reference inside A causes it to take up 4 bytes. That's because even though the standard does not mandate how references are to be implemented, the compiler still has to implement them somehow. This somehow might be very different depending on the context, but for a reference member of a class type the only approach that makes sense is sneaking in a double* behind your back and calling it a double& in your face.

                    So if your architecture is 32-bit (in which pointers are 4 bytes long) that would explain the result.

                    Just keep in mind that the concept of a reference is not tied to any specific implementation. The standard allows the compiler to implement references however it wants.

                    _______________________

                    A C++ reference is not a pointer. It is an alias of an object. Sometimes, the compiler chooses to implement this by using a pointer. But often, it implements it by doing nothing at all. By simply generate code which refers directly to the original object.

                    In any case, sizeof applied to a reference type does not give you the size of a reference. So it's not really clear what you're doing, making it impossible to explain what is happening.


                    1. stack_trace
                      25.04.2015 16:50

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


                      1. withoutuniverse
                        27.04.2015 00:15

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

                        На всякий случай, я говорю про этот код от вас:
                        struct Foo
                        {
                        	Foo(int& some) : a(some), b(some) { }
                        	int &a;
                        	int &b;
                        };
                        
                        int main() {
                        	std::cout << "Expecting 1 if reference has no size and 8 if it has" << std::endl << sizeof(Foo) << std::endl;
                        	return 0;
                        }
                        


                        1. withoutuniverse
                          27.04.2015 00:39

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

                          long long int a = 5;
                          long long int &ref = a;
                          
                          printf("%d = a\n",  sizeof(a));
                          printf("%d = ref\n",  sizeof(ref));
                          printf("%d = &ref\n",  sizeof(&ref));
                          
                          //... output
                          8 = a
                          8 = ref
                          4 = &ref


                        1. stack_trace
                          27.04.2015 01:49

                          Вы не правы. Размер ссылки и размер объекта, содержащего ссылку это не одно и то же. Доказательство.

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

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

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

                          Стандарт не говорит о том, как она реализована? Прекрасно, я с этим не спорил, более того, я первый указал на это (см. цитату выше), но это не отменяет того, что в большинстве случаев адрес — единственный возможный способ её реализации.

                          Я лично считаю некорректным высказывание «Ссылка это или ничего, или черный ящик.». Потому что если ссылка чёрный ящик всегда, если мы говорим о стандарте, нету никакого «либо ничего», потому как стандарт этого тоже не требует.


                          1. withoutuniverse
                            27.04.2015 11:40

                            Если мы создадим ссылку в теле метода, то эта ссылка будет лишь алиасом. К сожалению, на практике, как я написал ответом чуть выше — это не так, потому я был очень удивлен.
                            Я понимаю, что вполне себе логично не хранить ссылку вообще в ассемблерном коде, а просто ссылаться на адрес переменной по этой ссылке (в теле метода, не в объекте — по поводу объекта и при передаче как параметра я с вами согласен).

                            A C++ reference is not a pointer. It is an alias of an object. Sometimes, the compiler chooses to implement this by using a pointer. But often, it implements it by doing nothing at all. By simply generate code which refers directly to the original object.


                            1. withoutuniverse
                              27.04.2015 13:19

                              Все встало на свои места — компилятор не будет резервировать место под ссылку, если она в теле метода создается и больше не используется нигде. TEST
                              Если же ссылка будет полем объекта или пробрасываться в параметр методу — 4(8) байта.


                              1. stack_trace
                                27.04.2015 21:30

                                Так и есть ) Наконец-то мы с вами поняли друг друга )


              1. withoutuniverse
                25.04.2015 15:33

                Утверждение неверно, это возможно сделать. Хотя, разумеется, этого никто делать в здравии не будет. Даже спецификация кажется гласит о том, что словим UB, но сослаться на null вполне себе возможно

                Вырезка из спецификации
                8.3.2/1:
                A reference shall be initialized to refer to a valid object or function. [Note: in particular, a null reference cannot exist in a well-defined program, because the only way to create such a reference would be to bind it to the “object” obtained by dereferencing a null pointer, which causes undefined behavior. As described in 9.6, a reference cannot be bound directly to a bit-field. ]

                1.9/4:
                Certain other operations are described in this International Standard as undefined (for example, the effect of dereferencing the null pointer)


                1. withoutuniverse
                  25.04.2015 15:48

                  Тут объяснение, почему словим UB

                  $8.3.2/3 — It is unspecified whether or not a reference requires storage.
                  sizeof applied to references is basically the size of the referrand.


  1. antonyter
    24.04.2015 17:10
    +2

    ждем следующую статью на тему: «Как я понял, что такое L-/R- value!»


    1. is_0xBh_139 Автор
      24.04.2015 17:12

      ахахаха )) непременно ))


  1. zenden2k
    27.04.2015 00:43

    Указатели — это зло. Вот бы удалить из C++ указатели, добавить сборщик мусора…


    1. Suvitruf
      27.04.2015 10:14

      Слишком толсто.


    1. withoutuniverse
      27.04.2015 11:43

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


    1. stack_trace
      01.05.2015 09:56

      И получилась бы ещё одна Java. А зачем нужна ещё одна Java?


      1. Suvitruf
        14.05.2015 13:58

        Java много не бывает.


    1. stack_trace
      01.05.2015 10:05

      Кроме того, ничего плохого в указателях самих по себе нету. Плохи владеющие ресурсами сырые указатели. Но тут дело не в указателях — это лишь частный случай владения ресурсом без RAII. Любой сырой хэндл — такое же зло. И, кстати, сборщик мусора, на самом деле, решает только частную проблему, владения ресурсом, когда ресурс — именно память, тогда как RAII — решает общую проблему. Но так как сборщик мусора чаще всего исключает наличие деструктора, то RAII становится несовместим со сборщиком мусора. Поэтому, пожалуйста, не надо в плюсы тащить свои «гениальные» идеи.


  1. dyadyaSerezha
    30.04.2015 19:45

    «Переменная i_val — статическая, она явно будет размещена в стеке. В куче место выделяется под динамические объекты. Это важные вещи!» — дальше просто не смог читать. Это какое-то мракобесие. :)

    Вдогонку автору: чем меньше указателей в программе, тем лучше.