1.什么是指针
计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节不一样,例如int类型占4个字节,char类型占据1个字节。为了正确地存储这些数据,必须为每个字节编上号码,就像门牌号一样,每个字节的编号是唯一的,根据编号可以准确地找到每个字节。我们将内存中字节的编号成为地址或指针。地址从0开始依次增加,对于32位环境,程序能够使用的内存为4GB。
最小的地址为0x0000 0000,最大的地址为0xFFFF FFFF。
2.星号的不同用法
(1)表示乘法
(2)表示定义一个指针变量,以和普通变量区分开
(3)解引用,表示获取指针指向的数据,是一种间接操作
3.定义指针变量
定义指针变量和定义普通变量的唯一区别是要在变量名前加 *
格式为datatype *name 或 datatype *name = &value;
星号和变量名结合。
所谓指针就是地址,所谓指针变量就是保存了变量地址的变量。指针的大小在32位平台是4个字节,在64位平台是8个字节。不论什么类型的指针,其在相同位数的平台的字节数都是一样的。
使用指针变量是要首先明确指针变量自身的值(存储的是地址),再明确指针变量所指的实体(解引用)。
定义指针变量时,类型对指针变量起到两个作用:
1)解析存储单元的大小
接下来的示例将说明为什么指针类型决定解析内存的能力:
#include<stdio.h>
int main()
{
int a = 0x12345678;
int *ip = &a;
short *sp = (short*)&a;//类型不同,需要强制类型转换
cahr *cp = (char*)&a;//类型不同,需要强制类型转换
int ix = *ip;
short sx = *sp;
char cx = *cp;
return 0;
}
变量a的值以小端存储的方式存储在计算机中,如下图所示:
但是由于不同类型的指针解析内存的能力不同,int型指针能解析4个字节,short型指针能解析两个字节,char型指针只能解析一个字节,所以对它们三个进行解引用,ix的值是12345678,sx的值是5678,cx的值是78。
2)指针变量加一的能力
一个示例:
void fun(int *p)
{
int a = 200;
*p = 100;
p = &a;
}
int main()
{
int x = 0;
int *s = &x;
fun(s);
printf("%d %d\n",x,*s);
return 0;
}
这里s是一个指针,p也是一个指针,s将自己的值传给p,s指向x,于是p也指向x,给p解引用赋值100,于是x的值也变为100,接着执行下一条语句,p指向了a,但是这并不会带动s指向a,s扔指向x,所以对s解引用得到的值仍为100。综上,这个程序会输出2个100。
4.指针的运算
指针可以+1,可以相减,但不能相加。
指针+1会从当前地址,跳转到下一个地址。在16进制下,int型指针会+4,char型指针会+1,double型指针会+8,所以说类型决定了指针+1的能力。
看如下示例
#include<stdio.h>
int main()
{
int ar[5] = {12,23,34,45,56};
int *p = ar;//该语句相当于 int *p = &ar[0];
int x = 0;
int y = 0;
x = *p++;//该语句相当于int x = *p; p ++;
y = *p;
printf("%d %d\n",x,y);
x = ++*p;
y = *p;
printf("%d %d\n",x,y);
x = *++p;
y = *p;
printf("%d %d\n",x,y);'
return 0;
}
定义指针变量p指向数组ar的首地址,x,y初始化为0;接下来的语句对p进行解引用并赋给x,之后再将指针+1,于是x的值是12,由于指针+1,指向数组的下一个元素,此时再对p进行解引用赋给y得到的值便是23,于是第一个输出语句的值是12 23;接下来的语句先对p进行解引用然后再+1赋给x,于是x的值从23变为24,再将*p赋给y,于是y的值也是24,所以第二个输出语句输出两个24;接下来的语句先将p+1,再解引用,p指向下一个地址,此时指向34,于是赋值给x的值是34,y的值也是34,所以最后一个输出语句输出两个34。
5.指针与数组的关系
数组名被看作数组的第一个元素在内存中的首地址(仅在sizeof操作中例外,该操作给出数组所占内存的大小)。数组名在表达式中被自动转换为一个指向数组第一个元素的指针常量。
数组名是指针,非常方便,但是却丢失了数组的另一个要素:数组的大小,即数组元素的数量。编译器按数组定义时的大小分配内存,但运行时对数组的边界不加检测。这会带来无法预知的严重错误。
C提供根据数组的存储地址访问数组元素的方法若ar是数组第一个元素的地址,则 *ar是数组的第一个元素ar[0],而ar + 1 是数组第二个元素的地址, *(ar + 1)是第二个元素ar[1]本身。指针+1则地址移动一个数组元素所占字节数。
C语言的下标运算符[]是以指针为操作数的,ar[i]被编译系统解释为 *(ar + i),即表示为ar所指(固定不可变)元素向后第i个元素。无论以下标方式或指针方式存取数组元素时,系统都是转换为指针方式实现。逻辑上有两种方式,物理上只有一种方式。
以数组名访问:
#include<stdio.h>
int main()
{
const int n = 5;
int ar[n] = {1,2,3,4,5};
for(int i = 0; i < n; ++i)
{
printf("0x%08x %d %d\n",ar + i,ar[i],*(ar + i));
}
return 0;
}
运行结果如下:
以指针访问:
#include<stdio.h>
int main()
{
const int n = 5;
int ar[n] = {1,2,3,4,5};
int *p = ar;
for(int i = 0; i < n; ++i)
{
printf("0x%08x %d %d\n", p + i, p[i], *(p + i));
}
return 0;
}
运行结果如下:
可见,以数组访问和以指针访问,运行结果一样。
6.指针变量与const
const关键字修饰变量或数组,成为常变量或常数组。const也可以修饰指针变量,const与指针配合使用有两种作用,一是限制指针变量,二是限制指针变量指向的数据。
(1)限制指针变量本身,如 int * const p
限制指针变量本身的意思是,指针变量本身的值不可修改,但指针变量指向的值可以被改变。所以被const修饰的指针变量指针只能在定义时初始化,不能定义之后再赋值,例如
int * const ip;//error
int a = 10;
ip = &a;//error
//以下为正确写法
int a = 10, b = 20;
int * const ip = &a;//ok
*ip = 100;//ok
ip = &b;//error
限制指针变量指向的数据, 如 int const *ip 或 const int *ip
上面两种写法都可以,一般使用第一种,限制指针变量指向的数据的意思是指针可以指向不同的变量(指针自身的值可以修改),但是不能用指针修改指针指向的数据的值,例如
#include<stdio.h>
int main()
{
int a = 10; b = 20;
int const *ip = NULL;//ok
ip = &a;//ok
*ip = 100;//error;
ip = &b;//ok
}
(3)同时限制指针变量和指针变量指向的值,写法如下:
const int * const ip;
上面这种写法使指针变量和指针变量指向数据的值都不可修改,例如
int main()
{
int a = 10, b = 20;
const int * const ip;//error define
const int * const ip = &a;//ok
*ip = 100;//error
ip = &b;//error
}
7.用数组作为函数的形参,数组将退化为指针类型
如果想在函数中传递一个一维数组作为参数,必须以下面三种方式来声明函数形式参数,这三种声明方式的结果是一样的,因为每种方式都会转成指针。
void Print_Array(int br[], int n);//形式参数是一个未定义大小的数组
void Print_Array(int br[5], int n);//形式参数是一个已定义大小的数组
void Print_Array(int *br, int n);//形式参数是一个指针
8.指针的相减
指针相减后的结果仍然是指针类型
两个同类型指针,指向连续空间可以相减。相减后的结果是数据元素的大小。
如int型指针 - int型指针结果是整型元素的个数;
char型指针 - char型指针结果是char型数据元素的个数。
当且仅当两个同类型指针变量指向同一数组中的元素时,可以用关系运算符> == !=进行比较,比较规则是指向后面元素的指针高,指向同一元素的相等。
9.无类型指针void*
void不能定义变量,但可以定义指针变量。特别之处在于 void 指针可以指向任意类型变量的地址。
如果要把void指针赋给其他类型的指针,则需要强制类型转换。例如
char ch = 'a';
int x = 10;
double dx = 12.23;
void *vp = &ch;//空指针可以指向任意类型指针的地址
vp = &x;
vp = &dx;
int *ip = (int*)vp;?/空指针赋给其他类型的指针需要强制类型转换
double *dp = (double *)vp;
在ANSI C 标准中,不允许对void指针进行算术运算,因为既然是无类型,那么每次运算我们就不知道该操作几个字节。而在 GNU 中则允许,GNU认为void* 和 char* 一样占一个字节。
空指针的好处是void型指针可以指向任意类型变量的地址,函数中的形参为void*指针变量时,函数就可以接受任意类型变量的地址。
例如C语言string.h函数库中,
void *memcpy(void *destination, const void *source, size_t num);
void *memset(void *ptr, int value, size_t num);