[知识点] 1.4.3 指针与引用

总目录 > 1  语言基础 > 1.4  C++ 语言基础 > 1.4.3  指针与引用

前言

  当年学 Pascal 时就极度不理解指针这么个玩意儿,以至于搞 OI 这么多年几乎从没使用过指针;

  大一学 C 的时候给其他同学答疑,多次触碰到指针这么个知识盲区,不得不赶紧补习一下;

  这学期补修的 C++ 里更是大篇幅地讲授指针与引用,类与对象的实验里也是各种指针弄得心烦意乱,于是决定把打开这节好好整理一下;

  不过,虽然指针放在 C++ 语言基础类来讲,但它是 C 语言中就有的内容,而引用才是 C++ 专属的。

更新日志

  20200903 - 补充了引用的内容;

  20200910 - 补充了 new 和 delete;

  20211028 - 补充内容,修正错误,调整结构。

子目录列表

  1.4.3.1 什么是指针

  1.4.3.2 指针的声明

  1.4.3.3 NULL 指针

  1.4.3.4 void 指针

  1.4.3.5 const 指针

  1.4.3.6 指针的运算

  1.4.3.7 指针与数组

  1.4.3.8 指针与类

  1.4.3.9 指针与函数

  1.4.3.10 什么是引用

  1.4.3.11 动态内存分配

 

1.4.3  指针与引用

1.4.3.1 什么是指针

  指针,是 C 和 C++ 语言中一个非常重要的概念和特点,在其他很多主流语言中都是不存在的,尤其 Java 经常介绍自己时会引以为傲地说自己没有指针这种复杂而容易出错的东西;

  指针的本质为内存地址,了解指针的前提是必须了解计算机的数据存储方式,在学习《汇编语言》时涉及到了这方面的内容,这里以最简单的方式呈现一下:

  小明买了个 4GB 的内存条,4GB = 2 ^ 32 = 16 ^ 8 Byte,这 16 ^ 8 个字节每个字节大小固定,即 8 bit,以线性顺序排列,以十六进制数从 0x00000000 开始编号,到 0xFFFFFFFF,即为内存地址编号;

  每一个 byte 的数据和内存地址一一对应。

    

 

1.4.3.2 指针的声明

  ① 声明

    对于一个变量,数据是变量的值,内存地址是变量的指针;指针本身也是一个值,这个值表示这个变量的地址,而不表示任何数据;

    指针变量存放指针的变量,也有数据类型。下面给出几个声明:

