人小鬼不大

导航

 

  

  计算机中所有的数据都必须放在内存中,以二进制的形式存储在内存中,才能被CPU所使用。不同类型的数据占用的字节数不一样,为了正确地访问这些数据,必须为每个字节都编上号码。将内存中字节的编号称为地址(Address)或指针(Pointer)。地址从 0 开始依次增加,对于 32 位环境,程序能够使用的内存为 4GB,最小的地址为 0,最大的地址为 0XFFFFFFFF。

  计算机无法从格式上区分某块内存到底存储的是数据还是代码。当程序被加载到内存后,操作系统会给不同的内存块指定不同的权限,权限中包含执行权限的内存块就是代码,否则是数据。

  CPU 只能通过地址来取得内存中的代码和数据,程序在执行过程中会告知 CPU 要执行的代码以及要读写的数据的地址。如果程序不小心出错,或者开发者有意为之,在 CPU 要写入数据时给它一个代码区域的地址,就会发生内存访问错误。这种内存访问错误会被硬件和操作系统拦截,强制程序崩溃,程序员没有挽救的机会。CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。

1、指针变量

  若果一个变量存储了一份数据的指针,我们就称它为指针变量,指针变量的值就是某份数据的地址

1.1 定义

  定义指针变量和定义普通变量类似,不过需要在变量名前加*号。*表示其实一个指针变量,类型表示该指针变量所指向的数据的类型 。指针变量也可以被多次写入,随时都能够改变指针变量的值定义指针变量时必须带*,给指针变量赋值时不能带*。指针变量也可以连续定义,但是要注意每个变量前面都要带*

1.2 取值

  指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:*pointer;  *在此处称为指针运算符,用来取得某个地址上的数据。普通变量只需一次即可取到数据,指针变量需要2次,因为其内存中存的是地址,还需要根据地址去取数据,即使用指针是间接获取数据,使用变量名是直接获取数据,前者比后者的代价要高

  *在不同的场景下有不同的作用:*可以用在指针变量的定义中,表明这是一个指针变量,以和普通变量区分开;使用指针变量时在前面加*表示获取指针指向的数据,或者说表示的是指针指向的数据本身。指针变量定义时初始化,可使用*pointer取得数据,给指针变量本身赋值时不能加*

1.2.1 关于*和&

若有一个 int 类型的变量 a,pa 是指向它的指针,即int a = 12;int* pa = &a;那么*&a&*pa的含义是:

  *&a可以理解为*(&a)&a表示取变量 a 的地址(等价于 pa),*(&a)表示取这个地址上的数据(等价于 *pa),即*&a仍然等价于 a。
  &*pa可以理解为&(*pa)*pa表示取得 pa 指向的数据(等价于 a),&(*pa)表示数据的地址(等价于 &a),所以&*pa等价于 pa。

1.2.2 指针变量的运算

  对于指向普通变量的指针,我们往往不进行加减运算,虽然编译器并不会报错,但这样做没有意义,因为没有规定变量的存储方式,如果连续定义多个变量,它们有可能是挨着的,也有可能是分散的,这取决于变量的类型、编译器的实现以及具体的编译模式,这样也就不知道在这个变量地址的后面是否有数据。因此不要尝试通过指针获取下一个变量的地址,如*(p+i),获取到的数据有可能并不知道你想要的的。

  当对指针变量进行比较运算时,比较的是指针变量本身的值,也就是数据的地址。如果地址相等,那么两个指针就指向同一份数据,否则就指向不同的数据。最后对于普通变量而言,最好不要对指针变量进行乘法、除法、取余等其他运算,除了会发生语法错误,也没有实际的含义

2、数组指针

  定义数组时,要给出数组名和数组长度,数组名可以认为是一个指针,它指向数组的第 0 个元素。数组名和数组首地址并不总是等价,可以说“被转换成了一个指针”。对于数组而言,可以直接将数组名赋值给指针变量 。若一个指针指向了数组,就称它为数组指针(Array Pointer)。数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关。

1 int arr[] = { 99, 15, 100, 888, 252 };
2 int *p = arr;

  数组指针可以使用两种方法来访问数组元素,一种是使用下标,另外一种是使用指针。不同的是,数组名是常量,它的值不能改变,而数组指针是变量(除非特别指明它是常量),它的值可以任意改变。也就是说,数组名只能指向数组的开头,而数组指针可以先指向数组开头,再指向其他元素。

2.1 数组指针使用含义

若p 是指向数组 arr 中第 n 个元素的指针, *p++、*++p、(*p)++ 的含义是:

  *p++ 等价于 *(p++),表示先取得第 n 个元素的值,再将 p 指向下一个元素。
  *++p 等价于 *(++p),会先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 *(p+1),所以会获得第 n+1 个数组元素的值。
  (*p)++ ,会先取得第 n 个元素的值,再对该元素的值加 1。假设 p 指向第 0  个元素,并且第 0 个元素的值为 99,执行完该语句后,第 0  个元素的值就会变为 100。

3、指针数组

  若一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。指针数组的定义形式一般为:

      dataType *arrayName[length];

[ ]的优先级高于*,该定义形式应该理解为:

      dataType *(arrayName[length]);

若存放的是字符串,需要注意的是:字符数组 str 中存放的是字符串的首地址,不是字符串本身,字符串本身位于其他的内存区域,和字符数组是分开的。

4、二维数组指针

