使用指针是C语言的一大特点,可以说想学到C的精髓就逃不掉指针这一关。而对于很多初学而言指针并不像条件判断、循环控制语句一样友好,想要学好指针、能在代码中使用指针,笔者认为就要先了解数据在内存中如何存储等先修知识,这样更有利于指针的学习。以下是笔者根据《C Primer Plus》整理的学习笔记,希望能对大家学习指针有所帮助。由于水平有限,如在文中出现错误,感谢大家及时指正。
提示:以下是本篇文章正文内容,下面案例在VS中均能运行,如果读者使用的是DEVC++,有些细节需要改动
1.一般形式的常量和变量
首先,为了便于理解我们可以把内存简化成划分为很多格子的纸带,每一个格子的大都是1字节,即1byte,而每一个格子对应两个值,一个是它的地址,一个是它存储的值。
不同的数据类型所占据的内存大小不同,在不同的编译器和不同硬件条件上大小也不同,有32为二进制和64位两种(即4字节和8字节),笔者电脑int类型的数据在内存中占4个字节,而char类型的数组在内存中占1字节,这是我们最常用的两个数据类型,指针型变量(也取决于程序是32位还是64位)占4字节,如果还想知道其他数据类型所占的字节数,可以用sizeof语句来求得
int a=5;
double b=5;
printf("int类型占%d bytes",sizeof(a));
printf("double类型占%d bytes",sizeof(b));
2.指针的存储形式
和其他变量相同,系统会为声明的指针变量划分一块内存空间,而在这块空间中存储的不是一个普通的常数或字符,而是该指针所指向变量a的地址,系统通过这个地址找到这个变量a的值
为了让系统知道我们在内存单元格中存储的是一个地址而不是一个普通的16进制数(0x前缀表示该数是16进制),在数据类型后加上*表示该变量为指针变量(后文将详细解释指针数据类型)
注:* 的两种用途:
(1)在声明指针变量时,*表示该变量是指针类型
(2)在使用指针变量时,*作为解引用符,表示该指针所指向的变量中的值
int *ptr;
int a=5;
ptr=&a;//ptr中存储的是a的地址
int temp;
temp=*(ptr)//temp=5
3.一维数组和二维数组的存储形式
(1)与单个变量不同,当我们申请一个元素个数确定的数组时,系统会在内存上为我们划分一块连续的空间,让我们存储整个数组,而指向数组的指针中存储的数组的首地址
int array[3]={100,200,300};
int *ptr=array //ptr=0x11111111
(2)对于二维数组我们通常可以把他看成由一定行数和列数组成的矩阵,但在内存中二维数组的存储仍然是一维的,我们需要把一个二维数组看成一维数组在内存中存储的方式进行存储,数组的大小为矩阵的行数,但每个内存单元中存储的不是矩阵中的数值,而是一个地址,这个地址指向的矩阵中每一行的第一个元素。
通过图示我们可以不太严谨的把二维数组存储理解为一个line*row的矩阵被拆成了line个包含row个元素的数组。在内存中二维数组会变为一个元素个数为line的数组,而这个数组的每个元素的数据类型都是一个包含row个元素的数组。所以line数组的每个元素所占的内存不再是int型的4字节,而是row*int字节(如果是char型的二维数组计算方法同理,将4字节变成1字节)。
通过上述解释相信大家对二维数组的存储有了一定的理解,但这种描述并不严谨,因为数组型数据类型是为了方便理解我们假设出来的,而内存实际上是一个一维的。在实际的内存中,系统会为每一个line[i]先划分一块连续的大小为row*int的内存,并在这块内存上存入row个值
1.指针的数据类型
int a=5;
int* ptr;
ptr=&a;
在上文中说到,在内存中一个int型变量a=5在它的内存单元中存储的内容是5,而一个int*型的指针在它的内存单元中存储的是变量a所在的首地址
那么我们如何判断指针的类型呢,一个简单的办法是:在一个声明指针变量的语句中,我们把变量名去掉,剩下的就是指针的类型,例:
(1)int*ptr;//指针的类型是int*
(2)char*ptr;//指针的类型是char*
(3)int**ptr;//指针的类型是int**
(4)int(*ptr)[3];//指针的类型是int(*)[3]
(5)int*(*ptr)[4];//指针的类型是int*(*)[4]
2.指针所指向的数据类型
在1.1中提到不同的数据类型所占据的字节数不同,而一个指针变量中存储的是该指针指向的变量的地址,指针指向的数据类型作用为:
(1)说明指针变量所取内存的宽度(如int型占4字节)
(2)说明对指针变量进行+1操作的跨度是多少(该操作在2.3中会详细说明)
判断指针所指向的数据类型的方法:在一个声明指针变量的语句中,我们把变量名和离它最近的一个*去掉,剩下的就是指针所指向的数据类型,例
(1)int*ptr; //指针所指向的类型是int
(2)char*ptr; //指针所指向的的类型是char
(3)int**ptr; //指针所指向的的类型是int*
(4)int(*ptr)[3]; //指针所指向的的类型是int()[3]
(5)int*(*ptr)[4]; //指针所指向的的类型是int*()[4]
指针提供一种以符号形式使用地址的方法。因为计算机的硬件指令非常依赖地址,指针在某种程度上把程序员想要传达的指令以更接近机器的方式表达。
int array[5]={1,2,3,4,5};
int *ptr1;
int *ptr2;
ptr1=array;
ptr2=&array[0];
ptr1=array和ptr2=&array[0]都表示数组元素的内存地址
在c语言中指针具有很好的灵活性
int array[5]={1,2,3,4,5};
int *ptr1;
ptr1=array;
ptr+2==&array[2];//相同的地址
*(ptr+2)=array[2];//相同的值
注意:不要混淆*(ptr1+2)
和*ptr1+2
。间接运算符*的优先级高于+,所以*ptr1+2
相当于(*ptr1)+2
*(ptr1+2)//ptr1第三个元素的值
*ptr1+2//ptr1第一个元素的值加2
可以把地址赋给指针,例如用数组名,带&取地址符的变量名,或另一个指针进行赋值。注意:赋值的类型要和指针的类型相同,也就是说不能把double类型的地址赋值给int类型的指针变量
用乘号*来进行解引用,即*ptr表示的是指针ptr所指向地址所存放的值
运算符&可以用来取得变量的地址,即
int a=100;
声明一个int变量
int* ptr;
声明一个int型指针变量
ptr=&a;
将指针变量与int变量建立联系
表示将a的地址取到int类型的指针变量ptr中。
同时,和所有变量一样,指针变量也有自己的地址和值,所以&取地址符也可以给出指针本身的地址
在预备知识中提到过不同数据类型的变量在内存中所占的字节数不同,如常见的char型变量占1字节,而int型变量占4字节。
在指针与整数相加时,整数会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。如果相加结果超出了初始指针指向的数组范围,计算结果则是未定义的。
递增指向数组元素的指针可以让该指针移动至数组的下一元素,例:
int array[5]={1 ,2 ,3 ,4 ,5}
int* ptr=array //*(ptr)=1
ptr++ //*(ptr)=2
在指针与整数相减时,整数会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相减。
与递增指针类似。前缀或后缀的递增和递减运算符都是可以使用的。
可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素间的距离。差值的单位与数据类型的单位相同
int array[5]={1 ,2 ,3 ,4 ,5};
int* ptr1=&array[0];
int* ptr2=&array[2];
ptr2-ptr1//值等于2
上述代码的意思是这两个指针所指向的两二元素相隔2个int,而不是2个字节
当两个指针都指向相同类型的对象时,使用运算关系可以比较两个指针的值。注意:当直接对两个指针变量进行比较时,这时比较的是两个指针的地址,而不是指针所指向的值;
上述指针操作的示例如下
#define _CRT_SECURE_NO_WARNINGS
#include
#include
int main(void) {
int array[5] = { 100,200,300,400,500 };
int* ptr1, * ptr2, * ptr3;
ptr1 = array;//把一个地址赋给指针
ptr2 = &array[2];//把一个地址赋给指针
printf("指针指向的地址, 指针指向地址的值, 指针自己的地址\n");
printf("ptr1 = %p, *ptr1 = %d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
printf("\n");
//指针加法
ptr3 = ptr1 + 4;
printf("指针加一个整数后的值:\n");
printf("ptr1+4 = %p, *(ptr+4) = %d\n", ptr1 + 4, *(ptr1 + 4));
printf("\n");
ptr1++;//递增指针
printf("ptr1++后的值:\n");
printf("ptr1=%p, *ptr1=%d, &ptr1=%p\n ", ptr1, *ptr1, &ptr1);
printf("\n");
ptr2--;//递减指针
printf("ptr2--后的值:\n");
printf("ptr2=%p, *ptr2=%d, &ptr2=%p\n ", ptr2, *ptr2, &ptr2);
printf("\n");
--ptr1;//恢复初始值
++ptr2;//恢复初始这
printf("指针回复初始值:\n");
printf("ptr1=%p , ptr2=%p\n", ptr1, ptr2);
printf("\n");
//一个指针减去另一个指针
printf("一个指针减去另一个指针\n");
printf("ptr2 = %p, ptr1= %p ,ptr2-ptr1= %td\n", ptr2, ptr1, ptr2 - ptr1);
printf("\n");
//一个指针减去一个整数;
printf("一个指针减去一个整数");
printf("ptr3 = %p ,ptr3-2 = %p\n", ptr3, ptr3 - 2);
return 0;
}
1.用指针表示一维数组形参
函数示例
int sum(int* array,int n){
int total;
for(int i=0;i<n;i++){
total +=array[i];
}
return total;
}
因为数组名是该数组首元素的地址,作为实际参数的数组名要求形式参数是一个与之匹配的指针,只有在这种情况下,C才会把int array[]
和int *array
解释成一样的
下面让我们来看一下主函数中数组占据的内存大小和作为传入函数形参的数组在内存中占据的大小是否相同
#define _CRT_SECURE_NO_WARNINGS
#include
#include
int Sum(int array[], int n);
int main(void) {
int size = 10;
int list[10] = { 20 ,10 ,5 ,39,4,16,19,26,31,20 };
long answer;
answer = Sum(list, size);
printf("主函数中的数组list占据的内存为%zd bytes \n", sizeof(list));
printf("数组中所有数的和为%d\n", answer);
return 0;
}
int Sum(int array[], int n)
{
int total = 0;
for (int i = 0; i < n; i++) {
total += array[i];
}
printf("传到构造函数中数组array占据的内存为%zd bytes \n", sizeof(array));
return total;
}
因为主函数函数中定义的数组大小为10个int,所以它占据的内存为40bytes,而因为传到构造函数的形参实际传递的是数组的首地址,而不是整个数组,所以它的实际大小只有4bytes,即一个指针的大小(注意不是一个int的大小,虽然数值相同,但意义不同)
在知道如果使用指针作形参将数组传入函数后,我们还应知道处理该数组时何时开始、何时结束。在上述例子中我们传递了两个参数,一个是数组的首地址,另一个为数组中元素的个数。另一种方法为传递两个指针,第一个指针表明数组的开始处,第二个指针表明数组的结束处。
#define _CRT_SECURE_NO_WARNINGS
#include
#include
int SumPtr(int *start, int *end);
int main(void) {
int size = 10;
int list[10] = { 20 ,10 ,5 ,39,4,16,19,26,31,20 };
long answer;
answer = SumPtr(list, list+size);
printf("数组中所有数的和为%d\n", answer);
return 0;
}
int SumPtr(int* start, int* end){
int total = 0;
while (start < end) {
total += *(start);
start++;
}
return total;
}
2.用指针表示二维数组形参
如何用指针表示二维数组将来后文详细介绍,此处只介绍将二维数组作函数形参的三种形式
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#define Line 3 //二维数组的行数
#define Row 3 //二维数组的列数
//方法一:直接使用数组
int sum1(int matrix[][Row],int line,int row);
//方法二:使用指针,PtrArray是一个指向包含Row个int的数组指针
int sum2(int(*PtrArray)[Row], int line, int row);
//方法三,使用指针,ptr是一个指向int的指针
int sum3(int* ptr, int line, int row);
int main(void) {
int matrix[Line][Row] = { {1,4,7},{2,5,8} ,{3,6,9} };
int result;
//方法一
result = sum1(matrix, Line, Row);
printf("sum1的结果为%d\n", result);
//方法二
result = sum2(matrix, Line, Row);
printf("sum2的结果为%d\n", result);
//方法三
result = sum3(matrix[0], Line, Row);
printf("sum3的结果为%d\n", result);
return 0;
}
int sum1(int matrix[][Row], int line, int row)
{
int result = 0;
for (int i = 0; i < line; i++) {
for (int j = 0; j < row; j++) {
result += matrix[i][j];
//或使用指针形式
//result += *(*(matrix+i)+j);
}
}
return result;
}
int sum2(int(*PtrArray)[Row], int line, int row)
{
int result = 0;
for (int i = 0; i < line; i++) {
for (int j = 0; j < row; j++) {
result += PtrArray[i][j];
//或使用指针形式
//result += *(*(PtrArray)+j);
}
}
return result;
}
int sum3(int* ptr, int line, int row)
{
int result = 0;
for (int i = 0; i < line; i++) {
for (int j = 0; j < row; j++) {
result += ptr[i * row + j];
//或使用指针形式
//result += *(pr+i*row+j);
}
}
return result;
}
在1.1中已经详细解释了二维数组在内存中是如何存储的,此处我们重点演示用指针对二维数组进行操作。假设有下面的声明
int matrixe[4][2];
数组名matrix是该数组首元素的地址,在本例中,matrix的首元素是一个内含两个int值的数组,所以matrix是这个内含两个int值数组的地址。下面我们进一步分析
#define _CRT_SECURE_NO_WARNINGS
#include
#include
int main(void) {
int matrix[4][2] = { {2,4},{6,8}, {1,3} ,{5,7} };
printf(" matrix = %p, matrix+1 = %p\n",matrix,matrix+1);
printf(" matrix[0] = %p, matirx[0]+1 = %p\n",matrix[0],matrix[0]+1);
printf(" *matrix = %p, *matrix +1 = %p\n",*matrix,*matrix+1);
printf(" matrix[][] = %d \n",matrix[0][0]);
printf(" *matrix[0] = %d \n",*matrix[0]);
printf(" **matrix = %d\n",**matrix);
printf(" matrix[2][1]=%d\n",matrix[2][1]);
printf(" *(*(matrix+2)+1)=%d\n", *(*(matrix + 2) + 1));
return 0;
}
(注意:因为地址只用的是16进制,所以006FFE08+8=006FFE10)
如果程序恰好使用一个指向二维数组的指针,而且要通过该指针获取值时,最好使用简单的数组表示法而不是指针表示法
在本文中介绍了不同数据类型在内存中的存储方式,指针的两种类型,对指针的基本操作,使用指针形参,指针与二维数组
在C——指针学习知识总结(下)中将介绍指针与动态内存和指针在数据结构上的应用