指针

Posted 小屋庭院

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了指针相关的知识,希望对你有一定的参考价值。

指针类型

int* a;//a是指向整形的指针;
int* a[5];//一维指针数组(这里存放着5个指向整形的指针),a指向第一个元素的地址,a+1指向第二个......(a[5]是一个指针数组);
int (*a)[5];//指向数组(这里每个一维数组含5个元素)的指针,a是第一个一维数组的首元素地址,a+1指向第二个一维数组的首元素地址......(a是数组指针);
int (*a)();//a是指向函数的指针(函数指针);
int* a();//函数的返回类型是int *,a只是一个函数名;

记忆

指针声明的设计本意是解一个方程 [1],int ....,让 .... 的类型是 int。也就是 *ptr 的类型是 int。从而反推出 ptr 是 int 指针。

我自己还是写成 int* ptr 的。但是要注意

int* ptr_1, ptr_2

ptr_1是一个指针,而ptr_2是一个整数。

在很多现代C++编程规范中,建议星号*与int结合,而不是与后面的变量结合。

这样做的理由是更清晰,无论初学者还是老手都不容易看错。

定义和使用指针


指针的定义

在 C 和 C++ 中定义指针变量是很简单的,和定义普通的变量基本是一样的。所有的区别,仅在于我们需要在变量名称前使用解引用符号 * 来标记这是一个指针。

int *ip1, *ip2;
double d, *dp;

在上述定义中,我们看到,ip1, ip2, dp 是三个指针——因为在它们之前用 * 号标记处他们是指针;而 d 是一个普通的 double 类型变量。同时,我们注意到,ip1ip2 在定义之时,就确定了他们是指向 int 类型的变量。这意味着,被 ip1ip2 指向的内存,在使用 ip1ip2 进行访问的时候,将被当做是 int 类型的对象来对待。同理,dp 指向的内存,在使用 dp 进行访问的时候,将被当做是 double 类型的对象来对待。

回顾一下,我们在第一节中提到,内存空间中的内容有两个关键要素:地址和类型。在上述定义过程中,我们通过类型与解引用符号 * 相结合,已经确定了类型。如果要正确使用指针,我们还应该让指针记录一个地址。

获取对象的地址

上面说到,我们应该在定义指针之后,记录一个地址。在 C 和 C++ 中,我们需要使用取地址符号 & 来获取对象的地址。

int val = 42;
int *p  = &val;

(绝大多数情况下,)指针的类型和对象的类型需要严格匹配。例如,你不能用一个指向 int 类型的指针变量,保存一个 double 类型的对象的地址。

double dval = 0.0;
double *pd1 = &dval;
double *pd2 = pd1;

int *pi1 = &dval;
int *pi2 = pd1;

访问指针指向的对象

在下例中,指针 p 记录了变量 val 的地址。因此,我们可以通过解引用指针 p 来访问变量 val

int val = 42;
int *p  = &val;
cout << *p << endl;

*p = 360;
cout << *p << endl;
cout << val << endl;

空指针和空类型的指针

空指针是不指向任何对象的指针,在实际编程中,通常使用空指针作为指针变量有效性的判断标准。

C 语言和老版本 C++ 的空指针字面值是 NULL,它定义在 stdlib 当中;新版本的 C++ 使用 nullptr 作为空指针字面值。C++ 还支持用字面值常量 0 初始化指针变量,被这样初始化的指针变量会是一个空指针。

int *p1 = NULL;
int *p2 = nullptr;
int *p3 = 0;

if (nullptr == p1) 
    ;

空类型的指针,指的是形如 void *pv 的指针。这是一类特殊的指针;这里的空类型,不是说没有类型,而是说空类型的指针,可以用于存储任意类型对象的地址。

double pi = 3.14;
void *pv = π
double *pd = π
pd = pv;
pv = pd;
pd = (double *)pv;
pd = reinterpret_cast<double *>(pv);

让我们回顾一下指针的两个要素:地址和类型。由于空类型的指针可以接受任意类型对象的地址,所以,当编译器拿到一个空类型的指针的时候,它无法知道应该按照何种方式解释和使用指针中记录地址中的内容。因此,空类型指针能够做的事情非常有限:做指针之间的比较、作为函数的输入或输出、赋值给另外一个空类型指针。

指针变量的基本操作

