指针学习

来源:互联网 发布:淘宝dota2代练 编辑:程序博客网 时间:2024/06/08 14:10

printf("p = %p.\n", p); // %p打印指针和%x打印指针,打印出的值是一样的

printf("p = 0x%x.\n", p);


一、指针是什么?

1、指针变量和普通变量的区别

  • 指针的实质就是个变量,它跟普通变量没有任何本质区别。指针完整的名字应该叫指针变量,简称为指针。


2、为什么需要指针?

  • 为了实现间接访问。在汇编中都有间接访问,其实就是CPU的寻址方式中的间接寻址。
  • 间接访问(CPU的间接寻址)是CPU设计时决定的,这个决定了汇编语言必须能够实现间接寻址,又决定了汇编之上的C语言也必须实现简介寻址。
  • 高级语言如Java、C#等没有指针,那他们怎么实现间接访问?答案是语言本身帮我们封装了。

3、指针使用三部曲:定义指针变量、关联指针变量、解引用

  • 当我们int *p定义一个指针变量p时,因为p是局部变量,所以也遵循C语言局部变量的一般规律(定义局部变量并且未初始化,则值是随机的),所以此时p变量中存储的是一个随机的数字。
  • 此时如果我们解引用p,则相当于我们访问了这个随机数字为地址的内存空间。那这个空间到底能不能访问不知道(也许行也许不行),所以如果直接定义指针变量未绑定有效地址就去解引用几乎必死无疑。
  • 指针绑定的意义就在于:让指针指向一个可以访问、应该访问的地方(就好象拿着枪瞄准目标的过程一样),指针的解引用是为了间接访问目标变量。

4、符号的理解

(1)星号*

  • 2种用法:第一种是指针定义时,*结合前面的类型用于表明要定义的指针的类型;第二种功能是指针解引用,解引用时*p表示p指向的变量本身

(2)取地址符&

  • 直接加在一个变量的前面,然后取地址符和变量加起来构成一个新的符号,这个符号表示这个变量的地址。

(3)左值与右值

  • 放在赋值运算符左边的就叫左值,右边的就叫右值。所以赋值操作其实就是:左值 = 右值;
  • 当一个变量做左值时,编译器认为这个变量符号的真实含义是这个变量所对应的那个内存空间
  • 当一个变量做右值时,编译器认为这个变量符号的真实含义是这个变量的值,也就是这个变量所对应的内存空间中存储的那个数。


二、野指针问题

1、野指针的概念及危害

(1)野指针,就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的);

(2)野指针很可能触发运行时段错误(Sgmentation fault);

(3)野指针因为指向地址是不可预知的,所以有3种情况

  • 第一种是指向不可访问(操作系统不允许访问的敏感地址,譬如内核空间)的地址,结果是触发段错误,这种算是最好的情况了;
  • 第二种是指向一个可用的、而且没什么特别意义的空间(譬如我们曾经使用过但是已经不用的栈空间或堆空间),这时候程序运行不会出错,也不会对当前程序造成损害,这种情况下会掩盖你的程序错误,让你以为程序没问题,其实是有问题的;
  • 第三种情况就是指向了一个可用的空间,而且这个空间其实在程序中正在被使用(譬如说是程序的一个变量x),那么野指针的解引用就会刚好修改这个变量x的值,导致这个变量莫名其妙的被改变,程序出现离奇的错误。一般最终都会导致程序崩溃,或者数据被损害。这种危害是最大的。

(5)指针变量如果是局部变量,则分配在栈上,本身遵从栈的规律。

  • 即反复使用,使用完不擦除,所以是脏的,本次在栈上分配到的变量的默认值是上次这个栈空间被使用时余留下来的值。
  • 因此野指针的值是有一定规律不是完全随机,但是这个值的规律对我们没意义。因为不管落在上面野指针3种情况的哪一种,都不是我们想看到的。

2、避免野指针

