深入理解指针(C语言基础)

1.内存和地址

CPU(中央处理器),在处理数据的时候,需要处理的数据是从内存中读取的,当数据处理完时又会放回到内存中。

内存会被划分为一个个的内存单元,每一个的内存单元大小都是一个字节。每个字节可以放八个比特位每个内存空间大小都有对应的编号,有了这个空间单元编号,CPU就能迅速找到一个内存空间,当然,在C语言中内存单元叫作地址。

a10bb9224ef244ea9c8bfad0bd261c77.png

2.指针的变量

2.1和地址 

  在C语言中创造变量,就是向内存申请空间

比如:创建一个整形变量,好比向内存申请四个字节大小,每个字节都有地址

2.2取地址操作符和解引用操作符

  我们可以通过取地址的操作符(&)拿到地址的数值,并将这个变量它存储下,存放在指针变量中的值都会被理解为地址

6003fea028564a3498a12868d3f87e9c.png

上面&a取出的是a所占4个字节中地址较⼩的字节的地址,虽然说整形变量占4个字节,但我们只要知道了第一个字节地址,就能顺等摸瓜访问到4个字节的数据。
    当然这个出现的int*,*就是代表这个指针变量,当我们拿到指针时,想访问一个指针所指向的对象后,就需要用到一个解引用的操作符---------*,也可以是指向的对象值进行修改,如图所示:
int main()
{
	int a = 10;
	int* pa = &a;  //取出a的地址
	*pa = 0;       // 把里面的地址取出来,当然也可以更改这个值
	return 0;
}

可以看到a的值,进行了改变,由此可见,这个说法成立

2.3.指针变量的大小  

32位平台下的地址就是32bit,指针变量的大小是4个字节

64位平台下的地址就是64bit,指针变量的大小是8个字节

在指针变量的大小与类型是无关的,管他是什么整型,只要是在相同的平台下都是一样的字节4/8

3.指针变量类型的意义

指针的类型决定了,对指针解引用的地址一次又多大的权限(一次能操作多少字节)

3.1 指针的解引用

//代码1                    
#include <stdio.h> 
int main()
{
 int n = 0x11223344;
 int *pi = &n; 
 *pi = 0; 
 return 0;
//代码2
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 char *pc = (char *)&n;
 *pc = 0;
 return 0;
}

调试可以看出来,代码1会将n的4个字节全部改回0,而代码2会将n的第一个字节改为0别的不变。

注释:( char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节)

3.2 指针运算

   1.指针+整数

   2.指针-指针

    3.指针的关系运算

1.指针+整数 指针的类型决定了指针向前或者向后走多大的距离

(举例:当类型是int*类型是4个字节,加1便是4位的距离同理,

char*类型是1个字节,加1便是加1个字节)

2.指针-指针  可以理解成找到俩个指针然后进行相减

void my_strlen(char* s)
{
  char *p = s;
  while(*p != '\0');
   {
  p++;
  return p-s;
   }
}
int main()
{
  printf("%s ",my_strlen("abc"));
  return 0;
}

3.指针的关系的运算:是对用操作符进行指针的变量的比较。

3.3 void* 指针

在指针类型中void*是一种特殊的类型,可以接收任意指针类型的指针(简称万能桶)但他无法直接进行指针计算。

4. const修饰指针

变量是可以修改的,如果我们不想将这个变量进行修改的话,就需要用到const来修饰指针

//代码1:
void test1()
{
int n = 20;
int m = 30;
const int* p =&n;    //这会是修饰*左边整个整数指针的,没有修饰*右边的式子
*p = 20;  //err         //所以这是式子是执行不了的         
 p = &m;  //ok        //这个没有被修饰到,所以可以使用
}

//代码2:
void test2()
{
int n = 10;
int m = 20;
int const * const p = &n   //  当这俩个式子*左右俩边都被修饰到后,就都变成了不可修改式子
*p = 20; //err
 p = &m; //err
}

