接着上一次https://www.cnblogs.com/webor2006/p/15415876.html继续往下,这次是C语言基础巩固的最后一篇,之后终于迈入ios学习的oc部分了,想想还是挺激动的,虽说学这么久了连门都还没入,但。。踏实的过了下C也是有些价值的吧~~
结构体:
接下来学习一下结构体,这个在之后IOS学习中是会经常被用到的,所以这里好好的过一遍。
基本概念:
关于它的定义没啥好说的,它是一个可以保存不同数据类型的构造类型, 与之对应的数组它是保存相同数据类型的,而定义结构体的格式为:
struct 结构体名{ 类型名1 成员名1; 类型名2 成员名2; …… 类型名n 成员名n; };
比如:
初始化:
定义了一个结构体之后,接下来则需要对它进行赋值初始化对吧,有几种方式。
1.先定义结构体变量,然后再初始化:
具体是指:
2.定义的同时初始化:
如下:
也可以这样:
3.指定将数据赋值给指定的属性:
另外,对于结构体中的属性可以进行指定,如下:
内存存储细节:
其实它跟数组的内存存储细节很类似,关于这块可以参考https://www.cnblogs.com/webor2006/p/15183259.html,这里先来简单回忆一下数组的内存细节:
1、内存寻址从大到小;
2、存储数组元素从小到大;
3、数组的地址就是数组首元素的地址;
存储原理:
元素类型一样:
先来定义一个结构体,里面的成员变量都是同一数据类型:
很明显,由于每一个整型占4个字节,总共该结构体的内存大小就是12个字节,木毛病,这里有个细节需要说明一下:
而它的内存存储细节如下:
其实跟数组存储一样,元素是从小到大地址进行存储的,下面咱们可以打印一下元素的地址来进行验证:
我们知道数组变量名就是数组的首地址对吧,那对于结构体变量名是否也是一样呢?下面打印一下:
其实,结构体的地址就是第一个元素的地址,如下:
元素类型不一样:
对于结构体存储大小最麻烦的就是里面的元素不一致的情况了,先来定义这么一个结构体:
此时的大小就不如预期了,为啥呢?其实结构体会首先找到所有属性中占用内存空间最大的那个属性,然后按照该属性的倍数分配存储空间,回到咱们这个程序来理解:
接下来再来修改一下结构体,看另外一个细节:
整个结构体的大小依然是占16个字节,重点是要知道它的存储细节,下面用图来剖析一下:
很明显这次age只用到8个字节的前4个对吧,还剩4个字节的空间,接下来:
而由于目前已申请的内存中还有空间容纳,所以此时就不会再申请了而是直接用在这了:
其中不还是浪费3个字节的空间么?是的,这其实涉及到内存对齐的意义了,下面会说到,这里反正先记住内存分配的规则,好!!!接下来将元素位置调整一下,绝对会亮瞎你的眼:
其实分配原则还是不变,下面用图再来挼一下:
嗯,好浪费呀,还剩四个字节木有用到,好,继续往下:
最后:
我去,真是好豪呀,浪费了这么多空间:
总结一下整个分配细节哈:
1、找到结构体类型中占用存储空间最大的属性,以后就按照该属性占用的存储空间来分配。
2、会从第0个属性开始分配存储,如果存储空间不够就会重新分配,如果存储空间还有剩余,那么就会把后面的属性的数据存储到剩余的存储空间中。
3、会从第0个属性开始分配存储,如果存储空间不够就会重新分配,并且会将当前属性的值直接存储到新分配的存储空间中,以前剩余的存储空间就不要了。
结构体数据成员对齐的意义:
对于上面有一个细节需要探讨一下,就是为啥在结构体存储时总是以里面元素占用最大的那个进行空间申请呢?许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的起始地址的值是 某个数k的倍数,这就是所谓的内存对齐,而这个k则被称为该数据类型的对齐模数(alignment modulus)。这种强制的要求一来简化了处理器与内存之间传输系统的设计,二来可以提升读取数据的速度。比如这么一种处理器,它每次读写内存的时候都从某个8倍数的地址开始,一次读出或写入8个字节的数据,假如软件能保证double类型的数据都从8倍数地址开始,那么读或写一个double类型数据就只需要一次内存操作。否则,我们就可能需要两次内存操作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的8字节内存块上。
言而总之,也就是内存对齐存在的意义就是为了提高程序读取的效率,它会以牺牲一点内存空间作为代价。
结构体变量占用存储空间大小:
关于结构体的内存对齐大小是可以使用pragma pack(指定的对其长度)进行指定的,关于这块更详细的内容可以参考我在13年学习C这块的笔记https://www.cnblogs.com/webor2006/p/3462258.html,对于整个结构体的细节记录得非常详细了。
类型定义方式:
关于结构体的定义这里再来看一下,总共有三种方式:
1、先定义结构体类型,再定义结构体变量:
2、定义结构体类型的同时定义结构体变量:
但是,这种定义方式需要注意,就是我还可以拿这个结构体继续定义:
3、定义结构体类型的同时定义结构体变量,并且省略结构体名称:
这种定义方式有一个弊端:由于结构体类型没有名称,所以以后就不能使用该结构体类型了,但是!!!有一个优点:如果结构体类型只需要使用一次,那么这种方式就比较合适 。
类型作用域:
局部结构体:
跟变量一样,也分为局部和全局,先来看一下局部结构体:
则这只能在定义以下位置到函数结束之前进行使用,出了函数则就不可以了,另外如果定义在代码块中:
则只能在定义的位置至代码块结束的位置进行使用,这个规则跟变量是一样的。
另外结构体有可能在不同作用域中名称一样,如下:
那我们初始化的是哪个结构体呢?其实很好验证,咱们将大扩号作用域的Person结构体删除一个字段,然后咱们的初始化报不报错:
也就是在不同作用域中如有同名的局部变量,则就会以最近原则进行访问。
全局结构体:
这个跟变量差不多,写在函数外面就是一个全局的结构体了,如下:
结构体的指针:
结构体指针的定义与初始化:
这个也跟变量一样,直接使用一下:
通过结构体指针访问结构体成员:
那,接下来就得看一下如何通过指针的方式来访问结构体成员了,有如下两种方式:
1、(*结构指针变量).成员名:
接下来咱们通过打断点的方式来查看结构体的值是否被改了:
然后,此时在这个窗口中就可以敲如下命令进行值的查看了,类似于Android studio的watch功能:
感觉好麻烦呀,有木有不需要敲命令也能查看到断点调试中的内容呢?有的,得打开这个窗口:
另外,注意一个小细节:
如果省了,则会报错:
这是因为“.”号的优先级要比“*”号要高,所以需要加括号提权才行。
2、结构指针变量->成员名:
对于结构体指针,还有另一种访问方式,这种方式才是常用的,如下:
结构体-数组:
这里直接看程序使用一下既可,比较简单:
结构体-嵌套:
为啥要有结构体嵌套呢?其实也很好理解,像Java中的Model类中是不是还可以套其它Model,所以这里也没啥好说的,直接上代码过一遍既可:
// 结构体嵌套 #include <stdio.h> int main(int argc, const char * argv[]) { struct Date { int year; int month; int day; }; struct Time { int HH; int mm; int ss; }; struct Person { int age; int *name; struct Date birthDate;//出生日期 struct Time brithTime;//出生时间 struct Date admissionDate;//入学日期 struct Date graduateDate;//毕业日期 }; struct Person sp = { 18, "cexo", { 1990, 2, 11, }, { 15, 12, 8, }, { 1996, 6, 11, }, { 2005, 6, 11, }, }; printf("year = %i, month = %i, day = %i \n", sp.birthDate.year, sp.birthDate.month, sp.birthDate.day); return 0; }
运行:
结构体和函数:
那结构体如果作为函数的形参,如果函数中修改了形参的这个结构体会不会影响到结构体本身呢?下面来试一下:
成员值作为函数的参数:
这个比较容易理解,本身函数的形参是一个int类型,当然是值传递。
结构体变量名作为函数的参数:
那如果函数的形参本身是一个结构体类型呢?下面看一下:
说明结构体作为函数的形参,其实还是值传递。那如果结构体之间的赋值呢?下面试一下:
结构指针作为函数的参数:
一看到指针,肯定不用想,传递的是引用,当然也就能通过形参来更改本身的值了,如下:
枚举:
关于枚举,这块也是没啥可说的,这里用一用复习一下:
也就是默认情况下枚举的值是从0开始的,但是!!!这个默认值你可以改,如下:
接下来咱们再来定义一个枚举用来表示四季:
很正常的一个枚举,但是这里要说一个枚举好的命名的事, 通常枚举命名以k开头,后面再跟上枚举类型的名称,最后再跟上当前枚举值的含义,另外还有一个就是枚举名称通常首字母要大写,所以这块写得不规范:
修改一下:
另外咱们以上面的规则咱们来修改一下枚举,看一下修改之后的可读性是不是大大增强了:
通过这样修改之后,有这么一个好处,就是取枚举值时,都以k开头:
根据枚举类型就能知道枚举里面包含哪些选项,可读性大大增强。
全局变量和局部变量:
局部变量:
关于局部变量这块其实没啥可说的,这里理论的把这块的总结一下:
-
-
作用域:从定义哪一行开始直到与其所在的代码块结束
-
生命周期:从程序运行到定义哪一行开始分配存储空间到程序离开该变量所在的作用域
其中它有如下特点:
-
-
2、不同作用范围可以定义同名变量,内部作用域的变量会覆盖外部作用域的变量
其中局部变量有一个注意点就是如果未对它进行初始化,它是一些随机的值,所以在开发中千万不要使用未初始化的局部变量,关于这块用程序演示一下:
发现貌似没打随机值,其实这块并不是一定是随机值,但是只要有可能随机,就会造成程序出错,所以记住一定要对局部变量进行初始化这个原则就成。
全局变量:
对于全局变量,这里也先理论化的描述一下:
-
-
作用域范围:从定义哪行开始直到文件结尾
-
生命周期:程序一启动就会分配存储空间,直到程序结束
-
存储位置:静态存储区
它的特点:
而全局变量又分为外部全局和内部全局,那具体这俩有啥区别呢?下面来看一下。
外部全局变量:
概述:
默认情况下所有的全局变量都是外部全局变量,而所谓的外部全局变量是指可以被其它文件访问的全局变量,它的主要特点是:
1、可以定义同名的外部全局变量;
2、多个同名的外部全局变量指向同一块存储空间;
实践:
这就是一个外部全局变量,那它不是可以被外部文件所访问么?那下面咱们新建一个文件:
然后此时到other中来访问咱们定义的这个全局变量:
因为没有在other.c中进行声明,这里声明一下:
而为了能在main.c中调用到这个函数,需要将函数声明在它的头文件中:
此时就可以在main.c中来调用它了:
但是编译报错了:
说定义的num变量重复了,但是这跟上在理论概述所说的“全局变量是可以定义多个同名的”违背了呀,其实这是由于Xcode升级之后造成的,在Xcode6以前是可以的,这里可以用终端编译验证一下:
为了验证确实是打印的main.c中所定义的全局变量,这里做一个修改:
来,再来手动编译运行一下:
而为啥Xcode不允许这样搞呢?原因也比较容易理解,由于全局变量是指向同一块存储空间,如果在多个文件中有同名的外部全局变量,可能导致A文件的数据被B文件不小心修改了,降低了数据的封装性,提高了发生错误的概率。为了说明这点,咱们在other.c中来修改一下num:
再来运行:
而要想让xcode可以正常编译,需要在这块修改一下:
内部全局变量:
概述:
只要给全局变量加上static关键字就是内部全局变量,而所谓的内部全局变量是指只能被当前文件访问的全局变量,它的主要特点是:
1、也可以定义多个同名的内部全局变量;
2、多个同名的全局变量如果不在同一个文件夹中,那么指向不同的存储空间。
实践:
在上面程序中,发现使用extern声明全局变量还是有一个问题,就是全局变量会被其它文件所修改,那为了避免这个问题,咱们可以使用static关键字来将这个外部全局变量转变成内部全局变量,也就只能在当前文件来修改了,如下:
此时再运行就报错了:
说明此时内部全局变量确实是不能被外部文件所访问了,而这里再做一个修改:
另外对于extern它还能这么用:
这里对static和extern修饰全局变量再来梳理一下:
extern:用于声明一个外部全局变量。
static:用于定义一个内部全局变量。
声明和定义的区别:声明是不会开辟存储空间的,而定义是会开辟存储空间。
static和extern:
static对局部变量作用:
对于static它不仅能修饰全局变量,其实它也可以修饰局部变量,下面来看一下:
这个程序没啥可说的,太简单了,接下来加一个static关键字,你就能感受到它修饰局部变量的作用了:
也就是被static修饰的局部变量有累加的功效了,这是因为当使用static来修饰局部变量时,会延长局部变量的生命周期,并且会更改局部变量存储的位置,将局部变量由栈转移到静态区中,只要使用static修改局部变量之后,当执行到定义局部变量的代码就会分配存储空间了,但是只有程序结束之后才会释放该存储空间,相比全局变量而言它要弱一点,因为需要执行到这一行才会分配,而全局变量是只要程序一启动就会分配。
那。。它具体有啥实际价值呢?比如看这么一个程序:
其中你会发现这个程序中的pi局部变量是不是每次的值都是固定的?那目前这个每次调用都会重新分配空间,是不是很浪费资源?所以,此时给它定义成static是最合适的:
所以总结一下它的使用场景:
当某个方法的调用频率非常高,而该方法中有些变量的值是固定不变的,此时就可以使用static来修饰该变量,让该变量只开辟一次存储空间, 这样可以提高程序的效率和性能。
static和extern对函数作用:
对于static和extern来说,它还能修饰函数,所以下面来看一下,这里新建一个c文件:
而它要被其它文件使用,需要先在头文件中声明一下:
然后就可以这样调用了:
其实函数也分为内部和外部,默认情况下所有的函数都是外部函数,所谓的外部函数就是指能被其它文件所访问的函数,而相对的内部函数就是指只能在当前文件访问的,这块跟全部变量的概念是一样的,下面咱们将test定义成内部函数:
此时在main中访问一下:
此时运行会报错:
原因就是由于此时的函数是一个内部函数,貌似咱们没有在other.h头中来声明这个函数对吧,咱们来改一下:
运行还是会报错,所以在头文件中声明内部函数是没有任何意义的,此时只能在other.c内部进行调用了:
再运行:
总结:
关于C这块还剩宏定义、条件编译、文件包含、typedef、conts关键字这些木有学完,本想一口气都写在这篇,但发现篇幅已经过长了,还是再拿一篇对剩下的C语言进行收尾,总之不把这个C收尾,其它知识的学习暂不考虑,所以下篇继续。