赋值: 可以把地址赋给指针。例如,用数组名、带地址运算符(& )的变量名、另一个指针进行赋值。在该例中,把urn 数组的首地址赋给了ptr1 ,该地址的编号恰好是0x7fff5fbff8d0 。变量ptr2 获得数组urn 的第3个元素(urn[2] )的地址。注意,地址应该和指针类型兼容。也就是说,不能把double 类型的地址赋给指向int 的指针,至少要避免不明智的类型转换。C99/C11已经强制不允许这样做。

解引用运算符给出指针指向地址上储存的值。因此,ptr1 的初值是100 ,该值储存在编号为0x7fff5fbff8d0 的地址上。

取址: 和所有变量一样,指针变量也有自己的地址和值。对指针而言,& 运算符给出指针本身的地址。本例中,ptr1 储存在内存编号为0x7fff5fbff8c8 的地址上,该存储单元储存的内容是0x7fff5fbff8d0 ,即urn 的地址。因此&ptr1 是指向ptr1 的指针,而ptr1 是指向utn[0] 的指针。

指针与整数相加: 可以使用+ 运算符把指针与整数相加,或整数与指针相加。无论哪种情况,整数都会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。因此ptr1 + 4 与&urn[4] 等价。如果相加的结果超出了初始指针指向的数组范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。

递增指针: 递增指向数组元素的指针可以让该指针移动至数组的下一个元素。因此,ptr1++ 相当于把ptr1 的值加上4 (我们的系统中int 为4 字节),ptr1 指向urn[1] (见图10.4,该图中使用了简化的地址)。现在ptr1 的值是0x7fff5fbff8d4 (数组的下一个元素的地址),*ptr 的值为200 (即urn[1] 的值)。注意,ptr1 本身的地址仍是0x7fff5fbff8c8 。毕竟,变量不会因为值发生变化就移动位置。

指针减去一个整数: 可以使用- 运算符从一个指针中减去一个整数。指针必须是第1个运算对象,整数是第2个运算对象。该整数将乘以指针指向类型的大小(以字节为单位),然后用初始地址减去乘积。所以ptr3 - 2 与&urn[2] 等价,因为ptr3 指向的是&urn[4] 。如果相减的结果超出了初始指针所指向数组的范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。

递减指针: 当然,除了递增指针还可以递减指针。在本例中,递减ptr3 使其指向数组的第2个元素而不是第3个元素。前缀或后缀的递增和递减运算符都可以使用。注意,在重置ptr1 和ptr2 前,它们都指向相同的元素urn[1] 。

指针求差: 可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。例如,程序清单10.13的输出中,ptr2 - ptr1 得2 ,意思是这两个指针所指向的两个元素相隔两个int ,而不是2 字节。只要两个指针都指向相同的数组(或者其中一个指针指向数组后面的第1个地址),C都能保证相减运算有效。如果指向两个不同数组的指针进行求差运算可能会得出一个值,或者导致运行时错误。

比较: 使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。比较的依据是两个指针所指向的地址的前后。

C 和 C++ 中的指针,是一种特殊的复合类型。指针变量中存放着目标对象的内存地址,而与指针相复合的类型,则说明了相应内存区域中的内容具有哪些属性,以及能做什么事情。也就是说,在内存空间某块区域中的内容,原本可以是不可解读的;但是,如果有一个描述这块内存区域的指针存在,我们就能找到它(地址的作用),并且合理地使用它(类型的作用)。因此,我们说:指针是对内存区域的抽象

理解指针的定义


再探变量声明

在 C 和 C++ 中,变量的声明包括一个基本数据类型(或者类类型),以及一组声明符。定义指针使用的解引用符号 * 是类型修饰符,它是声明符的一部分。因此,在下列语句中,int 是基本数据类型,*p 是声明符,* 是类型修饰符作为声明符的一部分存在。

在同一个变量定义语句中,基本数据类型只能有一个,但是可以有多个形式相同或不同的声明符。这也就是说,同一个语句可以定义出不同类型的变量。

int *pi = nullptr,
    val = 1024;

理解稍微复杂的指针定义

因为指针本身也是变量,所以它当然也是存储在虚存空间里的。因此,我们当然也可以定义一个指向这一指针的指针。比如:

int val  = 1024;
int *p   = &val;
int **pp = &p;