在指针的解引用之前,一定确保指针指向一个绝对可用的空间。常规的做法是:

  • 第一点:定义指针时,同时初始化为NULL;
  • 第二点:在指针解引用之前,先去判断这个指针是不是NULL;
  • 第三点:指针使用完之后,将其赋值为NULL;
  • 第四点:在指针使用之前,将其赋值绑定给一个可用地址空间。

3、NULL

(1)NULL在C/C++中的定义

#ifdef _cplusplus// 定义这个符号就表示当前是C++环境#define NULL 0// 在C++中NULL就是0#else#define NULL (void *)0// 在C中NULL是强制类型转换为void *的0#endif
(2)在C语言中,int *p;你可以p = (int *)0;但是不可以p = 0;因为类型不相同。

  • NULL的实质其实就是地址0,然后给指针赋初值为NULL,其实就是让指针指向0地址处。
  • 为什么指向0地址处?0地址处作为一个特殊地址(我们认为指针指向这里就表示指针没有被初始化,就表示是野指针),这个地址0地址在一般的操作系统中都是不可被访问的,如果C语言程序员不按规矩(不检查是否等于NULL就去解引用)写代码直接去解引用就会触发段错误,这种已经是最好的结果了。

(3)一般在判断指针是否野指针时,都写成if (NULL != p)而不是写成 if (p != NULL)。


三、const关键字与指针

1、const修饰指针的4种形式

(1)const关键字,在C语言中用来修饰变量,表示这个变量是常量。

(2)const修饰指针有4种形式:

  • 第一种:const int *p;
  • 第二种:int const *p;
  • 第三种:int * const p;
  • 第四种:const int * const p;
  • 一个const关键字只能修饰一个变量,所以关键是搞清楚const是修饰谁的。

2、const修饰的变量其实是可以改的

  • 在某些单片机环境下,const修饰的变量是不可以改的。
  • const修饰的变量到底能不能真的被修改,取决于具体的环境,C语言本身并没有完全严格一致的要求。
  • 在gcc中,const是通过编译器在编译的时候执行检查来确保实现的(也就是说const类型的变量不能改是编译错误,不是运行时错误。)所以我们只要想办法骗过编译器,就可以修改const定义的常量,而运行时不会报错。
  • 更深入一层的原因,是因为gcc把const类型的常量也放在了data段,其实和普通的全局变量放在data段是一样实现的,只是通过编译器认定这个变量是const的,运行时并没有标记const标志,所以只要骗过编译器就可以修改了。
  • const是在编译器中实现的,编译时检查,并非不能骗过。所以在C语言中使用const,就好象是 一种道德约束而非法律约束,所以大家使用const时更多是传递一种信息,就是告诉编译器、也告诉读程序的人,这个变量是不应该也不必被修改的。

#include <stdio.h>int main(void){const int a = 5;//a = 6;// error: assignment of read-only variable ‘a’int *p;p = (int *)&a;// 这里报警高可以通过强制类型转换来消除*p = 6;printf("a = %d.\n", a);// a = 6,结果证明const类型的变量被改了/*int a = 5;// 第一种const int *p1;// p本身不是cosnt的,而p指向的变量是const的// 第二种int const *p2;// p本身不是cosnt的,而p指向的变量是const的// 第三种int * const p3;// p本身是cosnt的,p指向的变量不是const的// 第四种const int * const p4;// p本身是cosnt的,p指向的变量也是const的*p1  = 3;// error: assignment of read-only location ‘*p1’p1 = &a;// 编译无错误无警告*p2 = 5;// error: assignment of read-only location ‘*p2’p2 = &a;// 编译无错误无警告*p3 = 5;// 编译无错误无警告p3 = &a;// error: assignment of read-only variable ‘p3’p4 = &a;// error: assignment of read-only variable ‘p4’*p4 = 5;// error: assignment of read-only location ‘*p4’*/return 0;}


