文章目录
在计算机中,每一个写入的数据都要在内存中存储,拥有一小块空间。每块空间都有一个地址,这个地址一个数字来表示。根据地址我们可以对其进行访问,同时地址也叫做指针。
为了方便数据存储,内存被划分成一个个小的内存单元,每个内存单元是一个字节(8个比特位)
- 1字节(byte)=8比特位(bit)
- 1kb=1024byte
- 1mb=1024kb
- 1gb=1024mb
- 1tb=1024gb
其中一个比特位可以存放一个二进制的1或者0
编址
在32为(x86)环境下,地址总线有32根(x64环境中64根),每根线都可以由0或1表示,所以地址就可以有2^32种表示方式。
也就是说,x86环境下,地址是由一个32位的二进制序列表示的,所以无论是什么类型数据的地址,它都占32个比特位的空间,也就是4个字节(x64环境中则是64个比特位,8个字节)。
通过地址总线,内存中的数据就可以被传递到CPU中的寄存器里
数据总线可以传输数据
控制总线控制数据是写入还是读取
指针变量
#include<stdio.h>
int main()
{
int i = 10;
int* p = &i;
printf("%p\n", p);
return 0;
}
结果打印了一个一个8位16进制数字,也就是i的地址
- int* 是变量p的类型,其中int表示其指向内容的类型是int,*表示它是指针
- 变量p中存储了i的地址,即p指向i
- 打印地址时占位符要用%p
解引用操作符*
#include<stdio.h>
int main()
{
int i = 10;
int* p = &i;
*p = 20;
printf("%d\n", i);
return 0;
}
打印出来的结果是20
这说明p解引用后的*p可以用来访问该地址指向的内容
(注意:p是地址,*p是地址指向的内容)
指针类型
指针类型也分为char*,int*等等,虽然它们的长度都是4个字节,但是各自也有不同意义
#include<stdio.h>
int main()
{
int i = 0x12345678;
int* p = &i;
char* pc = &i;
*pc = 0;
return 0;
}
当代码运行完第一句时,以及把0x12345678的值放到了i里面
但是当用char*类型的指针去访问i时,只能访问到地址最低的两位
也就是说,指针变量的类型决定了指针的“步长”,即指针能够访问到空间的大小
图中char类型的指针只能访问到一个字节的空间,而int类型可以访问4个字节
指针的运算
指针 +(-)整数
#include<stdio.h>
int main()
{
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int* p = arr;//数组名是首个元素的地址
printf("%p\n", p);
p++;
printf("%p\n", p);
printf("%d\n", *p);
return 0;
}
可以看到,地址+1就相当于地址+“步长”,+n就是加上n倍的“步长”
这也就刚好保证了p++能访问到内存中下一个元素
(注意以上结论实现的前提是数组的元素随着下标是从低到高存储的)
指针-指针
#include<stdio.h>
int main()
{
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int* p = arr;
printf("%p\n", p);
int*s = &arr[9];
printf("%p\n", s);
printf("%d\n", s - p);
return 0;
}
指针-指针的绝对值就是中间元素的个数 + 1(如果低地址-高地址就是负数)
注意前提是两个指针必须指向同一块区域
指针比大小
#include<stdio.h>
int main()
{
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int* p = arr;
while(p <= &arr[9])
{
printf("%d ", *p);
p++;
}
return 0;
}
指针之间可以比较大小,用的时候要注意p++
和p <= arr[9]
是因为数组元素地址随着下标是递增的
const修饰指针
const修饰变量可以使该变量无法被修改,但是利用指针访问依然可以间接修改。
但是如果用const修饰指针,那么就有下面几种情况:const放在*前面或者后面,或者两边都放
- const放在*前(与放在int的前后无关),那么该指针指向的内容(*p)不能改变,但是指针的指向(p)可以改变
#include<stdio.h>
int main()
{
int i = 10;
int j = 20;
const int* p = &i;//const也可以放在int后面
*p = 0;//这条代码会报错
return 0;
}
- const放在*后,那么该指针的指向(p)不能改变,但是可以通过该指针改变它指向的内容(*p)
#include<stdio.h>
int main()
{
int i = 10;
int j = 20;
int* const p = &i;
p = &j;//这条代码会报错
return 0;
}
- 如果两边都放就都不能改变
野指针
野指针的可能成因
- 未初始化指针
int* p;
- 指针指向的内容销毁(空间释放)
int* test()
{
int n = 0;
return &n;
}//出函数时&n被保存在寄存器中传出来,但是n已经被销毁了!!!
int main()
{
int* p = test();
return 0;
}
- 指针访问越界
int arr[10] = 0;
int* p = arr[10];
规避野指针
- 指针初始化(如果提前不知道指针需要指向哪里,可以先置空
int* p = NULL
) - 及时置空(NULL)
- 指针使用前检查有效性
assert断言
在debug版本中,判断括号内的内容,如果为真则不产生任何效果;如果为假程序报错
在release版本中,assert宏被优化掉了,无论是否为真都不产生任何效果
assert断言需要包含头文件<assert.h>
#include<assert.h>
//......
int* p = NULL;
assert(p != NULL)
此时程序会报错
如果需要禁用assert宏,需要在assert头文件前面加上#define NDEBUG
这样assert语句就会失效
传值调用和传址调用
传值调用
传值调用是将实际参数的值传给形式参数,二者拥有独立空间,对形参的操作无法影响实参
#include<stdio.h>
void Change(int a, int b)
{
int change = b;
b = a;
a = change;
}
int main()
{
int x = 3;
int y = 5;
Change(x, y);
printf("%d", x);
printf("%d", y);
return 0;
}
代码运行后,发现x和y实际上并没有交换,这就是因为改变形参不会改变实参
传址调用
#include<stdio.h>
void Change(int* a, int* b)
{
int change = 0;
change = *b;
*b = *a;
*a = *change;
}
int main()
{
int x = 3;
int y = 5;
Change(&x, &y);
printf("%d", x);
printf("%d", y);
return 0;
}
把传入的变量变成地址后,形参和实参共用一个地址,所以是完全一样的,改变形参就是在改变实参
数组和指针
数组名的理解
在一般情况下,数组名就是首元素的地址,但有两种情况不同:
- 和数组在同一个函数内的sizeof(数组名),但是如果是数组传参后再用sizeof计算大小,此时的数组名就又算作首元素地址了,因为数组传参的本质是传首元素的地址,所以我们可以通过函数来改变数组
#include<stdio.h>
void test(int* ptr)//这里用数组(int ptr[])或者指针接收都可以
{
printf("%d\n", sizeof(arr));
}
int main()
{
int arr[10] = {0};
test(arr);
return 0;
}
此时打印的结果就是指针长度4而不是数组长度40了
- &数组名表示整个数组的地址,此时数组名就是整个数组,在数值上等于首元素地址,但是 &数组名+1跨越的长度是整个数组
使用指针访问数组
#include<stdio.h>
int main()
{
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
int i = 0;
for(i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
这个代码可以完成对该数组的打印
此处的p换成arr,变成*(arr + i)
也是可以的
那么就有*(arr + i)
=*(i + arr)
=arr[i]
=i[arr]