C语言学习心得

回想起来,自己接触C语言也有了三个年头,作为一个正经大学的科班出身,不能说自己对于C语言的了解太少,但是就目前自己的水平而言,远远没有那种敢于说自信的程度,近期华为软件经营挑战赛的进行,使得自己更加清晰的认清自己与他人所存在的差距。本科学习过多种语言,同样也跟随着老师探讨过诸多项目,但是相较于两年前的自己,感觉编程能力并没有什么进步,反而自己在时间的流逝当中,渐渐的迷失。总觉得C语言不过如此,虽然数组、指针、内存管理等诸多知识点也能够说出个123,但是总感觉自己还是一个在门外打转的边缘人物,在这种想法之下,就想进一步的巩固基础知识,为即将到来的学习生活负责,努力让自己认清C语言的整体脉络,而不仅仅是处于边缘的部分试探。

自己选择的第一本书是陈正冲老师的《C语言深度剖析》,这本书在整体上感觉并不是太厚,电子版也仅有100+的样子,但是细读起来,感觉老师讲述的并非普通教科书那种基础,而是那种不深入思考完全平时不会遇到的问题,也就是自己原本未曾涉及到的领域。

在书籍的第一章讲述的是关键字,可能大家一想到关键词,“哦,关键字嘛,不过如此”,也确实没有太多的概念,细数之下,C语言官方定义的关键字有32个之多,虽然有的关键字已经耳熟能详,但是有些关键字并没有太多的接触,诸如union、volatile、extern、sizeof等。

什么是定义?:所谓的定义就是(编译器)创建一个对象,为这个对象分配一块内存并给它取上一个名字,这个名字就是我们经常所说的变量名或对象名。这个名字一旦和这块内存匹配起来,它们就同生共死,并且这块内存的位置也不能被改变。一个变量或对象在一定的区域内(比如函数内,全局等)只能被定义一次,如果定义多次,编译器会提示你重复定义同一个变量或对象。

什么是声明?:告诉编译器,这个名字已经匹配到一块内存上了,下面的代码用到变量或对象是在别的地方定义的,同样声明可以出现多次;也会告诉编译器,这个名字我已经预定了,别的地方不能用它来作为变量名或对象名,但是就声明的这个节点,本身并没有使用这个空间或者对象名。

所以关于定义以及声明之间的区别就应该看是否通过这条语句实现对变量或者对象分配空间的操作

Register:使得变量尽可能的定义在CPU的内存中,从而加快CPU存取变量的速度,但是也有可能不再内存中,所以不能使用&符号来取得变量所在的地址,其所定义的变量一定是符合CPU寄存器类型的变量,长度需要小于或者等于一个整形的长度。

Sizeof:一个经常被认为是函数的关键字,sizeof 在计算变量所占空间大小时,括号可以省略,而计算类型(模子,诸如:int、signed、short等)大小时不能省略。

Extern:置于函数或者变量的前面,表示变量或者函数在别的文件中已经有了定义。当编译器在遇到extern这个关键字时,提示其去其他的模块寻找定义。

对于循环嵌套的的情况,如果有可能,应当将最长的循环放在最内层,将最短的循环放在最外层,以减少CPU跨切循环层的次数。

Main函数的参数问题,一般的情况下,在定义main函数的时候,并不会为其置入参数列表,但是main函数确实是有参数的存在的,其可以定义为以下三种形式:

  1. 函数没有参数,返回值为int类型:int main(void){/*······~~·*/}
  2. 函数有两个参数,类型分别为int以及char**,返回值是int类型:int main(int argc,char *argv[]){/*······~~·*/}
  3. 还有三个参数的:int main(int argc,char * argv[],char *envp[]){/*······~~·*/}

argc(全称为 argument count)的值为 0 或者为命令行中启动该程序的字符串的数量。程序本身的名称也算作该字符串,也要计算进去。

argv(全称为 arguments vector)是一个 char 指针数组,每个指针都独立的指向命令行中每个字符串:数组中元素的个数,比 argc 的值多 1;最后一个元素 argv[argc] 是空指针。如果 argc 大于 0,那么第一个字符串,argv[0],就是程序本身的名称。如果运行环境不支持程序名称,那么 argv[0] 为空。如果 argc 大于 1,从字符串 argv[1] 到 argv[argc-1] 包含该程序命令行参数。

