9月份,我将向工程学院的学生们提供C语言的第一次讲座(通常我会教数学和信号处理,但我也做了很多C语言的实际工作,没有给他们讲课)。计算机科学不是他们的主题(他们更多地研究电子和信号处理),但他们需要有一个良好的编程背景(其中一些可能会成为软件开发人员)
今年将是他们学习C的第二年(他们应该知道指针是什么以及如何使用它,但当然,这个概念尚未被同化)
除了经典的东西(数据结构,经典算法......)之外,我可能会把我的一些课程重点放在:
根据您的经验,您的老师从未教过您的C中最重要的概念是什么?我应该关注哪个特定点?
例如,我应该将它们介绍给某些工具(lint
,...)?
答案 0 :(得分:34)
在指针上下文中使用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 :(得分:32)
我的老师花了很多时间教我们,指针是可怕的小goobers,如果不正确使用可能会导致很多问题,他们从不打扰向我们展示他们真正有多强大。
例如,指针算法的概念对我来说很陌生,直到我已经使用C ++几年:
示例:
不要让学生害怕使用指针,而是教他们如何恰当地使用它们。
编辑:由于注释引起了我的注意,我刚刚读了一个不同的答案,我认为在讨论指针和数组之间的细微差别(以及如何放置)时也有一些价值。两个一起促进一些相当复杂的结构),以及如何在指针声明方面正确使用const
关键字。
答案 2 :(得分:19)
他们真的应该学习使用辅助工具(即编译器以外的任何工具)。
1)Valgrind是一个很好的工具。它非常容易使用,它可以很好地跟踪内存泄漏和内存损坏。
它将帮助他们理解C的记忆模型:它是什么,你能做什么,你不应该做什么。
2)GDB + Emacs与gdb-many-windows。或任何其他集成调试器,真的。
它会帮助那些懒惰的人用铅笔和纸张逐步完成代码。
不仅限于C;这是我认为他们应该学习的东西:
1)如何正确编写代码:How to write unmaintainable code。读到这一点,我发现至少有三项罪行我犯了罪。
说真的,我们为其他程序员编写代码。因此,对于我们而言,清楚地写比对巧妙地写更为重要。
你说你的学生实际上不是程序员(他们是工程师)。所以,他们不应该做一些棘手的事情,他们应该专注于清晰的编码。
2)STFW。当我开始编程时(我开始使用Pascal,而不是转到C语言),我通过阅读书籍来做到这一点。我花了无数个小时试图弄清楚如何做事。
后来,我发现我必须弄清楚的所有内容已经被许多其他人完成了,至少有一个人已经在网上发布了它。
你的学生是工程师; 他们没有太多时间投入编程。所以,他们有很少的时间,他们应该花费阅读其他人的代码,也许,刷上习语。
总而言之,C语言非常容易学习。他们在编写任何超过几行的东西时会遇到很多麻烦,而不是学习独立的概念。
答案 3 :(得分:13)
当我不得不使用C作为学校中较大项目的一部分时,能够正确使用gdb(即根本不能),最终预测谁将完成他们的项目,谁不会。是的,如果事情变得疯狂,你有大量的指针和内存相关的错误gdb将显示奇怪的信息,但即使知道这可以指向人们正确的方向。
同时提醒他们C不是C ++,Java,C#等是个好主意。当你看到有人像C ++中的字符串一样处理char *时,这种情况最常出现。
答案 4 :(得分:13)
unsigned vs signed。
位移操作符
比特屏蔽
位设置
整数大小(8位,16位,32位)
答案 5 :(得分:11)
面向对象:
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
};
答案 6 :(得分:11)
便携性 - 很少在学校教授或提及,但在现实世界中出现很多。
答案 7 :(得分:10)
宏的(危险)副作用。
答案 8 :(得分:9)
工具很重要,所以我建议至少提一下
关于C,我认为重要的是要强调程序员应该知道“未定义的行为”到底意味着什么,即知道即使它似乎与当前的编译器/平台组合一起使用也可能存在未来的问题。 / p>
编辑:我忘记了:教他们如何搜索并在SO上提出正确的问题!
答案 9 :(得分:9)
使用valgrind
答案 10 :(得分:8)
始终有效警告。使用GCC,至少使用-Wall -Wextra -Wstrict-prototypes -Wwrite-strings
。
I / O很难。 scanf()
是邪恶的。 gets()
永远不会使用。
当您打印的内容不是'\n'
时,如果您想立即打印,则必须刷新stdout
,例如
printf("Type something: ");
fflush(stdout);
getchar();
尽可能使用const
指针。例如。 void foo(const char* p);
。
使用size_t
存储尺寸。
通常无法修改Litteral字符串,因此请将它们const
。例如。 const char* p = "whatever";
。
答案 11 :(得分:8)
了解链接器。任何使用C的人都应该理解为什么“static int x;”在文件范围不会创建全局变量。在学习C的早期阶段,编写一个简单的程序,其中每个函数都在自己的翻译单元中并单独编译每个函数的工作都不够频繁。
答案 12 :(得分:8)
使用一致且可读的编码风格。
(这也有助于您查看他们的代码。)
相关:不要过早优化。首先了解瓶颈的位置。
答案 13 :(得分:8)
知道当你递增一个指针时,新地址取决于该指针指向的数据的大小...(IE,char *递增和unsigned long *之间的区别是什么)...
首先确切了解分段故障究竟是什么,以及如何处理它们。
知道如何使用GDB很棒。知道如何使用valgrind很棒。
开发C编程风格......例如,当我编写大型C程序时,我倾向于编写相当面向对象的代码(通常,特定.C文件中的所有函数都接受某些(1)特定结构*并运行在它...我倾向于有foo * foo_create()和foo_destroy(foo *)ctor和dtors ...)...
答案 14 :(得分:6)
答案 15 :(得分:5)
我认为你不应该是教学工具。那应该留给Java老师。 它们很有用并且被广泛使用但与C无关。调试器与它们希望获得的访问权限一样多。很多时候你得到的是printf和/或闪烁的LED。
教他们指针,但教他们好,告诉他们他们是一个表示记忆中位置的整数变量(在大多数课程中他们也有一些组装训练,即使它是为某些想象的机器所以他们应该能够理解并且不是一个星号前缀变量,它以某种方式指向某个东西,有时变成一个数组(C不是Java)。教他们C数组只是指针+索引。
让他们编写会溢出和段错误的程序,然后确保他们理解为什么会这样。
标准库也是C,让他们使用它并让他们的程序在私人测试中痛苦地因为使用了gets()和strcpy()或者双重释放了某些东西。
强制他们处理不同类型的变量,endianness(你的测试可以在不同的arch中运行),float to int conversion。让它们使用掩码和按位运算符。
即。教他们C.
我得到的是C语言中的一些批处理,这些处理也可以在GW-BASIC中完成。
答案 16 :(得分:5)
我认为整体想法似乎非常好。这些是额外的东西。
答案 17 :(得分:5)
希望之前没有发布过(只是非常快速地阅读),但我认为当你必须使用C时,非常重要的是了解数据的机器表示。例如:IEEE 754浮点数,大与小端,结构对齐(这里:Windows vs Linux)...... 为了实现这一点,做一些比特难题是非常有用的(解决一些问题而不使用任何功能,然后printf打印结果,有限数量的变量和一些逻辑运算符)。 此外,了解链接器如何工作,整个编译过程如何工作等基本知识也很有用。但特别要理解链接器(没有它,很难找到某种错误...)
这本书帮助我提高了我的C和C ++技能:http://www.amazon.com/Computer-Systems-Programmers-Randal-Bryant/dp/013034074X
我认为对计算机体系结构的深入了解会使好的和坏的C程序员之间产生差异(或者至少它是一个重要的因素)。
答案 18 :(得分:5)
教他们单元测试。
答案 19 :(得分:5)
一般最佳实践如何?
大多数功能都应返回状态
[致其他人:随时编辑此内容并添加到列表中]
关于检查输入:
我曾经匆忙写了一个大程序,并且在我的函数中编写了各种Guard Clauses,输入检查。当我第一次运行该程序时,那些快速流动的子句中的错误甚至无法读取它们,但程序没有崩溃,可以干净地关闭。这是一个简单的问题,通过列表和修复错误快速的错误。
将Guard子句视为运行时编译器警告和错误。
答案 20 :(得分:4)
C中的此关键字:volatile
答案 21 :(得分:3)
检查边界,
当然,
检查边界。
如果您忘记了其中一条规则,请使用Valgrind。这适用于数组,字符串和指针,但实际上很容易忘记在执行分配和内存aritmethics时你真正在做什么。
答案 22 :(得分:3)
鉴于他们的背景,可能很好地关注嵌入式系统的C,包括:
非常重要:版本控制软件。我在工业界工作并且虔诚地使用它,但我很惊讶它在我的学位课程中从未被提及过!
答案 23 :(得分:3)
答案 24 :(得分:2)
缩进风格。所有老师都说代码必须缩进,但没有人真正指出如何缩进。我记得所有学生的代码真的很乱。
答案 25 :(得分:2)
答案 26 :(得分:2)
调试器是你的朋友。 C是一种易于理解的语言,理解错误的最佳方法通常是在调试器下查看它们。
答案 27 :(得分:2)
C中的一个重要概念,我没有向我学习 老师是:
运算符*并不意味着“指向”(在左侧 侧)。它取而代之的是解引用运算符 - 完全如此 它在右侧(是的,我知道这是令人不安的 某些人。)
因此:
int *pInt
表示当取消引用pInt时,你得到一个int。从而 pInt是指向int的指针。或者换一种说法:* pInt是一个 int - dereferenced pInt是一个int;然后必须是一个 指向int的指针(否则我们不会得到一个int。) 解除引用的)。
这意味着没有必要学习更复杂的内容 心声明:
const char *pChar
* pChar的类型为const char。因此pChar是一个指针 到const char。
char *const pChar
* const pChar是char类型。因此const pChar是一个指针 to char(pChar本身是不变的)。
const char * const pChar
* const pChar的类型为const char。因此const pChar是一个 指向const char的指针(pChar本身是常量)。
答案 28 :(得分:2)
有太多的名字都没有。其中一些是C特定的;其中一些是一般的最佳实践类型。
答案 29 :(得分:2)
如果学生在某些时候接触到可以帮助他们编写更清晰,更好的代码的工具,那将是有益的。在这个阶段,这些工具可能并不全都与它们相关,但了解可用的工具会有所帮助。
还应强调使用具有严格编译器警告标志的不同(!)编译器并注意每个警告消息。
答案 30 :(得分:2)
浏览整个编程生命周期,包括代码完成后会发生什么。
这些都不是特定于C的,但我添加它是因为我个人刚刚在我的大学通过了“C for Electrical Engineers”,这就是我必须自己找到的所有内容。
答案 31 :(得分:1)
我很高兴地说我几乎教过这里提到的所有其他内容(包括单元测试和C中的OOP模式,真的!)。
答案 32 :(得分:1)
我希望我的教授们教会我们如何使用调试器。相反,我通过printf试图找出问题来检测我的代码。发现gdb就像打开灯泡一样。能够使用核心转储调试崩溃特别有用,因为很多新的C编程错误通常来自错误的指针逻辑。
如今单元测试可能是一种很好的教学方法。
答案 33 :(得分:1)
答案 34 :(得分:1)
#pragma
指令可用于向处理器发出其他详细信息。我使用C语言研究TI处理器,这对我定义内存段有很大帮助。
同样'__FILE__' & '__LINE__'
预定义的宏在调试/日志时非常有用,但我从来不知道这一点。应该告诉学生这类事情。
答案 35 :(得分:1)
虽然没有直接与C绑在一起,但我想学习 关于使用ASSERT提前发现错误的技术 (例如,在覆盖引起的一些奇怪错误之前很久 记忆)。相反,我在几年后独立发现了它 后来。这种技术已经捕获了许多错误(包括 否则会有一些非常微妙的东西 被忽视。
一般来说,只要有一些假设,就可以添加断言 关于程序中的值,例如它永远不会消极或 零或大于其他变量。
E.g:
assert(pInt)
如果假设pInt将指向合理的数据。将 触发空指针。经常用于指针传递给 功能
或者
assert(pInt < pMax)
其中pMax指向pInt正在运行的整数数组的末尾。
或者
断言(yMass&gt; 57.90)
(其中yMass是肽的单个带电y离子的质量)
答案 36 :(得分:1)
我的讲师偶尔会谈论性能,但从未提及与其他操作相比的分支成本,直到后来我研究微处理器才明白这一点。很多时候,当通过一些按位操作解决同样的问题时,我们会做出不必要的分支,例如在字母表中找到一个字母的位置:
if (islower(letter)) {
pos = letter - 'a' + 1;
} else if (isupper(letter)) {
pos = letter - 'A' + 1;
}
VS
pos = letter & 31;
当然,ascii的设计考虑到了这种情况,所以它并不像向我们展示这会教我们'糟糕的风格'或某种'神奇的黑客'......我现在发现自己每天使用按位技巧以避免分支。
- 我的2c值得
答案 37 :(得分:1)
数组是一个指针 *(取消引用)和&amp;之间的差异(地址)运营商 什么时候使用
并强调最近C的最佳(也是唯一)真实的地方是嵌入式系统和实时应用程序,其中资源稀缺且运行时间是一个因素。
在我使用嵌入式微处理器系统类之前,我并不真正欣赏C作为一种语言,我们通过阅读Motorolla Dragonball板手册中的程序员指南来实现硬件。因此,如果它完全可能(这可能很难,因为你需要获得廉价的硬件)尝试让它们在类似的项目上工作(实现UART和中断向量表等)......
因为虽然像字符串处理,排序等东西是玩具经典学校的问题,但它们实际上已经不那么有用了,并且让那些知道有更简单方法的学生感到沮丧。这对&amp; and更有价值。一个带位掩码的字节,看着LED亮起。
哦,我从来没有学过如何在学校使用像gcc这样的东西,或者在makefile上实际使用的东西。务实的程序员说这是一件好事。
答案 38 :(得分:1)
除了显而易见的指针之外,我在学习C时发现没有人谈论逗号。
a= 1, b= 2;
当然你在for(;;){}语句中使用它,但是没有人理解为什么,而且我从未见过任何其他人在语句之外使用它。
但是C
对待逗号与半冒号不同。例如:
"if (a) b = a, c = a;"
与
相同"if (a) { b = a; c= a; }"
与
不同"if (a) b = a; c = a;
现在,我并不是说带逗号的第一个表单更好,因为它会让不熟悉的程序员绊倒,而且很难看出你是否使用非常小的字体,但是那里是你可能遇到这种代码的时候,很高兴知道这种语言实际上做了什么。
另外,我发现如果我在函数顶部有很多初始化,
a = 1,
b = 2,
i1 = 0,
i2 = 0,
i3 = 0,
i4 = 0,
dtmp = 0.0,
p = strtmp;
让所有这些赋值用逗号分隔,使它们成为一个语句,然后让我在调试器中“步入”一步,而不是八步(或更多)。是的,现代的gui设定了一个断点并且减少了过去的痛苦,但是单个动作(步骤)仍然很难被击败。
答案 39 :(得分:1)
我在嵌入式编程中使用C89并调试硬件是噩梦。我们有一些编码惯例可以保存我们的理智:
E.g:
#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是一个调试宏,可以中止运行时。
答案 40 :(得分:1)
我希望看到更多编程教授教授的一件事是关于源代码控制。任何VCS上的一天:为什么你使用它,一些简单的操作,版本编号等。
有太多的毕业生认为源控制是一个外国概念......他们是EE或CS专业并不重要,如果他们正在编写代码,他们应该对版本控制系统有所了解。
答案 41 :(得分:1)
将所有宏参数包装在括号中。
如果宏是一个比赋值或函数调用更复杂的语句,那么将其包装起来:
#define M(A) do { ... (A) ... } while (0)
答案 42 :(得分:1)
整数促销规则; NULL指针的表示;对准;序列点;允许编译器进行某种有趣的优化;什么是未指定的,未定义的和实现定义的 - 以及它的含义。好的做法也很重要,一些专业的编码指南包含一些非常愚蠢的东西,这是一种耻辱。例如:当if (foo) free(foo);
可以是free(foo);
时,请foo
而不是NULL
,而正确的建议恰恰相反:做free(foo)
而不是{{1我也正式厌倦了糟糕的多线程代码,所以请告诉你的学生如何正确编写多线程程序(给他们一些已知和可证明安全技术的子集,并禁止他们使用其他任何东西或发明他们自己的东西)或警告他们这对他们来说太复杂了。还要告诉他们在任何上下文中都不能接受缓冲区溢出 - 堆栈溢出也不是;)
有些事情根本不是C特定的,但请同时提醒他们前/后条件是什么,循环不变量,复杂性......在严肃的行业中使用的一些基本指标很少被人知道(例如,圈复杂度< em>绝对至关重要,但到目前为止,我遇到过的唯一知道它的人已经开始研究安全关键软件,或者最终从安全关键软件开发人员那里学到了复杂的环境复杂性。
回到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-prototypes -Wmissing-declarations -Wold-style-definition -Wredundant-decls)
答案 43 :(得分:1)
永远不要相信编译器。通常存在问题是正确的,但除了最微不足道的错误之外,关于问题是什么以及问题在哪里几乎总是错误的。
注意:我没有说忽略编译器。我说不相信它。它知道存在问题,但它究竟是什么问题经常是错误的。以面值获取编译器输出是令人沮丧和浪费时间的方法。 特别是表示复杂错误。
答案 44 :(得分:0)
使用结构和函数指针模拟对象
答案 45 :(得分:0)
单独的分号是NOP操作:
if(cond0) { /*...*/ }
else if(cond1) ; //is correct and does nothing
else { /*...*/}
逗号操作员:
a = (++i, k); //eq: ++i; a = k;
答案 46 :(得分:0)
if(constant=variable)
{
work();
}
答案 47 :(得分:0)
简单的调试工具printf()。如果您没有任何调试工具!!
答案 48 :(得分:0)
指针只是用于存储地址的数据类型,就像int是用于存储整数的数据类型一样。当我同化它时,关于指针和指针算术的所有内容都已落实到位。
答案 49 :(得分:0)
初始化指针,否则将是未定义的指针 将使程序立即崩溃的值 解除引用(而不是覆盖某些内存中的内存) 任意位置)。
这将在大多数32位系统上按预期工作:
int * pInt =(int *)0xDEADBEEF
我不确定在64位系统上有什么好处。
答案 50 :(得分:0)
执行顺序和序列点的概念非常有用,并且没有太多讨论。
知道 x = x ++; 调用未定义的行为很有用。知道为什么它可以更有教育意义。
鉴于您的受众,对“volatile”的一些讨论可能是有用的,以及与硬件接口的其他概念。如何处理只写寄存器,那种事情。
答案 51 :(得分:0)
良好的可移植编码概念,编程模型(例如ILP32与LP64),并将它们暴露给不同的C编译器和工具链(并非所有世界都使用GCC)。
答案 52 :(得分:0)
如何将磁带加载到磁带播放器中 - 我不是在开玩笑,我在ZX Spectrum上学习了C,每次编译都需要从磁带加载编译器。
那些时候:D
答案 53 :(得分:0)
编译器并不总是正确的。特别是在开发嵌入式系统时。
答案 54 :(得分:0)
C宏中的卫生名称:
#define SOME_MACRO(_x) do { \ int *x = (_x); \ ... \ } while(0)
在宏内部以这种方式定义x是危险的,因为(_x)也可以扩展为x,最后得到:
do { int *x = x; ... } while(0)
可能没有得到编译器的任何警告,但实际上用垃圾初始化你的x指针(而不是外部作用域的阴影x)。
使用您知道的名称对于该宏是唯一的非常重要。 C预处理器没有自动执行此操作的机制,因此您只需为宏定义的变量选择丑陋的名称,或者为了这些目的而避免使用它们。