在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。
指针的声明
指针是一个变量,其值是另一个变量的地址,即,内存位置的直接地址。就像其他变量或常量一样,必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:type* var-name;
如下例所示:
#include<stdio.h>
int main() {
// 声明一个 int 类型的指针
int* ip;
// 声明一个 long 类型的指针
long *lp;
// 声明一个 double 类型的指针
double* dp;
// 声明一个 float 类型的指针
float* fp;
printf("size of int pointer is %d.\n", sizeof(ip));
printf("size of long pointer is %d.\n", sizeof(lp));
printf("size of double pointer is %d.\n", sizeof(dp));
printf("size of float pointer is %d.\n", sizeof(fp));
return 0;
}
执行结果:
size of int pointer is 4.
size of long pointer is 4.
size of double pointer is 4.
size of float pointer is 4.
在声明的时候,*
无论是偏向数据类型的关键字还是偏向变量名称,都是被认为是合法的。也就是说:int *ip;
和 int* ip;
都是正确的声明方式。但是当多个变量在同一个声明语句时候,需要使用 *ip
的形式。例如:
int *a, *b; // 声明两个 int 类型的指针变量,分别是 a 和 b
int a, *b, c; // 声明一个 int 类型的指针变量 b,变量 a 和 c 都是普通的 int 变量。
因为指针变量存储的值是一个地址值,所以,无论什么类型的指针,都不会影响其本身需要占用内存的空间。由于指针变量接受的是地址值,所以,在给指针变量赋值的时候需使用到取址符 &
,如下例所示:
#include<stdio.h>
int main() {
int number = 10;
int* ip = &number;
printf("number is %d\n", number);
printf("&number is %d\n", &number);
printf("ip is %d\n", ip);
printf("&ip is %d\n", &ip);
printf("*ip is %d\n", *ip);
return 0;
}
执行结果:
number is 10
&number is 2293564
ip is 2293564
&ip is 2293560
*ip is 10
既然指针也是变量,那根据上面得到的结果对照到表格中来看:
变量名称 | 变量的类型 | 变量的值 | 变量的地址 | 为变量赋值 | |
---|---|---|---|---|---|
int number = 10; | number | int | 10 | 2293564 | number = 10 |
int* ip = &number; | ip | int* | 2293564 | 2293560 | ip = &number |
指针是一种特殊的变量,其存储的数据不是一个可以直接被人识别的数字,或者文本,而是某个其他变量的内存地址值。
指针的简单应用
借助指针,可以对该内存地址的数据进行读写。如如下例:
#include<stdio.h>
int main() {
int a = 10;
int* ip = &a;
printf("a is %d,\t &a is %d\n", a, &a);
printf("ip is %d \t &ip is %d \t *ip is %d\n", ip, &ip, *ip);
// 修改指针所指向的内存地址(2293564)中的值
*ip = 100;
printf("a is %d,\t &a is %d\n", a, &a);
printf("ip is %d \t &ip is %d \t *ip is %d\n", ip, &ip, *ip);
return 0;
}
执行结果:
a is 10, &a is 2293564
ip is 2293564 &ip is 2293560 *ip is 10
a is 100, &a is 2293564
ip is 2293564 &ip is 2293560 *ip is 100
这样不需要变量 a 就能实现修改变量 a 所存储的值。
在执行过程中,也可以修改指针的存储的地址值为其他的变量。如下所示:
#include<stdio.h>
int main() {
int a = 10, b = 20;
int* ip = &a;
printf("a is %d,\t &a is %d\n", a, &a);
printf("b is %d,\t &b is %d\n", b, &b);
printf("ip is %d \t &ip is %d \t *ip is %d\n", ip, &ip, *ip);
*ip *= 2;
printf("a is %d,\t &a is %d\n", a, &a);
printf("b is %d,\t &b is %d\n", b, &b);
printf("ip is %d \t &ip is %d \t *ip is %d\n", ip, &ip, *ip);
ip = &b;
*ip *= 3;
printf("a is %d,\t &a is %d\n", a, &a);
printf("b is %d,\t &b is %d\n", b, &b);
printf("ip is %d \t &ip is %d \t *ip is %d\n", ip, &ip, *ip);
return 0;
}
执行结果:
a is 10, &a is 2293564
b is 20, &b is 2293560
ip is 2293564 &ip is 2293556 *ip is 10
a is 20, &a is 2293564
b is 20, &b is 2293560
ip is 2293564 &ip is 2293556 *ip is 20
a is 20, &a is 2293564
b is 60, &b is 2293560
ip is 2293560 &ip is 2293556 *ip is 60
这里的指针 ip 先指向的是变量 a 的地址,在对该地址的数据进行累乘操作后,指向了变量 b 的地址,又对变量 b 的地址中的值进行了累乘操作。最终 a 的值被乘以 2,b 的值被乘以3。
NULL 指针
如果在声明一个指针的时候,没有为其赋予确切的地址值,那指针可能会指向一个未知的地址。如下例所示:
#include<stdio.h>
int main() {
int *a, *b;
printf("a=%d, *a=%d, b=%d, *b=%d\n", a, *a, b, *b);
return 0;
}
执行结果:
a=2293540, *a=4200720, b=2147344384, *b=0
在没有为指针变量 a 和 b 赋予地址值的时候,既然还能有值,并且能读取到该地址里的值。因为一旦指针存储了地址值后,可以对该地址进行修改操作,可能会出现一些不该出现的现象,比如某个地方的变量的值因为这个指针的原因被修改了之类的。
如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值作为初始值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。
因为在 C 中,把任何非零和非空的值假定为 true,把零或 null 假定为 false,所以,当一个指针是空指针的时候,是可以被判断的。这也有利于后面更加顺利地操作指针。代码如下:
#include<stdio.h>
int main() {
int a = 10;
int* ip = NULL;
printf("a is %d,\t &a is %d\n", a, &a);
if(ip) {
printf("ip is not null pointer.\n");
} else {
printf("ip is null pointer.\n");
ip = &a;
}
*ip *= 2;
printf("ip is %d \t &ip is %d \t *ip is %d\n", ip, &ip, *ip);
return 0;
}
执行结果:
a is 10, &a is 2293564
ip is null pointer.
ip is 2293564 &ip is 2293560 *ip is 20
当然,在该程序中 ip 自然是为空指针,但当 ip 作为一个函数参数的时候,对指针判空的处理就很重要了,因为不知道外部传递的指针到底是否是空的。
指针的算术运算
指针是一种存储其他变量地址值的特殊变量,同时也能进行简单的算数运算和逻辑运算。运用自运算遍历数组一种常见的指针应用。
#include<stdio.h>
int main() {
int i, size = 7;
int nums[7] = {101, 202, 303, 404, 505, 606, 707};
int *p = NULL;
p = nums;
for(i = 0; i < size; i++) {
printf("nums[%d] = %d, p==%d, *p=%d\n", i, nums[i], p, *p);
p++;
}
return 0;
}
执行结果:
nums[0] = 101, p==2293528, *p=101
nums[1] = 202, p==2293532, *p=202
nums[2] = 303, p==2293536, *p=303
nums[3] = 404, p==2293540, *p=404
nums[4] = 505, p==2293544, *p=505
nums[5] = 606, p==2293548, *p=606
nums[6] = 707, p==2293552, *p=707
因为数组变量在内存中是以连续的内存空间来存储数据的,故而,p = nums;
等价于 p = &nums[0];
也就是说,将数组 nums 的地址值赋予给指针,等价于将数组中第一个元素的地址值赋值给指针。
从输出的结果上来看也会发现,连续的每个元素的地址相差 4 位,这个值正是在开篇使用 sizeof 量出来的指针的大小是一致的。
那既然 p = nums;
等价于 p = &nums[0];
那将数组中最后一个元素的地址值赋予给指针做自减运算输出会怎么样。代码如下:
#include<stdio.h>
int main() {
int i, size = 7;
int nums[7] = {101, 202, 303, 404, 505, 606, 707};
int *p = NULL;
p = &nums[size - 1];
for(i = size - 1; i > -1; i--, p--) {
printf("nums[%d] = %d, p==%d, *p=%d\n", i, nums[i], p, *p);
}
return 0;
}
执行结果:
nums[6] = 707, p==2293552, *p=707
nums[5] = 606, p==2293548, *p=606
nums[4] = 505, p==2293544, *p=505
nums[3] = 404, p==2293540, *p=404
nums[2] = 303, p==2293536, *p=303
nums[1] = 202, p==2293532, *p=202
nums[0] = 101, p==2293528, *p=101
因为数组中每个元素的地址是连续的,当指针指向第一个元素的时候,只要指针指向的地址没有超过最后一位,就能进行循环取值。这里就需要对指针中存储的地址值进行比较,示例代码如下:
#include<stdio.h>
int main() {
int size = 7;
int nums[7] = {101, 202, 303, 404, 505, 606, 707};
int *p = nums; // 等价于 &nums[0]
while(p <= &nums[size - 1]) {
printf("%d ", *p);
p++;
}
return 0;
}
执行结果:
101 202 303 404 505 606 707
指针数组
指针数组往往是用于存储一系列相同类型的指针的集合。其声明的一般形式为:type* var-name[size];
,例如:
int* ps[3];
在理解指针的时候,可以类比变量,同样的,理解指针数组可以类比数组。
数组名称 | 数组中每个元素的类型 | 获取下标为0的元素的值 | 获取下标为0的元素的地址值 | 为下标为0的元素赋值 | |
---|---|---|---|---|---|
int nums[3]; | nums | int | nums[0] | &nums[0] | nums[0] = 10; |
int* ps[3]; | ps | int* | ps[0] | &ps[0] | ps[0] = &nums[0] |
这样一来,很容易理解指针数组了,它就是一系列指针的集合。他们不一定是连续的,就像数组中的数据不一定是连续的数字的道理一样。这里使用一个案例来对指针数组和普通数组进行对比:
#include<stdio.h>
int main() {
int i, a = 101, b = 202, c = 303, size = 3;
int nums[3];
nums[0] = a;
nums[1] = b;
nums[2] = c;
int* ps[3];
ps[0] = &a;
ps[1] = &b;
ps[2] = &c;
// 输出各个变量的值
for(i = 0; i < size; i++) {
printf("nums[%d]=%d, &nums[%d]=%d\n", i, nums[i], i, &nums[i]);
}
printf("\na=%d, &a=%d, b=%d, &b=%d, c=%d, &c=%d\n\n", a, &a, b, &b, c, &c);
for(i = 0; i < size; i++) {
printf("ps[%d]=%d, *ps[%d]=%d, &ps[%d]=%d\n", i, ps[i], i, *ps[i], i, &ps[i]);
}
// 修改指针指向的地址中的值
for(i = 0; i < size; i++) {
*ps[i] *= 10;
}
// 再次输出各个变量的值
printf("\n------------------\n");
for(i = 0; i < size; i++) {
printf("nums[%d]=%d, &nums[%d]=%d\n", i, nums[i], i, &nums[i]);
}
printf("\na=%d, &a=%d, b=%d, &b=%d, c=%d, &c=%d\n\n", a, &a, b, &b, c, &c);
for(i = 0; i < size; i++) {
printf("ps[%d]=%d, *ps[%d]=%d, &ps[%d]=%d\n", i, ps[i], i, *ps[i], i, &ps[i]);
}
return 0;
}
执行结果:
nums[0]=101, &nums[0]=2293536
nums[1]=202, &nums[1]=2293540
nums[2]=303, &nums[2]=2293544
a=101, &a=2293556, b=202, &b=2293552, c=303, &c=2293548
ps[0]=2293556, *ps[0]=101, &ps[0]=2293524
ps[1]=2293552, *ps[1]=202, &ps[1]=2293528
ps[2]=2293548, *ps[2]=303, &ps[2]=2293532
------------------
nums[0]=101, &nums[0]=2293536
nums[1]=202, &nums[1]=2293540
nums[2]=303, &nums[2]=2293544
a=1010, &a=2293556, b=2020, &b=2293552, c=3030, &c=2293548
ps[0]=2293556, *ps[0]=1010, &ps[0]=2293524
ps[1]=2293552, *ps[1]=2020, &ps[1]=2293528
ps[2]=2293548, *ps[2]=3030, &ps[2]=2293532
从执行结果来看,指针数组中存储的三个指针,修改这三个指针对应地址的值,会影响变量 a, b, c,但是不影响数组 nums。因为在给数组赋值的时候,是将变量 a, b, c 的值赋予了数组 nums,也就是 传值。给指针数组赋值的时候,是将变量 a, b, c 的地址值赋予了 ps,也就是 传址。这样就更加好理解指针数组了,数组是一系列相同数据类型的数据的集合,而指针数组是一系列相同数据类型的指针的集合。
数组指针
在 C 中,数组到底是什么?
现象一:
在上文中的 指针的算术运算 段落中的内容可以知道:p = nums;
等价于 p = &nums[0];
。
分析:
赋值运算符 =
两边的数据类型在一致的时候才能将右值赋予左边的变量,如果不是因为数据类型发生了隐式转换,那就是符号两边的数据类型本就是一致的。这是不是能说明数组变量 nums 就是一个指针?如果是这样,那这个指针存储的值难道是数组中第一个元素的地址?如果是这样,那就能解释为什么在给指针 p 赋值的时候,nums 不需要 &
作为前缀了。
现象二:
回顾在之前的变量学习中,可以知道,变量的赋值可以是这样的:
int a = 10; int b = 20; b = a;
最终变量 b 的值变成了 20,那同样的代码,数组能这样使用吗?比如:
int a[] = {1, 2}; int b[] = {3, 4}; b = a;
程序在编译的时候会抛出一个错误信息:
error: assignment to expression with array type
b = a;
^
分析:
为什么这里会提示数组类型的变量在声明之后不允许使用赋值表达式?这个现象和常量很相似,常量在定义完成之后,也是不允许为其赋值。这是不是能说明数组是一个常量?
解释:
在 C 中,数组变量就是一个特殊常量指针,也称之为数组指针。它有如下特点:
- 数组变量本身表达的就是地址。所以
nums == &nums[0]
- 运算符
[]
可以对数组做运算,也可以对指针做运算。所以p[0]
等价于nums[0]
- 运算符
*
可以对指针做运算,也可以对数组做运算。所以*nums
是被允许的 - 数组变量是 const 的指针,所以不允许被赋值。也就是说
int nums[]
等价于int * const nums
实例:
#include<stdio.h>
#define SIZE 6
int main() {
int nums[SIZE] = {101, 202, 303, 404, 505, 606};
int *p = nums;
// nums 和 p 都能使用运算符 *
printf("*p=%d, *nums=%d\n", *p, *nums);
printf(" p=%p, nums=%p\n", p, nums);
printf("&p=%p, &nums=%p\n", &p, &nums);
// nums 和 p 都能使用逻辑运算符
printf("p <= &nums[1] is %s\n", p <= &nums[1] ? "true" : "false");
printf("nums < &nums[1] is %s\n", nums < &nums[1] ? "true" : "false");
// nums 和 p 都能使用下标来操作元素
for(int i = 0; i < SIZE; i++) {
printf("nums[%d]=%d, &nums[%d]=%p, p[%d]=%d, &p[%d]=%p\n", i, nums[i], i, &nums[i], i, p[i], i, &p[i]);
}
// nums 是常量指针,故而不能做自运算
while(p < &nums[SIZE - 1]) {
p++;
// error: lvalue required as increment operand
// nums++;
printf("p=%p, *p=%d, nums=%p, *nums=%d\n", p, *p, nums, *nums);
}
return 0;
}
执行结果:
*p=101, *nums=101
p=0022FF24, nums=0022FF24
&p=0022FF20, &nums=0022FF24
p <= &nums[1] is true
nums < &nums[1] is true
nums[0]=101, &nums[0]=0022FF24, p[0]=101, &p[0]=0022FF24
nums[1]=202, &nums[1]=0022FF28, p[1]=202, &p[1]=0022FF28
nums[2]=303, &nums[2]=0022FF2C, p[2]=303, &p[2]=0022FF2C
nums[3]=404, &nums[3]=0022FF30, p[3]=404, &p[3]=0022FF30
nums[4]=505, &nums[4]=0022FF34, p[4]=505, &p[4]=0022FF34
nums[5]=606, &nums[5]=0022FF38, p[5]=606, &p[5]=0022FF38
p=0022FF28, *p=202, nums=0022FF24, *nums=101
p=0022FF2C, *p=303, nums=0022FF24, *nums=101
p=0022FF30, *p=404, nums=0022FF24, *nums=101
p=0022FF34, *p=505, nums=0022FF24, *nums=101
p=0022FF38, *p=606, nums=0022FF24, *nums=101
指向指针的指针
我们知道指针存储的是其他变量的的地址值,而指针本身也是一个变量,那一个指针指向的变量正好也是一个指针变量呢?这种情况被称之为指向指针的指针。
指向指针的指针在声明的时候必须比被指向的指针变量多一个星号,如下所示:
#include<stdio.h>
int main() {
int num = 10;
int *p = #
int **ip = &p;
int ***ipp = &ip;
printf("num=%d, &num=%d\n", num, &num);
printf("p=%d, *p=%d, &p=%d\n", p, *p, &p);
printf("ip=%d, *ip=%d, &ip=%d\n", ip, *ip, &ip);
printf("ipp=%d, *ipp=%d, &ipp=%d\n", ipp, *ipp, &ipp);
return 0;
}
执行结果:
num=10, &num=2293564
p=2293564, *p=10, &p=2293560
ip=2293560, *ip=2293564, &ip=2293556
ipp=2293556, *ipp=2293560, &ipp=2293552
如果上例中的指针 ipp 指向指针 p 会在编译的时候抛出一个警告:
warning: initialization of 'int ***' from incompatible pointer type
'int **' [-Wincompatible-pointer-types]
int ***ipp = &p;
^
但是还是会编译通过,执行结果是:
num=10, &num=2293564
p=2293564, *p=10, &p=2293560
ip=2293560, *ip=2293564, &ip=2293556
ipp=2293560, *ipp=2293564, &ipp=2293552
指针 ipp 最终还是成功指向了指针 p,虽然这样做是可行的,但不建议这样去写。
如果按照正常的方式去编写,这种指向指针的指针能最多能写多少个星号呢?目前貌似没有找到相关资料来解释这个问题。就下面的案例来看,能写到至少 6 颗星。
#include<stdio.h>
int main() {
int num = 10;
int *p = #
int **ip = &p;
int ***ipp = &ip;
int ****ippp = &ipp;
int *****ipppp = &ippp;
int ******ippppp = &ipppp;
printf("num=%d, &num=%d\n", num, &num);
printf("p=%d, *p=%d, &p=%d\n", p, *p, &p);
printf("ip=%d, *ip=%d, &ip=%d\n", ip, *ip, &ip);
printf("ipp=%d, *ipp=%d, &ipp=%d\n", ipp, *ipp, &ipp);
printf("ippp=%d, *ippp=%d, &ippp=%d\n", ippp, *ippp, &ippp);
printf("ipppp=%d, *ipppp=%d, &ipppp=%d\n", ipppp, *ipppp, &ipppp);
printf("ippppp=%d, *ippppp=%d, &ippppp=%d\n", ippppp, *ippppp, &ippppp);
return 0;
}
执行结果:
num=10, &num=2293564
p=2293564, *p=10, &p=2293560
ip=2293560, *ip=2293564, &ip=2293556
ipp=2293556, *ipp=2293560, &ipp=2293552
ippp=2293552, *ippp=2293556, &ippp=2293548
ipppp=2293548, *ipppp=2293552, &ipppp=2293544
ippppp=2293544, *ippppp=2293548, &ippppp=2293540