envp(全称为 environment pointer)在非标准的、有 3 个参数的 main()函数版本中,是一个指针数组,每个指针都指向组成程序环境的一个字符串。通常,这个字符串的格式是“名称=值”。在标准 C 语言中,可以利用函数 getenv()获取得这些环境变量。

Const在修饰变量或者指针的时候,应该先将类型名忽略,然后按照就近原则进行修饰,比如const int *p可以见得const修饰的是*p,为指针指向的对象不可变,若 int * const p此时const修饰的为p,即指针p不可变,但是p指向的对象内容可变,也就是说仅仅绑定了p与所指对象的关系。

Volatile 随时会发生变化的变量,用的地方很少,就是对编译器说这个变量是随时会发生变化的,在做代码优化时会被跳过对该部分的一种优化,按部就班的从内存中存取变量的值,并不是将中间值进行赋值或者其他操作。

Union 维护足够的空间来放置多个数据成员中的一个,而并非为结构中所有的成员都分配好内存空间,也就是说在union中所有的数据成员公用同一块空间,在同一时间该地址块只能存放一个元素,并且所有的元素具有相同的初始地址。而空间的大小参照union中数据元素占用空间最大的那个。

当前系统的存储模式(大端模式/小端模式):所谓的大端模式是指字数据的高字节存储在低地址中,而字数据的低字节存放在高地址中,小端模式反之。这样就牵制到首地址存储的问题,首地址肯定是程序段内存分配的低地址位,而根据系统的存储模式则影响着首地址中的数据分配问题,这个在wenhex中可以对文件的数据信息进行查看,同样也可以利用输出共用体的数据来进行判断。

Typedef的真正意思是:给一个已经存在的数据类型(而不是变量)取一个别名,而非定义一个新的数据类型—rename。typedef struct student{/*~~~*/}Stu_st,*Stu_pst;上述typedef语句可以认为将struct student 取了一个别名Stu_st,还有一个*Stu_pst。从而可以利用这些别名来分别定义Struct student 类型的变量。

++ -- 运算符:对于前自加与后自加的区别,详述如下前自加(++a)先执行自加操作,然后再引用a 的值,对于后自加(a++)操作,先引用a的值,然后执行a的自加操作,当然在自加操作为孤立的一行时,上述两种操作的效果一样。

预定义与注释编译顺序问题:代码注释行要先于预定义指令被编译器进行处理。

#error预处理:编译程序时,只要遇到#error 就会生成一个编译错误提示消息,并停止编译。

#pragma指令:用于设定编译器的状态或者只是编译器完成一些特定的动作。例如:#pragma message(“信息文本”)当编译器遇到这个指令时,就会在编译信息输出窗口输出消息文本,从而给与文本提示。

内存对齐:字,双字,和四字在自然边界上不需要在内存中对齐。(对字,双字,和四字来说,自然边界分别是偶数地址,可以被4 整除的地址,和可以被8 整除的地址。)由于访问未对齐的内存,处理器需要作两次内存访问,然而,对齐的内存仅需要一次访问,则出现了内存对齐的必要性,即牺牲一点空间(成员之间有部分内存空闲),来提高处理器访问的性能。对于未对齐内存为何需要两次内存访问,解释如下:某些对双四字的操作指令需要内存操作数在自然边界上对齐,如果操作数没有对齐,这些指令将会产生一个通用保护异常。双四字的自然边界是能够被16 整除的地址。其他的操作双四字的指令允许未对齐的访问(不会产生通用保护异常),但是需要额外的内存总线周期来访问内存中未对齐的数据。

内存对齐的原则:首先,每个成员分别按自己的方式对齐,并能最小化长度。其次,复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度。然后,对齐后的长度必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐。

指针:int *p;

通过上述定义声明了内存中的4个字节的空间,然后把这个内存空间将其命名为p,同时限定着4个字节的空间里面只能存储某一个内存地址,即使存入了其他的数据,也都会被编译器当作地址来处理,并且由上述声明的那样,从首地址开始的连续4个字节只能存储某个指向int数据的地址。另外无论对于什么样的数据类型,指针类型始终都是4个字节(32位),因为其存放的是相关数据类型的地址。

 

