C语言---指针

一  内存与地址与指针

在计算机中处理数据时,需要调用内存中的数据,数据处理后的数据也会返回内存中。

对于内存空间,被划分为一个个内存单元,每个内存单元的大小为一个字节,即1Byte-8bit(一个字节内能放八个比特位)。当计算机需要处理内存中的数据时,通过内存单元的地址来找到它,这里的地址我们可以理解为指针。即内存单元被指针指向着,计算机通过指针可以找到该内存单元。我们可以理解为内存单元的地址==指针。

二  指针变量

我们知道,内存的地址被指针指向。那么我们可以取出某个数据的地址将其赋予给一个变量,那么这个变量被称作指针变量。

下列代码中的变量num被指针变量p指向,那么我们通过对p解引用可以得到变量num的值

图解如下:

三  指针变量的大小

32位的机器有32根地址线,每根地址线转换成数字信号有两种结果:0和1,那么32根地址线产生的二进制序列当作一个地址,就是32个bit位,即4个字节。同理,64 位机器的一个地址就是8个字节

在X86环境下,即32位机器,每个指针变量的大小是4个字节

在X64环境下,即64位机器,每个指针变量的大小是8个字节

指针变量的大小与1其指针类型无关,只有平台相关,在相同平台下,指针变量的大小是相同的。

四  指针变量类型的意义

在X86平台下,指针的大小为4个字节,在X64平台下,指针的大小为8个字节。

4.1 指针的解引用

下列代码在调试中观察内存可知,int*指针的解引用一次操作四个字节

但是char*类型的指针只修改了一个字节(char*)&num表示将num的地址强制转换为char类型的指针。

所以我们可以得到:指针的类型决定了指针解引用的时候的权限。

4.2指针+-整数

在指针的+-运算中,是根据指针的类型来决定跳过的字节数。如:int类型的指针每次跳过4个字节,char类型的指针每次跳过1个字节。

4.3  void*指针

void*指针也称作“泛型指针”,可以用来接收任何类型的地址,但不能直接进行+-整数运算与解引用操作。

例如下列代码,用char类型的指针来接收int类型的变量,VS就会报错,“int*类型无法转换成char*类型”。

但是,我们使用void*类型的指针来接收就不会显示这个错误,但是后续就无法进行指针的操作。

4.4  const修饰

被const修饰的变量属性变成常属性,不能被更改。在我们对数据进行调用时,如果不想数据在调用过程中被修改或者覆盖,我们可以使用const将其变成常量。

在下面的代码中,变量a被const修饰,当我们想对a进行修改时,VS报错。

但是,a的本质终究是变量,只不过被const修饰后有了限制。知道原因后,我们可以绕过这个限制,不直接对变量a进行修改,而是通过a的地址进行修改。

但是一般来讲,const修饰指针的时候,放在*的左边与右边意义是不一样的。

void test1() {
	int n = 10;
	int m = 20;
	int* p = &n;
	*p = 10;
	p = &m;
}
//const放在左边
void test2() {
	int n = 10;
	int m = 20;
	const int* p = &n;
	*p = 10;//报错
	p = &m;
}
//const放在右边
void test3() {
	int n = 10;
	int m = 20;
	int* const  p = &n;
	*p = 10;
	p = &m;//报错
}
//两边都有const
void test4() {
	int n = 10;
	int m = 20;
	const int* const p = &n;
	*p = 10;//报错
	p = &m;//报错
}

结论:

如果const放在*的左边,那么修改的是指针指向的内容,指针指向的内容不能改变,但是指针本身可以被改变。

如果const放在*的右边,那么修饰的是指针本身,指针变量的内容不能修改,但是指针指向的内容可以通过指针改变。

4.5  指针运算

指针有三种运算:指针+-整数,指针-指针,指针的运算关系。

下面让我们来依次分析

*指针+-整数

以数组为例,因为数组在内存中是连续存放的,知道数组首元素的地址就知道数组剩余的元素。

通过指针的+-运算可以得到数组中的元素:

 *指针-指针

指针-指针运算得到的是指针之间的元素个数,但是有一个前提:指针指向的是同一块空间。

在库函数中,我们可以通过strlen函数来获取字符串中的字符个数,我们可以通过指针-指针操作来复现库函数中的strlen函数。

注:strlen统计的是‘\0’之前的个数。

