深入理解指针(1)

前言
本章我们将学习指针这一难点,本小节会给大家讲解指针变量,指针类型及其指针的简单运用等等


文章目录

  • 一、初识指针
    • 1.1 内存和地址
      • 1.1.1 内存和指针关系
      • 1.1.2 计算机如何编址
  • 二、 指针变量
    • 2.1 取地址操作符(&)
    • 2.2 解引用操作符(*)
    • 2.3 指针变量的大小
  • 三、 指针变量类型
  • 四、指针的运算
    • 4.1 指针+- 整数
    • 4.2 指针 -- 指针
    • 4.3 指针的关系运算
  • 五、野指针
    • 5.1 野指针的形成
    • 5.2 如何规避野指针
  • 六、assert断言
  • 七、const
  • 八、strlen的模拟实现

一、初识指针

1.1 内存和地址

1.1.1 内存和指针关系

在讲解指针前,我们先了解一下计算机是如何存储数据和寻找数据的。

内存:内存是计算机用来存储数据和程序的地方。它是一个存储设备,用于临时保存正在使用的数据和指令,以便中央处理单元(CPU)可以快速访问它们。

地址:地址是用来标识内存中特定位置的值。在计算机内存中,每个存储单元都有一个唯一的地址,这样CPU可以通过指定地址来访问内存中的特定数据。

生活中普遍存在这样的例子,我们不妨将内存比作宿舍楼,地址即是门牌号,当我们想要找到一间房,只要知道门牌号,就可以轻松找到。
深入理解指针(1)_第1张图片
在我们的计算机中拥有不同大小的内存,比如8GB/16GB/32GB等等,而计算机将内存分为一个个内存单元,一个内存单元就是一个字节(byte),一个字节等于8个比特位(bit)。这是最小的单位,且一个比特位只能存储数字0或1。深入理解指针(1)_第2张图片

每个内存单元都有一个编号,在计算机语言中我们称这个编号为指针(它们是一一对应关系),也是地址。

深入理解指针(1)_第3张图片

1.1.2 计算机如何编址

首先CPU和内存之间的“交流”是通过计算机硬件中的多个组件共同协作来实现的。地址总线就是其中之一,我们仅了解地址总线。

地址总线: 地址总线是一组物理导线,用于在CPU(中央处理单元)和内存之间传递地址信息。它决定了计算机能够寻址的地址范围。地址总线的位数决定了计算机的地址空间大小,即可以寻址的存储单元数量。

我们可以这样认为,32位计算机有32根地址总线,每根线只有两态,表⽰0,1【电脉冲有⽆】,那么⼀根线,就能表⽰2种含义,2根线就能表⽰4种含义,依次类推。32根地址线,就能表示2^32种含义,每⼀种含义都代表⼀个地址。

深入理解指针(1)_第4张图片
CPU下达地址信息,通过地址我们可以在内存中找到相应的数据。

二、 指针变量

2.1 取地址操作符(&)

我们可以看到地址0x00AFFC14(实际上0x00AFFC14之后三个15,16,17也是变量a的地址,因为int类型占用四个字节,且是从低地址开始使用),正是存放着我们给变量a的值。因为是16进制,所以显示的是05.如果是10进制则是00000005,正好对应了地址和内存单元一一对应,且一个内存单元等于一个字节,等于8个比特位

深入理解指针(1)_第5张图片
另外printf(“%d”,&a)也可以直接计算出a的地址,前提条件是必须加上取地址操作符&。 这里的地址只会打印出首地址,通过首地址,且 int 占4个字节,便可以找到剩下的三个字节的地址。
在这里插入图片描述

2.2 解引用操作符(*)

我们通过&可以拿到一个变量的地址,为了方便使用,我们要将这个地址存起来。

我们创建一个指针变量,将这个地址存起来。

#include 
int main()
{
 int a = 10;
 //取a的地址(&a)放入pa中,pa的值就是a的地址
 //通过解引用符*,*pa就是a的值。
 int* pa = &a;
 
 return 0;

2.3 指针变量的大小

在“在计算机如何编址”中我们了解到,32位计算机有32根地址总线,即32位的二进制序列,也即是32个比特位,4个字节。

所以我们得出结论,32位计算机的指针大小(地址编码的大小)为4个字节。同理,64位的指针的大小为8个字节。需要注意一点的是,指针变量的大小与指针类型无关,只与操作环境的位数有关。

深入理解指针(1)_第6张图片

三、 指针变量类型

我们必须要区分清指针变量类型和指针所指向的类型

指针变量类型:指针变量能够存储哪种类型的地址。
指针所指向的类型:指针所指向的内存地址中存储的数据类型。

举例:我们初始化一个指针变量
其中 int* 是指针变量类型,而 int 则是指针所指向的类型

int a=0;
int* pa=&a;

在不同指针变量类型存储同一类型数据的地址时,对指针解引用并赋值时的不同之处

#include 
int main()
{
     //代码一
	int n = 0x11223344;
	int* pi = &n;
	*pi = 0;
	printf("%d",n);
	return 0;
}

输出结果在这里插入图片描述


#include 
int main()
{
    //代码二
	int n = 0x11223344;
	char* pi = &n;
	*pi = 0;
	printf("%d",n);
	return 0;
}

输出结果在这里插入图片描述

观察代码一和代码二,为什么会出现这两种结果?
首先我们赋给了n十六进制的整数,接着又取n地址存放在指针pi中,代码一和代码二唯一的区别就是指针所指的类型的不同。
在代码一中,首先n的类型和指针pi所指向的类型相同,我们在将0赋给*p时(相当于赋值给了n),又因为都是 int 类型,所以十六进制数的每一位都被改成了0。
在代码二中,指针所指的类型是char,char类型是一个字节大小,而 int 是四个字节,在0赋值给*p时只会把四个地址中首地址存放的数据更改(所以0x11223344会变成0x11223300)。在控制台上输出的结果是十进制,我们换成十六进制。与输出结果相同。
深入理解指针(1)_第7张图片

四、指针的运算

4.1 指针± 整数

数组的元素在内存中是连续存放的,这意味着找到首元素地址,及数组大小就能找到整个数组

#include 
//打印整个数组
//指针 +- 整数
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;
}

