Некоторые так и делают. Можно завести свою функцию, которая будет выставлять указатель в nullptr. Но лучше пользоваться умными указателями, там это всё делается без нужды следить за этим.
А почему start должен стать nullptr? free не меняет сам указатель, а освобождает память. Указатель - это грубо говоря просто число, вы передали его во free, память по этому адресу освободилось, но само-то число у вас осталось то же. free также не заполняет мусором, хотя, возможно, делает это в debug'е для диагностики. Что значит "указатели рабочие"? Вы можете написать даже так:
char * p = 0;
p = p + 12345;
Это не делает указатель "рабочим", это просто указатель куда-то там, хотя он и не nullptr. В вашем случае после free все указатели в ту область памяти так и остаются указателями в ту же область, но чтение/запись оттуда/туда уже приведёт к UB - т.е. может и сработать, а может и нет.
delete вызывает деструктор соответствующего объекта, а затем освобождает память, при этом если тип не соответствует исходному, будет вызван не тот деструктор, либо в случае с void * не будет вызван никакой. Память при этом освобождена будет, но только та, которая выделена была под сам объект. Если этот объект имел внутри себя ссылки на какие-то другие объекты, под которые была выделена память при его создании, они не будут уничтожены (так как это происходит в деструкторе) и не будет освобождена память под них.
Т.е., упрощённо, если объект содержит внутри себя только пару int, то ничего страшного не будет, а если он внутри содержит std::vector, который сам ссылается на выделенную под массив память, то деструктор std::vector'а не будет вызван и память под массив не будет освобождена.
Стоит отметить, что в случае с new/delete необходимо ещё и сохранять тип указателя, т.е. void * p = new SomeClass(); delete p; - UB. На деле не будет вызван деструктор, что может повлечь различные последствия в зависимости от того, чем этот деструктор занимается, но это особенность реализации, на которую полагаться не стоит. Можно только передавать указатель на базовый класс, если деструктор виртуален. Что касается new[]/delete[], то там тип должен быть строго тот же.
В C++ это обычная практика - начало указывает на первый элемент, а конец - за последний. Это касается указателей и итераторов. Т.е. начало - вполне валидный указатель, между началом и концом - валидный, а вот сам конец - это уже невалидный.
По поводу free(start) - сделать так можете, но получите то же UB, ибо этот блок будет помечен, как свободный. То, что временно он будет ничем не занят и туда можно будет писать - особенность, ничем не гарантированная. Т.е. это почти то же самое, что писать наугад.
free/malloc, new/delete, new[]/delete[] - парные функции и операторы, они обязаны вызываться именно в паре (т.е. если malloc, то free, если new, то delete, если new[], то delete[]) и ровно для того указателя, который вернули, а не для какого-то, который лежит внутри той области, которую они выделили.
malloc(size) возвращает указатель на начало области памяти, и именно это значение указателя должно быть передано во free по окончании работы с памятью, а какие вы в период работы заводите указатели внутрь этого блока - значения не имеет
Т.е. в вашем исходном примере последний адрес: tmp + 500 (tmp в данном случае эквивалентно &tmp[0]). Также надо иметь в виду, что sizeof(tmp) - размер массива в байтах, а не в элементах, т.е. 500 * sizeof(int32_t) в данном случае.
Смотря какой тип у &tmp. Указатель смещается не на байты, а на элементы, т.е. ptr + n смещается на n * sizeof(T) байт сам по себе, где T * ptr. Т.о. ptr + n ≡ reinterpret_cast(reinterpret_cast(ptr) + n * sizeof(T)). void * же смещать нельзя, сначала его надо к чему-то преобразовать.
@Nevars нет, обычная функция, определятся как fromIntegral = fromInteger . toInteger, где toInteger - функция класса Integral, а fromInteger - класса Num. Дальнейшее объяснение зависит от того, знакомы ли вы с понятием классов в Хаскеле :)