*指针的关系运算

通过指针间的预算关系我们也可以实现对数组的打印

五  野指针

野指针就是无法确定其指向的指针。

下列就是野指针,因为定义整型指针时没有确定其指向的位置,后面当我们为p指向的位置赋值时,就造成了非法访问(因为p的指向根本不知道,是随机的)。所以当我们要使用指针时,一定要对指针进行初始化操作或者置为NULL,避免野指针的出现。

int main(){
    int*p;
    *p=1;
    return 0;
}

除此之外,还有两中比较常见的野指针:越界访问和内存是释放。

越界访问顾名思义就是指针的指向范围超过了原先的范围,当超出原先的范围时,指针就无法找到准确的位置,这时指针是野指针 。

指针指向的内存释放:一个函数返回了一个地址,指针指向该地址,但函数调用后立马销毁,地址也不存在了,这时候指针就是野指针。

六  assert断言

assert是宏定义,被定义在assert.h头文件中。

当使用assert时,程序运行时如果不符合指定条件,就报错终止运行。如果assert()表达式为真,assert()就不会产生任何作用。

而对于程序中的assert(),我们也可以控制其是否进行断言,只需在assert.h头文件前定义宏NDEBUG即可。

#define NDEBUG
#include<assert.h>

当使用指针作为调用函数时,我们需要判断这个指针是否为空指针,这时候assert()就可以派上用场。

void test(char*arr){
      assert(arr);//判断传过来的指针是否为空指针
}

七  传值调用与传址调用

通过值的调用与通过地址的调用是不一样的。

//下面的代码不会实现两个数的更改,因为在函数内是开辟新的空间来存储,函数调用后立马销毁
void swap(int a, int b) {
	int temp = a;
	a = b;
	b = temp;
}
int main() {
	int a = 10;
	int b = 20;
	printf("%d %d\n", a, b);
	swap(a, b);
	printf("%d %d", a, b);

}

传址调用可以让函数与主函数的建立真正的关系,在函数内部可以实现修改主函数的变量。 

//通过调用两个数的地址来更改,那么在函数调用销毁后,地址指向的数已经改变,可以实现
void swap(int*a, int*b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}
int main() {
	int a = 10;
	int b = 20;
	printf("%d %d\n", a, b);
	swap(&a,&b);
	printf("%d %d", a, b);
	
}

当我们需要用主函数中的变量来实现计算时,可以使用传值调用。当我们需要修改主函数内部的变量数据时,应当使用传址调用。

八  数组名的理解

我们发现数组名和数组首元素的地址打印结果一致,数组名就是数组首元素的地址。

int main() {
	int arr[] = { 1,2,3,4,5,6 };
	printf("%p\n", arr);
	printf("%p", &arr[0]);
}

但是有两个例外:sizeof(数组名)和&数组名。

sizeof(数组名):表示整个数组的大小,单位是字节。如果是数组首地址的话,那么输出应该是4或者8.

&数组名:取出的是整个数组的地址,与数组首元素地址有区别。

当我们对数组的地址进行+-操作时,取数组首元素的地址与数组的地址实现的功能有区别。

我们发现,&arr[0]与arr都是首元素的地址,+1操作后跳过数组中的一个元素,即四个字节。

而对于&arr,取的是数组的地址,+1操作跳过的是整个数组。

九  通过指针访问数组

以下是通过指针对数组进行赋值与输出

int main() {
	int arr[10] = {0};
	int* p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < sz; i++) {
		scanf_s("%d", p + i);
	}
	for (int i = 0; i < sz; i++) {
		printf("%d", p[i]);//p[i]与*(p+i)本质上是等价的
	}
	return 0;
}

十  一维数组的传参本质

我们知道,数组可以作为参数传递给函数。

对于计算数组内的元素个数,我们一般用到:

int sz=sizeof(arr)/sizeof(arr[0]);

如果我们在函数的内部使用这串代码,那么还能不能实现计算数组元素的功能?

可以看到,两者的结果并不一致。

 在本质上,数组传参传递的是数组首元素的地址,那么在函数内部,sizeof(arr)计算的是一个地址的大小而不是如同主函数内整个数组的大小,在X86环境下,传递数组首元素大小为4个字节,数组首元素的大小也为4个字节,那么输出的结果为1。那么我们可以得知,数组传参的本质可以说传递的是指针,而不是整个数组,所以在函数内部无法求得整个数组的元素个数。