四、深入学习数组

1、从内存角度来理解数组

(1)从内存角度讲,数组变量就是一次分配多个变量,而且这多个变量在内存中的存储单元是依次相连接的。

(2)分开定义多个变量(譬如int a, b, c, d;)和一次定义一个数组(int a[4]);这两种定义方法相同点是都定义了4个int型变量,而且这4个变量都是独立的单个使用的;不同点是单独定义时a、b、c、d在内存中的地址不一定相连,但是定义成数组后,数组中的4个元素地址肯定是依次相连的。

(3)数组中多个变量虽然必须单独访问,但是因为他们的地址彼此相连,因此很适合用指针来操作,因此数组和指针天生就叫纠结在一起。

2、从编译器角度来理解数组

(1)从编译器角度来讲,数组变量也是变量,和普通变量和指针变量并没有本质不同。

  • 变量的本质就是一个地址,这个地址在编译器中决定具体数值,具体数值和变量名绑定,变量类型决定这个地址的延续长度。
  • 搞清楚变量、变量名、变量类型这三个概念的具体含义,很多问题都清楚了。

3、数组中几个关键符号(a a[0] &a &a[0])的理解(前提是 int a[10])

(1)a是数组名

  • a做左值时表示整个数组的所有空间(10×4=40字节),又因为C语言规定数组操作时要独立单个操作,不能整体操作数组,所以a不能做左值;
  • a做右值表示数组首元素(数组的第0个元素,也就是a[0])的首地址(首地址就是起始地址,就是4个字节中最开始第一个字节的地址)。
  • a做右值等同于&a[0];

(2)a[0]表示数组的首元素,也就是数组的第0个元素。

  • 做左值时表示数组第0个元素对应的内存空间(连续4字节);
  • 做右值时表示数组第0个元素的值(也就是数组第0个元素对应的内存空间中存储的那个数);

(3)&a就是数组名a取地址,字面意思来看就应该是数组的地址。

  • &a不能做左值(&a实质是一个常量,不是变量因此不能赋值,所以自然不能做左值。);
  • &a做右值时表示整个数组的首地址。

(4)&a[0]字面意思就是数组第0个元素的首地址(搞清楚[]和&的优先级,[]的优先级要高于&,所以a先和[]结合再取地址)。

(5)为什么数组的地址是常量?

  • 因为数组是编译器在内存中自动分配的。当我们每次执行程序时,运行时都会帮我们分配一块内存给这个数组,只要完成了分配,这个数组的地址就定好了,本次程序运行直到终止都无法再改了。那么我们在程序中只能通过&a来获取这个分配的地址,却不能去用赋值运算符修改它。

(6)总结

  • &a和a做右值时的区别:&a是整个数组的首地址,而a是数组首元素的首地址。这两个在数字上是相等的,但是意义不相同。意义不相同会导致他们在参与运算的时候有不同的表现。
  • a和&a[0]做右值时意义和数值完全相同,完全可以互相替代。
  • &a是常量,不能做左值。
  • a做左值代表整个数组所有空间,所以a不能做左值。

五、指针与数组

1、以指针方式来访问数组元素

(1)数组元素使用时不能整体访问,只能单个访问。访问方式有2种:数组形式和指针形式。

  • 数组格式访问数组元素是:数组名[下标]; (注意下标从0开始);
  • 指针格式访问数组元素是:*(指针+偏移量); 如果指针是数组首元素地址(a或者&a[0]),那么偏移量就是下标;指针也可以不是首元素地址而是其他哪个元素的地址,这时候偏移量就要考虑叠加了。
  • 数组下标方式和指针方式均可以访问数组元素,两者的实质其实是一样的。在编译器内部都是用指针方式来访问数组元素的,数组下标方式只是编译器提供给编程者一种壳(语法糖)而已。所以用指针方式来访问数组才是本质的做法。