4.2 指针 – 指针

//求字符串中字符个数
//指针-指针
#include 
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;
}

需要注意的是,没有指针+指针这个概念,这是无意义的操作

4.3 指针的关系运算

//指针的关系运算
#include 
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;
}

五、野指针

野指针:是指一个指针持有一个无效的、不再有效的内存地址。当指针指向的内存地址已经被释放或者不再包含有效数据时,该指针就成为野指针。野指针可能会导致程序运行时的未定义行为,因为对无效内存地址的访问可能会导致数据损坏、崩溃或其他问题。

5.1 野指针的形成

以下是野指针的形成

  • 指针未初始化
#include 
int main(){ 
 int *p;//局部变量指针未初始化,默认为随机值
 *p = 20;
 return 0;
}
  • 指针越界访问
#include 
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;
}
  • 指针指向的空间释放

如果程序能正常运行并且输出确实是100,那可能是由于编译器的优化或其他因素导致的。在理论上,这个程序应该会导致未定义的行为,因为 p 指向一个已经被销毁的变量的地址。

请记住,在实际编程中,避免使用指向局部变量的指针,尤其是在函数退出后使用,以避免产生野指针和未定义行为。

#include 
int* test(){
 int n = 100;
 return &n;
}

int main(){
 int*p = test();
 //函数调用之后,
 printf("%d\n", *p);
 return 0;
 }

5.2 如何规避野指针

  1. 声明指针时,记得初始化

NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。

int* p=NULL;
  1. 当不再使用指针时,释放该指针,并赋值NULL
int a=5;
int* p=&a;
if(NULL!=p){
    delete p;
    p=NULL;
}

六、assert断言

还有一种预防野指针的方法

assert 是一个在 C 和 C++ 等编程语言中常用的宏,用于在程序中进行断言。断言是一种在代码中插入的检查,用于验证程序的假设是否成立。如果断言的条件为假(即假设不成立),则会触发断言错误,通常会导致程序终止。

assert 宏的一般用法是将一个表达式作为参数传递给它,如果这个表达式的值为假(0),则 assert 会在运行时触发断言错误。如果表达式的值为真(非零),则程序会继续执行。

#include 
#include 

int main() {
    int* p= NULL;
    assert(p != NULL){
    
    } 
    return 0;
}

⼀般我们可以在debug中使⽤,在release版本中选择禁⽤assert就⾏,在VS这样的集成开发环境中,在release版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在release版本不影响⽤⼾使⽤时程序的效率。

七、const

const 是一个关键字,它可以用于不同的上下文,用来指定不同类型的常量。下面是 const 关键字的几个常见用法:

  1. 常量变量:通过将 const 放在变量声明之前,可以创建一个常量变量,其值在初始化后不能再被修改。
const int n = 10; // x 是一个常量,不能被修改

虽然我们不能通过赋值来修改n的值,但我们可以使用其地址,改变其值

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

深入理解指针(1)_第8张图片
为了防止通过指针来修改n的值或n的地址,可以在指针前加上const。

  1. 指向常量的指针:在指针类型前面加上 const,可以声明一个指向常量的指针,即该指针不能用来修改所指向的值。
int a=5;
//同 int const * ptr = &a; 
const int* ptr = &a; // ptr 是指向常量整数的指针,不能修改所指向的值
  1. 常量指针:将 const 放在指针变量名前面,可以创建一个常量指针,即该指针不能指向其他地址。
int x = 10;
int* const ptr = &x; // ptr 是一个常量指针,不能指向其他地址,但可以修改所指向的值

  1. 指向常量的常量指针:将 const 同时应用于指针类型和变量名,可以创建一个指向常量的常量指针,既不能修改所指向的值,也不能指向其他地址。
int x = 10;
const int* const ptr = &x; // ptr 是指向常量整数的常量指针,不能修改值,也不能指向其他地址

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

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

八、strlen的模拟实现

int my_strlen(const char * str){//传给str首字符地址
 int count = 0;
 assert(str);
 while(*str){//遇到\0退出循环
 count++;//计数
 str++;
 }
 return count;
}
int main(){
 int len = my_strlen("abcdef");
 printf("%d\n", len);
 return 0;
}

深入理解指针(1)_第9张图片

如果你喜欢这篇文章,点赞+评论+关注⭐️哦!
欢迎大家提出疑问,以及不同的见解。

你可能感兴趣的:(学习,c语言)