C语言指针精简版(一)

news2024/11/15 11:09:35

目录

理解内存、地址与指针之间的挂关系

编址与寻址(简单理解)

取地址操作符&

解引用操作符*

指针变量的大小

指针变量类型的意义

const修饰指针变量

const修饰变量

const修饰指针变量

指针运算

指针-整数

指针-指针

指针的运算关系

野指针

指针变量未初始化

指针的越界访问

指针指向的空间释放(下面程序运行后仍能输出200)

如何规避野指针

指针的初始化

assert宏(断言)

指针的使⽤和传址调⽤

传址调用

传值调用


理解内存、地址与指针之间的挂关系

        我们假设这里有一栋宿舍楼,楼里有很多个房间,每个房间都有自己的门牌号,每个房间中又会有多个床位。你的朋友正在这栋宿舍楼中的某个房间的某个床位上等你,那么你必须要做的就是知道你朋友的门牌号这样就可以快速的找到你的朋友,把上面的例子对照到计算机中,又是怎样的呢?

        我们知道CPU在处理数据的时候,这些数据都是在内存中读取的,处理后的数据也会返回到内存中,而我们买电脑的时候,电脑上的内存是8GB/16GB/32GB等,那这些内存空间如何⾼效的管理呢?

         其实就是将内存划分为一个个的内存单元,每个内存单元的大小取一个字节,这里的每个内存单元都相当于一个房间,内存单元中的八个比特位就相当于房间里的八个床位,既然每个内存单元都可以看作是是一个房间,那么这个内存单元就应该有它的门牌号,有了这个门牌号CPU就可以快速找到它所对应的内存空间。在计算机中我们把内存单元的编号(门牌号)称为 地址 ,C语言中给存储地址工具的起了个新名字: 指针  

编址与寻址(简单理解)

编址:

        存储器是由一个个存储单元构成的,为了对存储器进行有效的管理,就需要对各个存储单元编上号,即给每个单元赋予一个地址码,这叫编址。经编址后,存储器在逻辑上便形成一个线性地址空间。

寻址:

存取数据时,必须先给出地址码,再由硬件电路译码找到数据所在地址,这叫寻址

取地址操作符&

#include <stdio.h>
int main()
{
	int a = 10;
	return 0;
}

        在c语言中我们创建变量的过程其实就是在向内存申请一片内存空间,以int a = 10为例,我们可以看到a向内存申请了四个字节用于存放整数10:

0x000000ABECAFF7C4

0x000000ABECAFF7C5

0x000000ABECAFF7C6

0x000000ABECAFF7C7

通过取地址操作符得到整型变量a的地址:

#include <stdio.h>
int main()
{
	int a = 10;
	printf("%p\n", &a);
	return 0;
}

我们发现只取出了一个地址,这是因为取地址操作符获取地址时规定了只获取申请的最小地址

解引用操作符*

        上面我们通过&拿到a的地址在内存空间中申请的最低的地址:0x000000ABECAFF7C4 ,这个数值(地址)有时候也是需要存储起来⽅便后期再使⽤的,我们将它存储在 指针变量 中。
#include <stdio.h>
int main()
{
 int a = 10;
 int* pa = &a;//取出a的地址并存储到指针变量pa中
 return 0;
}

指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址

通过*我们就可以改变a在内存空间中存储的值:

#include <stdio.h>
int main()
{
 int a = 100;
 int* pa = &a;  //指针变量存储整型变量a的地址
 *pa = 20;      //通过对指针变量的解引用可以修改整型变量a内存空间中存储的值
 printf("%d",a);
 return 0;
}

指针变量的大小

        32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。指针变量是⽤来存放地址的,那么指针变的⼤⼩就得是4个字节的空间才可以。
        同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变的⼤⼩就是8个字节。
