C语言常见问题(六)——指针与数组

《Back C语言常见问题目录

目录

一、指针概述

1.变量是如何存储的

2.指针

3.地址运算

1) 取址运算符&

2) 间接寻址符*

3) 地址与整数的运算

4.指针作为参数在函数间的传递

5.多级指针

二、数组与指针

1.数组

1) 数组的定义

2) 数组在函数中的传递

3) 多维数组

2.数组与指针的关系

1) 数组与指针的区别

2) 数组名与指针 

3) 指针数组

4) 数组指针

三、指针的安全性问题——野指针


一、指针概述

1.变量是如何存储的

        计算机用于存放程序和数据的物理单元有寄存器和内存。其中寄存器通常只存放经常参与运算的少量数据,或者在进行计算时才将数据放入寄存器;程序和数据主要存放在内存中。

         变量的值为什么能变?是因为编译器编译时,在内存中给每个变量分配了一定大小的存储空间,用来保存变量的值。变量的值改变了,其实就是对应存储空间里存放的数值改变了。

         内存中每一块存储空间都有唯一标识它的地址,例如在上图中,0018FF44(16进制)就是变量v的地址。在32位机器中,存储空间的地址长度为4字节;在64位机器中,地址长度为8字节。

2.指针

        什么是指针(指针变量)?简单来说,指针其实就是地址变量。类似整型变量存储的是整数,浮点型变量存储的是实数,指针存储的是地址,只不过这个地址是有类型的,这个类型是该地址所标识的存储空间的类型。例如:

    int a=0;
    int *pa=&a;//&a为变量a的地址
    //指针pa的类型为int *,指针pa存放的值是int类型变量a的地址

        注意,虽然对于所有类型的指针,它的值都是相同长度的地址。但是在给指针赋值时,编译器会做类型的检查检查要赋予的地址是否与该指针指向的类型相同。例如,你不能给float *类型的指针赋予一个int变量的地址,除非使用强制类型转换。

3.地址运算

1) 取址运算符&

        用法是 &(变量名),可以计算该变量的地址,例如

    int a=0;
   	int *p=&a;//&a:计算变量a的地址

        取址符&所计算的变量可以是基本类型变量、结构体变量甚至可以是指针变量。

2) 间接寻址符*

        用法是 *(指针),用于访问指针所指向变量的存储空间,例如:

        通过间接寻址符*,只要在指针及指针所指向变量的生存周期内,我们可以在程序任何一个地方访问指针所指变量的存储空间,获取和修改该变量的值。

注意:

(1) 间接寻址符*与取址运算符&都是右结合运算符,并且运算优先级仅低于“()”“[]”“->”

(2) 间接寻址符*与取址运算符&互为逆运算,所以有a==*(&a)

3) 地址与整数的运算

        C语言中,地址是可以与整数进行运算的,例如:

        注意:(1) 对于一块存储空间,我们一般用它第一个字节的地址来表标识这块存储空间。(2) %p格式输出的地址是按16进制表示的。

        pa指向int类型的变量,pb指向double类型的变量。在按字节编址的情况下(即每偏移一字节长度的空间,地址+1),我们可以看到地址pa+1增加了4,地址pb+1增加了8,正好对应int类型和double类型占用空间的大小(int占4字节,double占8字节)

        总的来说,假设指针p指向class类型的变量,则p+i的值为p存储的地址+sizeof(class)*i

        指针与整数的运算实质上是地址与整数的运算。即使不使用指针变量,地址与整数运算的偏移量也取决于该地址空间的类型,例如:

4.指针作为参数在函数间的传递

        C语言函数传参的方式有两种,一种是值传参,另一种是地址传参(传指针)。在变量生存周期内,通过传指针可以跨变量的作用域访问变量的存储空间。

值传参