2、从内存角度理解指针访问数组的实质

  • 数组中各个元素的地址是依次相连的,而且数组还有一个很大的特点(其实也是数组的一个限制)就是数组中各个元素的类型比较相同。类型相同就决定了每个数组元素占几个字节是相同的(譬如int数组每个元素都占4字节,没有例外)。这两个特点就决定了只要知道数组中一个元素的地址,就可以很容易推算出其他元素的地址。

3、指针和数组类型的匹配问题

int *p; int a[5];p = a;// 类型匹配int *p; int a[5];p = &a;// 类型不匹配。p是int *,&a是整个数组的指针int (*)[5]

4、总结:指针类型决定了指针如何参与运算

  • 指针变量+1,并不是真的加1,而是加1*sizeof(指针类型);
  • 如果是int *指针,则+1就实际表示地址+4,如果是char *指针,则+1就表示地址+1;如果是double *指针,则+1就表示地址+8.
  • 指针变量+1时实际不是加1而是加1×sizeof(指针类型),主要原因是希望指针+1后刚好指向下一个元素(而不希望错位)。

六、指针与强制类型转换

1、变量的数据类型的含义

(1)所有的类型的数据存储在内存中,都是按照二进制格式存储的。所以内存中只知道有0和1,不知道是int的、还是float的还是其他类型。

(2)int、char、short等属于整型,存储方式(数转换成二进制往内存中放的方式)是相同的,只是内存格子大小不同(所以这几种整形就彼此叫二进制兼容格式);而float和double的存储方式彼此不同,和整形更不同。

  • int a = 5;时,编译器给a分配4字节空间,并且将5按照int类型的存储方式转成二进制存到a所对应的内存空间中去(a做左值的);
  • 我们printf去打印a的时候(a此时做右值),printf内部的vsprintf函数会按照格式化字符串(就是printf传参的第一个字符串参数中的%d之类的东西)所代表的类型去解析a所对应的内存空间,解析出的值用来输出。
  • 也就是说,存进去时是按照这个变量本身的数据类型来存储的(譬如本例中a为int所以按照int格式来存储);但是取出来时是按照printf中%d之类的格式化字符串的格式来提取的。
  • 此时虽然a所代表的内存空间中的10101序列并没有变(内存是没被修改的)但是怎么理解(怎么把这些1010转成数字)就不一定了。
  • 譬如我们用%d来解析,那么还是按照int格式解析则值自然还是5;但是如果用%f来解析,则printf就以为a对应的内存空间中存储的是一个float类型的数,会按照float类型来解析,值自然是很奇怪的一个数字了。

2、指针数据类型转换实例分析1(int * -> char *)

  • int和char类型都是整形,类型兼容的。所以互转的时候有时候错有时候对。
  • int和char的不同在于char只有1个字节而int有4个字节,所以int的范围比char大。
  • 在char所表示的范围之内int和char是可以互转的不会出错;但是超过了char的范围后char转成int不会错。

3、指针数据类型转换实例分析2(int * -> float *)

  • int和float的解析方式是不兼容的,所以int *转成float *再去访问绝对会出错。


七、指针、数组与sizeof运算符

1、sizeof

  • 是C语言的一个运算符(主要sizeof不是函数,虽然用法很像函数),sizeof的作用是用来返回()里面的变量或者数据类型占用的内存字节数。
  • sizeof存在的价值?主要是因为在不同平台下各种数据类型所占的内存字节数不尽相同(譬如int在32位系统中为4字节,在16位系统中为2字节···)。所以程序中需要使用sizeof来判断当前变量/数据类型在当前环境下占几个字节。

2、代码测试

char str[] = ”hello”;  //sizeof(str)sizeof(str[0])strlen(str)char *p=str; //sizeof(p)sizeof(*p)strlen(p)

  • 32位系统中所有指针的长度都是4,不管是什么类型的指针。
  • strlen是一个C库函数,用来返回字符串的长度(字符串的长度是不计算字符串末尾的'\0'的)。注意strlen接收的参数必须是字符串(字符串的特征是以'\0'结尾)
