C指针 --- 初阶

news2024/11/17 14:38:34

目录

1. 指针是什么

2. 指针和指针类型

1. 指针 +- 整数

2. 不同指针类型的解引用

3. 野指针

3.1. 野指针的形成原因:

1. 指针未初始化

2. 已释放的指针

3. 悬挂指针

3.2. 如何规避野指针

4. 指针运算

4.1. 指针 +- 整数

4.2. 指针 - 指针

4.3. 指针的关系运算

5. 指针和数组

6. 二级指针

7. 指针数组


1. 指针是什么

每个内存都有唯一的一个编号,这个编号也被称之为地址,也就是我们的指针。

指针是一个变量,它的值为另一个变量的内存地址(内存的编号)。它允许通过内存地址直接访问和修改变量的值。

可以将指针视为一个包含内存地址的容器,通过该内存地址可以找到存储在计算机内存中的数据。

一个小的单元一个字节。对于32位机器,假设有32根地址线,那么每根地址线在寻址的时候产生一个电信号正电/负电(1或者0),那么总共就会有2^32个地址,每个地址表示唯一的一个字节。我们知道,2^10 = 1024,那么2^30 = 1024 * 1024 * 1024 = 1GB,也就是说,2^32 个字节 = 4GB。

因此我们可以明白,在32位系统下,一个指针需要32个bit位,也就是4个字节。而在64位系统下,一个指针需要64个bit位,即8个字节。

void Test1(void)
{
    // 在内存中开辟一块空间给i这个整形
	int i = 10;
    // &a 可以取出a的地址, 其类型是 int*
    // 定义一个指针变量p,将i的地址赋值给p
	int* p = &i;
}

总结:

指针 == 地址 == 内存的唯一编号

指针(这里特指指针变量),用来存放地址的,地址是一块空间的唯一标识。
指针的大小在32位平台是4个字节,在64位平台是8个字节。
注意:我们平常口语中说的指针,通常是一个指针变量

2. 指针和指针类型

通过上面我们已经知道,指针本质是一块空间的唯一标识,是一个编号。那么指针类型又是什么呢?指针类型可以决定什么呢?

简单来讲,指针类型,就是你指向的数据的类型 + *。例如:

void Test2(void)
{
	int i = 10;
	int* i_ptr = &i;

	double d = 1.1;
	double* d_ptr = &d;

	char ch = 'a';
	char* c_ptr = &ch;
}

i_ptr指向的数据的类型是一个int,那么它的指针类型就是一个int*

同理,d_ptr指向的数据的类型是一个double,那么指针类型就是double*

同理,c_ptr指向的数据的类型是一个char,那么指针类型就是char*

那么指针类型有什么用呢?请看下面的例子:

1. 指针 +- 整数

void Test3(void)
{
	int i = 10;
	char ch = 'x';
	int* i_ptr = &i;
	char* c_ptr = &ch;
	printf("original address:> %p\n", i_ptr);
	printf("changed address:> %p\n", i_ptr + 1);
	printf("original address:> %p\n", c_ptr);
	printf("changed address:> %p\n", c_ptr + 1);
}

结果如下:

original address:> 0095FCD4
changed address:> 0095FCD8
original address:> 0095FCCB
changed address:> 0095FCCC 

通过结果,我们可以看到,指针的类型决定了  指针 + 一个整数向前或者向后移动多远。

具体,如果是一个int*,那么指针 + 1相当于,向后走了4个字节,那如果是type*,那么指针+n 相当于跳过了 n*sizeof(type)个字节。

2. 不同指针类型的解引用

void Test4(void)
{
	int num1 = 0x12345678;  // 0x开头的是一个 十六进制数
	char* c_ptr = (char*)&num1;
	*c_ptr = 0;

	int num2 = 0x12345678;
	int* i_ptr = &num2;
	*i_ptr = 0;
}

上面的代码不同之处在于,第一个是一个char*的指针指向一个int,并通过解引用将其置为0,第二个是一个int*的指着指向一个int,也通过解引用将其置为0,那它们有什么不一样吗?

我们看num1在内存中的变化情况:

初始态:

置为0后:

我们观察num2在内存的变化情况:

初始态:

置为0后:

通过对比,我们可以得出指针类型的另一个作用:指针类型决定了对指针进行解引用是能访问几个字节(指针的权限)。例如上面,如果你是一个int*,那么你就能访问四个字节。如果你是一个char*,那么你只能访问一个字节。

