C语言数组
数组是一种内存分配的形式,数组是定义了一个连续的空间,与指针类似。
数组定义方式
类型 数组名[数组大小];//类型为每个空间的大小,我们通过数组名和下标访问数据,数组名指向存储数据的首地址,但是我们不可以改变数组名指向的地址。数组大小只有在定义的时候生效,定义之后不可以被改变。
int a[100]; //申请了一个名叫a的数组,它包含了100个整型。
数组的访问
我们可以通过数组的下标访问数据,C不会检查我们的下标是否合格,就像指针不会检查越界一样。数组名是不可以进行加法和减法操作的,比如a++等等,这样还是相当于意图改变它指向的首地址,C中是不允许的。
数组空间的初始化
数组空间的初始化就是为每一个标签地址赋值。按照标签逐一处理。如果我们需要为每一个内存赋值,假如有一个int a[100];我们就需要用下标为100个int类型的空间赋值。这样的工作量是非常大的,我们就想到了让编译器做一些初始化操作,初始化操作是第一次赋值,第二次赋值就不能再这样赋值了。
int a[10]=空间;
我需要给它一个空间,让它对这里面的值进行批量处理;比如int a[10]={10,20,30}; //a[1]=10,a[2]=20,a[3]=30,a[4]=…=a[9]=0所以实际上还是批量内存拷贝的结果,未赋值的结果就是0。初始化与我们逐一赋值的效率是一样的。批量拷贝仅限于第一次赋值。
注意:数组空间的初始化和变量的初始化本质不同,尤其在嵌入式的裸机开发中,空间的初始化往往需要库函数的辅助。
char buf[10]={'a','b','c'}; //c的字符串有一个重要的属性,字符串结尾有一个字符'\0'代表字符串的结束。因为我们这里为字符串空间定义了10个字符的大小,但是有时候我们并没有使用完这10个字节,打印的时候10个字节的打印也是错误的,所以为了给字符串一个结束的标志,让其它的函数比如printf函数输出的时候知道它的结尾。
char buf[]="abc"; //省略数组大小,C会为数组指定大小,这里就是三个字符加上一个结尾标志,数组大小就是4
char buf[10]={"abc"};
char buf[10]="abc"; //将三个字符批量拷贝到它分配的空间,末尾加'\0'
char *p="abc"; //先给三个字符分配空间,再将指针指向字符串的首地址,这指向的是一个常量字符串。
buf[2]='e'; //这是可以的
p[2]= 'e'; //这是不可以的,p指向的是常量区
测试代码如下
我们将这个字符串作为printf函数的参数,它返回的实际上是常量字符串的首地址,而我们这里将它作为一个整型来打印,有一个警告,应该把它作为无符号整型(内存地址是正整型的)来打印。
#include <stdio.h>
int main(){
printf("the %x\n", "abc");
}
运行结果如下
the 404000
可以看到的确打印了一个地址,说明双引号返回的的确是一个地址,正好和我们前面介绍的字符数组获取到它的首地址,然后依次拷贝,但是字符指针就直接得到它的地址存储。这些不同的操作就是数据类型具体的操作了。
C语言数组初始化进阶
数组空间的初始化
char buf[10]="abc";
a="Hello World";
我们知道双引号里面的字符串是常量,系统为它们分配一个空间之后,企图将内存首地址赋值给数组名。我们前面已经说过,数组名存储的首地址不可以被改变,由于前面已经被初始化,数组名指向的地址已经确定,所以不能被再次赋值。
数组第二次的内存赋值只能对它们逐一赋值,如下:
buf[0]='h'; buf[1]='e'; …… buf[n]='d'; buf[n+1]=0; //结尾标志
字符拷贝函数的原则:内存空间和内存空间的逐一赋值的功能的封装。一旦空间中出现了结束标志0,函数就即将结束。
strcpy:通过在终端里面键入man strcpy查看函数声明。这个函数声明的时候,源字符串是常字符串,目的字符串不是常量的字符串。这是符合常理的,毕竟我们拷贝这个字符串却是不需要改变它的值的。这个函数接收两个字符串指针,将源地址里面的内容逐一拷贝到目的地址,拷贝在遇到源字符串的结束标志0结束拷贝。
我们可以想象出一个strcpy函数的模板:
int i=0;
while(src[i]!='\0') {
dest[i] = src[i];
i++;
}
随着硬件的升级,硬件已经能够帮助我们做一些事情,所以有些strcpy的代码里面会有一些汇编语言,汇编语言可以进一步提高函数的效率。所以如果我们在面试嵌入式开发工程师的时候,可能就会有面试官叫你写一个strcpy的标准函数,你可以将上面这个形式写出来,但是还要加上一句话,根据CPU的体系结构和能力不同,这段程序是可以优化的。等以后我们看到具体的CPU和内核之后,就明白了为什么这个strcpy不是我们想象的这么简单,我们只是能够猜测出它的目的,但是具体实现细节不一定就是上面的函数代码。
此时我们就可以简略的为buf赋值了。
strcpy(buf,"hello world"); //将hello world这个字符串拷贝到buf中,并自动在结尾加上结束标志0。
strcpy函数可能导致内存泄漏,如果有黑客使用一个很长的字符串作为源字符串拷贝到内存中,因为遇到源字符串的结束标志这个函数才会停止拷贝,它就有可能覆盖掉内存里面原来的数据,进而导致很多问题。在工程中我们是不允许使用这个函数的。我们使用的是strncpy这个函数,它提供了第三个参数,接受一个拷贝大小的值,这个时候只会拷贝这么多的字符,不会导致内存泄漏。
C语言数组初始化实战
数组空间的初始化
字符空间就是我们操作的最小空间,还有int类型,float类型,结构体类型的空间,这些类型的要求就没有字符空间的要求那么多。字符空间涉及到字符串的概念以及结束标志。我们接下来介绍的就是非字符空间。
字符空间:可以使用ASCII码解码的空间,我们可以看懂,使用%s查看,\0为结束标识符。
非字符空间:比如物联网中的数据采集,室内煤气安全,光照,温度检测等等,采集的数据都是以8位或者8位的倍数来存储的,这些数据没有正负号,也不是ASCII码。我们要存储这些数据,就需要使用unsigned char buf[10],我们可以使用字符串的strcpy将感温设备里面的数据直接拷贝出来吗?显然是不行的,因为感温设备里面的数据不是存储的字符串,没有结束标志,就会一直拷贝,这会导致内存泄漏的。
为了实现非字符空间的拷贝,C为我们提供了一个新的函数memcpy。
void *memcpy(void *dest, const void *src, size_t n);
它具有三个参数,目的地址,源地址,拷贝字节。int类型占4个字节,所以下面拷贝的字节应该是40个字节。
int buf[100];
int sensor_buf[100];
memcpy(buf, sensor_buf, 10*sizeof(int));
这个memcpy看起来参数和strncpy是一样的,但是它们内部的实现机制是不一样的。如果我们采用的是strncpy,存储的数据中有\0这个结束字符,拷贝就会提前中断。注意:memcpy第三个参数传递的是字节数,不是类型的个数,标准写法应该是 个数*sizeof(类型);
字符空间和非字符空间都是我们将要考虑的,C语言的目的就是对资源进行操作,这些资源就是内存里面的数据。在以后的面试过程中,面试官给我们提出一些问题。首先我们先将题目中操作的对象找到,分辨它是一个字符类型还是非字符类型,其次再说怎么操作,比如加减乘除等等,面试也就是这个道理,大家也不要将它想象的太复杂。
C语言指针数组的概述
指针数组的概述
熟悉了数组空间的概念之后,我们知道int a[100]存储的是int类型的数据,如果这里面存放的是地址,那么这就是指针数组。指针数值就是数组里面存放的是指针。我们如下定义指针数组;
char *a[100]; //a存放了100个指向char类型的指针
通过sizeof(a)可以看到它100个指针变量的大小为100*4(4是指针变量的大小)。指针数组和二维指针是十分相似的。但是它们的区别在下图可以体现。
C语言数组名的指针保存
数组名的指针保存
定义一个指针,指向int a[10]的首地址:
int *p=a;
对于int类型的数组和指针来说,它们读取的方法是一样的,都是以四个字节为单位进行读取。所以这样的赋值方法是正确的。
定义一个指针,指向int b[5][6]的首地址
我们是不可以使用int p=b这样的二维指针来赋值。我们先来分析一下结果为什么不对。二维数组读取的时候是一行一行的读取,而二维指针读取的是线性表。当我们定义一个char *p的时候,我们明白了读取的方式就是char类型。但是现在的需求不一样了,不再是一个简单的char类型读取方式了。
我们知道int a[6]; a就是一次读取6个int类型,所以我们现在需要指针也能够像这样读取,用指针变量去代替这个普通变量。就有了下面的定义:
int (*p)[6]=b;
[]的优先级高于*,在我们加了一个括号之后,定义了一个指向二维数组的指针p,p指向的是二维数组的首地址。P[1]指向的就是第二行的首地址。现在p的单位,一次读取6个int类型的数据。P[1][1]指向的就是第二行第二列的数据。前面的p[1]返回第一行的首地址,后面的[1]代表再移动一个单位,这个就是int类型的大小,移动4个单位。