1.什么是指针
众所周知,计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如int 占用4个字节,char占用1个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。我们将内存中字节的编号称为地址(Address)或指针(Pointer)。地址从О开始依次增加,对于32位环境,程序能够使用的内存为4GB。最小的地址为0x0000 0000,最大的地址为OXFFFF FFFF。
C/C++中*号运算符的意义;*号运算符所在的环境不一样,*号的含意也不一样。
int main()
{
int a = 10, b = 20;
int c = a * b; //这里的*为乘法的意思
int *p; //声明指针
p = &a;
*p = 100; //解引用
return O;
}
所谓指针,也就是内存的地址;所谓指针变量,也就是保存了内存地址的变量。指针的大小在32位平台是4个字节,在 64位平台是8个字节。
2.定义指针变量:
定义指针变量与定义普通变量非常类似,不过要在变量名前面加星号*,格式为: datatype *name;或者datatype name = &value;*表示这是一个指针变量,datatype表示该指针变量所指向的数据的类型。在指针定义中号是和变量名结合;。
虽说*是与datatype结合的,但一次要定义多个指针的时候,每一个变量名前都需要一个*号。
#include<stdio.h>
int main()
{
int *p,a=10; //p为一个整形指针变量(未初始化,野指针),a为一个整形变量
int *ap=&a; //ok
char *c=nullptr; //good
return 0;
}
综上两点,我们看来一个例子
使用指针变量是首先要明确指针变量自身的值(存储的是地址),再明确指针变量所指的实体(解引用)。在此我们可以粗浅的认为,对一个指针进行解引用就等于其指向变量的本身(如上例,即*ap=>a);
定义指针变量时,类型对指针变量起到2个作用。1)解析存储单元的大小,即将sizeof(datatype)个字节作为一个整体;2)指针变量加1的能力(即加1时,一次增加sizeof(datatype)个字节)。
3.指针的应用
以我们熟悉的交换函数为例
#include<stdio.h>
void swap1(int a, int b)
{
int temp = a;
a = b;
b = temp;
}
void swap2(int* ap, int* bp)
{
int temp = *ap;
*ap = *bp;
*bp = temp;
}
int main()
{
int a = 10, b = 20;
int* ap = &a, * bp = &b;
swap1(a, b);
swap2(ap, bp);
return 0;
}
那个是正确的呢,显而易见时第二个。因为我们知道,在调用一个函数时,系统会为次函数分配一个栈帧,同时将实参的值传递给形参,执行其函数体,函数执行完毕后,回收栈帧。对于swap1()来说,虽然看上去交换了a和b的值,当仅仅是交换了他自己函数里面的a,b的时,并没有对主函数里的a,b产生影响。对于swap2()来说,我们通过解引用,可以直接操作主函数中a,b的值,所以他就能交换成功。
4.指针和数组的关系
数组名被看作该数组的第一个元素在内存中的首地址(仅在sizeof操作中例外,该操作给出数组所占内存大小)。数组名在表达式中被自动转换为一个指向数组第一个元素的指针常量。
数组名是指针,非常方便,但是却丢失了数组另一个要素:数组的大小,即数组元素的数量。编译器按数组定义时的大小分配内存,但运行时(run time)对数组的边界不加检测。这会带来无法预知的严重错误。
C提供根据数组的存储地址访问数组元素的方法。上图中ar是数组第一个元素的地址,所以ar是数组的第一个元素ar[0],而ar+1是数组第二个元素的地址,(ar+1)是第二个元素ar[1]本身。指针加1,则地址移动一个数组元素所占字节数。
C语言的下标运算符[]是以指针作为操作数的,ar[i]被编译系统解释为*(ar+i),即表示为ar所指(固定不可变)元素向后第i个元素。无论以下标方式或指针方式存取数组元素时,系统都是转换为指针方法实现。逻辑上有两种方式,物理上只有一种方式。
因此ar[i]等效于i[ar];
5.指针的运算
指针变量与整型量的加减
指针变量与整型量的加减表示移动指针,以指向当前目标前面或后面的若干个位置的地址。指针与整型量i的加减等于指针值(地址)与i*sizeof(目标类型)积的加减,得出新的地址。运算结果并不表明那儿有一个指针所规定的数据类型的变量,即使是对数组操作。这称作不对数组边界做检查。指针的算术运算很容易超出数组的边界,需要小心越界问题。
指针变量与指针变量的加减
指针之间可以相减,但不可以相加。
两个同类型指针,指向连续空间可以相减。减后的结果是数据元素的大小。
6.指针与const
当使用const去定义一个指针的时候,这是这个指针必须初始化。
声明指针时,可以在类型前或后使用关键字const,也可在两个位置都使用。
例如,下面都是合法的声明,但是含义大不同:
#include<stdio.h>
int main()
{
int n = 5, m=6;
const int* ap = &n;
int* const bp = &n;
const int* const cp = &n;
*ap = m; //不合法,不能修改
ap = &m; //但可以改变指向
*bp = m; //可以修改
bp = &m; //不合法,不能改变指向
*cp = m; //不合法,不行修改
cp = &m; //不合法,不能改变指向
return 0;
}
我们可以这样理解,当*比const离变量名更近时,const修饰的是其指向的变量(*先于指针变量结合,解引用),指向的变量就不能修改了。当const比*离变量名更近时,const修饰的时指针变量,所以指针变量里的内容不能修改,但能修改指向变量的值。
除此之外,在很多情况下const通常用在函数形参中,这也是 const最有用之处了,用来限定函数的形参,这样该函数将不会修改实参指针所指的数据。在C标准库中有很多函数形参都用const限制了,为了防止在函数内部修改指针指向的数据。
char* strcpy(char* destination, const char* source);
int strcmp(const char* str1, const char* str2);
7.无类型指针void*
void 不能定义变量,但可以定义指针变量。特别之处在于void指针可以指向任意类型变量的地址。所以我们也把void指针变量称为泛型指针,是指针都可以给void指针变量赋值。甚至vp = &vp;也可以。如果要将void 指针vp赋给其他类型的指针,则需要强制类型转换。
在ANSIC标准中,不允许对void指针进行一些算术运算如vp++或vp+=1等,因为既然void是无类型,那么每次算术运算我们就不知道该操作几个字节,例如char型操作 sizeof(char)字节,而int则要操作sizeof(int)字节。
而在GNU中则允许,因为在默认情况下,GNU认为void和char一样,既然是确定的,当然可以进行一些算术操作,在这里sizeof(p〕==sizeof(char)。
好处是: void指针可以任意类型变量的地址。函数中的形参为void指针变量时,函数就可以接受任意类型变量的地址。
{
_memicmp(void const*, void const*, size_t)
_memicmp_l(void const*, void const*, size_t, _locale_t)
memccpy(void*, void const*, int, size_t)
memchr(void*, int, size_t)
memicmp(void const*, void const*, size_t)
8.数组与指针作为函数参数
用数组作为函数的形参,数组将退化为指针类型。如果想要在函数中传递一个一维数组作为参数,必须以下面三种方式来声明函数形式参数,这三种声明方式的结果是一样的,因为每种方式都会转成指针。
void Print_Array(int br[], int n);// 形式参数是一个未定义大小的数组:
void Print_Array(int br[5], int n);//形式参数是一个已定义大小的数组
void Print_Array(int *br,int n);//形式参数是一个指针
我们试想一下,若数组不退化成指针,则在调用此函数时,我们需要给数组中的每一个元素给予空间和赋值,这在时间和空间上都不是高效的,但若退化为指针则只需申请很少的空间,就可以访问到数组中所有元素。由于退化成了指针,丧失了数组大小的信息,因此我们在使用数组和指针作为函数参数时,因加以限制条件,避免产生越界。
9.多级指针
指针可以指向普通类型的数据,例如int、double、char等,也可以指向指针类型的数据,例如int *、double*、char* 等。如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。以此类推,就会有三级指针,四级指针等等。