int n=10;// sizeof(n)

  • sizeof测试一个变量本身,和sizeof测试这个变量的类型,结果是一样的。

int b[100];//sizeof(b)

  • sizeof(数组名)的时候,数组名不做左值也不做右值,纯粹就是数组名的含义。那么sizeof(数组名)实际返回的是整个数组所占用内存空间(以字节为单位的)。

void fun(int b[100]){  }//sizeof(b)   

  • 函数形参是数组时,实际传递是不是整个数组,而是数组的首元素首地址。也就是说函数传参用数组来传,实际相当于传递的是指针(指针指向数组的首元素首地址)。
int b1[100] = {0};printf("sizeof(b1) = %d.\n", sizeof(b1));// 400 100×sizeof(int)short b2[100] = {};printf("sizeof(b2) = %d.\n", sizeof(b2));// 200 100×sizeof(short)double b3[100];printf("sizeof(b3) = %d.\n", sizeof(b3));// 800 100×sizeof(double)*//*int n = 10;printf("sizeof(n) = %d.\n", sizeof(n));// 4printf("sizeof(int) = %d.\n", sizeof(int));// 4*//*char str[] = "hello";  char *p = str; printf("sizeof(p) = %d.\n", sizeof(p));// 4 相当于sizeof(char *)printf("sizeof(*p) = %d.\n", sizeof(*p));// 1 相当于sizeof(char)printf("strlen(p) = %d.\n", strlen(p));// 5 相当于strlen(str)*//*char str[] = "hello";  printf("sizeof(str) = %d.\n", sizeof(str));// 6printf("sizeof(str[0]) = %d.\n", sizeof(str[0]));// 1printf("strlen(str) = %d.\n", strlen(str));// 5*/


八、指针与函数传参

1、普通变量作为函数形参

  • 在子函数内部,形参的值等于实参。原因是函数调用时把实参的值赋值给了形参。值传递!

2、数组作为函数形参

  • 函数名作为形参传参时,实际传递是不是整个数组,而是数组的首元素的首地址(也就是整个数组的首地址。因为传参时是传值,所以这两个没区别)。所以在子函数内部,传进来的数组名就等于是一个指向数组首元素首地址的指针。所以sizeof得到的是4。
  • 在子函数内传参得到的数组首元素首地址,和外面得到的数组首元素首地址的值是相同的。传址调用就是调用子函数时传了地址(也就是指针),此时可以通过传进去的地址来访问实参。
  • 数组作为函数形参时,[]里的数字是可有可无的。因为数组名做形参传递的实际只是个指针,根本没有数组长度这个信息。

3、指针作为函数形参

  • 和数组作为函数形参是一样的。这就好像指针方式访问数组元素和数组方式访问数组元素的结果一样是一样的。

4、结构体变量作为函数形参

  • 结构体变量作为函数形参的时候,实际上和普通变量(类似于int之类的)传参时表现是一模一样的。所以说结构体变量其实也是普通变量而已。
  • 因为结构体一般都很大,所以如果直接用结构体变量进行传参,那么函数调用效率就会很低。(因为在函数传参的时候需要将实参赋值给形参,所以当传参的变量越大调用效率就会越低)。怎么解决?思路只有一个那就是不要传变量了,改传变量的指针(地址)进去。
  • 结构体因为自身太大,所以传参应该用指针来传(但是程序员可以自己决定,你非要传结构体变量过去C语言也是允许的,只是效率低了);回想一下数组,为什么C语言设计的时候数组传参默认是传的数组首元素首地址而不是整个数组?

5、传值调用与传址调用

  • C语言本身函数调用时一直是传值的,只不过传的值可以是变量名,也可以是变量的指针。


九、输入型参数与输出型参数

