top of page
作家相片Lingheng Tao

C++ Programming #3 Pointers and Array

已更新:1月15日


这篇笔记主要是关于 C++ 中指针与数组的知识点整理。


指针


基本概念


Pointer (指针) 是程序语言中的重要概念。指针存储的是内存地址,它直接访问内存中的数据。重要的事情说三遍。


指针的值是地址!指针的值是地址!指针的值是地址!


指针指向的地址里面对应的值,那个才是真正的数据。形象地理解,指针是门牌号,我们用门牌号来指代里面住着的人。


声明和使用


声明某种变量的指针遵循的语法是

type * ptr;

// or type* ptr;
// or type *ptr;

以上的三种格式都是合法的,且表达完全一样的意思。形式上,type* 强调 ptr 的类型是指针,type *强调(*ptr)的类型是 type。运算符(*)在这里被重载,表达的就是该变量(ptr)是一个指针的意思。


例如,声明一个 int 变量的指针,即

double *ptr;

注意,虽然指针的值是一个地址,这个地址通常可以被当做整数来处理,但指针并不是整数型。整数型可以加减乘除,地址虽然也许可以加减,但是两个地址相乘相除则没有意义。因此,简单地将整数赋给指针的做法是没有意义的。然而,可以通过强制转换,将其变成有意义的声明。

int *pt;
// pt = 0xB8000000; // type mismatch error
pt = (int *)0xB8000000; // type match

地址运算符 &


对于一个值变量,我们可以用 & 暴露其地址。之后,就可以将该值所对应的地址赋值给指针。

int *ptr;
int value = 5;
ptr = &value;

之后,以下的值就会是一致的。

*ptr == value;
ptr == &value;

利用指针记录分配内存的位置


使用地址运算符(&)的限制在于指针必须要有一个对应的值变量。值变量在声明的时候就由程序分配内存,但是很多时候我们使用的指针并不能跟着一个值类型。在运行阶段,我们可以手动在堆上分配内存,然后用指针指向堆的位置。


在 C 中,我们常用库函数 malloc()。在 C++ 中,malloc() 也可以使用,但是更多地会使用 new 关键字。例如,

int *ptr = new int;

// C type
// int *ptr = (int *)malloc(sizeof(int));
// or if you know sizeof(int) == 4
// int *ptr = (int *)malloc(4);

这里,new 后面跟着类型 int 关键字,然后 new 就会找到一个长度正确的内存块,然后返回该内存块的地址,将这个地址作为 ptr 的值。对于 malloc 函数,则要手动设置好 int 的长度,并且做好强值类型转换,否则指针可能不会按照该值的类型来运行(稍后我们会详细讨论这个问题)。


在 C/C++ 中,一旦用 new (或malloc)请求了一个新的内存块,我们就必须在这个内存块不再使用时将其释放,否则就会出现 Memory Leak (内存泄漏)。释放的方式为 new 对应 delete,malloc 对应 free。它们的使用必须是配套的,有多少个 new 就得有多少个 delete,有多少个 malloc 就得有多少个 free。

int *ptr = new int;
//... after some codes using int
delete ptr;

注意,释放的对象是内存块,而不是指针。ptr 这个变量依然存在,不能被重新声明。可以将其指向一个新的内存块。


同时,也不要尝试释放已经被释放过的内存块,结果在 C++ 中将是不确定的。也不能用 delete 来释放声明变量所获得的内存(&运算符对应的地址)。


数组


创建与释放


静态数组


在 C++ 中,创建静态数组的方式很简单。

typeName arrayName [arraySize];

这里的 arraySize 不可以是变量,必须是一个常数或者被标记为 const 的值。因此,我们说这个数组是静态的。


静态数组的声明和初始化的示例如下,以下几种格式都可以使用。

int cards[4] = {3, 4, 5, 6};
int hands[] = {3, 4, 5, 6}; // compiler will automatically count the size of the array

float balance[100] {}; // all elements set to 0;
double earnings[4] {1, 2, 3, 4};

动态数组


在 C++ 中,如果通过上面静态数组的方式来创建数组,则在程序被编译时,将为它分配内存空间。无论程序最后是否会使用数组,数组都会在内存中,这种内存分配策略叫做 Static binding(静态联编)


与之相对的,我们也可以用 new 关键字来创建一个动态数组。动态数组参与动态联编,是在编译时加入到程序中的,程序将在运行时确认它的长度。在不需要它的时候用带方括号的 delete 将其释放。

int *parray = new int[10];
// ... after some codes using parray
delete[] parray;

这里,delete[] 中的 [] 实际上告诉程序我们应该释放整个数组,而不是仅仅只是 parray 指向的元素(数组中的第一个元素的地址)。注意,在这里我们使用指针运算符(*)来标识数组头元素的地址。


指针算数


在 C/C++ 中,指针和数组基本等价。这是由于指针算数的存在。


定义


对于 T 类型的指针 p,假设 p 所指向的地址为 X。那么我们有以下两个恒等式:

1. *(p+i) == X + sizeof(T) * i;
2. *(&Expr) == Expr;

示例,我们现在假设有下面的数组。

int A[5] = {1,2,3,4,5};

然后,以下的打印的结果我们写在 comment 里面。

cout << A;
// 0x7ffd9a04f880

cout << A[0];
// 1

cout << *A;
// 1

cout << &A[0];
// 0x7ffd9a04f880

cout << A+1;
// 0x7ffd9a04f884

cout << A[1];
// 2

cout << *(A+1);
// 2

cout << &A[1];
// 0x7ffd9a04f884

cout << &A[0] + sizeof(int) * 1;
// 0x7ffd9a04f884

这样一来,我们就搞明白了数组和指针的关系。C++将数组的名字解释为指针。或者说,将数组名解释成数组的第一个元素的地址。将 A[i] 解释成 *(A+i)。


数组地址


整个数组取地址的时候,数组名不会被解释为其地址,而是会被解释为整个数组的地址


有点绕,但是我们可以通过这个示例来理解。

short B[10];

B 是一个长度为 10 的 short 数组,假设每个 short 是 2 字节,那么 B 占据的空间就是 20 字节。


此时,

  • B+1: 将地址值(从 B 的地址值的基础上)加 2。

  • &B + 1: 将地址值 + 20。



26 次查看0 則留言

Comments


bottom of page