«Цель OPN [Отладка] отменяет настройку сборки OTHER_LDFLAGS». Это был главный вопрос. После добавления $ (унаследованного) в новой строке в других флагах компоновщика решена моя проблема.
Массив - это указатель. Различия между * (разыменование) и & amp; (addressof) операторов и когда использовать оба
И подчеркивают, что лучшее (и действительно единственное) реальное место для C в наши дни - это встроенные системы и приложения реального времени, где ресурсы ограничены, а время выполнения является фактор.
Я не очень ценил C как язык, пока не взял класс встроенных микропроцессорных систем, и мы реализовали аппаратное обеспечение, прочитав руководство программиста в руководстве к плате Motorolla Dragonball. Следовательно, если это вообще возможно (что может быть сложно, так как вам понадобится дешевое оборудование), попытайтесь заставить их работать над аналогичными проектами (реализация таблиц UART и векторов прерываний и т. Д.) ...
Поскольку такие вещи, как обработка строк, сортировка и т. Д., Являются игрушечными задачами классической школы, они на самом деле уже не так полезны и расстраивают учащихся, которые знают, что существуют более простые способы. Это намного более полезно для & amp; байт с битовой маской и смотреть, как загорается светодиод.
О, и я никогда не узнал о том, как использовать такие вещи, как gcc в школе, или что на самом деле происходит с make-файлами. Прагматичные программисты говорят, что это полезно знать.
Я использовал C89 во встроенном программировании, и отладка аппаратного обеспечения была кошмарной. У нас было несколько соглашений о кодировании, которые спасли наши здравомыслия:
Например:
#define NOERR 0
#define VariableLookupNULL 1024
#define VariableLookupNOTFOUND 1025
... separate #define for each error
#define EvaluateExpressionNULL 1055
#define EvaluateExpressionUNKNOWNOP 1056
int EvaluateExpression( char *expression, int* result )
{
ASSERT(result != 0);
if (expression==0)
return EvaluateExpressionNULL;
*result = 0;
while (*expression != 0)
{
switch (*expression)
{
case ' ':
case '\t':
break; // ignore whitespace
case 'a':
... other variables
{
int var = 0;
int lookupResult = VariableLookup(*expression, &var);
if (lookupResult != NOERR)
return lookupResult;
*result += var;
break;
}
... check operators, et al.
default:
return EvaluateExpressionUNKNOWNOP;
}
++expression;
}
return NOERR;
}
ASSERT был макросом отладки, который прерывал время выполнения.
Хотя я и не был связан напрямую с C, я хотел бы узнать о технике использования ASSERT для раннего обнаружения ошибок (например, задолго до некоторой странной ошибки, вызванной перезаписью памяти). Вместо этого я самостоятельно обнаружил это несколько лет спустя. Эта техника обнаружила множество ошибок (в том числе очень тонких), которые в противном случае остались бы незамеченными.
В общем случае утверждение добавляется всякий раз, когда можно сделать некоторое предположение о значении в программе, например, оно никогда не бывает отрицательным или равным нулю или больше, чем какая-либо другая переменная.
Например:
assert(pInt)
, если предполагается, что pInt будет указывать на разумные данные. Будет стрелять по нулевому указателю. Часто используется для указателей, передаваемых функциям.
Или
assert(pInt < pMax)
, где pMax указывает сразу за концом целочисленного массива, над которым работает pInt.
Или
утверждают (yMass> 57.90)
(где yMass - масса одиночного заряженного y-иона для пептида)
Хотелось бы, чтобы мои профессора научили нас, как пользоваться отладчиком. Вместо этого я возился с инструментарием своего кода, пытаясь выяснить проблемы с помощью printf. Обнаружение GDB было похоже на включение лампочки. Возможность отладки сбоя с помощью дампа ядра была особенно полезна, поскольку многие новые ошибки программирования на C обычно возникают из-за плохой логики указателей.
В настоящее время юнит-тестирование, вероятно, будет хорошей практикой для обучения.
Инициализируйте указатели, которые в противном случае были бы неопределенными, к значению, которое приведет к аварийному завершению программы при разыменовании (вместо перезаписи памяти в каком-либо произвольном месте).
Это будет работать, как и предполагалось, в большинстве 32-битных систем:
int * pInt = (int *) 0xDEADBEEF
Я не уверен, что будет хорошим значением в 64-битной системе .
Простой инструмент отладки, printf (). Если у вас нет инструментов отладки !!
Одна точка с запятой является операцией NOP:
if(cond0) { /*...*/ }
else if(cond1) ; //is correct and does nothing
else { /*...*/}
Оператор запятой:
a = (++i, k); //eq: ++i; a = k;
Ориентация объекта:
struct Class {
size_t size;
void * (* ctor) (void * self, va_list * app); // constructor method
void * (* dtor) (void * self); // destructor method
void (* draw) (const void * self); // draw method
};
( Источник кода )
Когда мне приходилось использовать C как часть более крупного проекта в школе, именно умение правильно использовать gdb (то есть вообще) приводило к прогнозированию, кто завершит свой проект, а кто - нет. Да, если что-то сходит с ума, и у вас есть тонны ошибок, связанных с указателями и памятью, GDB будет показывать странную информацию, но даже зная, что может указать людям правильное направление.
Также напомним, что C - это не C ++, Java, C # и т. Д. - хорошая идея. Чаще всего это происходит, когда вы видите, что кто-то рассматривает char * как строку в C ++.
Всегда активные предупреждения. С GCC используйте как минимум -Wall -Wextra -Wstrict-prototypes -Wwrite-strings
.
Ввод / вывод сложен. scanf()
это зло. gets()
не следует никогда использовать.
Когда вы печатаете что-то, что не определено '\n'
, вы должны сбросить stdout
, если вы хотите напечатать это немедленно, например
printf("Type something: ");
fflush(stdout);
getchar();
По возможности используйте указатели const
. Например. void foo(const char* p);
.
Используйте size_t
для хранения размеров.
Литеральные струны, как правило, не могут быть изменены, поэтому сделайте их const
. Например. const char* p = "whatever";
.
Проверьте границы,
и, конечно,
Проверьте границы.
И если вы забыли одно из этих правил, используйте Valgrind. Это относится к массивам, строкам и указателям, но на самом деле очень легко забыть о том, что вы действительно делаете, когда делаете выделения и арифметику памяти.
Учитывая их опыт, возможно, хороший фокус на C для встроенных систем, включая:
И очень существенно: ПО для управления версиями . Я работаю в промышленности и использую это религиозно, но я поражен тем, что это никогда не упоминалось в моей степени!
Я не думаю, что вы должны обучать инструментам. Это следует оставить учителям Java. Они полезны и широко используются, но не имеют ничего общего с C. Отладчик - это столько, на что они должны надеяться получить доступ. Много раз все, что вы получаете, это printf и / или мигающий светодиод.
Обучайте их указателям, но учите их хорошо, говоря им, что они являются целочисленной переменной, представляющей позицию в памяти (в большинстве курсов они также проходят определенную подготовку по сборке, даже если это некая воображаемая машина, поэтому они должны быть способны понять это), а не переменная с префиксом звездочки, которая каким-то образом указывает на что-то и иногда становится массивом (C не Java). Научите их, что массивы C - это просто указатель + индекс.
Пусть они напишут программы, которые наверняка переполнятся и перестанут работать, а после этого убедитесь, что они понимают, почему это произошло.
Стандартной библиотекой является также C, пусть они используют ее, и их программы мучительно умирают в ваших частных тестах из-за использования get () и strcpy () или двойного освобождения чего-либо.
Заставьте их работать с переменными различного типа, порядком байтов (ваши тесты могут выполняться в другой арке), преобразованием с плавающей точкой в int. Заставьте их использовать маски и побитовые операторы.
то есть. научить их C.
Вместо этого я получил некоторую пакетную обработку в C, которая также могла бы быть выполнена в GW-BASIC.
Гигиенические имена в макросах C:
#define SOME_MACRO(_x) do { \ int *x = (_x); \ ... \ } while(0)
Определение x таким образом внутри макроса опасно, поскольку (_x) может также расширяться до x, заканчиваясь на:
do { int *x = x; ... } while(0)
, который может не получить никакого предупреждения от вашего компилятора, но фактически инициализировать ваш указатель x мусором (а не затененным x из внешней области видимости).
Важно использовать имена, которые, как вы знаете, уникальны для этого макроса. Препроцессор C не имеет механизма для автоматизации этого, поэтому вам просто нужно выбрать некрасивые имена для ваших макроопределенных переменных или просто избегать их для этих целей.
Мои учителя потратили так много времени на то, чтобы научить нас, что указатели - это пугающие маленькие обманщики, которые могут вызвать множество проблем, если их неправильно использовать, что они никогда не удосужились показать нам, насколько они действительно могущественны.
Например, концепция арифметики указателей была для меня чужой, пока я уже несколько лет не использовал C ++:
Примеры:
Вместо того, чтобы заставлять студентов бояться использовать указатели, научите их, как правильно их использовать.
РЕДАКТИРОВАТЬ: Как я обратил на себя внимание комментарием, который я только что прочитал на другой ответ, я думаю, что есть также некоторая ценность в обсуждении тонких различий между указателями и массивами (и как поставить два вместе, чтобы облегчить некоторые довольно сложные структуры), а также о том, как правильно использовать ключевое слово const
по отношению к объявлениям указателей.
Знайте, что когда вы увеличиваете указатель, новый адрес зависит от размера данных, на которые указывает этот указатель ... (IE, в чем разница между увеличивающимся символом * и длинным знаком без знака *) ...
Прежде всего, нужно точно знать, что такое ошибка сегментации, а также как с ними справиться.
Знание того, как использовать GDB, прекрасно. Знание того, как использовать валгринд, прекрасно.
Разработка стиля программирования на C ... Например, я склонен писать достаточно объектно-ориентированный код, когда пишу большие программы на C (обычно все функции в конкретном файле .C принимают некоторую (1) конкретную структуру * и работают на этом ... у меня, как правило, есть foo * foo_create () и foo_destroy (foo *) ctor's и dtors ...) ...
Важным понятием в Си, которое я не изучил у моих учителей, является:
Оператор * не означает «указатель на» (слева). Вместо этого он является оператором разыменования - точно так же, как и в правой части (да, я знаю, что это беспокоит некоторых).
Таким образом:
int *pInt
означает, что когда разыменовывается pInt, вы получаете int. Таким образом, pInt является указателем на int. Или по-другому: * pInt - это int - разыменованный pInt - это int; В этом случае pInt должен быть указателем на int (иначе мы не получили бы int, когда он разыменовывался).
Это означает, что нет необходимости изучать более сложные объявления наизусть:
const char *pChar
* pChar имеет тип const char. Таким образом, pChar является указателем на const char.
char *const pChar
* const pChar имеет тип char. Таким образом, const pChar является указателем на char (сам pChar является константой).
const char * const pChar
* const pChar имеет тип const char. Таким образом, const pChar является указателем на const char (сам pChar является константой).
Правила целочисленных рекламных акций; представление пустых указателей; выравнивание; последовательности точек; какие-то интересные оптимизации, которые разрешено делать компилятору; что не определено, не определено и определена реализация - и что это значит. Хорошие практики также важны, и, к сожалению, некоторые профессиональные руководящие принципы кодирования содержат некоторые действительно глупые вещи. Например: сделайте if (foo) free(foo);
вместо free(foo);
, когда foo
может быть NULL
, в то время как правильный совет будет совершенно противоположным: делайте free(foo)
и никогда if (foo) free(foo);
Я также официально устал от дерьмового многопоточный код, поэтому, пожалуйста, сообщите своим студентам, как правильно писать многопоточные программы (предоставив им подмножество известных и доказуемо безопасных методов и запретив им использовать что-либо еще или придумав что-то самостоятельно), или предупредите их, что это слишком сложно для них , Также скажите им, что переполнение буфера неприемлемо ни в одном контексте - и переполнение стека не допускается;)
Некоторые вещи вообще не специфичны для C, но, пожалуйста, также напомните им, каковы условия до / после, инварианты цикла, сложность ... Кроме того, некоторые фундаментальные показатели, используемые в серьезных отраслях, известны слишком редко (например, цикломатическая сложность абсолютно крайне важна, но до сих пор единственные люди, которых я знал об этом, работали над безопасностью критическое программное обеспечение или узнали о цикломатической сложности, в конечном счете, от людей, работающих над критически важным для безопасности программным обеспечением)
Возвращаясь к C: внимательно посмотрите на стандарт C99: вы найдете тонны интересных вспомогательных средств, редко известных даже хорошим программистам , Хуже всего то, что когда они принимают что-то для предоставления в течение длительного времени (и из-за плохого образования, это могут быть даже вещи, которые никогда не были правдой или не были правдой больше десятилетий), а затем им приходится сталкиваться с реальностью, когда их неправильный код вводит ошибки в реальной жизни и дыры в безопасности, они кричат на компиляторы и, вместо того чтобы сказать, что сожалеют о своей некомпетентности, пишут длинные глупые высказывания, настаивающие на том, почему поведение, которое они ложно считали используемым, является единственным, который имеет смысл. Например: арифметика переполнения целых чисел со знаком часто считается дополнением к двум (по крайней мере, если компьютер есть), когда это действительно не является обязательным и даже ложным с GCC.
Прежде чем я забуду: скажите им, чтобы они всегда компилировались с как минимум -Wall -Wextra -Werror (Я склоняюсь к добавлению -Wuninitialized -Winit-self -Wswitch-enum -Wstrict-aliasing -Wundef -Wshadow -Wpointer-arith -Wbad-function-cast -Wcast-qual -Wcast-align -Wwrite-strings -Wstrict-prototypes -Wmissing-прототипы -Wmissing-объявления -Wold-style-definition -Wredundant-decls)
Инструменты важны, поэтому я рекомендую хотя бы кое-что упомянуть о
Что касается C, я думаю, что важно подчеркнуть, что программист должен знать, что на самом деле означает «неопределенное поведение», то есть знать, что в будущем может возникнуть проблема даже если кажется, что он работает с текущей комбинацией компилятор / платформа.
Редактировать: я забыл: научить их искать и задавать правильные вопросы о SO!
Как загрузить ленту в магнитофон - я не шучу, я изучил C на ZX Spectrum, и каждая компиляция требовала загрузки компилятора с ленты.
Это были времена: D
Использование ключевого слова const
в контексте указателей:
Разница между следующими объявлениями:
A) const char* pChar // pointer to a CONSTANT char
B) char* const pChar // CONSTANT pointer to a char
C) const char* const pChar // Both
То есть с A:
const char* pChar = 'M';
*pChar = 'S'; // error: you can't modify value pointed by pChar
И с B:
char OneChar = 'M';
char AnotherChar = 'S';
char* const pChar = &OneChar;
pChar = &AnotherChar; // error: you can't modify address of pChar
Стиль отступа. Все учителя говорили, что код должен иметь отступ, но никто не давал указаний, как делать отступ. Я помню, что кодекс всех студентов был действительно беспорядочным.
Им действительно следует научиться использовать вспомогательные инструменты (то есть все, кроме компилятора).
1) Valgrind - отличный инструмент. Он феноменально прост в использовании и отлично отслеживает утечки и повреждение памяти.
Это поможет им понять модель памяти C: что это такое, что вы можете и чего не должны делать.
2 ) GDB + Emacs с gdb-many-windows. Или, на самом деле, любой другой интегрированный отладчик.
Это поможет тем, кому лень проходить код карандашом и бумагой.
На самом деле не ограничивается C; вот что, я думаю, им следует научиться:
1) Как правильно писать код: Как писать неподдерживаемый код . Читая это, я обнаружил как минимум три преступления, в которых был виновен.
Серьезно, мы пишем код для других программистов . Таким образом, для нас важнее писать ясно , чем писать умно .
Вы говорите, что ваши ученики на самом деле не программисты (они инженеры). Итак, они не должны делать хитрых вещей, они должны сосредоточиться на четком кодировании .
2) STFW. Когда я начал программировать (я начал на Паскале, затем перешел на C), я делал это, читая книги. Я провел бесчисленное количество часов, пытаясь понять, как что-то делать.
Позже я обнаружил, что все, что я должен был выяснить, уже было сделано многими другими, и по крайней мере один из них опубликовал это в Интернете.
] Ваши ученики инженеры; у них не так много времени посвящать программированию . Итак, немного времени у них есть, им следует потратить на чтение чужого кода и, возможно, на освежение идиом .
В общем, Си довольно простой язык для изучения. У них будет гораздо больше проблем с написанием чего-либо длиннее нескольких строк, чем у них будет усвоение независимых понятий.
беззнаковое и подписанное.
Операторы битового сдвига
Битовая маскировка
Битовая установка
целочисленные размеры (8 бит, 16 бит, 32 бит)
Портативность - редко изучается или упоминается в школе, но часто встречается в реальном мире.
Используйте последовательный и читаемый стиль кодирования.
(Это должно помочь вам при проверке их код тоже.)
По теме: Не оптимизируйте преждевременно. Сначала выполните профиль, чтобы увидеть узкое место.