如下面的代码,将形参改成指针形式,结果也一致。

十一  二级指针

指针存放着地址,但是指针变量也是变量,指针也有地址,  那么我们将存放一级指针变量的地址的变量称作二级指针

通过对二级指针pp解引用*pp找到一级指针p,再对一级指针p解引用*p找到a。三级指针四级指针与二级指针一致,每一级指针存放的是上一级指针的地址。

十二  指针数组

 整型数组存放的是整型变量,字符数组存放的是字符变量,同理,指针数组存放的是指针变量(每个元素存放的都是指针的)。

通过指针数组,我们可以模拟二维数组的实现:

//指针数组模拟二维数组
int main() {
	int arr1[] = { 1,2,3 };
	int arr2[] = { 3,4,5 };
	int arr3[] = { 4,5,6 };
	int* arr[] = { arr1,arr2,arr3 };
	for (int i = 0; i < 3; i++) {
		for (int j = 0; j < 3; j++) {
			printf("%d ", arr[i][j]);
		}printf("\n");
	}
}

上述代码只能模拟二维数组的效果,但并非是真实的二维数组,因为每一行并非是连续的。

十三  字符指针

对于字符指针的一般使用:

int main() {
	char ch = 'a';
	char* p = &ch;
	*p = 'A';
	return 0;
}

除此之外还有一种使用方式:

int main() {
	const char* p = "hellow.com";
	printf("%s", p);
}

但在这里并 不是将字符串“hellow.com”存放到指针p中,而是将字符串的首字符地址放进指针p中。

下面我们来看一下有关字符串与指针开辟空间的问题,在下述代码中的结果是怎样的呢?

int main() {
	char str1[] = "hellow";
	char str2[] = "hellow";
	const char* p1= "hellow";
	const char* p2 = "hellow";
	if (str1 == str2) {
		printf("str1=str2\n");
	}
	else {
		printf("str1!=str2\n");
	}
	if (p1 == p2) {
		printf("p1=p2\n");
	}
	else {
		printf("p1!=p2");
	}

}

 str1与str2字符串的内容虽然一致,但是这是两个不同的变量,常量字符串区初始化不同数组的时候会开辟出不同的内存块,所以str1与str2不同。

在C/C++里,常量字符串会存储到一个单独的内存区域,当几个指针指向同一个字符串时,他们会指向同一块内存。所以p1=p2。

十四  数组指针

指针数组与数组指针有上面区别呢?

指针数组是一种数组,里面的每个元素都是指针。

数组指针是指针变量,存放的是数组的地址,是能够指向数组的指针变量

//指针数组
int*arr[10];
//数组指针
int(*p)[10];

在数组指针中,*先与p结合,表示这是个指针,这个指针指向大小为10的数组,所以这是个数组指针

  数组指针的初始化

在前面我们了解到,通过&arr[0]获取到的是数组首元素的地址,&arr则是数组的地址

将数组的地址存放在指针中,如下:

int(*p)[10]=&arr;

十五  二维数组的传参本质

如二维数组arr[3][3]={{1,2,3},{2,3,4},{3,4,5}},我们一般会将其画成下面的样子:

但是在内存中,这些元素是连续存放的:

对于一个三行三列的二维数组arr[3][3],可以看成是三个一维数组,每个一维数组有三个元素组成。前面我们学到,对于数组来说,指针指向的是数组首元素的地址,那么对于二维数组来说,首元素地址是第一个一维数组的地址。 

