🎉welcome to my blog
请留下你宝贵的足迹吧(点赞👍评论📝收藏⭐)
💓期待你的一键三连,你的鼓励是我创作的动力之源💓
1.内存和地址 (引入)
2. 指针变量和地址
3. const修饰指针
4. 指针运算
5. 野指针
6. assert断⾔
7. 指针的使⽤和传址调用
8.拓展👀:数组名的理解
9. 使⽤指针访问数组
10. ⼀维数组传参的本质
11. 冒泡排序
12. ⼆级指针
13. 指针数组
14. 指针数组模拟⼆维数组
15. 字符指针变量
16. 数组指针变量
17. ⼆维数组传参的本质
18. 函数指针变量
19.两段有趣的代码
20.引入:typedef关键字
21. 函数指针数组
22.总结
1.内存和地址 (引入)
1.1内存
电脑上内存是8GB/16GB/32GB等
计算机把内存划分为⼀个个的内存单元,每个内存单元的大小取1个字节
计算机中常见的单位(补充):
1.bit - 比特位
2.byte - 字节
3.KB
4.MB
5.GB
6.TB
7.PB
1byte = 8bit
1KB = 1024byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
1PB = 1024TB
⼀个比特位可以存储⼀个2进制的位1或者0其中,每个内存单元,相当于⼀个学生宿舍,⼀ 个字节空间里面能放8个比特位,就好比同学们住 的八人间,每个⼈是⼀个比特位。 每个内存单元也都有⼀个编号(这个编号就相当 于宿舍房间的门牌号),有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间。⽣活中我们把门牌号也叫地址,在计算机中我们 把内存单元的编号也称为地址。C语言中给地址起 了新的名字叫:指针。
我们可以理解为: 内存单元的编号== 地址 ==指针
1.2编址
CPU访问内存中的某个字节空间,必须知道这个 字节空间在内存的什么位置,而因为内存中字节 很多,所以需要给内存进行编址(就如同宿舍很多,需要给宿舍编号⼀样)。计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。
计算机内有很多的硬件单元互相协同工作(至少相互之间要能够进行数据传递)
可以简单理解,32位机器有32根地址总线, 每根线只有两态,表示0,1【电脉冲有无】,那么 ⼀根线,就能表示2种含义,2根线就能表示4种含 义,依次类推。32根地址线,就能表示2^32种含 义,每⼀种含义都代表⼀个地址。 地址信息被下达给内存,在内存上,就可以找到 该地址对应的数据,将数据在通过数据总线传入CPU内寄存器。
2. 指针变量和地址
2.1 取地址操作符(&)
2.2 指针变量和解引⽤操作符(*)
2.2.1 指针变量
#include<stdio.h>
int main ()
{
int a = 20;
int* p = &a;//p是指针变量-存放指针(地址)的变量
char ch = 'w';
char* pa = &ch;
return 0;
}
总结:
1.指针即地址
2.指针变量即存放指针(地址)的变量
注:口头语中所说的指针一般都是指针变量
举个🌰:int * p;我们常说p为指针,而实际上严格来说p是指针变量
注意⚠️:指针变量的大小和类型无关,
只要是指针类型的变量,在相同的平台下大小都是相同的
2.2.2 解引⽤操作符(*)
结论:指针类型决定了指针进行解引用操作时访问多大空间
Int* 的指针解引用访问4个字节
Char* 的指针解引用访问1个字节
2.2.3指针±整数
结论:type* p;
p+i就相当于跳过了isizeof(type)个字节
2.2.4void指针
void * 类型的指针可以理解为⽆具体类型的指针(或者叫泛型指 针),这种类型的指针可以⽤来接受任意类型地址。
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;//int类型的地址放到char*的指针变量中不太合适,编译器会警告
void*pv = &a;//而void*类型可以接受任何类型的指针变量
double d = 3.14;
void* pd = &d;
printf("%d\n"pc);
return 0;
}
但是也有局限性, void* 类型的指针不能直接进⾏指针的±整数和解引⽤的运算。
3. const修饰指针
//引入
#include<stdio.h>
int main ()
{
const int n = 10;//const是常属性,变量一旦被const修饰后就不能被再次修改了
//但n的本质还是变量,即为常变量
n=0;//✖
printf("%d\n",n);
return 0;
}
#include<stdio.h>
int main()
{
int arr[10]={0};
const int n = 10;
int arr[n]={ 0 };//arr[],[]中应为常量,虽然变量n被const修饰了
//其值不能被修改了但n本质仍是变量,所以[]中不能放n
return 0;
}
4. 指针运算
4.1指针±整数
//求字符串长度
//法一
#include<string.h>
int main()
{
char arr[] = "abcdef";
int len=strlen(arr);//string()库函数,专门用来计算字符串长度,统计的是\0之前字符的个数
//其头文件是string.h
printf("%d\n", len);
return 0;
}
//法二
int my_strlen(char* p)
{
int count =0;
while (*p !='\0')
{
count++;
p++;
}
return count;
}
int main()
{
char arr[]="abcdef";
int len = my_strlen(arr);//数组名即数字首元素的地址
printf("%d\n", len);
return 0;
}
//法三
int my_strlen(char*p)
{
char* p1 = p;
while (*p !='\0')
{
p++;
}
return p - p1;//指针-指针
}
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);
printf{"%d\n",1en);
return 0;
}
4.2指针的关系运算
5. 野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
5.1 野指针成因
5.1.1. 指针未初始化
5.1.2. 指针越界访问
#include<stdio.h>
int main()
{
int arr[10]={0};
int* p = &arr[0];
int i = 0;
for (i = 0; i <= 11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++)=i;
}
return 0;
}
5.1.3. 指针指向的空间释放
5.2 如何规避野指针
5.2.1 指针初始化
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL.
NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错
//使用指针前可用以下方法检测指针的有效性
int main()
{
int a =10;
int*p = &a;
if (p != NULL)
{
*p = 20;//若p=NULL就不会被访问了
}
return 0;
}
指针变量不再使⽤时,及时置NULL(因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问),指针使⽤之前检查有效性
5.2.2⼩⼼指针越界
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是 越界访问。
5.2.3避免返回局部变量的地址,局部变量得值是可以返回的
6. assert断⾔
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报 错终⽌运⾏。这个宏常常被称为“断⾔”。
assert(p != NULL);
上面代码在程序运行到这⼀行语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序 继续运行,否则就会终止运行,并且给出报错信息提示。
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值非零), assert() 不会产生任何作用,程序继续运行。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误 流 stderr 中写入⼀条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。
#include<stdio.h>
#include<assert.h>
int main()
{
int a = 0;
scanf("%d", &a);
assert(a!=0);//也可以用来断言变量的值
printf("%d\n",a);
return 0;
}
使用assert() 有⼏个好处:
1.自动标识文件和出问题的行号,
2.无需更改代码就能开启或关闭 assert() 的机制。
如果已经确认程序没有问题,不需要再做断言,就在 #include 语句的前面,定义⼀个宏 NDEBUG 。
#define NDEBUG
#include <assert.h>
然后,重新编译程序,编译器就会禁用文件中所有的assert()语句。如果程序⼜出现问题,可以移除这条#define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语句。
assert() 的缺点是:引⼊了额外的检查,增加了程序的运⾏时间。
⼀般可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏了。像VS 这样的集成开发环境在 Release 版本中,直接就优化掉assert了。在debug版本写有利于程序员排查问题, 在 Release 版本不影响⽤⼾使⽤时程序的效率。
7. 指针的使⽤和传址调用
7.1 strlen的模拟实现
库函数strlen的功能是求字符串⻓度,统计的是字符串中 \0 之前的字符的个数。 函数原型如下:
size_t strlen ( const char * str );
7.2 传值调⽤和传址调⽤
7.2.1传值调⽤
7.2.2传址调⽤
传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量
如果函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改 主调函数中的变量的值,就需要传址调⽤。
8.拓展👀:数组名的理解
数组名是数组首元素(第⼀个元素)的地址,但是有两个例外:
• sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩, 单位是字节
• &数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素的地址是有区别的)
除此之外,任何地⽅使⽤数组名,数组名都表⽰⾸元素的地址。
9. 使⽤指针访问数组
10. ⼀维数组传参的本质
11. 冒泡排序
//冒泡排序
void bubble_sort(int arr[],int sz)//也可写成void bubble_sort(int* arr,int sz)
{
//int sz = sizeof(arr) / sizeof(arr[0]);//数组传参时不可在被调用函数中求数组元素个数
//因为数组传过来的实际是数组首元素的地址,利用int sz = sizeof(arr) / sizeof(arr[0]);
//所求结果为1,并不能求得数组元素个数
//应在主函数中求得数组元素个数后再传给被调用的函数
int i = 0;
for (i = 0; i < sz - 1; i++)//趟数
{
int j = 0;
for (j = 0; j < sz - i -1; j++)
{
if (arr[j] > arr[j + 1])//也可改为指针的形式if(*(arr+j)>*(arr+j+1))
{
int tmp = arr[j];//指针形式int tmp = *(arr+j);
arr[j] = arr[j + 1];//指针形式*(arr+j)=*(arr+j+1)
arr[j + 1] = tmp;//指针形式*(arr+j+1)=tem;
}
}
}
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1 };//降序
//编写代码将其排为升序
int sz = sizeof(arr) / sizeof(arr[0]);
print_arr(arr, sz);
bubble_sort(arr,sz);
print_arr(arr, sz);
return 0;
}
//以上代码针对已经有序的元素还会再次进行排序,比较低效,可将其优化为以下代码
int count = 0;
void bubble_sort(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++)//趟数
{
int flag = 1;//假设是有序的
int j = 0;
for (j = 0; j < sz - i - 1; j++)
{
count++;//记录比较的次数
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag = 0;//一旦发生交换就证明不是有序的,就将flag置为0
}
}
if (flag == 1)//若进行一趟之后,flag仍为1就证明是有序的,
//就不需要进行下一趟排序了,直接跳出循环
{
break;
}
}
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9};//降序
//编写代码将其排为升序
int sz = sizeof(arr) / sizeof(arr[0]);
print_arr(arr, sz);
bubble_sort(arr, sz);
print_arr(arr, sz);
printf("%d\n", count);
return 0;
}
未优化时比较次数是36
优化后比较次数是8,提高了效率
12. ⼆级指针
13. 指针数组
14. 指针数组模拟⼆维数组
15. 字符指针变量
⼀般使⽤:
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
还有⼀种使⽤⽅式:
16. 数组指针变量
int (*p)[10];
解释:p先和结合,说明p是⼀个指针变量变量,然后指着指向的是⼀个⼤⼩为10个整型的数组。所以 p是⼀个指针,指向⼀个数组,叫数组指针。
注意:[]的优先级要⾼于号的,所以必须加上()来保证p先和*结合。
int main()
{
int arr[10] = { 0 };
int(*p)[10] = &arr;//取出数组的地址
//p是数组指针,指向数组,数组有10个元素,每个元素的类型是int
//[]中的元素个数不能省
//此指针数组的类型是int [10]
char* str[5];
char* (*p)[5] = &str;//此指针数组的类型为char* [5]
return 0;
}
//利用指针打印数组中的元素
//法一
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10};
int* p = arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[10]);
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
//法二(一般不推荐这样用)
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10};
int(* p)[10] =&arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[10]);
for (i = 0; i < sz; i++)
{
printf("%d ", (*p)[i]);//(*p)[i]等价于(*&arr)[i],*与&抵消得arr[i]
}
return 0;
}
- ⼆维数组传参的本质
//二维数组的打印
// 法一
void test(int arr[3][5], int r, int c)//由于二维数组名第一行一维数组的地址
//此处可用指针数组接收一维数组,故int arr[3][5]可替换成int (*p)[5]
{
int i = 0;
for (i = 0; i < r; i++)
{
for (int j = 0; j < c; j++)
{
printf("%d ", arr[i][j]);//此处的arr[i][j]也可换成(*(p+i))[j],p+1就会跳到下一行,
//然后再对每一行的元素通过下标访问输出
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
test(arr, 3, 5);//二维数组首元素的地址(即二维数组数组名)是第一行的地址(第一行是一维数组)
return 0;
}
⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式。
- 函数指针变量
函数指针变量⽤来存放函数地址,可通过地址调⽤函数
int Add(int a, int b)
{
return a + b;
}
int* test(char* s)
{
return NULL;
}
int main()
{
int arr[8] = { 0 };
int(*p)[8] = &arr;//p是数组指针变量
int (*pa)(int, int) = &Add;//pf是函数指针变量
int* (*pt)(char*) = &test;//pt是函数指针变量
return 0;
}
19.两段有趣的代码
void (*p)();//p表示无参,返回类型为void的函数指针变量
其类型是void (*)()
int main()
{
(*(void(*)())0)();
//1.将0强制类型转化为void (*)()类型的函数指针
//2.调用0在地址处放的这个函数
return 0;
}
20.引入:typedef关键字
typedef是⽤来类型重命名的,可以将复杂的类型,简单化。
typedef unsigned int uint;//将unsigned int重新起名为uint,简化了复杂的类型
int main()
{
unsigned int num;//等价于uint num;
return 0;
}
//typedef对指针类型重命名
typedef int* pint;
int main()
{
int* p = NULL;//等价于pint p=NULL;
return 0;
}
// typedef对数组指针类型重命名
typedef int(*parr_t)[5];//将int (*)[5]重新起名为parr_t
//不能类比上面的将其写为typedef int(*)[5] parr_t,这样写语法上是不支持的
int main()
{
int arr[5] = { 0 };
int(*p)[5] = &arr;//p是数组指针变量,其类型为int (*)[5]
//int(*p)[5] = &arr;等价于parr_t=&arr;
return 0;
}
//typedef对函数指针类型重命名
void test(char* s)
{
}
typedef void (*pf_t)(char*);//将void (*)(char*) 重新起名为pf_t
不能类比上面的将其写为typedef void (*)(char*) pf_t,这样写语法上是不支持的
int main()
{
void (*pf)(char*) = test;//pf是函数指针变量,其类型为void (*)(char*)
//void (*pf)(char*) = test;等价于pf_t pf=test;
return 0;
}
void(*signal(int, void(*)(int)))(int);
//简化后的代码
typedef void(*pf_t)(int);
pf_t signal(int, pf_t);
//函数的形参名一般在声明时可以省略,但在函数定义时,一般会用到,故不可省
#define PRT_T int*//表示PRT_T的内容是int*
typedef int* prt_t;
int main()
{
PRT_T p1;//等价于int* p1;p1是整型变量
prt_t p2;//p2是整型变量
PRT_T p3,p4;//等价于int* p3,p4;只有p3是整型指针,p4是整型
prt_t p5,p6;//p5,p6均为整型指针
return 0;
}
- 函数指针数组
int (*parr1[3])();//parr1 先和 [] 结合,说明parr1是数组
//
应用:
//写一个计算机,完成两个整数的运算
//1.加法
//2.减法
//3.乘法
//4.除法
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 x = 0;
int y = 0;
int ret = 0;
int input = 0;
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
ret = Add(x, y);
printf("%d\n", ret);
break;
case 2:
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("%d\n", ret);
break;
case 3:
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
ret = Mul(x, y);
printf("%d\n", ret);
break;
case 4:
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
ret = Div(x, y);
printf("%d\n", ret);
break;
case 0:
printf("退出计算机\n");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
} while(input);
return 0;
}
//以上代码的不足之处:
//1.代码冗余
//2.如果功能扩展,代码也会大量增加
//由于所定义函数的参数类型,参数个数,返回类型都一样
//故可借助函数指针数组将代码进行优化
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 ret = 0;
//创建一个函数指针的数组
int (*pfArr[5])(int, int) = {NULL,Add,Sub,Mul,Div};//下标依次是0,1,2,3,4
//与menu()函数中的情况对应起来,用时较方便
do
{
menu();
printf("请选择:");
scanf("%d", &input);
if(input>=1&&input<=4)
{
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
int ret=pfArr[input](x, y);
printf("%d\n", ret);
}
else if (input == 0)
{
printf("退出比赛\n");
break;
}
else
{
printf("选择错误,请重新选择\n");
}
} while (input);
return 0;
}
22.总结
💓期待你的一键三连,你的鼓励是我创作的动力之源💓