也就是从 type* ptr ,我们可以得出,ptr是一个指针变量,p指向的对象的类型是type,p解引用的时候访问的对象的的大小是 sizeof(type);

3. 野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,其行为是未定义的。

3.1. 野指针的形成原因:

1. 指针未初始化

未初始化指针:当一个指针被声明但未被初始化时,它可能包含一个无效的内存地址。这样的指针是不安全的,使用时可能导致程序出错,例如,下面的情况:

void Test5(void)
{
	int* ptr;  
	*ptr = 20;  // 这个指针是一个野指针,对其解引用,其行为未定义
}

2. 已释放的指针

已释放的指针:当一个指针指向的内存被释放或销毁后,该指针就变成了野指针。在使用已释放的指针时,会访问到无效的内存地址,可能导致程序崩溃或产生不可预料的错误。

void Test6(void)
{
	int* ptr = new int;
	delete ptr;  // 释放了该指针,那么此时这个指针就是一个野指针
	std::cout << *ptr << std::endl;  // 非法访问,进程crash
}

3. 悬挂指针

悬挂指针:在某些情况下,指针可能指向了被销毁或超出作用域的对象。这样的指针也称为悬挂指针,使用悬挂指针可能导致未定义的行为

void Test7(void)
{
	int arr[5] = { 0 };
	int* ptr = arr;
	for (int i = 0; i < 6; ++i)
	{
		// 当i等于5时,此时ptr就是一个野指针
		*(ptr++) = i;
	}
}

3.2. 如何规避野指针

1. 指针初始化

如果知道指针应该初始化为谁的地址,就直接初始化。
不知道指针初始化为什么的地址,初始化为NULL
void Test9(void)
{
	int i = 10;
	// 如果知道初始化为谁的地址,就直接初始化
	int* ptr1 = &i;
	// 如果不知道,就初始化为NULL
	// 注意: 在C中 NULL ---> ((void*)0)
	// 在C++中, NULL ---> 0
    //  ptr2是一个空指针,没有指向任何有效的空间,这个指针不能直接使用
	char* ptr2 = NULL;  
}

2. 防止指针越界

3. 指针指向空间的释放,就将其置为NULL
4. 指针使用之前检查有效性
5. 避免返回局部变量的地址

4. 指针运算

4.1. 指针 +- 整数

如前面所说,指针 +- 整数,需要看这个指针的类型是什么。其指针类型决定了 +- 的长度。例如:

void Test8(void)
{
	int i = 10;
	int* i_ptr = &i;
	printf("%p 'int*:+1'---> %p\n", i_ptr, i_ptr + 1);
	printf("%p 'int*:-1'---> %p\n", i_ptr, i_ptr - 1);

	char ch = 'a';
	char* c_ptr = &ch;
	printf("%p 'char*:+1'---> %p\n", c_ptr, c_ptr + 1);
	printf("%p 'char*:-1'---> %p\n", c_ptr, c_ptr - 1);
}

结果:

00D6FA88 'int*:+1'---> 00D6FA8C
00D6FA88 'int*:-1'---> 00D6FA84
00D6FA73 'char*:+1'---> 00D6FA74
00D6FA73 'char*:-1'---> 00D6FA72 

总而言之,指针 +- 整数,需要看这个指针的类型是什么,如果是int*,+1,就会跳过四个字节,如果是char*, +1,就会跳过1个字节。

理解了上面,我们可以看看下面的例子: 

void Test10(void)
{
	int arr[10] = { 0 };
	int* ptr = &arr[0];    // 数组的首地址
	int* ptr1 = arr;      // 数组名也是首元素的低地址
	for (size_t i = 0; i < 10; ++i)
	{
		//arr[i] = i;
		*(ptr + i) = i;
		*(ptr1 + i) = i;
		// 本质来说, 编译器对于arr[i]的处理
		// 会将其转化为 *(arr + i) , arr 就是数组的首地址
	}

	// 我们知道 arr[i] 等价于 *(ptr + i)
	// 而根据加法具有交换律 *(ptr + i) 也等价于 *(i + ptr)
	// 那么 arr[i] 是不是也可以写成 i[arr]呢 ?
	// 我们验证一下
	for (size_t i = 0; i < 10; ++i)
	{
		std::cout << i[arr] << " ";
	}
	std::cout << std::endl;
}

结果:0 1 2 3 4 5 6 7 8 9

