Post

手撕指针第一章 | 声明

手撕指针第一章 | 声明

指针是C语言为什么如此流行的一个重要原因,他可以有效实现诸如 tree 和 list 这类高级数据结构。用C语言可以比使用其他语言编写出更为紧凑和有效的程序。

谈起指针,我们要从变量开始说起,相信大家对变量都不陌生。变量的值存储于计算机的内存中,每个变量都占据一个特定的位置。每个位置都由地址唯一确定并引用,就像一条街道上的房子由他们的门牌号码来标识一样。指针只是地址的另一个名字罢了,它就是一个值为内存地址的变量。

我们可以把内存看作一条长街上的一排房屋,每座房子都可以容纳数据,并通过一个房号来标识。每个房子中的数据和它的地址是独立且明显不同的,即使它们都是数据。

1. 内存和地址

计算机的内存以亿万计的位 (bit) 组成,每个位可以容纳值 0 和 1。由于一个位所能表示的值的范围太有限,因此,单独的位用处不大,通常将许多位合成一组作为一个单位,这样就可以存储范围较大的值。

每个字节包含 8 个位,可以存储无符号值 0~255 ,或有符号值 -128~127。每个字节通过地址来标识。为了存储更大的值,我们把两个或更多个字节合在一起作为一个更大的内存单位。

\[1 byte = 8 bit\]

说一句题外话:我们常说的32位计算机一次最多能处理 32 位,即 4 个字节的数据;64 位计算机能处理最多 64 位,即 8 个字节的数据。

许多机器以字为单位存储整数,每个字一般由 2 字节或者 4 字节组成。注意,尽管1个字包含了4个字节,但他仍然只有一个地址。

  1. 内存中的每个位置由一个独一无二的地址标识。
  2. 内存中的每个位置都包含一个值。

如果你记住了一个值的存储地址,以后就可以根据这个地址取得这个值。可是通过这样的方法来访问内存位置实在是太蠢了,所以 C 语言提供的特性之一就是通过名字而不是地址来访问内存位置,这个名字就是我们所说的变量。

名字与内存之间的关联并不是硬件来提供的,而是由编译器为我们实现。虽然所有这些变量给了我们一种更方便的方法记住地址,但硬件仍然通过地址访问内存位置。

2. 指针声明

1
int *a;

这条语句表示表达式 *a 产生的结果类型是 int 。知道了 * 操作符执行的是间接访问操作以后,我们可以推断出 a 肯定是一个指向 int 类型的指针。

C在本质上是个自由形式的语言,这很容易诱使你把星号写在靠近类型的一侧,如 int* p; 。这个声明与前一个声明具有相同的意思,而且看上去更为清楚,p被声明为类型为 int*的指针,但这并不是一个好技巧。

1
int* b, c, d;

人们很自然的认为这条语句把所有3个变量声明为指向整型的指针,但事实是星号只是表达式 *b 的一部分,b 是一个指针,其余两个变量只是普通的整型。

1
2
/* 如果要声明3个指针,正确语句如下 */
int *b, *c, *d;

对一个指针的一个基本操作是解引用(dereferencing),即引用指针所指的对象,也称为间接取值。

1
2
3
char c = 'a';
char *p = &c; // p 存放着 c 的地址,& 是取地址符
char c2 = *p; // * 是解引用运算符。c2 == ‘a'

符号 * 在用作类型名的后缀时表示「指向」的含义。如果我们想表示指向数组的指针或指向函数的指针,需要使用稍微复杂一些的形式。

1
2
3
4
5
int* pi; // 指向 int 的指针
char** pcc; // 指向字符指针的指针
int* ap[15]; // ap 是一个数组,包含 15 个指向 int 的指针
int (*fp) (char*); // 指向函数的指针,该函数接受一个 char* 实参,返回一个 int
int* f(char*); // 该函数接受一个 char* 实参,返回一个指向 int 的指针

3. void*

在某些偏向函数底层的代码中,我们偶尔需要在不知道对象确切类型的情况下,仅通过对象在内存中的地址存储或者传递对象。此时,我们会用到 void*, 意为「指向未知类型对象的指针」。

void* 最主要的用途是当我们无法假定对象的类型时,向函数传递指向该对象的指针。他还用于从函数返回未知类型的对象。要想使用这样的对象,必须先进行显式类型转换。

函数指针和指向类成员的指针不能被赋给 void*

1
2
3
4
5
6
7
8
9
10
11
12
void f(int* pi)
{
  void* pv = pi; // ok 从 int* 到 void* 的隐式类型转换
  *pv; // 错误 不允许解引用到 void*
  ++pv; // 错误 不允许对 void* 执行递增操作,对象尺寸未知
  
  int* pi2 = static_cast<int*>(pv); // 显式转换回 int*
  
  double* pd1 = pv; // 错误
  double* pd2 = pi; // 错误
  double* pd3 = static_cast<double*>(pv); // 不安全
}

一般情况下,如果某个指针已经被转换成(强制类型转换)指向一种与实际所指对象类型完全不同的新类型,则使用转换后的指针式不安全的行为。

用到 void* 指针的函数通常位于系统的最底层,这些函数的作用大多是操作硬件资源。

在系统较上层的代码中很少用到 void*,一旦出现就要认真核实是不是存在设计上的错误。当用于优化的目的时,void* 能隐藏在类型安全的接口中。

1
void* my_alloc(size_t n); // 从特定的堆上分配 n 个字节的内存空间

4. nullptr

字面值常量 nullptr 表示空指针,即不指向任何对象的指针。nullptr 只有一个,可以用于指向任意指针类型,C++ 并没有为美中指针类型各设计一个空指针。 在 nullptr 被引入之前,人们使用 0 表示空指针。

1
2
3
int* pi = nullptr;
int i = nullptr; // 错误 不是指针累心
int* x = 0; // x 的值是空指针

在原来的代码中,很多人习惯于定义一个宏 NULL 来表示空指针。然而,在不同的具体实现中,NULL 的定义有所差别。NULL 有可能是 0,也有可能是 0L。 在 C 语言中,NULL 通常是 (void*)0,这种用法在 C++ 中是非法的。

1
int* p = NULL; // 错误。不能把 void* 赋给 int*

使用 nullptr 好处很多,首先他的可读性更强。其次,当一组重载函数既可以接受指针,也可以接受整数时,用 nullptr 能够避免语义混淆。

This post is licensed under CC BY 4.0 by the author.