指针

指针是一种保存变量地址的变量

在c中指针的使用非常广泛,原因之一是指针常常是表达某个计算的唯一途径

另一个原因是同其他方法相比较,使用指针往往可以生成更高效更紧凑的代码

指针和数组之间的关系十分密切,使用数组的场景下基本上都会涉及指针

指针和goto语句一样,会导致程序难以理解

如果使用者不够细心的话,指针很容易就会指向错误的地方,但如果谨慎地使用指针,就可以利用其写出简单,清晰的程序

ANSI C的一个最重要的变化在于,它明确规定了操纵指针的规则,而事实上这些规则早已被很多优秀的程序设计人员和编译器所采纳

此外,ANSI C使用类型void*(指向void的指针)代替char*作为通用指针的类型

void*可以指向任意类型的数据,可用任意类型指针对void*赋值,在部分编译环境下也可用void*给任何类型赋值

但void*无法进行算数运算,因为不知道需要操作几字节的数据
(注意在c++中void*可以接受任何类型,反过来不可以,也就是一个有类型的指针不能指向一个void*所指向的地址)
(void几乎只有说明和限制程序的作用,从来没有人会定义一个void变量,如:void a; 编译器会报错,并且即使不会出错也没有任何实际意义) 

通常的机器都有一系列连续编号或编址的存储单元,这些存储单元可以单个进行操作,也可以连续成组的方式进行操作

一般情况下机器的一个字节可以存放一个char类型,2个相邻字节存储单元可存储一个short类型,4个相邻字节存储单元可存储一个long类型的数据

指针是能够存放一个地址的一组存储单元(通常是2或4个字节,保证能将32位机的最大地址存放进来)
案例:输出变量的值和地址
#include<stdio.h>
int main()
{
    int a=0,b=0;
    char c='A';
    printf("a is %d, &a is %p\n",a,&a);      输出结果: a is 0, &a is 0x7ffeef1df60c
    printf("b is %d, &b is %p\n",b,&b);               b is 0, &b is 0x7ffeef1df608     a和b的值相等,但地址不同
    printf("c is %c, &c is %p\n",c,&c);               c is A, &c is 0x7ffeef1df607
}
    指针变量是一种特殊的变量,变量中存放的是一个变量或常量的地址,例:
    int value=150;char ch='M';
    int *pv=&value;char *pc=&ch;
    由于pv,pc中存放的分别是value和ch的地址,故称pv和pc为指针变量,简称为指针(pointer)
    指针指向的对象称为目标或目标变量
    指针存放地址,访问速度更快,访问数组更加灵活,增加了访问内存空间的方法
    以上面pv和pc的例子为例。value,ch分别为整型和字符型变量,占4字节和1字节的空间,内存中地址按字节编号,每个字节存储单元均对应一个地址
    假设value所分配的地址空间为1000H-1003H,值为150,ch所分配空间为1050H
    但是通过指针访问数据时需按照指针类型决定访问空间的大小
    因此&value和&ch值分别为1000H和1050H(通常只包括首地址),而指针变量pv和pc分别储存&pv和&pc的值,假设为11A0H-11A3H和11A4H-11A7H
    计算机可以通过&value,&ch和pv,pc两种方式查询value和ch的地址(直接寻址,间接寻址)

指针声明

指针声明格式:数据类型 *指针变量名
例:int *ptr;char *name;float *ptf;       分别定义为指向整型,字符型和浮点型数据的指针;
    int a,bb,*pt;                        只要数据类型相同也可以与普通变量一起定义

指针变量同普通变量一样可以定义为外部,内部和静态等不同类型

指针初始化

使用指针前,必须给指针赋以指向目标变量的地址值

同其他类型变量一样,指针也可以初始化

通常对指针有意义的初始化值只能是0或者表示地址的表达式,表达式所代表的地址必须是在此前已定义的具有适当类型的数据的地址

c保证0永远不是有效的数据地址,因此返回值0可用来表示发生了异常事件

指针和整数之间不能相互转换,但0是唯一的例外:常量0可以赋值给指针,指针也可以和常量0进行比较

程序中经常用符号常量NULL代替常量0,这样便于说明常量0是一个指针的特殊值,NULL定义在标准头文件<stddef.h>中

“野指针”问题应尽量避免;没有初始化值需要给指针初始化NULL变成空指针,不能使用没有初始化的指针
初始化格式:指针名=地址
一元运算符&可用于取一个对象的地址,&只能应用于内存中的对象,即变量和数组元素,不能作用于表达式、常量或register类型的变量

