前言:本篇博客将系统梳理与指针相关的基础概念,希望对读者的理解有所帮助~
一、内存和地址
1.内存
(1)每个内存单元大小:1个字节
(2)每个内存单元都有一个编号。 内存单元的编号<=>地址<=>指针
(3)内存单元单位补充:
bit-比特位
byte-字节
KB
MB
GB
TB
PB
2.编址的理解方式
(1)地址总线:32位机器有32根地址总线,每根线有两态(0/1),可以表示个不同的地址
二、指针变量和地址
1.取地址操作符(&)
#include<stdio.h>
int main()
{
int a=10;
&a;
printf("%p\n",&a);
return 0;
}
会打印处理:0x007FFD38,取出a所占4个字节中最小的地址
2.指针变量和解引用操作符(*)
(1)指针变量:用来存放地址
(2)拆解指针类型
int a = 10;
int * pa = &a; //*说明pa是指针变量,int说明pa指向的对象是int类型
(3)解引用操作符(间接访问操作符)*
int main()
{
int a = 100;
int* pa = &a;
*pa = 0; //*pa就是通过pa里面存的地址,找到a并将其修改
return 0;
}
3.指针变量的大小:地址线(平台)决定指针变量的大小,相同平台下大小都相同
#include <stdio.h>
//指针变量的⼤⼩取决于地址的⼤⼩
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)
int main()
{
printf("%zd\n", sizeof(char *));
printf("%zd\n", sizeof(short *));
printf("%zd\n", sizeof(int *));
printf("%zd\n", sizeof(double *));
return 0;
}
三、指针变量类型的意义
1.指针的解引用:指针类型决定对指针解引用的时候有多大的权限(一次能操作几个字节)
(1)char*:一次访问一个字节
(2)int*:一次访问四个字节
2.指针±整数:指针的类型决定了指针向前或者向后走一步有多大(步长)
#include <stdio.h>
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}
3.void指针:无具体类型的指针(泛型指针)
(1)可以接受任意类型的地址
(2)不能直接进行指针的±整数和解引用的运算(不知道访问几个字节)
例子:使用void来接收地址
#include <stdio.h>
int main()
{
int a = 10;
void* pa = &a;
void* pc = &a;
*pa = 10;
*pc = 0;
return 0;
}
(3)void*的作用:一般用在函数参数的部分,接受不同类型的地址以实现泛型编程。
四、const修饰指针
1.const修饰变量
#include <stdio.h>
int main()
{
int m = 0;
m = 20;//m是可以修改的
const int n = 0; //n具有常属性,不可以被修改,但本质上还是变量
n = 20;//n是不能被修改的
return 0;
}
但是:通过指针可以修改n
2.const修饰指针变量
(1)int const* p:限制*p(p指向的内容不能被修改)
(2)int const p:限制p(p本身不能被修改)
(3)int constconst p: 两个都限制
五、指针运算
1.指针±整数
#include <stdio.h>
//指针+- 整数
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; i++)
{
printf("%d ", *(p+i));//p+i 这⾥就是指针+整数 ,打印1 2 3 4 5 6 7 8 9 10
}
return 0;
}
2.指针-指针
(1)前提:两个指针指向同一块内存空间
(2)|指针-指针|(绝对值)=两个指针之间元素的个数
#include <stdio.h>
int my_strlen(char *s)
{
char *p = s;
while(*p != '\0' )
p++;
return p-s;
}
int main()
{
printf("%d\n", my_strlen("abc")); //利用指针-指针可求字符串长度
return 0;
}
3.指针的关系运算(地址和地址比大小)
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
while(p<arr+sz) //指针的⼤⼩⽐较
{
printf("%d ", *p); //将数组中的十个元素按顺序打印到屏幕上
p++;
}
return 0;
}
六、野指针
1.野指针成因
(1)指针未初始化
(2)指针越界访问(通常是超出数组的范围)
(3)指针指向的空间释放**(空间还给操作系统)**
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
2.如何规避野指针
(1)指针初始化:不明确在指针指向哪里,可以赋值NULL。0也是地址,只不过无法使用,使用时会报错
(2)小心指针越界(C语言本身不会检查数组的越界行为)
(3)指针变量不再使用时,及时置NULL,指针使用前检查有效性
int main()
{
int arr[10] = {1,2,3,4,5,67,7,8,9,10};
int *p = &arr[0];
for(i=0; i<10; i++)
{
*(p++) = i;
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//下次使⽤的时候,判断p不为NULL的时候再使⽤
//...
p = &arr[0];//重新让p获得地址
if(p != NULL) //判断
{
//...
}
return 0;
}
(4)避免返回局部变量的地址【1.(3)】
七、assert断言
1.assert.h头文件定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合就报错。
2.assert()宏接受一个表达式作为参数。
(1)表达式为真则继续运行
(2)表达式为假则报错,显示没有通过的表达式及文件名,行号
(3)使用assert()的好处
①自动识别文件和出问题的行号
②无需更改代码就可以开启/关闭assert:如果不需要断言,在#include<assert.h>前面加上:#define NDEBUG
八、指针的使用和传址调用
1.strlen的模拟实现
int my_strlen(const char * str) (即char const*str,不准改arr的内容)
{
int count = 0;
assert(str);
while(*str)
{
count++;
str++;
}
return count;
}
int main()
{
int len = my_strlen("abcdef");
printf("%d\n", len);
return 0;
}
2.传值调用和传址调用
(1)传值调用:形参是实参的一份临时拷贝,对形参的修改不影响实参
(2)传址调用:在函数内部可以修改主调函数的变量
x和y的交换不影响a和b,因此Swap1失败
改进代码如下:
#include <stdio.h>
void Swap2(int*px, int*py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap1(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}