结论:const修饰指针变量的时候

  const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不被改变。
但是指针变量本⾝的内容可变。
 const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指
向的内容,可以通过指针改变。

5.assert断言

平时要是为了确保程序的符合指定条件,不符合就会报错,中止程序运行,我们一般会用到一个叫宏定义assert一个头文件,用来测这个程序的严谨性。

优点:假设他报错便会直接跳转到报错的那一行,对程序员十分的友好的,但他缺点也十分明显就是会增加程序运行的时间,加入了额外的检查

#define NDEBUG
#include <assert.h>

上面这个表示可以对这个assert暂用或者换种说法先注释他不执行这个行代码,当然这个是只能在debug版本下才能运行

6.指针的使⽤和传址调⽤和传值调用


//将a,b整数变量值进行交换

//传值的调用
void swap(int a,int b)
{
   int tmp = a;
   a  = b;
   b = tmp;
}

int main()
{
 int a = 10;
 int b = 20;
 printf("交换前:a = %d b = %d",a,b);
swap(a,b);
printf("交换后:a = %d b = %d",a,b);
return 0;
}

当程序进行传值调用的时候,运行结果时俩者俩个整形并没进行交换,是因为:实参的地址和形参的地址不同(无法进行交换),只是把值给传过来,临时空间接收实参罢了

但将传值调用换成传址调用 把整个地址跟主函数建立关系便能使用程序就成功运行了

结论:

 传值调用: 实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。

传址调用: 便是可以让函数和主函数建立真正得关系,可以在函数中修改主函数的变量。

7.野指针

概念:野指针指向的为位置是不可知的(随机的,不正确,没有明确限制),没有初始值,指针越界访问、指针指向的空间释放

最好将指针使用后,在添加一个NULL的符号,用来有效避免野指针的出现

8.数组名的理解

     数组名就是首元素的地址

注意:

  sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩,
单位是字节
• &数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素
的地址是有区别的)

使用指针访问数组

可以理解成把地址给传址过去访问数组,如图所示

一维数组的传参的本质

本质来说,一维数组的传参的本质传的就是首元素的地址

并且⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。(二者都可以)

二级指针

指针的变量也是变量,是变量也就有地址,那地址变量的地址就叫二级指针。

int main()
{
	int a = 0;
	int* p = &a;   //将取出a的指针变量地址放在*p里面
	int** pa = &p;    //这里便表示a地址要发生改变的话,就要再存储别的地址上,再整个级别高一点地方
	return 0;
}

指针数组

整数数组:存放整数的数组
字符数组:存放字符的数组
指针数组:存放指针的数组(数组的每个元素的都是指针)

用指针数组模拟实现二维数组

数组指针变量

定义:

• 整形指针变量: int * pint, 存放的是整形变量的地址,能够指向整形数据的指针。
• 浮点型指针变量: float * pf, 存放浮点型变量的地址,能够指向浮点型数据的指针。
• 数组指针变量:int(*p)[]  存放数组变量的地址,能够指向数组的指针变量。

数组指针变量初始化

俩者俩个指针变量类型都是一样的。

二维数组传参的本质

定义:

在二维数组的首元素就是第一行,是一个一维数组
本质上传的是地址,实际是传递一维数组的地址

函数指针变量

函数也是有地址的,函数名就是函数的地址,也可以用&函数 来进行表示

int(*p) (int x,int y).....p指定函数的参数类型和个数交代
     
 .   .       
 .   .     
 .   .     
 .   .     
 .   .  
 .  函数指针变量的名字
 .   
 .   
p指向函数返回类型

函数指针变量的使用

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int (*p)(int, int) = Add;
	printf("%d\n", (*p)(2, 3));  // 俩种表达式都可以哈!!!
	printf("%d\n", p(3, 5));     // 指针变量的名字就是首元素的地址
	return 0;
}

 typedef关键字