//二维数组的传参本质
void test(int(*p)[5], int x, int y) {
	for (int i = 0; i < 3; i++) {
		for (int j = 0; j < 5; j++) {
			printf("%d", *(*(p + 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} };
	test(arr, 3, 5);

}

对于形参,我们可以写出数组int arr[3][5](行数可以忽略不写),也可以写成指针int(*p)[5]的形式。

十六  函数指针

对于函数来说,函数也是有地址的:

void test() {
	printf("hehe\n");
}
int main(){
	printf("%p\n", test);
	printf("%p", &test);
}

函数名就是函数的地址,也可以通过&函数名的方法获得函数的地址。

函数既然有地址,那么我们就可以使用指针将其储存起来,这样的指针我们将其称之为函数指针

 函数指针的写法与数组指针十分类似:

//数组指针
int (*p)[10];

//函数指针
int (*p)(形参)=函数名;
如:
int (*p)(int x,int y)=Add;

我们可以通过函数指针调用指针指向的函数:

int add(int x, int y) {
	return x + y;
}
int main() {
	int a = 10;
	int b = 20;
	//先找到函数的位置:通过指针p找到函数,进而通过指针调用函数
	int(*p)(int, int) = add;
	printf("%d\n", (*p)(a, b));
	printf("%d", (*p)(a, 10));
}

输出结果:

十七  typedef关键字

typedef关键字是用来类型重命名的,可以将复杂的类型简单化。

如将unsigned int  重命名为uint。

typedef unsigned int  uint;

对于指针类型,我们也可以将其重命名,如下代码将int*的指针重命名为ptr_t。

typedef int* ptr_t;

但对于数组指针与函数种子则有些不同 :

数组指针需要将新的类型名放到*的右边,此时数组指针重命名为par_t。

typedef int(*par_t)[10];

 函数指针也需要将新的类型名放到*的右边,此时函数指针重命名为pbr_t。

tydepef void(*pbr_t)(int);

十八  函数指针数组

将多个函数的地址可以存放到数组中,这个数组被称为函数指针数组。

函数指针数组定义:

int (*p[3])();

p先与[]结合,说明p是数组,是int(*)()类型的函数指针。

利用函数指针函数,我们可以实现一个简单的计数器:

#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(*p[5])(int x, int y) = { 0,add,sub,mul,div };//函数名就是函数的地址
	int input = 1;
	int x = 0;
	int y = 0;
	do {
		printf("0.退出    1.add    2.sub    3.mul    4.div\n");
		printf("请输入:");
		scanf_s("%d", &input);
		//判断输入数是否合理
		if (input >= 1 && input <= 4) {
			printf("请输入两个操作数:");
			scanf_s("%d %d", &x, &y);
			int ret = (*p[input])(x, y);
			printf("结果是:%d\n", ret);
		}
		else if (input == 0) {
			printf("退出成功");
		}
		else  {
			printf("输入有误,重新输入\n");
		}
	} while (input);
}

十九  回调函数

将函数的地址作为参数传递给另一个函数,这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。回调函数可以很好的解决代码臃肿的问题。

下述代码通过switch语句实现简单的计数器功能,但是输入输出语句是冗余的。

#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 input = 1;
	int x = 0;
	int y = 0;
	do {
		printf("0.退出    1.add    2.sub    3.mul    4.div\n");
		printf("请输入:");
		scanf_s("%d", &input);
		int ret = 0;
		switch (input) {
		case 0:
			printf("退出成功");
		case 1:
			scanf_s("%d %d", &x, &y);
			 ret = add(x, y);
			printf("%d\n", ret);
			break;
		case 2:
			scanf_s("%d %d", &x, &y);
			 ret = sub(x, y);
			printf("%d\n", ret);
			break;
		case 3:
			scanf_s("%d %d", &x, &y);
			 ret = mul(x, y);
			printf("%d\n", ret);
			break;
		case 4:
			scanf_s("%d %d", &x, &y);
			 ret = div(x, y);
			printf("%d\n", ret);
			break;
		default:
			printf("输入错误\n");
			break;
		}
	} while (input);
}

但是通过回调函数我们可以解决这个问题,只是改变了调用函数的逻辑,从直接调用变成用函数调用。

#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;
}
void cald(int(*pt)(int x, int y)) {//函数的形参就是函数指针
	int ret = 0;
	int x = 0;
	int y = 0;
	scanf_s("%d %d", &x, &y);
	ret = pt(x, y);
	printf("结果是:%d\n", ret);
}
int main() {
	int input = 1;
	int x = 0;
	int y = 0;
	do {
		printf("0.退出    1.add    2.sub    3.mul    4.div\n");
		printf("请输入:");
		scanf_s("%d", &input);
		int ret = 0;
		switch (input) {
		case 0:
			printf("退出成功");
		case 1:
			cald(add);
			break;
		case 2:
			cald(sub);
			break;
		case 3:
			cald(mul);
			break;
		case 4:
			cald(div);
			break;
		default:
			printf("输入错误\n");
			break;
		}
	} while (input);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值