在这里想说的是,对于[]而言,它只是一个操作符(下标引用操作符),而i 和 arr是这个操作符的两个操作数,因此 arr[i] 也可以写成 i[arr],但正常情况下我们不建议写成i[arr],还是写成arr[i]。

4.2. 指针 - 指针

void Test11(void)
{
	int arr[10] = { 0 };
	std::cout << &arr[9] - &arr[0] << std::endl;
	std::cout << &arr[0] - &arr[9] << std::endl;
}

结果: 

9

-9

地址 - 地址得到的数据的绝对值 : 是指针和指针之间的元素个数。

指针 - 指针的前提条件: 这两个指针必须指向同一段空间的两个地址,否则,没有意义。

4.3. 指针的关系运算

void Test12(void)
{
	int arr[5];
	int *ptr;
	// 从最后一个元素的下一个位置的地址开始,与第一个元素的地址比较
	for (ptr = &arr[5]; ptr > &arr[0];)
	{
		*(--ptr) = 0;
	}
	// 从最后一个元素的地址开始,与第一个元素的地址比较
	for (ptr = &arr[4]; ptr >= &arr[0]; --ptr)
	{
		*ptr = 0;
	}
}

C标准规定允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。

5. 指针和数组

首先,指针不是数组,数组也不是指针。

指针变量就是指针变量,不是数组,指针变量的大小是4/8个字节。专门是用来存放地址的。

数组就是数组,不是指针。数组是一块连续的空间,可以存放一个或多个类型相同的数据。

它们的联系:

在数组中, 数组名是数组首元素的地址,可以理解为数组名是一个地址。当我们知道数组首元素的地址的时候,又因为数组是连续存放的,因此可以通过数组的首元素的地址来访问它的数据。

void Test13(void)
{
	int arr[5] = { 1, 2, 3, 4, 5 };
	// 数组名是数组首元素的地址,int类型的地址,是一个int*
	// 因此 我们在这里用一个int* 接收
	int* ptr = arr;
	for (size_t i = 0; i < 5; ++i)
	{
		printf("&arr[%d] = %p <==> ptr+%d = %p\n", i, &arr[i], i, ptr + i);
	}
}

&arr[0] = 012FF8F4 <==> ptr+0 = 012FF8F4
&arr[1] = 012FF8F8 <==> ptr+1 = 012FF8F8
&arr[2] = 012FF8FC <==> ptr+2 = 012FF8FC
&arr[3] = 012FF900 <==> ptr+3 = 012FF900
&arr[4] = 012FF904 <==> ptr+4 = 012FF904

通过结果,我们可以得知:ptr + i  就是这个数组下标为i的元素的地址。

通常讲,我们说一个数组的数组名就是首元素的地址。但注意有两种特殊情况:

void Test16(void)
{
	int arr[5] = { 1, 2, 3, 4, 5 };

	// 一般情况下,数组名代表着数组首元素的地址
	// 但有两种特殊情况
	// 第一: sizeof 数组名
	// 注意 :sizeof 数组名 代表着数组的大小
	std::cout << sizeof arr << std::endl;

	for (size_t i = 0; i < 5; ++i)
	{
		printf("&arr[%d] = %p\n", i, &arr[i]);
	}
	std::cout << " --------------- " << std::endl;
	// 第二: &数组名
	// 数组名 + 1 会跳过一个元素
	// &数组名 + 1 会跳过整个数组大小,即跳过sizeof arr 个字节
	printf("arr = %p\n", arr);
	printf("arr + 1 = %p\n", arr + 1);
	printf("&arr = %p\n", &arr);
	printf("&arr + 1 = %p\n", &arr + 1);
}

20
&arr[0] = 0024F864
&arr[1] = 0024F868
&arr[2] = 0024F86C
&arr[3] = 0024F870
&arr[4] = 0024F874
 ---------------
arr = 0024F864
arr + 1 = 0024F868
&arr = 0024F864
&arr + 1 = 0024F878

6. 二级指针

什么叫二级指针呢?简单的理解,一级指针变量的地址(指针)就是二级指针,即可以用二级指针变量存储一级指针变量的地址。

int** pp 的理解, 第二个*,我们可以理解为 pp是一个指针变量,int* 说明指针变量pp所指向空间中的存储的数据的类型是一个int*,因此,我们说pp是一个二级指针变量。

void Test14(void)
{
	int a = 10;
	// a的数据类型是一个int,那么&a的类型就是int*
	// 因此在这里用 int* p 接收 &a
	int* p = &a;
	// p的数据类型是一个int*,那么&p的数据类型是int**
	// 因此在这里用 int** pp 接受 &p
	int** pp = &p;
}

