c指針&數組

轉載:https://blog.csdn.net/u012741741/article/details/53242940

指针的概念

指针本质上是一个变量,和我们定义的普通变量没有任何区别,唯一不同的是,指针这个
变量的值,是某个变量的内存地址,也只能用来存储变量的内存地址,指针出现的目的,
是为了实现间接访问,其实就是使用CPU的寻址方式间接访问,间接寻址是由CPU设计的时
候决定的。

//指针使用的三个步骤

    定义指针变量:int *p;此时指针没有指向任何变量,且不能被解引用
    绑定指针:p = &a;给指针变量赋值,让指针指向另外一个变量
    解引用指针1:int b = *p;取出指针所指向的变量的值,并赋值给另外一个变量。
    解引用指针2:*p = 555;把555放到指针所指向的变量中去,等价于a = 555;

指针被定义的时候,根据变量的额基本规则,指针变量的值是一个随机的内存地址,此时
虽然指针可以被解引用,但是解引用后改内存地址是否可以被访问却是一个未知数,强制
解引用并访问该内存地址有可能造成系统崩溃,所以一般情况下我们会先对指针变量进行
绑定,然后才进行解引用,未经绑定就进行解引用指针操作是非常危险的操作。

//指针相关的符号
编译器通过符号来理解代码,正确理解C语言中符号才能像编译器一样的理解代码

*星号
在C语言中,号可以表示乘法,也可以表示指针符号,两种用法毫无关联,只是恰好名字一
样,号在指针相关的时候有两种用法,第一种用于指针定义,*号结合前面的类型用于表明
要定义的是一个特定类型的指针,例如int *p表明要定义一个int类型的指针,第二种用在
指针变量之前,表示要对该指针变量解引用,例如int b = *p;表示对指针变量p进行解引
用,得到其指向的变量的值。

&取地址符
取地址符号&用于取出一个变量的地址,把取地址符号放在变量之前就可以取出该变量的地
址,取出地址后常用于给指针变量赋值,例如int *p = &a;表示取出变量a的地址,赋值给
指针变量p

=号,左值和右值
=用于赋值,常用于连接左值和右值,格式为左值 = 右值,当一个变量做左值的时候,编译
器只关心该变量对应的那段内存空间,而不是该内存空间里存储的值,当一个变量做右值时,
编译器只关心变量对应的内存空间中存储的值,而不是这段内存空间,例如a = b;这段代码,
编译器只会关心右值b中的值是多少,至于b的内存地址,内存空间大小,占用多少字节,在
这段代码中一律不重要了,对于左值a,编译器只会关心a的内存空间,至于a变量的值是多少
已经无所谓了,所以,可以把赋值符号,左值,右值总结为:通过赋值符号,将右值变量中
的值存储到左值变量对应的内存空间中去。

野指针
野指针指的是那些指向位置不可预知,不正确或者没有明确限制的指针,使用野指针很可能
触发运行时的段错误,因为指针变量在定义时如果未初始化值是随机的,如果贸然对该指针
进行解引用,会访问到不被允许的内存空间,从而触发段错误

野指针的三种结果

    .指向了一段不可以被访问的地址,例如内核空间,此时会触发段错误
    .指向了一段可以访问,但没什么意义的内存地址,例如已经释放掉的栈或者堆空间,此
    时程序不会出错,也不会对程序造成什么危害,但是会隐藏这个野指针错误,使人难以察觉
    .指向了一段可用地址,并且这个地址正在被程序中的某个变量使用,此时操作指针会无
    缘无故的修改这个变量的值,导致程序崩溃或者计算错误

野指针的避免
野指针的值是不可预知的,所以我们只需要保证亮点即可避免野指针造成的危害:

    给野指针指定一个明确的可以访问的地址值,这样解引用的时候就不会出现问题
    在野指针还没有明确的可以访问的地址值之前,不去解引用指针,也不会 造成危害

在实际中,我们常常通过下面的方式来避免野指针:

    定义指针时同时初始化为NULL
    在指针解引用之前先去判断该指针是否为NULL
    指针使用完毕之后将其赋值NULL
    指针使用之前,为其绑定到一个可用的地址空间上去

NULL

NULL实际上就是0.指的是操作系统中的0地址,0地址作为特殊地址,我们认为指针指向0地址就表
示该指针为野指针,没有被初始化,另外,在一般的操作系统中,0地址是不可以被访问的,如果
用户不检查就去解引用,就会触发段错误。

const关键字

const在C语言中用来修饰变量,表示该变量被定义成了常量
const修饰指针的四种形式

    const int *p;指针本身不是常量,指针所指向的是一个常量
    int const *p;指针本身不是常量,指针所指向的是一个常量
    int * const p;指针时一个常量,指针所指向的是一个变量
    const int * const p;指针是一个常量,指针所指向的也是一个常量

从上边的现象可以看出,在有const的指针变量定义中,号之前有const表示指针所指向的是一个常量,
号之后有const表示指针是一个常量,如果*号前后都有指针,则表明指针和所指向的都是常量。
数组