1 int *a;     
2 char *b;     
3 int *c[10];   
4 int (*d)[10];
5 int **e;
6 int (*f)(int);
7 int (*g[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 值。

    和其他类型的变量相比,指针变量在变量名前加上了一个 '*';

    指针变量前面的类型名是用来规定它所指向变量的类型,而非它自身的类型,任何一个指针自身类型均为 unsigned long int

    也就是说,所有指针变量占用的内存单元数量是相同的;

    但是,这些指针变量,单纯声明是没有任何意义的,因为它只是一个箭头,而箭头指向何处却并没有定义。

  ② 初始化

    那么如何初始化指针变量?

1 int a = 1;
2 int *p1 = &a;
3 
4 int b = 2;
5 int *p2;
6 p2 = &b;
7 
8 int *p3;
9 p3 = (int*)malloc(sizeof(int) * 10);

    '&' 为取地址符,表示变量对应的内存地址;

    L1 中声明了一个 int 类型的变量 a,L2 声明了一个 int 类型的指针变量 p1,并将指针指向了变量 a 的内存地址;

    L4 中声明了一个 int 类型的变量 a,L5 声明了一个 int 类型的指针变量 p2,并在 L6 将指针指向了变量 b 的内存地址;

    L8 声明了一个 int 类型的指针变量 p3,L9 使用 malloc 函数动态分配了 10 个 int 类型大小的内存空间,并将指针 p3 指向了这个内存地址;

    malloc 函数用于动态分配内存,在下面 new 和 delete 部分会介绍;

    这样,这些指针通过初始化后,就都明确了自己所指向的位置。

    除了没有初始化的指针是错误的,还可能在使用过程中存在非法操作,比如:int *p1 = 1,并不能直接将数据赋值给指针变量!

  ③ '*' 的理解

1 int a = 1, b = 2;
2 int *p1 = &a, *p2;
3 p2 = &b;
4 
5 printf("%d %d", p1, *p1);

    '*' 到底是什么?上面代码的输出会是什么?结果是:变量 a 的地址 + 变量 a 的值(1);

    我们已经知道,在声明时加上 * 就可以表示这是一个指针变量,比如 *p1;

    但是,这个指针变量的变量名其实仍然是 p1,而 *p1 则表示他所指向的变量了,容易混淆的点在于,L2 和 L5 的 '*' 其实是有区别的:

      L2 的 '*' 是在变量声明时说明其为指针变量;

      L5 的 '*' 是间接寻址符间接引用运算符,作用于指针变量,表示该指针所指向的对象。

    这样就好理解上述输出结果了;同样好理解的是 p1 和 p2 分别采用声明时赋值和声明后赋值的写法差异;

    其实对于 "int *p1 = &a" 的正确理解是:(int*) p = &a,也就是说,定义了一个 int* 的变量 p,它的初值为 &a,而 int* 就是表示 int 类型的指针变量;

    但因为习惯问题和格式问题,'*' 往往还是会贴在变量名前而非类型名后。

 

1.4.3.3 NULL 指针

  如果需要让一个指针不指向任何变量也是有办法的,一般格式为:

int *p = NULL;

  NULL 为一种特殊的指针,表示指向内存地址 0,属于系统内置宏定义(即 #define NULL 0)。大多数操作系统中,地址 0 为保留地址,就是留给指针需要不指向任何变量时使用的,所以程序不允许访问地址为 0 的内存。

  还有下列两种格式都是等价的:

int *p = 0;
int *p = nullptr;

  其中后者只适用于 C++ 11。

 

1.4.3.4 void 指针

  前面说声明指针时的数据类型为所指向变量的类型,但如果未知该变量的类型,则可以使用 void 指针,比如:

1 int a = 1;
2 void *p1;
3 int *p2 = &a;
4 double *p3;
5 char (*p4)[10];
6 p1 = p2;
7 p1 = p3;
8 p1 = p4;
9 p1 = &a;

  上述都是合法的赋值;

  但不要把 void 指针理解成能指向任何类型,而应理解为指向空类型或者不确定指向的类型;

  void 指针一般用于:作为函数参数向函数传递一个类型可变的对象,或者从函数返回再显式转换为需要的类型;

  对于 L9 将 a 的地址赋值给 p1 是合法的,但 p1 只是单纯地获得了它的地址,而不能通过 *p1 来获得 a 的值,因为它并非指示 int 型变量 a 的 int 型指针。

  void 指针是在 C99 标准中加入的。

 

1.4.3.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,和 ① 一样,指向常量的指针常量也可以指向变量,但是只能通过变量自己赋值来修改值。

 

1.4.3.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。

 

1.4.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];

    并且,将 p 换成 a,即 *(a + i),也是完全等价的,但这实质上就不是在用指针了,所以效率是等同于用下标访问的;

    对于数组,指针的声明方式可以进一步简化,下面代码和上式是等价的。

int *p = a; 

  ② 指针数组与数组指针

int *p[10];

    指针数组是一个数组,其中 10 个元素全部都是指向 int 类型的指针;

int (*p)[10];

    数组指针是一个指针,它指向一个 int 类型的数组。

    前面说指针可以表示一维数组,同样地,指针数组可以表示二维数组

    关于为什么一个需要括号一个不需要,请参见 1.1 C 语言基础 中 运算符 部分。

 

1.4.3.8 指针与类

  请参见 1.5.1 类与对象

 

1.4.3.9 指针与函数

  指针与函数的关系可以分类为:

    ① 指针可以作为函数的参数,在 1.2 C 语言进阶 中的 函数 部分已经有所提及;

    ② 函数指针

      函数指针即指向函数的指针;

      和指针与数组的关系类似,函数也会被分配一段存储空间,设计函数指针就可以通过其访问所指向的函数,而不用函数名;

      函数指针的一般格式:

        类型名 (*指针变量名)(函数参数表列)

      比如 int (*p)(int, int);

      函数指针的最重要用途是可以作为函数参数传递给其他函数。

    ③ 指针变量

      指针变量即返回指针值的函数;

      和变量可以是指针变量一样,函数也可以是指针变量,在类型名和函数名之间加上 '*' 即可。

  指针数组可以作 main 函数的形参;

  一般情况下,我们主函数的书写形式为:

    int main()

  但其实它也可以有参数,例如:

    int main(int argc, char *argv[])

  argc 和 argv 都是 main 函数的形参,其中:

    > argc 是一个整型变量,argc 是 argument count 的缩写,表示参数个数;

    > argv 是一个指针数组,argv 是 argument vector 的缩写,表示参数向量;

  由于 main 函数时系统中调用的,那么其传递到形参的实参必然是由操作系统给出,通常是在执行生成的可执行文件时给出;

  命令行的一般格式为:

    命令名 参数1 参数2 ... 参数n

  命令名即包含 main 函数的可执行文件名,假设文件名为 jc.exe,命令为:

    jc Hello hhhh

  则 argc 值为 3,argv[0], argv[1], argv[2] 分别为 "jc", "Hello", "hhhh";

  所以,在主函数中,你同样可以使用这些变量;

  其实 argc 和 argv 只是习惯命名,可以任意修改。

 

1.4.3.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 语言数据类型 中的 函数 部分。

 

1.4.3.11 动态内存分配

  ① malloc() 和 free()

    指针常与堆空间的分配有关;

    所谓堆,就是一块内存区域,允许程序在运行时以指针的方式从其中申请一定数量的存储单元,用于程序数据的处理;

    不同于其他存储空间的分配是在编译时完成的,是静态的,堆内存则是动态的

    在 C 语言中,动态分配内存是通过 malloc free 两个函数来完成的,malloc() 用来申请,free() 用来释放,比如:

int *p;
p = (int*)malloc(sizeof(int));
*p = 22;
printf("%d", *p);
free(p); 

    释放内存的意义在于,如果某个变量不再需要,则可以将其所占用的内存归还,从而其他程序可以使用,而不释放,则造成内存泄漏,相当于无效地占用了内存;

    可以看到,malloc() 函数使用起来挺麻烦的,不仅要计算需求的内存大小(sizeof(...)),还要确定具体转换类型(int*);

    上面提到过,C99 标准引入了 void 指针的同时,将 malloc 等分配空间函数的基类型也修改为了 void 类型,允许不指定数据类型而分配空间;

 

    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 

  非常详细而不难懂的一篇博文,思路很清晰。

posted @ 2020-05-18 22:55  jinkun113  阅读(350)  评论(0编辑  收藏  举报