C语言编程之指针

 |   
C  

c 语言之所以强大,以及其自由性,很大部分体现在其灵活的指针运用上。因此,说指针是 c 语言的灵魂,一点都不为过。同时,这种说法也让很多人产生误解,似乎只有 C 语言的指针才能算指针。basic 不支持指针,在此不论。其实,pascal 语言本身也是支持指针的。从最初的 pascal 发展至今的object pascal,可以说在指针运用上,丝毫不会逊色于c语言的指针。还有 Java 语言,虽然任何教程都没有提及指针,但是在我看来任何类对象(基本数据类型不确定)的创建都使用了指针。所以对指针的理解决定了你对编程语言的理解。

指针基础

指针 (Pointer)是编程语言中的一个对象,它的值直接指向(points to)存在电脑存储器中另一个地方的值。这里先介绍一下变量的三要素:变量名,存储地址和存储内容。在下图的内存分配表中,存储变量(b)的地址(1008) 称为指向变量 b 的指针,意思是通过它(1008)能找到以它为地址的内存单元 b 。而 存储这个指针(1008)的变量(a) 叫做指针变量,又因为这个指针是指向b变量的,所以又叫它为指向变量 b 的指针变量。

pointer

对于指针变量,我们只要抓在两点就可以了,第一个是它指向的地址是多少,第二是它的基类型是啥。对于指针的基类型,在指针的定义中或者 malloc/new 申请内存时很容易获得。指向基本数据类型指针的基类型就是基本数据类型,而数组指针基类型的识别有点难,但其实只要抓住一点就能解决了,即去掉定义中的一个*和变量名,剩下的就是指针的基类型。

1    int *a;     // 基类型为int,它是管辖范围是一个变量
2    int **b;  // 基类型为int *,它是管辖范围是一个一维数组
3    b = (int **)row*sizeof(int*);   // b的类型是int **,其基类型为int*,管辖范围包含row个int*变量的一维数组[C]
4    b = new int*[row];      // b的类型是int **,其基类型为int*,管辖范围是包含row个int*变量的一维数组[C++]

操作符&*

操作符 & 叫取址操作符,其后面常跟变量,用来获取变量的地址的操作符。

操作符 * 叫取值操作符,其后面常跟地址,用来获取地址所定位内存空间里的值。如 *point 是 point 所指向的存储单元的内容,而对于这个内容的理解有两种,一个是变量本身,另一个是具体的存储内容,特别声明这里的内容是变量本身。换句话说就是存储内容的变化也会导致 *point 值的变化。对于指针还有另外一点,就是指针的增加 point+i ,这里指针实际移动的距离是 i*sizeof(指针的基类型)。下面来说一下这个经典案例 *point++,该操作首先考虑优先级,因为这三个操作符都是一元同级操作符,所以按照从右往左的顺序操作。即 *point++等价于*(point++)

这两个操作是一个对立的操作。一个是通过地址获取变量值,另一个是通过变量名获取地址。下面从管辖范围的角度来理解这两个操作。这个管辖范围是对指针基类型在内存上的解读,更物理测量里面的精确度概念类似。其层级为高维数组>一维数组>单个基本数据类型。

  • &E 相当于把E的管辖范围上升了一个级别
  • *E 相当于把E的管辖范围下降了一个级别
  • 数组名相当于指向数组第一个元素的指针,但是其管辖范围根据基类型来定。

样例代码如下所示:

 1    int a[4]={1,2,3,4};
 2    printf("%p\n",a);       //x
 3    printf("%p\n",a+1);     //x+4
 4    printf("%p\n",&a);      //x
 5    printf("%p\n",&a+1);    //x+16
 6    printf("%p\n",*(&a));   //x
 7    printf("%p\n",*(&a)+1); //x+4
 8    printf("\n");
 9
10    int b[2][3]={1,2,3},{4,5,6};    //这里由于jekyll问题,无法在两个一维数组外面添加{}
11    printf("%p\n",b);       //x
12    printf("%p\n",b+1);     //x+12
13    printf("%p\n",&b);      //x
14    printf("%p\n",&b+1);    //x+24
15    printf("%p\n",*(&b));   //x
16    printf("%p\n",*(&b)+1); //x+12
17    printf("%p\n",b[0]);    //x
18    printf("%p\n",b[0]+1);  //x+4
19    printf("%p\n",&b[0]);   //x
20    printf("%p\n",&b[0]+1); //x+12
21    printf("%p\n",*(&b[0]));    //x
22    printf("%p\n",*(&b[0])+1);  //x+4

代码中的一维数组 a 和二维数组 b 在内存中的表示情况如下图所示,其中红色的弧/圈表示指针的管辖范围(精确度)。

pointer

数组和指针

通常在函数调用中,我们会将数组名当作参数传递给函数,而函数中定义形参却通常定义为常量指针(const int* arr)。对于这二者还是有很多相识点的。

  • 最重要的点:数组名代表的就是数组首元素的地址
  • 数组名这样的操作(arr++)是不对的,而 p 却可以(p=arr,p++),因为 arr 是常量类型。
  • 数组中的移位记住这样一个等式 *(point+i )= point[i]

这里稍微提一下二维数组的定义。比如

1    int arr[2][3]={1,2,3,4,5,6};
2    int (*p)[3]=arr;

这里的二维数组指针 p 是代表包含3个 int 型元素的一维数组。

指针和字符串

指针和字符串的区别与指针和数组的区别很类似。因为C语言中没有字符型变量,只能用字符数组来存储,唯一的区别是字符串必须以’\0’结尾。不过在面试的时候经常会问这样的一个问题:下面两种定义有什么区别。

1    char p[10]="hello";
2    char *cp="hello";

对于这个问题,我们得了解这个定义,首先 p 是字符数组,虽然长度为5,但 p[5] 一定为’\0’;而 cp 是这字符指针,是个常量指针,二者存储/指向的内容都为 “hello” 字符串。最关键的一点是两者在内存中存储区不一样。字符数组 p 是存储在栈区,而常量指针本身也是存储在栈区,但是它指向的字符串 “hello” 是存储在一个专门放常量的地方,在程序结束后释放。关于程序存储区的详细讨论将在另一片博文 程序中的存储区中讨论。

常量和指针

常量比较常用来保护实参,限制形参,保证实参在被调函数中的不可改变的特性(const int *)。还有另外一个类似宏定义的功能,又因为常量会被存放在常量区,所以可以节省空间(const int max=100)。不过常量指针(const int *arr)和指针常量(int *const p)是两个非常容易混的概念,这里做一些总结吧。

对于二者的区分,只要记住三句话

  1. *(指针)和 const(常量)谁在前先读谁
  2. *(指针)象征着地址,而 const(常量)象征着内容
  3. 谁在前面谁就不允许改变。

常量指针(const int *p)是指向常量的指针,所以指向的内容是不能改变的,但是这个指针可以指向其他常量。指针常量(int *const p)是指向变量的地址为常量,而指向的内容可以改变,所以指针常量在声明的时候一定要初始化,不能被再赋值,指向别的地址

结合最开始的那幅指针图片来说,对于两个变量 a 和 b,其中 b 是正常的变量,a 是指针变量。如果 a 是常量(就是1008值不能改变),则 a 是指针常量;如果指向的 b 是常量(就是地址为1008里的内容不能改变),则 a 是常量指针。

参考文献

  1. wiki指针
  2. baike
  3. 程序如何使用内存区
  4. 程序中的存储区
  5. 常量指针和指针常量的区别详解
技术茶话会
< 前一篇 后一篇 >