#include <stdio.h>
void f(int a){
	a=666;
	return;
}
int main()
{
	int a=0;
    f(a);
	printf("%d\n",a);
    return 0;
}

 

        我们发现,通过值传参的方式,没办法修改在主函数里声明的变量的值。因为main函数中的a,与函数f中的a虽然名字相同,但它们的存储空间不同,即它们是两个变量,并且在函数f返回后,函数f中的a就被释放掉了。

        如果你想通过函数f中修改主函数中a的值,有两种方法,一是利用函数f的返回值,在函数调用结束后将返回赋给a:

        但这么做有个缺点,假设main函数中有两个变量a、b,我们希望调用函数f之后能修改a、b的值,这时我们没办法单单通过值传递来达到我们的目的,因为值传递只是传递了变量a、b在存储空间中存储的值,而函数的返回值也只能返回单个值。

地址传参

        于是我们很自然地想到,既然我们想在函数f中访问主函数a、b的存储空间,那么是不是可以将a和b的地址作为参数传递呢?在函数f中,只要我们知道a和b的地址,就能通过间接寻址符*来访问主函数的变量a、b的存储空间了:

#include <stdio.h>
void f(int * pa,int * pb){
	*pa=666;
	*pb=999;
	return;
}
int main()
{
	int a=0,b=0;
    f(&a,&b);
	printf("a=%d,b=%d\n",a,b);
    return 0;
}

         

5.多级指针

        指向int类型变量的指针是int *类型,那么指向指针的指针是什么类型?二级指针是指向指针的指针。同理,指向二级指针的指针就是三级指针,以此类推。

        二级指针的定义:

    int a=0;
	int *pa=&a; //pa是一级指针,类型为int *,指向int类型的变量
	int **ppa=&pa;//ppa是二级指针,类型为int **,指向int *类型的指针

        二级指针与一级指针的关系:

        因为int **ppa=&pa;可以看到二级指针ppa的值就是一级指针pa的地址,*ppa是一级指针pa指向变量的地址,通过**ppa我们获取了一级指针pa所指变量的值。 

        其实不用把二级指针想得太复杂。你想想,指针也是变量,是变量就有对应的存储空间,有存储空间就有相应的地址。二级指针也是指针,只不过它存储的是一级指针变量的地址。

        所以对于二级指针,我们用一次间接寻址符*就能访问一级指针的存储空间。而一级指针的存储空间存的也是一个地址,只不过这个地址是一级指针所指变量的地址,于是我们用**就能访问一级指针所指变量的存储空间。    


二、数组与指针

1.数组

1) 数组的定义

        数组是有序的元素序列,数组的元素属于同一类型,并且每个元素有对应的下标(数组的下标从0开始)。数组的定义方式如下:

        T  <数组名>[数组长度];

        其中T为数组元素的类型,可以是基本类型、结构体类型、指针类型等。因为数组所有元素都属于同一类型,所以也可以说数组的类型是T。

        数组名是一个标识符。在C语言中,数组名还是一个地址常量,是数组中第一个元素的地址(数组的首地址)。例如,对于数组int a[10]来说,a==&(a[0])。

        数组长度是一个常量表达式。值得注意的是,虽然编译器会为数组静态分配一块内存,并且这块内存的大小恰为sizeof(T)*数组长度,但C语言不检查数组越界的情况。

2) 数组在函数中的传递

        数组作为函数的参数时,只传递数组的首地址,即数组在函数中的传递是以地址传递方式进行的。所以在函数中对数组的修改,在函数返回后也会保留下来。例如:

#include <stdio.h>
#define N 10
void f(int a[],int n){
    //让数组的每个元素乘以10
    int i=0;
    for(i=0;i<n;i++){
        a[i]=a[i]*10;
    }
}
int main()
{
    int i=0;
	int array[N]={1,2,3,4,5,6,7,8,9,0};
	f(array,N);
	for(i=0;i<N;i++)
        printf("%d ",array[i]);
    return 0;
}

 下面三种函数原型是等价的:

void f(int a[N],int n);

void f(int a[],int n);

void f(int *a,int n);

函数调用时都这么写就可以了:f(array,N);//传数组时只传数组名,即数组的首地址