数组本质上就是在内存中,连续多个内存地址相接组成的一段内存空间,从编译器的角度来讲,数组变
量本身也是变量,和普通定义的变量没有什么不同,数组变量属于在一段内存空间中,一次分配多个变
量,而且这多个变量的地址是依次相连的,
数组和变量

那我们又知道,变量本质其实就是一个内存地址,在编译器中可以根据该地址找到一个具体数值,该具
体数值和该变量名绑定,变量类型决定该地址的连续长度,所以数组也是一个数据类型,该类型也有地
址连续长度,连续长度是由定义数组时给定的,这也就解释了定义数组时必须要给定长度,并且数组一
旦定义,长度就固定不能改变的原因,

数组中由于元素内存地址相互连接,这就为我们使用指针来访问操作数据提供了巨大的便利,我们简单
的对指针进行加减即可访问数组中的任一元素,可以说数组本身就适合用指针来操作,天生是相互匹配的
数组的符号

定义一个数组int[10] a,则数组名为a,a做左值时,表示整个数组的全部空间,由于C语言中,操作数组
只能操作单个元素,不能操作数组整体,所以a不能做左值,也就是说,一个数组名不能做左值出现,初
始化除外。

a做右值时,表示该数组的首个元素的地址,也就是a[0]的地址,由于数组中首元素的地址就是数组的地
址,所以也表示该数组的地址,等同于&a。由上面知道a[0]表示数组的首个元素,当a[0]做左值时,表示
数组第0个元素的内存空间,做右值时,表示数组第0个元素的值

&a符号,字面上表示该数组的地址,由上可知,在编译器为该数组分配内存空间之后,该内存空间的地址
和大小就不能改变了,所以实质上&a是一个常量,所以不能赋值,也就不能做左值,做右值时,表示整个
数组的首地址,需要注意的是,&a表示整个数组的首地址,而a表示数组中首元素的地址,两个在概念上
是完全不一样的,只不过值恰好一样罢了

&a[0],表示数组中第0个元素的首地址,做左值表示数组首元素对应的内存空间,做右值表示数组首元素
的值,做右值时&a[0]等同于a。
以指针方式访问数组

例如,有以下代码:

int main(void) {
    int a[5] = {1,2,3,4,5};
    printf("a[3] = %d.\n",a[3]);
    printf("*(a + 3) = %d.\n",*(a + 3));
    return 0;
}

   可以看到,使用指针方式访问和使用数组访问是一样的。第二种方式中,a是作为右值的,相当于取a的地址,
又因为a的地址和其中的第0个元素地址相同,再加上3就是第三个元素的地址,再使用*来解引用,就得到了
第三个元素中的值。

站在内存的角度中,数组是内存中的一片连续的存储区域,元素地址是依次连接的,元素的数据类型又是一样
的,占用的空间也是一样的,所以使用指针来访问是很合适的,所以使用指针相当于操作数组中元素的地址,
使用地址前移后移就可以很方便的访问数组中的每个元素。但是要注意的是,指针类型要和数组中的数据类型
一致,否则计算单位的宽度不一样,会导致访问出错,编译也不会通过,计算指针位置的时候,也是根据元素
的数据类型单位宽度来进行加减的,而不是按照具体的byte为单位的。
指针的类型转换
变量的数据类型

我们知道,所有类型的数据在内存中都是按照二进制的方式存储的,内存中只有0和1,并不知道是int类型或者
是float类型,至于数据类型,只不过是占用内存大小不同,放入取出的方式不同而已,整型数据把数据转换为
二进制放入内存的方式是相同的,float和double的存储方式各不相同,所以说,内存只负责存储,至于占多大
内存,如何取出,如何解析,则是数据类型层面的事情。

C语言中的数据类型的本质,就是决定了这个数在内存中怎么存储,如何转成二进制以及如何用二进制来表达的
问题,内存只负责存储1010类型的数据序列,而不管这些数据如何解析和理解。至于如何解析和理解,就是数
据类型的概念了。
指针的数据类型

指针的本质,是一个变量,所以指针也有一个自身的类型,这个类型就是指针类型,指针涉及两个变量,一个是
指针变量自身,一个是指针所指向的那个 变量,所以指针也涉及到两个类型,一个是自身的指针类型,一个是
锁指向的那个变量的类型,例如int *p;p就是指针变量本身,类型是int *类型,我们可以得到,*p就是指针所指
向的那个变量,类型是int类型的。指针类型数据占4个字节。
指针数据类型解析

指针类型的数据是按照地址的方式来解析的,将指针变量中的32个bit加起来,表示一个内存地址,所以也有说法
是说,指针是解地址的。所有的指针类型,都是解地址的。

指针所指向的那个变量,跟指针的类型有很大的关系,因为所指向的变量的类型在定义指针类型的时候就一并确定
了,例如上面的int *p;,只要这么一定义,该指针p所指向的变量的类型就确定了,就是int,不论以后这里边放
的数据是多少多大,类型始终是int型的,要按照int类型存取解析数据,和指针变量解地址是有很大的不同的。
指针数据类型的转换

