文章目录
- 前言
- 一、指针概念的引入与指针的基本介绍
- (一)、内存与地址
- (二)、指针变量和地址
- (三)、指针变量类型的意义
- (四)、const修饰指针
- 二、指针的运算
- (一)、指针+-整数
- (二)、指针-指针
- (三)、指针的关系运算
- 三、野指针
- (一)、野指针的成因
- (二)、如何规避野指针
- (三)、利用assert断言来判断指针的*有效性
- 四、传值调用与传址调用
- (一)、传值调用
- (二)、传址调用
- (三)、两种调用的总结
- 总结
前言
提示:这里可以添加本文要记录的大概内容:
本文初步引人了指针的概念,并对指针的一些基本知识做了概括,并提了一下野指针的问题,最后讲了一下传值调用与传址调用的区别以及指针在其中发挥的作用,我们要知道指针知识博大精深,这篇文章只是冰山一角呀,后期会出后面极板,一共打算出5版指针相关的文章,每一章节各有千秋。
提示:以下是本篇文章正文内容,下面案例可供参考
一、指针概念的引入与指针的基本介绍
(一)、内存与地址
- 计算机中的内存就是数据存储的地方。我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中。
- 我们了解一下计算机中内存的单位从小到大一次为: bit、Byte、KB、MB、GB、TB;
- 内存划分为一个个的内存单元,每个内存单元的大小是一个字节。 其中,每个内存单元,相当于⼀个学⽣宿舍,⼀个字节空间⾥⾯能放8个⽐特位,就好⽐同学们住的⼋⼈间,每个⼈是⼀个⽐特位。
- 生活中我们把门牌号等事物叫做地址(方便我们找寻相应的值),在计算中我们把内存单元的编号也叫做地址,而在C语言中我们将地址起名为指针。
- 如何理解编址:
CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,⽽因为内存中字节很多,所以需要给内存进⾏编址(就如同宿舍很多,需要给宿舍编号⼀样)。计算机中的编址,并不是把每个字节的地址记录
下来,⽽是通过硬件设计完成的。
首先,我们要理解的是,计算机内是有很多硬件单元,而硬件单元是要互相协作工作的。所谓的协同,至少相互之间要能够进行数据传递,这期间是通过“线”来链接的,具体有地址总线,数据总线,控制总线。
我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表⽰0,1【电脉冲有⽆】,那么⼀根线,就能表⽰2种含义,2根线就能表⽰4种含义,依次类推。32根地址线,就能表⽰2^32种含义,每⼀种含义都代表⼀个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传⼊CPU内寄存器。这就是编址的大概过程。
(二)、指针变量和地址
- 指针变量
我们通过取地址操作符(&)拿到的地址需要存放起来,这时候我们引入指针变量(即数据类型+*+变量名)
代码如下:
#include <stdio.h>int main(){int a = 10;int * pa = &a;//取出a的地址并存储到指针变量pa中return 0;
}
指针变量pa也是一种变量,这种变量用来存放地址的,存放指针变量中的值都会被理解成地址
- 详细拆解指针变量
这里我们定义一个指针变量 pa:
int a = 10;
int * pa = &a;
pa左边写的是int*,*是说明pa是指针变量,而前面的int则是说明pa指向的是整型(int)类型的对象。
具体关系如下:
- 解引用操作符
在C语言中,我们要得到了一个地址,就可以通过地址(指针)来找到地址(指针)所指向的对象,而这个找寻的过程中需要我们调用解引用操作符(*)
例如下面代码:
#include <stdio.h>
int main(){int a = 100;int* pa = &a;*pa = 0;printf("%d",a);return 0;}
这里我们通过解引用操作符(*)找到了pa指针所指向的对象a,并对其进行了修改,这样我们打印出来的a的内容由原来的100变为了0.
- 指针变量的大小:
通过之前学习我们认识到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。如果指针变量是用来存放地址的,那么指针变量的大小是4个字节的空间才可以。
同理,如果64位机器假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变量的⼤⼩就是8个字节。
代码操作:
#include <stdio.h>//指针变量的大小取决于地址的大小
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个字节,
故而指针变量的大小是和变量类型无关的,只要指针类型的变量,在相同平台下,大小都是相同的。
(三)、指针变量类型的意义
前面已经提过指针变量的大小和类型无关,只要是指针变量,在同一个平台下,大小都是一样的,那为啥子还要区分指针变量的类型呢?
下面解答大家的疑惑,给出指针变量类型的相关意义:
- 指针类型决定了,对指针解引用的时候有多大权限(一次操作几个字节)。
用指针的解引用来解释:
通过两个代码调试过程查看地址窗口来解释:
//代码1:
#include<stdio.h>
int main(){int n = 0x11223344;int *pi = &n; *pi = 0; return 0;}
//代码2:
#include<stdio.h>
int main(){int n = 0x11223344;char *pc = (char *)&n;*pc = 0;return 0;}
调试结果如下:
代码1:
n地址的四个字节所指向的内容全部赋值为0。
代码2:
n的四个字节只有第一个字节所指向的内容赋为0
通过调试我们可以看到代码1会将n的4个字节全部改为0,但是代码2只是将n的第一个字节改为0.
故而我们可以得出指针类型决定了,对指针解引用的时候有多大权限(一次操作几个字节)。
- 指针类型决定了,指针向前或者向后走一步有多大(距离)。
用指针±整数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个字节。这就是指针变量的类型差异带来的变化。指针+1,其实就是跳过1个指针指向的元素。
故而可以得出结论:指针类型决定了,指针向前或者向后走一步有多大(距离)。
- void指针的介绍:
void指针可以理解为无具体类型的指针(或者说叫泛型指针),这种类型的指针可以用来接受任意类型地址。但是也有局限性,那就是void*类型的指针不能直接进行指针的±整数和解引用的运算。
(四)、const修饰指针
-
const放在*的左边
如果const放在 * 的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 -
const放在*的右边
如果const放在 * 的右边,修饰的是指针变量本身,保证了指针变量的内容不能被修改,但是指针指向的内容,可以通过指针改变。 -
const在*号左右两边都存在
如果const在 * 左右两边都存在,那么指针变量内容不能被修改,同时指针指向的内容也不可以通过指针修改。 -
三种情况代码层面的解释:
#include<stdio.h>
void test1(){//代码1 : 测试⽆const修饰的情况int n = 10;int m = 20;int *p = &n;*p = 20;//ok?p = &m; //ok?}
//代码2:测试const放在*的左边的情况
void test2(){int n = 10;int m = 20;const int* p = &n;*p = 20;//ok?p = &m; //ok?}
// 代码3:测试const放在* 右边的情况
void test3(){int n = 10;int m = 20;int * const p = &n;*p = 20; //ok?p = &m; //ok?}
//代码4:测试*的左右两边都有const
void test4(){int n = 10;int m = 20;int const * const p = &n;*p = 20; //ok?p = &m; //ok?}
int main()
{
//测试无const的情况
test1();
//测试const在* 左边的情况
test2();
//测试const在* 右边的情况
test3();
//测试* 的两边都有const情况
test4();
return 0;
}
我们可以分别运行test1~4看一下代码能否执行,结果只有代码一能正常运行,而后面几个代码,不能执行,但能看到哪一行出的问题,追溯原因就能理解const的作用。
- 总结 :如果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;
}
(二)、指针-指针
- 指针-指针的绝对值是指针和指针之间元素的个数(计算的前提条件是两个指针指向的是同一个空间!)
- 模拟strlen函数的功能代码来解释指针-指针的操作:
#include<stdio.h>
size_t my_strlen(const char* p)
//这里用const修饰代表不能改变p所指向对象的内容,这里我们只做统计工作
{char* start = p;char* end = p;while (*end!='\0'){end++;}return end - start;//运用指针-指针来求两指针间元素的个数
}int main()
{char arr[] = "hello,world";size_t len = my_strlen(arr);printf("字符串长度为:%zd\n", len);
}
这里我们通过end与start指针之差来代表字符串中元素的个数。
(三)、指针的关系运算
- 指针的关系运算:其实让指针之间比较大小
- 用下面一段代码来解释:
#include<stdio.h>int main(){int arr[10] = {1,2,3,4,5,6,7,8,9,10};int *p = &arr[0];int sz = sizeof(arr)/sizeof(arr[0]);while(p<arr+sz) //指针大小的比较
{printf("%d ", *p);p++;
}
return 0;
}
这段代码其实就是比较指针所指向地址的数值大小,通过比较来是实现访问数组元素的功效。
注意这里的指针大小代表的是指针所指向的地址数值的大小,跟上面讲的指针变量大小(它所指的字节数)有本质的区别。
三、野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
(一)、野指针的成因
- 指针未初始化
- 指针越界访问
(二)、如何规避野指针
- 指针初始化
- 小心指针越界
- 指针不再使用时,及时置NULL,指针使用之前检查有效性
- 避免返回局部变量的地址
(三)、利用assert断言来判断指针的*有效性
头文件:assert.h
- 概念:assert.h头文件定义了宏assert(),用于运行时确保程序符合指定条件,如果不符合,就报错终止运行,这里的宏常常被称为“断言”。
- assert的用法:assert()宏接受一个表达式作为参数,如果该表达式为真(返回值为非零),assert()不会产生任何作用,程序继续运行。如果该表达式为假(返回值为零),assert()就会报错。
- 我们经常用assert断言来判断指针的有效性。
- assert的好处与缺点:
好处:assert()宏接受一个表达式作为参数,如果该表达式为真(返回值为零),assert()不会产生任何作用,程序继续运行,如果该表达式为假(返回值为零),assert()就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号
缺点:assert的缺点,因为额外引入了检查,增加了程序的运行时间。 - ⼀般我们可以在Debug 中使⽤,在发环境中,在
在 Release 版本中选择禁⽤assert 就⾏,在VS 这样的集成开发版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,Release 版本不影响⽤⼾使⽤时程序的效率。
四、传值调用与传址调用
以写一个交换两个数的位置的函数为例来展开讨论。
写一个函数来描述两个数交换。调用完函数后,将交换的结果打印出来,来展开描述传值调用与传址调用相关知识
(一)、传值调用
- 代码显示:
# include<stido.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;
- 运行结果如下:
- 分析结果:
这里我们可以看到交换前与交换后的结果相同,没有 产生交换的效果我们通过调试的过程来深度剖析一下原因。
通过调试的过程,我们可以看到在main函数的内部,我们创建了a,b两个变量,此时a的地址为0x00cffdd0,b的地址为:0x00cffdc4,在调用Swap1函数的时候,将a和b的值传递给了Swap1函数,而在Swap1函数内部我们创建了x,y变量分别来接受a和b的值,但是x的地址为:0x00cffcec,y的地址为:0x00cffcf0,虽然x,y接受了a,b的数值,但是x,y的地址明显跟a,b的地址不同,换句话说,x,y相当于是独立的空间,那我们在Swap1函数内部交换x,y的值自然不会影响a,b,当Swap1函数调用结束回到main函数中,a和b没办法交换。
总而言之,Swap1函数在使用的时候,是把变量本身直接传递给了函数,叫做传值调用,实参传给形参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参,故而Swap1函数交换。
(二)、传址调用
- 代码显示:
# include<stdio.h>
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);Swap2(&a, &b);printf("交换后:a=%d b=%d\n", a, b);return 0;
- 运行结果:
这里我们在调用Swap2函数的时候,Swap2函数内部的操作就是main函数中的a和b,直接将a和b的值进行交换律,因为我们在传递参数的时候是传的指针,即及那个a和b的地址传递给了Swap2函数,Swap2函数里边通过地址间接的操作main函数中的a和b,并达到了交换的效果就好了。
总而言之:调用Swap2函数的时候是将变量的地址传递给了函数,这种函数调用的方法叫做传址调用,传址调用,可以让函数和主调函数之间建立真正的联系,在函数的内部可以修改主调函数中的变量。
(三)、两种调用的总结
在未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用;如果函数内部要修改主调函数中的变量的值,就需要传址调用。
总结
以上就是对指针的初步介绍,如有错误,请批评指正,请大家多多支持。