#include <stdio.h>
//指针变量的⼤⼩取决于地址的⼤⼩
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)
int main()
{
printf("%zd\n", sizeof(char *));
printf("%zd\n", sizeof(short *));
printf("%zd\n", sizeof(int *));
printf("%zd\n", sizeof(double *));
return 0;
}

 
结论:
• 32位平台下地址是32个bit位,指针变量⼤⼩是4个字节
• 64位平台下地址是64个bit位,指针变量⼤⼩是8个字节
• 指针变量的⼤⼩和类型⽆关,只要是指针类型的变量,同平台下,⼤⼩相同

指针变量类型的意义

        指针变量的⼤⼩和类型⽆关,只要是指针变量,在同⼀个平台下,⼤⼩都是⼀样的,为什么还要有各种各样的指针类型呢?
//代码一
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 int *pi = &n; 
 *pi = 0; 
 return 0;
}

//代码二
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 char *pc = (char *)&n;
 *pc = 0;
 return 0;
}
调试后我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第⼀个字节改为0
结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)
#include <stdio.h>
int main()
{
	int n = 10;
	char* pc = (char*)&n;
	int* pi = &n;
	printf("&n   = %p\n", &n);
	printf("pc   = %p\n", pc);
	printf("pc+1 = %p\n", pc + 1);
	printf("pi   = %p\n", pi);
	printf("pi+1 = %p\n", pi + 1);
	return 0;
}

      我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。 这就是指针变量的类型差异带来的变化

结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤

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可以吗? (虽然这样其实并不符合语法规则)
#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;
}

        我们可以看到这⾥n被修改了,但是我们还是要思考⼀下,为什么n要被const修饰呢?就是为了不能被修改,如果p拿到n的地址就能修改n,这样就打破了const的限制,这是不合理的,所以 应该让p拿到n的地址也不能修改n ,那接下来怎么做呢?

const修饰指针变量

#include <stdio.h>
测试一
//void test1()
//{
//	int n = 10;
//	int m = 20;
//	int* p = &n;
//	*p = 20;    //ok
//	p = &m;     //ok
//	printf("%d\n", m);
//	printf("%d\n", n);
//}

测试二
//void test2()
//{
//	int n = 10;
//	int m = 20;
//	const int* p = &n;
//	*p = 20;    //no
//	p = &m;     //ok
// 	printf("%d\n", m);
//    printf("%d\n", n);
//}

测试三
//void test3()
//{
//	int n = 10;
//	int m = 20;
//	int* const p = &n;
//	*p = 20;    //ok
//	p = &m;     //no
//}

测试四
//void test4()
//{
//	int n = 10;
//	int m = 20;
//	int const* const p = &n;
//	*p = 20;    //no
//	p = &m;     //no
//}

int main()
{
	//测试⽆const修饰的情况
	/*test1();*/

	测试const放在*的左边情况
	/*test2();*/

	测试const放在*的右边情况
	/*test3();*/

	测试*的左右两边都有const
	/*test4();*/
	return 0;
}

结论:

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

!!!只要位于*左侧或者右侧即可,并不要求具体位置!!! 

指针运算

指针-整数

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

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

//字符类型指针也一样
#include <stdio.h>
int main()
{
    char arr[] = "abcdef";
    char* pc = &arr[0];
    while (*pc != '\0')
    {
        printf("%c ", *pc);
        pc++;
    }
    return 0;
}

注意在内存监视窗口中选择不同列时左侧的地址情况是不同的:

指针-指针

//指针-指针
#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;
}

结论:(指针-指针)=(地址-地址),且两个指针必须指向同一空间,得到的值的绝对值,是指针和指针元素之间的个数

指针的运算关系

//指针的关系运算
#include <stdio.h>
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 int *p = arr;  //这里的数组名就相当于数组首元素地址
 int i = 0;
 int sz = sizeof(arr)/sizeof(arr[0]);
 while(p<arr+sz) //指针的⼤⼩⽐较
 {
 printf("%d ", *p); //p指向某个地址,通过*p拿到该地址中存储的值
 p++;    //p++等于地址++
 }
 return 0;
}

野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
造成野指针的情况有三种: 指针未初始化、指针的越界访问、指针指向的空间释放

指针变量未初始化

#include <stdio.h>
int main()
{
int* p;//整型的指针变量未初始化,默认为随机值
*p = 20;
return 0;
}      //结果报错

