你从老师那里学到的重要概念是什么?

9月份,我将向工程学院的学生们提供我的第一次C讲座(通常我教数学和信号处理,但我也做了很多C语言的实际工作,没有给他们讲课)。 计算机科学不是他们的主题(他们更多地研究电子和信号处理),但他们需要有良好的编程背景(其中一些可能会成为软件开发人员)

今年将是他们学习C的第二年(他们应该知道指针是什么以及如何使用它,但当然,这个概念还没有被同化)

除了经典的东西(数据结构,经典算法……),我可能会把我的一些讲座集中在:

  • 在用C编码之前设计算法(并用伪代码编写)(在编码之前考虑)
  • 使您的代码可读(注释,变量名称,……)和
  • 指针,指针,指针! (它是什么,如何以及何时使用它,内存分配等……)

根据您的经验,您的老师从未教过您的C中最重要的概念是什么? 我应该关注哪个特定点?

例如,我应该将它们介绍给一些工具( lint ,…)?

在指针上下文中使用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 

我的老师花了很多时间教我们,指针是可怕的小goobers,如果不正确使用可能会导致很多问题,他们从不打扰向我们展示他们真正有多强大。

例如,指针算法的概念对我来说是陌生的,直到我已经使用C ++几年:

例子:

  • c [0]相当于* c
  • c [1]相当于*(c + 1)
  • 循环迭代:for(char * c = str; * c!=’\ 0′; c ++)
  • 等等…

而不是让学生害怕使用指针,教他们如何适当地使用它们。

编辑:由于注释引起了我的注意,我只是读了一个不同的答案,我认为在讨论指针和数组之间的微妙差异(以及如何将两者结合在一起以促进一些相当复杂的结构)时也有一些价值,以及如何在指针声明方面正确使用const关键字。

他们真的应该学习使用辅助工具(即编译器以外的任何东西)。

1) Valgrind是一个很好的工具。 它非常容易使用,它可以完美地跟踪内存泄漏和内存损坏。

它将帮助他们理解C的记忆模型:它是什么,你能做什么,以及你不应该做什么。

2) GDB + Emacs与gdb-many-windows。 或者任何其他集成调试器,真的。

它会帮助那些懒惰的人用铅笔和纸张逐步完成代码。


不仅限于C; 这是我认为应该学习的内容:

1)如何正确编写代码: 如何编写不可维护的代码 。 读到这一点,我发现至少有三项犯罪我是犯了罪。

说真的, 我们为其他程序员编写代码 。 因此,对我们来说, 写清楚写聪明更重要。

你说你的学生实际上不是程序员(他们是工程师)。 所以, 他们不应该做一些棘手的事情,他们应该专注于清晰的编码

2)STFW。 当我开始编程时(我开始使用Pascal,而不是转到C语言),我通过阅读书籍来完成。 我花了无数个小时试图找出如何做的事情。

后来,我发现我必须弄清楚的一切都已经被其他许多人完成了,至少其中一个人已经在网上发布了。

你的学生是工程师; 他们没有那么多时间投入编程 。 所以,他们有这么短的时间,他们应该花时间阅读其他人的代码 ,或许,可能会习惯于成语


总而言之,C语言非常容易学习。 他们在编写任何超过几行的东西时会遇到很多麻烦,而不是学习独立的概念。

当我不得不使用C作为学校中较大项目的一部分时,能够正确使用gdb(即根本不能)最终预测谁将完成他们的项目,谁不会。 是的,如果事情变得疯狂,你有大量的指针和内存相关的错误gdb将显示奇怪的信息,但即使知道这可以指向人们正确的方向。

还提醒他们C不是C ++,Java,C#等是个好主意。 当你看到有人像C ++中的字符串一样处理char *时,这种情况最常出现。

unsigned vs signed。

位移运算符

位屏蔽

位设置

整数大小(8位,16位,32位)

面向对象:

 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 }; 

( 代码来源 )

便携性 – 很少在学校教授或提及,但在现实世界中出现很多。

宏的(危险的)副作用。

使用valgrind

工具很重要,所以我建议至少提一下

  • Makefile以及构建过程的工作原理
  • GDB
  • 皮棉
  • 编译器警告的用处

关于C,我认为重要的是要强调程序员应该知道“未定义的行为”到底意味着什么,即知道即使它似乎与当前的编译器/平台组合一起工作也可能存在未来的问题。

编辑:我忘了:教他们如何搜索并在SO上提出正确的问题!

使用一致且可读的编码风格。

