这篇质量不太行:(

内存和地址

在了解指针之前,先讲讲内存是如何管理的

首先因为内存很大(一般有几个G),所以为了高效管理,有了内存单元的概念。而这个单元的大小,正好是一个字节。

因为一个比特位就是一个二进制位,太小了,超过一个字节,在处理char这样一个字节长的变量很麻烦。

定下长度后,就可以给内存单元编号,而每个内存单元获得的独一无二的编号,便是它的地址,以声明了一个变量a为例,示意图如下

变量的地址

上图中a占4个字节,每个字节都有自己的地址,但要找到a其实只需要找到第一个地址就行了,实际上在C语言中也是如此,a的地址就是首字节地址,即图中的0x000000AF88DFF6A4

关于几个名词

C语言中称地址指针,储存地址的变量叫指针变量,平时也简称指针,此时强调的是指针变量里储存的地址,而不是这个变量。

指针变量的组成

指针变量也要拆成两部分来看

一个是变量的,在同一个程序中,所有指针变量的值的长度都是一样的,都指向了某个内存中的字节, 至于具体多长,取决于环境:32位程序是4个字节,64位程序是8个字节

另一个是变量的类型,类型决定编译器从所指向的字节,向后总共读几个字节,以及用什么方式读取内存里的内容。以下图的代码为例

可以看到三种指针指向了同一个字节,即它们的值是相等的,但指针类型不同,解引用之后得到的也不同,

charint短,所以*p_char只能取到00,

而虽然floatint一样长,但对内存的读法不同,所以*p_float*p_int依然不同

指针(变量)的使用

声明指针变量

指针变量也是变量,在没有结合性问题时,和一般变量的声明方式差不多。

变量的声明:变量类型 + 变量名

指针的声明:指向的变量类型 + * + 变量名

以下以声明一个字符指针为例

1
char* pointer = NULL;

变量的声明逻辑如上图

进阶:二级指针->N级指针

我们可以用同样的逻辑声明更高级的指针

1
2
char* *ppstr = NULL;//ppstr是一个二级指针
char** *ppstr = NULL;//pppstr是一个三级指针

在声明中,前面的char*声明了ppstr指向的变量类型,后面的*变量名结合,声明ppstr是一个指针.

此处,称指向一级指针的指针为二级指针,同理有三级指针,至N级指针.

指针的解引用

指针最常见的用处就是通过变量里储存的地址,通过直接修改目标变量的内存来修改变量的值 , 当然还有强制转换指针类型来读取目标变量的一部分内存 之类的骚操作

函数的传址调用

在遇到指针前,使用函数时,由于实参传到函数里都变成了形参,无法通过形参(包括修改形参的值)来改变实参的值,因为形参终究只是实参的一份临时拷贝.

而有了指针之后,函数的实参,形参关系不变,但我们有了更高端的形参,也就是指针, 尽管函数内的指针依旧是函数外的指针临时拷贝,但我们已经能通过其储存的访问函数外变量的内存了,同时包括读取修改, 这种通过传入指针来修改外部变量的函数调用,便称为函数的传址调用

以如下代码为例

1
2
3
4
5
6
7
void Swap_int(int*a,int*b)
{
//给我两个整型的地址,我就能 真·交换它们的值
int tmp = *a;
*a = *b;
*b = tmp;
}

提问?如何修改函数外的指针的值?

依然还是把这一指针的地址传进去,而函数的形参写成更高一级的指针

如下代码,例如我想在函数里把外部的指针置空

1
2
3
4
5
6
7
8
9
10
11
void Reset(char* *pstr) 
{
*pstr = NULL;
}

int main()
{
char* str;
Reset(&str)//对一级指针取地址,传入二级指针
return 0;
}

有关指针的危险操作

野指针的解引用

有些指针因为错误操作,指向了不能访问的内存,一旦解引用,就有可能使程序崩溃

情形如下

使用了 未初始化/赋值 的指针

1
2
3
4
5
6
7
8
9
10
int* pa;//未初始化,pa的值为随机值
*pa = 0 ;//野指针的解引用

//正确的用法
int arr[10] = {0};

int* pa;
pa = arr;//立即初始化
int* pb = NULL;//初始化

所以声明指针时最好初始化,如果不知道初始化成什么,就用NULL空指针初始化

指向了 已回收的 内存空间

有的函数错误*地返回了内部临时变量的地址, 在外面使用返回的指针,因为此时函数的栈帧已经销毁,会发生野指针的解引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
char* fun(void)
{
char a = 0;
char* pa = &a;
return pa;
}

int main()
{
//情形一
char* pa = fun();
*pa = 1;//此时变量a已经销毁,发生野指针的解引用,即非法访问

//情形二
char* str = (char*) malloc(sizeof(char) *10) //在堆区开辟10个字节的空间
free(str);//然后释放掉
str[0] = 'A';//试图访问已free的内存,并写入,发生非法访问
return 0;
}

空指针的解引用

空指针NULL,0,一旦解引用就会报错,所以在解引用陌生指针时一定要注意判空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//情形一
char* func(char* str)
{
if(str == NULL)//防止使用者错误传入空指针
{
return NULL;
}

printf("%s\n");
}
//情形二
char* InitArray(char** pstr)
{
if(pstr == NULL)
{
return NULL;
}
*pstr = (char*)malloc(sizeof(char) * 10);
if(*ptr == NULL)//malloc一旦失败就会返回NULL,所以调用后一定要判空
{
return NULL;
}
}