目录
指针的基本概念
指针是一个值为内存地址的变量(或数据对象)
地址运算符:&
当&后跟一个变量名是,&给出它的地址。
p = &q;
对于该代码而言,其解释是p“指向”q。这样p里面就是q的地址
p是变量(即可修改的左值),&q是常量(右值)。
间接运算符:*
在创建指针变量之前,需要先对指针变量的类型进行声明,这就需要用到间接运算符:*。
间接运算符*可以找出储存在p中的值
p = &q;
r = *p;
两行代码得到的就是下面的结果
r = q;
指针的声明
指针所需要指向变量的类型有多种,此外不同的变量类型占用了不同的储存空间。
我们也需要知道在指定地址上储存的数据类型是什么。
char * p;//p是指向char类型变量的指针
int * p1, *p2;//p1,p2是指向int类型变量的指针
char,int等类型说明符表示指针所指向对象的类型
*表示声明的变量(p,p1,p2)是个指针
char * p表示p是一个指针,*p是char类型
对于p本身,它的类型是“指向char类型的指针”
指针变量的大小取决于地址的大小
32位平台下地址是32个bit位(即4个字节)
64位平台下地址是64个bit位(即8个字节)
指针类型的意义
1.决定指针的±整数的步长
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;
printf("pa = %p\n", pa);
printf("pa+1 = %p\n", pa+1);
printf("pc = %p\n", pc);
printf("pc+1 = %p\n", pc+1);
//pa和pc地址相同pa与pa+1的地址相差4个字节,但是pc与pc+1的地址相差1个字节,因为一个是int*一个是char*
// +1 跳过1*sizeof(int) 个字节
return 0;
}
如:
int * p p+1->4
char *p p+1->1
double * p p+1->8
2.决定指针的权限
即决定指针进行解引用操作符的时候访问几个字节
int main()
{
int a = 0x11223344;//十六进制0x
int* pa = &a;
*pa = 0;
//这个会改变a的地址对应的值为00 00 00 00(设置为4列)因为是四个字节 11 22 33 44各一个
return 0;
}
int main()
{
int a = 0x11223344;
char* pa = &a;//改成x86 char*和int都是四个字节
*pa = 0;
//这个会改变a的地址对应的值为00 33 22 11(设置为4列)
//说明char* 会从这个位置向后访问一个位置
//以此类推,short*会向后访问两个位置
return 0;
}
对于void*指针的一些认识
首先
char* 指向字符的指针
short* 指向短整型的指针
int* 指向整型的指针
float* 指向浮点型的指针
void* 无具体类型的指针(泛型指针)
//void* 不能直接进行+-整数的操作
//void* p = &a;
//*p = 20;这种不可以,因为不知道void*要访问多少个字节
//p = p + 1也不可以
int main()
{
int a = 0;
float f = 0.0f;
//char* p = &a;//int* 编译器会报警告,两边内容不兼容
void* p = &a;
p = &f;
//这两个都不会报错 如果当你不知道它是什么类型,就可以写void*
return 0;
}
//void* 用来接收不同类型的地址
const修饰变量
通过改变const修饰的变量的地址来改变其值
我们知道,const修饰的,在本质上还是变量(但是在C++中就成为常量)
可以用数组arr[a]来验证(数组初始化需要常量),会报错
int main()
{
const int a = 10;
//a=20;//err
int* p = &a;
*p = 0;
printf("a = %d\n", a);//这样可以修改
//const相当于把门锁上 const只是从语法层面限制不让修改a
//int* p翻墙 实际编程上不让这样 因为使用const就是想不让修改a
return 0;
}
1.const放*前
不可以修改*p
int main()
{
int a = 10;//&a--0x0012ff40
int b = 10;//&b--0x0012ff44
int const* p = &a;
//p = &b;//ok
//*p = 100;//不行
return 0;
}
2.const放*后
不可以修改p
const 限制的是指针变量本身,指针变量不能再指向其它变量
但是可以通过指针变量,修改指针变量指向的内容
int main()
{
int a = 10;//&a--0x0012ff40
int b = 10;//&b--0x0012ff44
int* const p = &a;
//p = &b;//这样不行 const限制了p
//但是可以改 *p
return 0;
}
指针作为函数参数的用法
#include<stdio.h>
void swap(int * x, int * y);
int main()
{
int x = 10, y = 20;
printf("before swap x:%d, y:%d\n", x, y);
swap(&x, &y);
printf("after swap x:%d, y:%d\n", x, y);
return 0;
}
void swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
利用指针来输出数组
原理:产生每个元素的地址,然后输出
*p + i也输出数组是因为第一个元素0与之后的都相差i!!!
int main()
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int* p = &arr[0];
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//第一个元素地址 p 第二个元素地址 p+1以此类推
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
//上面得出,每个元素地址位p+i
}
}
归根结底是因为 数组是连续存储的空间
指针之间的比较
指针 + 整数 == 指针 (指针 + 整数 是跳过n个元素)
推得 指针 - 指针 = 整数 (绝对值 得到的是两个指针之间元素的个数)
地址 + 地址 没什么意义
int main()
{
int arr[10] = { 0 };
printf("%zd\n", &arr[9] - &arr[0]);
printf("%zd\n", &arr[0] - &arr[9]);
}
野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
1.指针所指向的空间被释放
//空间释放了 出了test(),a的地址就被释放
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
2.指针越界访问
#include <stdio.h>
int main()
{
int ch[10]={0];
int * p = ch;
for(int i = 0; i < 12; i++)
{
*p++=i;
}
return 0;
}
3.指针未定义
#include <stdio.h>
int main()
{
int* p;//p未给初始值,默认是随机值
*p=10;
return 0;
}
NULL是一个标识符常量,它的值为0,0也是个地址,但这个地址无法使用,读写会报错
明确知道指针应该指向哪里,就给初始化一个明确的地址
如果现在还不知道应该指向哪里,那就初始化NULL
断言(assert)
断言(assertion)是一种在编程中用来检查程序在运行时是否满足特定条件的方法。断言通常用于在代码中插入一些检查点,以确保程序的正确性。如果断言的条件不满足,程序将抛出一个异常或错误消息。
在使用前需要引入头文件 assert.h
#define NEDBUG //使用了NEDBUG则会全局取消断言assert的使用
#include<assert.h>
//const是为了防止修改arr数组
//size_t是为了返回无符号的整型
size_t my_strlen(const char* str)
{
//使用 *str依次指数组中每个元素 count统计
size_t count = 0;
assert(str != NULL);
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
//strlen - 求字符串长度,统计的是字符串中、0之前的字符个数
char arr[] = "a bcdef";
//[a b c d e f \0]
int len = my_strlen(arr);//数组名arr是数组首元素地址 arr == &arr[0]
printf("%d\n", len);
return 0;
}
指针在数组中的使用
数组名的两个特例
1.sizeof(数组名)这里表示的不是首元素地址,而是整个数组,计算的是整个数组的大小,单位是字节
2.&数组名 这里数组名表示整个数组,取出的是整个数组的地址
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[11] = { 0 };
printf("arr = %p\n", arr);
printf("arr + 1 = %p\n", arr + 1);
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr = %p\n", &arr);
printf("&arr + 1 = %p\n", &arr + 1);
return 0;
}
//由此可以得出 arr = &arr[0]
//数组的地址和首元素的地址,从值的角度上看两者相等,但是区别在于一个是数组的地址,一个是数组首元素地址
//如何要看区别可以各给+1
//首元素指针+1跟类型有关 int*加4个字节
//数组+1与原来差44,因为数组有11个元素为int*类型 11 * 4 = 44 验证的话可以把数组元素个数改为10等
为什么访问数组可以使用指针
1.数组在内存中是连续存放的
2.指针的+-整数运算方便我们获得每一个元素的地址
#define _CRT_SECURE_NO_WARNINGS 1
#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++)
{
//输入1个值
scanf("%d", p + i);//p+i == &arr[i] == arr + i
//gets是用于输入字符串的
}
for (i = 0; i < sz; i++)
{
//输入1个值
printf("%d ", *(p + i));//arr[i] == *(arr + i) *(p + i) == p[i]
//gets是用于输入字符串的
}
return 0;
}
这样我们就可以得到
数组就是数组,是一块连续的空间(数组的大小与数组元素个数和元素类型都有关系)
指针(变量)就是指针(变量),是一个变量(4/8个字节
数组名是地址,是首元素的地址
可以用指针来访问数组
数组传参
以下程序印证了
1.数组传参的本质是传递了数组首元素的地址 形参访问的数组和实参访问的数组是一个
2.形参的数组是不会单独再创建数组空间的,所以形参的数组是可以省略数组大小的。
如void Print(int arr[10])可以写成void Print(int arr[])
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//数组传参的时候,形参是可以写成数组的形式的
//但本质上还是指针变量
void Print(int arr[10])//这里的int arr[10]相当于int* arr 本质上还是一个指针变量
{
int sz = sizeof(arr) / sizeof(arr[0]);//得不到元素个数
//这里的sizeof(arr)就是求一个指针变量的大小(int* ) 4
//4 / 4 == 1 故只循环1次
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
Print(arr);//这里不是两个例外中的,arr是首元素的地址
//应该用指针接受 int* p 而不是用 int arr[10]
//sizeof(arr) / sizeof(arr[0])
return 0;
}
//该程序执行得到的结果为 1 2
二级指针
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int a = 10;
int* p = &a;//p是一级指针 指针变量也是变量 p也有地址(&p)
int** pp = &p;//pp就是二级指针
//还是分开理解
//int* * 后面的*说明pp是指针变量 int*说明pp指向的p的类型是int*
//int* * 是pp的类型
//p+1 因为p指向int类型的a 跳过4个字节
//pp+1 因为pp指向int*类型的p 跳过4/8个字节(int*等指针变量的大小与 电脑32位/64位有关)
return 0;
}
指针数组
指针数组模拟二维数组的形式
是储存指针的数组
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr1[] = { 1, 2, 3, 4, 5 };
int arr2[] = { 2, 3, 4, 5, 6 };
int arr3[] = { 3, 4, 5, 6, 7 };
int* arr[] = { arr1, arr2, arr3 };
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
printf("%d ", arr[i][j]);
//这个虽然是二维数组的形式
//arr[i][j] == *(*(arr+i)+j) 这俩等价
//*(arr1 + j) == arr1[j]
// int (*a)[10]也是二维指针
}
printf("\n");
}
}
一个有关const的应用
相同的常量字符串,没必要保存2份,因为常量字符串不能被修改,所以共用一份,节省空间
arr1 arr2 是两个独立的空间
调试中 arr3 arr4地址相同
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
char arr1[] = "hello ffy.";
char arr2[] = "hello ffy.";
const char* arr3 = "hello ffy.";//一开始写成const char arr3[] 是错误的写法
const char* arr4 = "hello ffy.";
if (arr1 == arr2)
printf("arr1 and arr2 are same\n");
else
printf("arr1 and arr2 are not same\n");
if (arr3 == arr4)//两个的首元素地址
printf("arr3 and arr4 are same\n");
else
printf("arr3 and arr4 are not same\n");
printf("%p\n", &arr1);
printf("%p\n", &arr2);
printf("%p\n", arr3);
printf("%p\n", arr4);
return 0;
}
数组指针变量
存放的是数组的地址
例1
int main()
{
int a = 10;
int* pa = &a;
char ch = 'w';
char* pc = &ch;
int arr[10] = { 0 };
int (*p)[10] = &arr;//取出的是数组的地址
//arr -- 数组首元素的地址
int* p1[10];//p1是指针数组(重点是数组) 指针存放在数组里面
//*p1[10] p1首先与 [ 结合,就成为了数组 方块的优先级比*高
int(*p2)[10];//旁边放了*,p2是指针变量,指向十个类型为int的数组
}
例2
int main()
{
int arr[10] = { 0 };
//指针类型决定+1地址+?
int* p1 = arr;
// int* == int*
printf("%p\n", p1);
printf("%p\n", p1 + 1);//p1+1 地址+4
int(*p2)[10] = &arr;//[10]不能省略
//int(*)[10] == int(*)[10] 数组指针类型
printf("%p\n", p2);
printf("%p\n", p2 + 1);//p2+1 地址+40 == 4*10
return 0;
}
数组指针的用法 -- 二维数组传参
//一般的写法
//形参写成数组
void print(int arr[3][5], int r, int c)
{
int i = 0;
for (i = 0; i < r; i++)
{
int j = 0;
for (j = 0; j < c; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1, 2, 3, 4, 5},{2, 3, 4, 5, 6}, {3, 4, 5, 6, 7} };
//一维数组,数组名是首元素的地址
//二维数组,也是
//两个表示整个数组的例外同样
//1.&数组名
//2.sizeof(数组名)
//说明在函数中,也可以传递地址!!!
print(arr, 3, 5);
return 0;
}
//形参写成指针的形式
//可以把二维数组一行 理解为一维数组
//首元素的地址(arr == &arr[0]) 就是第一行的地址
//第一行的地址就是一维数组的地址 类型是数组指针 (把他看成一维数组 一维数组是二维数组里的元素 是为了理解首元素到底是谁)
void print(int (*arr)[5], int r, int c);
//int (*arr)[5] 这个就是指向一行的指针 int(*)[5]就是 二维数组中的一维数组的类型
//不能用int ** arr
//二级指针变量是用来存放一级指针变量的地址
{
int i = 0;
for (i = 0; i < r; i++)
{
int j = 0;
for (j = 0; j < c; j++)
{
//*(*(arr + i)+j) == arr[i][j] 因为指针的运算也会转换成数组的运算
//arr+i == &arr[i] *(arr + i) == arr[i] 对arr+i解引用 但它还是个地址
//arr[i]+j 找到第i行第j个元素的地址 &arr[i][j]
printf("%d ", *(*(arr + i) + j));
}
//printf("%p ", *(arr + i));
//printf("%p ", (arr + i));
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1, 2, 3, 4, 5},{2, 3, 4, 5, 6}, {3, 4, 5, 6, 7} };
//二维数组,地址实际上是连续存放
//第一行是arr[0] 第二行arr[1]
//第一行是int [5] []说明是数组 五个整形元素的数组
print(arr, 3, 5);
return 0;
}
二维数组传参的本质 -- 传递的是第一行这个一维数组的地址
函数指针
函数指针简单介绍
int (*pf3)(int x, int y)
int是pf3指向函数的返回类型
*pf3是函数指针变量名
int x, int y是pf3指向函数的参数类型和个数交代
//函数的地址
//函数名
//&函数名
//都是函数的地址,没有区别!!!
//对于函数名来说,前面的&和*都会被忽略,所以 函数名 前面加不加取地址都没区别。
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int, int) = Add;//pf是专门用来存放函数地址的,pf就是函数指针变量 (*pf)说明是指针
printf("%p\n", &Add);
printf("%p\n", Add);
//去掉名字剩下类型int (*)(int ,int)
printf("%p\n", *pf);
printf("%p\n", pf);
int c = Add(2, 3);//函数名调用
printf("%d\n", c);
int d = (*pf)(3, 4);//函数指针调用
printf("%d\n", d);//pf存放函数的地址
int e = pf(4, 5);//*其实可以不写
printf("%d\n", e);
if (*pf == &Add)//pf == *pf == Add ==&Add!!!
printf("dui");
}
例:声明一个指向含有10个元素的数组的指针,其中每个元素是一个函数指针,该函数的返回值是int,参数是int*
指向函数指针数组的指针
int (*(*p)[10])(int *)
(*p) 是 指针
(*p)[10] 是 指向含有10个元素的数组的指针
int (* )(int *) 是 函数指针
int 是 返回值
int* 是 参数
typedef的相关内容
typedef unsigned int u_int;
//指针类型的定义
typedef int* pint_t;
//数组指针定义
typedef int(*)[5] pf_t;//这样不行
typedef int(*parr_t)[5] ;//得这样写 parr_t == int(*)[5]
//对函数指针的定义
typedef void (*)(int) pf_t;//跟上面类似
typedef void (*pf_t)(int);// pf_t == void (*)(int)
//上面的那个就可以写成 pf_t signal(int, pf_t)
typedef int* ptr_t;
#define PTR_T int*;
ptr_t p1, p2;//p1 p2都是指针变量
PTR_T p3, p4;//p3是指针变量 p4是整型变量
//对于define 编译器是替换 相当于int* p3,p4
函数指针数组以及回调函数
函数指针数组
int (* a[10])(int *)
一个有10个指针的数组:*a[10];
该指针指向一个函数:(*a[10])();
该函数有一个整形参数:(*a[10])(int);
并返回一个整型数: int (*a[10])(int)
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
int main()
{
int (*pf1)(int, int) = Add;//pf1是函数指针变量
int (*pfarr[4])(int, int) = { Add, Sub, Mul, Div};//pfarr是函数指针数组
int i = 0;
for (; i < 4; i++)
{
//不用解引用,153行
int r = pfarr[i](8, 4);
printf("%d\n", r);
}
return 0;
}
回调函数
回调函数是指将一个函数作为参数传递给另一个函数,并在特定事件发生时被调用。
正常的选择语句
//第一种方法 -- 正常的switch
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
void menu()
{
printf("***************************\n");
printf("***** 1.add 2.sub *****\n");
printf("***** 3.mul 4.div *****\n");
printf("***** 0.exit *****\n");
printf("***************************\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int z = 0;
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
z = Add(x, y);
printf("%d\n", z);
break;
case 2:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
z = Sub(x, y);
printf("%d\n", z);
break;
case 3:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
z = Mul(x, y);
printf("%d\n", z);
break;
case 4:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
z = Div(x, y);
printf("%d\n", z);
break;
case 0:
printf("退出计算机\b");
break;
default:
printf("输入有误,请重新输入\n");
break;
}
} while (input);
}
其中我们会发现,case1,2,3,4中的内容都大差不大,
为此我们可以简化
先使用函数指针的数组
(注意需要手动添加数组arr[0]以便队名菜单)
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
void menu()
{
printf("***************************\n");
printf("***** 1.add 2.sub *****\n");
printf("***** 3.mul 4.div *****\n");
printf("***** 0.exit *****\n");
printf("***************************\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int z = 0;
//函数指针的数组创建
//先创建函数指针 (*pfArr[])
//添加返回类型 和 希望指向的参数 int(*pfArr[4])(int x, int y)
//int(*pfArr[4])(int x, int y) = { Add, Sub, Mul, Div };
0 1 2 3 不匹配
int(*pfArr[5])(int x, int y) = { 0, Add, Sub, Mul, Div };
// 0 1 2 3 4
do
{
menu();
printf("请选择:");
scanf("%d", &input);
if (input >= 1 && input <= 4)
{
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
z = pfArr[input](x, y);
printf("%d\n", z);
}
else if (input == 0)
{
printf("退出计算器\n");
}
else
printf("输入错误,请重新输入\n");
} while (input);
}
使用回调函数的写法
将Add, Sub, Mul, Div这些回调函数通过主函数中的选择语句来传递给*pf
int(*pf)(int, int)
{
int x = 0;
int y = 0;
int z = 0;
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
z = pf(x, y);//pf == Add 等传递过来的
printf("%d\n", z);
}
具体代码如下
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
void calc(int(*pf)(int, int))
//把选择后的Add等传递给 *pf 这里写pf也没问题,两者等价
//int(*pf)(int, int)
{
int x = 0;
int y = 0;
int z = 0;
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
z = pf(x, y);//pf == Add 等传递过来的
printf("%d\n", z);
}
void menu()
{
printf("***************************\n");
printf("***** 1.add 2.sub *****\n");
printf("***** 3.mul 4.div *****\n");
printf("***** 0.exit *****\n");
printf("***************************\n");
}
//主调函数
int main()
{
int input = 0;
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
calc(Add);
break;
case 2:
calc(Sub);
break;
case 3:
calc(Mul);
break;
case 4:
calc(Div);
break;
case 0:
break;
default:
printf("输入有误,请重新输入\n");
break;
}
} while (input);
}
// 1.数组传参的本质是传递了数组首元素的地址 形参访问的数组和实参访问的数组是一个
// 2.形参的数组是不会单独再创建数组空间的,所以形参的数组是可以省略数组大小的。
// 如void Print(int arr[10])可以写成void Print(int arr[])
// 1.数组传参的本质是传递了数组首元素的地址 形参访问的数组和实参访问的数组是一个
// 2.形参的数组是不会单独再创建数组空间的,所以形参的数组是可以省略数组大小的。
// 如void Print(int arr[10])可以写成void Print(int arr[])