(这也可以帮助您查看他们的代码。)

相关:不要过早优化。 首先了解瓶颈的位置。

知道当你递增一个指针时,新地址取决于该指针所指向的数据的大小……(IE,增加的char *和unsigned long *之间的区别是什么)…

首先要确切了解分段故障究竟是什么,以及如何处理它们。

知道如何使用GDB很棒。 知道如何使用valgrind很棒。

开发C编程风格……例如,当我编写大型C程序时,我倾向于编写相当面向对象的代码(通常,特定.C文件中的所有函数都接受一些(1)特定的struct *并对其进行操作。 ..我倾向于有foo * foo_create()和foo_destroy(foo *)ctor和dtors …)…

理解链接器。 使用C的任何人都应该理解为什么“static int x;” 在文件范围不会创建全局变量。 在学习C的早期阶段,编写一个简单的程序,其中每个函数都在自己的翻译单元中并分别编译每个函数,这种做法的运作不够频繁。

始终有效警告。 使用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";

  • 破坏的内存可以触发各种奇怪的错误。
  • 调试器可能骗你。

我认为整体想法似乎非常好。 这些是一些额外的东西。

  1. 调试器是一个好朋友。
  2. 检查边界。
  3. 确保指针在使用之前实际指向某个东西。
  4. 内存管理。

希望之前没有发布过(只是非常快速地阅读),但我认为当你必须使用C时,非常重要的是了解数据的机器表示。 例如:IEEE 754浮点数,大与小端,结构对齐(这里:Windows vs Linux)……为了实现这一点,制作一些有点谜题(解决一些问题而不使用任何function)非常有用然后printf打印结果,有限数量的变量和一些逻辑运算符)。 此外,了解链接器如何工作,整个编译过程如何工作等基本知识通常很有用。但特别是理解链接器(没有它,很难找到某种错误…)

这本书帮助我提高了我的C和C ++技能: http : //www.amazon.com/Computer-Systems-Programmers-Randal-Bryant/dp/013034074X

我认为对计算机体系结构的深入了解会使好的和坏的C程序员之间产生差异(或者至少它是一个重要的因素)。

教他们unit testing。

一般最佳实践如何?

  • 总是假设其他人已经编写了您的代码,并且它们都可以在互联网上免费获得,并且比您在截止日期之前生成的任何内容更好地编写和测试。
  • 提前退货/避免其他条款
  • 初始化所有变量
  • 每个function一页作为指导(即一起使用较小的代码段)
  • 何时使用switch,if-else if或hash表
  • 避免全局变量
  • 始终检查您的输入和输出(我不相信我自己的代码。)
  • 大多数function应该返回状态

    [致其他人:随意编辑此内容并添加到列表中]

关于检查输入:

我曾经匆忙写了一个大程序,并且在我的函数中编写了各种Guard Clauses,输入检查。 当我第一次运行程序时,那些快速流动的子句中的错误甚至无法读取它们,但是程序没有崩溃并且可以干净地关闭。 然后,这是一个简单的问题,通过列表和修复错误快速的错误。

将Guard子句视为运行时编译器警告和错误。

我不认为你应该是教学工具。 那应该留给Java老师。 它们很有用并且被广泛使用但与C无关。调试器与它们希望获得的访问权限一样多。 很多时候你得到的是printf和/或闪烁的LED。

教他们指针,但教他们好,告诉他们他们是一个整数变量代表在记忆中的位置(在大多数课程中,他们也有一些组装训练,即使它是为一些想象的机器,所以他们应该能够理解)和不是一个星号前缀变量,它以某种方式指向某个东西,有时变成一个数组(C不是Java)。 教他们C数组只是指针+索引。

让他们编写会溢出和段错误的程序,然后确保他们理解为什么会发生这种情况。

标准库也是C,让他们使用它并让他们的程序在私人测试中痛苦地死,因为使用了gets()和strcpy()或者双重释放了某些东西。

强制他们处理不同类型的变量,endianness(你的测试可以在不同的arch中运行),float to int conversion。 让他们使用掩码和按位运算符。

即教他们C.

我得到的是C语言中的一些批处理,也可以在GW-BASIC中完成。

这个关键字在C: volatile

  1. 检查边界
  2. 检查边界,

    而且当然,

  3. 检查边界。