1、函数为什么需要形参与返回值?

(1)函数名是一个符号,表示整个函数代码段的首地址,实质是一个指针常量,所以在程序中使用到函数名时都是当地址用的,用来调用这个函数的。

(2)函数体是函数的关键,由一对{}括起来,包含很多句代码,函数体就是函数实际做的工作。

(3)形参列表和返回值

  • 形参是函数的输入部分,返回值是函数的输出部分。对函数最好的理解就是把函数看成是一个加工机器(程序其实就是数据加工器),形参列表就是这个机器的原材料输入端;而返回值就是机器的成品输出端。
  • 其实如果没有形参列表和返回值,函数也能对数据进行加工,用全局变量即可。用全局变量来传参和用函数参数列表返回值来传参各有特点,在实践中都有使用。总的来说,函数参数传参用的比较多,因为这样可以实现模块化编程,而C语言中也是尽量减少使用全局变量。
  • 全局变量传参最大的好处就是省略了函数传参的开销,所以效率要高一些;但是实战中用的最多的还是传参,如果参数很多传参开销非常大,通常的做法是把很多参数打包成一个结构体,然后传结构体变量指针进去。

2、函数传参中使用const指针

  • const一般用在函数参数列表中,用法是const int *p;(意义是指针变量p本身可变的,而p所指向的变量是不可变的)。
  • const用来修饰指针做函数传参,作用就在于声明在函数内部不会改变这个指针所指向的内容,所以给该函数传一个不可改变的指针(char *p = "linux";这种)不会触发错误;而一个未声明为const的指针的函数,你给他传一个不可更改的指针的时候就要小心了。

3、函数需要向外部返回多个值时

  • 现实编程中,一个函数需要返回多个值是非常普遍的,因此完全依赖于返回值是不靠谱的,通常的做法是用参数来做返回(在典型的linux风格函数中,返回值是不用来返回结果的,而是用来返回0或者负数用来表示程序执行结果是对还是错,是成功还是失败)。
  • 普遍做法,编程中函数的输入和输出都是靠函数参数的,返回值只是用来表示函数执行的结果是对(成功)还是错(失败)。如果这个参数是用来做输入的,就叫输入型参数;如果这个参数的目的是用来做输出的,就叫输出型参数。输出型参数就是用来让函数内部把数据输出到函数外部的。

4、哪个参数做输入哪个做输出?

  • 函数传参如果传的是普通变量(不是指针)那肯定是输入型参数;
  • 如果传指针就有2种可能性,为了区别,经常的做法是:如果这个参数是做输入的(通常做输入的在函数内部只需要读取这个参数而不会需要更改它)就在指针前面加const来修饰;如果函数形参是指针变量并且还没加const,那么就表示这个参数是用来做输出型参数的。譬如C库函数中strcpy函数。

阅读全文
'); })();
0 0
原创粉丝点击
热门IT博客
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 少儿架子鼓培训 怎么学好架子鼓 九拍架子鼓 架子鼓老师 成人学架子鼓 成人学习架子鼓 在哪学架子鼓 funk架子鼓价格 零基础学架子鼓 架子鼓谱入门 funk架子鼓 成人架子鼓培训 架子鼓网站 架子鼓教学app 学习架子鼓费用 学习架子鼓的好处 哪里学架子鼓好 儿童学架子鼓 几岁可以学架子鼓 架子鼓学习费用 架子鼓价钱 dixon架子鼓 架子鼓学校 去哪学架子鼓 架子鼓招生 学架子鼓贵吗 架子鼓能自学么 架子鼓培训多少钱 学打架子鼓 架子鼓怎么敲 架子鼓专卖店 架子鼓学习app 花式架子鼓 架子鼓价位 相信自己架子鼓 学架子鼓的基本功 儿童架子鼓培训班 几岁学架子鼓合适 架子鼓可以自学吗 架子鼓女孩 架子鼓双踩教程