C语言 —— 此去经年梦浪荡魂音 - 深入理解指针(卷一)

目录

[1. 内存和地址](#1. 内存和地址)

[2. 指针变量和地址](#2. 指针变量和地址)

[2.1 取地址操作符(&)](#2.1 取地址操作符(&))

[2.2 指针变量](#2.2 指针变量)

[2.3 解引用操作符 (*)](#2.3 解引用操作符 (*))

[3. 指针的解引用](#3. 指针的解引用)

[3.1 指针 + - 整数](#3.1 指针 + - 整数)

[3.2 void* 指针](#3.2 void* 指针)

[4. const修饰指针](#4. const修饰指针)

[4.1 const修饰变量](#4.1 const修饰变量)

[4.2 const修饰指针变量](#4.2 const修饰指针变量)

[5. 指针运算](#5. 指针运算)

[5.1 指针 ± 整数](#5.1 指针 ± 整数)

[5.2指针 - 指针](#5.2指针 - 指针)

[5.3 指针的关系运算](#5.3 指针的关系运算)

[6. 野指针](#6. 野指针)

[6.1 野指针成因](#6.1 野指针成因)

[6.2 如何规避野指针](#6.2 如何规避野指针)

[7. 指针的使用和传址调用](#7. 指针的使用和传址调用)

[7.1 strlen的模拟实现](#7.1 strlen的模拟实现)

[7.2 传值调用和传址调用](#7.2 传值调用和传址调用)


1. 内存和地址

什么是内存,我们先举个例子:假设有⼀栋宿舍楼,把你放在楼⾥,楼上有100个房间,但是房间没有编号,你的⼀个朋友来找你玩,如果想找到你,就得挨个房⼦去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号

一楼:101 102 103...
二楼:201 202 203...

以此类推...

有了房间号,如果你的朋友得到房间号,就可以快速的找房间找到你

将这个例子对应我们的计算机里面就是:

CPU在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,那我们买电脑的时候,电脑上内存是8GB/16GB/32GB等,这些内存空间是把内存划分为⼀个个的内存单元,每个内存单元的大小取1个字节(1个字节=8 个比特位)

每个内存单元就相当于每间酒店房间,每个房间能住 8 个比特位,房间(内存单元)都有一个门牌编号(地址),有了门牌号,就能快速找到相应的房间,即CPU能通过地址快速找到内存空间,在C语言中,给地址起了个名字叫指针
所以我们可以理解为:

内存单元的编号 == 地址 == 指针

1byte = 8bit
1KB = 1024byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
1PB = 1024TB

2. 指针变量和地址

2.1 取地址操作符(&)

创建变量其实就是向内存申请空间,调试下面代码:

#include <stdio.h>

int main()
{
	int a = 10;
	&a;//取出a的地址

	printf("%p\n", &a);

	return 0;
}

&a取出的是a所占4个字节中地址较⼩的字节的地址也就是第一个地址

虽然整型变量占用4个字节,我们只要知道了第⼀个字节地址,顺藤摸⽠访问到4个字节的数据也是可行的

2.2 指针变量

我们通过取地址操作符(&)拿到的地址是⼀个数值,这个数值有时候也是需要存储起来,⽅便后期再使用的,那我们就可以把这样的地址值存放在指针变量中

int main()
{
	int a = 10;
	int* pa = &a;//取出a的地址并存储到指针变量pa中

	return 0;
}

指针变量就是用来存放地址的,存放在指针变量中的值都可以当成为地址

2.3 解引用操作符 (*)

我们把地址存储在指针变量后要如何将存放在里面的东西取出使用呢?在知道地址的前提下,可以通过解引用操作符找到指针指向的对象

int main()
{
	int a = 100;
	int* p = &a;

	*p = 0;

	return 0;
}

上面这段代码就是p 通过解引用(*)找到 a 并将其值改成 0 ,就像是通过门牌号找到特定酒店房间里的特定物品,并将其替换


3. 指针的解引用

#include <stdio.h>

int main()
{
	int n = 0x11223344;
	int* pi = &n;

	*pi = 0;

	return 0;
}

调试一下这段代码,代码会将n的4个字节全部改为0,如果把指针类型改成 char ,那么代码只是将n的第⼀个字节改为0

int main()
{
	int n = 0x11223344;
	char* pc = (char*)&n;

	*pc = 0;

	return 0;
}

结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)

⽐如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字

3.1 指针 + - 整数

#include <stdio.h>

int main()
{
	int n = 10;
	char* pc = (char*)&n;
	int* pi = &n;

	printf("%p\n", &n);
	printf("%p\n", pc);
	printf("%p\n", pc + 1);
	printf("%p\n", pi);
	printf("%p\n", pi + 1);

	return 0;
}

char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节

也就是说:指针类型决定了指针加减整数时一步走多大距离

3.2 void* 指针

在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进⾏指针的+-整数和解引⽤的运算

#include <stdio.h>

int main()
{
	int a = 10;
	void* pa = &a;
	void* pc = &a;

	*pa = 10;
	*pc = 0;

	return 0;
}

这⾥我们可以看到, void* 类型的指针可以接收不同类型的地址,但是无法直接进⾏指针运算

那么 void* 类型的指针到底有什么⽤呢?

⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得⼀个函数来处理多种类型的数据


4. const修饰指针

4.1 const修饰变量

变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量

当我们希望⼀个变量加上⼀些限制,不能被修改那么这个时候我们就可以加上一个const

#include <stdio.h>

int main()
{
	int m = 0;
	m = 20;//m是可以修改的

	const int n = 0;

	n = 20;//n是不能被修改的

	return 0;
}

上述代码中n是不能被修改的,其实n本质是变量,只不过被const修饰后,在语法上加了限制,只要我们在代码中对n就⾏修改,就不符合语法规则,就报错,致使没法直接修改n
但是如果我们绕过n,使⽤n的地址,去修改n就能做到了

#include <stdio.h>

int main()
{
	const int n = 0;

	printf("n = %d\n", n);

	int* p = &n;
	*p = 20;

	printf("n = %d\n", n);

	return 0;
}

4.2 const修饰指针变量

当const修饰指针变量的时候:

1. const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本⾝的内容可变

int const * const p = &a;

当const放在*的左边时,限制的就是p所指向的内容,也就是&a

2. const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变
当const放在*的右边时,限制的就是p本身


5. 指针运算

5.1 指针 ± 整数

因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素

#include <stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];

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

	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));//p+i 这⾥就是指针+整数
	}

	return 0;
}

5.2指针 - 指针

代替 strlen 函数(计算字符或字符串长度),实现一个自定义的函数 my_strlen 来计算输入字符串的长度

#include <stdio.h>

int my_strlen(char* s)
{
	char* p = s;

	while (*p != '\0')
		p++;

	return p - s;
}

int main()
{
	printf("%d\n", my_strlen("abc"));

	return 0;
}

5.3 指针的关系运算

//指针的关系运算
#include <stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int i = 0;

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

	while (p < arr + sz) //指针的⼤⼩⽐较
	{
		printf("%d ", *p);
		p++;
	}

	return 0;
}

指针的关系运算,实际上就是:


6. 野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

6.1 野指针成因

1. 指针未初始化

#include <stdio.h>

int main()
{
	int* p;//局部变量指针未初始化,默认为随机值
	*p = 20;

	return 0;
}

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

当循环执行到 i = 10 及之后时,指针 p 已经超出了数组 arr 的范围指向了数组 arr 所占用内存空间之外的未知区域,此时 p 就变成了野指针

3. 指针指向的空间释放

6.2 如何规避野指针

1.对指针变量都进行初始化操作

2.注意数组等变量的范围,小心指针越界

3.指针不使用时,及时置之为NULL空指针

4.不要返回局部变量的地址


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

7.1 strlen的模拟实现

库函数strlen的功能是求字符串⻓度,统计的是字符串中 \0 之前的字符的个数

size_t strlen ( const char * str );

参数str接收⼀个字符串的起始地址,然后开始统计字符串中 \0 之前的字符个数,最终返回⻓度

如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是 \0 字符,计数器就+1,这样直到 \0 就停⽌
strlen链接:

strlen - C++ Referencehttps://legacy.cplusplus.com/reference/cstring/strlen/?kw=strlen

int my_strlen(const char* str)
{
	int count = 0;

	assert(str);

	while (*str)
	{
		count++;
		str++;
	}

	return count;
}

int main()
{
	int len = my_strlen("abcdef");
	printf("%d\n", len);

	return 0;
}

7.2 传值调用和传址调用

先写一个普通的代码:

#include <stdio.h>

void Swap1(int x, int y)
{
	int tmp = x;
	x = y;
	y = tmp;
}

int main()
{
	int a = 0;
	int b = 0;

	scanf("%d %d", &a, &b);
	printf("交换前:a=%d b=%d\n", a, b);

	Swap1(a, b);

	printf("交换后:a=%d b=%d\n", a, b);

	return 0;
}

我们发现其实没产⽣交换的效果,调试⼀下

Swap1函数在使⽤的时候,是把变量本⾝直接传递给了函数,这种调⽤函数的⽅式我们之前在函数的时候就知道了,值是会出了作用域就会自动销毁,这种叫传值调用

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

我们使用指针的方法:

void Swap2(int* px, int* py)
{
	int tmp = 0;

	tmp = *px;
	*px = *py;
	*py = tmp;
}

int main()
{
	int a = 0;
	int b = 0;

	scanf("%d %d", &a, &b);
	printf("交换前:a=%d b=%d\n", a, b);

	Swap1(&a, &b);
	printf("交换后:a=%d b=%d\n", a, b);

	return 0;
}

我们可以看到实现成Swap2的⽅式,顺利完成了任务,这⾥调⽤Swap2函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫:传址调用
传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量

所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改主调函数中的变量的值,就需要传址调⽤


完结撒花~

相关推荐
神里流~霜灭35 分钟前
贪心算法简介(greed)
c语言·数据结构·c++·链表·贪心算法·动态规划·顺序表
落幕1 小时前
在线商城服务器
linux·服务器·c语言
若云止水4 小时前
ngx_openssl_conf_t
c语言·nginx
最后一个bug5 小时前
C语言为例谈数据依赖性
linux·c语言·开发语言·arm开发·stm32
BUG 劝退师7 小时前
C语言学习总结
c语言·学习·算法
XH华7 小时前
C语言刷题第五章(下)
c语言·算法
衡玖8 小时前
c语言闯算法--常用技巧
c语言·数据结构·算法
愈努力俞幸运8 小时前
vs code配置 c/C++
c语言
头发尚存的猿小二8 小时前
2024年第十五届蓝桥杯软件C/C++大学A组——五子棋对弈
c语言·c++·蓝桥杯
Cindy辛蒂9 小时前
C语言:计算并输出三个整数的最大值 并对三个数排序
c语言·数据结构·算法