C语言指针(超详细)

在这里插入图片描述

🎉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;
}
  1. ⼆维数组传参的本质
//二维数组的打印
// 法一
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;
}

在这里插入图片描述
在这里插入图片描述
⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式。

  1. 函数指针变量
    函数指针变量⽤来存放函数地址,可通过地址调⽤函数
    在这里插入图片描述
    在这里插入图片描述
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;
}
  1. 函数指针数组
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.总结
在这里插入图片描述
💓期待你的一键三连,你的鼓励是我创作的动力之源💓
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

engrave行而不辍

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值