指针进行强制类型转换是有很大风险的,不兼容的数据类型转换,一定会出现错误,例如int转float,但是兼容的
数据类型转换,则不一定会出错,例如char转int不会出错,但是int转char有可能会出错,要看int的数据有没有
超过char的范围。
指针,数组和sizeof

sizeof是C中的运算符,并不是一个函数,只不过是用法和函数类似罢了,sizeof主要用于计算后面()内的变量或
者数据类型所占用的内存字节数,sizeof一个主要应用方面是测量各种数据类型在不同平台中所占用的内存字节数,
例如int在32bit系统中是4字节,在64bit系统中是8字节。
使用sizeof测量变量和类型

sizeof测量char[]类型的字符串,返回该字符串的字符数组长度,包括字符串后面的’\0',使用sizeof测量一个变量
的类型,和使用sizeof测量该变量本身,结果是一样的,使用sizeof测量一个数组名,得到的是该数据在内存中总共
占用的字节数,使用sizeof测量一个取指针(*p),相当于测量该指针所指向的变量占用的字节数。
函数内部测量数组

在一个函数内部,如果形参数一个数组,使用sizeof测量该数组名,则会返回一个指针的长度,因为如果形参是数组,
实际传递的时候并不是传整个数组进来,而是传数组的指针进来,所以这个时候使用sizeof测量数组名,测量的是一
个指针的大小。所以在当数组作为函数参数时,通常还要传递一个int类型的形参,表示数组的长度。
测量自定义类型

在使用sizeof测量使用#define或者typedef定义的变量的时候,返回的结果会是原来类型的长度,例如#define char * p;
则sizeof(p)的长度就是指针类型的长度。
指针和函数参数

函数的参数类型可以有很多种,普通变量,数组,结构体,指针,都可以作为函数的参数。
普通变量作为函数参数

普通变量做为函数参数,其实本质传递的是变量的值,关于这点,可以在函数内部和外部分别打印形参和实参的地址,
可以看到这两个的地址是不一样,就说明普通变量作为函数参数时,传递的实际上是实参的值拷贝,而不是实参本身,
这样的参数传递也叫作值传递。
数组作为函数参数

当数组名作为函数参数的时候,形参得到的是实参的内存地址,也就是实参的指针,如果把形参和实参的地址打印出
来,可以看到实际上是同一个指针变量,所以说实际上是把实参的指针传递到了形参,这种传递方式也叫作传址调用
或者引用传递。
指针作为函数参数

指针作为函数参数和数组作为函数参数是一模一样的,本质上,都是传递的函数的内存地址。
结构体作为函数参数

当结构体作为函数参数的时候,其本质和普通变量作为函数参数一样,把结构体的值赋值给了形参,也是值传递,但
是在实际编程上,我们通常会传递结构体的指针,因为值传递中,如果结构体比较复杂,赋值的过程就会耗费资源,
造成性能下降,并且,使用指针传递,还可以方便的使用->符号来操作结构体中的条目。
传址调用和传值调用

传值调用表示的是,实参本身并没有进入函数的内部,而是复制了一份自身的副本进入到函数内部,这个副本和实参
的值是一样的。而传址调用的时候,函数的形参就是实参的地址,我们通过操作指针的方式,操作这个地址,从而达
到操作该地址所指向的变量。从更根本上来说,C语言本身并没有传址和传值两种调用方式的区分,其本质都是传值的,
如果希望实参可以被改变,就传该实参的指针值进去,否则就传递实参的值进去,不论传递的是实参本身的值,还是
传递实参指针变量的值,其实都是传值调用,理解了这一点,可以对上面的两种传递方式理解的更为透彻。
输入型参数和输出型参数

C语言的函数参数,既可以作为输入,也可以作为输出,就看是如何定义和使用这个参数。
函数的参数和返回值

函数中,一般函数名是一个符号,表示整个函数代码段的首地址,在程序中函数名的实质是一个指针常量,指向该函
数,程序中一般将函数名当做地址使用。函数的形参是函数的输入部分,返回值是函数的输出部分。形参和返回值不
是必须的,没有这些,函数也可以对数据进行加工,我们有多种方式可以使函数对数据进行操作。
const指针类型的函数参数

如果函数的参数中使用const指针,表示指针变量本身可变,但指针所指向的变量不可变,声明在在函数内部不会改变
该指针所指向的变量。
多个返回值的函数

一般来说,函数参数属于输入部分,返回值属于输出部分,但是一般情况下返回值只有一个,如果需要返回多个值怎
么办呢?

通常的做法是用参数当做返回值,而且在典型的linux编程风格中,返回值是不用于做返回结果的,而是用来返回操作
状态的,表示程序的执行结果正确与否(0,1…),linux和标准的C库函数中有很多这种例子,此时接收返回值的参数称
为输出型参数。

一般情况下,如果函数的参数是指针,就有可能是输出型参数,在函数内部,通常会对输入型参数做输出,所以形参
不会加const修饰。

猜你喜欢

转载自blog.csdn.net/qq_40618124/article/details/81189274