总目录 > 1 语言基础 > 1.4 C++ 语言基础 > 1.4.3 指针与引用
前言
当年学 Pascal 时就极度不理解指针这么个玩意儿,以至于搞 OI 这么多年几乎从没使用过指针。大一学 C 的时候给其他同学答疑,多次触碰到指针这么个知识盲区,不得不赶紧补习一下;这学期补修的 C++ 里更是大篇幅地讲授指针与引用,类与对象的实验里也是各种指针弄得心烦意乱,于是决定把打开这节好好整理一下。
更新日志
20200903 - 补充了引用的内容。
20200910 - 补充了 new 和 delete。
子目录列表
1、什么是指针
2、指针的声明
3、NULL 指针
4、void 指针
5、const 指针
6、指针的运算
7、指针与数组
8、指针与类
9、指针与函数
10、什么是引用
11、new 和 delete
1.4.3 指针与引用
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 函数表示动态分配内存,在下面 new 和 delete 部分会提到。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,属于系统内置宏定义(即 #define NULL 0)。大多数操作系统中,地址 0 为保留地址,就是留给指针需要不指向任何变量时使用的,所以程序不允许访问地址为 0 的内存。
还有下列两种格式都是等价的:
int *p = 0; int *p = nullptr;
其中后者只适用于 C++ 11。
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 类型的数组。
前面说指针可以表示一维数组,同样地,指针数组可以表示二维数组。
关于为什么一个需要括号一个不需要,请参见 1.1 C 语言基础 中 运算符 部分。
8、指针与类
请参见 1.5.1 类与对象。
9、指针与函数
指针可以作为函数的参数,更多介绍请参见 1.2 C 语言数据类型 中的 函数 部分。
10、什么是引用
左值是放在赋值语句左边的变量,右值是放在赋值语句右边的变量或表达式。从内存角度而言,左值为变量对应的内存区域,右值为内存区域中的内容。
引用是什么?简而言之,是给变量取外号、别名,是 C++ 新增的特性。以前只允许给变量的左值取别名,从 C++ 11 标准开始,可以给右值定义别名。它们分别叫做左值引用和右值引用。
下面暂且只介绍左值引用,且引用一词均表示左值引用。
定义引用的语法为:类型 &引用名 = 变量名
例如,int cab = 9; int &bebe = cab,即 bebe 是 cab 的别名。
和指针一样,引用并非独立的变量,只是一个名称而已。并且,引用不占用任何内存空间,引用的地址就是其所代表的的变量的地址。
谨慎区分取地址符和引用类型符,它们都是 ‘&’。简单区分的话,‘&’ 作为引用类型符出现时,前面必然有数据类型名,即在变量声明时出现的 ‘&’。举个例子:
int &ir = i;
int *ip = &i;
其中,ir 为 i 的引用,ip 为 i 的指针。
引用实质上也是一种指针,但有两点不同:访问方式不同,不再需要 ‘*’ 寻址符;指针占用内存,引用不占用内存。
引用同样可以作为函数的参数,更多介绍请参见 1.2 C 语言数据类型 中的 函数 部分。
11、new 和 delete
① malloc() 和 free()
指针常与堆空间的分配有关。所谓堆,就是一块内存区域,允许程序在运行时以指针的方式从其中申请一定数量的存储单元,用于程序数据的处理。不同于其他存储空间的分配是在编译时完成的,是静态的,堆内存则是动态的。在 C 语言中,动态分配内存是通过 malloc 和 free 两个函数来完成的,malloc() 用来申请,free() 用来释放,比如:
int *p; p = (int*)malloc(sizeof(int)); *p = 22; printf("%d", *p); free(p);
释放内存的意义在于,如果某个变量不再需要,则可以将其所占用的内存归还,从而其他程序可以使用,而不释放,则造成内存泄漏,相当于无效地占用了内存。
可以看到,malloc() 函数使用起来挺麻烦的,不仅要计算需求的内存大小(sizeof(...)),还要确定具体转换类型(int)。C++ 提供的 new 和 delete 运算符,分别与 malloc 和 free 对应,大大方便了程序的编写。
② new
一般格式为:
数据类型 p;
p = new 数据类型(初值);
初值可缺省。同样可以分配数组。如下:
int *p1; double *p2; char *p3; p1 = new int; p2 = new double(2.33); p3 = new int[20];
new 会自动匹配对应的数据类型,也会计算分配的内存大小。
② delete
一般格式为:
delete p;
delete []p;
前者适用于指向独立变量,后者适用于指向数组。如下:
delete p1; delete p2; delete []p3;
如果写 delete p3,则只释放了指向数组的第一个元素。
本文参考了:
https://www.cnblogs.com/tongye/p/9650573.html
非常详细而不难懂的一篇博文,思路很清晰。