指针是什么?
指针理解的2个要点:
1. 指针是内存中一个最小单元的编号,也就是地址
2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
总结:指针就是地址,口语中说的指针通常指的是指针变量。
那我们就可以这样理解:
内存
指针变量
我们可以通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个 变量就是指针变量
#include <stdio.h> int main() { int a = 10;//在内存中开辟一块空间 int *p = &a; //这里我们对变量a,取出它的地址,可以使用&操作符。 //a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量 中,p就是一个之指针变量。 return 0; }
int a = 10; //是向内存中的栈区空间申请4个字节的空间,这四个字节用来存放10这个数值
int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量中,p就是一个指针变量。
总结:
指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
那这里的问题是:
一个小的单元到底是多大? (1个字节)
如何编址? 经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。
对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);
那么32根地址线产生的地址就会是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
...
11111111 11111111 11111111 11111111
这里就有2的32次方个地址。
每个地址标识一个字节,那我们就可以给
(2^32Byte == 2^32/1024KB==2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB)
4G的空闲进行编址。
同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,自己计算。
这里我们就明白:
在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
如图32位系统下 各个类型的指针都是4个字节
如图64位系统下 各个类型的指针都是8个字节
总结:
内存被划分成一个个内存单元,每个内存单元的大小是1个字节
每个字节的内存单元都有一个编号,这个编号就是地址,地址在C语言中是指针
地址要存储的话,存放在指针变量中
每个内存单元都有一个唯一的指针来标识
指针是用来存放地址的,地址是唯一标示一块地址空间的。
指针的大小在32位平台是4个字节,在64位平台是8个字节。
指针和指针类型
这里我们在讨论一下:指针的类型
我们都知道,变量有不同的类型,整形,浮点型等。
那指针有没有类型呢? 准确的说:有的。
当有这样的代码:
int num = 10; p = #
要将&num(num的地址)保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢? 我们给指针变量相应的类型。
char *pc = NULL; int *pi = NULL; short *ps = NULL; long *pl = NULL; float *pf = NULL; double *pd = NULL;
这里可以看到,指针的定义方式是: type + *
其实:
char* 类型的指针是为了存放 char 类型变量的地址。
short* 类型的指针是为了存放 short 类型变量的地址。
int* 类型的指针是为了存放 int 类型变量的地址。
那指针类型的意义是什么?
指针+-整数
#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; }
总结:指针的类型决定了指针向前和向后走一步有多大
int* 的指针+1,跳过4个字节
char* 的指针+1,跳过1个字节
short* 的指针+1,跳过2个字节
double* 的指针+1,跳过8个字节
指针的解引用
//演示实例 #include <stdio.h> int main() { int n = 0x11223344; char *pc = (char *)&n; int *pi = &n; *pc = 0; //重点在调试的过程中观察内存的变化。 *pi = 0; //重点在调试的过程中观察内存的变化。 return 0; }
指针类型决定了指针进行解引用操作的时候,访问几个字节
char 1个字节
short 2个字节
int 4个字节
总结:
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针成因
指针未初始化
#include <stdio.h> int main() { int *p;//局部变量指针未初始化,默认为随机值 *p = 20; return 0; }
指针越界访问
#include <stdio.h> int main() { int arr[10] = {0}; int *p = arr; int i = 0; for(i=0; i<=11; i++) { //当指针指向的范围超出数组arr的范围时,p就是野指针 *(p++) = -1; } return 0; }
指针指向的空间释放
这里放在动态内存开辟的时候详细说
如何规避野指针
- 指针初始化 ——如果明确指针应该指向哪里,就初始化正确的地址
- int a =10;
- int*p = &a;
- ——如果指针不知道初始化什么值,为了安全,初始化NULL
- int*p = NULL;
- 小心指针越界
- 指针指向空间释放即使置NULL
- 避免返回局部变量的地址
- 指针使用之前检查有效性
初始化指针:
在定义指针变量时,立即将其初始化为 NULL 或有效的内存地址,这样可以避免它成为野指针。
避免未初始化的指针:
在使用指针之前,确保为其分配了有效的内存空间。
可以使用动态内存分配函数(如 malloc、calloc 等)或者将指针指向有效的对象或数组。
避免释放后仍然使用指针:
在释放了指针所指向的内存后,不要再对其进行访问或引用。可以在释放后将指针设置为 NULL,以避免意外使用。
合理使用指针:
确保在使用指针之前检查其是否为 NULL。可以使用条件语句(如 if)或者断言来验证指针的有效性。
避免悬挂指针:
当一个指针指向已经释放的内存时,将其设置为 NULL,以避免成为悬挂指针。
谨慎使用指针操作:
在进行指针操作时,要确保操作的对象是有效的,并且不会越界访问。
#include <stdio.h> int main() { int *p = NULL; //.... int a = 10; p = &a; if(p != NULL) { *p = 20; } return 0; }
指针运算
指针+- 整数
指针-指针
指针的关系运算
指针+- 整数
//arr --> p
//arr == p
//arr+i == p+i
//*(arr+i) ==*(p+i) == arr[i]
//*(arr+i)== arr[i]
//*(i+arr)== i[arr] ——方块仅仅只是操作符
//p指向的是数组首元素
//p+i是数组中下标为i的元素的地址
//p+i起始时跳过了i*sizeof(int)个字符
#define N_VALUES 5 float values[N_VALUES]; float *vp; //指针+-整数;指针的关系运算 for (vp = &values[0]; vp < &values[N_VALUES];) { *vp++ = 0; }
指针加减整数是指对指针进行偏移操作。当一个指针与一个整数相加或相减时,编译器会根据指针所指向的数据类型来计算偏移量,然后将指针移动到相应位置。
例如,假设有一个指向整型数据的指针ptr,可以使用以下方式进行偏移操作:
ptr + n
:将指针ptr向后移动n个单位,每个单位的大小由指针所指向的数据类型决定。
ptr - n
:将指针ptr向前移动n个单位。
指针-指针
指针与指针之间的减法运算用于计算两个指针之间的偏移量。如果有两个指针ptr1和ptr2,它们指向同一数组或同一内存块的不同位置,可以使用以下方式计算它们之间的偏移量:
ptr2 - ptr1
:计算ptr2相对于ptr1的偏移量,结果将以单位为指针所指向的数据类型的大小表示。指针的关系运算包括大于(>)、小于(<)、大于等于(>=)、小于等于(<=)、等于(==)和不等于(!=)等比较运算符。这些运算符用于比较两个指针的值,比较结果基于指针所指向的内存地址。
//指针减去指针的前提:两个指针指向同一块区域,指针类型相同的
// 指针减去指针差值的绝对值:得到的是指针之间的元素个数
模拟strlen
1.计数器
2.递归
int my_strlen(char *s) { char *p = s; while(*p != '\0' ) //八进制的0 p++; } return p-s;
指针的关系运算
for(vp = &values[N_VALUES]; vp > &values[0];) { *--vp = 0; }
代码简化, 这将代码修改如下:
for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--) { *vp = 0; }
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证 它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与 指向第一个元素之前的那个内存位置的指针进行比较。
指针和数组
指针就是指针 (指针变量,存放地址,指针变量的大小是4/8个字节)
数组就是数组 (存放一组数,数组的大小是取决于元素的类型和个数)
数组的数组名是数组首元素的地址,地址是可以访问指针变量中
我们看一个例子:
#include <stdio.h> int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,0}; printf("%p\n", arr); printf("%p\n", &arr[0]); return 0; }
运行结果:
可见数组名和数组首元素的地址是一样的。
结论:数组名表示的是数组首元素的地址。(2种情况除外,数组章节讲解了)
sizeof(数组名),数组名单独放在sizeof内部,数组名表示整个数组,计算的是数组的大小,单位是字节
&数组名,数组名表示整个数组,取出的是数组的地址,数组的地址和数组首元素的地址,值是一样的,但是类型和意义是不一样的。
那么这样写代码是可行的
int arr[10] = {1,2,3,4,5,6,7,8,9,0}; int *p = arr;//p存放的是数组首元素的地址
注:前两个(arr//&arr[0])表示是数组首元素 +1 跳过4个字节
&arr表示数组 +1 跳过40个字节
既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个就成为可能。
例如:
#include <stdio.h> int main() { int arr[] = {1,2,3,4,5,6,7,8,9,0}; int *p = arr; //指针存放数组首元素的地址 int sz = sizeof(arr)/sizeof(arr[0]); for(i=0; i<sz; i++) { printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p+i); return 0; } }
所以 p+i 其实计算的是数组 arr 下标为i的地址。
那我们就可以直接通过指针来访问数组。
如下:
int main() { int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; int *p = arr; //指针存放数组首元素的地址 int sz = sizeof(arr) / sizeof(arr[0]); int i = 0; for (i = 0; i<sz; i++) { printf("%d ", *(p + i)); } return 0; }
二级指针
二级指针是指一个指针变量的值是另一个指针变量的地址,也就是说这个指针变量存储的是另一个指针变量的地址。在C语言中,可以通过使用二级指针来传递指针的地址,以便在函数内部修改指针的值。
具体来说,我们先来看一下一级指针的定义和使用。一级指针是最常见的指针,它存储了一个对象的地址。通过解引用操作符(*),我们可以获取到该地址所对应的对象的值。
而二级指针则更进一步,它存储了一个一级指针变量的地址。通过解引用操作符(**),我们可以获取到该一级指针所指向的对象的值。
为了更好地理解二级指针,我们可以举个例子。
假设我们有一个整型变量x,并且有两个指针p1和p2,其中p1指向x的地址,p2指向p1的地址。这样,p2就成为了一个二级指针。
下面是一个简单的示例代码,演示了如何声明和使用二级指针:
#include <stdio.h> int main() { int x = 10; int* p1 = &x; int** p2 = &p1; printf("x = %d\n", x); printf("*p1 = %d\n", *p1); printf("**p2 = %d\n", **p2); return 0; }
在上面的代码中,我们首先声明了一个整型变量x,并初始化为10。然后声明了一个一级指针p1,并将其指向x的地址。接着声明了一个二级指针p2,并将其指向p1的地址。
在打印输出部分,我们使用解引用操作符()来获取到指针所指向的对象的值。可以看到,通过p1我们可以获取到x的值,通过**p2我们也可以获取到x的值。
二级指针在某些情况下非常有用,例如在函数中传递指针的地址,以便在函数内部修改指针的值。它也可以用于动态分配二维数组等复杂数据结构。
*ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa .
int b = 20; *ppa = &b;//等价于 pa = &b;
**ppa先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a .
**ppa = 30; //等价于*pa = 30; //等价于a = 30;
指针数组
指针数组
指针数组是指针还是数组?
答案:是数组。是存放指针的数组。
数组我们已经知道整形数组,字符数组。
int arr1[5]; //整型数组 char arr2[6]; //字符数组
那指针数组是怎样的?
int* arr3[5];//是什么?
arr3是一个数组,有五个元素,每个元素是一个整形指针。
整型指针数组
模拟二维数组
i 遍历指针数组arr // j 遍历数组arr1/2/3
模拟出二维数组的效果