简述int *p=NULL与*p=NULL之间的区别:首先第一句为初始化过程,定义一个指针变量p,其指向的内存里面保存的是int类型的数据,在定义变量p的同时把p的值设置为0,而不是把*p(即指针p指向的内存空间)的设置为0;第二句为赋值过程,有可能指针p指向的为非法地址,从而导致在编译器编译之时导致编译错误,然后赋值语句将这个p指向的内存空间的内容赋值为NULL。

数组:int a[5];

当我们定义一个数组a 时,编译器根据指定的元素个数和元素的类型分配确定大小(元素类型大小*元素个数)的一块内存,并把这块内存的名字命名为a,名字a 一旦与这块内存匹配就不能被改变。也就是说a为整个数组的一个名字,其代表的是数组首个元素的首地址,并且数组不能以整体的形式被访问,仅能访问数组中的某一个元素,而不能将数组当作一个整体进行访问。&a为整个数组的首地址,虽然它的值与a的值相同,但是含义却是有所区别,a为数组首个元素的首地址,&a为争着数组的首地址。

 

指针

数组

保存数据的地址,任何存入指针变量p 的数据都会被当作地址来处理。p 本身的地址由编译器另外存储,存储在哪里,我们并不知道

保存数据,数组名a 代表的是数组首元素的首地址而不是数组的首地址。&a 才是整个数组的首地址。a 本身的地址由编译器另外存储,存储在哪里,我们并不知道。

间接访问数据,首先取得指针变量p 的内容,把它作为地址,然后从这个地址提取数据或向这个地址写入数据。指针可以以指针的形式访问*(p+i);也可以以下标的形式访问p[i]。但其本质都是先取p 的内容然后加上i*sizeof(类型)个byte 作为数据的真正地址。

直接访问数据,数组名a 是整个数组的名字,数组内每个元素并没有名字。只能通过“具名+匿名”的方式来访问其某个元素,不能把数组当一个整体来进行读写操作。数组可以以指针的形式访问*(a+i);也可以以下标的形式访问a[i]。但其本质都是a 所代表的数组首元素的首地址加上i* sizeof(类型)个byte 作为数据的真正地址。

通常用于动态数据结构。

通常用于存储固定数目且数据类型相同的元素。

相关的函数为malloc 和free。

隐式分配和删除

通常指向匿名数据(当然也可指向具名数据)

自身即为数组名

 

&p[4][2] - &a[4][2]的值为多少?

代码如下:

int a[5][5];

int (*p)[4];

p = a;

由上述定义可知,p为一个数组指针,共有四个元素,所以p+1就意味着内存往后往后移动了4*sizeof(int)的空间,也就是说,&p[4][2]的值为&a[0][0]+4*4*sizeof(int)+3*sizeof(int)

所以原式可以等于4*sizeof(int)

内存管理

野指针:未被绑定的指针,也就是说定义指针的时候并没有为该指针绑定内存空间,导致这个指针在内存中没有规则的约束,从而导致某些问题的发生。而解决野指针的最好方法就是在每一次定义指针的时候,都要为其初始化,即使目前并没有指针他的亲身参与的项目,那就把它赋值NULL。

静态区:保存自动全局变量和static 变量(包括static 全局和局部变量)。静态区的内容在总个程序的生命周期内都存在,由编译器在编译的时候分配。

栈:保存局部变量。栈上的内容只在函数的范围内存在,当函数运行结束,这些内容也会自动被销毁。其特点是效率高,但空间大小有限。

堆:由malloc 系列函数或new 操作符分配的内存。其生命周期由free 或delete 决定。在没有释放之前一直存在,直到程序结束。其特点是使用灵活,空间比较大,但容易出错。

内存分配

Malloc函数: (void *)malloc(int size)

malloc 函数的返回值是一个void 类型的指针,参数为int 类型数据,即申请分配的内存大小,单位是byte。内存分配成功之后,malloc 函数返回这块内存的首地址。需要一个指针来接收这个地址,但由于函数的返回值是void *类型,所以必须强制转换成你所要接收的类型,也就是说,内存将要用来存储什么类型的数据。同样在堆上分配的地址空间并没有赋予名字,也就是说堆上的内存都是以匿名访问的形式进行。在使用malloc函数时应注意函数的返回值是不是一个可用的地址,也就是说要判定堆上的空间是否被分配成功,若失败,函数返回的时NULL。

 

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页