ios从入门到放弃之C基础巩固-----数组、字符串
前言:
接着上一次https://www.cnblogs.com/webor2006/p/14115791.html的c基础继续向前,学了这么长时间了,还没进入oc部分,真是学了个寂寞~~这次争取把C基础给结束了,如果还结束不了,那就。。再来一篇~~学习效率固然重要,但是我更看中学习效果~~
数组:
对于数组应该都比较熟悉了,但是呢在C语言中它也是有一些需要注意的地方,所以下面把觉得对自己有用的一些点给记录一下,其实吧这里面所谓“简单”的细节,并非人人都掌握了,所以还是值得过一过,目的是为了学IOS,但是也可以把C语言基础顺带巩固一下有利无害。
数组初始化注意点:
注意点一:
对于数组的初始化这块没啥可说的,定义数组时可以只其元素进行部分初始化:
定义了一个3个元素的整型数组,但是呢没有对它进行初始化,那运行结果会怎样呢?可以看一下:
是不是输出的值不都是为0?这就是需要注意的地方:如果没有对数组进行初始化(完全或部分),那么不要随便使用数组中的数据,因为有可能是一段垃圾数据(随机值),所以正确的使用姿势就是一定要对其数组进行初始化。
注意点二:
在定义数组时,也许会这样来定义:
那,我来对数组进行一个初始化,你会发现:
所以这个注意点出来了:不建议使用变量定义数组,如果使用了变量定义数组,作为数组的元素个数时,不初始化的情况下是随机值,而如果你想初始化是会直接报错的。
注意点三:
默认2,3这个初始化的值是赋值给了第一个元素和第二个元素对吧,但是!!!如果想改变这默认行为,只给指定的位置进行初始化,可以这么搞:
这里就当个语法扩展熟悉一下,实际这样用的不是很多。
注意点四:
在Java中,我们的数组可以先定义,后赋值,如下面:
但是!!!在C中是不行的:
所以需要注意:只能在定义的同时利用{}进行整体初始化,如果是先定义后再初始化则不能使用{}进行整体初始化了,只能使用[]单个元素进行赋值,如这样:
数组的内存存储细节:
对于数组来说,有必要了解一下它的内存存储细节,可以让你在使用上避免一些坑出现。
1、先回忆下变量的内存存储:
由于数组里面存储的其实就是一个个变量,所以先从变量的角度来看一下它的存储细节,其实这块在之前https://www.cnblogs.com/webor2006/p/13468408.html已经详细了解过了,这里当复习一下,比如定义这么一个变量:
整型占用4个字节,也就是32位,所以这里在excel中先准备一些模拟地址便于观看整个存储过程:
然后按照“内存地址是由大到小来寻址”的原则,此时会在这块开辟四个字节的空间用来存储该变量:
然后变量内容存储的是二进制对吧,而且是补码,由于正数的补码跟原码一样,所以将"10"转为二进制就为“0000 0000 0000 0000 0000 0000 0000 1010”,然后按高字节进行存储,也就是从左往右:
而对于变量的地址就是所占内存空间最小的地址,也就是:
2、数组的内存存储:
接下来再来看一下数组的内存存储细节,它跟变量还是有一些区别的,先来定义一个数组:
很显然该数组占用4个字节,所以开辟空间如下:
而具体数组的内容是如何存储的呢?这里要注意啦!!!变量是从高地址开始往低地址进行存储的对吧,而数组则是相反,它是从低地址往高地址进行存储的【但是内存寻址还是从高到低哟】,也就是:
那咱们可以验证一下,看是不是如咱们描述的按从低往高存的,把各数组元素的地址打印出来就可以了:
3、认识数组首地址:
目前咱们已经对于数组元素的内存存储逻辑清楚了,那对于数组这个变量,它也是有地址的呀,像变量它本身的地址是所占用空间最小的地址,那数组它本身的地址呢?跟变量一样,也是所占空间最小的地址,如下:
那么,咱们来打印看一下是不是这样:
确实是,另外还有个细节:其实数组名就是数组地址,啥意思?也就是这个意思:
也就是要知道:“&charValues=&charValues[0]=charValues”。
4、数组元素的内存存储:
其实,目前有一个细节描述是不太准确的,就是:
也就是接下来需要看一下,每个数组变量中它所对应的元素内容是如何存储的,其实,它的存储规则跟单个变量的规则是一模一样的,也就是“内存寻址从大到小,而内存存储也是从大到小”,这里为了方便进行说明这个问题,还是用int数组来举例,如下:
好,开辟八个字节的数组空间,因为2个int类型:
接下来就需要给元素的内容进行内存存储了,它的存储规则跟变量是一模一样的,所以将“5”、“6”转换成二进制为:
“0000 0000 0000 0000 0000 0000 0000 0101”、“0000 0000 0000 0000 0000 0000 0000 0110”,然后按从大到小的顺序进行存放如下:
5、看一个坑:
关于数组的使用上存在一个坑,也是稍不注意就会犯,这里先看一个例子:
这结果不很白痴么,当然等于1呀,好,看结果:
面对这结果,是不是一脸懵逼?那是什么原因造成呢?这上就需要你懂数组内存的存储细节啦,咱们来分析一下,先把数组的存储模型给画出来:
其中咱们写了个赋值代码:
很明显此时有下标3已经越界了对吧,最大应该只支持2,但是在C中是不会像Java抛数组越界异常的,它反而会有这么一个效果:
然后,错误的将nums[0]变量存储的内容改为44了,那么这也就是为啥输出结果是这样的原因了。
所以,一定要记住:在使用数组时,一定不要访问不属于字节的存储空间,这样会导致数据混乱的。这个例子还比较温和,没有抛异常,因为越界使用的是咱们自己申请的地址,假如你越界用到了一段不该使用的,可能程序直接就崩了。
下面再来串改一下,加深一下 这个坑的印象,也蛮有意思的:
原因是由于找不到位置为-1的nums,所以会找到它:
然后,结果就如预期了~~
函数与数组避坑:
函数传参问题了解:
对于这么一段程序应该人人皆知:
好,那如果这里传递的是数组呢?改一下程序:
值你认为是多少呢?运行:
为啥呢?其实也很好理解:
另外还有一个小小的细节需要知晓:如果数组作为形参,其元素的个数是可以省略的,也就是指的它:
这点其实也是比较熟悉了,不过这里拎出来是因为理解它之后,可以避免下面即将要写的例子的一个坑。
函数遍历数组的坑:
如果说叫你编写一个通用的遍历数组元素的函数,你会怎么写呢?很简单,如下:
这逻辑木毛病的吧,好,运行:
恭喜,成功入坑,如果这样实现,“永远”只能打算两个元素出来,为啥?其实还是跟数组的特性有关:
数组名是首地址,这一条在C语言中是非常重要的规则,那么要正确的打印,则元素个数需要由调用方来传递进来了,修改如下:
数组练习---进制查表法:
回忆十进制输出二进制:
接下来则来一个数组的小小练习,加强巩固,就是实现进制的转换,在之前https://www.cnblogs.com/webor2006/p/14115791.html咱们学过如何将一下十进制输出为二进制,回忆一下:
//进制查表法:可以很方便的输出任意进制 #include <stdio.h> void printBinary(int value); int main(int argc, const char * argv[]) { /* 0000 0000 0000 0000 0000 0000 0000 1010 */ int num = 10; printBinary(num); return 0; } void printBinary(int value){ //1、定义变量,需要向右移动的位数; int offset = 31; while (offset >= 0) { //2、通过循环取出每一位; int result = (value >> offset) & 1; printf("%i", result); //3、每次取出一位就让控制右移变量-1; offset--; } printf("\n"); }
运行:
对于这块思路还不太清楚的可以复习一下之前写的,这里就不过多啰嗦了。
不过,这次要实现一个通用的任意进制的输出功能,很强大,不过得基于这个程序慢慢的进行拓展,因为要达到这样的效果一下是很难想到的,所以接下来一点点进行改造。
offset改造:
对于目前代码中,offset写死了:
很明显这不是一个很好的编程习惯,我们可以动态计算出来,如下:
另外,对于*8这个很显然可以通过更牛逼的方式来写----位运算,如下:
对于这个位运算还不太清楚的可以参考https://www.cnblogs.com/webor2006/p/14115791.html:
变换打印思路:改从尾部输出
目前咱们的思路是从头部一位位输出的对吧:
这里将其改为从尾部进行打印,思路为:
接着让其右移一位,准备从尾部取第二位了,如下:
然后再让其右移一位,准备从尾部取第三位,如下:
好,咱们来实现一下:
为啥要这么改造呢?为了之后一个通用的封装,目前可能看不出头绪,木有关系,一点点进行演进,目前就知道可以倒序的进行进制的输出了。
完成八进制、十六进制的尾部输出:
好,按此思路接下来再来拓展一下,拓展的目的最终是要找出进制输出的规律,为封装成通用作铺垫,先来八进制的输出:
然后再来输出一下十六进制,比较简单:
这个很好处理,如下:
封装核心改造:
好,目前所做的一切改造都是为了封装打基础,接下来再改造就是非常非常关键的步骤了,这里先以十六进制的输出来改造,如何改呢?先来定义一个字符数组:
看到这个数组的定义是有规律的没?字符0所在数组的位置就是0、字符1所在数组的位置就是1、字符2所在数组的位置就是2...,那有这样一个规律,是不是我们只要取出4位二进制的值直接到这个数组对应位置去拿字符就可以正常输出每一位十六进制了?嗯,接下来往下编写:
那我们再试一下其它值,看一下输出是不是准确:
嗯,木问题,这就是查表法来输出进制的一个思想。
不过咱们目前的顺序是倒着的,换一个大点的值就可以看出:
接下来咱们解决一下这个问题,其实也很简单,其实就是把每个输出的字符先存到一个临时数组中,然后再正序将其输出出来就可以了,具体代码如下:
接下来咱们再来用这种思路实现一下八进制的输出,也是先来定义八进制的所有数值的数组:
然后调用试一下:
好,为了让其规律更加明显,再以此思想实现二进制的输出:
是不是一样的套路,这些套路相同也就为之后的通用封装提供了一个非常好的条件,先调用看一下输出是不是对:
万能进制查表法封装:
经过前面的铺垫,接下来就可以进行通用封装了,经过封装之后,任意进制的转换输出都非常之简单,而封装的思路也很简单,就是把可变的提到参数上,让方法体逻辑不变,具体如下:
1、定义查询字符数组:
其实这个数组就已经包含二进制、八进制的了。
2、定义临时结果数组:
这里定义一个32位的就成了:
其中pos的值可以根据rs数组的长度来确定对吧,所以将它改活:
3、输出逻辑:
其中,两个问号处是不确定的对吧,不同的进制输出其值是不一样,很明显可以作为参数提出来就成了,如下:
4、任何进制输出瞬间实现:
有了这个函数的封装之后,接下来就可以实现各种进制的输出,而且还非常之简单,不信,咱们来试试:
此时调用看一下对不对:
二维数组:
关于二维数组这里就提这么一点,就是在定义时,可以省略下标,具体如下:
但是!!!二维的大小是不能省的:
字符串:
字符串不管是哪个语言里都是非常重要的,所以对于这个知识点复习是很有必要的。
字符串的定义:
在c语言字符串其实跟字符数组的定义很类似,定义如下:
但是!!!它不是字符数组哟,为啥?那咱们再定义一个字符数组,把它们的长度打印一下就知道区别了:
那是因为,在C语言中,字符串是以“\0”结束的,所以字符串变量的元素个数比字符数组的元素个数多一个。
字符串的初始化:
其中对于字符串的初始化有如下几种:
其中特别要注意最后一种,由于是部分初始化时,其它未初始化的元素是0,而"\0"的ASCII就是0,所以符合一个正常的字符串的定义。
字符串的输出:
字符串的输出是这样的:
这就是所谓的脏读现象,其原因是“%s”它的原理决定的:从传入的“地址”开始逐个输出,直到遇到“\0”位置,下面从内存的角度来分析一下这种现象造成的原因:
由于内存寻址是从大到小,所以对于这段代码的内存存储情况如下:
接下来对于字符数组:
擦,hello拼错了,错了就错了吧,不影响学习,它的内存的细节为:
好,接下来准备输出了:
其中charValues是数组的首地址,而%s的原理是根据传入的地址,一个个字符进行输出,直到遇到\0才结束:
这也就是为啥输出的结果是这样的原因了,为了进一步验证%s是遇到\0会停止,那咱们修改一下代码:
另外输出还可以另外一个函数:
从输出结果中可以看到,会自动换行,不像printf需要我们手动使用"\n"来换行,另外它有一个缺点就是不能自定义格式,也就是在输出时在前面或后面加一些后缀之类的,但是printf是可以的,这一点有个了解既可,几乎用不上。
字符串的常用方法:
计算字符串的长度:
自己动手实现:
其实这个strlen的实现原理为:从传入的地址开始逐个取出字符串,每取出一个就让计数器+1,直到遇到\0为止。
接下来咱们根据这个原理自己来实现这个函数的功能,很简单:
其实吧,这些就做为c语言基础巩固既可,在OC中,算字符串的长度跟java类似,直接调一个length就o了,下面可以新建一个oc工程简单感受一下:
看着这OC的语法,真的觉得好神奇,字符串前面有个"@"符号,也好期待未来全面学习OC的那一天的到来~~
字符串连接函数
strcat【有注意点】:
但是有一个注意点:dest的数组长度必须大于拼接之后的长度,也就是说:
其中这个总长度还要包含一个"\0"字符,那我们试一下,如果不满足这一条程序会发生什么?
所以,这一点一定要注意,很容易犯错。
strncat:
正因为strcat有当dest长度不够时会报错的问题,strncat就出现了,它的使用其实也比较简单,先看一下函数原型:
咱们来使用一下它:
那么,对于这第三个参数,我们其实可以动态的计算出来,只要算出str1剩余空间个数传进来就可以了,如下:
所以说,这个函数使用更加贴切。
不用纠结:
你可能说,c语言中字符串拼接好麻烦呀,是的,但是!!!其实对于iphone开发不用过多纠结,因为OC是很简单的,这里再来提前感受一下:
也许你可能会说,也不简单呀,各种"@",看着头晕,那是因为还没搞懂OC的规则,这也是未来要学习的。
字符串拷贝函数:
strcpy:
先来看一下它的函数原型:
dst,目标;src,源,它会将源的数据拷贝到目标中,并且会覆盖掉目标原有的数据,下面来使用一下:
注意:它跟strcat类似,目标dst的容积必须能够存放拷贝的数据,如果容积不够是会报错的,试一下:
strncpy:
跟strcat一样,提供了另一个版本,该版本是不会报错的,使用方式也跟strncat类似,下面简单使用一下:
但是!!!有个注意点,接下来改一下程序:
也就是:该拷贝操作是逐个进行替换,拷贝了几个就替换几个。
这样第三个参数我们就可以根据源目标的字符串的大小来动态进行拷贝调整,如下:
字符串比较函数:strcmp
看一下函数原型:
使用一下:
练习:
接下来针对字符串做一个小小的练习加以巩固,这个练习描述如下:
“
编写一个函数char_contains(char str[],char key),
如果字符串str中包含字符key则返回数值1,否则返回数值0
”
比较简单,下面来实现一下,先搭建一个框架:
接下来则进行遍历,一个字符一个字符进行比较,思路比较简单:
再转换一个实现思路,也比较简单,代码如下:
其实这个代码还可以进一步精简,如下:
字符串数组:
字符串数组其实就是定义一个数组保存所有的字符串,直接看代码:
还可以这样定义:
总结:
本来吧,是想把指针的也给过一过的,学着学着发现细节越来越多,要一带而过吧又感觉不太踏实,本身C语言老早就学了,但是呢工作基本上用不到它,渐忘是肯定的,借此机会好好再复习一下对我来说是非常有利的,工作用不到的技术要想掌控它不只能在平常学习中去加强练习,IOS学到这,目前C语言的基础巩固还剩最核心的指针了,这个必须把所有细节都过一遍,下次正式开始指针之旅。