如果您忘记了其中一条规则,请使用Valgrind。 这适用于数组,字符串和指针,但实际上很容易忘记在执行分配和内存aritmethics时你真正在做什么。

  • 语言结束和实现开始的地方:例如,stdio.h是标准库的一部分,conio.h不是,类似的东西;
  • 未定义和实现定义的行为之间的区别,以及为什么像x = x ++这样的东西是未定义的;
  • 仅仅因为它编译并不意味着它是正确的;
  • 优先级和评估顺序之间的差异,以及为什么a * b + c不能保证a将在b或c之前进行评估;
  • “它适用于我的机器”并不能胜过语言标准指定的行为:例如,因为void main()或x = x ++为您提供了您对特定平台所期望的结果,并且编译器并不意味着它可以使用;
  • 假装你从未听说过gets();

鉴于它们的背景,可能很好地关注嵌入式系统的C,包括:

  • 静态分析工具(例如PC-Lint )
  • MISRA-C 。
  • 暴露于多个处理器(例如PIC,STM32)和编译器
  • 如何调试。
  • 实时问题,包括中断,去抖动信号,简单调度/ RTOS。
  • 软件设计。

而且非常重要: 版本控制软件 。 我在工业界工作并且虔诚地使用它,但我很惊讶它在我的学位课程中从未被提及过!

调试器是你的朋友。 C是一种易于理解的语言,理解错误的最佳方法通常是在调试器下查看它们。

如果学生在某些时候接触到可以帮助他们编写更清晰,更好的代码的工具,那将是有益的。 在这个阶段,这些工具可能并非都与它们相关,但了解可用的工具会有所帮助。

  • 调试器 – gdb,totalview,…
  • 静态/动态分析仪 – 夹板 , valgrind ,……
  • unit testing框架 – CUnit , cmockery , Check ,…
  • 文档管理 – Doxygen ,……
  • 构建管理 – 制作, 记忆 , 制作 ……
  • 代码指标 – CCCC , 了解 , SLOCCount ,……
  • 代码覆盖率 – gcov,LCOV,……

还应强调使用具有严格编译器警告标志的不同(!)编译器并注意每个警告消息。

有太多的名字都没有。 其中一些是C特定的; 其中一些是一般的最佳实践类型的东西。

  • 学习使用可用的工具
    • 修订控制系统。 每次工作时,请检查。
    • 差异工具:diff,rdiff,meld,kdiff3等。特别是与RCS结合使用。
    • 编译器选项。 -Wextra -Wall __attribute __((aligned(8))),如何打包结构。
    • make:生成调试和生产版本
    • 调试器:如何获取和解释堆栈跟踪。 如何设置断点。 如何单步执行代码。
    • 编辑:在编辑器中编译。 打开多个窗口,Mx标签 – 查询 – 替换(我的emacs根显示?)等。
    • cscope,kscope,[ce]标签或其他源浏览工具
  • 防守计划。 在-DDEBUG中断言(foo!= NULL); 擦除用户输入。
  • 检测到错误时暂停和捕获火灾。 在检测到问题后核心转储2行时,调试会更容易。
  • 使用-Wextra和-Wall保持0警告编译。
  • 不要将所有内容放入1个巨大的鸣笛.c文件中。
  • 测试。 测试。 并测试一些。 并与您的来源一起检查这些测试。 因为教师可能会回来并在转入要求后更改要求。

浏览整个编程生命周期,包括完成后代码会发生什么

  • 预先计划阶段,以及如何查找可用于减少原始代码量的现有项目/现有代码
  • 许可证的小(基本)概述以及外部代码如何影响您可以使用和不能使用的许可证(以及许可中的其他注意事项)
  • 并发版本控制和版本控制。 我会做SVN / Git,但对每个人都有。 如果你现在把它们介绍给他们而不是在工作中学习,你将会节省很多时间。
  • 向他们展示开源代码(Google Code,Github等)以及何时/如何判断它是否合适的途径。

这些都不是特定于C的,但我添加它是因为我个人刚刚在我的大学通过了“C for Electrical Engineers”,这就是我必须自己找到的所有内容。

我从老师那里学到的一个重要概念是:

运算符*并不意味着“指向”(在左侧)。 它取而代之的是解除引用操作符 – 就像它在右侧一样(是的,我知道这对某些人来说是令人不安的)。

从而:

 int *pInt 

意味着当pInt被解除引用时,你得到一个int。 因此pInt是指向int的指针。 或者换一种说法:* pInt是一个int – dereferenced 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本身是常量)。

缩进风格。 所有老师都说代码必须缩进,但没有人真正指出如何缩进。 我记得所有学生的代码都是一团糟。