目录
正文开始
1. 内存、地址和指针
1.1 内存
内存(Memory)是计算机的重要部件,也称内存储器和主存储器,它用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。简单来说,内存就是计算机中存储数据的硬件。
1.2 地址
我们已经知道了内存中可以存储数据,那只用它来存数据显然是不够的。我们还要对里面的数据进行读写操作,此时就需要用地址来找到他们。
在内存中的每块存储单元,都有与之对应的一个编号,这个编号就是这个存储单元的地址,如果把内存想象成一栋楼,那么地址就是这栋楼里每个房间的房间号。每个存储单元的大小为一个字节,这也是数据存储的基本单位。
值得注意的是,存储单元的地址是制造商在制造内存时就已经在硬件层面设计好了的,就如同钢琴上的每个音,在制造之初就设计好了。
1.3 指针
在C语言中,地址有了新的名字:指针,可以理解为:指针 = 地址 = 内存单元的编号
2. 指针变量和地址
2.1 取地址操作符(&)
C语言提供了取地址操作符&,顾名思义取地址操作符的作用就是将操作对象在内存中地址取出来。
例如:
#include <stdio.h>
int main()
{
int a = 1;
&a; //取出a的地址
printf("%p\n", &a); //%p为占位符,对应指针
return 0;
}
上述代码的内存监视窗口:
上述代码运行结果:
可以看出,由于一个整型变量大小为四个字节,所以在创建整型变量a时,它申请了四个字节的内存空间,其中每个字节都有地址:
0x000000D272AFFB94
0x000000D272AFFB95
0x000000D272AFFB96
0x000000D272AFFB97
而当我们使用&a并打印后,从执行结果可以看出,取地址操作符将变量a的第一个字节的地址取出了。
2.2 指针变量
我们通过取地址操作符取出地址后,肯定还有后续的操作,这时就需要将取出的地址存放在指针变量中。
指针变量的定义格式:
type* name
其中:
- type为指针变量的类型,它代表着指针变量里存储的地址所指向的对象的类型。
- *是在说明这时一个指针变量。
- name代表着指针变量名,可根据需求设置。
例如:
#include <stdio.h>
int main()
{
int a = 1;
int* p = &a; //指针变量的定义
printf("%p\n", p);
return 0;
}
int* p就代表着定义了指针变量p,它所指向的对象a是一个整型类型。
指针变量也是变量,它是用来存放地址的,存放在指针变量中的值都会被理解为地址。
2.3 解引用操作符(*)
我们将地址保存在指针变量中,而后要使用时,需要用到解引用操作符*,它的作用是通过指针变量里存储的地址,来找到地址所指向的对象
例如:
#include <stdio.h>
int main()
{
int a = 1;
int* p = &a;
*p = 0; //解引用操作符
return 0;
}
上述代码的操作就是通过解引用操作符,修改了变量a的值,可以理解为*p就是变量a。
2.4 指针变量的大小
指针变量里存储的是指针,而指针变量的大小也就是指针的大小,也就是存储地址所需要的 bit 位,这与指针指向的类型并无关系。
在32位机器中,用32bit 来标识每一个存储单元,所以地址大小为4字节;
在64位机器中,用64bit 来标识每一个存储单元,所以地址大小为8字节;
我们可以通过以下代码验证:
#include <stdio.h>
int main()
{
printf("%zd\n", sizeof(char*));
printf("%zd\n", sizeof(short*));
printf("%zd\n", sizeof(int*));
printf("%zd\n", sizeof(double*));
return 0;
}
执行结果:
3. 指针变量类型的意义
既然各种类型的指针变量的大小都是一样的,那他们有什么区别呢?
- 在解引用指针时,指针变量的类型决定了它的权限(一次能操作几个字节)。例如:char*的指针解引用只能访问一个字节,而int*的指针解引用能访问四个字节。
- 指针类型决定了指针 +、- 整数时所变化的距离。例如对整型指针加一,那么它的地址变大四个字节,而如果对字符指针加一,它的地址只变大一个字节。
4. void* 指针
指针类型中有一种特殊的void*类型,可以理解为无具体类型的指针(泛类型指针),他有以下特点:
- 可以接收不同类型的地址。
- 不能直接进行指针运算和解引用的运算。
例如:
int main()
{
int a = 0;
char b = ‘i’;
void* p1 = &a; //接收整型类型指针
void* p2 = &b; //接收字符类型指针
return 0;
}
在我们实际应用中,一般使用void*作为函数的参数部分,用来接收不同类型的指针,可以实现泛型编程的效果,使得一个函数来处理多种数据类型的效果。
5. 指针运算
指针的基本运算分为三种:
- 指针 +、- 整数
- 指针 - 指针
- 关系运算
5.1 指针 +、- 整数
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int sz = sizeof(arr) / sizeof(arr[0]);
for(int i = 0; i < sz; i++)
{
printf(“%d”, *(p + i)); //指针+整数
}
return 0;
}
5.2 指针 - 指针
#include <stdio.h>
int my_strlen(char* p)
{
char* p1 = p;
while(*p1 != '\0')
p1++;
return p1 - p; //指针 - 指针
}
int main()
{
printf("%d", my_strlen("abcde"));
return 0;
}
5.3 关系运算
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int sz = sizeof(arr) / sizeof(arr[0]);
while(p < sz + arr) //指针的关系运算
{
printf("%d", *p);
p++;
}
return 0;
}
6. 野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
6.1 野指针的成因
- 指针未初始化
int main()
{
int* p; //指针为初始化,默认为随机值
*p = 20;
return 0;
}
- 指针越界访问
int main()
{
int arr[5] = { 0 };
//i为5是指针越界访问,超出了arr的范围
for(int i = 0; i < 6; i++)
{
*(p + i) = 1;
}
return 0;
}
- 指针指向的空间释放
int* test()
{
int a = 1;
return &a;
}
int main()
{
//变量a出函数就销毁,p指向的空间释放,形成野指针
int* p = test();
return 0;
}
6.2 如何规避野指针
- 初始化 :在定义指针变量时,如果明确了要指向的位置,就初始化为该地址。若没有明确要指向哪里,就定义为NULL,它是C语言中的标识符常量,值为0,这个地址是无法使用的,读写该地址会报错,这样就避免了野指针的形成。
int main()
{
int a = 0;
int* p2 = &a; //初始化为指定地址
int* p1 = NULL; //初始化为NULL
return 0;
}
- 越界访问 :程序申请了某些空间,指针就只能访问这些空间,否则就会越界访问。
- 空间释放 :自定义函数中不要返回函数中的局部变量的地址。
6.3 指针使用规范
当我们使用指针过后,若不再使用时,就把指针定义为 NULL。当我们使用指针之前,还可以判断目标指针是否为 NULL 指针。通常我们认为:只要是 NULL 指针就不去访问。
例如:
int main()
{
int arr[5] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[0];
for(int i = 0; i < sz; i++)
{
*(p++) = 1;
}
//for循环结束后指针变量p已经越界了
p = NULL;
//将p赋值为NULL,防止野指针的使用
//…
p = &arr[0]; //重新定义使用p变量
if(p != NULL) //判断是否为NULL
{
//…
}
return 0;
}
7. 传值调用和传址调用
在我们使用函数时,传递参数分为两种情况:
- 传值调用:将变量的內容做一份临时拷贝,并传递给函数。
- 传址调用:将变量的地址传递给函数。
7.1 传址调用
传值调用所传递的参数仅仅是原参数的一份拷贝,并不能直接的影响到原参数。
我们可以通过以下代码验证:
#include <stdio.h>
void num(int x)
{
x = 1;
}
int main()
{
int a = 0;
num(a);
printf("%d\n", a);
return 0;
}
输出:
可以看到,变量 a 的值并没有发生改变,因为本质上形参x和实参a不是同一个变量,x 的改变并不会影响 a。
7.2 传址调用
顾名思义,传址调用就是将参数的地址作为形参传递给函数。
例如:
void num(int* x)
{
*x = 1;
}
int main()
{
int a = 0;
num(&a);
printf(“%d\n”, a);
return 0;
}
输出结果:
传址调用可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的参数。所以,若函数中只是需要主调函数中的变量来实现计算,就用传值调用;如果函数内部要修改主调函数中变量的值,就用传址调用。
完