目录
1. 简介
指针是C语言中一个非常重要的概念,也是C语言的特色之一。使用指针可以轻松的执行某些C编程任务,而不使用指针则无法执行其他任务,例如动态分配内存。所以我们学习指针就变得很有必要。
2. 地址和指针
地址和指针是计算机中两个重要概念,在程序运行过程中,变量或者程序代码被储存在以字节为单位的存储器中。在C语言中,如果定义了一个变量,在编译时就会根据变量的类型给它分配相应大小的内存单元。例如:假设int类型变量占2个字节,则需要分配2个字节的内存单元。
计算机为了对内存单元中的数据进行操作,一般是按照“地址”存取的,也就是说对内存单元进行标识编号。假如如下有变量定义:
int x = 18, y = 24, z = 20;
因为int类型变量的存储长度占用两个字节。因此假设C编译器将他们分配到地址为1000-1001,1002-1003,1004-1005的内存单元中。如下图:
实际上就在程序中,通过变量名进行操作,如调用函数printf("%d", x),输出x的值20。而程序执行时是将变量翻译为它所在的内存地址进行操作的,上述输出操作可以描述为:将x所在的内存地址1000 ~ 1001单元的内容按照整形格式输出。一般以变量所在的内存单元的第一个字节的地址作为它的地址
,如变量x的内存地址是1000,y的内存地址是1002,z的地址是1004。变量x、y、z的内容分别为20、24、20。
要注意区分内存单元的内容和内存单元的地址。
在C程序中还有一种使用变量的方法,即通过变量的地址进行操作:使用指针访问内存和操纵地址。
假设定义一个变量P,它的内存单元的地址是2000,该单元中存放了变量x的内存单元的内容1000,如下图:
此时,取出变量P的值1000,就可以访问内存单位地址为1000变量,实现对变量x的操作。也就是说通过变量P,可以间接的访问到变量x。与直接使用变量x相比较,使用变量P访问变量x的过程实现了对变量x的间接操作。这种专门用来存放变量地址的变量称为“指针变量”,简称指针
。
指针是用来存放内存地址的变量,如果一个指针变量的值是另外一个变量的地址,则称指针变量指向某个变量。上面所说的P就是一个指针变量,它存放了变量x的地址,即指针变量P指向变量x。
2. 指针变量的定义
如果在程序中声明一个变量并使用内存单元的地址作为该变量的值,那么这个变量就是指针变量。定义指针变量的一般形式为:
类型名 *指针变量名
类型名指定指针变量所指向变量的类型,必须是有效的数据类型。如int,float,char等。指针变量名是指针变量的名称,必须是一个合法的标识符。
int i,*p;
声明变量i是int类型,变量p是指向int类型变量的指针。指针值可以是特殊的地址0,也可以是一个代表机器地址的正整数。
指针声明符*在定义指针变量时被使用,说明被定义的那个变量是指针。
指针变量用于存放变量的地址,由于不同类型的变量在内存中占用不同大小的内存单元。所以如果只知道内存地址,不能确定该地址上的对象。因此在定义指针变量时,除了指针变量名。还需要说明该指针变量所指向的内存空间上所存放的数据类型。如下:
int *p; /* 定义一个指针变量P,指向整形变量 */
char *cp; /* 定义一个指针变量cp,指向字符型变量 */
float *fp; /* 定义一个指针变量fp,指向实型变量 */
double *dp1,dp2; /* 定义两个指针变量dp1和dp2,指向双精度实型变量 */
定义多个指针变量时,每个指针变量前面都必须加上*
指针变量的类型不是指针变量本身的类型,而是指定它所指向的变量的数据类型。
定义指针以及指针赋值
int i, *p;
p = &i;
p = 0;
p = NULL;
p = (int *) 100;
p=&i;语句中的指针p被看作是指向变量i或存放变量i的内存单元的地址。也就是将指针p和变量i关联了起来。这也是指针最常用的赋值方法。指针变量p和整型变量i之间的关系如下:
p = 0;和p = NULL;语句说明了怎样把特殊值0赋值给指针p,这时指针的值为NULL。常量NULL在系统文件stdio.h中被定义,其值为0,将他赋给指针时代表空指针。
p = (int *) 100;使用强制类型转换(int *)来避免编译错误,表示p指向地址为100的int类型变量。不建议这样使用,一般不将绝对地址赋值给指针,NULL例外。
在定义指针变量时,要注意以下几点:
- 指针变量名是一个标识符,要按照C标识符的命名规则对指针变量进行命名。
- 指针变量的数据类型是它所指向的变量的类型,一般情况下一旦指针变量的类型被确定后,他只能指向同种类型的变量。
- 在定义指针变量时需要使用指针声明符 * ,但指针声明符并不是指针的组成部分。例如:定义int p;说明p是指针变量,而不是p;
3. 指针的基本运算
如果指针的值是某个变量的地址,通过指针就能间接访问那个变量,这些操作由取地址运算符&
和间接访问运算符 *
完成。此外,相同类型的指针还能进行赋值、比较和算术运算
。
3.1 取地址运算和间接访问运算
单目运算符 & 用于给出变量的地址。例如:
int *p, a = 3;
p = &a;
将整型变量a的地址赋给整型指针p,使指针p指向变量a。也就是说,用运算符&取变量a的地址,并将这个地址值作为指针p的值,使指针p指向变量a。
指针的类型和它所指向变量的类型必须相同。
在程序中,“ * ” 除了被用于定义指针变量外,还被用于访问指针所指向的变量,它也称为间接访问运算符
。例如:当p指向a时,*p和a访问同一个存储单元,*p的值就是a的值,如下图:
取地址运算和间接访问运算示例:
#include<stdio.h>
int main(){
int a = 3, *p; /* 定义整型变量a和整型指针p */
p = &a; /* 把变量a的地址赋给指针p,即p指向a */
printf("a = %d, *p = %d\n", a, *p); /* 输出变量a的值和指针p说指向变量的值 */
*p = 10; /* 对指针p所指向的变量赋值,相当于对变量a赋值 */
printf("a = %d, *p = %d\n", a, *p);
printf("请输入a的值:");
scanf("%d", &a); /* 输入a */
printf("a = %d, *p = %d\n", a, *p);
(*p)++; /* 将指针所指向的变量加1 */
printf("a = %d, *p = %d\n", a, *p);
return 0;
}
运行结果:
a=3,*p=3
a=10,*p=10
请输入a的值:5
a=5,*p=5
a=6,*p=6
3.2 赋值运算
一旦指针被定义并赋值后,就可以如同其他类型变量一样进行赋值运算。例如:
int a = 3, *p1, *p2; /* 定义整型变量指针p1和p2 */
p1 = &a; /* 使指针p1指向整型变量a */
p2 = p1;
将变量a的内存地址赋给指针p1,再将p1的值赋给指针p2,因此指针p1和p2都指向变量a。此时,*p1、*p2和a访问同一个存储单元,它们的值一样。如下:
只能将一个指针的值赋给另一个相同类型的指针。
3.3 示例程序
#include <stdio.h>
int main()
{
int *a, b = 100, *c;
printf("a value = %p, a address = %p, a point value = %d\n", a, &a, *a);
printf("b value = %d, b address = %p\n", b, &b);
printf("c value = %p, c address = %p, c point value = %d\n", c, &c, *c);
a = &b;
printf("a value = %p, a address = %p, a point value = %d\n", a, &a, *a);
c = a;
printf("c value = %p, c address = %p, c point value = %d\n", c, &c, *c);
printf("b value = %d, b address = %p\n", b, &b);
return 0;
}
运行结果:
示例中的运行结果中的十六进制所表示的地址每台机器都会不太一样,甚至每次运行都会发生变化。因为操作系统每次为系统分配的内存都不太一样。
a value = 0x7fffaf0005a0, a address = 0x7fffd2942a28, a point value = -1991643855
b value = 100, b address = 0x7fffd2942a24
c value = 0x7fffd2942b20, c address = 0x7fffd2942a30, c point value = 1
a value = 0x7fffd2942a24, a address = 0x7fffd2942a28, a point value = 100
c value = 0x7fffd2942a24, c address = 0x7fffd2942a30, c point value = 100
b value = 100, b address = 0x7fffd2942a24
示例程序中声明了一个整数类型的指针 a ,同时还声明并初始化了一个整数类型的变量 b 。
我们首先展示了此时此刻指针变量 a 中存储的值,也就是一个内存地址,这个地址为 0x7fffaf0005a0 ,同时 a 变量本身的地址为 0x7fffd2942a28 ,此时其存储的内存地址中所存储的值为 -1991643855 。这里需要说明的是, a 变量此时还没有初始化,没有赋值,里面存储的数值是随机的,所以其代表的值也是随机的,必须在赋值以后才能使用。这点和所有的变量的使用是一致的。
变量 b 的存储的数值为 100 ,其内存的地址为 0x7fffd2942a24 。
指针变量 c 中存储的数值是地址 0x7fffd2942b20 ,其本身的地址是 0x7fffd2942a30 ,里面存储的地址中存储的数值为 1 。
然后我们进行了一次赋值的操作。这里的赋值操作,就是将变量 b 的地址赋给了变量 a。
这个时候你会发现变量 a 中存储的数值变成了变量 b 的地址,而变量 a 自己的地址是没有发生变化的,而变量 a 中所存储的地址中的值也编程了变量 b 中所存储的值 100 。
指针之间的赋值就比较直接,和普通变量的赋值是一样的,只要直接赋值就可以了。
下面的表格展示了这一系列的变化:
初始状态
变量名 | 变量中数值 | 地址 | 指针地址中数值 |
---|---|---|---|
a | 0x7fffaf0005a0 | 0x7fffd2942a28 | -1991643855 |
b | 100 | 0x7fffd2942a24 | |
c | 0x7fffd2942b20 | 0x7fffd2942a30 | 1 |
赋值后
变量名 | 变量中数值 | 地址 |
---|---|---|
a | 0x7fffd2942a24 | 0x7fffd2942a28 |
b | 100 | 0x7fffd2942a24 |
c | 0x7fffd2942a24 | 0x7fffd2942a30 |
3.4 指针算术运算
C语言中的指针不仅是一个地址,它还是一个数值。因此,我们可以像是使用数值一样对指针变量执行算数运算。
有四种算术运算符可用于指针:++,–,+,-。
为了理解指针运算,假设如下:
ptr是一个指向内存单元的地址为1000的整数指针。假设是32位整数,让我们对指针执行以下算术运算。
ptr++;
经过上面的操作之后,ptr将会指向的内存地址为1004,因为每次ptr递增时,它将指向下一个整数的位置,即当前旁边的4个字节。此操作将会将指针移动到下一个内存位置,而不会影响内存位置的实际值。
如果ptr指向地址为1000的字符,则上述操作ptr将指向位置为1001,因为下一个字符在1001处(一个字符占8bit)。
自增运算
一般情况下我们可能更喜欢使用指针来代替程序中的数组名称,因为指针可以进行自增运算。
以下程序中指针自增来访问数组中每个后续元素:
#include <stdio.h>
const int MAX = 3;
int main () {
int var[] = {10, 100, 200};
int i, *ptr;
/* let us have array address in pointer */
ptr = var;
for ( i = 0; i < MAX; i++) {
printf("Address of var[%d] = %x\n", i, ptr );
printf("Value of var[%d] = %d\n", i, *ptr );
/* move to the next location */
ptr++;
}
return 0;
}
编译并执行上述代码时,会产生以下结果:
Address of var[0] = bf882b30
Value of var[0] = 10
Address of var[1] = bf882b34
Value of var[1] = 100
Address of var[2] = bf882b38
Value of var[2] = 200
特殊说明:
++,–,*,&运算符的优先级是一样的。
*p++ : 等同于 *p; p+=1;
*++p : 等同于 p+=1;*p;
(*p)++ : 使用了()强制将*与p结合,只能先计算*p,然后对*p整体的值++;
++(p) : 先p取值,再前置++
自减运算
同样的考虑也适用于自减运算,它通过其数据类型的字节数减少其值,如下所示:
#include <stdio.h>
const int MAX = 3;
int main () {
int var[] = {10, 100, 200};
int i, *ptr;
/* let us have array address in pointer */
ptr = &var[MAX-1];
for ( i = MAX; i > 0; i--) {
printf("Address of var[%d] = %x\n", i-1, ptr );
printf("Value of var[%d] = %d\n", i-1, *ptr );
/* move to the previous location */
ptr--;
}
return 0;
}
编译并执行上述代码时,会产生以下结果:
Address of var[2] = bfedbcd8
Value of var[2] = 200
Address of var[1] = bfedbcd4
Value of var[1] = 100
Address of var[0] = bfedbcd0
Value of var[0] = 10
指针比较
可以使用关系运算符来比较指针。例如==,< 和 >。
如果 p1 和 p2 指针是彼此相关的变量,例如同一个数组的元素,那么 p1 和 p2 可以有意义地进行比较。
下面的程序修改了前面的例子,只要指针变量指向的地址小于或等于数组最后一个元素的地址,即 &var[MAX - 1],就增加变量指针:
#include <stdio.h>
const int MAX = 3;
int main () {
int var[] = {10, 100, 200};
int i, *ptr;
/* let us have address of the first element in pointer */
ptr = var;
i = 0;
while ( ptr <= &var[MAX - 1] ) {
printf("Address of var[%d] = %x\n", i, ptr );
printf("Value of var[%d] = %d\n", i, *ptr );
/* point to the next location */
ptr++;
i++;
}
return 0;
}
编译并执行上述代码时,会产生以下结果
Address of var[0] = bfdbcb20
Value of var[0] = 10
Address of var[1] = bfdbcb24
Value of var[1] = 100
Address of var[2] = bfdbcb28
Value of var[2] = 200
4. 指针变量的初始化
指针变量在定义后需要先赋值再引用。在定义指针变量时,可以同时对它赋初值。例如:
int a;
int *p1 = &a; /* 在定义指针p1的同时给其赋值,使指针p1指向变量a */
int *p2 = p1; /* 在定义指针p2的同时对其赋值,使p2和p1的值相同,都指向变量a */
在进行指针初始化的时候需要注意一下几点:
- 在指针变量定义或者初始化时变量名前面的“ * ”只表示该变量是个指针变量,它既不是乘法运算又不是间接访问符。
- 把一个变量的地址作为初始化值赋给指针变量时,该变量必须在此之间已经定义。因为变量只有在定义后才被分配存储单元,它的地址才能赋给指针变量。
- 可以用初始化了的指针变量给另一个指针变量作初始化值。
- 不能用数值作为指针变量的初值,但可以将一个指针帮你初始化为一个空指针。例如:int *p = 1000; 是不对的,而int *p = 0; 是将指针变量初始化为空指针。这里0是ASCII字符NULL的值。
- 指针变量定义时的数据类型和它所指向的目标变量的数据类型必须一致,因为不同的数据类型所占用的存储单元的字节数不同。
5. 指针数组
在我们理解指针数据的概念之前,让我们先考虑以下示例,使用3个整数的数组。
#include <stdio.h>
const int MAX = 3;
int main () {
int var[] = {10, 100, 200};
int i;
for (i = 0; i < MAX; i++) {
printf("Value of var[%d] = %d\n", i, var[i] );
}
return 0;
}
编译并执行上述代码时,会产生以下结果
Value of var[0] = 10
Value of var[1] = 100
Value of var[2] = 200
另外还可能存在我们想要维护一个数组的情况,该数组可以存储指向int或者char或者其他任何可用数据类型的指针。
以下是指向整数的指针数组的声明:
int *ptr[MAX];
它将ptr声明为 MAX 整数指针数组。因此,ptr 中的每个元素都包含一个指向 int 值的指针。以下示例使用三个整数,它们存储在指针数组中,如下所示 :
#include <stdio.h>
const int MAX = 3;
int main () {
int var[] = {10, 100, 200};
int i, *ptr[MAX];
for ( i = 0; i < MAX; i++) {
ptr[i] = &var[i]; /* assign the address of integer. */
}
for ( i = 0; i < MAX; i++) {
printf("Value of var[%d] = %d\n", i, *ptr[i] );
}
return 0;
}
编译并执行上述代码时,会产生以下结果
Value of var[0] = 10
Value of var[1] = 100
Value of var[2] = 200
您还可以使用指向字符的指针数组来存储字符串列表,如下所示
#include <stdio.h>
const int MAX = 4;
int main () {
char *names[] = {
"Zara Ali",
"Hina Ali",
"Nuha Ali",
"Sara Ali"
};
int i = 0;
for ( i = 0; i < MAX; i++) {
printf("Value of names[%d] = %s\n", i, names[i] );
}
return 0;
}
编译并执行上述代码时,会产生以下结果
Value of names[0] = Zara Ali
Value of names[1] = Hina Ali
Value of names[2] = Nuha Ali
Value of names[3] = Sara Ali
6. 指向指针的指针
指向指针的指针是多重间接寻址或指针链的一种形式。通常,指针包含变量的地址。当我们定义一个指向指针的指针时,第一个指针包含第二个指针的地址,第二个指针指向包含实际值的位置,如下所示。
作为指向指针的指针的变量必须这样声明。这是通过在其名称前面放置一个额外的“*”来完成的。例如,以下声明声明了一个指向 int 类型指针的指针:
int **var;
当目标值被指向指针的指针间接指向时,访问该值需要应用星号运算符两次,如下面的示例所示
#include <stdio.h>
int main () {
int var;
int *ptr;
int **pptr;
var = 3000;
/* take the address of var */
ptr = &var;
/* take the address of ptr using address of operator & */
pptr = &ptr;
/* take the value using pptr */
printf("Value of var = %d\n", var );
printf("Value available at *ptr = %d\n", *ptr );
printf("Value available at **pptr = %d\n", **pptr);
return 0;
}
编译并执行上述代码时,会产生以下结果:
Value of var = 3000
Value available at *ptr = 3000
Value available at **pptr = 3000
7. 将指针传递给C语言中函数
C 编程允许将指针传递给函数。为此,只需将函数参数声明为指针类型。
以下是一个简单的示例,我们将无符号long类型的指针传递给函数并更改函数内部的值。
#include <stdio.h>
#include <time.h>
void getSeconds(unsigned long *par);
int main () {
unsigned long sec;
getSeconds( &sec );
/* print the actual value */
printf("Number of seconds: %ld\n", sec );
return 0;
}
void getSeconds(unsigned long *par) {
/* get the current number of seconds */
*par = time( NULL );
return;
}
编译并执行上述代码时,会产生以下结果:
Number of seconds :1294450468
该函数可以接受一个指针,也可以接受一个数组,如下例所示
#include <stdio.h>
/* function declaration */
double getAverage(int *arr, int size);
int main () {
/* an int array with 5 elements */
int balance[5] = {1000, 2, 3, 17, 50};
double avg;
/* pass pointer to the array as an argument */
avg = getAverage( balance, 5 ) ;
/* output the returned value */
printf("Average value is: %f\n", avg );
return 0;
}
double getAverage(int *arr, int size) {
int i, sum = 0;
double avg;
for (i = 0; i < size; ++i) {
sum += arr[i];
}
avg = (double)sum / size;
return avg;
}
当上面的代码一起编译并执行时,它会产生以下结果
Average value is: 214.40000
8. 从C语言的函数中返回指针
C 也允许从函数返回指针。为此,您必须声明一个返回指针的函数,如下例所示:
int * myFunction() {
.
.
.
}
要记住的第二点是,在函数外部返回局部变量的地址不是一个好主意,因此您必须将局部变量定义为静态变量。
现在,考虑以下函数,它将生成 10 个随机数并使用表示指针的数组名称返回它们,即第一个数组元素的地址。
#include <stdio.h>
#include <time.h>
/* function to generate and return random numbers. */
int * getRandom( ) {
static int r[10];
int i;
/* set the seed */
srand( (unsigned)time( NULL ) );
for ( i = 0; i < 10; ++i) {
r[i] = rand();
printf("%d\n", r[i] );
}
return r;
}
/* main function to call above defined function */
int main () {
/* a pointer to an int */
int *p;
int i;
p = getRandom();
for ( i = 0; i < 10; i++ ) {
printf("*(p + [%d]) : %d\n", i, *(p + i) );
}
return 0;
}
当上面的代码一起编译并执行时,它会产生以下结果:
1523198053
1187214107
1108300978
430494959
1421301276
930971084
123250484
106932140
1604461820
149169022
*(p + [0]) : 1523198053
*(p + [1]) : 1187214107
*(p + [2]) : 1108300978
*(p + [3]) : 430494959
*(p + [4]) : 1421301276
*(p + [5]) : 930971084
*(p + [6]) : 123250484
*(p + [7]) : 106932140
*(p + [8]) : 1604461820
*(p + [9]) : 149169022