由浅入深剖析 C语言 指针
指针的声明
类型 *变量名 = 实体地址, 例子:int *p = &number; 表示 变量名 p 指向 实体number 的地址。
int main(){
int *p; // 声明变量时 *号表示 声明的变量为指针类型
int number = 10;
p = &number; // &号表示 取实体变量的地址(还包含了number 占用存储空间的大小 信息); 变量名 p 指向 实体number 的地址
return 0;
}
指针的两大重要要素
首地址、占用存储空间的大小。
任何变量,记录存储,都必须 有的两个重要信息:首地址、占用存储空间的大小
void testAddress(){
// TODO 一:内存地址
/**
* CPU:算数逻辑单元(对数据进行运算),控制单元(协调硬件活动电路),寄存器组(用于临时存储数据)
* 寄存器组(用于临时存储数据,有限的,如果鱼的记忆只有七秒,那他还不到七秒)
* 所以需要出现 运行内存(只要真正CPU需要用的时候,运算的时候,才会从 运存 到 寄存器组,平时大部分情况下 都在运存中)
* 等CPU运算完成后,再把结果 放入到 运存中(寄存器组 到 运存)
* 结论:运存才是数据中心大本营,寄存器组 仅仅是 CPU在运算时 临时存储区域而已哦
*/
/**
* 运存:
* [][][][][][][][] 8个为一组 = 1字节,以这种形式保存到 运存中的
* 每一个 1字节 都会有一个编号,一个房间,而此编号,就是 【内存地址】
*/
printf("数据类型 char,占用字节大小:%d\n", sizeof(char)); // 1字节
printf("数据类型 short,占用字节大小:%d\n", sizeof(short)); // 2字节
printf("数据类型 int,占用字节大小:%d\n", sizeof(int)); // 4字节
// 运行在 arm64手机设备上是8字节, 若是运行在32位Windows上是4字节,64位Windows上是8字节
printf("数据类型 long,占用字节大小:%d\n", sizeof(long));
printf("数据类型 long long,占用字节大小:%d\n", sizeof(long long)); // 8字节
printf("数据类型 float,占用字节大小:%d\n", sizeof(float)); // 4字节
printf("数据类型 double,占用字节大小:%d\n", sizeof(double)); // 8字节
// 以上打印,我们发现,只有char是一个房间,其他的 都有多个房间,那么多个房间如何管理?
// 答:无论他几个字节,只需第一个字节的首地址 即可
// 以上任何变量,记录存储,都必须 两个重要信息, 以int为例也一样?
// 答:①.此变量的首地址,用第一个字节记录, ②.字节1 字节2 字节3 字节4 代表 占用存储空间的大小 为 4个字节
int arr[2] = {1,2};
int *pInt = arr;
printf("指针pInt首地址:%u, 下一个地址:%u, 步长:%d\n", pInt, pInt+1, sizeof(int));
}
& 取 数据对象 地址的时候 会取得两个重要信息:首地址、占用存储空间的大小
void testPointerDataType(){
// TODO 二:指针数据类型
/**
* &数据对象 &i
* & 获取两个重要级信息:
* 信息一:获取数据对象的首地址。
* 信息二:获取数据对象所需的存储空间大小
*/
// 一个变量,两个要素,
// 要素一:num1变量的第一个字节 房间1 记录了内存地址 首地址 2000H
// 要素二:num1变量占用四个字节空间存储大小。
int num1 = 100;
printf("首地址:%u, 占用存储空间的大小:%lu", &num1, sizeof(int));
}
指针类型
void testPointerTypeSpace(int *pn, char *pc); //先声明(方便后面的函数调用),后实现
void testPointerType(){
// TODO 三:指针类型
int n1 = 100;
int * pN1 = &n1; // pN1 是指针变量(保存 n1 的 首地址 与 所需空间大小,两个重要级 信息)
// 所以会诞生这样的大白话:pN1指针指向n1,其实就是因为pN1指针存储的值 == n1自己的内存地址
char sex = 'M';
char * pSex = &sex; // pSex 是指针变量(保存 sex 的 首地址 与 所需空间大小,两个重要级 信息)
// 所以会诞生这样的大白话:pSex指针指向sex,其实就是因为pSex指针存储的值 == sex自己的内存地址
// 类型* 指针变量 = 内存地址; 类型 * 指针变量 = 内存地址; 类型 *指针变量 = 内存地址; 三种方式都是可以的,没有说那种方式就是错误的
int i1 = 100, i2 = 200, i3 = 300, i4 = 400;
int * pi1 = &i1;
int * pi2 = &i2;
int * pi3 = &i3;
int * pi4 = &i4;
printf("pi1存放的内存地址是:%u\n", pi1); // 3756679752 3756679751 3756679750 375667949
printf("pi2存放的内存地址是:%u\n", pi2); // 3756679748
printf("pi3存放的内存地址是:%u\n", pi3); // 3756679744
printf("pi4存放的内存地址是:%u\n", pi4); // 3756679740
// 为何相差4个字节? 答:因为是从 i1到i4的首地址, 一个i1就有四个字节 第一个字节记录首地址 以此类推
// 所以再次定论一句话:“指针类型的值 是 目标数据对象 的 首地址”
// 经过前面的分析,我们已得知(指针存放 目标数据对象的首地址)但是 数据对象 空间大小 存储到哪里?
// 答:请看下面的代码
int n = 100;
int *pn = &n;
char c = 'A';
char *pc = &c;
pn + 1; // 指针运算,为什么可以, 有【空间存储大小】,内部可以计算步长
// pn = pc;
// 报错:”无法将 char* 转换为 int*“
// 大家会思考,不都是属于整形领域么,为什么不可能呢?
// 答:其实 pn = pc 首地址可以赋值的,首地址赋值是没有任何问题的
// 报错的原因【指针类型改变,会导致数据长度改变,因此无法正确赋值】
// 也就是int* 存放 目标数据对象的 存储空间大小4字节,无法修改成1字节
void * v1 = pc; // 如果你敢使用 void * ,主动丢失【空间存储大小】 所以可以, 直接安安心心的保存 首地址
void * v2 = pn;
// v2 + 1; // 就是因为你丢失 【空间存储大小】 没有任何记录了,所以无法计算
// 【指针最专业 两句话】
// 第一句话:指针类型 是 通过值 来 保存 目标数据对象 的 首地址 int *p = 值
// 第二句话:指针类型 是 通过类型本身 来 标记 目标数据对象 的 空间大小 int *p = 值==sizeof(目标数据对象==int)
// 所以 pn = pc 行不通,就在于 第二句话的严格要求
testPointerTypeSpace(pn, pc);
}
void testPointerTypeSpace(int *pn, char *pc){ // 实现
// TODO 五:指针类型占用空间大小
// 我们知道 char类型占用1字节,int类型占用4字节,代表了 他们的 目标数据对象 占用空间大小 是不同的
printf("sizeof(int * pn) = %d\n", sizeof pn);
printf("sizeof(int * pc) = %d\n", sizeof pc);
// 注意:在Windows的32位环境中,打印的都是4, 在安卓设备 CPU架构指令 arm64位中打印都是8
// 那么为什么是 4 或 8 呢?
// 答:因为 int* 或者 char* 等等,他们的目的,都是存放 目标数据对象 的 首地址/空间大小,而自己本身 只是代表指针类型而已
// 所以此指针类型,的存储访问 用 4个字节 或 8个字节 足矣
}
指针的使用
void testUsePointer(){
// TODO 四:使用指针
// *pj (取pj存放的内存地址 的 值)
// *指针 (根据 指针中存储的(首地址 与 空间大小)找到 目标数据对象, 注意:目标数据对象 就是存放了值 666 888 这种)
int j = 666; // 自己的内存地址 == 1000H
int *pj = &j;
printf("j自己的内存地址:%u\n", &j); // 1000H
printf("*pj存放的值:%u\n", pj); // 1000H pj == pj存放的值 等价于 pj存放的 内存地址
printf("*pj自己的内存地址:%u\n", &pj); // 2000H
printf("*pj存放的值(j的内存地址)的值:%d\n", *pj); // 根据*pj存放的1000H内存地址 找到 值本身 == 666
*pj = 888; // 通过 *pj存放的1000H内存地址 找到 666 修改为 888
printf("*pj存放的值(j的内存地址)的值:%d", *pj); // 根据*pj存放的1000H内存地址 找到 值本身 == 888
}
不同指针类型的转换对应关系
void testt(char * pChar);
void testChangePointerType(){
// TODO 六:不同指针类型的转换对应关系
int i = 999999;
int * pInt = &i;
// 前面说 指针中存储(首地址 与 空间大小) 此时*pChar 能够存储值==首地址, 却不能存储“空间大小”(埋下伏笔:此时空间大小 已丢失)
char * pChar = (char *) pInt;
printf("*pInt存储的值:%u\n", pInt); // 值 等于 i变量自己的内存地址
printf("*pChar存储的值:%u\n", pChar); // 值 等于 i变量自己的内存地址
// *pInt存储 内存地址 的值 == 999999【因为取值时,是根据 首地址 取 四个字节的数据 从而得到完整int值】目标空间大小4字节 未丢失
printf("*pInt存储的值的值:%d\n", *pInt);
// *pChar存储 内存地址 的值 == 63【因为取值时,是根据 首地址 取 一个字节的数据 从而无法得到完整int值】目标空间大小4字节 已丢失
printf("*pInt存储的值的值:%u\n", *pChar); // 数据精度丢失
testt(pChar);
}
void testt(char * pChar){
// TODO 七:《对前面内容的总结》:
// 内存地址:在C语言中,万物皆地址,任何一个变量 都一定有自己的 内存地址
// 指针类型:指针类型 保存 首地址 与 空间大小
// 指针要素:指针存储的值==内存地址,指针自己也有内存地址(因为任何变量都有 自己的内存地址)【这个是指针 最绕的环节,很多开发者 就因为这个不理解 而晕菜】
int * pInt2 = (int *) pChar; // 是不是可以这样想?pChar记录首地址,有了首地址,啥事都可以做,可以通过首地址 还原之前的空间大小
printf("*pInt2存储的值的值:%d\n", *pInt2);
// 既然 指针存放的是地址,为啥要用二级指针存放一级指针的地址,不用一级指针存放另一个一级指针的地址
// 答:语法不支持
// 规律 规则:
int number1 = 100;
int * pnumb1 = &number1;
int ** pnumb2 = &pnumb1; // &取出pnumb1自己的内存地址 的 值 == number1的内存地址
int *** pnumb3 = &pnumb2;
}
函数指针
*定义函数指针:返回值(名称)(参数类型1,参数类型2...)
无论 函数指针 还是 普通指针 本质都是 指针
int add(int num1, int num2){
printf("num1 + num2 = %d\n", num1+num2);
return num1+num2;
}
int mins(int num1, int num2){
printf("num1 - num2 = %d\n", num1-num2);
return num1-num2;
}
/**
* 定义函数指针:返回值(*名称)(参数类型1,参数类型2...)
* @param method 函数指针别名,
* int 表示函数的返回值类型
* (int,int) 表示函数的形参 的 参数类型
* @param num1
* @param num2
* @return
*/
int methodPointer(int (*method)(int,int), int num1, int num2){
printf("method address = %p, sizeof = %d, sizeof int = %d\n", method, sizeof(method), sizeof(int));
return method(num1, num2);
}
/**
* 数组和函数变量都是表示地址值,array == &array,method == &method
*/
void useMethodPointer(){
// 方法一:
int ret = methodPointer(add, 10, 10);
printf("method add ret = %d\n", ret);
ret = methodPointer(&mins, 100, 10);
printf("method mins ret = %d\n", ret);
//方法二:
// int (*metName)(int, int) = add;//直接把add函数地址赋值给 *metName指针;(相当于 int i = 10)
int (*method)(int,int);
method = &add;
methodPointer(method, 60, 60);
}
指针数组 与 数组指针
前提:符号优先级:() > [] > *
函数声明的**()** 与 数组声明的**[]** 优先级相同
如果优先级相同:从左往右 依次读取 作为优先级顺序
int * p_arr[10]; // 指针数组,表示数组p_arr 能存放 10个指针
int (* arr_p) [10]; // 数组指针,表示指针arr_p 指向的是 int[10] 的实体数组
无论 数组指针 还是 普通指针 本质都是 指针,以下为 指针数组 与 数组指针 实战代码:
// 指针数组 与 数组指针
void test_pointer_arr(){
// 元素
int arr2[5][10] = {
{1,2,3,4,5,6,7,8,9},
{11,22,33,44,55,66,77,88,99},
{111,222,333,444,555,666,777,888,999},
{1111,2222,3333,4444,5555,6666,7777,8888,9999},
{11111,22222,33333,44444,55555,66666,77777,88888,99999}
};
printf("%d\n", *( *(arr2+2)+5 ) );
int * pIndex2 = (int*)(arr2 + 2);
printf("%d\n", *pIndex2);
// 符号优先级:() > [] > *
// TODO 指针 数组: 指针为类型,数组为实体
int * pointArr[5];
for (int i = 0; i < 5; ++i) {
*(pointArr+i) = arr2[i];
}
for (int i = 0; i < 5; ++i) {
printf("%d, %d, %d\n", (*(pointArr+i))[0],*( *(pointArr+i) + 1 ),(*(pointArr+i))[2]);
}
printf("\n");
int arr[10] = {0,1,2,3,4,5,6,7,8,9};
// TODO 数组 指针: 数组为类型, 指针为实体
// 类型:int [10] 实体:*pa(pa指针)
int (*pa) [10]; // *pi一定要加括号否则会被认为 指针数组
pa = &arr; // 数组 指针pa 指向一维数组 的地址
printf("%d,%d,%d\n", *(*pa), *( *pa + 1 ), *(*pa+2) ); // *pa: 取得arr的地址; *(*pa): 取 arr元素值
int (*pda) [10] = arr2;
printf("pda元素的地址:%u,%u,%u,%u,%u\n", pda,pda+1,pda+2,pda+3,pda+4);
printf("pda元素首个值:%d,%d,%d,%d,%d\n", *(*pda),*(*(pda+1)),*(*(pda+2)),*(*(pda+3)),*(*(pda+4)));
// 无论 数组指针 还是 普通指针 本质都是 指针
// 一维数组arr:10*sizeof(int)=10*4=40,指针pda=8(64位平台)或者 4(32位平台),二维数组arr2: 5*(10*sizeof(int))=5*(10*4)=200
printf("sizeof(arr)=%d, sizeof(pda)=%d, sizeof(arr2)=%d\n", sizeof(arr), sizeof(pda), sizeof(arr2));
}
目标类型的推导
为什么要使用符号优先级呢?
答:C/C++编译器设计规范: 声明 与 使用 统一化 的设计
前提:符号优先级:() > [] > *
函数声明的**()** 与 数组声明的**[]** 优先级相同
如果优先级相同:从左往右 依次读取 作为优先级顺序
按照符号优先级 () > [] > * 就能推导出所有指针的类型。看下图:
void test(){
// 元素类型 数组名 [元素个数][元素个数]...
int arr [5] [10];
// 声明指针
// 目标类型 * 指针名
int * pInt;
int* * ppInt; // 目标类型为 指针 的 指针
// 按照符号优先级 () > [] > * 就能推导出所有指针的类型
//C/C++编译器设计规范: 声明 与 使用 统一化 的设计
int * (*id)[2]; // 声明 ,指针数组指针,指针 指向的数组 里面存放元素为指针
int n = * (*id)[2]; // 实现
int * pInt; // 指针
int** ppInt; // 指针的指针
int * arr[10]; // [] > * 指针数组
int (*arrp) [10]; // () > [] > * 数组指针
int * (*id)[2]; // () > [] > * 指针数组指针
int * pfun(char *, double); // []“函数的()相当与[]的优先级” > * 指针函数
int (*funp)(char *, double); // () > [] > * 函数指针
int (*funparr[10])(char *, double); // () > [] > * 函数指针数组
int *(*funparr1[10])(char *, double); // 指针函数指针数组
// 从以上可得结论:名字里越往后的优先级越高,例如:指针“*” 数组“[]” 指针“(*)”,
}