一、基本知识
1、什么是内存?
- 计算机的存储器
- 大小:通常为4 - 16G
2、什么是内存单元?
- 为了有效率的使用内存,以 1 字节为单位,对内存进行划分
- 大小:1byte
3、什么是地址?
- 为了有效率的使用内存单元
- 内存单元的编号
- 一个地址中存1 byte的数据
4、32位 和 64位 的含义
- 32位 – 32根地址线(物理线) – 通、断电 – 表示1 / 0
- 电信号 --> 数字信息(1 / 0组成的二进制序列)
5、什么是指针?
- 内存单元的编号 - 地址 - 指针
- 指针变量 - 指针(口头表述)
6、什么是指针变量?
- 存放地址
- 存储在指针中的值被当做地址处理
7、指针的大小
- 32位 - 地址由32个 0 / 1 组成二进制序列 - 32 bit - 4byte
- 64位 - 地址由64个 0 / 1 组成二进制序列 - 64 bit - 8byte
8、一个地址中存储1 byte大小的数据
9、为什么把地址称为指针?
- 通过地址可以找到所需的变量
- 地址指向该变量
- 地址形象化的称呼
二、指针类型
1、指针
- char*
- int*
- float*
- double*
···
- * :变量为指针变量
- int : 指针指向的变量的类型为int
2、野指针
- 指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
3、数组指针
- 指向数组的指针 - 存储数组的地址
- int (*变量名)[ 所指向的数组的元素个数 ]
- (*p)说明 p 是指针
- int [ ] 说明指针指向的对象是整型数组
4、函数指针
- 指向函数的指针 - 存储函数的地址
- 函数返回类型 (*变量名)(函数形参类型)
- (*p)说明 p 是指针
- 返回类型 (形参类型)说明指针指向函数
5、问题分析
A、指针类型的意义是什么?
-
指针访问地址中内容权限的大小
-
指针的步长 (走一步走多远)
B、利用数组分析指针类型的意义
三、野指针
- 指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
1、为什么会形成野指针?
-
指针未初始化
-
指针越界访问
-
指针指向的空间释放
指针内存储已释放空间的变量地址
2、如何避免野指针的出现?
-
指针初始化
-
小心指针越界 - 用户无NULL访问权限
-
指针指向的空间(例如局部变量)被释放之后,指针要及时置为 NULL
-
避免返回局部变量的地址
-
指针使用之前检查有效性
3、代码分析
#include<stdio.h>
int main()
{
//1、i的定义在 arr[10] 之前
int i = 0;
int arr[10] = { 0,1,2,3,4,5,6,7,8,9,10 };
//2、局部变量(i和arr)在内存中的栈区存储
//3、栈区内存先使用高地址空间
//4、数组随着下标增长,地址由低向高变化
//5、恰好:arr[12] == i;
//6、arr[12] == i赋值为0,i++循环;
int* p = arr;
for (i = 0;i <= 12; i++)
{
*(p + i) = 0;
printf("Love\n");
}
//7、程序死循环
return 0;
}
i 和 arr[9] 的地址间距大小
- VS2019 - 2个整型
- gcc - 1个整型
- VC6.0 - 0
四、指针的运算
-
指针 ± 整数 指针\pm 整数 指针±整数
-
指针 − 指针 指针 - 指针 指针−指针(指针相加无意义)
-
指针比较大小(关系运算)
C标准规定:
- 允许指针P 与数组元素arr[Vlue]比较
- 不允许指针P 与数组元素arr[-1]比较
五、二级指针
存储指针的地址
- * 间接访问操作符 - 访问地址中存储的数据内容
- 以此类推:n级指针
六、字符指针
1、两种使用方法
#include<stdio.h>
int main()
{
char* ch = "N";
//1、存储字符类型的地址
char* p_1 = &ch;
printf("%c\n", *p);
//2、存储常量字符串首字符地址
const char* p_2 = "love";
//常量字符串 - 不可以修改
//const修饰 *p_2
//无const修饰 - 一般编译器会报错
printf("%s\n", p); //love
printf("%c\n", *p); //l
printf("%c\n", *(p + 1));//o
return 0;
}
2、代码理解
《剑指offer》中的一段代码
#include<stdio.h>
int main()
{
char str_1[] = "love";
char str_2[] = "love";
const char* str_3 = "love";
const char* str_4 = "love";
//不同的数组 - 开辟不同的内存空间
if (str_1 == str_2)
printf("Same\n");
else
printf("Not same\n");
//C/C++会把常量字符串存储到单独的一个内存区域
//str_3 和 str_4 指向地址相同
if (str_3 == str_4)
printf("Same\n");
else
printf("Not same\n");
return 0;
}
七、指针与数组
数组与指针的关系
-
p+i 计算数组 arr[i] 的地址。
-
通过指针访问数组
-
编译器翻译arr[i] --> *(arr + i)
-
指针数组 - 存储地址的数组
-
数组指针 - 指向数组的指针
八、指针数组
存储地址的数组
指针数组的使用
#include<stdio.h>
int main()
{
//指针数组
//不常见的使用
int a = 0;
int b = 0;
int c = 0;
int* arr[3] = { &a, &b, &c };//存放整型指针的数组
int i = 0;
for (i = 0; i < 3; i++)
{
//* 访问地址中存储的数据
printf("%d\n", *arr[i]);
}
return 0;
return 0;
}
指针数组模拟二维数组
#include<stdio.h>
int main()
{
int a[5] = { 1,2,3,4,5 };
int b[] = { 2,3,4,5,6 };
int c[] = { 3,4,5,6,7 };
int* arr[3] = { a, b, c };
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
printf("%d ", *(arr[i] + j));
printf("%d ", arr[i][j]);//写出这样也可以打印
}
printf("\n");
}
return 0;
}
九、数组指针
指向数组的指针 - 存储数组的地址
1、arr 和 & arr
arr : 数组首元素的首字符地址 - 元素的地址
- 类型:指针
&arr:整个数组的首字符地址 - 数组的地址
- 类型:数组指针
2、数组指针
- 指向数组的指针
- int (*变量名)[所指向的数组的元素个数]
- 自定义了指针的访问权限大小 (访问一个数组大小)
- 自定义了指针的步长 (一步走过一个数组)
- 一般用于二维数组
数组指针 和 一维数组
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//存储首元素的地址
int *p = arr;
//存储整个数组的地址
//1、优先级:[] > *
//2、(*p) - 保证*先和p结合 - 说明p是一个指针
//3、[] - 说明指针p指向数组
int(*p)[10] = &arr;
//数组指针访问一维数组元素
int i = 0;
for (i = 0; i < 10; i++)
{
// *p -> arr
// arr -> 首元素地址
//* - 访问地址里存储的数据
// *(*p + i) -> arr[i] <- *(arr + i)
printf("%d ", *(*p + i));
}
return 0;
}
数组指针 和 二维数组
#include<stdio.h>
void print_1(arr[][5], r, 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");
}
}
void print_2(int(*p)[5], r, c)
{
int i = 0;
for (i = 0;i < r; i++)
{
int j = 0;
for (j = 0;j < c; j++)
{
//(*p + i) -> arr[i] -> 每行看作一个一维数组
// arr[i] -> 指针 -> 首元素地址
//访问一维数组arr[i]的元素
// *((*p + i) + j) --> arr[i][j] <-- *(arr[i] + j)
printf("%d ", *(*(p + i) + j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{6,7,8,9,10},{1,3,5,7,9}
}
//二维数组的首元素 - 第一行
//二维数组数组名 - 第一行的地址 - 一维数组的地址- 类型 - 数组指针
print_1(arr, 3, 5);
print_2(arr, 3, 5);
return 0;
}
3、数组 和 指针
十、数组传参
一维数组传参的几种方式
- 形参是数组
- 形参是指针
void test_1(int arr[]){;}
void test_1(int arr[10]){;}
void test_1(int* p){;}
void test_2(int* arr[20]){;}//指针数组 - 传参 -> 指针数组
void test_2(int** p){;}//指针数组 - 传参 - > 二级指针 - 指针的地址
int main()
{
int arr_1[10] = { 0 };
int* arr_2[20];
test_1(arr_1);
test_2(arr_2);
return 0;
}
二维数组传参的几种方式
- 形参是数组
- 形参是指针
void(int arr[][5]){;}//列不可省
void(int arr[3][5]){;}
void(int(*P)[5]){;}//二维数组首元素 - 第一行 -> 数组指针类型
int main()
{
int arr[3][5] = { 0 };
test(arr);
return 0;
}
十一、指针传参
1、指针类型的实参,形参的类型是什么?
#include<stdio.h>
//int* 类型的实参用 int* 接收
void print_1(int* p, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
}
//int** 类型的实参用 int** 接收
void print_2(int** pp, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(*pp + i));
//*pp -> p 访问pp中存储的地址 -> p
//**pp -> arr 访问*pp中存储的地址 -> arr
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);//计算数组元素个数
int* p = arr;
print_1(p, sz);
printf("\n");
int** pp = &p;
print_2(pp, sz);
return 0;
}
2、指针类型的形参,可以接收什么?
void test_1(int* p){;}
void test_2(int** p){;}
int main()
{
int arr[10] = { 0 };
int a = 10;
int* p = arr;
test_1(arr);//1、一维数组
test_1(&a); //2、变量的地址
test_1(p); //3、一级指针
int* parr[10];
int** pp = parr;
test_2(parr); //1、指针数组
test_2(&p); //2、一级指针的地址
test_2(pp); //3、二级指针
return 0;
}
十二、函数指针
指向函数的指针 - 存储函数的地址
1、什么是函数指针?
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int x = 0;
int y = 0;
scanf("%d%d", &x, &y);
int ret = Add(x, y);
//1、函数指针 - 存储函数的地址
//2/优先级:() > *
//3、(*p) - *先和p结合 - 说明p是指针
//4、(int, int) - 表明指针p指向函数
//5、int, int - 表明指向函数的两个参数类型分别是int, int
//6、Add <==> &Add
int(*p)(int, int) = &Add;//只写Add也一样
//7、函数指针调用函数
ret = (*p)(x, y);
//8、(*****p)也是一样的,*是摆设
//因为Add <==> &Add <==> p
ret = p(x, y);
}
2、代码理解
代码1
int main()
{
//《C陷阱与缺陷》中,调用0地址处的函数
(*(void(*)())0)();
//1、void(*)() - 函数指针类型
//2、(void(*)())0 - 强制类型转换
//3、*(void(*)())0 - 访问0地址处函数
//4、(*(void(*)())0)() - 调用0地址处函数
return 0;
}
代码2
int main()
{
//声明函数 - 返回类型是函数指针
void (*signal(int, void(*)(int)))(int);
//1、signal先和()结合 - 说明signal是函数
//2、函数参数类型是int 和 void(*)(int)
//3、void(*)(int) -指向一个参数类型int,返回类型void 的函数
//4、signal是返回类型是void(*)(int)
//5、让人理解的写法 - 语法不支持 - 是错误的
void(*)(int) signal(int, void(*)(int));//c错误的
// 拆分成void(*)(int) 和 signal(int, void(*)(int))
// 语法要求signal(int, void(*)(int))放在*后
//6、类型重定义typedef
//重新定义int(*)(int)类型
//重新定义的类型 - pfun_t - 也要放在*后
typedef int(*pfun_t)(int)
//7、声明函数
pfun_t signal(int, pfun_t);
return 0;
}
十三、函数指针数组
1、什么是函数指针数组
2、函数指针数组的用法
简单的整数计算器
#include<stdio.h>
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(*parr[5])(int, int) = { NULL,Add,Sub,Mul,Div };//使下标与数组中存储的函数地址对应
int input = 0;//选择加减乘除
//要计算的两个数
int x = 0;
int y = 0;
do
{
printf("请选择(0 - 退出):\n");
printf("1 - 加法 2 - 减法\n");
printf("3 - 乘法 4 - 除法\n");
scanf("%d", &input);
if (input >=1 && input <= 4)
{
printf("请输出两个整数:");
scanf("%d%d", &x, &y);
int ret = (*parr[input])(x, y);//访问函数指针 - 调用函数
printf("%d\n", ret);
}
else if (input == 0)
break;
else
printf("输入错误,请重新输入\n");
}while(1);//循环程序
return 0;
}
十四、指向函数指针数组的指针
存储函数指针数组的地址
int main()
{
//函数指针数组
int(*parr[5])(int, int) = { NULL };
//指向函数数组的指针
//1、(*p) - *先和p结合 - 说明p是指针变量
//2、(*p)[5] - 说明p指向数组
//3、p指向数组的类型 - int(*)(int, int) - 函数指针类型
int(*(*p)[5])(int, int) = parr;
return 0;
}
十五、回调函数
- 在函数B中,通过函数指针,调用函数A
void Fun_A(int n){;}
void Fun_B(void(*p)(int))
{
int n = 0;
p(n);//回调函数
}
int main()
{
Fun_B(Fun_A);
return 0;
}
共勉!