一、指针的入门
(1)、预备知识
0、图解:
1、内存地址
- 字节:字节是内存的容量单位,英文称为 byte,一个字节有8位,即 1byte(0000 0000 --- 1111 1111) = 8bits(0 --- 1)
- 地址:系统为了便于区分每一个字节而对它们逐一进行的编号,称为内存地址,简称地址。
在32位系统:
说明:
地址+1就是加1个字节
为什么选32位系统来讲内存??
32位:4G内存:
64位:理论:1800亿亿GB,操作系统支持:16TB,电脑实际的内存条:4G-32G(所以本质上还是基于4G)
2、基地址
- 单字节数据:对于单字节数据而言,其地址就是其字节编号。
- 多字节数据:对于多字节数据而言,其地址是其所有字节中编号最小的那个,称为基地址。
3、取址符
- 每个变量都是一块内存,都可以通过取址符 & 获取其地址
- 注意:
- 虽然不同的变量的尺寸是不同的,但是他们的地址的尺寸确实一样的。
- 不同的地址虽然形式上看起来是一样的,但由于他们代表的内存尺寸和类型都不同,因此它们在逻辑上是严格区分的。
- 示例代码:
// (1)、如何获取一个内存的地址?? char ch1 = 200; int num1 = 100; float f1 = 3.14; double f2 = 6.18; // 1、不同的变量的尺寸是不同的 printf("ch1的地址 == %p\n", &ch1); printf("num1的地址 == %p\n", &num1); printf("f1的地址 == %p\n", &f1); printf("f2的地址 == %p\n", &f2); /* // 3- 不同的地址虽然形式上看起来是一样的,但由于他们代表的内存尺寸和类型都不同,因此它们在逻辑上是严格区分的。 解析: ch1的地址 == 0x7ffd1cf9c917 // char型内存占1字节 num1的地址 == 0x7ffd1cf9c918 // int型内存占4字节 f1的地址 == 0x7ffd1cf9c91c // float型内存占4字节 f2的地址 == 0x7ffd1cf9c920 // float型内存占8字节 */ // 2、但是他们的地址的尺寸确实一样的 printf("ch1的地址的尺寸 == %lu\n", sizeof(&ch1)); printf("num1的地址的尺寸 == %lu\n", sizeof(&num1)); printf("f1的地址的尺寸 == %lu\n", sizeof(&f1)); printf("f2的地址的尺寸 == %lu\n", sizeof(&f2));
(2)、指针的概念
-
0、图解:
1、指针的说明
由于翻译的问题,以及口语表达的习惯,在日常表述中,指针在不同的场合会代表以下几个含义:
- 指 地址
- 比如变量a的地址 &a,这是一个地址当然也是一个指针,我们可以说指针 &a 指向变量 a。
- 指 指针变量
- 比如 int *p; 此处变量p是指针变量,又常被简称指针。
- 示例代码:
// (1)、指针的说明 // 1、指地址 int num2 = 200; printf("num2的地址 == %p\n", &num2) ; // 可以认为&num2为指针,这个指针&num指向了变量num2(或者num2的地址被&num2给存放了) // 2、指指针变量 int num3 = 300; int *p1 = &num3; // 指针变量p里面存放了num3的内存的地址(指针变量p指向了num的内存)
2、指针的初始化(定义)
// 1、未初始化的类型 // 注意:定义指针的时候,必须要有具体的合法指向,否则指针将会在内存中乱指,导致数据处理出错 char *p1; // 字符型指针类型 int *p2; // 整型指针类型 float *p3; // 浮点型指针类型 double *p4; // 双精度浮点型指针类型 // 2、推荐的初始化 char ch = 0; char *p5 = &ch; // 指向确定的我们申请的合法内存(可读可写) char *p6 = NULL; // 指向内存中一个临时的安全的保留区域(不可访问区域)
3、指针的赋值
int num4 = 100; int *p7 = NULL; p7 = &num4; // 一般性的赋值操作,和初始化(定义)是不一样的,不需要加*号,p8本身就是指针变量
4、指针的索引(引用)
int num5 = 5; num5 = 55; // 直接通过内存的名字,对其内存进行赋值操作 int *p8 = &num5; // 让指针p8指向这块内存 *p8 = 8; // 间接通过指针p8来控制num5的内存,然后给其赋值 printf("num5 == %d\n", num5); /* int *p和*p的不同: int *p: 这个是初始化,只是表明这个p变量是个指针 *p: 这个不是初始化,是后面的语句用的,这个时候表明其是指针p指向的那块内存 p: 这个不是初始化,是后面的语句用的,这个时候表明其是指针变量p */
二、特殊指针
(1)、野指针
- 概念:指向一块未知区域的指针,被称为野指针。野指针是危险的。
- 危害:
-
- 引用野指针,相当于访问了非法的内存,常常会导致段错误(segmentation fault)
- 引用野指针,可能会破坏系统的关键数据,导致系统崩溃等严重后果
- 产生原因:
-
- 指针定义之后,未初始化
- 指针所指向的内存,被系统回收
- 指针越界
- 如何防止:
-
- 指针定义时,及时初始化
- 绝不引用已被系统回收的内存
- 确认所申请的内存边界,谨防越界
-
示例代码:
#include <stdio.h> #include <stdlib.h> #include <string.h> // 计算数组元素的个数 #define CAL_ARR_NUM(A) (sizeof(A)/sizeof(A[0])) // 整个数组的大小 / 数组的首元素的大小 == 数组元素个数 // 自己定义的NULL #define SELF_NULL ((void *)0) // 官方定义的NULL // #define NULL ((void *)0) // 空指针 // 主函数 int main(int argc, char const *argv[]) { // (1)、野指针 // 1、指针定义之后,未初始化 char *p1; /* 解决方法: 方法一:char *p1 = NULL; // char *p1 = SELF_NULL 方法二: char ch = 'a'; char *p1 = &ch; */ // 2、指针所指向的内存,被系统回收(笔试题) char *str = malloc(100); // 申请堆内存空间:申请一块100个字节的内存空间(堆空间),str指针指向了这块内存 strcpy(str, "hello"); // 通过str指针将"hello"字符串数据,复制到str指向的内存中(堆空间) // free(str); // 释放堆内存空间:通过str指针释放其指向的内存空间 if (str != NULL) // str指针释放堆内存空间后,其指向依然是那个空间的位置,不会是NULL(具体看系统,看编译器) { strcpy(str, "world"); // 之前已经释放了str指针指向的堆内存空间了,对其已经没有访问权限了,所以会报错误或警告 printf("str == %s\n", str); } // 解决方法:使用完该内存,再释放,绝不再次使用 // 3、指针越界 char buf[] = "shijie"; char *p2 = buf; // 数组的地址 for (int i = 0; i < 7; i++) { printf("buf[%d]的地址 == %p\n", i, &buf[i]); } // p2逐渐加1的地址 for (int i = 0; i < 7; i++) { printf("p2+%d的地址 == %p\n", i, p2+i); // 将指针变量p2里面的地址打印出来 } *(p2+10) = 'a'; // 此处不能赋值,因为越界了,不是我门申请的合法内存空间 /* 解决方法: 让指针赋值操作在其限定的合法内存的区域上进行 比如:使用这个函数CAL_ARR_NUM(A) // 计算其合法内存区域的长度 */ return 0; }
(2)、空指针
很多情况下,我们不可避免地会遇到野指针,比如刚定义的指针内无法立即为其分配一块恰当的存,又或者指针所指向的内存被释放了等等。一般的做法就是将这些危险的野指针指向一块确定的内存,比如零地址内存。
- 概念:空指针即保存了零地址的指针,亦即指向零地址的指针。
// 自写的NULL空指针 #define SELF_NULL ((void *)0) // 官方定义的NULL空指针 #define NULL ((void *)0)
三、指针运算
-
(1)、计算指针的大小
- 指针的大小(尺寸) --- 只和系统位数相关,和数据类型无关 (char * int* 等等,大小都和系统位数有关,不和数据类型相关)
-
那指针的数据类型有什么用呢?
-
可以让指针确定其作用范围,或者其移动范围