C 指针

2018-08-28  本文已影响0人  苏沫离

1、什么是指针?

指针(pointer)是一个值为内存地址的变量(或数据对象)。

正如 char 类型变量的值是字符, int 类型变量的值是整数,指针变量是一种特殊的变量,是专门用来存储内存地址的。

1.1 变量的名称、地址、值之间的关系?

编写程序时,可以认为变量有两个属性:名称、值(还有其他性质,如变量类型,暂且不讨论)。计算机编译和加载程序时,认为变量也有两个属性:地址、值。地址就是变量在计算机内部的名称。

一个指针变量有两个属性:地址值、指针类型。地址值用来标识指针所指向的变量的首地址; 指针类型告诉编译器, 应该以什么数据类型对指定的内存区域进行访问。

简而言之,普通变量把值作为基本量,把地址作为通过 & 运算符获得的派生量;而指针变量,把地址作为基本量,把值作为通过 * 运算符获得的派生量。

1.2 什么是地址?

既然指针变量的值是地址,那么地址又是什么?为什么不直接使用地址,还要使用指针,搞的这么麻烦?

内存中,以 1 个字节(8 位二进制)作为 1 个基本存储单元,每个存储单元都有一个地址,是一个整数编号,一般用十六进制表示。
如果一个变量占有多个字节(即 占有多个存储单元),那么这个编号就是该变量的首地址(连续内存中最前面的存储单元的编号)。

通过这个地址,可以找到内存中对应的存储单元;根据指针类型,可以知道这个变量在内存中占有多少个字节(多少存储单元);有了对应的存储单元,又有了占有的字节数,就能访问内存中所存储的数据了。

1.3 指针 与 地址 的关系?
指针与地址的关系.png

前文已经说过,一个指针变量有两个属性:地址值、指针类型。指向数据的指针不仅记录该数据在内存中存放的地址,还记录该数据的类型,即在内存中占有多少字节,这是地址所不具备的。正因为地址是没有类型的,所以不能对地址进行算术操作。

变量的地址是内存地址,是系统给每个存储单元拟定的编号。
存放地址的变量,称之为指针变量。

1.4 关于指针的运算符
1.4.1 一元 & 运算符 :给出变量的内存地址
int a = 10;
printf("变量 a 的内存地址 :%p  \n",&a);
//变量 a 的内存地址 :0x7fff5fbff5e0

从上面代码看出,& 运算符写在变量前面,是取地址运算符, 任何变量都是放在内存中的, & 就是获得变量在内存中地址。

1.4.2 解引用运算符(dereference operator)*
int a = 10;
int *p = &a;
printf("*p :%d  \n",*p);//*p :10
printf(" p :%p  \n",p);// p :0x7fff5fbff5e0

上面代码, * 在声明处 和 后面操作变量出现两次:

注意:

虽然指针p的值为 0x7fff5fbff5e0 ,是一个十六进制的整数,但是并不能用处理整数的操作来处理指针,两个整数可以相乘,但是两个把两个指针相乘。实际上,指针是一种新类型,而非整数类型。下文将会提到指针类型

1.5 指针本身所占据的内存区
int *p1;
char *p2;
double *p3;
printf("sizeof(p1) : %zd ; sizeof(p2) : %zd ; sizeof(p3) : %zd \n",sizeof(p1),sizeof(p2),sizeof(p3));
//sizeof(p1) : 8 ; sizeof(p2) : 8 ; sizeof(p3) : 8 

在64位系统中,采用 sizeof()函数打印出,指针本身占据 8 个字节的长度。

2、指针(pointer)的声明:

如何使用一个指针呢?就像其他类型的变量一样,在使用指针变量之前,必须先对其声明。

指针变量声明的一般形式为: type *varName;

2.1 指针的类型 和 指针所指向的类型:

上文提到了指针的类型 和 指针变量的数据类型,这是两个概念,切忌混淆。

2.1.1 指针的类型:

任何变量都有自己的类型,指针变量也有其数据类型,指针的类型是指针变量本身所具有的数据类型。例如:

//变量 a 的数据类型为 int,
int a = 1;
//我们可以理解为:用什么声明这个变量,那么这个变量的类型就是什么。

从语法上看:把指针声明语句里的指针变量名去掉,剩下的部分就是这个指针的类型

int *p1;//指针的类型是 int *  (用 int  *声明的变量p1) 
char *p2;//指针的类型是 char *
int **p3;//指针的类型是 int ** (用 int  **声明的变量p3) 
int (*p4)[3];//指针的类型是 int (*)[3]
int *(*p5)[4]; // 指针的类型是int *(*)[4]
2.1.2 指针所指向的类型:

指针所指向的类型:决定了编译器把那块内存区的数据当做什么来看待。

int a = 1;
int *p = &a;
//指针变量 p 指向变量 a 的地址,那么指针 p 所指向的类型为 int

从语法上看:把指针声明语句中的指针名及其左边的第一个指针声明符 * 去掉,剩下的就是指针所指向的类型

int *p1;//指针所指向的类型是 int 
char *p2;//指针所指向的类型是 char 
int **p3;//指针所指向的类型是 int *
int (*p4)[3];//指针所指向的类型是 int ()[3]
int *(*p5)[4]; //指针所指向的类型是int *()[4]

每当遇到一个指针,我们都应该问自己:这个指针的类型是什么?指针指向的类型是什么?该指针指向了哪里?

2.2 空指针 NULL

注意:声明一个指针变量时,如果没有确切的地址,为指针变量赋一个 NULL 值是一个良好的编程习惯。

例如:

int  *a = NULL;
2.2.1 解引用未初始化的指针:
void statementPointer(void)
{
    int *p;//未初始化的指针
    //waring:Variable 'p' is uninitialized when used here
    printf("指针 p 的地址为: %p \n",p);
    //指针 p 的地址为: 0x1f0000001e
    *p = 5;//严重的错误
    //报错:Thread 1: EXC_BAD_ACCESS (code=1, address=0x1f0000001e)
}

为何不行?将数据 5 存储在指针变量 p 指定地址的这块内存,但是 p 未被初始化,其值是一个随机的内存地址,所以不知道数据 5 将被存储在哪块内存。
这时会出现什么错误?可能会擦写数据或代码,或者导致程序崩溃。

切记:创建一个指针时,系统只分配了存储指针本身的内存,并未分配存储数据的内存,因此在使用指针之前,必须先用已分配的地址初始化它。

2.2.2 关于空指针 NULL

上文例子说明:声明指针变量时,如果暂时不能确定其指向,可以先赋值为NULL

NULL 是一个定义在标准库中的值为 0 的指针常量。赋为 NULL 的指针被称为空指针

#define NULL  ((void *)0)   // NULL的宏定义

外层的括号是为了防止宏定义歧义; 里层的括号则是强制类型转换, 把0 转换成 void *类型, 本来 void * 型就是用来存放地址的, 那么这里的 0自然就是地址 0 了。

空指针是有指向的指针, 但它指向的地址是很小的地址, 约定俗成为地址0x0, 是程序的起始, 这个地址不保存数据, 同时不允许程序访问。所以空指针不能操作该地址, 我们就理解为 指针指向了空, 无法操作了

void statementNULLPointer(void)
{
    int *p = NULL;//初始化为NULL的指针
    printf("指针 p 的地址为: %p \n",p);
    //指针 p 的地址为: 0x1f0000001e
    *p = 5;//严重的错误:0x 地址不保存数据, 同时不允许程序访问,所以 NULL 不能操作该地址
    //报错:Thread 1: EXC_BAD_ACCESS (code=1, address=0x1f0000001e)
}
2.3 关于无确定类型 void *

上文 NULL 的宏定义里提到的 void * 型指针又是什么呢?看起来像是空指针。然而,void * 型指针并不是空指针,这个指针指向了实实在在存放数据的地址,但是该地址存放数据的数据类型我们暂时不知道, 可以理解为无确定类型指针。
void * 型指针可以通过类型转换强制转换为其他类型的指针。

3 指针的初始化与赋值

前文我们声明了一个指针变量,要给它初始化才有意义。给指针变量初始化或者赋值,其实就是让指针指向某个内存地址

下面我们看一段程序:(注:printf() 语句后面的注释为该语句打印的内容)