3) 多维数组

        当我们想存储N个整数时,我们可以使用int [N]数组来存储。如果我们想存储M个int [N]数组怎么办?我们可以使用二维数组(int [M][N])来存储……以此类推,如果想存储K个n-1维数组,我们可以使用n维数组(int [K]…[M][N])来存储。

        什么是多维数组?简单来说,多维数组就是一个数组,它的元素仍是数组。对于n维数组来说,它的元素是n-1维数组。

        有同学把二维数组理解为矩阵,这没问题。但是如果把二维数组理解为矩阵,那怎么理解三维数组?再往下推,怎么理解四维五维乃至n维数组呢?

        首先明确一点,不管数组是几维的,它在计算中都是线性存储的。例如:对于4维数组int a[2][3][4][5],它的首地址是a,而a的值与&(a[0][0][0][0])的值相同。在内存中数组a是从a[0][0][0][0]、a[0][0][0][1]、a[1][2][3][3]、a[1][2][3][4]线性排列的,它们的地址也是线性增长的。

#include <stdio.h>
#define N 10
int main()
{
	int i1=0,i2=0,i3=0,i4=0,n=0;
	int a[2][3][4][5]={0};
	int *p=&(a[0][0][0][0]);
	//通过循环访问多维数组:
	for(i1=0;i1<2;i1++)
		for(i2=0;i2<3;i2++)
			for(i3=0;i3<4;i3++)
				for(i4=0;i4<5;i4++)
					a[i1][i2][i3][i4]=n++;
	//通过指针线性访问多维数组:
	for(i1=0;i1<n;i1++){
		printf("%-5d",*(p+i1));
		if((i1+1)%15==0)
			printf("\n");
	}
	//访问a[1][2][3][4]的两种方式:
	printf("a[1][2][3][4]=%d=%d\n",a[1][2][3][4],*(p+(1*3*4*5)+(2*4*5)+(3*5)+4));
    return 0;
}

2.数组与指针的关系

1) 数组与指针的区别

        数组是有序的、同一类型的元素的集合。指针是存储地址的变量,并且指针的类型由它所指向的存储空间的类型所决定。

        数组所占存储空间的大小由数组元素的类型和数组长度共同决定,而在32位机器下指针所占存储空间的大小永远都是4字节。

        看到这里,你可能觉得指针和数组之间没什么关系,因为数组是包含多个元素的集合,而指针只是单个变量。别急,指针和数组的联系体现在两个方面,一是数组名和指针的联系,二就是指针数组和数组指针。

2) 数组名与指针 

        我们先理解数组名和指针的联系。可以这么说,数组名和指针的值都是地址,但数组名是地址常量,指针是地址变量。

当一个指针想要访问数组中的元素时,可以用下面的方法:

方法一,让指针p=a

	int a[4]={1,2,3,4};
	int *p=a;
	printf("%d",*(p+1));

方法二,让指针p=&(a[0])

	int a[4]={1,2,3,4};
	int *p=&(a[0]);
	printf("%d",*(p+1));

总结一下,前面我们说了数组名是一个地址常量,那么这个地址是什么呢?数组名的值其实就是数组中第一个元素的地址。例如,对于数组int a[4]来说,a==&(a[0])

我们再结合多维数组来看:

#include <stdio.h>
int main()
{
	int a[2][4]={{1,2,3,4},{10,20,30,40}};
	int (*p1)[4]=a;		//p1为数组指针,指向数组a[0]
	int *p2=a[0];		//p2为int*类型的指针,指向a[0][0]
	int *p3=&(a[0][0]);	//p3为int*类型的指针,指向a[0][0]
	printf("*(p1+1)=%d,**(p1+1)=%d\n",*(p1+1),**(p1+1));
	printf("*(p2+1)=%d,*(p3+1)=%d\n",*(p2+1),*(p3+1));
	return 0;
}

         a即a[0]的地址。对于数组int a[2][4],它的第一个元素是int [4]类型的数组,所以只能由数组指针来指向它的第一个元素a[0]:int (*p1)[4]=a。所以*(p1+1)访问的是一维数组a[1]的地址空间,而**(p1+1)访问的是a[1]中第一个元素a[1][0]的地址空间。

        a[0]是数组int a[2][4]的第一个元素,a[0]表示的是一维数组a[0]的首地址(a[0]第一个元素a[0][0]的地址)。因为有int *p2=a[0],*(p2+1)访问的就是a[0][1]的地址空间。

        &(a[0][0])即一维数组a[0]第一个元素a[0][0]的地址。因为有int *p3=&(a[0][0]),*(p3+1)访问的也是a[0][1]的地址空间。

        总结一下,数组名a等价于&(a[0])。对于n维数组a[k][l]…[m],a的值为n维数组第一个元素a[0]的地址(即a[0]的地址),a[i]的值为n维数组第i个元素的首地址(即a[i][0]的地址),以此类推。如果单从值的角度来判断,a==&(a[0])==&(a[0][0])==…==&(a[0][0]…[0]),尽管它们的值相等,但是这些地址所表示存储空间是不同的,所表示存储空间的类型和大小也是不同的。

