Я должен начать с того, что C и C ++ были первыми языками программирования, которые я выучил. Я начал с C, потом много занимался C ++ в школе, а затем вернулся к C, чтобы свободно владеть им.
Первое, что смутило меня в отношении указателей при изучении C, было простое:
char ch;
char str[100];
scanf("%c %s", &ch, str);
Эта путаница была главным образом связана с использованием ссылки на переменную для аргументов OUT до того, как указатели были должным образом представлены мне. Я помню, что пропустил написание первых нескольких примеров на C для чайников, потому что они были слишком просты, чтобы никогда не заставить работать первую программу, которую я написал (скорее всего, из-за этого).
Что смущало, так это то, что на &ch
самом деле означало, а также почему str
это не нужно.
После того, как я познакомился с этим, я вспомнил, что был озадачен динамическим распределением. В какой-то момент я понял, что указатели на данные не очень полезны без динамического выделения некоторого типа, поэтому я написал что-то вроде:
char * x = NULL;
if (y) {
char z[100];
x = z;
}
попытаться динамически выделить некоторое пространство. Это не сработало. Я не был уверен, что это сработает, но я не знал, как еще это может сработать.
Позже я узнал о malloc
и new
, но они действительно казались мне генераторами волшебной памяти. Я ничего не знал о том, как они могут работать.
Некоторое время спустя меня снова учили рекурсии (раньше я изучал ее самостоятельно, но сейчас был в классе) и спросил, как она работает под капотом - где хранятся отдельные переменные. Мой профессор сказал «в стеке», и многое мне стало ясно. Я слышал этот термин раньше и уже реализовывал программные стеки. Я слышал, что другие упоминали о «стеке» задолго до этого, но забыл об этом.
Примерно в это же время я также понял, что использование многомерных массивов в C может привести к путанице. Я знал, как они работают, но их было так легко запутать, что я решил попытаться обойти их, когда смогу. Я думаю, что проблема здесь была в основном синтаксической (особенно передача или возврат их из функций).
Так как я писал C ++ для школы в течение следующего года или двух, я получил большой опыт использования указателей для структур данных. Здесь у меня возник новый набор неприятностей - путаница указателей. У меня было бы несколько уровней указателей (вещи как node ***ptr;
) сбивают меня с толку. Я бы разыменовал указатель неверное число раз и в итоге прибегнул к определению, сколько *
мне нужно методом проб и ошибок.
В какой-то момент я узнал, как работает куча программы (вроде, но достаточно хорошо, что она больше не держала меня ночью). Я помню, что читал, что если вы посмотрите на несколько байтов перед указателем, который malloc
возвращается в определенной системе, вы увидите, сколько данных было фактически выделено. Я понял, что код malloc
может запросить больше памяти у ОС, и эта память не была частью моих исполняемых файлов. Наличие достойной рабочей идеи о том, как malloc
работает, действительно полезно.
Вскоре после этого я взял урок ассемблера, который не научил меня так много, как, вероятно, думает большинство программистов. Это заставило меня задуматься о том, на какую сборку может быть переведен мой код. Я всегда пытался написать эффективный код, но теперь у меня была лучшая идея, как это сделать.
Я также взял пару классов, где я должен был написать какой-то шутку . При написании lisp я не был так обеспокоен эффективностью, как в C. Я очень мало представлял, во что этот код может быть переведен, если скомпилирован, но я знал, что похоже на использование множества локальных именованных символов (переменных), созданных все намного проще. В какой-то момент я написал немного кода поворота дерева AVL с небольшим количеством шуток, и мне было очень трудно писать на C ++ из-за проблем с указателями. Я понял, что мое отвращение к тому, что я считал избыточными локальными переменными, помешало мне написать и несколько других программ на C ++.
Я также взял класс компиляторов. Находясь в этом классе, я переключился на расширенный материал и узнал о статических одиночных присваиваниях (SSA) и мертвых переменных, что не так уж важно, за исключением того, что он научил меня, что любой приличный компилятор справится с переменными, которые больше не используется. Я уже знал, что больше переменных (включая указатели) с правильными типами и хорошими именами помогло бы мне держать вещи прямо в моей голове, но теперь я также знал, что избегать их по соображениям эффективности было даже более глупо, чем мои менее склонные к микрооптимизации профессора говорили меня.
Так что для меня очень помогло знание структуры памяти программы. Размышления о том, что означает мой код, как символически, так и на оборудовании, помогают мне. Использование локальных указателей, имеющих правильный тип, очень помогает. Я часто пишу код, который выглядит так:
int foo(struct frog * f, int x, int y) {
struct leg * g = f->left_leg;
struct toe * t = g->big_toe;
process(t);
так что если я испорчу тип указателя, то по ошибке компилятора станет ясно, в чем проблема. Если бы я сделал:
int foo(struct frog * f, int x, int y) {
process(f->left_leg->big_toe);
и любой неверный тип указателя будет ошибкой компилятора. Я испытал бы соблазн прибегнуть к пробным и ошибочным изменениям в моем разочаровании и, вероятно, усугубить ситуацию.