1 int (*p)[4] = a;

  括号中的*表明 p 是一个指针,它指向一个数组,数组的类型为int [4],这正是 a 所包含的每个一维数组的类型。[ ]的优先级高于*( )是必须要加的,否则就是指针数组。对指针进行加法(减法)运算时,它前进(后退)的步长与它指向的数据类型有关,p 指向的数据类型是int [4]。p+1就是前进 4×4 = 16 个字节。

  根据定义可以知道:p指向数组 a 的开头,也即第 0 行;p+1前进一行,指向第 1 行;*(p+1)表示取地址上的数据,也就是整个第 1 行数据

4.1 表达式含义

  *(p+1)+1表示第 1 行第 1 个元素的地址:*(p+1)单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址,因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针。

  *(*(p+1)+1)表示第 1 行第 1 个元素的值:*(p+1)+1表示第 1 行第 1 个元素的地址,加上*表示取地址中的值。

可知:

1 a+i == p+i
2 a[i] == p[i] == *(a+i) == *(p+i)
3 a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j)

4.2 指针数组和二维数组指针的区别

定义如下:

1 int *(p1[5]);  //指针数组,可以去掉括号直接写作 int *p1[5];
2 int (*p2)[5];  //二维数组指针,不能去掉括号

  指针数组是一个数组,只是每个元素保存的都是指针。二维数组指针是一个指针,它指向一个二维数组。

5、字符串指针

  2种方法:定义字符串数组,再赋值给指针变量;直接使用一个指针指向字符串。

1   //使用*(str+i)
2     for(i=0; i<len; i++){
3         printf("%c", *(str+i));
4     }
5     printf("\n");
6     //使用str[i]
7     for(i=0; i<len; i++){
8         printf("%c", str[i]);
9     }

  使用指针变量输出字符串和下标输出的区别:在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。内存权限的不同导致的一个明显结果就是,字符数组在定义后可以读取和修改每个字符,而对于第二种形式的字符串,一旦被定义后就只能读取不能修改,任何对它的赋值都是错误的

6、指针变量作为函数参数

  用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。

1 max(int *intArr, int len)

  数组是一系列数据的集合,无法通过参数将它们一次性传递到函数内部,如果希望在函数内部操作数组,必须传递数组指针参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。但数组是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行内存拷贝有可能是一个漫长的过程,会严重拖慢程序的效率。需要强调的是:不管使用哪种方式传递数组,都不能在函数内部求得数组长度,因为 intArr 仅仅是一个指针,而不是真正的数组,所以必须要额外增加一个参数来传递数组长度。

7、指针作为函数返回值

  允许函数的返回值是一个指针(地址),这样的函数称为指针函数。用指针作为函数返回值时需要注意的一点是,函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误。也就是所谓的销毁并不是将局部数据所占用的内存全部抹掉,而是程序放弃对它的使用权限,弃之不理,后面的代码可以随意使用这块内存。

8、函数指针

  函数指针的定义形式为:returnType (*pointerName)(param list);

  注意( )的优先级高于*,第一个括号不能省略,如果写作returnType *pointerName(param list);就成了函数原型,它表明函数的返回值类型为returnType *

8.1 使用指针实现对函数的调用

1     //定义函数指针    max是一个函数
2     int (*pmax)(int, int) = max;  //也可以写作int (*pmax)(int a, int b)
3     ...
4     maxval = (*pmax)(x, y);

  pmax 是一个函数指针,在前面加 * 就表示对它指向的函数进行调用。注意( )的优先级高于*,第一个括号不能省略。

9、二级指针

  若一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。指针变量也是一种变量,也会占用存储空间,也可以使用&获取它的地址。获取指针指向的数据时,一级指针加一个*,二级指针加两个*,类推其他级别指针。

1  int a =100;
2     int *p1 = &a;
3     int **p2 = &p1;
4     int ***p3 = &p2;
5     printf("%d, %d, %d, %d\n", a, *p1, **p2, ***p3);
6     printf("&p2 = %#X, p3 = %#X\n", &p2, p3);
7     printf("&p1 = %#X, p2 = %#X, *p3 = %#X\n", &p1, p2, *p3);
8     printf(" &a = %#X, p1 = %#X, *p2 = %#X, **p3 = %#X\n", &a, p1, *p2, **p3);

以上每行输出的值是一样的,因为指向一样的地址。

 

 

 方框里面是变量本身的值,方框下面是变量的地址。

10、总结

  程序在运行过程中需要的是数据和指令的地址,程序被编译和链接后,变量名、函数名等都会消失,取而代之的是它们对应的地址。

10.1 常见指针变量的定义

定  义含  义
int *p; p 可以指向 int 类型的数据,也可以指向类似 int arr[n] 的数组。
int **p; p 为二级指针,指向 int * 类型的数据。
int *p[n]; p 为指针数组。[ ] 的优先级高于 *,所以应该理解为 int *(p[n]);
int (*p)[n]; p 为二维数组指针。
int *p(); p 是一个函数,它的返回值类型为 int *。
int (*p)(); p 是一个函数指针,指向原型为 int func() 的函数。

10.2 使用

  1)指针变量可以进行加减运算,并不是简单的加上或减去一个整数,而是跟指针指向的数据类型有关

  2)给指针变量赋值时,要将一份数据的地址赋给它,不能直接赋给一个整数

  3)使用指针变量之前一定要初始化,对于暂时没有指向的指针,建议赋值NULL

  4)两个指针变量可以相减。如果两个指针变量指向同一个数组中的某个元素,那么相减的结果就是两个指针之间相差的元素个数

  5)数组也是有类型的,数组名的本意是表示一组类型相同的数据。在定义数组时,或者和 sizeof、& 运算符一起使用时数组名才表示整个数组,表达式中的数组名会被转换为一个指向数组的指针

posted on 2020-01-23 11:41  人小鬼不大  阅读(210)  评论(0编辑  收藏  举报