首先明确一个概念,令人闻风丧胆的指针就是一个内存的地址
概念
这个时候就可以用到vs的调试的查看内存的功能
这个时候的&a就可以查看a的起始地址,而&就是取地址操作符
如果我们要查看一个数的地址的时候我们可以这样打印
#include <stdio.h>
int main()
{
int a = 10;
printf("%p\n", &a);
return 0;
}
我们也可以用一个变量来存储地址
*就是解引用操作符,可以拿到这个地址里的数值
理解
接下来我们来深入了解指针
我们来思考一下这个会输出什么
答案是输出的都是8
因为 无论什么类型的地址都是64为0/1组成的二进制序列,所以长度都是一样的(如果是x89环境的话就是32为0/1组成的二进制序列)(因为sizeof得出来的是字节,8个bite位等于一个字节)
下面验证一下我所说的
以下是指针的入门操作
下面是一个错误示范,char*可以放跟int*一样的地址,但是只可以改变一个字节
总结一下
我们再来编写一段代码加深理解指针加减整数的问题
对于不同的指针类型,与整数相加减的时候跳过的字节也是不一样的,这样才能确保访问下一个地址的时候能够正确访问,加一减一的时候跳过的是字节类型的指向值
//
操作
那么指针类型的这些特点,我们要怎么使用呢?
我们也从最简单的数组入手
#include <stdio.h>
int main()
{
int a[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &a[0];
for (int i = 0; i < 10; i++)
{
printf("%d ", *p);
p = p + 1;
}
return 0;
}
///
#include <stdio.h>
int main()
{
int a[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &a[0];
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p+i));
}
return 0;
}
///
#include <stdio.h>
int main()
{
int a[] = { 1,2,3,4,5,6,7,8,9,10 };
//int* p = &a[0];
for (int i = 0; i < 10; i++)
{
printf("%d ", *(a+i));
}
return 0;
}
注意点 关于数组理解
这样应用的就是加1跳过四个字节,三种写法都是可以的,加深我们对理解(我们不难发现a+i其实就是&a[i]),然后我们可以知道&a[0]跟a 这两种表达都是指向数组首元素的地址
const修饰
然后我们学一下const是如何修饰指针的,const首先是修饰变量的,使得这个变量不得修改
但是先别高兴的太早
#include <stdio.h>
int main()
{
const int n = 100;
int* p = &n;
*p = 20;
printf("%d\n", n);
return 0;
}
我们给它用指针一顿操作,诶,这下改变了,这种事情是不合理的,它违背了我们的初衷,那我们也要进行相应的修改,我们来用const修饰指针
#include <stdio.h>
int main()
{
const int n = 100;
const int* const p = &n;
*p = 20;
p = &m;
printf("%d\n", n);
return 0;
}
注意const放在*的前面是限制指针指向的内容,意思是不能通过指针修改指针指向的内容,但是可以修改指针的指向
const放在*的右边限制的是指针变量本身,意思是不能修改指针变量的指向,但是可以修改指针变量指向的内容
/
下面来讲指针的基本运算
还是指针加减整数,这次我们来看一下字符串,也是一个简单的例子
#include <stdio.h>
int main()
{
char arr[] = "abcdef";
char* pc = &arr[0];
while (*pc != '\0')
{
printf("%c", *pc);
pc++;
}
return 0;
}
接下来讲一下指针减去指针,前提是两个指针指向同一个空间,否则会没有意义,结果是什么全部取决与编译器
这样我们会得到指针与指针之间元素的个数 (绝对值,避免小地址减去大地址)
然后我们来巩固一下指针的用法
我们来用指针自己模拟一个strlen函数
函数传过去的就是数组首元素的地址
为了呼应上文的指针减去指针,这里再提供一种写法
再来一个更安全的写法,自己体会一下
#include <stdio.h>
#include <string.h>
int mstrlen(const char*arr)
{
char* start = arr;
while (*arr != '\0')
{
arr++;
}
return arr- start;
}
int main()
{
char arr[] = "abcdef";
int len = mstrlen(arr);
printf("%d\n", len);
return 0;
}
//
然后是指针的关系运算
我们利用这个关系运算来打印一下数组
/
野指针
我们初始化指针的时候一定不能空着,否则指针就会变成野指针,很危险!要么我们就给它设置为空指针
要是数组的指针越界也会变成野指针
还有就是函数返回的时候,也要特别注意,举一个例子
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int* p = test();
return 0;
}
看起来好像没问题,但是从函数出来的时候n已经被销毁了,所以*p就变成野指针了,也就是这个指针没有意义了,失去有效性了,也就是指针指向的空间释放,避免返回局部变量的地址
那既然野指针那么危险,我们也要利用一些手段来注意预防,最大限度避免野指针的出现,造成程序的不稳定性
assert断⾔
⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。
这里就会报错
虽然if语句也可以做到这种效果,但是assert在报错的时候我们可以知道是哪一行错了
在我们不想使用assert的时候,我们在前面加上一个定义就可以关闭assert,也是很便捷的
指针的使⽤和传址调⽤
这个传址调用跟传值调用是不一样的
来用一个简单的交换函数来测试一下
这个是传值调用
打印出来的结果并不符合我们的交换想法
然后我们来利用一下传址调用
我们就达到目的了,是不是很疑惑
这种现象的原因是 传值调用的时候,实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。所以Swap是失败的了。那么传址的时候调⽤Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,直接 将a和b的值交换了。那么就可以使⽤指针了,在main函数中将a和b的地址传递给Swap函数,Swap函数⾥边通过地址间接的操作main函数中的a和b就好了。
总结一下,当要改变主函数里的值的时候就用传址,要不然就传值就可以了
系统理解数组与指针
我们要知道&arr 代表整个数组,在下面的代码中可以自己试一下,+1就可以看出arr &arr[0]和&arr的区别
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", sizeof(arr));
printf("%p\n", &arr[0]);
printf("%p\n", arr);
printf("%p\n", &arr);
printf("%p\n", &arr + 1);
printf("%p\n", arr + 1);
printf("%p\n", &arr[0] + 1);
return 0;
}
再来一个代码加深理解,可以复制到本地ide试试
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int * p = arr;
int i = 0;
for (i = 0; i < sz; i++)
{
scanf("%d", p);
p++;
}
p = &arr[0];
for (i=0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5 };
int sz = sizeof(arr) / sizeof(arr[0]);
int * p = arr;
int i = 0;
/*for (i = 0; i < sz; i++)
{
scanf("%d", p);
p++;
}
p = &arr[0];*/
for (i=0; i < sz; i++)
{
printf("%d ", p[i]);//首元素地址
}
return 0;
}
看起来很神奇的写法背后底层逻辑其实是一样的,我们要清楚,但是平时还是要挑最清晰的写法来用
还再强调一下arr[i]==*(arr+i),还是要多多理解这些基础
一维数组传参的本质
我们用函数来打印一下一维数组
但是结果貌似错了
这是因为我们传过去的是数组首元素的地址,所以sz是错的 ,我们在主函数算出sz再传进去我们就可以正确打印了
我们用指针实现一下冒泡排序
#include <stdio.h>
void pao_sort(int* arr,int sz)
{
for (int i = 0; i < sz - 1; i++)//交换趟数
{
for (int j = 0; j < sz - 1 - i; j++)//交换
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[] = { 15,2,3,4,84,6,7,66,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
pao_sort(arr,sz);
for (int i = 0; i < sz; i++)
{
printf("%d ", i[arr]);
}
return 0;
}
二级指针
我们理解一下,二级指针存放一级指针,三级指针存放二级指针,所以这些并没有什么特别的
贴上代码,可以根据这个以此类推
#include <stdio.h>
int main()
{
int a = 10;
int *p = &a;
int* * pp = &p;//pp旁边的星表示pp是一个指针,再旁边的int*表示的是p的类型
printf("%d",**pp);二级指针要解引用两次
int** * ppp = &pp;//三级指针,促进理解
return 0;
}
强调一下,pp旁边的星表示pp是一个指针,再旁边的int*表示的是p的类型 ,这对后面的数组指针理解有帮助
指针数组
存放指针的数组
来一个简单的代码实现
#include <stdio.h>
int main()
{
int a = 1;
int b = 2;
int c = 3;
int d = 4;
int* par[4] = { &a,&b,&c,&d };
for (int i = 0; i < 4; i++)
{
printf("%d ", *(i[par]));//也可以**(par+i)
}
return 0;
}
我们用新学的知识来模拟一下二维数组
#include <stdio.h>
int main()
{
int a1[] = { 1,2,3 };
int a2[] = { 4,5,6 };
int a3[] = { 7,8,9 };
int* a[3] = { a1,a2,a3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
printf("%d ", *(*(a + i) + j));//也可以看成二维数组a[i][j] 都是两个**的意思
}
printf("\n");
}
return 0;
}
字符指针变量
这个代码加深引出常量字符串,加深我们对指针的理解
#include <stdio.h>
int main()
{
//char*p= "abcdef";//不是将全部存进p中,而是将首字符a的地址存进p中
const char* p = "abcdef";
printf("%c\n", *p);//
//"abcdef"这是一个常量字符串,存放在常量区,不可修改,根据前面知识,我们加上const确保安全
*p='w';//这样是不行的,因为常量区不能修改,加上const就不能运行了,更加安全
printf("%s\n",p);//这样是可以的,因为只是打印,不是修改
return 0;
}
在理解一下
#include <stdio.h>
int main()
{
const char* a1 = "hello";//只保存一份
const char* a2 = "hello";
if (a1 == a2)
{
printf("same");
}
return 0;
}
数组指针变量
数组指针变量是⽤来存放数组地址的,那获得数组的地址就是我们之前学习的 &数组名
下面给出代码例子加强理解
#include <stdio.h>
int main()
{
int arr[6] = { 1,2,3,4,5,6 };
int (*ptr)[6] = &arr;//ptr是数组指针 注意arr跟&arr的区别
char* ch[8];
char* (*str)[8] = &ch;
return 0;
}
我们用这个来打印出数组里的元素来加深理解
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int (*p)[10] = &arr;
for (int i = 0; i < 10; i++)
{
printf("%d ", (*p)[i]);//因为p=&arr,加一个*来抵消
}
return 0;
}
在数组指针的基础上我们来拓展一下
⼆维数组传参的本质
#include <stdio.h>
void test(int (*arr)[3], int x, int y)
{
for (int i = 0; i < x; i++)
{
for (int j = 0; j < y; j++)
{
printf("%d ", *(*(arr + i) + j));//*(arr + i)[j]也可以
}
printf("\n");
}
}
int main()
{
int arr[][3] = { {1,2,3},{4,5,6},{7,8,9} };
test(arr, 3, 3);
return 0;
}
把(*arr)[3]跟直接传arr是一个意思,我们可以把二维数组看成看成是多个一维数组的合并,至此我们就和上一个知识点穿起来了
总结:⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式
函数指针变量
函数指针变量应该是⽤来存放函数地址的,未来通过地址能够调⽤函数的
这个的写法跟数组指针很类似
#include <stdio.h>
int add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int, int) = &add;//第一个int是返回类型 ()里面的代表函数的参数
int a = 1;
int b = 2;
printf("%d\n",(*pf)(a, b));//这里不加*也可以,但是写了更容易理解,可读性更好
return 0;
}
分享一段很有意思的代码
#include <stdio.h>
int main()
{
(*(void (*)())0)();
return 0;
}
这个代码很考验我们对函数指针的理解,这段代码的意思是调用0地址处的函数,函数没有参数,返回类型是void,要我们一层层抽丝剥茧的去分析
typedef关键字
typedef 是⽤来类型重命名的,可以将复杂的类型,简单化
例如
typedef 是⽤来类型重命名的,可以将复杂的类型,简单化
如果是指针类型也是可以的
typedef int* pt;
⽐如我们有数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那要把名字放到括号里
typedef int(*parr_t)[5];
函数指针类型的重命名也是⼀样的
typedef void(*pfun)(int);//新的类型名必须在*的右边
函数指针数组
那要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,也就是存放函数指针的数组,先给上模板
#include <stdio.h>
int main()
{
int (*def_zu[2])(int, int) = { hanshu1,hanshu2 };
return 0;
}
这个有什么作用呢,我们来实现一个简单计算器来感受一下
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;//输入数
int input = 1;//初始化
int ret = 0;//答案
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
do
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf(" 0:exit \n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出\n");
}
else
{
printf("输⼊有误\n");
}
} while (input);//合理利用0
return 0;
}
这样写的话整体就简洁了很多,节省了很多代码量,可读性也好,要是用switch语句的话就看起来有一些冗余了,要是后来功能更多的话switch语句就越来越长,就看起来非常的不舒服
回调函数
回调函数就是⼀个通过函数指针调⽤的函数
当这个指针被⽤来调⽤其所指向的函数 时,被调⽤的函数就是回调函数。回调函数不是由该函数的实现⽅直接调⽤,⽽是在特定的事件或条 件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应
这个需要我们熟练使用函数指针传参
写一个简单的例子
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
void use_def(int (*p)(int,int))
{
int x, y;
scanf("%d%d", &x, &y);
int ret = p(x, y);
printf("%d\n", ret);
}
int main()
{
int n;
scanf("%d", &n);
switch (n)
{
case 1:
use_def(add);
break;
case 2:
use_def(sub);
break;
}
return 0;
}
为了巩固指针运算,同时加强排序知识,我们来下一个部分
qsort函数的模拟实现
因为qsort大家还没那么清楚,所以我们用冒泡排序的内核来模拟
这个模拟我们要能排序多种类型的变量的数组
#include <stdio.h>
int arr[100100] = { 0 };
int my_compar(const void* a, const void* b)
{
return *(int*)a - *(int*)b;
}
void my_swap(void* p1, void* p2, int size)//只交换其中一个变量,一个个来
{
int i = 0;
for (i = 0; i < size; i++)//不管什么类型,一个个字节交换
{
char tmp = *((char*)p1 + i);
*((char*)p1 + i) = *((char*)p2 + i);
*((char*)p2 + i) = tmp;
}
}
void my_sort(void * base,int n,int size,int (*my_compar)(void*,void*))//冒泡排序逻辑
{
int i = 0;
int j = 0;
for (i = 0; i < n - 1; i++)
{
for (j = 0; j < n - i - 1; j++)
{
if (my_compar((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
{
my_swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
}
}
}
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
scanf("%d", &arr[i]);
}
my_sort(arr,n,sizeof(arr[0]),my_compar);//把多少个字节传过去
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);//打印出有序数组
}
return 0;
}
笔试题解析
我们学会指针就得有解决问题的能力
下面我来解析几道面试题
第一道
这是最基础的指针
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int* ptr = (int*)(&a + 1);
printf("%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
就可以看成数组,输出2和5
第二道
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct Test
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*p = (struct Test*)0x100000;
int main()
{
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
这道题的答案是
就是要明白强制类型转换或者地址加一是什么意思
第三道
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int* p;
p = a[0];
printf("%d", p[0]);
return 0;
}
下面是解析
意思就是只有1 3 5放进去了
然后答案就显而易见是1了
第四道
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
我们把二维数组跟p的关系画一个图
然后自己相减就可以了
注意%p的强制类型转换
第五道
int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int* ptr1 = (int*)(&aa + 1);
int* ptr2 = (int*)(*(aa + 1));
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}
跟前面差不多的,这里会输出10 5,画一个图就很好理解了,整形指针减去1也就是整形数组往后退一个
重点其实还是前面的基础知识,基础牢固才能熟练运用