typendef是用来类型重命名的,可以将复杂的类型,简单化。
比如 ,要是觉得unsigned int 写起来不方便
就可以改成 typedef unsigned int uint;
之后你使用一个uint 就能代表整个式子
指针类型也是可以修改名字哈,列如
typedef int* ptr_t
之后用一个ptr_t 就可以代表指针类型

回调函数

回调函数就是通过函数指针调用的函数
 如果把函数的指针作为参数传递给另一个函数,当整个指针被用来调用所指向函数,被调用的函数就是回调函数

 回调前:

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");
	
 }
int main()
{
	int x,y = 0;
	int input = 0;
	int ret = 0;
	menu();
	printf("请选择:\n");
	scanf("%d", &input);
	switch (input)
	{
	case 1:
		printf("输入操作数:\n");
		scanf("%d %d", &x, &y);
		ret = add(x,y);
		printf("ret = %d\n", ret);
		break;
	case 2:
		printf("输入操作数:\n");
		scanf("%d %d", &x, &y);
		ret = sub(x, y);
		printf("ret = %d\n", ret);
		break;
	case 3:
		printf("输入操作数:\n");
		scanf("%d %d", &x, &y);
		ret = mul(x, y);
		printf("ret = %d\n", ret);
		break;
	case 4:
		printf("输入操作数:\n");
		scanf("%d %d", &x, &y);
		ret = div(x, y);
		printf("ret = %d\n", ret);
		break;
	case 0:
		printf("退出游戏\n");
		break;
	default:
		printf("输入有误,请重新输入\n");
		break;
	}
	
	return 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");
	
 }
int main()
{
	int x, y;
	int input = 1;
	int ret = 0;
	int (*p[5])(int x, int y) = {NULL,add,sub,mul,div };
	do {
		menu();
		printf("请选择:\n");
		scanf("%d", &input);
		if (input <= 4 && input >= 1)
		{
			printf("输入操作数:\n");
			scanf("%d %d", &x, &y);
			ret = (*p[input])(x, y);
		   printf("ret = %d\n", ret);
		   printf("\n");
		}
		else if (input == 0)
		{
			printf("退出计算\n");
		}else
		{
			printf("输入有误,请重新输入\n");
		}
	} while (input);
	return 0;
}

qsort函数使用举例

函数实现排序

#include <stdio.h>
//qosrt函数的使⽤者得实现⼀个⽐较函数
int int_cmp(const void * p1, const void * p2)
{
 return (*( int *)p1 - *(int *) p2);
}
int main()
{
 int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
 int i = 0;
 
 qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
 for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
 {
 printf( "%d ", arr[i]);
 }
 printf("\n");
 return 0;
}

qsort函数模拟实现

print(int arr[], int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

int int_cmp(const void* p1,const void* p2)
{
	return *(int*)p1 - *(int*)p2;
}

void Swap(char* buf1, char* buf2, size_t sidth)
{
	char tmp = *buf1;
	*buf1 = *buf2;
	*buf2 = tmp;
	buf1++;    //为啥这里不需要再用*buf1++?
	buf2++;    // 因为这里只是为指针向后移动一位,不能指针所指向的元素内容
}

//接下来是qsort模拟的实现函数
void bubble_sort2(void* base, size_t sz, size_t width, int(*cmp)(const void* p1, const void* p2))
{
	for (int i = 0; i<sz - 1; i++)
	{
		for (int j = 0; j<sz - 1 - i; j++)
		{
			if (cmp((char*)base + j *width,(char*)base + (j+1) * width)> 0)
			{
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
		}
	}
}
void test1()
	{
		int arr[] = { 6,2,4,5,3,7,9,8,1,0 };
		int ret = sizeof(arr) / sizeof(arr[0]);
		print(arr, ret);
	    bubble_sort2(arr, ret, sizeof(arr[0]), int_cmp);
		print(arr, ret);
	}
int main()
{
	test1();
	return 0;
}

到你看到这里,想必你也是很认真的看了本章博客,非常感谢您的阅读,后面我也会尽心尽力的给大家带来好的博客。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值