C语言快速入门之指针详解

一.指针基础

1.指针定义的理解

就像我们住房子划分房间一样,在系统中,内存会被划分为多个内存单元,一个内存单元大小是一个字节,我们在搜索房间时有门牌号,与之类似,每个内存单元都会有一个编号 = 地址 = 指针,计算机中把内存的编号叫地址,C语言中给地址起了新名字指针

2.变量的创建和指针的指向

变量创建的本质是在内存中开辟一块空间

int main() {
	int a = 10;	//变量创建的本质是在内存中开辟一块空间
	//分配了四个字节,取地址是第一个字节的地址(较小的)
	printf("%p",&a);		//&取地址操作符	%p专门打印地址的(十六进制的形式打印的)
	//printf("%X", &a);
	return 0;
}

在上面,我们定义了一个int类型的变量a,而当a创建时,就要为a分配空间,int型分配四个字节,我们使用&(取地址符号)获得的就是a的第一个字节(最小的字节)的地址

假设内存中这样存放a,我们取地址取出的就是0x0012ff40

3.指针的书写和含义

那么我们将a的地址取出来后,怎么把它放到一个变量里呢?

我们这样来书写:

int a = 10;
int* pa = &a;

int * pa中,*代表pa是指针变量,int代表该指针指向的类型是什么(a的类型)

那么,既然指针也是一个变量,那么指针变量的大小是多少?

4.指针变量的大小

指针变量需要多大的空间,取决于存放的是什么(地址),地址的存放需要多大的空间,指针变量的大小就是多大(与指针指向的内容的类型无关)

我们可以写出如下代码来观察指针的内存

int main() {
	int a = 20;
	char ch = 'w';
	char* pc = &ch;
	int* pa = &a;
	printf("%d\n", sizeof(pc));
	printf("%d\n", sizeof(int*));
	return 0;
}

分开写结果如下:

5.指针类型的意义

我们已经知道了*代表这是一个指针变量,那么前面指向的类型又有什么作用呢?

事实上,指针类型决定了指针在进行解引用的时候访问多大的空间,例如,我们在进行解引用操作时,int*解引用访问4个字节,char*解引用访问1个字节

分析图如下:

当我们使用char *去指向int类型时,解引用只会访问一个字节的内容,而我们知道int类型是四个字节,所以出现了错误

6.指针相加减

指针类型决定了指针的步长,向前或者向后走一步大概的距离

int main() {
	int a = 10;
	int* pa = &a;
	printf("%p\n", pa);
	printf("%p\n", pa + 1);
	char* pc = &a;
	printf("%p\n", pc);
	printf("%p\n", pc + 1);
	return 0;
}

对于int*类型的指针变量,+1会移动四个字节,char*类型的指针变量,+1会移动一个字节,因此,我们要根据实际的需要,选择适当的指针类型才能达到效果

7.void*指针

无具体类型的指针(范性指针)可以接受任意类型的地址,不能进行解引用的操作,和指针的+-操作

int main() {
	int a = 10;
	int* pa= &a;
	void* pc = &a;
	char b;
	void* pd = &b;
	return 0;
}

8.const和指针

我们知道,const表示常属性,不能被修改,例如:

int main() {
	const int n = 10;	//n是变量,但不可以改变,常变量
				//如果不想让n变,就在前加const
	printf("%d", n);
	return 0;
}

此时,n是无法改变的

但是我们可以使用指针来更改数字,如下:

int main() {
	const int n = 10;
	int* p= &n;
	*p = 20;
	printf("%d", n);
	return 0;
}

这样,我们就可以改变n的变量

const修饰指针有两种情况,分为const在*前和const在*后,

1.const放在*的左边,修饰的是*p        const int* p

2.const放在*的右边,修饰的是p        int* const p

两者有什么区别呢?

