㊙️小明博客主页:➡️ 敲键盘的小明 ㊙️
✅关注小明了解更多知识☝️
文章目录
前言
提示:
本篇文章为C语言指针的详细笔记和完整的源码,内容如若有误,请联系小明及时更正。
- 转载请注明原创,谢谢。
提示:以下是本篇文章正文内容:
一、指针
指针是什么?
1. 指针是内存中一个最小单元的编号,也就是地址
2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
也就是说:指针就是地址,口语中说的指针通常指的是指针变量。
单元编号 == 地址 == C语言中也叫:指针
指针变量
我们可以通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个变量就是指针变量
#include <stdio.h>
int main()
{
int a = 10;//在内存中开辟一块空间
int *p = &a;//这里我们取出变量a的地址,可以使用&操作符。
//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量中
//p就是一个之指针变量。
return 0;
指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
那这里的问题是:
一个小的单元到底是多大?(1个字节)
对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);
那么32根地址线产生的地址就会是:
这里就有2的32次方个地址。
每个地址标识一个字节,那我们就可以给 4G 的空闲进行编址。
同样,那64位机器,大家自己可以计算一下如果给64根地址线,能编址多大空间。
这里我们就可以知道:
在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
总结:
- 指针是用来存放地址的,地址是唯一标示一块地址空间的。
- 指针的大小在32位平台是4个字节,在64位平台是8个字节。
二、指针的类型
在前面,我们知道了变量着有不同的类型:整型、浮点型、字符型等等。
那么指针有类型嘛?
通过上面的示例:
我们可以知道指针的定义方式是:type + *
char*
类型的指针是为了存放char
类型变量的地址。short*
类型的指针是为了存放short
类型变量的地址。int*
类型的指针是为了存放int
类型变量的地址。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
printf("%d\n", sizeof(int*));
printf("%d\n", sizeof(short*));
printf("%d\n", sizeof(long*));
printf("%d\n", sizeof(float*));
printf("%d\n", sizeof(double*));
printf("%d\n", sizeof(long long*));
return 0;
}
代码运行结果:
由上面的地址线可知:
在32位的机器上,地址是32个 0 或者 1 组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
那如果是在64位机器上,如果有64根地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
即:
在32位的机器上,指针的大小是4个字节,在64位的机器上,指针的大小是8个字节。
那么,指针类型的意义是什么呢?
2.1 指针的解引用
指针类型的意义:
先说结论:
- 指针类型可以决定指针解引用的时候的访问权限(可以访问多好个字节)
- 指针类型可以决定指针+/-的步长
例如:
int类型指针+1跳过4个字节,char类型的指针+1跳过1个字节,那+/-n 就是跳过n *sizeof(type)个字节。
关于这段代码解释两点,首先a存的这个数(16进制)不会溢出,十六进制的1位相当于二进制的4位,int类型的a32个二进制位所以11223344刚好把a放满!
而且,我们会发现它在内存中是倒着存的,原因是大小端字节序的问题,这个大小端的问题以后会有详细解释。
int main()
{
int a = 0x11223344;
int* pa = &a;
*pa = 0;
return 0;
}
代码运行结果:
我们发现刚开始的11223344被改成了00000000
这时我们将 int* 改成 char* 类型试一试 :
int main()
{
int a = 0x11223344;
char* pa = &a;
*pa = 0;
return 0;
}
所以我们就得到了总结:指针的类型决定了对指针解引用的时候有多大的权限。
比如: char* 的指针解引用就指针访问 1 个字节,而 int* 的指针解引用可以访问 4 个字节。
2.2 指针 ± 整数
接下来我们看一段代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 1;
char* pc = &a;
int* pi = &a;
printf("pc = %p\n", pc);
printf("pc+1 = %p\n", pc+1);
printf("pi = %p\n", pi);
printf("pi+1 = %p\n", pi+1);
return 0;
}
代码运行结果:
所以我们就得到了总结:指针的类型决定了指针+1/-1跳过几个字节。
比如: char*的指针+1,跳过1个字节,int* 的指针+1,跳过4个字节。double* 的指针+1,跳过8个字节等。
三、野指针
概念:
野指针就是指针指向的位置是不可知的(随机的、不正常的、没有明确限制的)。
3.1 野指针的成因
1️⃣ 指针未初始化
int main()
{
int* p;
*p = 10;
return 0;
}
代码运行结果:
此时,p是一个指针变量未初始化,也是局部变量(在函数内)由前面的函数栈帧的创建与销毁可知,局部变量未初始化是随机值,把随机的一块内存给改成10,这多少是有点危险!
2️⃣ 指针越界访问
int main()
{
int arr[10] = { 1 };
int* p = arr;
int i = 0;
for (i = 0; i <= 10; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
代码运行结果:
arr数组只有十个元素,下标0–9,你直接访问10将是一个随机值!
3️⃣ 指针指向的那块空间已经释放(还给操作系统了)
int* test()
{
int a = 3;
return &a;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
这里a 是在test函数里面创建的,当return &a后test的函数栈帧就已经销毁了(还给操作系统了),当下面在去解引用的时候就指向随机位置了。
3.2 如何规避野指针
- 指针进行初始化
int main() { int a = 10; int* pa = &a; //指针的初始化 int* p = NULL; //空指针,相当于0 - (void*)0,是专门用来初始化指针的 return 0; }
NULL - 空指针,是专门用来初始化指针的,就相当于先把那条野狗先栓到树上,不让它乱跑,但也不一定意味着就绝对安全了,因为如果你去靠近这棵树并进行挑衅,那还是很危险的。如果定义了一个指针为空指针,但是去解引用进行改变值,那程序就会崩掉。因为p是指向的是一个空的地方。
- 小心指针越界
当我们在运用指针指向的数组空间的时候,注意不要越界即可。
- 指针指向空间释放,及时置NULL
如果一个指针指向的空间释放掉了,那我们需要把它赋一个空指针,让这个指针变得安全。
- 避免返回局部变量的地址
出了局部变量指针就会销毁,因为局部变量是有周期性的。
- 指针使用前检查有效性
在指针使用之前要判断这个指针是不是能用的,只有指针能用代码才能跑起来。
四、指针运算
4.1 指针 ± 整数
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//使用指针打印数组内容
int* p = arr;
//arr == p
//arr + i == p + i
// *(arr + i) == *(p + i) == arr[i]
//p指向的是数组首元素
//p + i是数组中下标为i的元素的地址
//p + i起始时跳过了i*sizeof(int)个字节
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
//printf("%d ", *(arr + i));
//printf("%d ", arr[i]);
//printf("%d ", i[arr]);
}
return 0;
}
代码运行结果:
#define N_VALUES 5
int main()
{
float values[N_VALUES];
float* vp;
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0;
}
return 0;
}
代码分析:
4.2 指针 - 指针
例1:
int main()
{
int arr[10] = { 0 };
//
//指针-指针的前提:两个指针指向同一块区域,指针类型时相同的
//指针-指针差值的绝对值,指针和指针之间的元素个数
//
printf("%d\n", &arr[9] - &arr[0]);
printf("%d\n", &arr[0] - &arr[9]);
return 0;
}
代码运行结果:
例2:
//模拟实现strlen
//1.计算器
//2.递归
size_t my_strlen(char* str)
{
char* start = str;
while (*str)
{
str++;
}
return str - start;
}
int main()
{
char arr[] = "abcdef";
size_t len = my_strlen(arr);
printf("%zd\n", len);
return 0;
}
代码运行结果:
4.3 指针的关系运算
int main()
{
int arr[5] = { 1 };
int* p = NULL;
for (p = &arr[5]; p > &arr[0]; )
{
*--p = 0;
}
return 0;
}
分析:
越界但不非法,没有进行访问,只是超出范围,就相当于你不可以抢银行,但是允许在银行门口看看,转转,没抢银行,所以未非法访问,但确确实实是越界了。
C语言标准规定:
允指向数组元素的指针与指向数组左后一个元素后面的那个内存中位置的指针比较,但不允许与指向数组第一个元素的那个内存位置的指针比较!
五、二级指针的使用
二级指针就是取一级指针的地址,一级指针就是取原来数字的地址
解释:
ppa就是一个二级指针!! int** ppa后面的一颗 * 说明paa是一个指针变量!前面的 int* 则说明ppa指向的对象是(一级)指针类型。
理解到这里就可以以二级指针类比三级指针了:三级指针就是里面存的是二级指针变量地址的指针变量。
六、指针与数组
- 指针就是指针,指针变量就是一个变量,存放的是地址,指针变量的大小是4/8
- 数组就是数组,可以存放一组数,数组的大小取决于元素类型和个数
- 数组的数组名是首元素地址
- 通过一个指针可以访问数组元素
数组名表示数组首元素地址,但有两个例外:
- sizeof(数组名),数组名单独放在sizeof内部,数组名表示整个数组,计算的是数组的大小,单位是字节
- &数组名,数组名表示整个数组,取出的事数组的地址,数组的地址和数组首元素地址的值是一样的,但是类型和意义不同
案例1:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%p\n", arr);
printf("%p\n\n", arr + 1);//跳过4个字节
printf("%p\n", &arr[0]);
printf("%p\n\n", &arr[0] + 1);//跳过4个字节
//例外:
printf("%p\n\n", &arr);
printf("%p\n", &arr + 1);//跳过40个字节
printf("%d\n\n", sizeof(arr));
return 0;
}
代码运行结果:
案例二:
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%p===%p\n", p + i, arr + i);
}
return 0;
}
代码运行结果:
七、指针数组
我们经常听说的两个东西:一个是指针数组,一个是数组指针。很多人都搞不清楚这个东西,其实:
数组指针本质是指针, 而 指针数组本质是数组。
指针数组 是一个数组!是存放指针的数组!
我们知道一个整型或字符行的数组,里面都存放的是相对应类型或比该类型小的元素,那指针数组里面存的应该就是地址!
举个栗子:
#include <stdio.h>
int main()
{
//使用指针数组模拟二维数组
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
//指针数组
int* arr[] = { arr1,arr2 };
int i = 0;
for (i = 0; i < 2; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
代码运行结果:
完结
好啦,阅读到这里就已经看完了本期博客的全部内容了