每日励志:拼搏十年,征战沙场,不忘初心,努力成为一个浑身充满铜臭味的有钱人。
一.内存和地址
1.内存
计算机内存是一系列存储单元的集合,每个存储单元都有唯一的地址来标识。这些存储单元用于存储程序的数据和指令。内存可以被看作是一个巨大的数组,地址就像是这个数组的索引,通过地址可以找到对应的存储单元。内存分为一个一个的内存单元,每一个内存单元的大小是一个字节,一个字节等于8bit。(一个比特位可以存储一个2进制的位1或者0)
2.地址
地址是内存单元的编号,用于定位内存中的数据。在 C 语言中,当我们声明一个变量时,系统会自动为该变量分配一块内存区域,变量的地址就是这块内存区域的起始位置。
可以类比为一个宿舍楼(内存),一个内存单元就是一个宿舍,而8bit就是8个人,同时,每一个宿舍号就是地址
3.指针
指针是一个变量,它的值是另一个变量的地址。
例如: int a = 10
int * p = &a;
这里,p 是一个指针变量,&a 表示变量 a 的地址。
指针 p 存储的就是变量 a 在内存中的地址。
通过指针,可以间接访问和操作内存中的数据,即通过指针可以修改变量 a 的值。
变量创建的实质是向内存申请相应空间来存储数据,例如定义int a = 10时,就是在内存里申请 4 个字节的空间去放置数值 10,而这 4 个字节的每个单元都有独一无二的地址编号。在程序中,我们所设置的变量名,其实主要是为了方便程序员自身识别和操作,编译器在实际处理时并不会识别变量名,它依据的是内存地址来精准定位并操作对应的内存单元。
二.指针变量
1.取地址操作符 & 与解引用操作符 * (间接访问操作符)
在C语言中创建交量其实就是向内存申请空问,例如我们在创建int a = 10时就是在内存里申请 4 个字节的空间去放置数值 10,每一个字节都有地址
这个时候通过 & 便可以获得地址(第一个字节的地址),剩下的三个地址随之而然也可以得知
#include <stdio.h>
int main()
{int a = 10;int *pa = &a;*pa = 20;printf("%d\n",a);printf("%p\n",&a);return 0;
}//第一个printf输出的是20,这是因为通过指针pa修改了变量a的值。//第二个printf输出的是变量a的内存地址,不同设备和运行环境输出会有所不同
2.指针类型
例如: int a = 10
int * pa = &a;
*pa = 20;
int * ,是在说明pa是指针变量,int 是在说明pa指向的是整型(int)类型的对象, *pa 是表示指针pa指向的内存地址中存储的值,*pa = 20 是将pa指向的地址处存储的值改为20。
3.指针地址大小
32位平台下地址是32个bit位(即4个字节),而64位平台下地址是64个bit位(即8个字节)
指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。
三.指针的解引用
1.
#include <stdio.h>
int main()
{int n = 0x11223344;printf("初始值:n = 0x%X\n", n);printf("n的地址:&n = %p\n", &n);int *pi = &n;printf("解引用pi前:*pi = 0x%X\n", *pi);*pi = 0;printf("解引用pi后:*pi = 0x%X\n", *pi);// 分别输出每个字节的值unsigned char *byte_ptr = (unsigned char *)&n;printf("修改后n的每个字节值(按字节拆分):");for (int i = 0; i < sizeof(n); i++) {printf("0x%02X ", byte_ptr[i]);}printf("\n");return 0;
}
#include <stdio.h>
int main()
{int n = 0x11223344;printf("初始值:n = 0x%X\n", n);printf("n的地址:&n = %p\n", &n);char *pc = (char *)&n;printf("解引用pc前:*pc = 0x%02X\n", *pc);*pc = 0;printf("解引用pc后:*pc = 0x%02X\n", *pc);// 分别输出每个字节的值unsigned char *byte_ptr = (unsigned char *)&n;printf("修改后n的每个字节值(按字节拆分):");for (int i = 0; i < sizeof(n); i++) {printf("0x%02X ", byte_ptr[i]);}printf("\n");return 0;
}
我们可以观察到代码1会将n的4个字节全部改为0,但是代码2只是将n的第一个字节改为0
char * 的指针解引用就只能访问一个字节,而 int * 的指针的解引用就能访问四个字节。由此可知,指针的类型决定了指针解引用的时候能够一次操作几个字节
2.void * 指针
它是通用指针,不指向特定数据类型。它不能直接解引用,需强制转换为目标类型指针后才能操作。常用于函数参数部分,用来接收不同类型数据的地址。
#include <stdio.h>int main()
{int age = 520;double price = 13.14;char initial = 'A';// 使用void指针指向不同类型的变量void* ptr;// 将int变量的地址赋给void指针ptr = &age;printf("使用void指针访问int变量的值:%d\n", *(int*)ptr);// 将double变量的地址赋给void指针ptr = &price;printf("使用void指针访问double变量的值:%.2f\n", *(double*)ptr);// 将char变量的地址赋给void指针ptr = &initial;printf("使用void指针访问char变量的值:%c\n", *(char*)ptr);return 0;
}
四.const修饰指针
1.const修饰变量
变量是可以修改的,如果我们把变量的地址交给一个指针变量,我们也可以通过这个指针变量来修改这个变量,但如果我们不希望这个变量被修改,那么我们就引进 const 来限制
1.
int n = 0;
n = 20;
2.
const int m = 0;
m = 20;(编译器会报错)
const的限制是语法层面的限制,所以在2中如果对m进行修改就报错。但是我们可以通过指针绕过const的限制
#include <stdio.h>int main()
{const int m = 0;printf("%d\n",m);int *pm = &m;*pm = 100;printf("%d\n",m);return 0;
}
同时,在const修饰变量的时候,这个被修饰的变量本质上还是变量,叫做变常量,只不过它不能被修改。
2.const修饰指针变量
可以放在 * 的左边,也可以故在 * 的右边,
a.const 放在指针声明的左边
当 const 放在指针声明的左边时,表示指针指向的数据是常量,不能通过该指针修改其值。指针本身仍然可以指向其他地址。
#include <stdio.h>int main()
{int num1 = 520;int num2 = 1314;const int *p = &num1; // 指针指向的值是常量printf("通过指针访问num1的值: %d\n", *p); // 可以访问值// *p = 100; // 错误:不能通过指针修改值// 可以改变指针指向的地址p = &num2;printf("通过指针访问num2的值: %d\n", *p);return 0;
}
b. const 放在指针声明的右边
当const放在指针声明的右边时,表示指针本身是常量,不能改变它指向的地址。但是可以通过指针修改指向地址的值。
#include <stdio.h>int main()
{int num1 = 520;int num2 = 1314;int *const p = &num1; // 指针本身是常量printf("通过指针访问num1的值: %d\n", *p); // 可以访问值*p = 100; // 可以通过指针修改值printf("修改后通过指针访问num1的值: %d\n", *p);// p = &num2; // 错误:不能改变指针指向的地址return 0;
}
五.指针运算
分别有三种:
- 指针+-整数
- 指针-指针
- 指针的关系运算
1.指针 + / - 整数
- 含义:指针和整数相加或相减,会改变指针的值,使其指向数组中前后的元素。
- 规则:指针指向某种数据类型,当指针和整数n相加或相减时,指针的值会增加或减少n个该数据类型所占的字节数。(由指针类型决定+-1的步长)
#include <stdio.h>int main()
{int arr[] = {10, 20, 30, 40, 50};int *p = arr; // 指向数组第一个元素printf("arr[0] = %d\n", *p); // 输出10p = p + 2; // 指向arr[2]printf("arr[2] = %d\n", *p); // 输出30p = p - 1; // 指向arr[1]printf("arr[1] = %d\n", *p); // 输出20return 0;
}
#include <stdio.h>int main()
{int arr[] = {1,2,3,4,5,6,7,8,9,10};// 数组索引 0 1 2 3 4 5 6 7 8 9int i =0;int sz = sizeof(arr)/sizeof(arr[0]);int * p = &arr[0];for(i = 0; i<sz;i++){printf("%d ",*p);p++;}return 0;
}
2.指针 - 指针
-
含义:两个指针相减的绝对值,得到它们之间的元素个数。
-
规则:两个指针都指向同一种数据类型,相减的结果是它们在内存中所指向元素的个数之差。
#include <stdio.h>int main() {int arr[] = {10, 20, 30, 40, 50};int *p1 = arr; // 指向arr[0]int *p2 = arr + 3; // 指向arr[3]int difference = p2 - p1;printf("指针之间的元素个数: %d\n", difference); // 输出3return 0;
}
数组名其实是数组首元素的地址
为什么没有指针加指针呢?
因为指针加指针就像日期加日期一样没有意义
eg 还原 strlen函数
#include <stdio.h>size_t my_strlen(char*p)
{char* start = p;char* end = p;while(*end != '\0'){end++;}return end - start;
}int main()
{char arr[] = "abcdefg";// a b c d e f g \0size_t len = my_strlen(arr);//数组名其实是数组首元素的地址 arr == &arr[@]printf("%zd\n",len);return 0;
}
3.指针的关系运算
-
含义:比较两个指针的大小,判断它们指向的内存位置的前后关系。
-
规则:两个指针都指向同一种数据类型,可以使用关系运算符(==、!=、>、<、>=、<=)进行比较。
#include <stdio.h>int main()
{int arr[] = {10, 20, 30, 40, 50};int *p1 = arr; // 指向arr[0]int *p2 = arr + 2; // 指向arr[2]if (p1 < p2) {printf("p1 < p2\n");} else if (p1 == p2) {printf("p1 == p2\n");} else {printf("p1 > p2\n");}int *p3 = arr + 1;if (p3 != p2) {printf("p3 != p2\n");}return 0;
}
六.野指针
它指的是一个指针指向了一个已经被释放或者无效的内存地址。野指针的存在会导致程序行为不可预测,可能会引发崩溃或数据损坏。
1.指针未初始化
#include <stdio.h>int main()
{int *p; // 声明一个指针,但未初始化// 尝试访问指针指向的值printf("指针p指向的值: %d\n", *p);return 0;
}
当我们尝试访问 *p 时,程序可能会崩溃,或者输出一个随机的值,因为p指向的内存地址未被程序合法使用。
2.指针越界访问
#include <stdio.h>int main()
{int arr[3] = {10, 20, 30}; // 定义一个大小为3的数组int *ptr = arr; // 指针初始化为数组首地址// 正常访问数组元素printf("arr[0] = %d\n", *ptr); // 输出10printf("arr[1] = %d\n", *(ptr + 1)); // 输出20printf("arr[2] = %d\n", *(ptr + 2)); // 输出30// 越界访问,尝试访问arr[3]printf("越界访问arr[3] = %d\n", *(ptr + 3)); return 0;
}
使用*(p + 3)尝试访问 arr[3],这超出了数组的实际大小(数组只有3个元素,索引为0、1、2)。这种越界访问会导致未定义行为,可能读取到随机值或引发程序崩溃。