指针的越界访问

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

 指针访问越界就会导致栈溢出问题

指针指向的空间释放(下面程序运行后仍能输出200)

#include <stdio.h>
int* test()   //因为返回的是一个地址,所以返回类型应该是int*类型
{
	int n = 100;
	return &n;
}

int main()
{
	int* p = test();   //用指针变量p接收返回回来的地址
	*p = 200;
	//n出函数释放内存空间,但是p指针仍然保存了n内存空间的地址
    //这时如果再使用*p=200就会出问题,此时p就为野指针
	printf("%d\n", *p);
	return 0;
}
	

        通俗来讲就是:相当于你今天开了个住一晚的酒店房间,但是你第二天走后告诉另一个人这个房间还可以住,你让你朋友去住那个房间,虽然这个房间你还能进去但是里面的东西已经被保洁阿姨打扫过了没有你朋友住过的痕迹了。

如何规避野指针

主要是一些具体的操作方式,涉及因为检查不仔细导致的问题不予描述

指针的初始化

#include <stdio.h>
int main()
{
 int num = 10;
 int*p1 = &num;
 int*p2 = NULL;//当我们还没有规定该指针指向哪里的时候,即使将该指针赋值为NULL
 return 0;
}

assert宏(断言)

作用:确保程序符合指定条件,如果不符合,就报错终⽌运行程序

包含头文件:assert.h
使用方式:assert(表达式);
表达式为真, assert() 不会产⽣任何作⽤程序继续运⾏
表达式为假, assert() 就会报错
好处:

1、⾃动标识⽂件和问题所在⾏号

        当表达式为假时,assert()会在标准错误流 stderr 中自动写⼊⼀条错误信息:显⽰没有通过的表达式,以及该表达式所在文件的⽂件名和⾏号

#include <stdio.h>
#include <assert.h>
int main()
{
	int* p = NULL;
	assert(p != NULL);
	return 0;
}

拥有⽆需更改代码就能开启或关闭 assert宏的机制

       如果已经确认程序没有问题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG :
#define NDEBUG
#include <assert.h>

        然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移除这条 #define NDBUG 指令(或者把它注释掉),再次编译,就重新启⽤了 assert() 语句。

缺点:因为引⼊了额外的检查,增加了程序的运⾏时间
         ⼀般我们在debug版本中使⽤ ,这样有利于程序员排查问题,如果在rekease版本使用会影响⽤⼾使⽤时程序的效率

指针的使⽤和传址调⽤

传址调用

如果要写一个交换两个整型变量的值的函数,我们可能会这样写:

#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_s("%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;
}

但是它们并未产生实际的调用效果,调试一下看看:

        我们发现a的地址是0x000000b39e3bf884,b的地址是0x000000b39e3bf8a4,在调⽤Swap1函数时,将a和b的值传递给了Swap1函数,在Swap1函数内部创建了形参x和y来接收a和b的值,但是x的地址是0x000000b39e3bf860,y的地址是0x000000b39e3bf868,x和y确实接收到了a和b的值,不过 x的地址和a的地址不⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间 ,那么在Swap1函数内部交换x和y的值,⾃然不会影响a和b,当Swap1函数调⽤结束后回到main函数,a和b的没法交换。Swap1函数在使⽤的时候,是把变量的值传递给了函数,这种调⽤函数的方式叫做: 传值调⽤
结论:形参是实参的一份临时拷贝,对形参的修改不影响实参

传值调用

那怎么办呢?
        我们现在要解决的就是当调⽤Swap1函数的时候,Swap1函数内部操作的就是main函数中的a和b,直接将a和b的值交换了。那么就需要使⽤指针了,在main函数中将a和b的地址传递给Swap函数,Swap1函数⾥边通过地址间接的操作main函数中的a和b就好了:
