目录
首先我们需要了解:在计算机中,数据存储在内存里,CPU(中央处理器)在处理数据时,是从内存中读取数据然后再放回内存中的。
1.内存和地址
1.1:内存
为帮助理解,需先了解计算机中的常见二进制单位:
- bit——比特位 一个比特位可以存储一个二进制的0/1
- Byte——字节 1Byte = 8bit
- KB 1KB = 1024 Byte
- MB 1MB = 1024KB
- GB 1GB = 1024 MB
- TB 1TB = 1024 GB
我们可以将内存理解为一大块空间,而内存被划分为一个个内存单元,每个内存单元的大小为一字节(也就是8bit)。
其中每个内存单元都有一个编号,编号是为了让CPU快速找到内存空间的。而内存单元的编号就称为地址,c语言中给地址取了新的名字--指针。因此我们也可以这样理解:
内存单元的编号 = 地址 = 指针
1.2:编址
因为内存中字节很多,所以需要给内存进行编址,也就是上文的编号。
首先我们需要知道:CPU与内存间是用线连接起来的(如下图)
有地址总线、数据总线和控制总线,不过我们今天只讨论一组线--地址总线。
地址总线每一根线只有两态(0/1),那么每根线都能表示两种含义,两根线就能表示四种含义,······。依此类推,n根地址线就能表示2^n种含义,每一种含义都表示一个地址。
地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据通过数据总线传入CPU寄存器。
2.指针变量和地址
2.1:取地址操作符(&)
顾名思义,&是用来取出变量的地址的,前面我们学过如何创建一个变量,但变量创建的本质其实是在内存中申请空间来存放这个变量,如下图:
我们创建的变量a其实是给我们自己看的,编译器编译时不看a,而是通过地址找内存单元的。
而&就可以帮我们拿到a的地址(如下图):
14是十六进制数,&取出来的是a的第一个字节的地址(因为当第一个地址取出来后后面的地址可以直接按顺序找到)。
2.2:指针变量
指针变量也是一种变量,这种变量是专门用来存放地址的,用于存储取地址操作符(&)拿到的地址。如下图:
这时候的pa就是a中第一个字符的地址,打印如图:
2.3:解引用操作符(*)
当我们拿到了地址(指针)时,我们该怎么使用呢?——这时候解引用操作符(*)就派上用场了。如图:
当我们用&取出a的地址并放入指针变量pa中时,我们可以用*将pa中存放的a的地址重新修改,如图中的a本来为20,后面用*pa将a变为了10086;
2.4:指针变量的大小
指针变量是用来存放地址的,因此地址的存放需要多大空间,那么指针变量的大小就为多大。
如上图:
在x86即32位环境中指针变量的大小是4个字节;
在x64即64位环境中指针变量的大小是8个字节。
3.指针变量类型的意义
3.1:指针的解引用
指针的类型决定了对指针解引用时有多大的权限(一次能操作几个字节)
当指针指向a的类型为int时如图:
本来我所定义的整型变量a的地址如上,但当解引用后a的地址全为0(如下图)
而如果是字符型的话如下:
因此,指针的类型决定了对指针解引用时有多大的权限(一次能操作几个字节)
3.2:指针+-整数
直接上图:
可以看到,int*类型的指针变量+1跳过了4字节,char*类型的变量+1跳过了1字节,
实际上,
pa + 1 == pa + 1 * sizeof(int)
pc + 1 == pc + 1 * sizeof(char)
因此,指针的变量类型决定了指针变量向前/向后走一步的距离有多远。
3.3:void*指针
void*指针是无具体类型的指针(也可以叫泛型指针),如果用char*来接收一个int的整型的话编译器会报错,但是void*就不一样了,这玩意可以接收不同类型的指针,就是不能直接进行指针运算。
4.const修饰指针
const修饰指针又可以分为修饰变量和修饰指针变量两部分。
- const修饰变量
直接先上一段代码:
int main()
{
int a = 10;
a = 20;//当我们创建变量a时变量a是可以被修改的,如此处被修改为了20
//但是当用const修饰变量时:
const int b = 100;
b = 200;//此处我们想修改b的值,但是b的值被const加上了语法限制,无法被直接修改了
return 0;
}
注:const修饰变量只是给变量添加了常属性,const修饰变量的时候叫常变量,变量本质上还是变量,只是被添加了常属性因此不能被修改。
由上述代码可知,当const修饰变量时变量就无法被直接修改了;但是我们可以取出b的地址,通过指针变量和解引用的操作修改b的值。
不过既然我们用了const那就应该是希望我们的变量不被修改,如果用地址修改b的值就打破了const对变量的限制,由此,我们引出了下面的知识:const修饰指针变量👇
- const修饰指针变量
当我们用const修饰指针变量时,const既可放在*的左边,也可放在*右边
如图:有两种形式,但是不同的形式意义不同。
- 当const放在左边时
如图,当const放在左边时对p解引用后赋值编译器会报错,由此可推知,此时const修饰的是指针指向的内容
而修饰指针变量本身是没问题的。
- 当const放在右边时
此时和放在左边时的情况相反
如图,当const放在右边时将b的地址赋给指针变量会报错,由此可推知,此时const修饰的是指针变量本身
而修饰指针指向的内容是没问题的。
- 当const放在两边时
由以上内容可知,const修饰指针变量时,不管将const放在左边还是右边,都是可以对指针变量本身或其指向的内容进行修改的,而我们想要完全使变量不被修改的话,直接将const放在两边即可
如图,此时不论如何修改编译器都会报错。
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]);//sz为数组的元素个数
for (int i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
运行结果如下:
指针间的减法同理,将上述代码中for循环的条件改为(int i=sz;i>0;i--)即可。
5.2:指针-指针
指针-指针计算的前提是两个指针指向的是同一个空间,计算的绝对值是指针之间的元素个数。
代码及运算结果如上,pa为字符数组首元素的指针处,pb为\0处的指针,计算并输出的是它们间的元素个数。
5.3:指针的关系运算(比大小)
连续的地址是由小👉大的,如下图的输入输出就可以体现出来。
6.野指针
野指针的意思就是指针指向的位置不可知,随机不确定。
6.1:野指针的成因
- 指针未初始化
- 指针越界访问(数组)
- 指针指向的范围的空间(地址)释放
6.2:野指针的规避
其实就是由果导因,根据它的成因来进行相应的避免。
- 记得指针初始化
如果不知道指针该指向哪里,可以直接给指针赋NULL。
NULL:NULL是c语言中定义的一个标识符常量,值为0,0也是地址只是无法使用,在读写时会报错。
- 小心指针越界
- 避免返回局部变量的地址
- 指针变量不再使用时及时置NULL,指针使用之前检查有效性。
在c语言中只要是NULL指针就不去访问,因此可及时置NULL进行避免
当我们要写语句时,可以先通过if(p != NULL)
{
......
}
的形式对p(假设p是我们所创建的一个指针变量)是否为NULL进行判断,再写出我们的语句。
7.assert断言
使用形式为:assert(表达式)
当系统运行到assert语句时会对括号内的表达式进行判断,
当括号内表达式为真时:assert()不会产生任何作用且程序继续运行;
当括号内表达式为假时:程序就会终止运行并给出报错信息提示。
但使用宏assert()时需包含头文件assert.h,assert用于在运行时确保符合指定条件,如果不符合就报错终止运行,这个宏常常被称为“断言”。
当我们在程序在程序中使用完assert(),确定程序不需要再做断言后,就在#include <assert.h>的语句前面,定义一个宏NDEBUG,然后重新编译程序时编译器就会禁用文件中所有的assert()语句。
当然,虽然assert()在检查程序时十分有用,但也有缺点——因为引入了额外的检查,增加了程序的运行时间。
8:指针的使用和传址调用
首先假设我们需要写一个函数来交换两个整型变量的值。
我们也许会这样进行编写:
//写一个函数交换两个整型变量的值
void test(int x, int y)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d%d", &a, &b);
test(a,b);
printf("%d %d\n", a, b);
return 0;
}
看上去好像没问题,运行一下试试呢?
我们发现,a、b的值并没有进行交换,这是因为我们在调用test函数时在test函数的内部创建了形参x和y接收a和b的值,然而x、y、a、b的地址各不相同,形参只是相当于实参的临时拷贝,因此在test函数内部x和y的值发生了交换并不影响a和b。当test函数调用完后回到main函数,a和b无法交换。像这种把变量本身直接传递给了函数进行调用的方式叫传值调用。
既然传值调用行不通那我们该怎么办呢?
我们既然无法直接通过传递变量进行交换,那不妨直接传递变量的地址,在函数内部通过地址间接操作main函数中的a和b使其进行交换。实现代码如下:
//写一个函数交换两个整型变量的值
void test(int* x, int* y)
{
int tmp = 0;
tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d%d", &a, &b);
test(&a, &b);
printf("%d %d\n", a, b);
return 0;
}
我们可以看到,调用test函数的时候是将变量的地址传递给了函数,这种函数调用方式叫做传址调用。
传址调用可以让函数和主调函数间建立真正的联系,在函数内部可以修改主调函数间的变量;所以之后函数中若是只需要主调函数的变量来进行运算就可以使用传值调用。如果函数内部要修改主调函数中的变量的值则要传址调用。
完
创作不易,给作者君随手来个三连吧。