一、内存与地址
我们可以把计算机的内存看作一条长街上的一排房屋。每座房子都可以容纳数据,并通过一个房号来标识。由于一个位所能表示的值的范围有限,通常将许多位合成一组作为一个单位,这样就可以存储范围较大的值。下图展示了一些内存位置,也就是每座房子。
每个位置可以被称为字节,每个字节都包含了存储一个符号所需要的位数,大多为8个二进制位。为了存储更大的值,可以用字来存储2字节或4个字节。下图展示由4字节组成的字的内存位置。
每个字节通过地址来标识,就是方框上的数字,虽然一个字包含多个字节,但一个字仍然只有一个地址。内存中的每个位置由一个独一无二的地址标识,且每个位置都包含一个值。当访问这些内存位置时,是通过名字来代替地址访问的,而这些名字就是变量,专门存储内存地址的变量叫指针变量。
注意:变量与内存位置之间的关联并不是硬件提供的,而是由编译器实现的。所以硬件仍然是通过地址访问内存的。
二、指针含义
1、指针是一种数据类型,使用它可以定义指针变量,简称为指针。
2、指针变量中存储的内存地址(整数),以32位系统为例的整数范围为:0x00000000~0xffffffff,32为系统的指针变量占用4字节内存、64位系统的指针变量占用8字节内存。
3、可以根据指针变量中存储的内存地址去访问相应的内存。
三、指针用法
1、定义指针变量:类型 *指针变量名
(1)、指针变量名与普通变量名的取名规则一样,但使用方式完全不同,为了避免与普通变量混用,指针变量名一般以p结尾。
(2)、指针变量不能连续定义:
int* p1,p2,p3; //这样写只有p1是指针变量,而p2、p3只是普通的int类型变量
int *p1,*p2,*p3; //p1 p2 p3都是指针变量,一个* 只能定义一个指针变量
typedef int* intp;
intp p1,p2,p3; //经重定义后的指针类型可修饰多个指针变量,p1 p2 p3都是指针变量
(3)、指针变量中存储的内存地址存储的是某个字节的内存首地址。
(4)、指针变量的类型决定了访问内存时的字节数量和进步值(指针变量+1时所增加的字节数)。
(5)、指针变量的默认值与普通变量一样是随机的(野指针),建议赋值为NULL(空指针)。
2、给指针变量赋值(初始化): 指针变量名 = 内存地址
1. int *p =# /*获取普通变量的地址赋值给指针变量,且变量的类型要与指针变量的类型相同,
**否则会产生警告,并且在后续指针变量解引用时有可能产生段错误或脏数据
*/
2. int *p =malloc(4); //把堆内存的地址赋值给指针变量
注意:如果给指针变量赋值非法的内存地址,那么指针变量解引用时会产生段错误、脏数据。
3、根据指针变量中存储的地址访问内存(解引用):*指针变量名
int num = 8;
int *p = #
*p; //这里对指针进行解引用,相当于num
(1)、如果指针变量中存储的是非法的内存地址,*p读取这个内存中的数据就会有段错误。
(2)、如果指针变量中存储的内存地址所指向的内存块是只读的(text内存段),读取这个内存中的数据不会发生段错误;当修改这个内存中的数据时,会发生段错误。
(3)、*p访问到的内存字节数,由指针变量的类型决定。
四、为什么要使用指针变量
1、使用指针可以在函数之间共享变量,这样可以获取函数的多个返回值。
应用:time函数就可以以指针的方式获取返回值、swap()交换两个变量的函数。
另外,虽然全局变量也可以在函数之间共享,但是使用过多容易造成命名冲突,而且全局变量在程序运行期间无法释放,因此会浪费内存,所以全局变量尽量少用。
2、使用指针可以提高函数的传参效率
C语言的函数传参都是值传递(内存拷贝),比如:doube、long double、long long 、自定义类型(结构、联合、枚举),它们的字节数都是大于等于8字节的,而使用指针传参不需要传递变量的值,只需要传递指针变量的地址(4字节)即可,这样可以大大提高函数的传参效率。
注意:虽然这样做传递效率提高了,但会有变量被修改的风险,解决方法就是给指针变量前加一个const 修饰符。
3、堆内存无法取名字,必须与指针变量配合使用
当执行 int num;语句时,系统会分配4个字节的栈内存的内存空间给num变量并建立联系,在后续操作中使用该变量时就相当于使用这4字节的栈内存,就是为这个栈内存取名了。而堆内存无法取名,当向系统申请一块内存时,系统只会返回这块内存的首地址,这块内存无法与一个变量名建立联系,也就是无法取名字,所以堆内存必须配合指针变量使用。
五、使用指针注意点
1、针对空指针的问题
空指针:值为NULL的指针变量,系统规定不能对空指针解引用,否则会产生段错误。
如何避免空指针解引用产生的段错误?
要点:对来历不明的指针解引用前,先判断是否为空指针。
(1)、当实现的函数参数是指针变量,我们无法确定调用者传递的是否为空指针,所以对于这类指针解引用前要前判断是否为空指针。
(2)、当调用的函数返回值是空指针,C语言规定返回值是指针的函数当执行出错时返回值是NULL,判断该返回值可以知道函数执行是否出错,也可以避免对空指针解引用。
注意:大多数系统中的NULL是0地址,但有少数的系统中的NULL是1,所以判断空指针时
if(!p) // 不通用,对于NULL不为0的系统来说会出错
{
}
if(p == NULL) // 这种写法有瑕疵,漏写一个“=”不会产生警告
{
}
if(NULL == p) // 完美
{
}
2、针对野指针的问题
野指针:指针变量中存储的地址不知道是否合法。
对野指针进行解引用的后果:(1).段错误 (2).脏数据 (3).一切正常 。野指针比空指针的危害更大,因为无法判断一个指针变量是否是野指针,野指针无法通过条件判断出来,通过maps文件的地址范围表只能判断指针解引用时是否产生段错误,而无法判断是否是野指针,所以野指针的错误是隐藏的、潜伏的,存在不确定性,一旦出现错误很难寻找、定位和重现。
如何避免野指针产生的危害?
前提:所有的野指针都是程序员自己制造出来的,所以只要不制造野指针,就不会使用到野指针。
(1)、定义指针变量时一定要初始化,宁可赋值为NULL,也不要产生野指针。
(2)、函数不要返回局部变量的地址,因为当函数执行结束时,变量的内存就会随即释放,因此这种情况返回的是野指针。
(3)、当堆内存被释放后,与它配合的指针变量要及时的赋值为NULL。
六、const与指针
前提:使用指针优化函数的传参效率,变量就会有被修改的风险,可以使用const配合指针来保护变量,一共有五种写法(三种功能)。
1.const int *p; //保护*p不被修改,也就是保护p所指向的内存,适合传参
p = NULL; /*Yes*/ *p = 6666; /*No*/
2.int const *p; //同上
应用范围:当使用指针作为函数的传参时,变量就会有被修改的风险,可以使用const配合指针来保护变量
3.int* const p; //保护p不被修改,也就是保护指针变量,适合数组
p = NULL; /*No*/ *p = 6666; /*Yes*/
应用范围:当使用数组作为函数的参数时,数组就会蜕变为指针,为了让指针永远指向数组,可以使用这种方法来保护指针变量的值。
4.const int* const p; //既保护指针变量也保护它所指向的内存
5.int const* const p; //同上
七、指针的指针(二级指针)
二级指针:专门指向指针变量的指针,它也是一种指针变量,但它里面存储的是普通指针变量的地址。
1、声明:类型** 二级指针变量名
为了与普通指针区别,一般变量名以pp结尾,如:int** pp 。
2、赋值:二级指针变量=&普通指针(一级指针)。
int num=1234;
int* p = #
int** pp=&p;
注意:二级指针与一级指针的类型必须相同才能赋值。
3、解引用:
(1)、*二级指针 《=》 一级指针
如: *pp 《=》 p;
(2)、**二级指针 《=》 *一级指针
如: **pp 《=》 *p 《=》 num;
八、指针表达式
前提:左值是指可以出现在赋值符号左边的东西,也就是可标识的内存地址;右值就是那些可以出现在赋值符号右边的东西,也就是内存地址的值。
以下面声明为例,分析所求表达式的左值和右值:
char ch = 'a';
char *cp = &ch;
1、ch
当它作为右值使用时,表达式的值为‘a’;作为左值时,就是这个内存的地址。
2、&ch
当它作为右值使用时,表达式的值是变量ch的地址,但要注意的是,这个值虽然同指针变量cp中存储的值是一样的,但它并不是由cp产生的,而是通过拷贝一份ch的地址作为值存储于内存中的某个位置,所以它的左值是非法的,因为它未标识内存的位置。
3、cp
当它作为右值使用时,表达式的值为ch的地址;作为左值时,就是cp所处的内存的位置。
4、&cp
当它作为右值使用时,表达式的值为cp的地址,这个结果的类型是一个二级指针,即指向字符的指针的指针;作为左值时,是非法的。
5、*cp
当它作为右值使用时,表达式的值为‘a’;作为左值时,就是这个ch的地址。
6、*cp + 1
当它作为右值使用时,这个表达式先执行间接访问操作,得到ch的值'a',然后再把这个值的副本加1,结果为'b';而表达式最终结果的存储位置未定义,所以左值是非法的。
7、*(cp+1)
这个表达式先把cp中存储的地址加1,得到ch后面的那个地址,再解引用。当它作为右值使用时,表达式的值就是ch后面那个地址中存储的值;而左值就是这个位置。
8、++cp和cp++
这两个表达式的区别在于++cp是先增加了cp的值,再拷贝一份作为结果值;而cp++则是先拷贝一份cp值作为结果值再增加cp的值,也就是前缀++和后缀++的区别。左值都是非法的。
9、*++cp
这个表达式先执行++,所以把cp增加的值拷贝一份作为副本后,再执行间接运算符,所以右值为ch后面那个地址的值;左值则为ch后面的那个地址。
10、*cp++
这个表达式先执行后缀++运算符,把cp的值拷贝一份作为副本,然后cp值会+1,再执行间接运算符,所以右值为ch的值;左值为ch的地址。
11、++*cp
这个表达是先执行间接运算符,然后cp所指的位置的值会+1,并把这个结果拷贝一份作为表达式的右值。
12、(*cp)++
这个表达式与上一个表达式类似,区别在于拷贝的值是原先cp所指的地址的值。
13、++*++cp
一步步来,根据从右到左的结合性,这个表达式先执行 ++cp,把cp增加的值拷贝一份作为副本再执行间接运算符,访问到ch后面的内存位置。最后执行++,把增加的值拷贝一份作为表达式的结果值。
14、++*cp++
这个表达是去上一个表达时的区别在于此时的cp++是后缀++,所以拷贝的副本是cp的值,而不是cp增加的值。
九、指针运算
前提:指针变量中存储的是代表内存地址的整数,理论上整数可以使用的运算符,指针变量都可以使用,但是只有以下运算才有意义。
指针的进步值:由指针变量的类型决定,当指针变量+1时所增加的字节数就是指针变量解引用时的步长。
1、算数运算
(1)、指针 ± 整数
运算结果是:代表地址的整数加减(进步值 * 整数)
例如 :int* p =0; p+1 的结果就是 4
(2)、指针 - 指针
限制条件:只有当两个指针都指向同一数组中的元素时,才允许从一个指针减去另一个指针。
运算结果是:(代表地址的整数1 - 代表地址的整数2)/ 进步值
例如 : int* p1=32; int* p2=64; p2-p1 的结果就是8
注意:指针变量加减一个整数时,相当于指针变量以进步值前后移动;指针减指针可以计算出两个地址之间相隔多少个元素(指针变量解引用的对象)。
2、关系运算
对于指向同一数组的指针,可以通过>、>=、<、<= 来比较它们在数组中的相对位置。
下面比较两个循环来说明一个问题:
#define N_VALUES 5
float values[N_VALUES];
float *vp;
//第一种方法,从前往后比较
for(vp = &values[0];vp < &values[N_VALUES]; )
*vp++ = 0;
//第二种方法,从后往前比较
for(vp = &values[N_VALUES];vp > &values[0]; )
*--vp = 0;
//第二种方法的变式,存在一个问题
for(vp = &values[N_VALUES-1];vp >= &values[0]; vp--)
*vp = 0;
第一种方法通过比较vp和一个指向数组最后元素后面的那个内存位置的指针常量,虽然vp最后也会指向这个位置,但不进行间接访问,所以是安全的。第二种方法是让vp指向数组最后元素后面的那个内存位置,但在执行间接访问前先自减了,并且当vp指向数组第一个元素时循环就结束了,所以也是安全的。但最后一种方法当vp指向数组第一个元素时,还是会-1,这时vp就移到数组边界外了,这可能导致循环失败。
注意:标准允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针进行比较,但不允许与指向数组元素第一个元素之前的那个内存位置的指针进行比较。