#include <stdio.h>
void Swap1(int* px, int* py)
{
	int tmp = 0;
	tmp = *px;
	*px = *py;
	*py = tmp;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf_s("%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;
}

        我们可以发现,此时px、py与a、b的地址就相同了,这样就可以实现在Swap1函数中直接修改a和b的值,这种函数调⽤⽅式叫: 传址调⽤
~over~

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

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

相关文章

高频行情数据应用解决方案

高频行情数据的因子研发和相关策略&#xff0c;是在当前金融量化投资领域普遍关注的内容。由于高频行情数据量庞大&#xff08;Level1的Tick每日10G&#xff0c;Level2的Tick每日40G&#xff09;、以及高频数据时序化处理复杂、数据访问性能等问题&#xff0c;为研发工作的开展…

C++ 的强制类型转换

C 的强制类型转换 目录(转换方式): 1 、static_cast 2 、dynamic_cast 3、reinterpret_cast 4、const_cast 5、类型转换使用建议 一、 static_cast&#xff1a; 用于基本类型之间的转换 static_cast<>() 可以说是神通广大。 使用方法 &#xff1a; type y stat…

Linux系统编程:进程part_1

进程 进程以概念为主&#xff0c;知道是怎么回事就行&#xff0c;工作一般都操作线程。 进程的基本概念 因为这些概念在OS教课书中讲的太多&#xff0c;故而这一块不会详述。 什么是进程&#xff1f; 通俗的说进程就是正在运行的程序。进程是动态的程序是静态的。 使用cat…

Pytest测试框架搭建的关键6个知识点(建议收藏)

在现代软件开发中&#xff0c;测试是确保代码质量和功能稳定性的关键步骤。而Pytest作为一个功能强大且易于使用的Python测试框架&#xff0c;为我们提供了一个优雅的方式来编写和管理测试。本文将为你介绍如何构建高效可靠的测试环境&#xff0c;着重探讨Pytest测试框架搭建时…

基于SSM的二手商品交易系统

基于SSM的二手商品交易系统 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringSpringMVCMyBatisVue工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 首页 商品详情 登录界面 管理员界面 商品管理 摘要 基于Spring&#xff08;Spring MVC&…

mysql第一篇---索引

文章目录 mysql第一篇---索引索引的数据结构为什么使用索引&#xff1f;索引的及其优缺点InnoDB中索引的推演常见的索引概念InnoDB的B树索引的注意事项MyISAM中索引方案索引的代价MySQL数据结构选择的合理性 mysql第一篇—索引 索引的数据结构 为什么使用索引&#xff1f; 索…

Git快速安装【附安装包资源】

软件安装包 项目版本管理软件 Git windows版本安装包 安装步骤 双击按照包之后&#xff0c;直接next 安装位置尽量不要选择C盘&#xff0c;如果只有C盘&#xff0c;可以尝试分盘&#xff0c;如果C盘已经很小了&#xff0c;那就没办法了 选择完安装位置之后&#xff0c;直…

班主任好物 班级查询系统来啦

哈喽各位&#xff0c;作为一名教育博主&#xff0c;今天我要给大家分享一个班主任的好物——班级查询系统&#xff01;这个系统可真是太方便了呢&#xff0c;那么&#xff0c;这个神秘的班级查询系统到底是什么呢&#xff1f;别急&#xff0c;听我慢慢道来。 班级查询系统&…

数据分析:小红书节点投放指南,引爆双十一!

导语 双11大促将临&#xff0c;对于多数品牌方而言&#xff0c;其中蕴藏着巨大的流量增长机遇。面对竞争激烈的小红书种草平台&#xff0c;品牌方们又该如何提前做好准备&#xff0c;掌握营销节奏&#xff0c;真正抓住流量密码呢&#xff1f;接下来由小编一一道来。 双十一剁…

前端 js 之 面向对象(原型、原型链及继承) 06

今天又是一个美好的一天耶 &#xff01; ✌ 文章目录 一、面向对象编程之前二、原型 与 原型链 &#x1f381;三、new 的原理 &#x1f91e;四、面向对象的优势五、继承 (构造函数之间的)ps&#xff1a; 一、面向对象编程之前 在面向对象编程之前&#xff0c;我们是 面向二进制…

Jmeter(五):json提取器元件及jsonpath介绍,响应断言元件

Jmeter&#xff1a;son提取器元件及jsonpath介绍 json提取器元件介绍 json提取器与正则表达式提取器功能类似&#xff0c;也是用来截取响应信息的部分保 存到指定的变量中去&#xff0c;不同的是&#xff0c;它只能用来处理响应正文&#xff0c;并且响应正文必须 是json格式的…

分享一下微信小程序开发的步骤是什么

随着微信小程序的日益普及和深入人心&#xff0c;许多企业和开发者都开始投身于小程序开发领域。那么&#xff0c;如何从零开始&#xff0c;一步步开发出一个自己的微信小程序呢&#xff1f;下面就让我们一起探讨微信小程序开发的步骤。 一、确定开发目标和定位 在开始开发小程…

Cesium Vue(六)— 材质(Material)

1. 设置entity材质 添加棋盘纹理材质 // 棋盘纹理 let material new Cesium.CheckerboardMaterialProperty({ evenColor: Cesium.Color.RED, oddColor: Cesium.Color.YELLOW, repeat: new Cesium.Cartesian2(2, 2), });添加条纹纹理材质 // 条纹纹理 let material new Cesium…

蓝牙5.4的几个新特性

前述文章《蓝牙5.4引入PAwR&#xff0c;电子价签迎来新机遇》中我们介绍了蓝牙5.4的PAwR特性&#xff0c;该特性的引入使得电子价签领域迎来了新的机遇&#xff0c;但其实蓝牙5.4一共引入了4个特性&#xff0c;本文将逐一进行介绍后面3个特性。 首先来回顾一下蓝牙5.4的几个新…

安装Sentinel

大家好今天来安装Sentinel . 安装Sentinel 下载 : 大家可以选择相应版本(最新版本1.8.6) 官网下载地址 : Release v1.8.6 alibaba/Sentinel GitHub 链接&#xff1a;Sentinel_免费高速下载|百度网盘-分享无限制 (baidu.com) 提取码&#xff1a;8eh9 运行 : 将jar包放到任…

4957B/D/E/F射频/微波综合测试仪

4957B/D/E/F 射频微波综合测试仪 频率范围&#xff1a;30k~40GHz ​4957B/D/E/F射频/微波综合测试仪频率范围可达6.5GHz/18GHz/26.5GHz/40GHz&#xff0c;集双端口矢量网络分析、电缆和天馈线测试、矢量电压测量、频谱分析&#xff08;通道功率、邻道功率、占用带宽、干扰分…

认识web自动化测试!

1.什么是自动化测试&#xff1f; 自动化测试的概念: 软件自动化测试就是通过测试工具或者其他手段&#xff0c;按照测试人员的预定计划对软件产品进行自动化测试&#xff0c;他是软件测试的一个重要组成部分&#xff0c;能够完成许多手工测试无法完成或者难以实现的测试工作&a…

2023年天猫双十一预售下定金抢红包玩法介绍

2023年天猫双十一预售下定金抢红包玩法介绍 2023年双11预售期间&#xff0c;用户可下定金抢红包。红包可用于抵扣商品货款金额&#xff0c;但有使用规则&#xff1a;红包需在有效期内使用&#xff0c;逾期作废;不同商品可使用的红包有优先级。不同场景下的红包使用要求不同&…

无人机遥控中应用的2.4GHz无线芯片

无人驾驶飞机简称“无人机”&#xff0c;英文缩写为“UAV”&#xff0c;是利用无线电遥控设备和自备的程序控制装置操纵的不载人飞机&#xff0c;或者由车载计算机完全地或间歇地自主地操作。是一种不需要人操控就能够自主飞行的飞行器&#xff0c;它可以执行多种任务&#xff…

亲测好用教师小程序

作为一名老师&#xff0c;经常需要面对的一大挑战就是如何有效地向学生和家长传达重要的学业信息。而其中&#xff0c;成绩的发布与查询更是重中之重。传统的做法是手动录入数据&#xff0c;或者通过电子邮件发送Excel表格&#xff0c;这样做既繁琐又耗时。幸运的是&#xff0c…