手撕指针第一章 | 声明
指针是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个字节,但他仍然只有一个地址。
- 内存中的每个位置由一个独一无二的地址标识。
- 内存中的每个位置都包含一个值。
如果你记住了一个值的存储地址,以后就可以根据这个地址取得这个值。可是通过这样的方法来访问内存位置实在是太蠢了,所以 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 能够避免语义混淆。