之前说准备写个自己入门C语言的教程,顺便记录心得,总结,结果就列了一个提纲,没有时间来凑文字,先放这。有时间慢慢来写写。
C我学的很早,也用了很多年(08开始的),但是仅仅是自己使用到的部分用过而已,博大精深的c,c++还会继续学习。
一。序言,铺垫,准备
- 为何有这个笔记教程(P1.1)
- 有效的学习方法:为用而学,带着疑问而学,寻找答案而学,最有效(P1.2)
- C产生的年代(P1.3)
- C主要用于写什么样的程序
- 实际工作中,我们用C做什么
- 学习C用什么工具IDE,实际工程中使用什么工具
二。计算机基本组成与C的关系
- 基本模块
- 运行原理
- C编译到运行的流程
- 初级知识和烧脑的知识
- 内存的样子
三。C代码最基本的文件形式
- 简单程序,写在一个文件里面,扩展名为.c。则变量,函数声明放在最上部main函数之前。
- 声明写在.h头文件中,实现的代码写在.c文件中。方便第三方开发时引用头文件
Main函数,程序入口
- 有且只有一个
- 接收命令行参数
- 结束时,程序就退出了。
- 基于状态的检测并持续执行内部有个while
代码书写要求,基本规范
注释:介绍,解释程序代码的作用,作者,日期等
- 过去,强调函数说明,参数说明。写上作者,日期,现在针对改动的细节,应该写上详细的原因,日期。如何实现的。这点显得很重要了。
标识符:为变量,函数等命名的问题
基本数据类型:规定:它里面放的值是什么意义,值的范围,怎么解释它,它长度是多少字节
- 类型和范围
- 不同操作性系统对int的定义不同,导致了变量长度依赖,如何解耦这个依赖呢? _int2 _int4 _int8或者用define定义自己的类型
- 涉及变量长度敏感,比如文件格式,单片机存储,特殊协议时,要特别留意变量长度在不同编译器版本的适配
基本的控制台输出printf: 不仅输出显示给操作者看,也从CGI中输出到web服务器实现c写网页
常量与字面量,左值和右值
- 常用是一个字面量或者说直接就是值的代称,它看起来像变量那样,其实是为了方便使用引用它,编译后它已经被后面的字面量直接替代进到程序内部
- 常量就是固定的了,不改变它。用常量是为了不产生那么多无法解释的”魔数“,不会产生莫名其妙的紊乱。比如id为5还是单价为5.
- 字面量是实际的值,比如5,“abc”,’A’
- 常量,常数,字面量,他们都是一经定义不再改变。
四。变量:数据临时存储位置
- 主要变量类型就是两种
- 简单数字:以多个字节很单纯地来存储能表示不同范围大小的数字
- 复杂数字:存储数字和指数,比如浮点数。它实际上是一个结构,不同bit是不同意义。
- 字串:理解为hello为5个字母的排列
- 声明变量,是否赋初始值,运算中赋值
- 什么时间给初始值,什么时间不用给初始值
- 小心逻辑运算时给变量赋值造成bug
- 结构:基本变量组合起来表示一个部件的多种特性
- 联合:一块空间有两个名字可以读写但他们相互覆盖
- 一般编程我们用不着,我们没有了节省1K字节的习惯。
- 特殊情况用得上,比如要节省这几个字节,或者对于文件格式来说,没必要造成冗余的字节。文件越短越精炼越好。
- 静态的变量还是动态的变量。加载时是否存在而定
- 字面量被编译到代码一起,变量预留一个固定空间,你可以改变它,改变程序走向,也可以加密它,甚至动态解释运行它。
全局变量与局部变量
- 变量的作用域
- 声明在什么位置
- 其他文件对变量的引用
- 存储类别
- 自动(auto)、
- 静态(static)、
- 寄存器的(register)
- 外部的(extern)。
数据类型转换
- 自动转换
- 强制类型转换
运算符
- 算术运算符
- 自增自减
- 变量赋值
- 比较运算符
- 运算的结果是布尔值,true或者false。 c里面是0和非0
- 逻辑运算符
- 什么是与或非,异或
- 电子学的逻辑与或非可以化简,但是编程中我们按人能理解的逻辑来写
- 逻辑的短路算法OR找到一个就满足
- 学过电阻的串并联吗?没学过就想开关的串并联。我们必须把它归纳为是串模型还是并模型,才好编程。
- 三目运算?:可以进行一个二值判定带入表达式,简化版的if else。
- 运算符优先顺序
- 我们有必要使用小括号明确表示运算先后顺序,不依赖于优先顺序,因为表达式太复杂我们无法一眼望见。
- 不要采用非常费解的写法。看似高超,实则费事。
流程控制,程序分支
- 世上最简单逻辑判断,是怎么样,否则怎么样 IF ELSE
- 想用最简单的IF判断但是一次又判断不完全IF ELSEIF ELSEIF ELSE
- 逻辑函数的值跟二进制一样是二值的
- 专业逻辑写法
- 举个逐渐复杂的逻辑例子
- 针对具体值的判断用if写就很烦琐采用switch case
- 单个字符也可以用switch case
- 多个字符串也想用switch case怎么做呢
- 多个值同时执行相同一段代码
- 都判断完了,万一有个漏网之鱼怎么办用default包含
循环:要遍历数组就引出了
- for循环,知道起止次数
- for循环王牌循环模式
- while 不明确次数,根据条件判定结束
- do while也可以改写成while形式
- 只知道要执行什么和什么条件结束,没有明确的次数 用while
- 跳过后面的代码不执行continue
- 从while中终止
- 从循环中直接转到下一次循环用continue
- 提前跳出循环 *循环的嵌套
- 如果要访问二维数组要使用两个for套在一起
- 多重嵌套中如何跳出:跳出内层,而且要跳出外层。
- 无限循环
- 循环又叫迭代,代表了演进但是又有所变化的意思。
数组
- 最简单数组的模型就是排成一列的事物
- 二维数组是排成行列的事物
- 三位数组直观可以想象成魔方,它有行列还有层
- 三维数组另外一个例子:火车可以分车厢,车厢内可以分排,每排可以分座位。
- 多维数组的本质是对事物的层次切分
- 不管几维数组,内存存储模式都是单列,内存是线性的。
- 内存从寻址上说可以分页,分段,分块。但每个byte,bit都可以用数字来唯一表示。它也是线性的。直线的。
函数
- 内部函数static修饰符(限定不对外可见)等同于其他语言中的private。它的作用通常是被其他函数所调用构成完整都的程序。可以理解为子函数,不对外公布。
- 外部函数 extern修饰符
- 声明
- 实现
- 调用
- 函数参数传递方式: 参数传值,参数传地址, 参数传指针(后两种本质是一样的) 如果传值的话,会产生副本,就是会生成新的对象,花费时间和空间,而在退出函数的时候,又会销毁该对象,花费时间和空间。
- 返回值
- 什么情况下需要把代码独立成一个函数
递归
函数里面又调用自己,执行到调用自己时,当前压栈,进入一个新的函数调用,最后各层出栈继续执行。
内存申请: 我们要用到一整块内存写入数据时的最好办法,比如要制作一个特殊格式的文件
- 申请内存获得地址malloc
- 根据内存块起始地址加上偏移量来使用内存
- 初始化或者抹掉信息
- 使用完归还释放否则造成无法再次利用free
- 内存申请与数组创建内存块的区别
- 内存泄露 1.2 尽量是调用者申请内存,传入指针,不在函数内部申请空间并返回。这样内存容易被检查到。 1.3 局部的动态内存,做到严格配对申请和释放。动态申请的内存不使用时要及时free,最起码在程序最末尾,return之前要释放内存。避免此问题,一是malloc和free配对。在提前return时,也要检查是否fee。如果没有提前return。很容易检查配对。 1.4 数据在多个指针之间传递和保留内存时,要严格规定好谁释放。是否成功释放。需要一个检核。
堆heap、栈stack
栈,就是栈板,仓库里面用栈板放东西,最里面的东西如果要拿出来,得把外面的取出来先。就是先进后出。用于类似递归和程序分支,函数调用等保留现场。
当前要执行的函数或者程序分支,在栈顶帧开辟的区域运行局部变量,以及返回值。
读写文件
- 文件种类总的来说是可读的字符型和不可读的二进制型
- 如何读取
- 如何写入 a. 字符型的写入 b. 格式化数据(二进制)如何写入 c. 用什么工具观察写入到的二进制: ultraedit,十六进制观察。学会按字节看文件。
C效率体现在什么地方:借助地址+偏移量进行魔幻似的直接修改。这是C的精髓
a. memset b. memcpy c. memcmp 2. 对 &|位运算等比较低阶 3. 指针取地址等操作非常直接干脆。减少了很多对变量的时间消耗。 4. 机器硬件,电脑平台,手机平台等的开发首先考虑的是c的移植。这决定了它在编译到最终可执行码的效率上面比其他语言是要快。我只能这样认为。 5. 保持了轻装上阵,这是目前c还能被使用的最关键因素。
位于位操作
- 电路中对BIT操作的用法
- 位的操作符
- 如何从一个字节或者几个字节的类型(比如int)中取出某个bit: 移位,& 运算 | 运算。 a. 对位操作的最恰当最实用例子 网络的网罩network mask b. 对BIT,byte,内存块的简便操作,对低阶的机器,单片机,嵌入式来说,C/C++以上的语言都不直接提供 c. 如果单片机内存足够大,比如100K字节,针对BIT的操作对于高层逻辑来说不重要但是对效率来说,C仍然有它的优势 d. 我们不会再节省1bit,但是对于效率非常讲究的算法时,比如有一万个开关。如果用bit表示一个开关量,则我们需要很少的内存。而且查看它的状态可以用位的操作。非常方便。 e. bit看起来不需要节省,但是对于文件编码,则非常重要。表示开关量的话,它能很少字节。 f。如果某种传输协议。头一个byte表示协议的话。不同bit表示不同参数的话。这是bit的意义也非常重要,它精简了传输的数据量。
魔术大师:指针
- 指针的本质就是直接操作变量的位置,地址,或者说相对偏移量,或者说变量的一个“柄”。本质是变量声明在内存中。
- 指针快的本质是不用复制一个相同的样本,“隔空取物”,直接而干脆,简单粗暴。
- 指针的害处:基本没害处。因为你可以不用。不小心可以写数据到本不应该写的地方。这是最大的害。但指针肯定是给能操作它的人来用的。指针没用对,最多算BUG。不害怕。
- 指针本质是啥,如何存储,它表现出来是什么。其实就是一个变量。这是它存的都是是另外一个变量的地址。比如信封上面写了收信者的地址。一个道理。信封大小就是指针内存的大小。指针里面的内容就是寄件地址本身。对寄件地址的操作就可以把信寄往目的地。
- 声明和使用指针有两种,第一就是一开始就有了指示位置,比如动态申请的内存块。第二种是运行时去取一个别人的地址。就像你抄下朋友的住址一样。
- 魔咒:指针的指针的指针的指针。是什么玩意?其实就是数据结构中的链。指针可以悄无声息地记录下其他变量,对象,结构,字符串的位置,魔幻的操作它。被指向的事务根本无法察觉。可以理解为链,也可以理解为字典和搜索引擎的索引。索引为什么快。本质就是对于指针的预处理。试想:预先知道朋友家的地址和门牌号,还用不断打电话询问吗,不用。这又提示我们,要对复杂信息进行处理,分层建立多级的索引,或者叫指针,就能快速处理数据。
- const 指针 const intp=&a // p都不能变化 intconst p=&a; // p不能变化 const intconst p=&a; //p 中存放的内存单元的地址和内存单元中的内容都不可变 因为指针本身可以被轻易改变,为了保护首地址,用改变偏移量来取值。就是:*(Point+Offset)方式来访问。保护基准地址不被擅改。
- 使用指针的习惯,指向具体事务,或者=NULL
- 指针运算 加减的都是一个具体数据类型的宽度(字节数)。
- 要不就是指针变化,要不就是指针+偏移量的变化,来操纵数据。
- 使用指针的我觉得不用追求技巧,晦涩难懂的技巧不必使用。自己熟悉的方式,可控,不搞bug。
指针退化
int len, size; char a[100] = {0}; len = strlen(a); //len为0,strlen查找到第一个\0返回,a退化为指针 size = sizeof(a); //size为100,实际的内存占用1001。a不退化。 char a[100] = {1,2,3,4,5}; len = strlen(a); //len为5,strlen查找到第一个\0返回,a退化为指针 size = sizeof(a); //size为100,实际的内存占用1001。a不退化。 数组退化为指针问题 1、数组作为函数入参后,会退化为指针。 2、char a[100]数组被strlen(a)后,退化为指针。 3、在带数组型入参函数内对数组运算sizeof就不对了,因为被退化为了指针,因此传递的时候要加一个参数lenth。
- 不能依赖这样的不确定性。正常应该传入地址和大小size。
- 什么叫退化,其实就是变成了不知道长度的一个地址,丢失了长度。需要单独传长度参数。
void和void指针
- void 作为返回参数时,代表无参数,无返回
- void p时,本质代表这个指针可以指向任何数据类型。不具体致命数据类型。方便写通用函数。 void memcpy(void* dest, const void* src, size_t len); //当然这个函数内部是不管你什么参数地址给他,它是把数据当做一段byte数组来循环执行拷贝的。也就是说void到具体操作时,仍然要具体到数据类型。
- void *p根据需要用强制类型转换,转换到其他类型。
- void *p方便我们写一个指向函数的参数,实现泛型编程
二级指针: 指向指针的指针
- int **p;
二维数组指针
int a[4][5]; int (*p)[5]=a; int (*p)[4][5]=a;
指针数组
数组里面放的都是指针 int *p[3][3]
函数指针
int (*pf)(int, int) = &Add; //实质,指向函数代码段的起始位置
另一个魔术大师:取地址 & 计算符
- 本质,和指针是一样的。作用也是一样的。但他们是两个不同的事物。
- 指针是一个装地址的容器,比如信封壳。 而地址是地址本身,是地址本身。
- 对使用指针和取地址的焦虑:毫无必要。在充分理解的基础上使用。并且依赖IDEden智能提示,轻松加愉快。
如何驾驭指针和取地址
- 用久了就不怵了。配合IDE调试。只有正确无误的程序才能得到正确无误的结果。有bug的程序可能偶尔出一个正确结果。但不可能永久正确。
- 现在的IDE已经不是DOS时代了。写程序智能化多了。通常我们看到:期望什么值,你送入的是什么值。你就明白了。
如何调试程序
- 现在IDE普遍支持debug模式编译,可以下断点,逐行执行,可以跟踪到变量值,函数内部。
- 弹出提示,运用到本地无法调试,比如要取数据库的值,编译环境无法联网时。
- 写日志,有时逻辑判断非常复杂,而且不方便调试,比如它要求处理速度,而且连续运行,或者过程很随机,复杂。可以写日志。把需要的异常状态记录下来,进一步分析出现这个异常是什么导致的。
- 写文件的情况下,用ultraedit打开文件分析指定位置的十六进制。对比分析
需求的拆分
- 需求提出的可行性,用什么技术来实现
- 拆分我要用什么方式,分哪几个模块,如何组合调用他们才能完成伟大的任务
- 定义模块模组,定义变量,结构,或者数据库表
- 一个小任务又要拆分成几个大特质函数来实现。比如购票,支付,退票,打单。这是在业务逻辑定义上可以容易分割的。
- 一个函数,如果代码太长,比如超过千行了。就证明这其中某些小的部分是可以独立成为一段代码的。我们再用下一级函数来实现它。把它归纳出来,写成一个完成部分功能的函数。比如购票可以再次拆分:首先检索票源,核对数量,选择票,比较价格,计算价格,出票。
- 可读的程序比效果高更重要。有时为了好阅读,不得不做出取舍折中。记住我们用c。目标就是高效,但是高效的同时,要好读,容易理解,后续容易维护改进。
- 规范。比如define定义常数,变量的命名,模组的命名,注释的书写,详细解释。我们都得严格遵守,否则写出的代码不仅漏洞百出,无人敢接手继续维护,而且效率低下。
如何提高自己的实际编程水平
- 为什么看入门的教程老是入不了门? 很多次煞有介事地立下宏伟计划说要学习某门语言,结果一拖就是几年。反倒是有几次逼急了。非要写个东西,才把自己给逼到某个语言里来了。实际感受啊。
- 有针对性的解决问题,才能对知识有很多索取。一定要练实际的例子,不要去练解决数学问题。解决数学问题不是编程的主要工作。比如写一个文件格式转换,里面可以碰到许多的知识空缺和亟待解决的难题,这个难题是做练习题的数倍,数十倍。这就是用实际案例来学习的好处。
- 有人说为何在初学期间就要做很复杂的练习呢。我学习语言包括我用PHP和JAVASCRIPT,我都是拿起来就做任务,当时没有时间其他办法了,遇到问题再找资料。逼着自己马上进入真正的编程。如果你不要永远都在入门徘徊的话,听我的。
- 多思多想多总结。项目历练人,这个历练绝非看书,练习,搜索,单纯思考所能累计的。
- 为什么解决实际问题能极大极快提高编程水平:实践的问题很多,可能在一行代码上面卡住,就要读一堆的别人的资料。要尝试很多技术手段,很多知识才能解决问题。这个过程真实有效,而且毫不给你留情面。这种情况下,你会穷尽一切办法。