一.指针简介及定义
C语言中指针是一种数据结构,其定义为存储地址值的数据,它的存在为访问数据提供了一种新的方式,使用指针按照规律取用数据变得更加快捷高效,也让数组、结构体等大型数据能够作为函数参数,使得灵活分配内存成为可能,即使是一个函数也可以作为参数输入另一个函数。也就是说一个指针变量可以指向每一种数据类型,每一种数据都可以通过指针来访问和传递,并且通过地址值的传递能够做到直接修改。指针变量指的是一类变量,他们有相同的功能和特点,包括普通类型指针、指向行向量的指针、指针数组、空指针、函数指针。
二.指针的运算
2.1取地址&
通过&地址符号,可以将普通数据类型的地址取出并赋值给指针量.
2.2解运算
通过*p可以获得指针p所指向变量的值,亦可以对其进行赋值,则将改变p指针所指变量本身的值。
2.3对指针变量进行加减
对指针变量进行加减时务必要注意指针的初值,最重要的是注意指针变量对应的单位地址长度,例如整形指针加一表示其实际地址值加4(int类型大小为4个字节)。尤其是定义了数组指针时,要注意该数组的大小。
我们知道数组的名是一个指针常量,它可以参与运算但是不能改变他本身的值。
指向同一数组的不同元素地址的指针可以进行减法运算,不能进行加法运算。
2.4指针变量进行比较
指向同一数组的不同元素地址的指针可以进行比较,我们知道在定义一个变量或者数组亦或者结构体时,他们的地址是系统随机分配的,并不连续,可能差距极大,所以对不同变量的指针是不允许进行比较的。
三.对不同指针变量的详细介绍
3.1指向基本数据类型的指针。
int a,*pa= NULL(表示指针不指向任何数,NULL = 0唯一可以赋值给指针的整数);
float b,*pb = NULL;
char c,*pc = NULL;
pa = &a;pb = &b;pc = &c;(也可以将数组中的某个元素的地址值赋给指针变量)
通过取地址并赋值的形式,可以将一个普通数据类型的地址值直接给到指针进行赋值。从而可以用该指针对所指的变量进行取用,于是当一个普通变量作为参数传入函数中时,可以将形参定义为普通类型,也可以定义为指针的形式,区别在于指针传递可以改变原变量的值
int array[3] = {0};(外部数组和自动数组忘记赋初值默认为0,函数内部定义的数组忘记赋初值则会随机赋值)
float f[5] = {0};
char string[] = "Hello world";
pa = array;
pb = float;
pc = string;(字符类型的指针还可以通过直接将字符串赋值的方式进行赋值,实际上是把该字符串的第一个字符的地址给到了pc)(printf("%s",pc);
pa、pb、pc都满足以下等式,通过连续取地址加解地址的方式取用数组中的值效率更高、运算速度更快。
*pa = f[0] = pa[0];*(pa+1) = f[1] = pa[1];……
3.2指向行向量的指针变量
定义方式为(以整形行向量为例,浮点数类型和字符类型行向量均相同):
int(*pa)[4];
int array [3][4] = {0};
pa = array;(可以将一个二维数组的数组名赋给它)
pa = array+1;
pa =array+2;
以上三个式子表示的pa分别指向array的每一行的第一个元素,但他的物理含义不是一个基本指针变量那样,他的单位长度是一个4个整形变量的行向量,需要通过解运算才可以将他的转化成一个指向该行第一个变量的指针。
3.3数组指针
数组指针顾名思义:该数组的每一个变量都是一个指针,且都是同一类型的指针。
定义方式如下:
int*p[4];float*pf[5];char*pc[6](可用于处理字符串数组);
3.4指向指针的指针
跟套娃一样就不多赘述了。
3.5空指针
void *p;
表示指针p是一个通用类型的指针,其他类型的指针量可以直接赋值给他,而将他赋值给其他类型指针时,必须进行强制类型转换,可用于通用函数的撰写。
3.6指向函数的指针
该指针储存的实际上是一个函数被记录在内存区的首地址,通过指向数组的指针可以将函数作为参数传入另一个函数中,从而可以利用不同函数间的组合灵活的实现一些功能。
double func1(int a,int b);(一个函数的声明)
double (*p)(int a,int b);
p = func1;
于是p(1,6) = func1(1,6) = (*p)(1,6);
四.指针用于动态分配内存空间和创建动态数组。
动态一维数组创建:
#include<stdlib.h>
#define n 10
int *p = malloc(n*sizeof(int));
使用:p[0]、p[1]
或者int*q = p;再使用q的自增使用数组元素
free(p);记得释放内存,否则这段内存将一直被占用。
动态二维数组创建;
int**p = (int**)malloc(row*sizeof(int *));
for(int i = 0;i<row;i++)//list是二维数组的列数
{
p[i] = (int*)malloc(list*sizeof(int));
}
释放内存:
for(int i = 0;i<row;i++)//list是二维数组的列数
{
free(p[i]);
}
free(p);
四.C数组与指针的联系与区别
1.关联
数组的名本身是个指针常量,他不占据内存空间。(所有变量名在编译前作为字符两存储在硬盘中,形成一章映射着所储存对象的初始地址、内存占用大小、类型等信息的表,然后在编译过程中会查表来进行相应操作,编译完成后这个表就被销毁了,所以不会占用内存)。
对于一维数组来说数组名的值为数组的第一个元素的地址值,所以一维数组的取用方式包括两种:
int array[10] = {0};//定义一个一维整型数组
int *p = array;
可以通过元素序号和将数组名作为一个指针常量计算进行遍历:
for(int i =0;i<10;i++)
{
printf("%d\n",array[i]);//方式一
printf("%d\n",*(a+i));//方式二
}
for(;p<=&array[9];)
{
printf("%d\n",*p++);//方式三
}
二维数组要稍显复杂一些,二维数组的名同样是一个指针常量,但是他的所属指针类型是指向数组的指针,他的单位长度是该二维数组一行的大小,对于二维数组同样有两种取用的方式:
int array[4][3] = {0};//定义一个二维数组。
int (*p)[3] = array;//定义了一个数组指针(指向行向量的指针),可以将数组的名给到这个指针。
int *q = array[0];//或者q = &array[0][0];都是将数组的第一个元素地址给到一个整型指
for(int i=0;i<4;i++)
for(int j =0;j<3;j++)
{
printf("%d%c",a[i][j],(j==2)?'\n':' ');//方式一
printf("%d%c",*(*(a+i)+j),(j==2)?'\n':' ');//方式二
}
for(;q<&array[4][3];q++)
{
printf("%d%c",q++,(q+4-&array[0][0])%16==0?'\n':' ');//方式三
}
不论是一维数组还是二维数组,这两种方式本质上都是通过指针的方式进行的取值,例如a[5]来计算机内就是*(a+5),先计算元素的地址值然后取用;
由于数组的名本身就是一个指针常量,所以可以将数组名赋值给一个同类型的指针,一旦进行了赋值,该指针就指向了该数组的第一个元素。然后就可以利用指针来进行对数组的调用。如以上代码中的方式三所示。方式三通过指针的规律性的增加来取用数组元素,效率更高。
同样由于数组名本身是一个指针常量,所以可以将数组名给到一个函数的指针形参,从而将数组首地址传过去,再利用指针进行取用;即使函数的形参就是一个数组,并后面跟上了该数组的大小,也是将数组名给到函数,从而进行地址值的传递,不过在函数中我们往往将他按照数组元素索引的方式进行取用。
2.区别
与数组名类型相同的指针变量可以获取数组名的值,但他们在有些用法上存在差异:
由于在C语言中,数组总是在定义时就完成了内存地址的分配,所以数组名的值即数组第一个元素的地址值是固定不变的,后续也不能发生改变,这也就意味着,对于一个数组不可以进行整个数组的赋值,只能遍历元素来改变其值,例如:
char string[] = "Hello World";
string = "good morning";//出错,一方面后一个数组比string的内存要大,其次string是不可以发生改变的左值,即不可以把右边这个字符串的首地址值给到string,从而完成数组的传递。
但是对于一个指针变量来说就完全不是问题,可以通过多次赋值,使得指针指向不同的数组。
总结:对于一个数组来说,分别对他的数组名、首元素取地址,以及他数组名本身给到一个指针,得到的地址值都是一样的,但所蕴含的信息不同。
五.数组指针与二级指针
1.数组指针可以通过动态内存分配完成一个二维数组的建立,而二级指针可以形成一个高级的数组,其中元素为一个数组。而二维数组在传递参数,以及获取地址的时候会自动降维成数组指针。
例如想用一个数组指针构造一个二维数组:
int(*p)[10] = (int(*)[10])malloc(sizeof(int[10])*maxsize);
而用二维数组构建一个动态的高级数组就比较麻烦了;
int**arr = (int**)malloc(sizeof(int*)*maxsize);
此后还需要每次对arr这个指针数组中的每个指针进行动态分配或者赋值:
arr[0] = (int*)malloc(sizeof(int)*maxsize);(以此类推)
2.函数的指针传递
其实质仍然是地址传递,例如;
void func(int**nums,int numsSize;int* numsColumnSize);
六.函数传递参数与指针
当你希望一个指针在函数内部改变其指向的时候应该传递一个他的二级指针进去。
七.对于C语言中数据的存储在使用指针和数组的情况下的思考。
1.声明变量可以获得自动分配的栈内存用来存储数据,但会在作用域结束后被自动销毁。
2.有了变量可以将变量的地址给到相同类型的指针,于是就有两种可以取用数据的方式,一中是变量,一种是指针解引用,二者都可以,没有好坏之分。
3.因为数组变量在声明时就必须给出一个用常量声明的固定的大小,而不能说用变量来声明其大小,所以有了手动管理内存分配的动态分配,而动态分配的结果是分配一段内存并将内存的头地址给到某个指针,这意味着这段动态内存就只能使用指针来访问了,且不会自动销毁,必须手动释放。
4.指针是一种非常灵活的数据大类,其下除了最基本的内置类型的指针,还有多级指针、数组指针、函数指针,而且可以将指针也组合成数组的形式,一维数组指针、二维数组指针,如此嵌套就有很多很多不同的存储形式十分的灵活。而我们要使用这些组织形式各异类型各异的指针,最重要的就是搞清楚我究竟想存储的是什么类型的数据,这些数据单独来看需要哪些指针,最后综合成一个整个的指针映射框架。
5.C语言中函数是不能接受大量的数据以及返回大量的数据的,所以指针用来传递地址从而可以将一段连续的数据或者一些可以由指针访问的数据传入函数中,从而进行访问和修改(const变量可以禁止修改),并且也只有动态分配的内存指针可以将局部作用域产生的大量数据传递出来,此外我们可以看出在C语言中想要将一些数据真正的拷贝而不是值传递就需要对他们进行取地址。
注:程序在编译成机器语言并执行之前只是一段文本而已,其中的变量名也好指针名也好,所有的代号都只是为了人类阅读方便,在真正编译的时候会将这些名称按照内部的映射表进行转换为对应的信息,然后参与运算,所以变量名在程序运行时是不需要存储空间的。一个对象的名称在编译时会映射成一张表,而一个名称对应的关系不仅仅是起始地址,还包括他的大小,类型,这些信息都会在编译时用到,用完即销毁。
而利用变量名进行参数传递,或者赋予指针值时,确实是将变量名本身携带的信息中的首地址给到了指针,但如果仅看一个变量或者一个数组名绝对不是一个常量指针,这些变量名在定义的时候就与一个地址绑定与一个类型绑定甚至与一块内存的大小绑定了,与这个数据的信息绑定。数组名会在需要的时候进行退化(C语言设计哲学),按理来说应该一个一维数组的名应该给到的是数组指针,但实际并不是而是降维成了一个普通指针,因为计算机底层始终是那几种基础内置实体在互相计算,其余的数据类型不过是组织形式逻辑契约罢了,为了方便取用这些实体数据,数组名会自动进行一次降维,但是在获取其信息例如SIZEOF的时候仍然是取的数据名绑定的信息中的大小。这也是为什么我们在将二维数组名传递和划分一块连续内存作为二维数组的动态内存时,我们会定义一个数组指针,而结构体变量并不会自动降维成指针,是可以整个进行拷贝值传递的,但是通常不推荐:
int arr[2][3] = {{}};
int(*p)[3] = arr;
int (*p)[10] = (int(8)[10]) malloc (sizeof(int[10])*size);就已经分配好了二维数组的内存。
以及为什么对于一维数组我们是如下操作:
int arr[10] = {};
int*p = arr;
int*p = (int*)malloc(sizeof(int)*size);