前言
从2021/4/16日开始学习c高级,5月16日结束,刚好1个月,这个月每天下班后和周末,都在坚持学习,前段时间因工作需要到外地出差,出差期间也没停止我的学习,每个章节我有自己的笔记,本文就是学完后一个总结,一是怕之前学的东西忘记,再回忆一遍加深记忆,另外也算是为这一个月的学习画上一个句号,说实话学完后心里感觉空唠唠的,不静下心来细想,好像不知道到底学到了什么,写这篇总结的另外一个目的就是,细数这个月的过往,以求心安。
之前看课程简介时印象最深刻的是说很多同学学习完c高级后有一种脱胎换骨的感觉,这点我到没有这方面的体会,可能是我现在的工作更偏向于硬件,软件编写得并不多,没有在工作中应用,才无法体会到那种感觉吧,又或许c功底的提升我自己不知道吧,看视频学习期间,我每看完一个老师的视频,都会凭着自己的理解和记忆,把课程中的示例代码自己写一遍,基本都问题不大,功能实现也差不了太多,有时候卡住了,简单瞄一眼老师的代码,就能完成课程代码功能。
自己能力提升了多少我不知道但是自我感觉还是收获还是颇丰,在一次与朋友讨论c问题时我才意识到,我考虑c的思想已经被改变,大脑首先居然就是从内存管理的角度切入。学习c高级前一直觉得指针很神秘,内心带有畏难心理。学完后觉得也就这么回事。下面分章节说说自己的收获或一些感悟吧。
内存这个大话题
这个章节学习过程中,印象很深刻的就是老师说的那句“懂得从内存角度分析c语言,那么你就成功了一半”,学完c高级后我感觉我的思想已经随课程改变,我的大脑和老师视乎一样,看待c问题时,首先就是从内存出发,这一点我深有体会,举个例子,有一次在和同事聊天时,同事说他最近在学习python,谈到两个字符串组合的方法,我首先想到的就是分配2段内存,分别存放两段字符串,组合时再开辟新的一段内存,分别将原来的2个内存的全部拷贝过来,我同事当时就评价说搞硬件的和搞软件的思维不一样,搞硬件的就喜欢用硬件思维考虑问题,就如操作内存,他说实际在python语言中只要对两个字符串相加就可以了(这一点我不知道对不对,但c肯定是不行的),举这个例子我并不是想讨论这个问题本身,而是想说思维的变化,换之前我最多会想到数组,但是肯定不会扯到内存。
数据类型
这章节我收获最大的也是老师所讲的核心,就是弄清楚了到底什么是数据类型,说到底我们定义的各种类型的变量最终无非就是放在存储器中,之前没考虑过这个问题,就单纯的认为这就是c的游戏规则,不问出处,遵守即可,然而学完这个章节后我才明白其实不同数据类型是规定的编译器在对不同类型变量进行内存大小的分配和这段内存的解析方法进行定义。存储器说白了就是一个有很多格子的仓库而已,场地给你了,仓库里放什么,你要占用多大面积,以及货物怎么堆放它都不管,你自己看着办。而数据类型就是定义占用场地大小,或者说是占用几个格子,和这片多个格子的组合方法,存放的货物也许是数字也许是字符,也可能是代码。从这里开始就引入了指针,从内存的管理中引入指针也让我感觉到是最顺理成章的,毫无违和感。因为我实在是想不到能有其它什么更好的办法来访问内存,指针当然是不二之选。
位操作
位操作这个章节是在学习裸机的时候一起学的,现在回想一下,位操作天生就是用来操作硬件的,帅呆了,之前我写单片机程序都是与和或一个特定数字,别说别人看,我自己写的时候都要计算半天,调试程序遇到问题时,首先就怀疑是特定数字自己计算错误,心里没有一点底气。学完本章节中; 置1 用 |=1<<x;置0用 &=~(<<x) , 非0转1用 !!x;感觉好极了,就想说一句“不装了,摊牌了”课程中还讲到了些稍微复杂点的位操作实现,这里就不一一讲述了,实际也用得不多,用到的时候再去回忆吧。
指针
指针、指针、指针重要的事情说三百遍,指针才是c的精髓,因为编译器喜欢用它,很多语法糖的底层,编译器都是用指针在处理,任何类型的指针都是一个占用4字节的整形变量,不同类型的指针只是对指向对象的解析方法不同,如p++到底该移动几个字节(地址),还得提一下,一个地址对应一个字节,这也该说1百遍,确很少有人说,这个章节的核心我认为是左值右值的含义,特别是数组做右值时的含义。
数组右值
右值 a[ 0 ] –指数组的首元素首地址;
右值 &a —数组的起始地址,虽然实际地址同上,但意义不一样;
右值 a ——指数组的首元素首地址,与a[0]完全一样;
别看简单的这么3行,弄混了几代人啊,有的工程师可能怕是在咽气的那一刻都还不能区分这这3种表达,编译时报错总是傻傻摸不着头脑,有木有。
另外要提一下的是数组不能越界写入,如果出现越界写入,编译器会将越界部分的数据丢掉。
数组的指针实现
该章节还讲到了,数组的指针实现方法,哈哈哈哈哈,想想都觉得好嗨啊。
用普通指针指向数组,访问a[1] 等效于 *(a+1);用了这么多年的a[ x ],真没想过编译器在内存操作中是怎么实现的。原来编译器把他还原成了指针,[ x ]只是c的语法糖而已。
一维数组指针
定义一维数组指针 (*P)[5]= &Table,而数组指针访问Table[1]时,则是*(*p + 1),&Table指针指向的不是内部元素,而是指向的整个数组外壳,*p是访问外壳里面的其中一个元素的地址(就如同指向一个变量地址),要获取地址的值当然还得加一个*,即*(*p+1)了。恍然大悟吧,指针的世界怎么就这么妙啊。
二维数组指针
明白一维数组指针,那么二维数组指针就不难明白了,二维数组指针的定义时 (*p)[2][3];指针访问 a[1][2] 则是,*(*(*P+1)+2);*P进入第1维度,*p+1则是第一维度的第二个元素数组,这个时候就可以看成是一个一维数组了就再加*再加*呗。p=&Table就是指针指向的是二维数组的外壳,呵呵,二维数组里面是2个1维数组啊,*P向内进入1层,指到了内部的一维数组的外壳,**P就到了一维数组内部的元素的地址了,***P就把地址的内容取出来了呗;
咋看都觉得好复杂啊,指针+指针+指针;是什么鬼啊,虽然看起来很屌,其实就是增强版的一维数组指针啊;
额!好吧,说起来容易,其实我在学这个的时候也是被绕晕在厕所过的。擦屁股的纸都差点找不到了。
sizeof
sizeof这个常用的这个关键字又有谁能想到是在编译时就计算出了对象的大小,并赋值了了,而不是在程序运行过程中去计算的,之前还觉得这玩意儿效率低,还特意避开他,自己算好了再放进去。现在只想说,兄弟你想多了。还有sizeof算的不是数据个数哦,而是占用字节长度。又有谁会注意到这个问题。也许只有翻车了才会恍然大悟吧。
传值传址
除数组外,都是传值调用,函数传参时是新建变量拷贝数据值,要是你传了个结构体,甭提效率有多低了并且函数内的操作不影响原始数据,想要提高效率和修改原始数据还是让指针漫天飞吧,它真的能给你节省好多全局变量。
之前有个项目,指针指向了结构体,折磨了得我头昏脑涨啊,编译器就是不给面子,不是警告就是错误,*p 和 . 都不好使,还是一个老家伙跟我一起折腾了半天后说用->试试才解决,虽然解决了,但是我真不知道为啥要这样,学了这个章节才知道,嗦嘎,指针访问结构体时要用箭头 ->。
高级指针
真的很高级?低调点,其实没那么高,但是确实有点难,我认为是c高级课程中最难的的一个章节,好在老师将得细,不然又白瞎了。上来就是数组指针、指针数组。
函数指针
函数原型:int func(int a,char b) 函数指针int (*PT) (int,char);怎么样是不是感觉函数指针和函数原型很像了,没错就是很像,直接把函数原型拷贝,再加个(*PT),再把形参的变量删除了,就完事了。
函数指针指向函数PT=func;访问函数就可以痛痛快快的int a=PT了。
然鹅,函数指针前加上typdef就又不一样了,PT就不是指针了,而是一个函数指针类型了,就不能直接等于func了,既然是类型当然还要定义量啊,所以还得PT p定义一个函数指针变量,函数访问就得使用int a= p了。
二重指针
int a=5,*p1,*p2;p1=&a;p2=&p1;**p2就等于5了,重点是第二重指针一定要指向指针的地址。别把上面的数组指针扯过来,他们不一样。不过非要扯在一起,那也行,都是一层一层的剥,(如果你愿意一层一层一层剥开我的心,你会发现,你会诧异,你是我最压抑最深处的秘密)还学毛啊,跟我一起唱起来。
堆栈 & bbs段
经常听到堆栈,一直都是一知半解的,鬼知道这玩意儿是干毛的。直到学习了本章节,才知道堆栈就是扯淡的,堆是堆,栈是栈,2种不同的东西,挂嘴边的堆栈其实就是栈,不用整得这么高大上,不就是用来存放数据的吗:
堆内存:比较连续的一段内存,编译器不会主动分配,他藏着掖着的,只有用户自己使用malloc才能申请到。
栈内存:存放局部变量的,据说操作系统会给每一个程序分配一段内存作为栈,程序内所有函数共用,函数结束,栈内存归还,而且还是不洗碗的归还,所以大家用之前都自己洗呗,因为系统分配给栈的空间有限,所以别搞大面积的局部变量,不然溢出了就完犊子了。
bbs :第一次听说bbs段是在裸机中,当时没太懂,提到bbs段就不得不提数据段,两者都是用来存放静态局部变量和全局变量,bbs用来存放为0的静态局部变量和为0的全局,非0的全局和静态局部变量则放在数据段。按道理来说,两则完全可以混在一起用啊,为啥要分开了,搞事情啊,呵呵,你太天真了,我们使用的大部分变量起始初始值都为0,如果编译器逐个清零效率太低了,但是也不能全部直接清零,因为偶尔有那么一个他不是0,所以编译器这家伙将两者分开存放,bbs段数据内容就不拷贝了,直接全部清零,效率老高了。至于数据段,反正也不多,逐个去赋值一遍也耽误不了太多时间。bbs与数据段与程序相伴一生,不会在程序运行过程中被释放,所以还是要少使用,没错我就是指的要少使用全局变量和静态局部变量,占着茅坑不一坨接一坨的拉屎,关键是程序移植性还差,要用大片的全局,就直接malloc向堆要吧,堆好说话则了,用完记得free还哦。释放了就行了。
字符串
字符串,你大爷的,难住了我半个硬件工程师的职业生涯,”字符串” ‘字符’ 单双引号就是弄不清啊。主要是单片机用字符串太少,其实字符串就是一段连续的内存组合解析,当然可以用语法糖的数组,也可以使用指针,目的就是为了操作连续内存呗。
\0是字符串的结尾,每个字符占1个字节,\0的ascll位置号是0;
\n 换行符,在linux下换行且使光标移动首行,在windows下被称为回车,光标下移一格,\r是换行符,光标移动到首行。
\b 是退格键;
\r\n 是换行+回车;
字符是字符串的最小单位,或者说字符串是由字符构成,一个字符占1个字节,8位,就是内存的一个地址单位。
字符串的定义有两种,char table[ ]=“linux”或char *p=“linux”;
字符串一直存储在栈上,在定义字符指针时,指针也分配在栈上
字符串的操作略显麻烦,判断数据长度、比较是否相同,都不能直接使用C的关键字因为别人是1段内存嘛,需要使用相关的函数来实现。或者使用指针自己逐个去计算。
结构体
作为一个单片机老油条程序员,结构体当然没少用,结构体存在的意义就是为了解决数组不能存放类型不同的数据,结构体与typdef联系到一起就略显复杂,typedef其实就是为了简化struct S 类型定义的文字数量的,typdef struct S{ }S;后就可以直接使用S a定义结构体变量a了,结构体括号后面的S在添加typedef前代表结构体类型的变量s,加typedef后s就是吧结构体类型进行了重名成S了,S仍然是类型,所以不能直接当变量访问,有意思的是括号后面的S可以与struct后面的S相同,定义结构体变量a,的两种方法struct S a和S a;都是可以的。所以见到两者就不要惊讶。
结构体内访问时使用点“.”;指针访问结构体需要用箭头“ -> ”;
以上2点之前给我造成了困扰,知道学完本章节才彻底的弄明白了结构体。不得不多提一句,大学c二级没过是有原因的,那时候经常逃课,学的点东西还是写单片机程序练出来的,当然老师们其实也研究得不深,课堂上当然也不会讲得太明白,以至于刚开始工作的时候去问别人什么是共用体,还傻傻的说没听说过,现在想想那一刻就被别人鄙视了。
对齐访问
int *p;p++;地址加4(即1个int型);
char *p;p++;地址加1(即1个char型);
这是我最新领悟,之前一直还在想,既然任何类型的指针变量都是一个int型常量。为什么要区分了,上面的例子就是原因,指针操作时,地址累加单位是不同的,即p的移动不是按字节移动,而是按数据类型长度移动,这个我觉得也应该说100遍。如果想让其按1个字节移动,可以强制转换成char型。
对齐访问目的是为了提高效率,特殊时候对齐访问,虽然访问的内存变多,但比访问得少效率更高。这个必须要明白内存管理,内存访问以整形效率最高,因为处理器访问内存时都是以整形为单位,结构体的不同类型的成员编译器分配的的内存并不是以字节为单位首尾相接的,都是要满足一些条件的,变量的起始地址必须是变量类型长度的倍数关系,如果遇到前面的的变量占用到不是整数倍,那么没用到的内存会被抛弃浪费,且整个结构体占用空间必须是最大数据类型的整数倍,不是同样会抛弃内存,例如:
struct{char a;double b;};占用内存16个字节;
结构体内同类型数据时, char-1字节对齐;short-2字节对齐;int-4字节对齐;
double-8字节对齐;多种类型时,
当然如果很在意内存,不在意效率,可以使用宏,在变量定义区域的前添加 __attribute__((aligned( n )))指定为n位对齐;
计算结构体内成员的偏移量
#define offsetof(TYEP,MEMBER) (int) (&((TYEP *)0) -> MEMBER);
实现原理是将0地址转换成1个结构体类型的指针,访问指向结构体内的成员并取其地址,因为起始地址是0,所以成员地址即是偏移量;注意:->的优先级比取地址符要高;
说到优先级,-> . [ ] 这3个符号优先级是最高的。其它的可以不记了;
typeof()关键字
typeof(n):是c的关键字,用来获得变量类型的;例如:
int i;typeof(i) a; sizeof(a)则等于4,即定义了一个int型的a;这个字符清晰了吧;
container
根据结构体成员计算结构体的起始地址;
#define container(ptr,type,member) typeof( ((type *)0)->member) *__mptr = ptr; \
(type *) ( (char *)__mptr – offset(type,member) )
释义:
typeof( ((type *)0)->member) *_mptr //获取结构体成员的类型,并定义同类型指针;
(type *) ( (char *)__mptr – offset(type,member) )//指针减去偏移量得到了起始地址;
好复杂啊!!!
用&取地址符号获得成员的地址;
struct test *p = &x.e
offsetof计算成员的偏移地址;
int a=offsetof(struct test, e)
成员地址 – 偏移地址 = 起始地址;
(char)p - a = 起始地址;注意:p需要强制转换成char,使地址以1字节为单位,不能以其它数据类型为单位。
结构体本身不复杂,但是其中字节对齐还是有点考究,计算结构体偏移地址和起始地址就更恼火了。不过好在不急,可以慢慢研究。
大小端模式
这个概念是在大学的时候准备学习arm7,买了本书,在书中看到的。arm7没学会,书也没看完,就知道了这个名词(大小端格式),我觉得记住这个概念就行了:
小端模式 à 低字节放在低位,高字节放高位;
要想测试平台使用的那个模式,可以写一个共用体unon测试;
枚举:enum
枚举与结构体很相似,一些语法上如typedef重命名都可参考结构体。
枚举主要用在结果是有限可能下,如1星期只有7天,通过枚举定义的变量,那么变量的赋值就只能是枚举内的成员了,其它的会报错,在提高程序的可读性上有很大的帮助。
预处理
包含的头文件,其实在预处理时会把头文件所有内容都搬过来,不会进行错误检查,所以使用时需要格外小心,注释不搬,好粗暴;
标准库的头文件使用 方括号 “< >”,编译器只在系统指定目录查找文件。
自定义的头文件使用 引号 “ ”,编译器会先在当前目录查找,找不到再去系统指定的目录去找。
条件编译
写了这么多年的单片机程序,对这个还不明白,惭愧啊,没系统学习过预处理,只是用到啥去学啥呗;
#define dbug #ifdef dbug 语句A #else 语句B #endif
#define dbug 1 #if(dbug==1) 语句A #else 语句B #endif
为了保持良好的编程习惯,带参数宏尽量用括号括起来,不然容易出错,在搬运的时候很容易因为运算优先级问题出错。
防止多重定义
#ifndef _MAI__H #define _MAIN_H #endif
判断_MAI_H是否被定义没有定义则执行#define _MAIN_H;
函数
作为一个写了8年单片机软件的老家伙,函数还要学吗?当然要,又有几个人真正知道什么是函数了。学习本章节时老师曾这样描述函数,函数就如同一个加工的机器,机器所需的原材料就是函数输入参数,将加工的成品以返回值或输出值输出。函数名是一个指向代码的指针,调用函数名时,则是函数指针的解引用,函数的调用时,处理器会将原来的运行数据进行压栈,函数执行完成后再弹栈,函数的输入参数不宜过多,应<4个,如果一定要输入多个参数,可以使用结构体,函数中尽量少用全局变量,全局变量会降低函数的模块化、移植性。
内联函数inline
内联函数是函数,主要是为了减少处理器的开销,普通函数调用时需要进行压栈和弹栈,而内联函数则不需要,编译器会像对待宏定义一样,直接将函数内的表达式拷贝并插入到调用函数的地方(说白了就是利用空间来换取效率),与宏不同的是,宏是在预处理的时候进行,而内联函数则是编译过程中进行,一旦对内联函数进行了修改,整个项目工程都要重新编译去替换,内联函数具备普通函数的所有功能,内联函数内不宜有过多的代码一般小于5行,不能有循环、开关等功能代码,如果内部功能复杂,即使使用inline修饰成内联函数,编译器也会将其转换成普通函数。换句话说,普通函数非常简单,有的编译器也会自动将其转换成内联函数,所以这玩意儿还真不一定。
定义内联函数:inline void a(void);
申明内联函数 :void a(void);
调用函数:a();
递归函数
c语言允许函数自己调用自己,这种行为被称为递归。使用递归函数一定要注意,必须有递归的结束条件,结束条件又被称为收敛,不能无限循环,当然不可能一直无限循环,因为内存大小是有限的,内存没有了自然就崩溃了。内联语法如下:
void func(int i)
{
printf(“递归前i= %d\n”,i);
func(i-1);
printf(“递归后i= %d\n”,i);
}
链接库
静态链接库
静态链接库,扩展名 .a;多用于代码的版权保护,将代码进行编译(不链接)的 .o文件归档成.a文件,该 .a文件则被称为静态链接库,再将 .a和配套的 .h文件打包后发布,在.h中有所有.a文件的函数申明和函数功能注释,使用者只需关注.h文件,.a文件则不用关心,用户将自己的功能码同样先进行编译(不链接),再将用户的.O文件与静态库的.a文件进行链接成为最终的代码,该方式最大的缺点是由于在静态库中有很多无用的代码,会导致最终生成的可执行代码体量较大。
静态链接库第一次接触是在刚进入公司不久的一次软件标准化评审会议上,上位机软件部的同事提出的解决方案,当时没听懂。大概意思就是将.c文件转换成.a文件,现在终于知道当年他在说什么了,原来我与他差的不是一泡尿的距离,而是赛车时看不到尾灯的距离。
静态链接库的制作方法:使用gcc -c 只编译不链接,生成.o文件,然后使用ar工具打包.a归档文件。gcc编译器默认使用的就是动态链接库;如果想使用静态链接库使用-static标识;
静态链接库的名字必须以lib开头。
动态链接库
动态链接库扩展名 .so(windows下是.dll)。动态链接库很好的解决了静态链接库体量大的问题,我想动态链接库也是为此而发明出来的吧,动态链接库是将功能函数编译并链接转换成不可单独执行的可执行文件,与用户的可执行文件放不同位置。当用户程序需要时去调用另一个位置的动态链接库的可执行代码,动态链接库的发明,主要是为了降低内存开销和程序的模块化。
动态链接库制作方法:
编写库函数源码 -> 只编译不连接,链接成动态库;
动态库只能是位置无关码,因为操作系统会随时调用。
运行方法:
第一种:将动态库拷贝到系统的目录 /usr/lib
第二种:修改系统环境变量 LD_LIBRARY_PATH。操作系统在加载固定目录/usr/lib前会先在环境变量内指定的目录下去查找。
存储类及作用域
存储类及作用域主要是综合讲解不同类型的代码存储的位置及作用域,在之前的篇章都有单独描述,本章节只是将前面的内容统一起来分析。
c程序在linux的内存存储映像:
序号 | 段名称 | 存储数据类型 | 说明 |
代码段 | 可执行程序 | 文本段(.text) | |
只读数据段 | const常量 | 不同平台不同,下载程序时写入数据 | |
数据段 | 非0全局常量 非0局部静态量 | - | |
bbs段 | 为0全局常量 为0局部静态量 | - | |
堆 | 默认:无 | 默认无,程序员malloc申请使用 | |
文件映射区 | 文件临存 | 文本编辑时,点击保存时写入硬盘 | |
栈 | 局部变量、形参 | - | |
内核映射区 | 操作系统 | 见详解 |
作用域的关键字
作用域
指变量的访问范围,分为代码块作用域和文件作用域;
代码块作用域,使用{ }标识,如局部变量,在{ }内定义后可以使用,出了大括号就被回收了。
文件作用域,则是在当前c文件中可访问;
同名变量在作用域不存在交叉时,相符不影响,如果存在交叉则作用域小的有效,作用域大的被小的掩蔽。当然不允许有作用域完全相同的同名变量。
关键字 | 功 能 | 作用域 | 生命周期 | 链接属性 |
auto | 修饰变量为局部 | 申明后的代码块 | 代码块内 | 无链接 |
static | 修饰静态局部变量 | 申明后的代码块 | 程序开始到结束 | 无链接 |
全局变量、函数作用域修饰 | 单文件作用域 | 程序开始到结束 | 外链接 | |
register | 修饰变量优选寄存器存储 | 修饰不同类型变量其属性不同 | ||
extern | 外部全局变量生命 | 通常修饰全局变量其属性遵循全局变量规则 | ||
volatile | 修饰变量会被硬件改变 | 修饰不同类型变量其属性不同 | ||
restrict | 只修饰指针指向的变量仅本指针改变 | 修饰不同类型变量其属性不同 |
剩余的一些关键字
在C99版本中c的关键字共32个,注意是c99,新版c标准会有更多。
存储类的4个(auto、static、register、extern);
数据类12个(char、double、enum、float、int、long、short、signed、struct、unsigned、void);
控制语句12个(for、do、while、break、continue、if、else、goto、switch、case、default);
其它4个:const、sizeof、typedef、volatile、return;
大部分都熟悉,单也有个别列外,如:
auto—自动存储,虽然很少写,但是常常用,因为定义普通变量时类型前不添加修饰符时默认的就是auto类型,即自动存储类型;
register—寄存器存储,是指将变量放在寄存器中,因为寄存器在cpu中,所以访问速度特别快,值得注意的是,寄存器资源非常稀少,所以在使用过程中不能定义太多,就算变量使用register修饰后编译器也不一定会将其分配在寄存器上,原因还是资源太少,编译器没有寄存器为其分配。
continue—结束本轮循环,与break、return功能类型相似,但不一样,return是跳出当前函数,break是跳出当前循环,即终止当前循环,而continue则是结束本轮循环,开始下一次循环。
void—很多人理解为无类型,其实应该说是不知道类型,需要进行强制转换后就有类型了,void除了函数外一般与指针配合使用,在指针指向一片内存时,内存本身是不知道类型的,所以通常用void表示,当然也必然会配套强制转换使用,因为内存在没有类型时,无法操作。
volatile一般用来修饰变量,简单说是告诉编译器不能优化该变量,如;b=1;a=b那么编译器可能会又换成a=1;b=1;但是在处理器中时存在中断的,很有可能执行完b=1后被中断,恰巧中断将b的值进行了修改,这个时候a就不等于1了。这种情况下就被编译器坑了,添加volati后编译器就不会优化。
NULL—不是c的关键字,是一个宏,在很多库中都有定义该宏如stdio.h;string.h等,其原型是 #define NULL (void *)0;//将0地址强制转换void类型,多用于判断野指针。
变量的生命周期
外链接:基于工程内所有文件,垮文件访问。如函数和全局变量
内链接:.c文件内进行连接,不能跨文件访问,如static修饰的全局变量和函数
无链接:符号不参与连接,如局部变量。
总结如下表:
局部变量 | 静态局部变量 | 全局变量 | 静态全局变量 | 函数 | |
存储位置 | 栈 | 数据段/bbs段 | 数据段/bbs段 | 数据段/bbs段 | 代码段 |
地址变化 | 随机 | 固定 | 固定 | 固定 | 固定 |
默认初值 | 随机 | 0 | 0 | 0 | - |
链接属性 | 无链接 | 无链接 | 外部链接 | 内链接 | 外链接 |
作用域 | 代码块 | 代码块 | 工程内 | 工程内 | 工程内 |
生命周期 | 代码块 | 永久 | 永久 | 永久 | 永久 |
链表
链表和脑子一样可是个好东西,第一次看见同事写的链表程序完全摸不着头脑,在这之前只闻其名,未见其人,链表是在上海出差期间学习的,看到链表,出差的情景历历在目啊。
链表的发明和结构体一样是为了解决数组的缺陷的。数组主要有两大缺陷,成员数据类型必须相同,大小必须在编译时固定,运行过程中不能改变。结构体解决了其类型必须相同的缺陷,而链表则是解决运行过程中数组长度不能修改的缺陷。链表分为单链表和双链表。链表还有1个特点就是并不要求内存地址是前后连接,可以将不同地址内存连接在一起使用,可有效的利用零散的内存。
链表的每一个节点都是一个结构体,该结构体由指针和有效数据2部分组成,指针用来指向前后节点,形成链,有效数据则是用户需要存储使用的数据,可以是任意类型的变量,如常规的int、char、数组、结构体、函数指针等等。链表需使用堆内存,链表使用过程中需要删除节点,堆内存才支持内存的手动释放。
链表的创建步骤有:申请内存、清理内存、创建节点,填充数据、操作指针将其与前后节点连接。
链表的插入可头部插入也可尾部插入,无论什么插入最后一个节点的后向指针应是NULL,头部的前向指针也应该是NULL;
单链表准确的说叫单向链表,只能从表头向后单向遍历;没有前向指针,所以操作时不能向前访问,为了解决该问题就有了双链表。
双链表准确的说叫双向链表,节点内有两个指针,分别指向前一节点和后一节点。操作灵活,所以大部分时候使用双向链表。
结语
本课程总结至此结束,细数本课程知识点,着实不少,以上总结的全是以前不清楚或完全不知道的知识点,通过为期1个月,对本课程100个小时的学习使我有了新的进步,同时学习过程中也发现我的不足:知识面的缺失,课程学习的不系统,基础不牢固。彻底明白“难的不会,是因为简单的没学好,会的越多,学的越快,会的越多,解决问题的方法也会更多”,也许这次学习的内容不久后会变模糊,但我相信只要有印象,就能成为我解决问题的方法,只要有印象用到时即使不能一次解决问题,回来查看时,也能快速上手。
本课程博大精深,涉及到的部分算是略知一二,自我评价已不再是青铜段位,至此我想以后不会再有系统的学习本科目的过程了,该科目大的框架已形成,后续则是查漏补缺的过程,要更好的掌握,需经时间洗礼,在应用实践中摸索,不断在错误中吸取教训和总结,逐渐星火燎原,当然课程的所述的知识点我并没能完全吸收,后续有必要再进行复读,每复读一遍应该也会有新的收获,也会再来完善本总结,本总结并没有将知识点详细展开,要细究还需要查看对应章节的学习笔记,或查阅其它网络资料。
本课程只是实现我目标的开始,是基础中的基础,学习还远远没有结束,掐指一算我已原地踏步5年。如果28理论适用我的职业生涯,那我希望在10年的这最后2年完成蜕变。
总结人:胡代洲 总结日期:2021/5/21