int main() {
	const int n = 10;
	int m = 100;
		//const int(* p) = &n;
		*p = 20;		//不能改变对象的值了
		// p = &m;		//可以改变指向的对象
		//int* const p = &n;
		p = &m;		//不可以改变指向的对象
		//*p = 20;		//可以改变对象的值
	//p是指针变量,里面存放了其他变量的地址
	//p是指针变量,*p(n)是指针指向的对象
	//p是指针变量,p也有自己的地址&p
	return 0;
}

const放在*前,会管整个(*p),使其不能改变值,但是p不受管理

const放在*后,只管p,不能改变指向的对象,但是可以改变*p

9.指针的运算

指针的基本运算分为三种:
指针+-整数
指针 - 指针
指针的关系运算

指针加减整数运算

int main() {
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
		//数组在内存中连续存放,随着下标增长,地址由低到高变化的
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = &arr[0];
	for (i = 0; i < sz; i++) {
		printf("%d\n", *p);
		p++;	//p = p + 1
	}
	return 0;
}

指针-指针 == 指针之间元素的个数(运算的前提条件是两个指针指向了同一块的空间)

int main() {
	int arr[10] = { 0 };
	printf("%d", &arr[9] - &arr[0]);
	printf("%d", &arr[0] - &arr[9]);
	//指针 - 指针的绝对值是指针和指针之间的元素个数

	return 0;
}

指针的运算关系:两个指针比较大小

int main() {
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = &arr[0];
	while (p < arr + sz) {
		printf("%d\n", *p);
		p++;
	}
	return 0;
}

10.野指针

野指针    指针指向的位置是不可知的(随机的,不正确的,没有限制的)很危险

野指针的成因:

//成因一:指针未初始化
int main()
{
	int* p;		//局部变量不初始化的时候,他的值是随机值
	*p = 20;
	printf("%d\n", *p);
	return 0;
}
//成因二:指针的越界访问
int main() {
	int arr[10] = { 0 };
	int* p = arr[0];
	int i = 0;
	for (i = 0; i < 11; i++) {		//指向了不属于的空间,且用指针访问了
		*(p++) = i;
	}
	return 0;
}
//成因三:指针空间释放
int* test() {
	int n = 100;
	return &n;
}
int main() {
	int* p = test();	//p存n的地址,但是空间回收了
	printf("%d", *p);
	return 0;
}

如何规避野指针

1.指针初始化    知道就直接赋地址,不知道就赋值NULL
2.小心指针越界
3.指针不在使用时,及时置为NULL,指针使用前检查有效性
4.避免返回局部变量的地址

int main() 
{
	//int a = 10;
	//int* pa = &a;
	//*pa = 20;
	//printf("%d", a);

	//int* p = NULL;	//空指针,地址是0,不能赋值
	//*p //err

	int a = 10;
	int* p = &a;
	if (p != NULL) //检测指针有效性
	{
		*p = 20;
	}

	return 0;
}

那是否我们可以更简单的方法来检测指针的有效性呢?

11.assert断言

assert的作用:运行时确保程序符合指定条件,不合条件,报错并终止运行

形式如下        assert(表达式);        表达式如果不成立则会报错

当然,assert需要头文件

#include	<assert.h>

上面的代码可以更简单的写成下面的形式

int main()
{
	int a = 10;
	int* p = NULL;
	assert(p != NULL);	//err
	*p = 20;
	printf("%d\n", *p);

	return 0;
}

在这里,由于*p是NULL,直接报错并终止运行了

如果后面我们不再需要assert断言,我们不需要把所有写出的assert代码都手动删除,只需要在头文件位置写成:

#define	NDEBUG
#include	<assert.h>

即可使得assert失效

12.指针的使用和传址调用

(1)我们尝试用代码实现strlen()

size_t Mystrlen(const char* s)
{
	size_t count = 0;
	assert(s != NULL);
	while (*s != '\0') {
		s++;
		count++;
	}
	return count;
}

int main() 
{
	//strlen()-求字符串长度
	char arr[] = "abcdef";
	size_t len = Mystrlen(arr);	//字符串中\0前的字符个数
	printf("%zd\n", len);

	return 0;
}

