指针与指针变量

  • 指针就是地址,指针和地址是同义词
  • 存放指针的变量就是“指针变量”

一、符号 &

1. 位与运算符

&作为双目运算符使用时,为“位与”运算符

int a = 0x10;
int b = 0x04;

int c = a & b;

2. 取地址运算符

&作为单目运算符使用时,为取地址运算符

1)取变量的地址
int a = 100;

&a代表的是a变量第一个字节的地址,也即是a的指针

2)取函数的地址
int fun()
{

}

&fun即函数的指针,也即函数代码所在空间第一个字节的地址,只不过为了方便使用,
往往将&省略,也就是说函数名可以直接代表函数的指针。也就是说fun&fun等价

3)取数组的地址
int buf[7] = {0};

&buf为整个数组的地址,注意是整个数组的地址,不是数组第一个元素buf[0]的第一个字节的地址(指针),
&buf[0]buf才是数组第一个元素地址,与数组指针相关的内容,我们留到讲数组时再介绍


二、符号 *

*作为双目运算符时是乘号,作为单目运算符时则与指针有关

1. 用于组建指针类型

*int/float/struct Student等类型相结合,构成指针类型。

int a = 100;
int *p = &a;

此时pa是两个不同的变量,各自拥有自己独立的变量空间:

  • a:是整形变量,用于存放整形数100
  • p:是指针变量,用于存放整形变量a的指针

由于p中存放了a的指针,所以我们说p也指向了a,但是本质是p里面放的指针指向a

什么叫“指向”?

答:指向的意思就是,可以通过这个地址找到对应的空间,并访问它

2. 解引用时

int a = 100;
int *p = &a; 

*p = 200;  //*p:解引用

此时整个*p代表就是p中指针所指向的空间,也就是a的空间,所以*p=200a=200的效果是一样的,
此时*的作用就是解引用,解引用时*p是一个整体

这里还要注意的是,使用*解引用时,其实解引用的是p中的指针,而不是p本身,p只是装指针的容器

总结解引用的目的:找到指针所指向空间,找到后我们就可以访问(读写)它了

读:从空间读数据

int a = 100;
int *p = &a; 

int c = *p; //从*p所代表的空间中读数据

写:修改空间数据

int a = 100;
int *p = &a; 

*p = 200; //往*p所代表的空间中写数据

三、在c程序中,获取存储空间地址(指针)方式有哪些

1. 直接获取数字形式的地址

int *p = (int *)0x4234f5e4; // 这里强制转换为int,如果没转,就是普通的整型数
*p = 200; //向0x4234f5e4所指向的空间,写入200

像这种方式有个前提,那就是程序员需要事先知道这个地址所对应的空间是可用的,可用的意思是?

  • 1)地址对应的空间存在
  • 2)这个空间没有被别人(变量、常量、代码)使用
  • 3)空间的访问权限,能够满足我们的访问要求

在纯应用的C程序中,我们是不知道具体哪个地址是可用的,我们只能采用&a等方式来使用,
编译器在编译时,会将它变为具体的地址值,程序员不需要知道具体的地址值

疑问:什么时候会直接使用数字形式的地址呢?

答:这个一般在单片机、驱动等偏硬件的C程序中才会用到,因为开发偏底层的程序时,
往往会直接通过数字形式的地址来访问存储空间,为什么会这样呢?

其实只要你学习过单片机和驱动,你很容易就能理解我所说的,否则就算我解释了你不太明白,
所以目前大家只需要知道存在这种情况即可

我们目前讲的各种C例子程序,都是属于应用的c程序,与底层硬件无直接关系,
所以我们不会用到纯粹数字形式的地址

疑问:进行单片机、驱动开发时,我怎么知道具体那个数字地址可用?

答:我们需要产看硬件的说明手册

2. 使用&

int a;
int *p = &a;
*p = 200;

作为程序员的我们,其实并不知道a`的指针具体是多少,所以无法直接使用数字形式的地址, 所以只能使用&a`方式来代表,编译器会把它变为具体的地址

这里一定要注意,a不是指针,a只是一个的符号,&a才是a的指针

3. malloc返回地址

int *p = malloc(4);

malloc从堆中开辟空间后,程序员也不需要知道这个空间的指针(第一个字节的地址)是多少,
也没法知道,因为malloc在运行的过程时才会到堆中开辟空间,到那个时候才知道

所有只能是在运行的过程中,让malloc将地址(指针)返回并放在p中,然后通过p去间接的使用指针

4. 某些符号本身就是地址(指针)

1)例子1
char *p = "hello wolrd,it is butefull"

"hello wolrd,it is butefull"是一个字符串常量,它放在了.rodata中,
而p中放的是"hello wolrd,it is butefull"所在常量空间的指针

千万不要以为字符串存放在了p中,p只有48个字节,这个字符串存放时需要十几个字节,
不可能将字符串放到p中的,所以p中只能放字符串的指针,然后通过这个指针去访问常量空间中的字符换

对于程序员来说,我们并不知道字符串所在常量空间的指针是多少,
所以在c语法中,此时整个字符串"hello wolrd,it is butefull"就直接代表那个指针

疑问:char buf[] = {"hello wolrd"},这种情况也是的吗?

答:这种情况不是的,此时字符串字节放在了数组中,所以这里的"hello wolrd"不代表指针。