一元运算符*是间接寻址或间接引用运算符,当其作用于指针时,将访问指针所指的对象

在声明指针时,“*”作为说明符,如:int *p相当于表明表达式*p是int类型,对函数参数的声明也采用这种方式,如:double atof(char*);

在使用指针时,“*”是运算符,用于访问目标变量的值(间接引用)

因此,变量名和指针所指空间是是等效的,因此对变量进行的操作与对记录了该变量地址的指针进行操作是一样的,例:
int x=1,y=2,z[10],*ip;    ip是指向int类型的指针
ip=&x,y=*ip+1;            ip现在指向x,y的值现在为2

一元运算符*和&的优先级比二元高,因此*ip=*ip+1等同于*ip+=1,++*ip,(*ip)++,
其中(*ip)++的圆括号是必需的,否则的话使用*ip++是先取指针指向空间的值再将指针进行自增运算,等价于*(ip+1)
原因是由于包括*在内的一元运算符都是从右向左的结合顺序

*ip=*ip+10;               x的值为10,只要ip指向x,则x可以出现的任何上下文都可以使用*ip代替
ip=&z[0];                 ip现在指向z[0]
指针也是变量,所以在程序中可以直接使用,而不是只能通过间接引用(*)的方法使用
相同类型的指针间可以相互赋值,如:int *pa=&a,*pb;pb=pa; 将pa的值拷贝到pb当中,使指针pb也指向pa指向的对象
指针变量的类型和目标变量的类型应一致(一个例外情况是void*可以存放指向任意类型的指针),例:
int c=10;char *pc=&c;
此时编译器会发出警告;可以考虑将地址转换成对应的指针变量类型pc=(char*)&c
但在这种做法下pc仍然是char*,所以还是无法正确访问整型变量c的地址空间对c进行操作,例:
char c;int *pc=&c;*pc=12891;       此时char类型的c接收不了这么大的值,发生越界错误
指针使用原则:指针使用非常容易出错,需要格外注意
  • 永远清楚每个指针指向了哪里,指针必须指向一块有意义的内存

  • 永远清楚每个指针指向的内容是什么

  • 永远不要使用未经初始化的指针

地址算数运算

指针<->地址 指针运算<->地址运算

c中的地址运算方法是一致且有规律的,将指针、数组和地址的算术运算集成到一起是该语言特性中的一大优点

指针的各类运算都与其所指向的基类型有关,移动字节数以其基类型占有的字节数为基本单位

指针的算术运算具有一致性,所有的指针运算都会自动考虑它所指向对象的长度

有效的指针运算包括:

相同类型指针之间的赋值运算,指针同整型之间的加减法运算,指向相同数组中元素的两指针间减法或比较运算,将指针赋值为0或与0进行比较的运算

其他所有运算都是非法的,如:

两指针间的加法、乘法、除法、移位或屏蔽运算,指针同float或double类型之间加法运算

以及不经强制类型转换而直接将指向一个类型的指针值赋给指向另一种类型的指针(两指针之一是void*的情况除外)

赋值运算 pa=&a;pa=pb;

取地址运算"&"与取内容运算"*"互为逆运算,例:int x,*ptr=&x;cout<<(*(&x)==1);输出结果为1

指针间关系运算:>, <, ==, !=, >=, <=

若指针p和q指向同一个数组成员,p指向的数组元素位置在q指向的数组元素位置之前,则p<q的值为真

任何指针与0进行相等或不等的比较运算都有意义,但指向不同数组的元素之间的指针进行算数或比较运算通常没有意义

(有一个特殊情况下该操作可能是有意义的:有时指针的算术运算会使用数组最后一个元素的下一个地址)

(c的定义保证数组末尾之后的第一个元素的指针运算可以正确执行)

比较运算必须在同类型的指针之间进行

指针间减法运算

同理指针的减法运算也是有意义的,如果p、q指向相同数组中的元素且p<q,则q-p+1就是位于p和q指向元素之间(包括p、q)的元素个数

指针的减法运算也必须在相同类型指针之间进行,例:
size_t strlen(char*s)
{
    char*p=s;
    if(!*s)return 0;
    while(*++s!='\0');
    return s-p;
}
(由于字符串中的字符数有可能超过int类型所能表示的最大范围

因此可考虑使用头文件<stddef.h>中定义类型ptrdiff_t,该类型足以表示两个指针之间的带符号差值

不过这里使用size_t以和标准库中的版本匹配,size_t是由sizeof返回的无符号整型(typedef unsigned size_t))

