目录
前言
C语言的指针是由其自身的内存管理机制而来的。在早期的C语言中,内存管理主要由程序员负责,每个变量都会分配一段内存,变量的值存储在该地址的字节中。这使得通过直接操作内存地址来操作变量成为可能。这种通过直接操作内存地址来访问或操作数据的方式,就是指针的基本概念。
指针在C语言中有许多用途:
- 用于变量的存储和访问:通过指针,程序可以直接访问和操作内存中的数据,这在需要动态分配和回收内存的场景中尤其重要。
- 用于数组操作:指针允许程序直接操作数组的每一个元素,而无需使用循环。
- 用于函数参数传递:通过指针,函数可以间接地访问和修改其参数。
- 用于结构体操作:指针允许程序直接访问结构体的字段,无需逐一访问。
- 用于动态内存分配:使用指针,可以动态地分配和回收内存,这在一些需要大量内存的场景中非常有用。
需要注意的是,由于指针直接操作内存,如果不正确地使用,可能会导致程序崩溃或者数据丢失。因此,理解和使用指针需要对C语言的内存管理有深入的理解。总之,指针是C语言的一个重要特性,它提供了对内存的直接访问和操作的能力,使得程序员可以更灵活地编写高效、可靠的代码。
一、什么是指针
计算机内存是以字节为单位的存储空间,内存的每一个字节都有一个唯一的编号,这个编号就称为地址。当C程序中定义一个变量时,系统就分配一个带有唯一地址的存储单元来存储这个变量,程序对变量的读取操作 (即变量的引用),实际上是对变量所在存储空间进行写入或取出数据。通常我们引用变量时是通过变量名来直接引用变量,例如赋值运算 b=55,系统自动将变量名转换成变量的存储位置(即地址),然后再将数据 55 放入变量 的存储空间中。这种引用变量的方式称为变量的“直接引用”方式。
此外,C 语言中还有另一种称为“间接引用”的方式。它首先将变量A 的地址存放在一个变量B(存放地址的变量成为指针变量)中,然后通过存放变量地址的这个变量B 来引用变量A。
一个变量的地址称为该变量的指针。用来存放一个变量地址的变量称为指针变量。当指针变量 p的值为某变量的地址时,可以说指针变量 p 指向该变量。
如果上面的话说的不清楚,那么我们总结一下:
1. 定义变量的本质:开辟出一块空间,并用变量名代表那一片空间。
2. 内存空间的最小单位是字节,每一个字节都有一个编号,这个编号我们称之为地址。
3. 有一种特殊的变量,专门存储地址,这种变量叫做指针变量,一般我们也简称为指针。
二、指针的基本使用方式
指针的使用一般分为三步:
1)定义指针变量
2)给指针变量赋值
3)指针解引用
在这个过程中你需要使用到两个运算符:&和*
2.1 定义指针变量
指针变量定义的一般形式为
类型名 *指针变量名;
如 int *pNums;
注意:
A. 变量名前面的“*”是一个说明符,用来说明该变量是指针变量,这个“*”是不能省略的,但是它不是变量名的一部分
B.类型名表示指针变量所指向的变量的类型,而且只能指向这种类型的变量。
指针变量允许在定义时进行初始化,例如:
int nNumA, nNumB;
int *pA=&nNumA, *pB=&nNumB;
表示:定义了两个指向 int 型变量的指针变量 pA 和 pB,pA 的初值为变量 nNumA 的地址&nNumA ,pB 的初值为变量 nNumB 的地址& nNumB ,不是表示*pA的初值为&nNumA ,*pB的初值为&nNumB。
2.2 给指针变量赋值
一般我们会用到&运算符,用于取一个对象的地址。通常情况下我们应该把一个变量的地址赋值给指针,类似于这样:
int main(){
int nNum =100;
int* pl;
pl = &nNum;
return 0;
}
2.3指针变量的引用
指针变量有两个有关的运算符
1)& 取地址运算符
2)* 指针运算符
例如:
int a =12;
int *p=0;
p = &a;
int temp = *p; //等价于 temp = a;
*p = 200; //等价于 a= 200;
&a 表示变量 a的地址
*p 表示指针变量 p指向的变量
注意:
指针变量是用来存放地址的,不要给指针变量赋常数值,例如:
int *p= 1000; //错误
指针变量没有指向确定地址前,不要对它所指的对象赋值,例如:
int nNum=5, *p=nNum; // 类型不匹配
总结:
1)&操作符用于获取变量的内存地址。
2)*操作符用于声明指针变量(在这种情况下,它表示该变量是一个指针),以及解引用指针(在这种情况下,它表示获取指针指向的值)。
三、指针与函数传参
指针是间接引用,可以利用这一特性进行参数传递,使得函数内值的变化能够影响到函数外部:
void swap(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main(void)
{
int a;
int b;
swap(a, b);
}
在上面代码例子中,x与y的改变,不会影响到a和b的值。
然而在下面代码例子中,p和q 指向了main 函数中的a与b,故而在 swap 函数内部实际上访问到了a 和b所处的内存空间,间接的修改了 a 和b,但这并不意味着形参改变了实参,因为形参是 px与 py,实参是 a 的地址,b 的地址,很明显,a 的地址没有改变,b 的地址也没有改变。
void swap(int*px, int*py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a;
int b;
swap(&a, &b);
}
四、指针的运算
指针是可以有数学运算的但是指针的运算和它的类型息息相关。指针能够支持一些算数运算,不过含义和普通的数据的算数运算非常不同。首先指针只能进行加法和减法运算,比如+,-,++,--,+=,-=这些。并且只有两种形式:
1.指针加或减整数
2.指针减指针,而不能指针加指针
比如如下代码示例:
int a = 100;
int *pl = &a;
pl + 1; //不是将地址+1,而是将地址加到下一个 int 型的位置, pl + 1 等同于(int)&a + 4
//p1++的话地址值会自增 4;
short a = 20;
short *p2 = &a;
p2 + 1; //将地址加到下一个 short 型的位置,p2++的话地址值会自增 2;
4.1 指针加或减整数
指针加或者减一个整数的话,得到的数据类型还是一个指针。得到的数据值是+(-)整数*sizeof(数据类型)。比如:
int*p=0;
然后p+5 得到的数据类型还是整型指针?得到的数据0+5*4就是20。
继续看下面的代码示例:
#include <stdio.h>
int main() {
//下面是一个结构体数据结构,后面补充知识点
struct TEST
{
int a;
int b;
int c;
int d;
int e;
int f;
};
int Num = 0;
int* pl = &Num;
pl = (int*)1;
printf("%d\n", pl);//1
printf("%d\n", pl + 1);//5
printf("%d\n", pl + 2);//9
printf("%d\n", pl + 5);//21
double* p2 = NULL;
p2 = (double*)1;
//printf("%d", *p2);//运行会报错,因为这个地址不能访问,但是不耽误编译。
printf("%d\n", p2);//1
printf("%d\n", p2 + 1);//9
printf("%d\n", p2 + 2);//17
printf("%d\n", p2 + 5);//41
TEST * p3 = NULL;
p3 = (TEST*)1;//这个结构体是 24 个字节
printf("%d\n", p3);//1
printf("%d\n", p3 + 1);//25
printf("%d\n", p3 + 2);//49
printf("%d\n", p3 + 5);//121
return 0;
}
4.2 指针减指针
指针减指针得到的是这两个地址间能够存放多少个这种类型的数据。例如下面代码:
double* p4=(double*)10;
double* p5 = (double*)30;
printf("%d", p5 - p4);
上面得到的结果是 2。
五、指针与一维数组
为何指针的运算的特性是这样的?这主要是为了数据访问的便利性,这和数组也有一段渊源:
一维数组名其实是地址,并且一维数组名这个地址,它也是有类型的,就是一级指针类型。
例如代码示例:
int Array[5] = (2, 4, 6, 8, 10);
int *p = Array;
printf("%d", p[0]); //和打印Array[0]一样2
printf("%d", p[1]); //和打印Array[1]一样4
printf("%d", p[2]); //和打印Array[2]一样6
printf("%d", p[3]); //和打印Array[3]一样8
printf("%d", p[4]); //和打印Array[4]一样10
//更令人惊奇的是:
printf("%d", *(Array+0)); //和打印*(p+0)一样
printf("%d", *(Array+1)); //和打印*(p+1)一样
printf("%d", *(Array+2)); //和打印*(p+2)一样
printf("%d", *(Array+3)); //和打印*(p+3)一样
printf("%d",*(Array+4)); //和打印*(p+4)一样
这可能很令人诧异,但请大家牢记这种用法,后面我们会更深入的剖析指针和数组的关系,还有指针各种各样的特性。指针只是一个变量,这个变量是用来存储地址的,指针的复杂性在于它具有多种多样的类型。
数组名就是数组的起始地址,也就是第一个元素的地址。数组名是个常量指针。
例如:
int a[] = {1,2,3,4, 5);
int* p = a;// 这里数组名 a,可以看成整型指针。
p是第一个元素1的地址,p+1就是第一个元素2的地址,以此类推。
类型的变化规律:
1)对变量取地址,得到的是地址,类型为一级指针类型
2)数组名,也是一级指针类型
3)对一级指针解引用,得到的是相应的数据类型
4)对一级指针类型使用下标运算符,得到的是相应数据类型
例如:
int a[] ={1,2,3,4,5};
int* p=a;
这里数组名a,可以看成整型指针类型。
a+2,&a[2],p+2,&p[2]表示同一个一级指针类型的同一地址,
*(a+2)、*&a[2]、*(p+2)、 *&p[2]都表示int 型,数据是a[2]。
使用示例如下:
int main()
{
int a[5] = {1, 2, 3, 4, 5};
int i;
int* p;
for (i = 0; i < 5; i++)
{
printf("a[%d]=%d, *(a+%d)=%d\n", i, a[i], i, *(a + i));
printf("&a[%d]=%p, a+%d=%p\n", i, &a[i], i, a + i);
}
for (p = a; p < a +5; p++)
{
printf("address:%p,value:%d\n", p, *p);
}
}
六、指针与二维数组
二维数组名也是一个指针类型,叫做一维数组指针类型
我们自己定义一维数组指针:类型名 (*变量名)[数组长度]
注意:
对于数组指针,应该给出所指向数组的长度
例如: int (*p)[10];char (*p)[10];
使用示例:
int a[3][4];
int (*p)[4];
p = a;
这里的a或p的类型是 int (*)[4];
类型变化规律:
1) 对一维数组指针解引用会降维,变成 1级指针。
2) 对一维数组指针使用下标运算符也会降维,变成1级指针。
3) 对于一维数组指针+1,会得到下一排起始地址,类型还是数组指针
4) 对1一维数组名取地址,会变为二维数组指针,数值不变。
例如:
对于inta[3][4];
a[i]和*(a+i)等价,都是第 i 行第0列元素的地址,那么 a[i]+j、*(a+i)+i、&a[0][0]+4*i+j都是第 i 行第j列元素的地址。对于 int a[3];
&a,它的类型为 int (*)[3]。即数组指针类型。
使用代码示例如下:
#include <stdio.h>
int main(void)
{
int a[4][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
{10, 11, 12}
};
int(*p)[3] = a;
int i, j;
for (i = 0, j = 0; j < 3; j++)
{
printf("%d\t",*(*p + j));
}
putchar('\n');
for (i = 1, j = 0; j < 3; j++)
{
printf("%d\t", * (p[i] + j));
}
putchar('\n');
for (i = 2, j = 0; j < 3; j++)
{
printf("%d\t", (*(p + i))[j]);
}
putchar('\n');
for (i = 3, j = 0; j < 3; j++)
{
printf("%d\t", *(&p[0][0] + i * 3 + j));
}
putchar('\n');
return 0;
}
七、指针与多维数组
多维数组指的是一维以上的数组,其数组名为多维数组指针类型,数组指针也自然有多维数组指针。
例如:
int Array[2][3][4][5] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14};
int (*p)[3][4][5] = Array;
转换规律:
1)对多维数组指针解引用会降维,变成降一维的数组指针,直到降成一级指针。
2)对数组指针使用下标运算符也会降维,直到降维成一级指针。
3)对于数组指针+1,会得到下一个数组的起始地址,类型不变。
4)对多维数组名取地址,会变为更高维度的数组指针,数值不变。
八、指针数组和数组指针
8.1 指针数组
指针数组就是其元素为指针的数组
每一个元素都是指针变量
说明指针数组的语法格式为:
数据类型 *指针数组名[常量表达式]
示例代码:
#include <stdio.h>
int main(void)
{
//demo1
char* color1[] = {"RED", "GREEN", "BLUE"};
int i1;
for (i1 = 0; i1 < 3; i1++)
puts(color1[i1]);
//demo2
char color2[][6] = {"RED", "GREEN", "BLUE"};
char(*pcolor1)[6] = color2;
int i2;
for (i2 = 0; i2 < 3; i2++)
puts(pcolor1[i2]);
//demo3
char color3[][6] = {"RED", "GREEN", "BLUE"};
char* pcolor2[3];
int i3;
for (i3 = 0; i3 < 3; i3++)
pcolor2[i3] = color3[i3];
for (i3 = 0; i3 < 3; i3++)
puts(pcolor2[i3]);
return 0;
}
指针数组内存储的数据:之前我们演示的示例,指针数组中的指针都是一级指针,其实可以是任意指针类型,例如:
int *p[5]; //一级指针数组
int **p[5]; //二级指针数组
int (*p2[5])[10]; //数组指针数组
8.2 指针数组和二维数组的区别
指针数组存储的是指针,二维数组存储的是数据
如下图示例:
8.3 指针数组和数组指针的区别
在C语言中,指针数组和数组指针是两个不同的概念,它们在内存中的存储方式以及访问方式有所不同。
1)指针数组:指针数组是一个数组,其每个元素都是一个指针。即,这个数组存储的是指针,这些指针指向了内存中的某个位置。
例如,以下是一个指针数组的声明:
int *ptr_array[5];
在这个例子中,ptr_array是一个包含5个整型指针的数组。每个指针都指向一个整型变量的位置。
2)数组指针:数组指针是一个指针,它指向一个数组。即,这个指针指向的是一个连续的内存区域,这个区域存储了一个数组。
例如,以下是一个数组指针的声明:
int (*arr_ptr)[5];
在这个例子中,arr_ptr是一个指向包含5个整型的数组的指针。这个指针指向的是一个连续的内存区域,这个区域存储了一个数组。
两者的主要区别在于:
1. 指针数组的每个元素都是一个指针,这些指针可以指向不同的内存位置。而数组指针指向的是一个数组,即一个连续的内存区域。
2. 通过指针数组访问内存中的数据需要先访问指针,再通过指针访问数据。而通过数组指针访问内存中的数据可以直接访问数据,因为数组指针指向的就是数据所在的内存区域。
数组指针的代码示例:
#include <stdio.h>
int main() {
int arr[] = { 1, 2, 3, 4, 5 };
int* ptr = arr; // 数组指针,指向数组的首元素
// 使用数组指针遍历数组
for (int i = 0; i < sizeof(arr) / sizeof(int); i++) {
printf("%d ", *(ptr + i));
}
// 使用数组指针访问特定元素
printf("\nThe third element is: %d\n", *(ptr + 2));
// 使用数组指针修改特定元素
*(ptr + 2) = 10;
printf("After modification, the third element is: %d\n", *(ptr + 2));
return 0;
}