我们需要仔细理解一下 pp 的定义。理解这类稍微复杂的定义语句,一个基本的办法就是:从最靠近变量名字的地方开始,一层一层剖析变量的类型。我们来看

  • 距离 pp 最近的是一个解引用符 *,这预示着 pp 是一个指针,它指向 int * 类型的变量;
  • 再来看 int *,距离 *pp 最近的,依然是一个解引用符,这意味着 *pp 也是一个指针,它指向 int 类型的变量;
  • 因此 pp 是一个指向指向 int 类型变量的指针的指针

你可以仔细斟酌一下这段内容。

const 与指针


常量的值在生存期内不允许改变。这一特性经常是有用的:可以定义一个常量,然后在多个地方使用;当认为这个常量的值不合适的时候,修改它的定义,即可在所有使用到它的地方生效(而无需依次手工修改);此外,还可以防止程序意外修改这个值。定义常量,只需要在基本类型前,加上 const 关键字即可;它是 constant 的缩写,意为常量。

const double pi = 3.141592653580793;

const 与指针牵扯到一起,就有些复杂了。至少有以下几种情况:

int val = 0;
const int cnst = 1;

int *pi = &val;

pi = &cnst;

const int *pci = &cnst;

pci = &val;

int *const cpi = &val;

int fake = 2;
cpi = &fake;

const int *const cpci = &val;


cpci = &fake;
cpci = &cnst;

因为变量可以是常量,而指针本身也可以是常量。因此在变量和指针两个维度,都可以选择是否为常量。这样一来,就像上面代码展示的那样,当 const 与指针牵扯在一起的时候,就有 4 中可能性。为了区分这两个维度,我们引入顶层 const底层 const 的概念:

  • 顶层 const:指针本身是常量。此时,指针在定义初始化之外,不能被赋值修改。称指针为指针常量。
  • 底层 const:指针指向的变量是常量。此时,不能通过解引用指针的方式,修改变量的值。称指针为常量的指针。

登峰造极的 (*(void(*)())0)();


这恐怕是一个会令所有 C/C++ 程序员战栗不已的函数调用语句。因此,在解释这个语句之前,我愿意先给出它的含义,安抚读者战栗的心灵。它表示:访问内存地址 0,将它作为一个参数列表和返回类型均为空的函数,并执行函数调用。(这是一个特殊场景下的函数调用,不用纠结为什么会调用 0 位置上的函数)

类型定义与 C 风格的类型强制转换符

C 风格的类型强制转换符应该不是个稀罕玩意儿。比如 (double)a 就能将变量 a 强制转换为 double 类型。在这个简单的例子里,我们希望能够找到一些朴素的规律,破解这一登峰造极而又令人战栗的函数调用语句。

同样以 double 类型及相关指针类型为例,我们首先看下面的代码:

double a;
double *b;

(double) c;
(double *) d;

我们不难发现,类型转换和对应类型的变量定义,有着千丝万缕的联系:首先去掉变量定义语句末尾的分号,然后去掉变量定义语句中的变量名,最后给剩余的部分加上括号——一个 C 风格的类型强制转换符,就得到了。

破解谜题

我们知道 void(*pfunc)(); 定义了一个函数指针 pfunc,它指向的函数参数列表为空、返回值类型也为空。因此,(void(*)()) 就是一个 C 风格的类型转换符。

因此,(void(*)())0 会将 0 转换成一个函数指针,然后交给 * 解引用,最后传入参数(空的参数列表 ()),执行函数调用。

在 C++ 中,这个函数调用应该写作

(*reinterpret_cast<void(*)()>(0))();

显而易见,这个写法,相较 C 风格的类型强制转换符,要清晰明朗得多。因此,请不要再吐槽 C++ 风格的强制转换是「语法盐」了。

参考 liam.page C/C++ 里指针声明为什么通常不写成 int* ptr 而通常写成 int *ptr ? - 知乎 (zhihu.com)

以上是关于指针的主要内容,如果未能解决你的问题,请参考以下文章

06指针.

指针的指针&指向指针数组的指针

指针的指针,指针的引用(不是二维指针)

指针,数组指针,指针数组,函数指针解析

指针,数组指针,指针数组,函数指针解析

这些指针的含义,指针指针,函数指针和数组指针