(2)函数实现两个数的交换

void swap(int x, int y) {
	int z = 0;
	z = x;
	x = y;
	y = z;
	return 0;
}

int main() 
{
	int a = 10;
	int b = 20;
	printf("%d %d\n", a, b);
	swap(a, b);	
	printf("%d %d\n", a, b);


	return 0;
}

我们写出代码发现a与b的值没有发生改变,实际上,实参变量传给形参时候,形参是一份临时拷贝,对形参的修改不会影响实参,因此,我们应该传地址过去,实现改变

void swap(int* x, int* y) {
	int z = 0;
	z = *x;
	*x = *y;
	*y = z;
	return 0;
}

int main() 
{
	int a = 10;
	int b = 20;
	printf("%d %d\n", a, b);
	swap(&a, &b);	//传址调用
	printf("%d %d\n", a, b);


	return 0;
}

二.指针和数组

1.数组名的理解

在大部分情况下,数组名是数组首元素的地址,有两个例外:sizeof数组名,这里表示整个数组的大小,&数组名,这里的数组名也表示整个数组,取出整个数组地址

int main() 
{	
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("%p\n", arr);
	printf("%p\n", arr + 1);	//跳过了一个元素
	printf("%p\n", &arr[0] + 1);	//跳过了一个元素
	printf("%p\n", &arr + 1);	//跳过了一个数组
	printf("%d\n", sizeof(arr));
	printf("%d\n", sizeof(arr[0]));

	return 0;
}

2.指针表示数组

我们知道了,数组名实际上是数组第一个元素的地址,因此,指针表示数组可以写成

arr[i] == *(arr+i) == *(p + i) == p[i]

我们输出数组元素可以使用指针的表示:

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	//for (i=0; i < sz; i++) {
	//	scanf("%d", p + i);
	//}
	for (i=0; i<sz; i++) {
		printf("%d ", *(p+i));
	}

	return 0;
}

3.一维数组传参的本质

一维数组传参传过去的是数组首元素的地址

void test(int arr[])	//本质上arr是指针
{
	int sz2 = sizeof(arr) / sizeof(arr[0]);
	printf("%d\n", sz2);
}

int main()
{
	int arr[10] = { 0 };
	int sz1 = sizeof(arr) / sizeof(arr[0]);
	printf("%d\n", sz1);

	test(arr);

	return 0;
}

4.冒泡排序法

冒泡排序的思想

1.两两相邻的元素进行比较

2.如果不满足顺序就交换,满足顺序就找下一对

例如,现在给出我们一个数组,让我们将数组由小到大(升序)排列

过程大致如下

从头开始进行冒泡排序,一趟冒泡排序会排好最后一个的内容,我们如果需要排n个元素,最多需要进行n-1次,而且由于每次都可以排好一个,因此每次冒泡需要的次数越来越少

我们写出以下的代码实现了冒泡排序:

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-1-i; j++)
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
				flag = 0;
			}
		if (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[] = { 9,8,7,6,5,4,3,2,1 };
	//实现排序,排成升序
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr,sz);
	print_arr(arr, sz);
	return 0;
}

当然,我们所说的进行n-1次冒泡排序是我们考虑的最坏的结果,很多时候,在我们没有进行到n-1次时排序就已经完成,排序完成的标志是本次排序中没有进行过一次交换,因此,我们可以加入判断是否交换过来提前结束本次的排序

我们写出了如下函数:

void bubble_sort2(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
			if (*(arr+j) > *(arr+j+1))
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
	}
}

二级指针

一级指针指向的是其他变量,而二级指针是存放一级指针地址的

int main()
{
	int a = 10;
	int* p = &a;
	int** pp = &p;
	**pp = 20;
	printf("%d\n", a);

	return 0;
}

5.指针数组

指针数组是存放指针的数组,是一个数组,数组内容是指针

	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };
	int* arr[3] = {arr1, arr2, arr3};

其中int * arr[3]就是一个指针数组

我们可以使用指针数组来模拟实现二维数组