2)例子2
int fun()
{

}

fun函数名,这个符号就是函数的指针,即函数代码所在空间的第一个字节的地址


四、解引用时,指针的使用方式有哪些

两种方式:

  • 一种是直接使用
  • 另一种是间接使用

1. 直接使用

直接对指针进行解引用,这就是直接使用。

1)例子1
*((int *)0x43355435) = 100;
2)例子2
int a;
*(&a) = 200;

2. 间接使用

先放到指针变量中,然后通过指针变量去使用。

int a = 10; 
int *p = &a;

*p = 200;

这种间接的使用方式,才是我们最常见的方式。

这里一定要注意,指针变量只是一个放指针的“篮子”而已,真正起作用的是里面所放的指针。


五、 * 和 & 互为逆向运算符

int a = 100;
int *p = &a;

*p = 200;

由于p中放的是&a,而解引用的是p里面的指针,所以*p = 200实际上就等价于*&a = 200;
其实相当于*&相互抵消了,因为这两个能相互抵消,所以*&互为逆向运算符


六、指针变量空间大小

所有指针变量的大小(宽度)都是固定的,因为存储空间所有字节的指针都是相同“宽度”的

  • 比如,如果地址宽度是32位的话,那么每个字节的地址都是32位的
  • 比如,如果地址宽度是64位的话,那么每个字节的地址都是64位的

既然所有字节的指针宽度都是一样的,因此所有指针类型的宽度也都是一样的,

int *
float *
struct Student *
double *

指针变量是用来存放指针的,既然所有指针的宽度都是一样,那么存放指针的指针变量的宽度必然也是一样

测试自己LinuxWindows下地址的宽度:

#include <stdio.h>

int main(void)
{
    int *p = NULL;
    printf("%d\n", sizeof(p)); //或者sizeof(int *))

    return 0;
}

七、指针类型

1. 指针的类型长什么样子

指针类型由基本类型 + *组成

int *
float *
...

2. 定义指针变量

定义时,在指针类型*的后面加上变量名即可

int *p;

int **p;

3. 指针类型中基本类型的作用

在前面就说过,int * / struct student *等指针类型的宽度都是固定的,
既然“宽度”固定的,那么指针类型中区分intstruct Student等基本类型有什么意义呢?

其实意义就在于,正是由于有intstruct Student等基本类型,
所以对指针进行解引用时,才能正确的解释指针所指向的空间

比如:

int a = 100;
int *p = &a;

*p = 10000;

a的空间有4个字节,访问a的空间,只需要知道如下三件事:

  • (1)知道a第一个字节的地址,即a指针
  • (2)知道访问哪一个字节时结束
  • (3)知道空间的存储结构

*p解引用时,很显然是能够知道以上三件事的
(1)p中放的就是a指针
(2)int *中的基本类型为int,所以访问指针所指向的空间时,需要访问的空间大小为四个字节
(3)int *中的int也指明了,在访问指针所指向的空间时,按照整形的存储结构来解析的

总之对指针进行解引用时,指针类型中基本类型的作用,就是用于决定对所指向空间的解释方式

指针类型都是固定宽度的,指针类型中的基本类型,与指针本身宽度无关,只与指针所指向空间的宽度有关


八、对指针进行强制转换

前面说过=两边的类型必须一致,自然也包括指针类型

1. 天然的一致

int a = 100;
int *p = &a;

2. 通过“强制转换”来保证类型一致

double a = 100.6;

float *p = &a;            //隐式强制转换
float *p = (float *)&a;   //显式强制转换

3. 强制转换时做了什么

强制转换时,&a本身的值不改变,变的只是对指针所指向空间的“解释方式”

double a = 100.6;
float *p = (float *)&a;

*p = 12.6;

&a:指针类型为double *
pfloat *

两边的类型不一致,因此需要做强制转换,直接使用&a访问所指向的空间时:

*(&a) = 200.02;

是按照double类型来解释的,但是将指针强制转换为float *后,
再使用该指针去访问时,就会以float的类型去访问


struct Student
{
    int num;  
    char name; 
    float score;
};

struct Teacher
{
    char name; 
    int num; 
};

struct Student stu = {123, "zhangsan", 87.6};

struct Teacher *stup = (struct Teacher *)&stu; //&stu为结构体指针,即第一个字节的地址

指针类型被强转为struct Teacher *后,通过stup解引用访问stu的空间时,
则会以struct Teacher类型来解析


九、解释指针

指针(地址)就是一个数,至于这个数应该如何解释,就看这个数的类型

比如:

int a = 0;
  • &a:为int *的指针,解引用时按照整形来解释&a所指向的空间
  • (int)&a:此时这个数就只是一个普通的整形数,此时不再是一个指针,不能进行解引用
  • (void *)&a:为void *指针,这是一个空类型指针
    空类型指针仅仅就是一个指针,不能被直接解引用,如果要解引用的话,
    必须将指针强制转换为int *float *等具体指针类型,然后才能进行解引用,后面还会再次介绍void *
  • (struct student *)&a:结构体指针,解引用时会按照student student结构来解释
    a的合法空间只有4个字节,但是由于struct student类型大小4个字节,
    所以按照struct student进行解引用访问时,所访问的空间会超过4个字节,这样会访问到其它的非法空间

所以在实际编程中进行强制转换时,我们必须慎重使用强制转换