C/C++ 中指针和数组表示法的区别
指针和数组无疑是 C++ 最重要和最复杂的方面之一。它们支持链表和动态内存分配,并且允许函数更改其参数的内容。
C++ 数组
数组是一组由索引访问的相同类型的元素 - 数组中元素的序号。例如:
int ival;
它将 ival
定义为 int
类型变量和指令。
int ia[10];
它设置了一个由十个 int 对象组成的数组。这些对象或数组元素中的每一个都可以使用获取索引的操作来访问。
ival = ia[2];
它将数组 ia
中索引为 2 的元素的值分配给变量 ival
。类似地
ia[7] = ival;
它将 ival
值分配给索引为 7 的元素。
数组定义由类型说明符、数组名称和大小组成。大小指定数组元素的数量(至少 1 个)并用方括号括起来。数组的大小需要在编译阶段就已经知道,因此,它必须是一个常量表达式,尽管它不一定由文字设置。
元素的编号从 0 开始,因此对于 10 个元素的数组,正确的索引范围不是 1 到 10,而是 0 到 9。下面是一个对数组的所有元素进行排序的示例。
int main() {
const int array_size = 10;
int ia[array_size];
for (int ix = 0; ix < array_size; ++ix) ia[ix] = ix;
}
定义数组时,你可以通过在大括号中列出其元素的值来显式初始化它,用逗号分隔。
const int array_size = 3;
int ia[array_size] = {0, 1, 2};
如果我们明确指定一个值列表,我们可能不会指定数组的大小:编译器本身会计算元素的数量。
C++ 指针
指针是包含另一个对象的地址并允许间接操作该对象的对象。指针通常用于处理动态创建的对象,构建相关的数据结构,例如链表和分层树,以及将大对象(数组和类对象)作为参数传递给函数。
每个指针都与某种类型的数据相关联。它们的内部表示不依赖于内部类型:指针类型的对象占用的内存大小和取值范围都相同。不同之处在于编译器如何感知可寻址对象。指向不同类型的指针可能具有相同的值,但对应类型的内存区域可能不同。
这里有些例子:
int *ip1, *ip2;
complex<double> *cp;
string *pstring;
vector<int> *pvec;
double *dp;
指针由名称前的星号表示。在通过列表定义变量时,应在每个指针之前放置一个星号(见上文:ip1 和 ip2)。在下面的例子中,lp 是一个指向 long 类型对象的指针,而 lp2 是一个 long 类型的对象。
long *lp, lp2;
在以下情况下,fp
被解释为一个浮点对象,而 fp2 是一个指向它的指针:
float fp, *fp2;
给定一个 int 类型的变量:
int ival = 1024;
以下是定义和使用指向 int pi 和 pi2 的指针的示例。
[//] pi is initialized with the null address
int *pi = 0;
[//] pi2 is initialized with the address ival
int *pi2 = &ival;
[//] correct: pi and pi2 contain the ival address
pi = pi2;
[//] pi2 contains the null address
pi2 = 0;
不能为指针分配不是地址的值。
[//] error: pi cannot take the value int
pi = ival
同样,如果定义了以下变量,则不能将值分配给一种类型的指针,该指针是另一种类型的对象的地址。
double dval;
double *ps = &dval;
那么下面给出的两个赋值表达式都会导致编译错误。
[//] compilation errors
[//] invalid assignment of data types: int* <== double*
pi = pd
pi = &dval;
不是变量 pi 不能包含对象 dval
的地址 - 不同类型的对象的地址具有相同的长度。故意禁止这种地址混合操作,因为编译器对对象的解释取决于指针的类型。
当然,在某些情况下,我们对地址本身的值感兴趣,而不是它指向的对象(假设我们想将此地址与其他地址进行比较)。为了解决这种情况,我们可以引入一个特殊的无效指针,它可以指向任何数据类型,下面的表达式是正确的:
[//] correct: void* can contain
[//] addresses of any type
void *pv = pi;
pv = pd;
void*
指向的对象的类型是未知的,我们无法操作该对象。我们对这样一个指针所能做的就是将它的值分配给另一个指针或将它与某个地址值进行比较。
要使用其地址访问对象,你需要应用取消引用操作或间接寻址,由星号 (*
) 指示。例如,
int ival = 1024;
, ival2 = 2048;
int *pi = &ival;
我们可以通过对指针 pi 应用解引用操作来读取和存储 ival 的值。
[//] indirect assignment of the ival variable to the ival2 value
*pi = ival2;
[//] value indirect use of variable value and pH value value
*pi = abs(*pi); // ival = abs(ival);
*pi = *pi + 1; // ival = ival + 1;
当我们将取地址 (&) 的操作应用于 int 类型的对象时,我们会得到 int* 类型的结果
int *pi = &ival;
如果将相同的操作应用于 int*
类型的对象(指向 int C 类型的指针),我们得到一个指向 int 类型指针的指针,即类型 int**
。int**
是包含 int 类型对象地址的对象的地址。
取消引用 ppi
,我们得到一个包含 ival
地址的 int*
对象。要获得 ival
对象本身,必须将取消引用操作应用于 PPI 两次。
int **ppi = π
int *pi2 = *ppi;
cout << "ival value\n"
<< "explicit value: " << ival << "\n"
<< "indirect addressing: " << *pi << "\n"
<< "double indirect addressing: " << **ppi << "\n"
<< end;
指针可用于算术表达式。请注意以下示例,其中两个表达式执行完全不同的操作。
int i, j, k;
int *pi = &i;
[//] i = i + 2
*pi = *pi + 2;
[//] increasing the address contained in pi by 2
pi = pi + 2;
你可以向指针添加一个整数值,也可以从中减去。向指针加 1 会增加分配给相应类型对象的内存区域大小的值。如果 char 类型占用 1 个字节,int – 4
和 double - 8
,则将 2 加到指向字符、整数和 double 的指针上,它们的值将分别增加 2、8 和 16。
这怎么解释?如果相同类型的对象一个接一个地位于内存中,那么将指针加 1 将使其指向下一个对象。因此,在处理 >array 时最常使用带指针的算术运算;在任何其他情况下,它们几乎没有道理。
以下是使用迭代器迭代数组元素时使用地址算术的典型示例:
int ia[10];
int *iter = &ia[0];
int *iter_end = &ia[10];
while (iter != iter_end) {
do_the event_ with_(*iter);