这样使用了指针数组来实现了二维数组的访问

int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };
	int* arr[3] = {arr1, arr2, arr3};

	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
		{
			printf("%d", arr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

三.字符指针,数组指针和函数指针

1.字符指针-存放字符的指针

字符指针是一个指针,指向的内容是字符/字符串

int main()
{
	char ch = 'w';
	char* pc = &ch;
	printf("%c\n", *pc);
	*pc = 'q';
	printf("%c\n", ch);

	return 0;
}

也可以拿字符指针指向字符串,代码如下

int main()
{
	char* p = "hello world";	
	return 0;
}

但是,对于上面的代码,我们如果尝试修改指针的内容:

int main()
{
	const char* p = "hello world";		
	* p = "hi world";
	return 0;
}

发现会报错,实际上,我们可以把字符串理解成字符数组,但并不完全是,区别是:数组是可以修改的,而这里的p指向的是常量字符串,不能修改,但是可以使用

int main()
{
	const char* p = "hello world";
	printf("%s\n", p);
	printf("%s\n", "hello world");
	
	int len = strlen(p);
	int i = 0;
	for (i = 0; i < len; i++)
	{
		printf("%c",*(p + i));
	}

	return 0;
}

这样我们就可以使用字符指针输出字符串啦。

2.数组指针

数组指针是指向数组的指针,是指针,存放的是数组

int main()
{
	int arr[10] = { 0 };
	int (*p)[10] = &arr;//取出的是数组的地址	数组指针
//	int *p[10]; 指针数组

	return 0;
}

请注意数组指针和指针数组的区别,这里写出了一些代码:

int main()
{
	char arr[5];
	char (*p1)[5] = &arr;	//char (*)[5]是数组指针类型
	//5不能省略掉
	char* p2 = arr;
	char* p3 = &arr[0];

	return 0;
}

char (*p1)[5] = &arr;定义了一个数组指针

    char* p2 = arr;
    char* p3 = &arr[0];定义的都是指针,指针指向的是arr数组

二维指针传参时候,二维数组的数组名是第一行的地址

我们在平时输出二维数组时,代码是这样的:

void test(int arr[3][5],int r,int 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");
	}
}

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;
}

我们可以利用数组指针来实现二维变量

void test_(int(*p)[5], int r, int c)
{
	int x = 0;
	for (x = 0; x < r; x++)
	{	
		int y = 0;
		for (y = 0; y < c; y++)
		{
			printf("%d ", (*(p+x))[y]);
		}
		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);
	test_(&arr, 3, 5);

	return 0;
}

3.函数指针

函数指针是一个指针,指向的是函数,存放的是函数的地址

int Add(int a, int b)
{
	return a + b;
}

int* test(char* s)
{
	return NULL;
}

int main()
{
	int (*pf)(int, int) = Add;
	int* (*ph)(char* s) = test;
	//int x = 10;
	//int y = 20;
	//int z = Add(x, y);
	printf("%p\n", &Add);
	printf("%p\n", Add);

	//&函数名和函数名都表示函数的地址

	return 0;
}

函数指针变量的写法和数组指针变量的写法类似

4.typedef重命名

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

typedef	unsigned int uint;

int main()
{
	unsigned int num1;
	uint num2;

	return 0;
}

在这里我们对指针类型进行重命名

typedef int* pint;

int main()
{
	int i = 10;
	int* p1 = &i;
	pint p2 = &i;
	
	return 0;
}

typedef和define的区别

我们知道两者都可以定义全局变量,两者的区别如下

typedef int* ptr_t;
#define PTR_T int*

int main()
{
	ptr_t p1, p2;//p1,p2是整型指针
	PTR_T p3, p4;//int *p3,p4
				//p3是指针,p4是整型
}

5.函数指针数组

如果把多个相同类型的函数指针存放在一个数组中,这个数组就是函数指针数组

我们写出如下的函数:

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 (*pf1)(int, int) = Add;
	int (*pf2)(int, int) = Sub;
	int (*pf3)(int, int) = Mul;
	int (*pf4)(int, int) = Div;
	
	return 0;
}

