Недавно я решил, что мне просто нужно наконец выучить C / C ++, и есть одна вещь, которую я действительно не понимаю в указателях или, точнее, в их определении.
Как насчет этих примеров:
int* test;
int *test;
int * test;
int* test,test2;
int *test,test2;
int * test,test2;
Теперь, насколько я понимаю, первые три случая делают то же самое: Test - это не int, а указатель на него.
Второй набор примеров немного сложнее. В случае 4 и test, и test2 будут указателями на int, тогда как в случае 5 только test является указателем, а test2 - «реальным» int. А как насчет случая 6? То же, что и в случае 5?
c++
c
pointers
declaration
Майкл Штум
источник
источник
int*test;
?Foo<Bar<char>>
>>
> >
++
оператор инкремента не может быть разделен пробелом, идентификаторы не могут быть разделены пробелом (и результат может быть допустимым для компилятора, но с неопределенным поведением во время выполнения). Точные ситуации очень сложно определить, учитывая синтаксический беспорядок, который представляет собой C / C ++.Ответы:
4, 5 и 6 - это одно и то же, только test - это указатель. Если вам нужны два указателя, вы должны использовать:
int *test, *test2;
Или еще лучше (чтобы все было понятно):
int* test; int* test2;
источник
Пробелы вокруг звездочек значения не имеют. Все три означают одно и то же:
int* test; int *test; int * test;
"
int *var1, var2
" - это злой синтаксис, который предназначен только для того, чтобы запутать людей, и его следует избегать. Он расширяется до:int *var1; int var2;
источник
int*test
.int *test
( google-styleguide.googlecode.com/svn/trunk/… ). Просто будьте последовательныМногие руководства по кодированию рекомендуют объявлять только одну переменную в каждой строке . Это позволяет избежать путаницы, которая была у вас до того, как задать этот вопрос. Большинство программистов на C ++, с которыми я работал, похоже, придерживаются этого.
Я знаю немного в сторону, но кое-что, что я нашел полезным, - это читать объявления задом наперед.
int* test; // test is a pointer to an int
Это начинает работать очень хорошо, особенно когда вы начинаете объявлять константные указатели, и становится сложно понять, является ли указатель константным или константой является то, на что указывает указатель.
int* const test; // test is a const pointer to an int int const * test; // test is a pointer to a const int ... but many people write this as const int * test; // test is a pointer to an int that's const
источник
Используйте «Правило спирали по часовой стрелке» для облегчения анализа объявлений C / C ++;
Кроме того, объявления должны быть по возможности в отдельных выражениях (что верно в подавляющем большинстве случаев).
источник
int* x;
стилю, а не к традиционномуint *x;
стилю. Конечно, для компилятора интервал не имеет значения, но он влияет на людей. Отрицание фактического синтаксиса приводит к правилам стиля, которые могут раздражать и сбивать с толку читателей.Как уже упоминалось, 4, 5 и 6 одинаковы. Часто люди используют эти примеры, чтобы аргументировать, что
*
переменная принадлежит переменной, а не типу. Хотя это вопрос стиля, есть некоторые споры о том, следует ли вам думать и писать так:int* x; // "x is a pointer to int"
или так:
int *x; // "*x is an int"
FWIW Я нахожусь в первом лагере, но причина, по которой другие приводят аргумент в пользу второй формы, заключается в том, что она (в основном) решает эту конкретную проблему:
int* x,y; // "x is a pointer to int, y is an int"
что потенциально вводит в заблуждение; вместо этого вы бы написали либо
int *x,y; // it's a little clearer what is going on here
или если вам действительно нужны два указателя,
int *x, *y; // two pointers
Лично я советую использовать одну переменную в каждой строке, тогда не имеет значения, какой стиль вы предпочитаете.
источник
int *MyFunc(void)
? a*MyFunc
- это функция, возвращающаяint
? нет. Очевидно, мы должны написатьint* MyFunc(void)
, скажем,MyFunc
функцию, возвращающуюint*
. Так что для меня это ясно, правила синтаксического анализа грамматики C и C ++ просто неверны для объявления переменных. они должны были включать квалификацию указателя как часть общего типа для всей последовательности запятой.*MyFunc()
это файлint
. Проблема с синтаксисом C заключается в смешивании синтаксиса префикса и постфикса - если бы использовался только постфикс, не было бы путаницы.int const* x;
, которые я считаю такими же вводящими в заблуждение, какa * x+b * y
.#include <type_traits> std::add_pointer<int>::type test, test2;
источник
#include <windows.h>LPINT test, test2;
В 4, 5 и 6
test
всегда есть указатель, аtest2
не указатель. Пробелы (почти) никогда не имеют значения в C ++.источник
Смысл C в том, что вы объявляете переменные так, как вы их используете. Например
char *a[100];
говорит, что
*a[42]
это будетchar
. Иa[42]
указатель на символ. Итак,a
это массив указателей на символы.Это потому, что авторы исходного компилятора хотели использовать один и тот же синтаксический анализатор для выражений и объявлений. (Не очень разумная причина для выбора языкового дизайна)
источник
char* a[100];
также выводит, что*a[42];
это будет указатель на символыchar
иa[42];
символы.a[42]
, этоchar
указатель, а*a[42]
это символ.На мой взгляд, ответ - ОБА, в зависимости от ситуации. Вообще, IMO, лучше ставить звездочку рядом с именем указателя, а не с типом. Сравните, например:
int *pointer1, *pointer2; // Fully consistent, two pointers int* pointer1, pointer2; // Inconsistent -- because only the first one is a pointer, the second one is an int variable // The second case is unexpected, and thus prone to errors
Почему второй случай противоречив? Потому что, например,
int x,y;
объявляются две переменные одного и того же типа, но этот тип упоминается в объявлении только один раз. Это создает прецедент и ожидаемое поведение. Иint* pointer1, pointer2;
это несовместимо с этим, потому что он объявляетсяpointer1
как указатель, ноpointer2
является целочисленной переменной. Очевидно, что он подвержен ошибкам, и, следовательно, его следует избегать (поставив звездочку рядом с именем указателя, а не с типом).Однако есть некоторые исключения, когда вы не сможете поставить звездочку рядом с именем объекта (и там, где это имеет значение, где вы его поместите) без получения нежелательного результата - например:
MyClass *volatile MyObjName
void test (const char *const p) // const value pointed to by a const pointer
Наконец, в некоторых случаях, это может быть , возможно , яснее поставить звездочку рядом с типом имени, например:
void* ClassName::getItemPtr () {return &item;} // Clear at first sight
источник
Я бы сказал, что первоначальное соглашение заключалось в том, чтобы поставить звездочку на стороне имени указателя (правая часть объявления
в языке программирования c Денниса М. Ритчи звезды находятся в правой части объявления.
посмотрев исходный код Linux на https://github.com/torvalds/linux/blob/master/init/main.c, мы увидим, что звездочка также находится справа.
Вы можете следовать тем же правилам, но это не имеет большого значения, если вы поставите звездочки на стороне шрифта. Помните, что последовательность важна, поэтому всегда ставьте звезду на одной стороне, независимо от того, какую сторону вы выбрали.
источник
Указатель является модификатором типа. Лучше читать их справа налево, чтобы лучше понять, как звездочка изменяет тип. 'int *' можно читать как «указатель на int». В нескольких объявлениях вы должны указать, что каждая переменная является указателем, иначе она будет создана как стандартная переменная.
1,2 и 3) Тест имеет тип (int *). Пробелы не имеют значения.
4,5 и 6) Тест имеет тип (int *). Test2 имеет тип int. Опять же пробелы несущественны.
источник
Эта головоломка состоит из трех частей.
Первая часть заключается в том, что пробелы в C и C ++ обычно не имеют значения, кроме разделения соседних токенов, которые в противном случае неразличимы.
На этапе предварительной обработки исходный текст разбивается на последовательность токенов - идентификаторы, знаки препинания, числовые литералы, строковые литералы и т. Д. Эта последовательность токенов позже анализируется на предмет синтаксиса и значения. Токенизатор является «жадным» и построит максимально длинный допустимый токен. Если вы напишете что-то вроде
токенизатор видит только два токена - идентификатор,
inttest
за которым следует знак препинания;
. Наint
этом этапе он не распознается как отдельное ключевое слово (это происходит позже в процессе). Итак, чтобы строка считалась объявлением целого числа с именемtest
, мы должны использовать пробелы для разделения токенов идентификаторов:int test;
Этот
*
символ не является частью какого-либо идентификатора; это отдельный токен (пунктуатор) сам по себе. Итак, если вы напишетеint*test;
компилятор видит 4 отдельных лексем -
int
,*
,test
, и;
. Таким образом, пробелы не важны в объявлениях указателей, и всеint *test; int* test; int*test; int * test;
интерпретируются одинаково.
Вторая часть головоломки - это то, как объявления на самом деле работают в C и C ++ 1 . Объявления разбиты на две основные части - последовательность спецификаторов декларации ( спецификаторы класса хранения, спецификаторы типа, квалификаторы типа и т. Д.), За которой следует список (возможно, инициализированных) деклараторов, разделенных запятыми . В декларации
unsigned long int a[10]={0}, *p=NULL, f(void);
в декларации спецификаторы
unsigned long int
и declarators являютсяa[10]={0}
,*p=NULL
иf(void)
. Декларатор вводит имя объявляемой вещи (a
,p
иf
) вместе с информацией о массиве, указателе и функции этой вещи. С декларатором также может быть связанный инициализатор.Тип
a
- «10-элементный массивunsigned long int
». Этот тип полностью определяется комбинацией спецификаторов объявления и декларатора, а начальное значение указывается с помощью инициализатора={0}
. Точно так же типомp
является «указатель наunsigned long int
», и снова этот тип определяется комбинацией спецификаторов объявления и декларатора и инициализируется значениемNULL
. И по той же причине типf
"возврат функцииunsigned long int
".Это ключевой момент - нет спецификатора типа "указатель на" , так же как нет спецификатора типа "массив из", точно так же, как нет спецификатора типа "возвращающий функцию". Мы не можем объявить массив как
int[10] a;
потому что операнд
[]
оператораa
неint
. Аналогично в декларацииint* p;
операнд
*
isp
, notint
. Но поскольку оператор косвенного обращения является унарным, а пробелы не важны, компилятор не будет жаловаться, если мы напишем его таким образом. Однако он всегда интерпретируется какint (*p);
.Следовательно, если вы напишете
int* p, q;
операнд
*
isp
, поэтому он будет интерпретирован какint (*p), q;
Таким образом, все
int *test1, test2; int* test1, test2; int * test1, test2;
сделайте то же самое - во всех трех случаях
test1
является операндом*
и, следовательно, имеет тип "указатель наint
", аtest2
имеет типint
.Заявители могут быть очень сложными. У вас могут быть массивы указателей:
T *a[N];
у вас могут быть указатели на массивы:
T (*a)[N];
у вас могут быть функции, возвращающие указатели:
T *f(void);
у вас могут быть указатели на функции:
T (*f)(void);
у вас могут быть массивы указателей на функции:
T (*a[N])(void);
у вас могут быть функции, возвращающие указатели на массивы:
T (*f(void))[N];
у вас могут быть функции, возвращающие указатели на массивы указателей на функции, возвращающие указатели на
T
:T *(*(*f(void))[N])(void); // yes, it's eye-stabby. Welcome to C and C++.
и тогда у вас есть
signal
:void (*signal(int, void (*)(int)))(int);
который читается как
signal -- signal signal( ) -- is a function taking signal( ) -- unnamed parameter signal(int ) -- is an int signal(int, ) -- unnamed parameter signal(int, (*) ) -- is a pointer to signal(int, (*)( )) -- a function taking signal(int, (*)( )) -- unnamed parameter signal(int, (*)(int)) -- is an int signal(int, void (*)(int)) -- returning void (*signal(int, void (*)(int))) -- returning a pointer to (*signal(int, void (*)(int)))( ) -- a function taking (*signal(int, void (*)(int)))( ) -- unnamed parameter (*signal(int, void (*)(int)))(int) -- is an int void (*signal(int, void (*)(int)))(int); -- returning void
и это лишь малая часть того, что возможно. Но обратите внимание, что массив, указатель и функция всегда являются частью декларатора, а не спецификатора типа.
Одна вещь, на которую следует обратить внимание -
const
можно изменить как тип указателя, так и тип указателя:const int *p; int const *p;
Оба из вышеперечисленных объявляются
p
как указатель наconst int
объект. Вы можете написать новое значение, чтобыp
оно указывало на другой объект:const int x = 1; const int y = 2; const int *p = &x; p = &y;
но вы не можете писать в объект, на который указывает:
*p = 3; // constraint violation, the pointed-to object is const
Однако,
int * const p;
объявляется
p
какconst
указатель на неконстантныйint
; вы можете написать то, на чтоp
указываетint x = 1; int y = 2; int * const p = &x; *p = 3;
но вы не
p
можете указать на другой объект:p = &y; // constraint violation, p is const
Это подводит нас к третьей части головоломки - почему декларации структурированы именно так.
Предполагается, что структура объявления должна точно отражать структуру выражения в коде («объявление имитирует использование»). Например, предположим, что у нас есть массив указателей на
int
namedap
, и мы хотим получить доступ кint
значению, на которое указываетi
'th элемент. Мы получили бы доступ к этому значению следующим образом:printf( "%d", *ap[i] );
Выражение
*ap[i]
имеет типint
; таким образом, объявлениеap
записывается какint *ap[N]; // ap is an array of pointer to int, fully specified by the combination // of the type specifier and declarator
Декларатор
*ap[N]
имеет ту же структуру, что и выражение*ap[i]
. Операторы*
и[]
ведут себя в объявлении так же, как и в выражении -[]
имеют более высокий приоритет, чем унарный*
, поэтому операнд*
равенap[N]
(он анализируется как*(ap[N])
).В качестве другого примера предположим, что у нас есть указатель на массив
int
named,pa
и мы хотим получить доступ к значениюi
'th элемента. Мы бы написали это какprintf( "%d", (*pa)[i] );
Тип выражения
(*pa)[i]
-int
, поэтому объявление записывается какint (*pa)[N];
Опять же, применяются те же правила приоритета и ассоциативности. В этом случае мы не хотим разыменовывать
i
'th элементpa
, мы хотим получить доступ кi
' th элементу, на которыйpa
указывает , поэтому мы должны явно сгруппировать*
оператор сpa
.Операторы
*
,[]
и()
являются частью выражения в коде, поэтому все они являются частью декларатора в объявлении. Декларатор сообщает вам, как использовать объект в выражении. Если у вас есть такое объявлениеint *p;
, это говорит вам, что выражение*p
в вашем коде дастint
значение. В расширении он сообщает вам, что выражениеp
дает значение типа «указатель наint
» илиint *
.Итак, что о таких вещах , как литые и
sizeof
выражения, где мы используем такие вещи , как(int *)
илиsizeof (int [10])
или тому подобные? Как мне прочитать что-то вродеvoid foo( int *, int (*)[10] );
Там нет описателя, не является
*
и[]
операторами , изменяющих типа напрямую?Что ж, нет - есть еще декларатор с пустым идентификатором (известный как абстрактный декларатор ). Если мы представим пустой идентификатор с символом X, то мы можем читать эти вещи , как
(int *λ)
,sizeof (int λ[10])
иvoid foo( int *λ, int (*λ)[10] );
и они ведут себя точно так же, как и любое другое объявление.
int *[10]
представляет собой массив из 10 указателей, аint (*)[10]
представляет собой указатель на массив.А теперь самоуверенная часть этого ответа. Мне не нравится соглашение C ++ об объявлении простых указателей как
T* p;
и считают это плохой практикой по следующим причинам:
T* p, q;
, все дубликаты этих вопросов и т. Д.);T* a[N]
асимметричным с использованием (если вы не привыкли писать* a[i]
);T* p
аккуратно применить соглашение, а это… нет );В конце концов, это просто указывает на путаницу в размышлениях о том, как работают системы типов двух языков.
Есть веские причины декларировать товары отдельно; работа над плохой практикой (
T* p, q;
) не входит в их число. Если вы правильно напишете свои деклараторы (T *p, q;
), вы вряд ли запутаетесь.Я считаю, что это сродни намеренному написанию всех ваших простых
for
циклов какi = 0; for( ; i < N; ) { ... i++ }
Синтаксически верный, но сбивающий с толку, а намерение может быть неправильно истолковано. Однако это
T* p;
соглашение укоренилось в сообществе C ++, и я использую его в своем собственном коде C ++, потому что согласованность всей базы кода - это хорошо, но каждый раз, когда я это делаю, у меня начинает чесаться.источник
Хорошее эмпирическое правило, многие люди, кажется, постигают эти концепции следующим образом: В C ++ много семантического значения получают путем привязки слева ключевых слов или идентификаторов.
Например:
int const bla;
Константа применяется к слову "int". То же самое и со звездочками указателей, они применяются к ключевому слову слева от них. А собственно имя переменной? Ага, это декларируется тем, что от него осталось.
источник