1.指针变量及其使用和变量类型
2.指针运算
番外 arr与&arr
3.const修饰指针
4.野指针和assert断言
5.传值调用和传址调用
一.指针变量和变量类型
1.1 指针的定义及其引入
假如你一个人去旅游,然后进入酒店夜宿,而这时候你要去找他,如果你的朋友没有告诉他的房间,这时候你就要一个一个房间找,这就显得很麻烦。此时假如你的朋友告诉他的房间,这时候你就可以快速找到他所在房间,这样是不是节省了很多时间。此时我们可以把指针理解为酒店的房间号。
在C语言中地址又被称作指针 即 指针==内存单元编号==地址
1.2 指针变量
定义: 我们通过取地址操作符(&)拿到的地址是⼀个数值,⽐如: 0x006F356,这个数值有时候也是需要存储起来,⽅便后期再使⽤的,那我们把这样的地址值存放在哪⾥呢?
答案是:指针变量中。指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址。
1.3 指针的类型与拆解
类型:
char* pa;//字符型指针
int* pa;//整型指针
float* pc;//浮点型指针
拆解:
int a = 10 ;
int * pa = &a;
这⾥pa左边写的是 int* , * 是在说明pa是指针变量,⽽前⾯的 int 是在说明pa指向的是整型(int) 类型的对象。
1.4 解引用操作符
上面我们说到存放在指针变量中的值都会理解为地址。那我们将地址保存起来,未来是要使⽤的,那怎么使⽤呢?这时候就要引入解引⽤操作符。
作用:在现实⽣活中,我们使⽤地址要找到⼀个房间,在房间⾥可以拿去或者存放物品。C语⾔中其实也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,而解引⽤操作符(*)就有这作用。
1 # include <stdio.h>
2 int main ()
3 {
4 int a = 100 ;
5 int * pa = &a;
6 *pa = 0 ;
7 return 0 ;
8 }
上⾯代码中第5⾏就使⽤了解引⽤操作符, *pa 的意思就是通过pa中存放的地址,找到指向的空间, *pa其实就是a变量了;所以*pa = 0 ,这个操作符是把a改成了0 (因为pa是一个指针变量其是一个地址又因为使用了 解引⽤操作符解引用故名思义解掉操作符作用得到其地址对应的变量)
有同学肯定在想,这⾥如果⽬的就是把a改成0的话,写成 a = 0; 不就完了,为啥⾮要使⽤指针呢?其实这⾥是把a的修改交给了pa来操作,这样对a的修改,就多了⼀种的途径,写代码就会更加灵活
附:如下代码经过调试我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第⼀个字节 改为0
结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。
⽐如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节。
1.5 指针变量的大小
指针变量的⼤⼩是不是确定的呢?答案是确定的,机器有32位机器和64位机器,32位机器有32根地址线那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4 个字节才能存储。64位机器有64根地址线假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要 8个字节的空间,指针变量的⼤⼩就是8个字节。
x64输出环境
x86输出环境
结论:(指针变量的⼤⼩和类型⽆关,只要是指针变量,在同⼀个平台下,⼤⼩都是⼀样的)
• 32位平台下地址是32个bit位,指针变量⼤⼩是4个字节
• 64位平台下地址是64个bit位,指针变量⼤⼩是8个字节
•注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台(环境)下,⼤⼩都是相同的。
二.指针运算
•指针+- 整数
•指针 - 指针
•指针的关系运算
2.1 指针+-整数(一般指针访问)
我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。 这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可以-1。
结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。
2.2 指针+-整数(指针访问数组)
这里把数组首元素的地址放到指针变量p中,再通过解引用得到首元素,然后提供for循环改变p并且解引用。我们这里在给p地址时未用&这是因为在数组中等价与数组首元素地址。
2.3 指针-指针
这里我们可以发现两个指针相减得到8
结论 1. 两个指针一定是指向同一块区域(不能一个是int类型一个是char类型)
2.指针-指针得到的值的绝对值就是两个指针之间的元素个数。
(到此为止其实大家可能会对一个问题有所疑惑,即arr(指针)和&arr的区别) 这就不得不进行下一项议程了,详情请见我的另外一篇博客(arr与&arr)
2.4 指针的关系运算
(可用于判断是否越界(列如与数组的最后一个元素作比较))
三.const修饰指针及变量
3.1 const的作用
const在C语言中是一个关键字,用于限定一个变量不允许被改变,产生静态作用。使用const可以在一定程度上提高程序的安全性和可靠性。
3.2 const修饰符的用法
修饰局部变量:
例如:const int n = 5; 表示变量n的值不能被改变。
修饰指针:
指向常量的指针:指针指向的值不可修改,但指针本身可以修改。
例如:
常量指针:指针本身的值不可修改,但指针指向的值可以修改。
例如:
ok,以上展示的分别是const int* q(也可以写成int const*q) 和 int* const q的区别,总结一下,就是:
• const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变。
• const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指 向的内容,可以通过指针改变。
• const 放在*的两边做修饰指针指向的内容和指针变量本⾝的内容都不能改变。
最后一点也很好理解,稍微看看以下这条代码就好了哇:
四.野指针和assert断言
4.1 野指针的概念: 野指针是指针指向的位置不可知(随机、不正确、没有明确限制)
4.2 野指针的三种形成原因:
4.2.1 指针未初始化
# include <stdio.h>
int main ()
{
int *p; // 局部变量指针未初始化,默认为随机值
*p = 20 ; //非法访问,p就是 野指针
return 0 ;
}
4.2.2 指针越界访问
# include <stdio.h>
int main ()
{
int arr[ 10 ] = { 0 };
int *p = &arr[ 0 ];
int i = 0 ;
for (i = 0 ; i <= 11 ; i++)
{
*(p++)=i //当指针指向的范围超出数组arr的范围时,p就是野指针
}
return 0;
}
4.2.3 指针指向的空间释放
4.3 野指针的规避方案
1 指针初始化 2 ⼩⼼指针越界 3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性 4 避免返回局部变量的地址
值得注意的是:
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL。NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的(无法进行指针运算等操作),读写该地址 会报错
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问, 同时使⽤指针之前可以判断指针是否为NULL
4.4 assert断言
4.4.1 定义:assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏,这个宏常常被称为“断⾔”
4.4.2 语法
assert (p != NULL ) ;
当代码在程序中运⾏到这⼀⾏语句时,就会验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰
4.4.3 规则
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣
任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误
流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。
4.4.4 好处
能⾃动标识⽂件和出问题的⾏号。
4.4.5
有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG。 然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移 除这条 #define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语句
# define NDEBUG
# include <assert.h>
4.4.6 坏处
引⼊了额外的检查,增加了程序的运⾏时间。
4.4.7 范围
⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开 发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题, 在 Release 版本不影响⽤⼾使⽤时程序的效率
五.传值调用和传址调用
5.1 传值调用和传址调用的区别
5.1.1 传值调用和传址调用的定义 :
传值调用是指函数调用时,将实参的值复制一份传递给形参,形参和实参分别占用不同的内存空间。因此,对形参的修改不会影响实参的值
传址调用是指函数调用时,传递的是实参的地址而不是值,形参和实参共享同一块内存空间。因此,对形参的修改会影响到实参的值
5.1.2 传值调用和传址调用的具体区别:
5.2 数据传递方式:
传值调用:将实参的值复制一份传递给形参。
至于为啥会出现下面的结果呢?因为函数传参的本质是实参传递给形参的时候,形参是实参一份临时拷贝,所以对形参的修改,不会影响实参。
传址调用:传递实参的地址给形参,形参和实参共享同一块内存空间。
内存占用: 传值调用:形参和实参分别占用不同的内存空间
传址调用:形参和实参共享同一块内存空间
数据修改的影响: 传值调用:对形参的修改不会影响实参
传址调用:对形参的修改会影响实参
传值调用和传址调用的应用场景
传值调用适用于不需要修改实参值的场景,例如计算函数、传递基本数据类型等
传址调用适用于需要修改实参值的场景,例如交换变量值、修改数组元素等