因此,我们可以创建一个函数指针数组,代码如下:

int main()
{
	
	int (*pfarr[4])(int, int) = {Add, Sub, Mul, Div};//pfArr就是函数指针
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		int ret =pfarr[i](8, 4);
		printf("%d\n", ret);
	}

	return 0;
}

这样我们就可以更简便的定义啦

如果让我们编写一个整型的加减乘除计算器,我们还可以写出一个函数

void Calc(int (*pf)(int, int))
{
	int x = 0;
	int y = 0;
	int ret = 0;
	printf("请输入两个操作数");
	scanf("%d %d", &x, &y);
	ret = pf(x, y);
	printf("%d", ret);
}
void menu()
{
	printf("**********************\n");
	printf("*****1.add  2.sub*****\n");
	printf("*****3.mul  4.div*****\n");
	printf("*****4.exit***********\n");
	printf("**********************\n");

}

int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请输入");
		scanf("%d", &input);
		switch(input)
		{
			case 1:
				Calc(Add);
				break;
			case 2:
				Calc(Sub);

				break;
			case 3:
				Calc(Mul);

				break;
			case 4:
				Calc(Div);

				break;
			case 0:
				printf("退出计算器");
				break;
			default:
				printf("选择错误");
				break;
		}
	} while (input);
	return 0;
}

这样就可以了

但我们发现,如果我们运用switch还是很麻烦,因此我们创建一个函数指针数组

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
	do
	{
		menu();
		printf("请输入");
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数");
			scanf("%d %d", &x, &y);
			ret = pfArr[input](x, y);
			printf("%d\n", ret);
		}
		else if (input == 0)
		{
			printf("退出");
			break;
		}
		else
		{
			printf("选择错误,重新选择");
		}
	} while (input);
	return 0;
}

这样就可以使得代码更加的优秀了

四.冒泡排序及其进阶版本

之前我们已经学过了冒泡排序的方法,

void bubble_sort(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int k = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = k;
			}
		}
	}
}

但是我们发现,这个只能排序整型数组,那么我们是否可以写出一个冒泡排序来排序所有我们想要放入的数组呢?

这里,我们引入qsort()函数

它的原型如下:

qsort(void* base,				//指针,指向数组的第一个元素,	
		size_t num,				//指针指向的待数组中的元素的个数,
		size_t size,				//元素的大小,
		int(*compar)(const void*, const void*)//函数指针,指向的是两个元素的比较函数)

我们可以仿照qsort函数来写出新的冒泡排序,详细代码如下:

void change_base(const void* p1, const void* p2, size_t width)
{
	int i = 0;
	for (i = 0; i < width; i++)
	{
		char a = *((char*)p1);
		*((char*)p1) = *((char*)p2);
		*((char*)p2) = a;

	}
}

void bubble_sort(void* base, size_t sz, size_t width, int (*p)(const void*, const void*))
{
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (p(((char*)base + width * j), ((char*)base + width * (j + 1))) > 0)
			{
				change_base(((char*)base + width * j), ((char*)base + width * (j + 1)), width);
			}
		}
	}
}

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

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

void int_test()
{
	int arr[] = { 1,5,8,23,56,44,32,7,6,66 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz, sizeof(arr[0]), Int_sort);
	print_int(arr, sz);
}

struct  Stu
{
	char name[100];
	int age;
};

int Char_sort(const void* p1, const void* p2)
{
	return (strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name));
}

void print_Stu(struct Stu arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%s ", arr[i].name);
		printf("%d ", arr[i].age);
	}
	printf("\n");
}

void stu_test()
{

	struct Stu arr[3] = { {"zhangsan",13},{"lisi",40},{"wangwu",22} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz, sizeof(arr[0]), Char_sort);
	print_Stu(arr, sz);
}


int main()
{
	int_test();
	stu_test();

	return 0;
}

感谢您的观看

  • 51
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值