前言
当年学 Pascal 时就极度不理解指针这么个玩意儿,以至于搞 OI 这么多年几乎从没使用过指针。大一学 C 的时候给其他同学答疑,多次触碰到指针这么个知识盲区,不得不赶紧补习一下;这学期补修的 C++ 里更是大篇幅地讲授指针与引用,类与对象的实验里也是各种指针弄得心烦意乱,于是决定把打开这节好好整理一下。
(总目录:https://www.cnblogs.com/jinkun113/p/12528423.html)
子目录列表
1、什么是指针
2、指针的声明
3、NULL 指针
4、void 指针
5、const 指针
6、指针的运算
7、指针与数组
8、指针与类
9、指针与函数
1.3.4 指针与引用
1、什么是指针
指针,是 C 和 C++ 语言中一个非常重要的概念和特点,在其他很多主流语言中都是不存在的,尤其 Java 经常介绍自己时会引以为傲地说自己没有指针这种复杂而容易出错的东西。指针本质为内存地址,了解指针的前提是必须了解计算机的数据存储方式,在学习汇编语言时涉及到了这方面的内容,这里以最简单的方式呈现一下:
小明买了个 4GB 的内存条,4GB = 2 ^ 32 = 16 ^ 8 Byte,这 16 ^ 8 个字节每个字节大小固定,即 8 bit,以线性顺序排列,以十六进制数从 0x00000000 开始编号,到 0xFFFFFFFF,即为内存地址编号。每一个 byte 的数据和内存地址一一对应。
2、指针的声明
① 声明
指针对应的就是内存地址,其本身不表示任何数据,而是对数据的一个指向。指针变量指存放指针的变量,也有数据类型。下面给出几个声明:
1 int *a; 2 char *b; 3 int *c[10]; 4 int (*d)[10]; 5 int **e; 6 int (*f)(int); 7 int (*h[10])(int);
它们分别表示:
L1: 声明一个 int 类型的指针 a
L2: 声明一个 char 类型的指针 b
L3: 声明一个指针数组 c,该数组有 10 个元素,每一个元素都是 int 类型的指针
L4: 声明一个数组指针 d,指针指向一个有 10 个元素的 int 类型的数组
L5: 声明一个 int 类型的指针 e,该指针指向一个 int 类型的指针
L6: 声明一个函数指针 f,该函数有一个 int 参数并返回一个 int 值
L7: 声明一个有 10 个指针的函数指针 g,该函数有一个 int 参数并返回一个 int 值
注意到,和其他类型的变量相比,指针变量在变量名前加上一个 ‘*’。‘*’ 为间接寻址符或间接引用运算符,当它作用于指针时,表示访问指针指向的对象。所以,比如 L1 中,p 是一个指针,保存着一个地址,该地址指向内存中的一个变量,而 *p 表示访问该地址指向的变量。
指针变量的类型表示它所指向变量的类型,而非它自身的类型。任何一个指针自身类型均为 unsigned long int。
但是,这些指针变量,声明完后,本身没有任何意义,因为它只是一个箭头,而箭头指向何处却并没有定义。
② 初始化
那么如何初始化指针变量?
1 int x = 1; 2 int *p1 = &x; 3 4 int *p2; 5 p2 = (int*)malloc(sizeof(int) * 10);
‘&’ 为取地址符,表示变量对应的内存地址。
L1 中声明了一个 int 类型的变量 x,L2 声明了一个 int 类型的指针变量 p1,并将指针指向了 x 对应的内存地址。
malloc 函数表示动态分配内存。L4 声明了一个 int 类型的指针变量 p2,L5 使用 malloc 函数动态分配了 10 个 int 类型大小的内存空间,并将指针 p2 指向了这个内存地址。
这样,这些指针通过初始化后,就都明确了自己所指向的位置。
除了没有初始化的指针是错误的,还可能在使用过程中存在非法操作,比如:int *p1 = 1,并不能直接将数据赋值给指针变量!
③ int *p = &a 问题
前面说了 p 是指针,*p 是指针指向的变量,那为什么上面的代码中是这样赋值的 —— int *p1 = &x,这岂不是将 x 的内存地址赋值给 p1 所指向的变量了吗?这个问题就和 ‘*’ 这个符号有关系了。
我们说 ‘*’ 表示间接寻址符,加在变量名前,这个说法并不严谨。
1 int *p = &a; 2 int *p; 3 p = &a;
对于 L1,它完全等价于 L2, 3,原因在于其正确理解应该是:(int*) p = &a,也就是说,定义了一个 int* 的变量 p,它的初值为 &a,而 int* 就是表示 int 类型的指针变量。但因为习惯问题和格式问题,‘*’ 往往还是会贴在变量名前而非类型名后。
3、NULL 指针
如果需要让一个指针不指向任何变量也是有办法的:int *p = NULL。
NULL 为一种特殊的指针,表示指向内存地址 0。大多数操作系统中,地址 0 为保留地址,就是留给指针需要不指向任何变量时使用的,所以程序不允许访问地址为 0 的内存。
4、void 指针
前面说声明指针时的数据类型为所指向变量的类型,但如果未知该变量的类型,则可以使用 void 指针,它能指向任何数据类型,比如:
void *p1; int *p2; double *p3; char (*p4)[10]; p1 = p2; p1 = p3; p1 = p4;
都是合法的。
void 指针一般用于:作为函数参数向函数传递一个类型可变的对象,或者从函数返回再显式转换为需要的类型。
5、const 指针
const 常量通常很好理解,但修饰指针时情况则很复杂,非常难以区分。
① 指向常量的指针
定义:指针指向的对象为常量,但指针本身为变量。
格式:const <类型> *<指针变量> / <类型> const *<指针变量>
举例:
1 const int a = 77, b = 88; 2 const int *p = &a; 3 *p = 1; // error 4 p = &b;
代码声明了一个 int 类型的常量 a 和一个 int 类型指向常量的指针 p 并指向 a。显然:
L3 是不可行的,因为 *p 指向的 a 是常量,L3 相当于修改常量;
L4 是可行的,将 p 所指向的地址进行修改,因为 p 本身是个变量。
但是,虽然它叫“指向常量的指针”,但实际上也可以指向变量,变量本身已经可以修改值,但是不能通过该指针间接修改,如下代码:
int a = 1; const int *p = &a; *p = 2; // error a = 2;
② 指针常量
定义:指针本身为常量,但所指向的对象为变量。
格式:<类型>* const <指针变量>
举例:
1 int a = 77, b = 88; 2 const c = 99; 3 int* const p1 = &a; 4 *p1 = 1; 5 p1 = &b; // error 6 int* const p2 = &c; // error
L3 声明了一个 int 类型的指针常量 p1 并指向变量 a;
L4 现在是可行的了,因为所指向的对象是变量,可以修改;
L5 不可行,因为指针本身是常量,不能修改;
L6 声明了一个 int 类型的指针常量 p1 并指向常量 c,也是不可行的,这种赋值等同于将 const int* 类型数据赋值给了 int* const 类型。
③ 指向常量的指针常量
定义:指针本身为常量,指向的对象也是常量。
格式:const <类型>* const <指针变量名>
举例:
1 const int a = 77; 2 int b = 88; 3 const int* const p1 = &a; 4 const int* const p2 = &b; 5 p1 = &b, *p1 = 1; // error 6 p2 = &a, *p2 = 2; // error 7 b = 99;
L3, 4 声明了 2 个 int 类型的指向常量的指针常量;
L5, 6 的所有操作都是不可行的,因为指针和指针指向对象都是常量;
L7,和 ① 一样,指向常量的指针常量也可以指向变量,但是只能通过变量自己赋值来修改值。
6、指针的运算
① 指针 ± 整型数
假设 p 指向的 char 类型的变量地址为 0x00000003,则关系如下图:
而 int 类型占用 4 个字节,则关系如下图:
以此类推。
指针与整型数的加减运算只是访问不同的地址,不会改变指针的指向。
② 指针 - 指针
当且仅当两个指针指向同一个数组的元素时,才允许相减,其结果为两个指针在内存中的距离(内存地址 / 数据类型占用字节),比如:
int a[5] = {0, 10, 20, 30, 40}; int *p1 = &a[2], *p2 = &a[5]; cout << p2 - p1;
其结果为 3。
7、指针与数组
① 指针与数组的转化
从指针的角度来看,数组的实质是占用连续一段内存的一系列数据。C 语言中,许多数组操作都可以用指针编写,效率更高,但理解更难。
比如对于数组 int a[10],我们可以用 a[1], a[2], ... 访问元素,也可以声明一个指针变量:
int *p = &a[0];
则 *p 指向 a[0],*(p + 1) 指向 a[1],以此类推,*(p + i) 指向 a[i]。
对于数组,指针的声明方式可以进一步简化,下面代码和上式是等价的。
int *p = a;
② 指针数组与数组指针
int *p[10];
指针数组是一个数组,其中 10 个元素全部都是指向 int 类型的指针。
int (*p)[10];
数组指针是一个指针,它指向一个 int 类型的数组。
前面说指针可以表示一维数组,同样地,指针数组可以表示二维数组。
关于为什么一个需要括号一个不需要,请参见关于运算符的优先级的内容:https://www.cnblogs.com/jinkun113/p/12755769.html。
8、指针与类
请参见:
9、指针与函数
指针可以作为函数的参数。下面给出两个不同的交换 swap 函数:
1 void swap1(int x, int y) { 2 int tmp; 3 tmp = x, x = y, y = tmp; 4 } 5 6 void swap2(int *x, int *y) { 7 int tmp; 8 tmp = *x, *x = *y, *y = tmp; 9 } 10 11 int main() { 12 int x = 1, y = 2; 13 swap1(x, y); 14 cout << x << ' ' << y << endl; 15 x = 1, y = 2; 16 swap2(&x, &y); 17 cout << x << ' ' << y << endl; 18 return 0; 19 }
执行 swap1 函数后,输出结果依旧为 x = 1, y = 2,因为 swap1 虽然进行了交换,但仅仅只是交换其函数内定义的 x 和 y,对主函数定义的变量并无影响;
执行 swap2 函数后,输出结果为 x = 2, y = 1。swap2 传递的是 x 和 y 这两个变量的地址,函数内直接对地址所对应的数据进行交换,相当于直接修改主函数定义的 x 和 y。
本文参考了:
https://www.cnblogs.com/tongye/p/9650573.html
非常详细而不难懂的一篇博文,思路很清晰。