指针与整数的加减运算,常见于指针与数组结合进行的运算(因为数组中的元素地址是连续的),例:

int a[5],*pa=a;p++;     此时指针p自增代表指针从数组的首地址开始每次向下增加一个数组元素数据类型的空间

指针与整数n相加(减)代表:初始地址+(-)n*sizeof(指向目标变量类型的大小)

由于一元运算符'+'和'-'既可以作为前缀运算符也可以作为后缀运算符,因此二者与*有多种组合使用方式,不过用法不多见
如:*--p在读取指针p指向的对象前对p先执行自减操作
   *p++=val表示将val压入栈,val=*--p表示将栈顶元素弹出到val中,这两种是进栈和出栈的标准用法
    (压栈时指针+1后再将存储到指针原先的位置,出栈时把当前指向的值弹出然后指针-1,栈顶指针始终在栈顶元素的下一个位置)

指针运算示例程序:依次输入两个数据,先输出小的,后输出大的

#include<stdio.h>
int main()                  在不修改原变量值的情况下实现递增输出的目的
{
    int a,b;
    int *pa=&a,*pb=&b,*tmp;
    scanf("%d %d",*pa,*pb);
    if(*pa>*pb)
    {
       tmp=pa;
       pa=pb;
       pb=tmp;
    }
    printf("min is %d max is %d",*pa,*pb);
}

字符指针

关于指针和数组的规则同样也适用于字符指针和字符数组,例:
    char buffer1[]={'h','e','l','l','o','\0'};
    char buffer2[]="hello";
    char *pcr1=buffer1,*pcr2=buffer2;
    printf("%s %s\n",pcr1,pcr2);        按字符数组输出时数组如果末尾找不到\0的话会一直向下非法读取其他空间的数据
    printf("%c %c\n",*pcr1,*pcr2);*pcr和*pcr2取的是首元素的值,此时输出结果为h h
    printf("%d",*pcr1-*pcr2);           输出0
    void strcpy(char*s,char*t)          strcpy函数,将指针t指向的字符串复制到指针s所指向的位置
    {
       while(*s++=*t++);
    }
(虽然这种写法初看不易理解,但这种表示方法很有好处,c程序中经常用到这种写法,应当充分掌握

标准库<string.h>中提供的函数strcpy除拷贝外还会将目标字符串作为函数值返回)

printf如果按照%s输出的话默认会向下移动指针到存储\0的空间

也就是说对于字符串而言,以字符串形式输出字符数组中某一元素的地址,会默认该地址为字符串指针,输出原字符数组的字符串中剩下的部分,例:
int main()
{
    char string[]="programming";
    char *p=string;
    p+=5;
    printf("%s",p);                  输出结果:amming
}
字符串区别与普通数组的一大特点是字符串可以直接作为常量为指针赋值
如:char *par="hello";
注意:使用字符串常量对字符数组和字符指针的定义是不同的,例:
char amessage[]="now is the time";
char *pmessage="now is the time";
此时,amessage是一个长度等于初始化字符串加空字符'\0'的一维数组,数组中的字符元素可修改,但amessage指向的内存地址是始终不变的

另一方面pmessage是一个字符指针,初值指向字符串常量,指向的地址可以被修改,但由于其指向的是一个常量,因此修改其值是非法且没有定义的

不过用字符数组存储字符串常量,再用指针修改数组中字符是可行的,不过这种方法无法进行字符串整体的修改
如:char buffer[]="hello",*par=buffer;*par='c'; 

二重指针

指针也是变量,但指针变量不能存储指针的值
如果在一个指针变量中存放指向另一个变量的指针地址,则称该指针为指向指针的指针,即二重指针,例:
int v=120;int *q=&v;int **p=&q;
此时 v=*q=*(*p)
二重指针只能用来存储指针的地址,不能存放普通变量的地址,否则编译器会报错
对于指针数组名,由于其本身就是一个二重指针常量(指向指针数组首元素指针的地址),因此可以给二重指针赋值,例:
char *pc={"abc","def","hig"};char **pc;ppc=pc;
二重指针示例程序:用二重指针输出数组元素
#include<stdio.h>
#include<string.h>
int main()
{
    const int n=3;
    int array[n]={1,2,3},*parray[n]={&array[0],&array[1],&array[2]};
    int **pa,i;
    pa=parray;
    for(i=0;i<n;i++)
    {
        printf("%d ",**pa);
        pa++;
    }
}