3) 指针数组

        指针数组是指针还是数组?指针数组其实是数组,它的每个元素都是指针类型。指针数组的定义如下:

        T *p[N];

        //因为“[ ]”的优先级比*高,所以p先和“[ ]”形成p[],说明p是一个数组。这个数组的类型是T *,即它的每个元素都是指向T类型的指针

#include <stdio.h>
int main()
{
	int i=0,a[10]={1,2,3,4,5,6,7,8,9,0};
	int *p[4]={&(a[4]),&(a[5]),&(a[6]),&(a[7])};//指针数组,数组长度为4
	//指针p[i]指向的整数*10:
	for(i=0;i<4;i++)
		*p[i]=(*p[i])*10;
	//输出int数组a:
	for(i=0;i<10;i++)
		printf("%d ",a[i]);
	return 0;
}

 

4) 数组指针

        数组指针是指针,是指向一个数组的指针。其声明如下:

        T (*p)[N];

        //p指向T [N]的数组

例如,实现一个数组指针p指向int [10]的数组:

#include <stdio.h>
#define N 10
int main()
{
	int i=0,array[N]={1,2,3,4,5,6,7,8,9,0};
	int (*p)[N]=NULL;
	p=&array;//也可以写成p=(int (*)[N])array; 但不推荐
	for(i=0;i<N;i++)
        printf("%d ",*(*p+i));
    return 0;
}

注意array的类型是int [10],而p的类型是int (*)[10]。所以要么使用强制转换,要么用取址符&,推荐使用p=&array;

辨析:指向数组元素的指针 与 指向数组的指针

假设有一个T类型的数组T a[N]

如果p=a,则p是指向数组元素的指针,p的类型为T *

如果p=&a,则p是指向数组的指针,p的类型为T (*)[N]


三、指针的安全性问题——野指针

        在使用指针的时候,请务必明确指针指向了哪一块内存。

        如果一个指针它所存储的地址是非法的,比如指向了一块已经被销毁的内存(比如堆区中经过free后的内存块)或者由于未初始化指针导致它存储的地址是个不明确的值,我们称这样的指针为野指针

        野指针的危害很容易理解。野指针存储的地址是无意义的,我们不知道这个地址表示的存储空间是什么类型的,占多大空间。如果我问你这块内存要用来做什么,你估计也没法回答——因为你压根没想使用这块内存。

        如果你不小心使用了野指针,有可能引发未知的bug,并且这个bug可能很难找出。所以为了避免这些情况的发生,我们需要养成一些良好的编程习惯:


  1. 在创建指针的时候,对其初始化。如果你暂时不能确定给它赋什么值,先给它赋值NULL。
  2. 因为C语言不检查数组越界,也不检查指针越界访问,你需要自己检查,限制指针访问的范围。 
  3. 如果指针所指的内存被销毁了,及时给指针赋值NULL。比如使用malloc申请了一个堆空间,指针p指向该空间,当使用free释放掉这块内存后,应有p=NULL。

        总之,永远不要让指针指向一块不确定的内存。即你可以不关注这块内存的地址具体是什么(比如在动态内存分配的时候),但是你必须明确这块内存是用来干什么的,以及你用指针指向这块内存想进行什么操作。

  • 7
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

易水卷长空

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值