一个数据对象的内存位置有两个重要信息:
1. 数据对象的首地址。
2. 数据对象占用存储空间大小。
指针类型通过下面的方式,存储这两个重要信息。
1. 指针类型通过值来保存目标数据对象的首地址。
2. 类型本身标记目标数据对象的空间大小。
指针类型的值存储的是内存地址。内存地址是从0开始,依次加1的整型数据。那么,我们自然会产生疑问,既然指针类型的值是整数,它是否能进行运算呢?
我们让指针变量从地址100开始加减,看看能不能成功。如果可以,计算后的结果是什么。
#include
intmain()
{
char* pc;
short* ps;
int* pn;
long* pl;
longlong*pll;
float* pf;
double* pd;
// 赋值为100
pc =100;
ps =100;
pn =100;
pl =100;
pll =100;
pf =100;
pd =100;
// 均加1
pc = pc +1;
ps = ps +1;
pn = pn +1;
pl = pl +1;
pll = pll +1;
pf = pf +1;
pd = pd +1;
// 查看值
printf("pc=%u\n", pc);
printf("ps=%u\n", ps);
printf("pn=%u\n", pn);
printf("pl=%u\n", pl);
printf("pll=%u\n", pll);
printf("pf=%u\n", pf);
printf("pd=%u\n", pd);
return0;
}
编译后,我们发现编译器提示我们,不能将int类型转换为指针类型。
这个错误应该是意料之中的,指针类型可以记录两种信息,一个是首地址,一个是目标数据空间大小。
而整型数据的值可以被当作首地址,但是目标数据空间大小却无法表示。赋值操作符两边提供的信息不 统一,那么肯定无法成功赋值。
既然无法直接赋值,我们将整型转换为对应的指针类型后,再进行赋值
int main()
{
char* pc;
short* ps;
int* pn;
long* pl;
longlong*pll;
float* pf;
double* pd;
// 现将整型转换为对应的指针类型再赋值
pc = (char*)100;
ps = (short*)100;
pn = (int*)100;
pl = (long*)100;
pll = (longlong*)100;
pf = (float*)100;
pd = (double*)100;
pc = pc +1;
ps = ps +1;
pn = pn +1;
pl = pl +1;
pll = pll +1;
pf = pf +1;
pd = pd +1;
printf("pc=%u\n", pc);
printf("ps=%u\n", ps);
printf("pn=%u\n", pn);
printf("pl=%u\n", pl);
printf("pll=%u\n", pll);
printf("pf=%u\n", pf);
printf("pd=%u\n", pd);
return 0;
}
现在编译可以通过了,让我们仔细观察结果。所有的指针内保存的首地址一开始均为100,加1后,现在变成了不同的值。与初始值相比,分别移动了:1、2、4、4、8、4、8。
如果稍微数值敏感一点,你应该可以看出这些数值分别是对应的目标数据对象的空间大小。sizeof(char) =1
sizeof(short) =2
sizeof(int) =4
sizeof(long) =4
sizeof(longlong) =8
sizeof(float) =4
sizeof(double) =8
指针类型加1后,将首地址向后移动了 sizeof(目标数据对象) 字节。我们再让指针加2试试看?
pc = pc +2;
ps = ps +2;
pn = pn +2;
pl = pl +2;
pll = pll +2;
pf = pf +2;
pd = pd +2;
与初始值相比,分别移动了:2、4、8、8、16、8、16。
指针类型加2后,将首地址向后移动了两个 sizeof(目标数据对象) 字节。
pc = pc -1;
ps = ps -1;
pn = pn -1;
pl = pl -1;
pll = pll -1;
pf = pf -1;
pd = pd -1;
指针类型与整型也可以进行减法运算。指针类型减1后,将首地址向前移动了 sizeof(目标数据对象)字节。
sizeof(目标数据对象) 被称作步长。
指针类型加 n 后。其首地址向后移动 n * 步长字节。
指针类型减 n 后。其首地址向前移动 n * 步长字节。
#include
int main()
{
int n1;
int n2;
int n3;
int n4;
int*pn1 =&n1;
int*pn2 =&n2;
int*pn3 =&n3;
int*pn4 =&n4;
printf("pn1 = %u\n", pn1);
printf("pn2 = %u\n", pn2);
printf("pn3 = %u\n", pn3);
printf("pn4 = %u\n", pn4);
return0;
}
n1、n2、n3、n4之间的首地址刚好相差sizeof(int) 。那么我们是否可以通过指针与整型加减来访问它们呢?
int main()
{
int n1;
int n2;
int n3;
int n4;
printf("&n1=%d\n", &n1);
printf("&n2=%d\n", &n2);
printf("&n3=%d\n", &n3);
printf("&n4=%d\n", &n4);
putchar('\n');
// 获取n1的首地址
int* p =&n1;
printf("p = %d\n", p);
printf("p - 1 = %d\n", p -1);
printf("p - 2 = %d\n", p -2);
printf("p - 3 = %d\n", p -3);
putchar('\n');
// 给n1到n4赋值
*p =111; // p指向n1
*(p -1) =222; // p - 1后前后移动4字节,现在指向n2 *(p -2) =333; // p - 2后前后移动8字节,现在指向n3 *(p -3) =444; // p - 3后前后移动12字节,现在指向n4
printf("n1=%d\n", n1);
printf("n2=%d\n", n2);
printf("n3=%d\n", n3);
printf("n4=%d\n", n4);
return0;
}
n1的首地址为10484280。
n2的首地址为10484276。
n3的首地址为10484272。
n4的首地址为10484268。
指针 p 指向 n1 。
p 的值为 n1 的首地址,即10484280。
p - 1 后,向前移动 sizeof(int) ,即10484276。 p - 2 后,向前移动2个 sizeof(int) ,即10484272。 p - 3 后,向前移动3个 sizeof(int) ,即10484268。
根据首地址我们发现:
p 指向 n1 。
p - 1 指向 n2 。
p - 2 指向 n3 。
p - 3 指向 n4 。
接着,我们使用取值运算符*,将指针指向的目标数据对象进行修改。 *p = 111 ,使得 p 指向的数据对象改为111。
*(p - 1) = 222 ,使得 p - 1 指向的数据对象改为222。 *(p - 2) = 333 ,使得 p - 2 指向的数据对象改为333。 *(p - 3) = 444 ,使得 p - 3 指向的数据对象改为444。
n1到n4被修改为了111到444。
注意,表达式 p - 1 必须先被括号包裹,再使用取值运算符*。这是因为取值运算符*的优先级高于算术运算符。
我们需要先让首地址移动,再进行取值操作。
若不使用括号, *p 会先被取值,之后值再被加1。
不使用括号:
*p 的值为111, *p - 1 的结果为110。
使用括号:
(p - 1) 使得首地址移动到n2, *(p - 1) 得到结果为222。
请格外注意,函数内声明的变量在内存中不一定是首尾相接的。受到内存对齐的影响,它们有可能不连 续。这个例子仅仅是为了让读者理解指针与整型加减运算,请勿在实际使用中使用这种写法。
另外,数组可以保证各元素在内存中首尾相接,各元素连续分布在内存上。所以,C语言中可以使用指 针与整型加减运算访问和修改数组元素。
这一次,为了严格保证数据对象在内存中一定连续分布,我们使用数组来举例。
#include
int main()
{
int arr[10];
printf("&arr[0] = %d\n", &arr[0]);
printf("&arr[5] = %d\n", &arr[5]);
printf("&arr[5] - &arr[0] = %d\n", &arr[5] -&arr[0]);
return0;
}
第一个元素 arr[0] 的首地址为20970856。
第六个元素 arr[5] 的首地址为20970876。
&arr[5] - &arr[0] 进行减法运算后,结果居然是5。
类比于指针类型与整型加减,这里肯定也受到了步长影响。
步长为目标类型所占空间大小,步长为 sizeof(int) 。
arr[5] 的首地址减 arr[0] 的首地址为20970876 - 20970856 = 20。两首地址差值除以步长刚好为5。这里的5意义为:第一个元素与第六个元素之间相隔了5个元素。
规律:
sizeof(目标数据对象) 被称作步长。
指针类型与指针类型相减后,其结果为两首地址差值除以步长。
上面我们介绍了两种有指针类型参数的运算:
1. 指针类型与整型加减。
2. 同类型的指针相减。
它们的运算结果都在内存上拥有实际的意义。
还有另外几种运算:
1. 指针类型与整型进行乘除运算。
2. 同类型的指针相加。
3. 同类型的指针乘除。
这些运算结果都没有实际意义,在C语言中无法通过编译。