C语言中的指针:掌握内存的钥匙
引言
C
语言是一种结构化编程语言,它提供了对硬件底层的直接访问,其中最强大的特性之一就是指针。指针允许程序员直接操作内存地址,这对于理解程序的内部工作原理以及优化代码性能至关重要。本文将深入探讨C语言中指针的概念、使用方法以及一些高级技巧。
什么是指针
指针就是内存地址,指针变量是用来存放内存地址的变量。就像其他变量或常量一样,必须在使用指针存储其他变量地址之前,对其声明。
声明形式:
type *pointName;
其中type
是指针的基类型,它必须是一个有效的C
数据类型,pointName
是指针变量的名称。用来声明指针的星号*
与乘法中使用的星号是相同的,但是在c
语句中,星号是用来指定一个变量是指针。
例如:指针简单使用
输出结果
指针与变量
内存区的每一个字节都有一个编号,这就是"地址"
,如果在程序中定义了一个变量,在对程序进行编译运行时,系统就会给这个变量分配一个内存
单元,并确定它的内存地址也就是一串这个地址独有的编号,指针是内存单元的编号,指针变量是存放地址的变量,通常我们会把指针变量称为指针,其实指针与指针变量含义式不一样的。
指针变量赋值
指针变量在使用之前, 一定要先赋值,也就是让指针变量指向一个地址。注意,给指针变量赋值只能式地址,通过"&"符号来获取普通变量的地址。“&” - 取地址运算符,“*” - 指针运算符,或称为间接访问运算符,取地址的值。
例子:
输出结果:上面代码演示了如何在C语言
中使用指针,第一步定义一个整型变量i和一个指向整型变量的指针p
。然后将i的值初始化为10
,并将p
指向i的地址,然后打印出i
,和p
的内存地址以及i的值和通过指针p
访问到的值。
i = 0x7ffd55fc953c
变量i的存储地址, p = 0x7ffd55fc953c
是指针变量p指向的地址,以为代码中把i的地址赋给了p
,所以p
指向i
的存储地址。i=10
给i
赋值10
,*p = 10
指针变量p
指向地址里的值,p
指向的是地址0x7ffd55fc953c
,而这个地址里存储的值为10
,所以*p=10
。
通过地址取值:*((unsigned char *)p))
输出结果:((unsigned char *)7c3d726c))
代表把 0x7c3d726c
这个值,强制转换成一个无符号字符类型指针,相当于告诉编译器,这是一个地址,强制转换的类型和变量a
的数据类型保持一致,因为要通过地址,把变量i的值读出来,最后通过*
把这个地址的数据读出来,结果7c3d726c = 10
通过指针改变某个内存地址里的值
上面说了如何获取内存地址的值,那也可以通过指针改变某个内存地址里面的值。格式: *指针变量 = 数值; 【*p = 20】
输出结果:
案例2:输入a和b两个整数,然后按先大后小顺序输出a和b。不交换整型变量的值,而是交换两个指针变量的值。
分析:输入a=5,b=6,由于a<b,将p1和p2交换,注意的是,a和b的值并未交换,他们仍然保持原值,但是p1和p2的值改变了,p1的值原来为&a,后来变成了&b, p2的值原来为&b后来变成了&a,这样在输出p1和p2的时候,实际上是输出了变量b和a的值,所以先输出6后输出5。
指针变量作为函数参数
函数的参数不仅可以是整型,浮点型,字符型,还可以是指针类型,它的作用将一个变量的地址传送到另一个函数中。
案例:通过指针类型的数据作为函数的参数,对输入的两个整数按大小顺序输出。【通过指针实现交换两个变量的值】
案例:输入三个整数a,b,c,
要求按由大到小的顺序输出。
指针与一维数组
一个变量有地址,一个数组包含若干个元素,每个数组元素都在内存中占有存储单元,它们都有相应的地址,指针变量既然可以指向变量,当然也可以指向数组元素,把某一元素的地址放到一个指针变量中,所谓的数组元素的指针就是数组元素的地址。c语言定义数组时,编译器会分配连续地址的内存,来存储数组里的元素,实际上数组本质上也是指针。
分别打印了arr
和 &arr[0]
的地址,发现地址是一样的,既然是个地址,那就可以使用指针的形式,去访问地址里存储的值,然后打印*arr
的值,得到的结果为1
,正好和arr[0]
的值对应,最后得出数组也可以使用指针的形式去使用。
数组元素的引用
引用数组元素可以使用下标法,也可以使用指针法,即通过指向数组元素的指针找到所需的元素。使用指针法能使程序质量高,占用内存少,运行速度快。
p = &a; // p的值是a[0]的地址
p = a; // p的值是数组a首元素即a[0]的地址
注意:程序中的数组名不代表整个数组,只代表数组首元素的地址。上面的p = a
的作用是把a数组首元素的地址赋给指针变量p
,而不是把数组a
各元素的值赋给p
,可以简写为 int *p = &a[0];
也可以写成int *p = a;
也可以写成两行的形式int *p; p = &a[0];
他们都有一个作用:将a
数组首元素即a[0]
的地址赋给指针变量p
(而不是*p
)。
- 下标法
a[i]
的形式 - 指针法如
*(a+i)
或*(p+i)
。其中a
是数组名,p
是指向数组元素的指针变量。
或者用指针变量指向数组元素
注意:在使用指针变量指向数组元素的时候,可以通过改变指针变量的值指向不同的元素,例如上面代码中的方法是使用指针变量p
来指向元素,用p++
使p
的值不断改变宠儿指向不同的元素。
换一种想法,如果不用p
变化的方法而用数组名a
变化的方法如a++
行不行呢。for(p=a;a<(p+10);a++) printf("%d",*a);
答案是不行的,因为数组名a
代表数组首个元素的地址,他是一个指针型常量,它的值在程序运行期间是固定不变的,既然a
是常量,所以a++
是无法实现的。
例:通过指针变量输出整型数组a的10个元素。
很明显,输出的数值并不是a
数组中各个元素的值,因为在执行第二个for
循环读入数据后,p
已指向a
数组的末尾,因此在执行第二个for
循环时候,p的起始值不是&a[0]
了,而是a+10
。
解决上面问题只要在第二个for循环之前加一个赋值语句即可
常用指针引用数组元素的情况
- (1) p++; p; p++使p指向下一个元素a[i],然后再执行p,则得到下一个元素a[i]的值。
- (2) p++; 由于++和同优先级,结合方向为自右而左,因此它等价于*(p++)。先引用p的值,实现*p的运算,然后再使p自增1。
- (3) (p++)与(++p)作用,前者先取p值,然后使p加1。后者是先使p加1,再取p。若初始值为a即&a[0],若输出*(p++)得到a[0]的值,而输出*(++p)得到a[1]的值。
- (4) ++(*p) 表示p所指向的元素值加1,如果p = a,则++(*p)相当于++a[0],若a[0]的值为3,则在执行++(*p)即++a[0]后的值为4。
- (5) 如果p当前指向a数组中第i个元素a[i],则:
*(p--)
相当于a[i--]
,先对p进行运算(求p所指向的元素的值),再使p自减。
*(++p)
相当于a[++i],先使p自加,在进行运算。
*(--p)
相当于a[–i],先使p自减,再进行*运算。
将++和–运算符用于指针变量十分有效,可以使指针变量自动向前或向后移动,指向下一个或上一个数组元素。
在引用数组元素时指针的运算
在引入数组元素时会遇到指针的算术运算,当指针指向数组元素的时候,譬如指针变量p
指向数组元素a[0]
,我想用p+1
表示指向下一个元素a[1]
,如果能实现这种运算会对引用数组元素提供很大的便利。
在指针指向一个数组元素时可以进行以下运算
- 加一个整数用
+
或者+=
,如p+1
;- 减一个整数用
-
或者-=
,如p-1
;- 自加运算,如
p++
,++p
;- 自减运算,如
p--
,--p
;- 两个指针相减,如
p1-p2
如果p1
和p2
都指向同一个数组中的元素时才有意义。
解释:
(1)如果指针变量p已经指向数组中的一个元素,则p+1
指向同一数组中的下一个元素,p-1
指向同一数组中的上一个元素,执行p+1
时并不是将p
的值或地址简单的加1
而是加上一个数组元素所占用的字节数,例如数组元素是float
型,每个元素占4
个字节,则p+1
意味着p
的值是地址加上4
个字节,使它指向下一个元素,p+1
实际上代表p+1*d
,d
是一个数组元素所占的字节数。
(2)如果p
的值为&a[0]
,则p+i
和a+i
就是数组元素a[i]
的地址,或者说他们指向a数组序号为i
的元素。这里要注意的是a代表数组首元素地址,a+1
也是地址,它的计算方法同p+1
,即它的实际地址为a+1*d
。
(3)*(p+i)
或*(a+i)
是p+i
或a+i
所指向的数组元素,即a[i]
。实际上在编译的时候,对数组元素a[i]
就是按*(a+i)
处理的,即按数组首元素的地址加上相对位移量得到要找的元素地址,然后找出该单元中的内容。
(4)如果指针变量p1和p2都指向同意数组中的元素,如果执行p2-p1
,结果是p2-p1
的值两个地址之差,除以数组元素的长度。两个地址不能进行p1+p3
是毫无意义的。
- 指针的解引用:可以通过指针的解引用来访问它所指向的变量的值,解引用的操作符是"*",与乘法运算符不同。
例如:
int value = 10;
int *p = &value;
printf(“The value is %d”,*p); // 输出value的值- 指针的加减法:我们可以对指针进行加减操作,让指针移动到数组中的其他元素上。
int arrPtr = arr;
printf(“The second element is %d\n”,(arrPtr+1)); // 这里*(arrPtr+1)实际上就是指向数组第二个元素的地址,并解引用获取其值。
值得注意的是,指针算数的步长取决于指针所指向的数据类型。例如arrPtr指向的是一个整型的指针,那么arrPtr+1实际上是在当前地址基础上加上了整型的大小。- 指针 - 指针 :得到的数值的绝对值是指针和指针之间元素的个数。
其他指针使用方法:
指针与一维数组地址关系
指针与一维数组值的关系
用数组名做函数参数的情况
第一种,数组元素做实参的情况,假设swap
是将两个形参x,y
进行交换使用
swap(a[0],a[1]);
void swap(int x,int y);
得出:与用变量作为实参的情况一样,是”值传递“
方式,将a[0]
和a[1]
的值单向传递给x
和y
。当x
和y
的值改变时a[0]
和a[1]
的值不改变。
第二种: 数组名做函数形参的情况,实参数组名代表该数组首元素的地址,而形参是用来接受实参传递过来的数组首元素地址的。因此形参应该是一个只恨变量,只有指针变量才能存放地址,实际上c语言的编译都是将形参数组名作为指针变量来处理的。
void fun(int arr[],int n); === void fun(int *arr,int *n);
在函数被调用时,系统会在fun
函数中建立一个指针变量arr
,用来存放从主调函数传递过来的实参数组元素的地址。如果在fun
函数中用运算符siezof
确定arr所占字节数,可以发现sizeof(arr)
的值为8
,这就这个名了系统把arr
作为指针变量来处理的。当arr
接受了实参数组的首元素地址后,arr
就指向实参数组的首元素,也就是指向了brr[0]
。因此,arr
就是brr[0]
。
注意: 实参数组名代表一个固定的地址,或者说是指针常量,但形参数组名并不是一个固定的地址,而是按指针变量处理。在函数调用进行虚实结合后,形参的值就是实参数组首元素的地址,在函数指向期间,它可以在被赋值。
常用这种方法通过调用一个函数来改变实参数组的值。
第三种:变量名做函数参数和用数组名做函数参数比较
- 1.当实参类型是变量名时,要求形参类型也是变量名,通过形参传递的信息是变量的值,通过函数调用不能改变实参量的值。
- 2.当实参类型是数组名时,要求形参的类型是数组名或者指针变量,通过形参传递的信息是实参数组首元素地址,通过函数调用能改变其实参变量的值。
总结 :说明c语言调用函数时虚实结合的方法都是采用“值传递”方式,当变量名作为函数参数时传递的是变量的值。当用数组名作函数参数的时候,由于数组名代表的是数组首元素的地址,因此传递的值是地址,所以要求形参为指针变量。
第四种:数组名和指针变量作为函数的形参,在c语言
中,用下标法和指针法都可以访问一个数组,如果有一个数组a
,则a[i]
和*(a+i)
无条件等价,用数组名作形参,以便于实参数组对应,比较直观便于理解。从应用的角度看,用户可以认为有一个形参数组,他从实参数组哪里得到起始地址,因此形参数组与实参数组共占同一段内存单元,在调用函数期间,如果改变了形参数组的值,也就改变了实参数组的值,在主调函数中就可以利用这些已经改变的值。
例:将数组a
中n
个整数按相反顺序存放,用一个函数reversal
来实现交换。实参用数组名a
,形参可用数组名,也可以用指针变量。
上面代码中,在main
函数中定义整型数组a
,并赋初值,函数reversal
形参数数组名为x。在定义reversal
函数时,可以不指定形参数组x
的大小。因为形参数组名实际上是一个指针变量,并不是真的开辟一个数组空间。reversal
函数的形参n用来接受需要处理的元素个数。在main
函数中有函数调用语句reversal(a,10);
表示要求将a
数组的10
个元素颠倒排列。
改写上面代码 ,将reversal
中的形参改成指针变量,函数reversal
的形参有数组名x[]
变为指针变量*x
,相应的实参仍是数组名a
,即数组a
首元素的地址,将它传递给形参指针变量x
,这个时候x
就指向x[0]
。x+m
是a[m]
元素的地址。
总结归纳:
例:用选择排序+指针方法对是个整数由大到小顺序排序:
通过指针引用多维数组
指针变量可以指向一维数组中的元素,也可以指向多维数组中的元素,多维数组的指针比一维数组的指针要复杂一些。
多维数组元素的地址
以二维数组为例:int a[3][4] = {{1,3,5,7},{9,11,13,15},{17,19,21,23}}
,a
是二维数组名,a
数组包含三行,即三个元素,每一行元素又是一个一维数组,它包含4
个元素即4
列元素。可以看出二维数组是数组的数组,即二维数组a
是由3个
一维数组组成的。
从二维数组角度来看,a
代表二维数组首元素的地址,现在的元素不是一个简单的整型元素,而是由四个整型元素所组成的一维数组,因此a
代表的是首行即序号为0
的行的起始地址。a+1
代表序号为1
的行的起始地址。如果二维数组的首行的起始地址为2000
,一个整型数据占4
个字节,则a+1
的值应该是2000+4*4=2016
因为第0
行有4
个整型数据。a+1
指向a[1]
,或者说a+1
的值是a[1]
的起始地址。a+2
代表a[2]
的起始地址,它的值是2032
。
上面说了如何表示首行地址,那么a[0]
是一维数组名,该一维数组中序号为1
的元素的地址显然应该用a[0]+1
来表示。此时a[0]+1
中的1
代表一列元素的字节数,即4
个字节。a[0]
的值是2000
,a[0]+1
的值是2004
而不是2016
,这是因为现在在一维数组范围内讨论问题的,正如有一个一维数组x
,x+1
是其第一个元素x[1]
的地址一样。a[0]+0
,a[0]+1
,a[0]+2
,a[0]+3
分别是a[0][0]
,a[0][1]
,a[0][2]
,a[0][3]
元素的地址,即(&a[0][0],&a[0][1],&a[0][2],&a[0][3])
。
一维数组与指针的时候已经说了a[0]
和*(a+0)
无条件等价,a[1]
和*(a+1)
无条件等价,a[i]
和*(a+i)
无条件等价。因此a[0]+1
和*(a+0)+1
都是&a[0][1]
。a[1]+2
和*(a+1)+2
的值都是&a[1][2]
。但是要注意不要将*(a+1)+2
错写成*(a+1+2)
,后者相当于a[3]
。
再深一步解析,既然a[0]+1
和*(a+0)+1
是a[0][1]
的地址,那么*(a[0]+1)
就是a[0][1]
值。同理,*(*(a+0)+1)
或*(*a+1)
也是a[0][1]
的值。*(a[i]+j)
或*(*(a+i)+j)
是a[i][j]
的值。*(a+i)
和a[i]
是无条件等价。
a[i]
从形式上看是a数组中序号为i的元素。如果a
是一维数组名,则a[i]
代表a
数组序号为i的元素存储单元。a[i]
是一个有确定地址的存储单元。但是如果a
是二维数组,则a[i]
是一维数组名,它只能是一个地址,并不代表一个存储单元,也不代表存储单元中的值如同一维数组名只有一个指针常量一样。a,a+i,a[i],*(a+i),*(a+i)+j,a[i]+j
都是地址,而*(a[i]+j)
和*(*(a+i)+j)
是二维i数组元素a[i][j]
的值。
下面是二维数组常用a的有关指针:
根据上面解释,输出二维数组的有关数据【地址和元素的值】
案例:根据输入数组下标,显示二维数组中的值
用指向数组的指针作为函数参数
一维数组名可以作为函数参数,多维数组名也可以做函数参数,用指针变量作形参,以接受实参数组名传递来的地址,两种方法:
i. 用指向变量的指针变量。
ii. 用纸箱一维数组的指针变量。
例:一个班级,三个学生,各4门课,计算总平均分数以及第n个学生的成绩。
代码解释:先调用average
函数求平均值。在函数average
中,形参p
被升为float *
类型指向float
型变量的指针变量,它的基类型是float
型,实参用*score
,即score[0]
也就是&score[0][0]
,即score[0][0]
的地址。把score[0][0]
的地址传给p
,使p
指向score[0][0]
。然后在average
函数中使用p
先后指向二维数组的各个元素,p
每加1
就改为指向score
数组的下一个元素。
函数search
的形参p的类型是float(*)[4]
,他不是指向整型变量的指针变量,而是指向包含4
个元素的一维数组的指针变量。函数开始调用时,将实参score
的值也就是数组0
行的起始地址传给p
,使p
也只想score[0]
。
注意:实参与形参如果是真真类型,应当注意他们的基类型必须一致,不应把int *型的指针即数组元素的地址传递给int(*)[4]
型(指向一维数组)的指针变量,反之亦然。
例:在上面的基础上,查找有一门以上课程不及格的学生,输出他们的全部课程的成绩。
代码分析:实参score
和形参p
的类型是相同的。在调用search
函数时,p
得到实参score
的值,即score[0]
的起始地址,也就是说p
也指向score
数组的第一行。然后p
先后指向隔行包括每行学生的几门课的成绩。
通过指针引用字符串
字符串的引用方式
在c语言中,字符串是存放在字符数组中的,像引用字符串有两种方法
- 用字符数组存放一个字符串,可以通过数组名和下标引用字符串中一个字符,也可以通过数组名和格式声明
%s
输出该字符串。
实际上string[7]
就是*(string+7)
,string+7
就是一个地址,它指向字符“C”
. - 用字符指针变量指向一个字符串常量,通过字符指针变量引用字符串常量。
在上面代码中没有定义字符数组,只定义了一个char *型
的字符指针变量string
,用字符串常量"I love China!"
对它初始化。c语言
对字符串常量是按字符数组处理的,在内存中开辟一个字符数组用来存放该字符串常量,但是这个字符数组是没有名字的,因此不能通过数组名来引用,只能通过指针变量来引用。
注意: 有人误认为string
是一个字符串变量,以为在定义时把"I love China!"
这几个字符赋给该字符串变量,这是不对的,在C语言
中只有字符变量,没有字符串变量。string
被定义一个指针变量,基类型为字符型。它指能指向一个字符类型数据,而不能同时指向多个字符数据,更不是把I love China!
这些字符存放到string
中,也不是把字符串赋给*string
,只是把I love China!
的第一个字符的地址赋给指针变量string
。
例:将a串复制为字符串b,然后输出字符串b。
程序分析:程序中a和b
都定义为字符数组,通过地址访问其数组元素。在for
语句中先检查a[i]
是否为'\0'
。如果不等于'\0'
,表示字符串尚未处理完成,就将a[i]的值赋值给b[i]
,即复制一个字符串。
例:将a
串复制为字符串b
,然后输出字符串b。(用指针变量来处理)
代码解析 :p1
和p2
时指向字符型数据的指针变量,现使p1
和p2
分别指向字符串a
和b
的第一个字符。*p1
最初的值时字母“I”
。赋值语句*p2 = *p1
的作用是将字符串I赋值给篇所指向的元素,即b[0]
。然后p1
和p2
分别加上1
,分别指向其下面的一个元素,知道碰到'\0'
为止。
字符指针作函数参数
如果想把一个字符串从一个函数"传递"到另一个函数,可以用地址传递的办法,即用字符数组名作参数,也可以用字符指针变量作参数。在被调用的函数中可以改变字符串的内容,在主调函数中可以引用改变后的字符串。
- 字符数组名作为函数参数
例:用函数调用实现字符串的复制,用字符数组名作为函数参数。
程序分析:a
和b
时字符数组,copy_string
函数的作用是将from[i]
赋给to[i]
,直到from[i]
的值等于'\0'
为止。 - 用字符型指针变量作实参
分析: 指针变量from
的值时a
数组首元素的地址,指针变量to
的值时b
数组的首元素的地址。他们作为实参,把a
数组首元素的地址和b数组首元素的地址传递给形参数组名from
和to
(它们实质上也是指针变量)。 - 用字符指针变量作形参和实参
分析: 形参使用char *
类型变量即字符指针变量,main
函数中a
时字符指针变量,指向字符串"I am a teacher"
的首字符。b
时字符数组,在其中存放了字符串"You arr a student."
。p
是字符指针变量,它的值是b
数组第一个元素的地址,因此也指向字符串"You are a student."
的首字符。copy_string
函数的形参from
和to
是字符指针变量,再调用copy_string
的时候,将数组a
首元素地址传递给from
,把指针变量p
的值即数组b元素的地址传给to
。因此form
指向a
串的第一个字符a[0]
,to
指向b[0]
,在for
循环中,先检查from
当前所指向的字符是否为'\0'
,如果不是,表示需要复制此字符,就执行*to = *from
,每次将*from
的值赋给*to
,一致遇到'\0'
结束。
其实上面的字符串复制还可以写很多形式,如下
输出结果都是
![C语言中的指针(https://i-blog.csdnimg.cn/direct/6bdbe065eafa45ce928478f034eccabb.png)
调用函数时实参与形参的对应关系
使用字符指针变量和字符数组的比较
用字符数组和字符指针变量都能实现字符串的存储和运算,但它们二者之间是有区别的,不应混淆,主要区别如下:
- a.字符数组由若干个元素组成,每个元素中放一个字符,而字符指针变量中存放的是地址(字符串第一个字符的地址),绝不是将字符串放到字符指针变量中。
- b.赋值方式,可以对字符指针变量赋值,但是不能对数组名赋值。
- c.初始化的含义,对字符指针变量赋初始值,数组可以在定义时对个元素赋初始值,但不能用赋值语句对字符数组中全部元素整体赋值。
- d.存储单元的内容。编译时字符数组分配若干个存储单元,以存放各元素的值,而对字符指针变量,只分配一个存储单元。
- e.指针变量的值是可以改变的,而字符数组名代表一个固定的值(数组元素的地址)不能改变。
- f.字符数组中各元素的值是可以改变的(可以不对他们再赋值),但字符指针变量指向的字符串常量中的内容是不可以被取代的(不能对他们再赋值)。
- g.引用数组元素,对字符数组可以使用下标法(用数组名和下标)引用一个数组元素,也可以使用地址法(如
*(a+5)
)引用数组元素a[5]
,如果定义了字符指针变量p
,并使它指向数组a的首地址,则可以用指针变量带下标的形式引用数组元素(如p[5]
),同样可以用地址法(如*(p+5)
)引用数组元素a[5]
,但是如果指针变量没有指向数组,则无法用p[5]
或*(p+5)
这样的形式引用数组中的元素。这是若输出p[5]
或者*(p+5)
这样的引用形式引用数组中的元素。这时若输出p[5]
或*(p+5)
,系统将输出指针变量p
所指的字符后面5
个字节的内容。- h.用指针变量指向一个格式字符串,可以用它们替代
printf函数
中的格式字符串。
因此只要改变指针变量format所指向的字符串,就可以改变输入输出的格式。这种printf函数称为可变格式输出函数。如:
因此,用指针变量指向字符串的方式更为方便。
指向函数的指针
持续更新中。。。
标签:变量,指向,元素,C语言,地址,内存,数组,指针 From: https://blog.csdn.net/qq_42696432/article/details/140436883