指针!!是C语言最本质的特征,学好了指针才能算正式入门C语言喔!!如果你是C语言小白,看这篇文章就对啦!!✍
什么是指针?
在学习指针之前,我们要先了解内存。我们的代码在运行的时候,会把数据存放在哪里?放在内存里!
内存地址
内存就像是一栋大旅馆,这栋旅馆里有许多房间,每个房间有着不同的房号,房间里面住着人。
大旅馆 | ||||||
房号 | 101 | 102 | 103 | …… | 505 | …… |
客人 | Tom | Ben | Kiki | …… | Ami | …… |
内存 | ||||||
地址 | 0x12ffff66 | 0x12ffff6a | 0x12ffff6e | …… | 0x13ffffff | …… |
数据 | 二进制数据1 | 二进制数据3 | 二进制数据4 | …… | 二进制数据n | …… |
如图所示,通过这样类比,内存也被划分为许多块,划分为最小的内存单元是bit。1 bit只能存放一个二进制数字 0 或 1 。
而1byte(字节)=4bit(比特),bit 和byte 都是表示内存大小的单位,我们在编程时创建的变量数据所占的内存空间大小都是以字节byte为单位的。因此,我们以byte为基本单位,每个字节都给一个编号,这个编号就是这个字节byte的地址。
(想要知道关于各种数据类型所占内存空间的大小信息可以跳转到下面这篇博客喔!)
C语言各数据类型大小和取值范围_c不同数据类型精度-CSDN博客
而指针,它的含义就是上面所说的地址。
指针作为内存编号,我们就可以通过它访问到该内存空间里的数据,也就可以说这个指针指向了这里内存空间的数据。
到这里,我们已经建立起了对指针的基本认识。让我们继续往下看吧!
& 和 *
&为单目操作符,能取出某个变量的地址;*解引用符号,能取出某个指针指向的变量(这里做简单复习)。
指针变量
指针变量,顾名思义就是存放指针的变量。
指针变量的大小
这取决于系统环境,32位环境上指针变量大小为4byte,64位上为8byte
指针变量的类型
指针变量的类型,取决于这个指针指向什么类型的数据。
常见指针类型:
被指向的变量类型 | 指针变量的类型 | 名称 |
char | char* | char指针 |
int | int* | int指针 |
long | long* | long指针 |
float | float* | float指针 |
double | double* | doouble指针 |
struct stu | struct stu* | 结构体stu指针 |
…… | …… | …… |
如表,创建一个指针变量,只要根据它指向的变量类型,加上” * “号就是这个指针变量的类型了。
二级指针
而指针变量作为一个变量,也需要开辟内存来存储指向数据的地址,所以它自己也有一个地址,那么指针变量的地址存在哪里呢?存在二级指针里,也就是指针的指针,或者说指向指针变量的指针。
void*
其中还有个特殊的指针类型,void* ——泛型指针,这个类型的指针变量可以存放任意一种类型的指针,但是也因为其类型不确定,因此不可以被解引用,也不可以进行任何运算。
上文我们说到,指针能够准确地指到某个byte的内存单元上,但是指针对指向的内存要如何访问,就取决于这个指针的类型了。
PS:实际上对于任何数据都一样,只有赋值“ = ”操作、“++”、“ - - ”自增自减操作才可以修改内存里的二进制数据,其他任何操作都不会对其二进制数据进行修改。而对于某块内存里的二进制数据,在没有被修改的情况下,如何解读和操作其中的二进制数据,取决于数据的类型。
比如对于二进制数据0000 0000 0000 0000 0000 0000 0011 0000这个4byte大小的数据来说:
这一点在接下来的内容就会有所体现了。
(除此之外,还有指针数组,数组指针,函数指针……下面会一一解答。)
const与指针
const(缩写constant恒定不变的)是变量的修饰符,被它修饰过的变量被称为常变量。常变量虽然本质上还是一个变量,但是它的值不能被直接修改。
const与指针变量之间的用法有两种:
①const*pf,const限制通过指针pf修改它指向的变量,硬改会报错。
②*const pf,const限制对指针变量pf本身进行修改,硬改会报错。
注意“ * ”位置的不同带来的不同效果。
常变量不能直接被修改,但是可以间接地去修改它,也就是通过它的地址来修改。
对于情况①要修改它指向的变量,只要通过另外一个和它指向同一变量的指针变量来修改即可。
对于情况②要修改指针pf,就通过指向pf的指针(这是个二级指针)来修改即可。
但是,写了const就是为了提醒程序员编程时避免对该变量进行了修改的,所以既然用了const就别修改变量的值;要修改变量的值,就别用const。
指针的运算
!!记住!!对指针的访问和操作,具体方式取决于他的指针类型。
指针+-
①指针加减的结果还是指针。
②指针只能和整数进行加减。因为内存地址的编号都是整数,所以加减之后的结果只能是整数的地址编号。
举个例子,现在有一个地址0x12FFFF60,这个指针+1或 -1 ,结果是什么呢?可不是0x12FFFF61或者0x12FFFF5F喔!
//X86环境下(64位)
int a = 10;//假设地址是0x12FFFF60
int* p = &a;
char* q = &a;
p = p + 1;
q = q + 1;
看上方代码1,
p的类型是int*,而int类型占4个字节的内存空间,所以p + 1,p会往后走4个字节,因此最后p保存的地址是0x12FFFF64,数值上加了4;
q的类型是char*,而char类型占1个字节的内存空间,所以q + 1,q会往后走1个字节,因此最后q保存的地址是0x12FFFF61。
所以,假设某个!指针类型所指向的变量!占x个字节,指针加减整数n时,就对应的就在数值上加减n*x。
总之,指针+ - 整数,就是让指针进行偏移,+就是向高值地址偏移,-就是向低值地址偏移,偏移量的单位取决于指针变量的类型,指向int就是4个字节,指向char就是1个字节,如此类推。也就是说int*类型的指针-2就向低值地址偏移 2 * 4 个字节;+3就向高值地址偏移 3 * 4 个字节。
PS:地址都是用十六进制数字表示的,1个十六进制数字的大小在0~15之间,需要4个二进制位来存储,也就是说,1个十六进制数据的大小在0000(0)~1111(15)之间,1个二进制数据占1个bit位,所以1个十六进制数据占4个bit位,也就是1byte。而地址都是以十六进制数的形式来表示的,指针指向的最小单位又刚好为1byte,所以对指针的加减就刚好可以体现在指针数值的加减上了。
指针 - 指针
指针+ - 一个整数得到的是指针,所以反过来指针 - 指针得到的就是一个整数。
int num[5]={ 1,2,3,4,5 };
int* p = #
int* q = &num[1];
int ret = q - p;
看上方代码2,
p存放的是数组num的首元素地址,q存放的是数组num的第2个元素的地址。而数组在内存中是连续储存的,假设p存放的首元素地址是0x12345600,那么q存放的就是0x12345604(int数组,一个元素占4个字节)。q-p在数值上的结果是4,但是q和p都是int*类型,int类型在内存里占4个byte,所以数值结果4 ÷ 占用字节数4 = 1,所以,最后ret的结果为1。
因此,我们可以利用这个特点去计算两个指针之间的元素个数,当然前提是,两个指针都指向在同一段连续内存里的变量,比如上方代码都,p和q都指向同一个数组num,数组在内存里是连续的。
那么有没有指针+指针呢?答案是没有,指针-指针能得出两个指针之间的元素个数,但指针+指针的结果是没有任何具体意义的,所以我们不会让一个指针+另一个指针的,编译器对此情况也会报错。
数组与指针
数组名的意义
数组名表示的就是首元素地址,但数组名有两种不同的意义:
①只是表示首元素地址。
②代表整个数组。
意义②只存在于sizeof(数组名)和&数组名两种情况,其余情况都是意义①。
int num[10] = { 0 };
int* p = #
int* q = &num[0];
int* r = num;
printf("%x", p);
printf("%x", q);
printf("%x", r);
代码5
因此,在数值上p,q,r是一样的,打印结果都是一样的首元素地址。因为&num、&num[0]、num在数值上,其表示的地址是一样的,但是实际意义又有所不同。
数组的索引
还是用上面的代码5,我们要访问num数组的第一个元素,我们会写num[0];访问第五个元素我们会写num[4]。而在编译时,num[4]会被当做*(num+4)进行编译,两者效果是相同的。
int num[10] = { 0 };
int p = num[0];
int q = *(num+0)
if(p == q)
printf("good\n");
如代码6,最后会打印一个good。num能表示首元素地址,num+0就是指针不偏移仍然指向首元素,再 * 解引用之后,拿到的就是num数组的首元素,也就是num[0]。这样一来,也就能解释为什么数组的索引要从0开始了。
所以num[ i ] 和 *(num + i) 是完全等价的。
顺便一提,操作符[ ]是双目操作符,一个操作数是整数,一个是指针,而且满足交换律,所以就会有num[ 0 ]和0 [num]是完全等价的,但是还是别这么写了,不然会被老板或者老师骂hhhhh。
当然在数组定义的时候就不是这么回事了,以上是针对索引来说的。
一维数组传参
当我们明白了数组名能表示首元素地址之后,我们来看下面的代码7:
int numAdd(int arr[10],int num)
{
int ret = 0;
for(int i = 0; i < num; i++)
{
ret += arr[i];
}
return ret;
}
这个函数的作用是:传入一个int类型的数组,求出所有元素的和并返回。而我们会如此来调用这个函数,看代码8:
int arr[10]={1,2,3,4,5,6,7,8,9,0};
int result = numAdd(arr,10);
传参我们传的是数组名arr而不是传arr[10](这么传明显是错的)。换句话说,我们实际上往函数里面传入的是一个指针(因为数组名能表示首元素地址嘛)。那我们是不是可以这么写函数的声明,代码9↓
int numAdd(int* arr,int num);
事实上这样声明和原来是没有区别的。而且因为num[ i ] 和 *(num + i) 是完全等价的。代码7的声明还有可以写成:
int numAdd(int* (arr+10),int num);
因为这里是在声明参数,而不是在定义数组,所以[ ]会被当成索引看。那么不管是arr+多少,最后传进去的就是一个int*类型的指针,所以这里声明形参的时候,[ ]里数字是多少是不是就没关系了。也就是说,代码7还可以像下面这样声明:
int numAdd(int arr[], int num);
[ ]里没有数字,arr[ ]可以解读为*(arr),那是不是就和代码9一样了?
综上所述,数组传参传入的是一个指针,它指向数组的首元素,以上对数组传参的声明均有效,且效果完全一样,但是为了代码的可阅读性,代码7和代码9的声明是推荐的。
二维数组的本质
我们在定义和索引二维数组时,我们用行和列来进行理解,认为它是n行m列的数组,同时二维数组在内存里也是完全连续的。如图:
int num[3][3]={1,2,3,4,5,6,7,8,9}如此存储。
索引 | [0][0] | [0][1] | [0][2] | [1][0] | [1][1] | [1][2] | [2][0] | [2][1] | [2][2] |
地址 | 0x10 | 0x14 | 0x18 | 0x1c | 0x21 | 0x25 | 0x29 | 0x2d | 0x32 |
元素 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
和int num[9]={1,2,3,4,5,6,7,8,9}的存储方式是一模一样的。存储方式相同却有不一样的索引方式,是为什么呢?
实际上,我们在定义int num[3][3]={1,2,3,4,5,6,7,8,9}时,其实是创建了三个数组,再把这三个数组放在一个新的数组里面。
如图:二维数组num里面存着3个一维数组的数组名,一行就是1个一维数组,它们的数组名分别是num[0],num[1],num[2]。其中,num[0]存放1,2,3;num[1]存放4,5,6;num[2]存放7,8,9 。
这就是二维数组的本质——一个存放了若干个一维数组的一维数组就是二维数组,存放一维数组的方式就是存放它们的数组名。
而我们前面就说到,数组名也是一个指针,它指向的是它的首元素。那么,存放了一维数组的数组名的二维数组,就可以认为,它里面存放的就是若干个指针。这里就可以引入一个概念——指针数组了。
指针数组
顾名思义,就是存放指针的数组。
定义:指针类型 数组名[ 数组大小 ]。例: int* pf [3],表示的是一个大小为3的一维数组,存放的元素类型是int*。
让我们看代码10:
int main()
{
int num[3][3] = { 1,2,3,4,5,6,7,8,9 };
int* q[3] = { num[0],num[1],num[2] };
printf("%d %d", num[1][1], q[1][1]);
return 0;
}
这里定义了一个大小为3的指针数组q,它的里面依次存储了num[0],num[1],num[2],num二维数组里的三行一维数组的数组名。
这时我们就会发现,num和q是等价的了,你可以在本地实现这段代码10,然后改变printf语句里对num和q的索引,只要索引一样,输出的结果就一样。因此我们也可以认为,p此时已经和一个二维数组无异。
我们再用“ num[ i ] 和 *(num + i) 是完全等价的 ”结论来看二维数组。
比如我们要拿出整数5,我们就会这样索引——num[1][1],整数5存放在第二行第二列,也就是第2个一维数组num[1]的第2个元素,我们就认为对数组num[1]进行索引[1]拿到了整数5。num[1]为数组名,所以num[1][1] <==> *(num[1] + 1)。而对数组num进行索引[1]拿第2行的一维数组数组名,所以还有*(num[1] + 1) <==> *(*(num + 1) + 1)。
问题又来了:如果说,二维数组可以认为是一个存放了指针的指针数组,那么存放在里面的指针又是什么指针类型呢?代码10里的num[0],num[1],num[2]的类型是int*吗。当然不是!这里我们就可以继续引入第二个概念——数组指针。
数组指针
顾名思义,就是指向数组的指针。
定义:数组类型(* 指针变量名)[数组大小]。
例:int(*pf)[3],表示的是pf这个指针指向一个大小为3的int类型数组。pf的类型为int(*)[3](对于某个变量,在定义时去掉标识符,剩下的就是它的类型,所有变量都适用喔!!后面就知道这个结论有什么用了)。
同样以代码10为例
int main()
{
int num[3][3] = { 1,2,3,4,5,6,7,8,9 };
int* q[3] = { num[0],num[1],num[2] };
printf("%d %d", num[1][1], q[1][1]);
return 0;
}
拿上文的结论num[1][1] <==> *(num[1] + 1) <==> *(*(num + 1) + 1)来看,这里我们可以发现,num + 1它指的是哪的地址?它指的是num数组的第二行的一维数组首元素4,而不是第一行第二个元素2。这就说明,num+1,它并不是往后偏移了一个int类型,而是直接往后偏移了一整个一维数组num[0]指向了num[1]的首元素。
而指针偏移的单位取决于指针的类型,因此我们可以反过来得知:因为num的+ - 的偏移量单位是一个大小为3的int数组,所以num就是一个数组指针。
可是num不是指针数组的数组名吗?没错!两者并不冲突,这个num就是一个数组指针数组指针!
我通俗地解读一下——是【(数组指针)数组】指针,num是3个数组指针num[0],num[1],num[2]组成的数组的指针。将其推广,实际上所有的二维数组都能叫做数组指针数组,二维数组的数组名都能叫做数组指针数组指针(很绕口是吧,不用记的,叫人家二维数组就行)。
你还记得&数组名的特殊意义吗?没错!它代表了整个数组,因为&数组名的类型就是数组指针。
结语
关于指针的内容还没有圆满完成!!因为篇幅很长,所以这里分开两篇发布。
传送门
标签:变量,int,C语言,数组名,num,数组,合集,指针 From: https://blog.csdn.net/Elnaij/article/details/139131225