/* Ребят, в статье было найдено много ошибок. Спасибо тем людям, которые внесли свои замечания. В связи с этим — после прочтения статьи обязательно перечитайте комментарии */
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 и присваиваем ее указателю. Здесь стоит обратить внимание на следующие вещи:
- Тип, используемый при объявлении указателя в точности должен соответствовать типу переменной, адрес которой мы присваиваем указателю.
- В качестве типа, который используется при объявлении указателя, можно выбрать тип void. Но в этом случае при инициализации указателя придется приводить его к типу переменной, на которую он указывает.
- Не следует путать оператор взятия адреса со ссылкой на некоторое значение, которое так же визуально отображается символом &.
Теперь, когда мы имеем указатель на переменную 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
}
- Здесь все ясно — используем саму переменную.
- Во втором случае — мы обращаемся к значению переменной 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;
}
Замечания:- Запись array[i] эквивалентна записи *(array + i). Никто не запрещает использовать их комбинированно: (array + i)[1] — в этом случае смещение идет на i, и еще на единичку. Однако, в данном случае перед выражением (array + i) ставить * не нужно. Наличие скобок это «компенсирует.
- Следите за вашими „перемещениями“ по элементам массива — особенно если вам захочется использовать
порнографическийтакой метод записи, как (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, учитывая, что это должно произойти в некоторой функции, которая так или иначе занимается обработкой массивов. Цель: разобраться в способе передачи указателей для их дальнейшей модификации.
- Вариант первый. Передаем собственно указатели a и b в качестве параметров функции:
После вызова данной функции в теле main — f2(a, b, 4) содержимое массивов 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]; } } }
- Вариант второй. Заменить значение указателя: просто присвоить значение указателя 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. То есть реализовать следующую функцию:
Сработает ли она? Если мы внутри функции f3 вызовем функцию f1(a, 4), то увидим, что значения массива действительно поменялись. НО: если мы посмотрим содержимое массива a в main — то обнаружим обратное — ничего не изменилось. Так в чем же причина? Все предельно просто: в функции f3 мы работали не с самим указателем a, а с его локальной копией! Все изменения, которые произошли в функции f3 — затронули только локальную копию указателя, но никак не сам указатель a. Давайте посмотрим на следующий пример:void f3(int** a, int** b){ a = b; }
Итак, я думаю, вы поняли, к чему я веду. Переменной a нельзя присвоить таким образом значение переменной b — ведь мы передавали их значения напрямую, а не по ссылке. То же самое и с указателями — используя их в качестве аргументов таким образом, мы заведомо лишаем их возможности изменения значения.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; }
Вариант третий, или работа над ошибками по второму варианту:
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); // Вот и удалился одномерный массив
На данной ноте я хотел бы закончить свое повествование. Если найдется хотя бы пара ребят, которым понравится стиль изложения материала, то я постараюсь продолжить… ой, да кого я обманываю, мне нужен инвайт и все на этом, дайте инвайт и вашим глазам больше не придется видеть это околесицу. Шучу, конечно. Ругайте, комментируйте.
Комментарии (59)
datacompboy
23.04.2015 12:09+11Если пользуетесь оператором new[], то и удалять надо через delete[].
да и удалять надо все уровни, а то сейчас у вас утекла пачка памяти, на которую ссылались массивы a и bis_0xBh_139 Автор
23.04.2015 12:14Можно немного подробнее?
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.
GarryC
23.04.2015 12:09+1Переменная i_val — статическая, она явно будет размещена в стеке
Вспоминается анекдот про Ходжу Насреддина.
Однажды султан прочитал в книге фразу о том, что извинение бывает хуже проступка, и пристал к Ходже в требованием объяснить суть.
Тот долго пытался, но султан оставался недоволен. Решили отложить вопрос на потом.
Вечером, когда он поднимались по лестнице на минарет, и султан, естественно, шел первым, Ходжа шлепнул его по заду.
Удивленный султан повернулся и спросил «Что случилось ?».
Ходжа ответил: «Ваш зад, о мой султан, напомнил мне зад моей любимой жены, и почему бы нам не заняться любовью ?».
Возмущенный султан воскликнул «Ты что, совсемох.елс ума сошел !».
На что Ходжа ответил: «Вот теперь, мой повелитель, Вы поняли, как извинение может быть хуже проступка ?».
По моему, здесь тот самый случай.is_0xBh_139 Автор
23.04.2015 12:12+1Спасибо за замечание. Впредь постараюсь не делать подобных упущений)
withoutuniverse
24.04.2015 11:42Суть в том, что указатель
тоже будет размещен в стеке.int* i_ptr = &i_val;
is_0xBh_139 Автор
26.04.2015 20:33потому что мы сразу его проинициализировали? а если бы мы объявили указатель на объект типа int, а инициализировали его позже, тогда бы размещение произошло в куче, я правильно понимаю? т.е. если бы мы, например, выделили память на массив из N элементов c помощью оператора new.
is_0xBh_139 Автор
26.04.2015 21:01причем после инициализации массива — значения элементов хранились бы в куче, а значение указателя — все-таки в стеке?
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 // куча, указатель ссылается на этот адрес
brn
23.04.2015 13:59+11А с каких пор переменные размещенные на стеке стали называться статическими? Статические переменные, это те что помечены модификатором static и это совсем другое.
Fil
23.04.2015 14:07+1Тип, используемый при объявлении указателя в точности должен соответствовать типу переменной, адрес которой мы присваиваем указателю
Не совсем. Ведь указатели незаменимы для реализации наследования и полиморфизма:
class Base { }; class Derived : public Base { }; int main() { Derived d; Base* b = &d; }
saluev
23.04.2015 15:06+8В свете всех найденных ошибок и неточностей у меня возникает вопрос: зачем вообще писать статью по теме, в которой разбираешься так плохо?
is_0xBh_139 Автор
23.04.2015 15:11-1За ошибки извиняюсь, впредь буду тщательнее проверять то, что написал. И спасибо тем ребятам, которые прочитали этот пост и сделали вполне корректные, важные замечания. Ну и для меня это — как некоторая форма обучения )))
FoxCanFly
01.05.2015 16:09Для обучения можно писать это своему преподавателю, а не в публичном доступе, а то один напишет, другой прочитает и будет так делать.
MaximChistov
23.04.2015 15:44+2void 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; }
Вы это серьёзно? Указатели и так не самая простая тема для новичков(вы вот в ней до сих пор не разобрались даже наполовину), а Вы им такой жутью хотите мозги загрузить…is_0xBh_139 Автор
23.04.2015 16:01Учту, спасибо за замечание. Перепишу. Я пытался подвести к этому, раз не получилось, буду исправляться )
grechnik
23.04.2015 16:44*i_ptr++; // результат эквивалентен операции инкремента самой переменной: i_val++ // т.е. в данном случае в i_val сейчас хранится значение не 7, а 8.
Почитайте про приоритеты операций.*i_ptr++
означаетi_ptr++
.is_0xBh_139 Автор
23.04.2015 16:58Спасибо, ознакомлюсь. Однако моя VS скомпилировала это так, как я предполагал. Все равно спасибо, учту.
Flex25
23.04.2015 19:50В продолжение темы указателей C++: всем новичкам, кто хоть немного дружит с английским, настоятельно рекомендую к просмотру серию великолепных коротких обучающих видео: www.youtube.com/playlist?list=PL2_aWCzGMAwLZp6LMUKI3cc7pgGsasm2_
Xitsa
24.04.2015 09:02Если уж человек понимает английский, то можно советовать книгу Richard M. Reese — Understanding and Using C Pointers.
На мой взгляд она покрывает тему целиком.
stack_trace
24.04.2015 01:52Хм, я всегда воспринимал указатель, как просто тонкую абстракцию над адресом в памяти. По-моему, если их рассматривать так, то вообще никаких сложностей нет.
withoutuniverse
24.04.2015 11:38Вот только указатель совсем не абстракция, он вполне себе занимает место в памяти и по своему адресу имеет какое-то значение, этим значением будет адрес, на который ссылается наш указатель. Пример:
0x00004 - адрес нашего указателя, занимает 4 байта (корректно для 32 битных систем), 0x00008 - его значение (тоже 4 байта, очевидно) 0x00008 - адрес некой переменной, занимающей 4 байта, 12345 - ее значение.
Для ссылок ваше утверждение будет верно (хотя даже тут я усомнюсь в его корректности).withoutuniverse
24.04.2015 11:45Немного поправлю себя
Корректнее назвать 0x0004 началом адреса, так как 4 байта будут занимать 0x0004, 0x0005, 0x0006, 0x0007.
stack_trace
24.04.2015 12:49+1А что странного в том, что адрес тоже имеет размер и тоже располагается в памяти? Это не отменяет того, что он всего лишь указывает на какое-то другое место в памяти. Собственно, если распечатать указатель вы этот адрес и увидите. А абстракцией я его назвал из-за того, что указатели ведут себя не совсем так, как сырой адрес в памяти, имея ввиду арифметические операции с указателями.
Видимо, у нас просто проблема с терминалогией ). Поясню, слово «абстракция» я здесь употребляю не в смысле нечто неосязаемое, а в смысле объект, обладающий функциональностью отличной от объекта, на основе которого он выполнен. В данном случае, указатель обладает свойствами, отличными от просто адреса в памяти. С точки зрения данных — это именно адрес, но операции, определённые для указателя и адреса немного различаются. Надеюсь, объяснение получилось не слишком путаным.withoutuniverse
24.04.2015 14:29После развернутого ответа, соглашусь с вами.
Предлагаю сойтись на данных утверждениях:ссылка — тонкая абстракция над адресом в памяти
указатель — абстракция над адресом в памятиbrn
24.04.2015 16:31С этим я не совсем согласен. Сcылка такая же абстракция с одним исключением. Это указатель, никогда не указывающий на NULL.
withoutuniverse
24.04.2015 17:11Ссылка не занимает места и не имеет адреса в памяти, какой же это указатель?
Это всего лишь алиас для переменной, на которую она ссылается.
Другое дело, когда ссылка передается в качестве аргумента функции — там действительно на стеке будет выделена память, соразмерная занимаемой указателем памяти (4 байта для x86).
Если я не прав, пусть специалисты по C++ меня поправят, предоставив пруфы.int a=5; // на стеке выделится место для переменной "a" int &b=a; // ничего не произойдет, "b" это алиас для "a"
stack_trace
24.04.2015 21:17+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.
stack_trace
25.04.2015 16:50Ну я же так и написал. Спецификация не говорит о том, как ссылки должны быть реализованы, однако чаще всего это именно указатели.
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; }
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
stack_trace
27.04.2015 01:49Вы не правы. Размер ссылки и размер объекта, содержащего ссылку это не одно и то же. Доказательство.
В общем-то, именно поэтому я и положил ссылку в класс, а не вывел её размер напрямую. Я выше написал, что в спецификации не говорится о том, как ссылка должна быть реализована, но чаще всего это указатель. Я не понимаю в чём конкретно вы со мной не согласны и как это противоречит тому, что вы написали или тому, о чём говорит спецификация.
Про спецификацию тоже не вижу ничего ( как минимум до того момента, как я на нее сослался)
Гм.
Хотя стандартом это и не требуется, но по сути, это единственный возможный способ её имплементации в большинстве случаев.
Давайте по пунктам, в чём вы со мной не согласны? В том, что ссылка это чаще всего адрес? Если хотите, я могу ассемблерный код привести вам для этого примера и вы увидите, что там адрес. И мне бы было очень интересно услышать вашу версию того, как ссылку в классе или в аргументе функции можно реализовать по другому.
Стандарт не говорит о том, как она реализована? Прекрасно, я с этим не спорил, более того, я первый указал на это (см. цитату выше), но это не отменяет того, что в большинстве случаев адрес — единственный возможный способ её реализации.
Я лично считаю некорректным высказывание «Ссылка это или ничего, или черный ящик.». Потому что если ссылка чёрный ящик всегда, если мы говорим о стандарте, нету никакого «либо ничего», потому как стандарт этого тоже не требует.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.
withoutuniverse
27.04.2015 13:19Все встало на свои места — компилятор не будет резервировать место под ссылку, если она в теле метода создается и больше не используется нигде. TEST
Если же ссылка будет полем объекта или пробрасываться в параметр методу — 4(8) байта.
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)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.
zenden2k
27.04.2015 00:43Указатели — это зло. Вот бы удалить из C++ указатели, добавить сборщик мусора…
withoutuniverse
27.04.2015 11:43В С++ давно уже есть умные указатели, которые позволяют свести работу со звездочкой к минимуму.
По сути, в ios пошли таким же путем, хотя это и С.
stack_trace
01.05.2015 10:05Кроме того, ничего плохого в указателях самих по себе нету. Плохи владеющие ресурсами сырые указатели. Но тут дело не в указателях — это лишь частный случай владения ресурсом без RAII. Любой сырой хэндл — такое же зло. И, кстати, сборщик мусора, на самом деле, решает только частную проблему, владения ресурсом, когда ресурс — именно память, тогда как RAII — решает общую проблему. Но так как сборщик мусора чаще всего исключает наличие деструктора, то RAII становится несовместим со сборщиком мусора. Поэтому, пожалуйста, не надо в плюсы тащить свои «гениальные» идеи.
dyadyaSerezha
30.04.2015 19:45«Переменная i_val — статическая, она явно будет размещена в стеке. В куче место выделяется под динамические объекты. Это важные вещи!» — дальше просто не смог читать. Это какое-то мракобесие. :)
Вдогонку автору: чем меньше указателей в программе, тем лучше.
Suvitruf
Шёл 2015 год, а я всё ещё наблюдаю статьи по азам работы с указателями в C++
NeoCode
Люди же новые рождаются:) У каждого из нас был момент, когда указатели в С++ были чем-то новым и необычным… а для кого-то этот момент еще не настал.
Nikiti4
для меня, например=)
Автор, спасибо за статью
is_0xBh_139 Автор
Вам спасибо ) Это очень приятно быть кому — то полезным )