Страницы

Поиск по вопросам

четверг, 19 декабря 2019 г.

Зачем нужны две эквивалентные записи char** и char*[]?

#cpp #c


Судя по этому ответу, записи char** и char*[] в параметрах функции означают один
и тот же тип. Зачем так сделано и в каких ситуациях они будут означать разные типы?
    


Ответы

Ответ 1



Тип char *[] трансформируется (adjusted) в тип char ** в списках параметров функций. Во всех остальных случаях char *[] и char ** - совершенно разные типы. (char *[] - неполный тип. Не ясно, намеренно ли вы использовали в своем вопросе именно неполный тип.) Ответ на вопрос о том, почему так сделано в параметрах функций, надо искать в истории языка С. Короткий ответ на этот вопрос: это было сделано так для повышения обратной совместимости по исходному коду и семантике с языком-предшественником С - языком B. Длинный ответ на этот вопрос: детальный ответ содержится в статье Дениса Ритчи "The Development of the C Language" 1. Как и почему массивы в С перестали быть указателями. Подход к реализации массивов в языке С был изначально позаимствован из языков-прародителей - B и ВCPL (см. вторую часть раздела "Origins: the languages"). В этих языках "массивных" типов как таковых не было: объявление массива там всегда, во всех контекстах фактически объявляло обыкновенный указатель, вместе с которым сразу же автоматически выделялся отдельный блок памяти, на который этот указатель указывал. Доступ к элементам массива осуществлялся через обычную адресную арифметику указателей. Более того, такой указатель являлся самым обычным указателем - пользователь в любой момент мог присвоить ему новое значение и тем самым заставить указывать в любое другое место. Изначально Ритчи планировал использовать такой подход к реализации массивов и в С. Однако (см. раздел "Embryonic C") этот подход быстро пришел в противоречие с идеей одного важного нововведения языка С: struct типов (которых не было ни в В, ни в BCPL). Если бы массив в С реализовывался как явный указатель, то структурные типы, содержащие массивы, сразу же превратились бы в нетривиальные многоуровневые типы. Они бы требовали нетривиальной "конструкции", "деструкции" и, самое главное, нетривиального копирования. И это при том, что в раннем С применение оператора присваивания к тяжелым типам не поддерживалось вообще (!), а "тяжелые" типы копировались именно и только через memcpy. Понятно, что копировать структуру, содержащую скрытые указатели через memcpy бесполезно. Здесь понадобилось бы некое неявное "глубокое" копирование, о котором в С тогда не могло быть и речи. То есть, в рамках B-шного указательного подхода к массивам, struct типы получались некопируемыми вообще. Вот для того, чтобы решить эту проблему, Ритчи и отказался от идеи реализовывать массивы, как физические указатели. Массивы в С перестали быть указателями и превратились в непосредственные блоки памяти требуемого размера, т.е. в то, что мы имеем в С и С++ сегодня. При этом, во многом для того, чтобы сохранить совместимость с существующим кодом на B, массив в С стал на лету неявно приводиться к типу "указатель", через который и реализовывалась вся адресная арифметика для доступа к элементам массива. Так родилось всем нам известное неявное стандартное array-to-pointer conversion. Т.е. подход из языка B во многом сохранился практически неизменным на поверхности, но физический указатель исчез навсегда, заменившись на временный "воображаемый" указатель - результат неявного преобразования. 2. Почему эти изменения не задели параметры функций. Однако в параметрах функций, как вы видите, был полностью сохранен подход из языка B - объявление массива автоматически преобразуется в объявление обыкновенного указателя. Т.е. в списках параметров функций (и только там) объявление int a[] практически эквивалентно объявлению int *a. Почему здесь все осталось по-старому? Это объясняется в разделе "Critique". Буквально: "... это - живущее и поныне ископаемое, остаток B-шного подхода к объявлению указателей, в рамках которого массив, только в этом исключительном случае, интерпретируется как указатель. Этот вариант записи выжил частично ради обратной совместимости, частично в надежде на то, что он позволит программистам предупреждать читателя кода о том, что в данном месте ожидается указатель на элемент массива, а не указатель на отдельный объект. К несчастью, в итоге это скорее запутывает читателя, чем предупреждает его." Это цитата - ответ на вопрос о том, зачем была сохранен синтаксис [] при объявлении параметров функций, несмотря на то, что он все равно эквивалентен указательному синтаксису. Т.е. это фактически прямой ответ на ваш вопрос.

Ответ 2



Это всегда были разные типы. Указатель на указатель против указатель на массив и массив указателей. Вот примеры присвоения и приёмы аргументов разных типов: // > g++ -Wall -Wpedantic -std=c++11 arrpoi.cpp int * * pointerToPointer ; //int * arrayOfPointers [ ] ; // error: storage size of ‘arrayOfPointers’ isn’t known int * arrayOfPointers [ 1 ] ; int ( * pointerToArray ) [ ] ; void ptp(int * * pointerToPointer ); void aop(int * arrayOfPointers [ ]); //void pta(int ( * pointerToArray ) [ ]); // error: parameter ‘pointerToArray’ includes pointer to array of unknown bound ‘int []’ void pta(int ( * pointerToArray ) [ 1 ]); int main(int argn, char**args){ pointerToPointer = arrayOfPointers ; //arrayOfPointers = pointerToPointer ; // error: incompatible types in assignment of ‘int**’ to ‘int* [1]’ //arrayOfPointers = pointerToArray ; // error: incompatible types in assignment of ‘int (*)[]’ to ‘int* [1]’ //pointerToArray = pointerToPointer ; // error: cannot convert ‘int**’ to ‘int (*)[]’ in assignment //pointerToPointer = pointerToArray ; // error: cannot convert ‘int (*)[]’ to ‘int**’ in assignment //pointerToArray = arrayOfPointers ; // error: cannot convert ‘int* [1]’ to ‘int (*)[]’ in assignment ptp(pointerToPointer); ptp(arrayOfPointers); //ptp(pointerToArray); // cannot convert ‘int (*)[]’ to ‘int**’ for argument ‘1’ to ‘void ptp(int**)’ aop(pointerToPointer); aop(arrayOfPointers); //aop(pointerToArray); // error: cannot convert ‘int (*)[]’ to ‘int**’ for argument ‘1’ to ‘void aop(int**)’ //pta(pointerToPointer); // error: cannot convert ‘int**’ to ‘int (*)[1]’ for argument ‘1’ to ‘void pta(int (*)[1])’ //pta(arrayOfPointers); // error: cannot convert ‘int**’ to ‘int (*)[1]’ for argument ‘1’ to ‘void pta(int (*)[1])’ //pta(pointerToArray); // error: cannot convert ‘int (*)[]’ to ‘int (*)[1]’ for argument ‘1’ to ‘void pta(int (*)[1])’ return 0;} void aop(int * arrayOfPointers [ ]) { int * x = arrayOfPointers [ 10000 ] ; } Самый всеядный аргумент типа char** он может спокойно принимать массив указателей char*[], просто принимается первый элемент. Но если вы поставите приём аргументов char*[] то при аргументе типа char** будет всё хорошо, но при присваивании двух переменных будет конфликт типов. Вывод такой: если вы поставили аргумент как массив указателей неопределённой длины то можете исследовать данные на своё усмотрение, всё зависит от логики программы. А если поставили указатель на указатель, то это показывает, что функция читает/модифицирует только один указатель.

Комментариев нет:

Отправить комментарий