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

时间:2024-03-02 17:10:01

一.指针基础

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