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

191
10 марта 2019, 01:30

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

Answer 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-шного подхода к объявлению указателей, в рамках которого массив, только в этом исключительном случае, интерпретируется как указатель. Этот вариант записи выжил частично ради обратной совместимости, частично в надежде на то, что он позволит программистам предупреждать читателя кода о том, что в данном месте ожидается указатель на элемент массива, а не указатель на отдельный объект. К несчастью, в итоге это скорее запутывает читателя, чем предупреждает его."

Это цитата - ответ на вопрос о том, зачем была сохранен синтаксис [] при объявлении параметров функций, несмотря на то, что он все равно эквивалентен указательному синтаксису. Т.е. это фактически прямой ответ на ваш вопрос.

Answer 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** будет всё хорошо, но при присваивании двух переменных будет конфликт типов.

Вывод такой: если вы поставили аргумент как массив указателей неопределённой длины то можете исследовать данные на своё усмотрение, всё зависит от логики программы. А если поставили указатель на указатель, то это показывает, что функция читает/модифицирует только один указатель.

READ ALSO
ServletOutputStream, получение данных

ServletOutputStream, получение данных

Моя проблема состоит в следующем:

198
Сравнить java-код по функциональности

Сравнить java-код по функциональности

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

235
Динамическое изменение курсора в EditText

Динамическое изменение курсора в EditText

Динамическое изменение курсора в EditText

191