void pointerInitialize(void)
{
    int a = 10, b = 20;
    printf("&a = %p , &b = %p \n",&a,&b);
    //&a = 0x7fff5fbff65c , &b = 0x7fff5fbff658    
    /*
     1、定义一个指针变量 p ,并初始化其值为变量 a 的地址(指向a的地址)
     2、这个指针变量名为 p ,而不是 *p
     3、指针的类型为 int * ,指针所指向的类型是 int,
     4、指针的类型(指针本身的类型) 与 指针所指向的类型是两个概念
     5、解引用运算符 * :给出指针指向地址上存储的值
     6、& 运算符:给出变量的存储地址, &a 给出变量 a 的内存地址
     7、通过 printf() 语句,可以知道我们成功初始化指针变量 p 的值为 int 型变量 a 的内存地址,
        这时通过解引用 * 可以打印出:存储在指针变量 p 所指向地址的数据为 5,
        即变量 a 的值
     8、注意:地址应该和指针类型兼容,不能把 double 类型的地址赋给指向 int 的指针
     */
    int *p = &a;
    printf("a = %d, *p = %d ,p = %p  \n", a, *p ,p); 
     // a = 10, *p = 10 ,p = 0x7fff5fbff65c
    
    /*
     如同变量 a 可以重新赋值一样,指针变量也可以修改其指向的内存地址(给指针变量重新赋值)
     通过 & 运算符,将变量 b 的地址赋给指针变量 p
     */
    p = &b;
    printf("b = %d, *p = %d ,p = %p  \n", b, *p ,p); 
    // b = 20, *p = 20 ,p = 0x7fff5fbff658

    // 允许修改指针变量所指向的内存地址的值
    // 此时 p 所指向的地址是变量 b 所在的内存地址,修改此地址的内容也就是给该地址所存储的变量 b 重新赋值
    *p = 30;
    printf("b = %d, &b = %p ,*p = %d ,p = %p  \n", b, &b, *p ,p);
    // b = 30, &b = 0x7fff5fbff658 ,*p = 30 ,p = 0x7fff5fbff658

    //变量 b 的内存地址不变,将该内存的内容换成 40;
    //指针变量 p 的值也不变,是变量 b 的内存地址,所以 *p 是 40
    b = 40;
    printf("b = %d, &b = %p ,*p = %d ,p = %p  \n", b, &b, *p ,p);
    //b = 40, &b = 0x7fff5fbff658 ,*p = 40 ,p = 0x7fff5fbff658
}

从上述程序,我们可以了解到:

4 指针 和 数组

前文介绍过,指针提供了一种以符号形式使用地址的方法。在接下来我们就会了解到数组表示法其实是在变相的使用指针。

4.1 数组名是数组首元素的地址
void pointerAndArray(void)
{
    int array[3] = {1,2,3};
    int *p = array;
    printf("p = %p ,array = %p ,&array[0] = %p \n",p,array,&array[0]);
    //p = 0x7fff5fbff61c ,array = 0x7fff5fbff61c ,&array[0] = 0x7fff5fbff61c
    printf("*p = %d ,*array = %d ,array[0] = %d \n",*p,*array,array[0]);
    //*p = 1 ,*array = 1 ,array[0] = 1
}


我们可以观察到:数组名 array 就是数组首元素地址,将 array 赋值给指针变量 p 后,对变量 p 通过解引用运算符 * 可以得到数组首元素 1。

4.1.1 指针加上一个数时,它的值会发生什么变化?

我们来看下面程序:

void pointerAndArray(void)
{
    //在我们的系统中,地址按字节编址,short 类型占 2 个字节,double 类型占 8 个字节
    printf("sizeof(short) : %zd ;sizeof(double) : %zd \n",sizeof(short),sizeof(double));
    //sizeof(short) : 2 ;sizeof(double) : 8

    short dates[4];
    short * pti;
    short index;
    double bills[4];
    double * ptf;
    pti = dates;//把数组地址赋给指针
    ptf = bills;
    
    printf("%30s %15s \n","short","double");
    for (index = 0 ; index < 4; index ++)
    {
        printf("pointers      + %d : %10p %10p \n",index,pti + index,ptf + index);
        printf("array element + %d : %10p %10p \n",index,&dates[index],&bills[index]);
    }
}

打印结果:

                         short          double 
pointers      + 0 : 0x7fff5fbff5b0 0x7fff5fbff590 
array element + 0 : 0x7fff5fbff5b0 0x7fff5fbff590 
pointers      + 1 : 0x7fff5fbff5b2 0x7fff5fbff598 
array element + 1 : 0x7fff5fbff5b2 0x7fff5fbff598 
pointers      + 2 : 0x7fff5fbff5b4 0x7fff5fbff5a0 
array element + 2 : 0x7fff5fbff5b4 0x7fff5fbff5a0 
pointers      + 3 : 0x7fff5fbff5b6 0x7fff5fbff5a8 
array element + 3 : 0x7fff5fbff5b6 0x7fff5fbff5a8 

在 C 中,指针加 1 指的是增加一个存储单元(这个存储单元的大小依据变量的类型来确定)。对于数组而言,这意味着加 1 后的地址是下一个元素的地址,而不是下一个字节的地址。这也是为什么必须声明指针所指向的对象类型的原因之一。只知道内存首地址是不够的,还需要知道这个变量占据多少的字节的存储空间。