7. 指针数组

什么叫指针数组呢?指针数组是一个数组,其每一个元素是一个指针变量。

void Test15(void)
{
    // 数组中的每一个元素类型为  int
	int arr1[3] = { 1, 2, 3 };
	int arr2[3] = { 2, 3, 4 };
	int arr3[3] = { 3, 4, 5 };
    
    // 数组中的每一个元素类型为 int*
    // p_arr 就是一个指针数组,该数组的每一个元素都是一个int* 
	int* p_arr[3] = { arr1, arr2, arr3 };

	for (size_t i = 0; i < 3; ++i)
	{
		for (size_t j = 0; j < 3; ++j)
		{
			std::cout << *(*(p_arr + i) + j) <<" ";
		}
		std::cout << "\n";
	}

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1125257.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言实现调整数组中奇数偶数顺序

目录 1.思路2. 代码 1.思路 给定两个下标left和right&#xff0c;left放在数组的起始位置&#xff0c;right放在数组中最后一个元素的位置循环进行一下操作 a. 如果left和right表示的区间[left, right]有效&#xff0c;进行b&#xff0c;否则结束循环 b. left从前往后找&#…

关于 硬盘

关于 硬盘 1. 机械硬盘1.1 基本概念1.2 工作原理1.3 寻址方式1.4 磁盘磁记录方式 2. 固态硬盘2.1 基本概念2.2 工作原理 1. 机械硬盘 1.1 基本概念 机械硬盘即是传统普通硬盘&#xff0c;硬盘的物理结构一般由磁头与盘片、电动机、主控芯片与排线等部件组成。 所有的数据都是…

网络拓扑图怎么画最好?

你们好&#xff0c;我的网工朋友。 好久没和你们聊拓扑图了&#xff0c;群里总是不乏有人问&#xff0c;拓扑图怎么设计&#xff0c;怎么配置&#xff0c;或者让大佬看看自己做的这图有没有啥问题的…… 画拓扑图的方式有很多&#xff0c;在线软件&#xff0c;Visio&#xff…

比例运算放大电路为什么要加平衡电阻

这个是反相比例运算放大电路&#xff0c;输出电压等于-Rf/R1乘以输入电压。 这个是同相比例运算放大电路&#xff0c;输出电压等于1Rf/R1乘以输入电压。 大家可以看到这两个电路中&#xff0c;都有一个电阻R2&#xff0c;反相比例运算放大电路放在同相端到地&#xff0c;同相比…

空间地图GIS基础

一、GIS基本概念 地理信息系统&#xff08;Geographic Informaiton System, GIS&#xff09;是一个可以建立、浏览、查询、分析地理空间数据的软件系统&#xff0c;其功能小至地图的展示&#xff0c;大至空间决策分析与支持。 1.GIS基础 (1)地理信息系统(GIS)的概念与组成 …

# 开发趋势 Java Lambda 表达式 第三篇

开发趋势 Java Lambda 表达式 第三篇 一&#xff0c;Lambda 整合集合常规操作 List Java Lambda 表达式可以与List集合和常规操作进行整合&#xff0c;以提供一种更简洁、更可读的代码编写方式。以下是几个示例&#xff1a; 集合遍历操作&#xff1a; List<String> n…

PI证书导入总结

当我们在用pi调用https的方式时&#xff0c;接口会报错提示iaik.security.ssl.SSLCertificateException。这需要我们导入对应的证书。 一.下载证书 根据对方提供的url &#xff0c;在浏览器中输入&#xff0c;点击锁头图标&#xff0c;点击证书信息 二.点击详细信息标签&…

【蓝桥每日一题]-动态规划 (保姆级教程 篇11)#方格取数2.0 #传纸条

目录 题目&#xff1a;方格取数 思路&#xff1a; 题目&#xff1a;传纸条 思路&#xff1a; 题目&#xff1a;方格取数 &#xff08;跑两次&#xff09; 思路&#xff1a; 如果记录一种方案后再去跑另一个方案&#xff0c;影响因素太多了&#xff0c;所以两个方案要同时开…

Mysql数据库 3.SQL语言 DML数据操纵语言 增删改

DML语句&#xff1a;用于完成对数据表中数据的插入、删除、修改操作 一.表数据插入 插入数据语法&#xff1a; 步骤例&#xff1a; 1.声明数据库&#xff1a;use 数据库名; 2.删除操作&#xff1a;drop table if exists 表名; 3.创建数据库中的表&#xff1a;create table 表…