4.2 指针变量的基本操作

C 提供了一些基本的指针操作;我们来看程序:

void pointerOperation(void)
{
    printf("sizeof(int) = %zd \n",sizeof(int));
    // sizeof(int) = 4 (int 型占 4 个字节)
    
    /*
      1、声明一个含有5个int 类型元素 的 数组
      2、初始化每个元素的值
      3、array 数组名,是数组首元素的地址
      4、int 占四个字节,这个数组有五个 int 类型元素,占 20 个字节
     */
    int array[5] = {100,200,300,400,500};//声明一个整型数组,并初始化
    printf("sizeof(array) = %zd \n",sizeof(array));
    // sizeof(array) = 20

    /*
     1、声明三个 int* 类型的指针变量
     2、由于未初始化,所以指针变量指向的地址,既指针变量的值不确定是随机值
     */
    int *p1 , *p2 , *p3;//声明指针变量
    printf("p1 = %p , p2 = %p , p3 = %p \n",p1,p2,p3);
    //p1 = 0x2b00000028 , p2 = 0x280000003d , p3 = 0x3d00000024

    
    /*
     此时:p1 与 p2 的值 相差 8 个字节,2个 int 元素
     指针 p1 指向内存地址的值 为100 ,既数组 array 的首位元素 100 ,是第0位元素的地址
     指针 p2 指向内存地址的值 为300 ,既数组 array 的第2位元素 300,是第2位元素的地址
     以上说明:说明数组元素的内存地址是连续的
     
     指针变量 p1 的内存地址为 0x7fff5fbff638
     指针变量 p2 的内存地址为 0x7fff5fbff630
     为什么 p1 比 p2 的地址高呢? 因为栈空间是从 高地址 向 低地址 扩展的,先声明的 p1,那么 p1 的内存地址自然比 p2 的高
     */
    p1 = array;//数组名是数组首元素的地址
    p2 = &array[2];//把一个地址赋给指针
    printf("p1 = %p , *p1 = %d , &p1 = %p \n",p1,*p1,&p1);
    //p1 = 0x7fff5fbff640 , *p1 = 100 , &p1 = 0x7fff5fbff638
    printf("p2 = %p , *p2 = %d , &p2 = %p \n",p2,*p2,&p2);
    //p2 = 0x7fff5fbff648 , *p2 = 300 , &p2 = 0x7fff5fbff630
    
    
    
    /*
     指针 p1 + 4 = 0x7fff5fbff640 + 4 * sizeof(int) = 0x7fff5fbff640 + 16 = 0x7fff5fbff650
     指针减去一个整数:指针必须是减数,整数是被减数
     */
    p3 = p1 + 4;//指针加法
    printf("p1 + 4 = %p , *(p1 + 4)  = %d , &p3 = %p \n",p1 + 4,*(p1 + 4),&p3);
    //p1 + 4 = 0x7fff5fbff650 , *(p1 + 4)  = 500 , &p3 = 0x7fff5fbff628
    printf("p3 = %p , *p3  = %d , &p3 = %p \n",p3,*p3,&p3);
    //p3 = 0x7fff5fbff650 , *p3  = 500 , &p3 = 0x7fff5fbff628
    
    /*
     递增:让 该指针 移动至数组的下一个元素
     此时:变量 p1 的内存地址 仍为 0x7fff5fbff638
     注意:变量不会因为值发生变化就移动位置
     */
    p1 ++;//递增指针
    printf("p1++ = %p , *(p1++) = %d , &p1 = %p \n",p1,*p1,&p1);
    //p1++ = 0x7fff5fbff644 , *(p1++) = 200 , &p1 = 0x7fff5fbff638
    
    
    p2 --;//递减指针
    printf("p2-- = %p , *(p2--) = %d , &p2 = %p \n",p2,*p2,&p2);
    //p2-- = 0x7fff5fbff644 , *(p2--) = 200 , &p2 = 0x7fff5fbff630
    
    /*
     两个指针相减:指针求差 (通常求差的两个指针分别是同一数组的不同元素)
     通过求差,计算出两个元素之间的距离
     */
    printf("p3 - p2 = %td  \n",p3 - p2);
    //p3 - p2 = 3
    
    //指针减去整数
    printf("p3 - 2 = %p  \n",p3 - 2);
    //p3 - 2 = 0x7fff5fbff648
}

上面一段程序演示了指针变量的基本操作

5、指针 和字符串

6、指针 和 函数

7、指针、数组 和 函数

8、指针 和 结构

上一篇 下一篇

猜你喜欢

热点阅读