企业云网盘:如何选择最适合您的解决方案?

企业日常办公每天都会产出大量的文件&#xff0c;如何安全管理文件&#xff1f;企业如何进行高效的文件的共享&#xff1f;企业云网盘产品为企业提供了一个文件解决方案&#xff0c;其安全便捷的特点已成为文件数据管理的热门之选。然而越来越多的品牌进入了企业云网盘市场&…

C++ 学习 之 名字空间 namespace

必须在模块里面 extern 声明 在一个 cpp 文件中&#xff0c; 一个namespace 可以多次定义&#xff0c;最后合并&#xff0c;使用 using namespace A 这种引入方式的话&#xff0c;使用的时候可以用所有 A 中的数据 多个 cpp 文件的话&#xff0c;不能会自动合并相同的 名字空…

【双向链表的插入和删除】

文章目录 双向链表双向链表的插入双向链表的删除操作 双向链表 双向链表的结构定义如下&#xff1a; //双向链表的结构定义 typedef struct DuLNode {ElemType data;struct DuLNode* prior, * next; }DuLNode,*DuLinkList;双向链表的结点有两个指针域&#xff1a;prior&#…

【带你找回童年的快乐,Python实现坦克大战】

文章目录 前言&#xff1a;第一步&#xff1a;安装Pygame库第二步&#xff1a;实现思路&#xff1a;场景实现&#xff1a;石头墙&#xff1a;钢墙&#xff1a;地面类&#xff08;Grass&#xff09;地图&#xff1a; 第三步&#xff1a;坦克类的详细实现&#xff1a;坦克类&…

国际刑事法院系统遭网络间谍攻击

导语&#xff1a;国际刑事法院&#xff08;ICC&#xff09;近日披露&#xff0c;其系统遭受了一次网络间谍攻击。这次攻击被认为是有目的的&#xff0c;旨在进行间谍活动。作为一个国际法庭&#xff0c;ICC的总部设在荷兰海牙&#xff0c;其职责是调查并追究犯下国际社会关注的…

微机原理与接口技术-第七章输入输出接口

文章目录 I/O接口概述I/O接口的典型结构基本功能 I/O端口的编址独立编址统一编址 输入输出指令I/O寻址方式I/O数据传输量I/O保护 16位DOS应用程序DOS平台的源程序框架DOS功能调用 无条件传送和查询传送无条件传送三态缓冲器锁存器接口电路 查询传送查询输入端口查询输出端口 中…

【Linux系统编程】命令模式2

目录 一&#xff0c;Linux下的初阶认识 1&#xff0c;管道 2&#xff0c;时间戳 二&#xff0c;Liunx系统命令操作 1&#xff0c;date时间指令 2&#xff0c;cal日历指令 3&#xff0c;which和find查找指令 3-1&#xff0c;which指令&#xff1a; 3-2&#xff0c;find…

c语言一维数组和二维指针

c语言一维数组和二维指针&#xff1a;测试目的&#xff0c;了解二维指针赋值。 #include <stdio.h> //c语言一维数组和二维指针 int main(int argc,char *argv[]) { int MyArray[2]; int *p1; int **p2; //int **p2&p1;//在声明变量时&#xff0c;可以这么赋值 …

如何保养维护实验室超声波清洗机

实验室是用于各个行业产品的研发以及检验的场所&#xff0c;所以对其中所使用的各种物品都有着极高的要求&#xff0c;因此只有品类齐全的实验室超声波清洗机能够满足实验室对于清洁以及其他方面的一些需求&#xff0c;但是要想实验室超声波清洗机设备的性能能够始终如一&#…

后 Cookie 时代如何实现精准的用户运营与管理?

Cookie 时代的营销&#xff1a;用隐私换取个性化服务方式帮助他们找到回家的路。而在数字世界中&#xff0c;网站运营者可以通过这些“碎饼干屑 &#xff08;Cookie&#xff09;”追踪用户行为。 用户隐私和个性化服务之间的平衡一直是一个备受争议的话题。随着技术的发展&…

二叉排序树(BST)

二叉排序树 基本介绍 二叉排序树创建和遍历 class Node:"""创建 Node 节点"""value: int 0left Noneright Nonedef __init__(self, value: int):self.value valuedef add(self, node):"""添加节点node 表示要添加的节点&quo…