C语言总结-----指针

目录

 

一、什么是指针

二、指针变量(*取数据,&表地址)

三、指针与数组

(一)数组指针

(二)指针数组

(三)指针与二维数组(二维数组指针)

四、指针与字符串

(一)数组存放字符串(字符数组)

(二)字符指针

(三)两类存放字符的区别

(四)何种情况使用字符数组还是字符串常量

五、指针与函数

(一)为什么用指针或数组名作为函数的参数

(二)指针变量作为函数返回值(指针函数)

(三)函数指针

六、二级指针

七、main函数的形参

八、总结


一、什么是指针

  • 地址就是指针
  • 数据和代码:

数据和代码都以二进制的形式存储在内存中

操作系统会给不同的内存块指定不同的权限

拥有读取和执行权限的内存块就是代码

而拥有读取和写入权限(也可能只有读取权限)的内存块就是数据

  •  CPU只能通过地址来取得内存中的代码和数据CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。
  • 注意:虽然变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符,但在编写代码的过程中,我们认为(1)变量名表示的是数据本身(2)函数名、字符串名和数组名表示的是代码块或数据块的首地址。

二、指针变量(*取数据,&表地址)

  • 数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量
  • 同时使用&和*

假设有一个 int 类型的变量 a,pa 是指向它的指针,那么*&a&*pa分别是什么意思呢?
 
*&a可以理解为*(&a),&a表示取变量 a 的地址(等价于 pa),*(&a)表示取这个地址上的数据(等价于 *pa),所以*&a仍然等价于 a。
 
&*pa可以理解为&(*pa),*pa表示取得 pa 指向的数据(等价于 a),&(*pa)表示数据的地址(等价于 &a),所以&*pa等价于 pa。

  • 指针变量的计算

(1)数组中的所有元素在内存中是连续排列的,如果一个指针指向了数组中的某个元素,那么加 1 就表示指向下一个元素,减 1 就表示指向上一个元素,这样指针的加减运算就具有了现实的意义。

 (2)除去数组等特殊数据体外,不要对指向普通变量的指针进行加减运算。(如果连续定义多个变量,它们有可能是挨着的,也有可能是分散的)

三、指针与数组

(一)数组指针

  • 定义:如果一个指针指向了数组,我们就称它为数组指针(Array Pointer)。

数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关,p 指向的数组元素是 int 类型,所以 p 的类型必须也是int *。

int arr[] = { 99, 15, 100, 888, 252 };
int *p = arr;
arr 是数组第 0 个元素的地址,所以int *p = arr;也可以写作int *p = &arr[0];。也就是说,arr、p、&arr[0] 这三种写法都是等价的。

  • 访问数组元素的两种方案

引入数组指针后,我们就有两种方案来访问数组元素了,一种是使用下标,另外一种是使用指针
(1) 使用下标
也就是采用 arr[i] 的形式访问数组元素。如果 p 是指向数组 arr 的指针,那么也可以使用 p[i] 来访问数组元素,它等价于 arr[i]。
(2) 使用指针
也就是使用 *(p+i) 的形式访问数组元素。另外数组名本身也是指针,也可以使用 *(arr+i) 来访问数组元素,它等价于 *(p+i)。 
     不管是数组名还是数组指针,都可以使用上面的两种方式来访问数组元素。不同的是,数组名是常量,它的值不能改变,而数组指针是变量,它的值可以任意改变。也就是说,数组名只能指向数组的开头,而数组指针可以指向数组开头,也可以指向其他元素。

  • 假设 p 是指向数组 arr 中第 n 个元素的指针,那么 *p++、*++p、(*p)++ 分别是什么意思呢?

 
1、*p++ 等价于 *(p++),表示先取得第 n 个元素的值,再将 p 指向下一个元素,上面已经进行了详细讲解。
 
2、*++p 等价于 *(++p),会先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 *(p+1),所以会获得第 n+1 个数组元素的值。
 
3、(*p)++ 就非常简单了,会先取得第 n 个元素的值,再对该元素的值加 1。假设 p 指向第 0  个元素,并且第 0 个元素的值为 99,执行完该语句后,第 0  个元素的值就会变为 100。

(二)指针数组

  • 定义:如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组
  • 指针数组的定义形式一般为:

dataType *arrayName[length];                 int *arr[ 20 ]
[ ]的优先级高于*,该定义形式应该理解为:
          dataType *(arrayName[length]);    int *( arr[ 20 ] )
括号里面说明arr是一个数组,包含了20个元素,括号外面说明每个元素的类型为int *,即一个存放int类型数据的地址。

(三)指针与二维数组(二维数组指针)

  • 定义一个指向 a 的指针变量 p: 

int  (*p)[4] = a;     //【4】表示每行有4个元素

注意:

1、括号中的*表明 p 是一个指针,它指向一个数组,数组的类型为int [4],这正是 a 所包含的每个一维数组的类型。
2、[ ]的优先级高于*,( )是必须要加的,如果赤裸裸地写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数组指针。

  • 用指向二维数组的指针访问二维数组

     

(1) p指向数组 a 的开头,也即第 0 行;p+1前进一行,指向第 1 行。

(2) *(p+1) = a[1]表示取地址上的数据,也就是整个第 1 行数据。
(3) *(p+1)+1表示第 1 行第 1 个元素的地址。如何理解呢?
       *(p+1)单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址,因为使用         整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针;就像一维数组的名字,在定义时或者和                 sizeof、& 一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针。
(4) *(*(p+1) +1) = a[1][1]  表示第 1 行第 1 个元素的值。很明显,增加一个 * 表示取地址上的数据。
       根据上面的结论,可以很容易推出以下的等价关系:
        a+i == p+i
       a[i] == p[i] == *(a+i) == *(p+i)
      a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j)

a+i 第i行   *(a+1)第i行的第0个元素    *(a+1)+j  第i行的第j个元素      

*(*(a+i)+j)   第i行的第j个元素的内容

四、指针与字符串

(一)数组存放字符串(字符数组)

C语言中没有特定的字符串类型,我们通常是将字符串放在一个字符数组中,

字符数组归根结底还是一个数组,上节讲到的关于指针和数组的规则同样也适用于字符数组

(二)字符指针

  除了字符数组,C语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串,
例如:char *str = "hello";     或者:    char *str;str = "hello";
字符串中的所有字符在内存中是连续排列的,str 指向的是字符串的第 0 个字符;我们通常将第 0 个字符的地址称为字符串的首地址。字符串中每个字符的类型都是char,所以 str 的类型也必须是char *。

(三)两类存放字符的区别


     它们最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。内存权限的不同导致的一个明显结果就是,字符数组在定义后可以读取和修改每个字符,而对于第二种形式的字符串,一旦被定义后就只能读取不能修改,任何对它的赋值都是错误的。另外,字符数组的sieof(str)为数组中元素个数(包括‘\0’),字符指针的sizeof(str)为指针大小


(四)何种情况使用字符数组还是字符串常量


     在编程过程中如果只涉及到对字符串的读取,那么字符数组和字符串常量都能够满足要求;如果有写入(修改)操作,只能使用字符数组,不能使用字符串常量。

五、指针与函数

(一)为什么用指针或数组名作为函数的参数


1、使用由于函数最多有一个返回值,为了得到更多的函数返回值,用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。因此可以得到多个函数的返回值。
(任务:编写mystrlen,mystrcmp,mystrcat,mystrcpy)
2、像数组、字符串、动态分配的内存等都是一系列数据的集合没有办法通过一个参数全部传入函数内部,只能传递它们的指针,在函数内部通过指针来影响这些数据集合。

  • 注意:

(1)将数组作为函数参数时会被弱化成指针,因此传递到函数内部的都是数组指针(地址),所以在函数内部无法通过这个指针获得数组长度,必须将数组长度作为函数参数传递到函数内部。
(2)用数组做函数参数时,它的参数还可以写成下面的形式:(也都是将数组地址传递过来而已)
int max(int Arr[6], int len);     或者    int max(int Arr[], int len);
 

  • C语言为什么不允许直接传递数组的所有元素,而必须传递数组指针呢?

参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。这样会严重拖慢程序的效率,因此C语言没有从语法上支持数据集合的直接赋值。
 

(1)一维数组名作为函数的参数
注意:定义函数时指定的数组大小不起作用,编译时系统不检查形参数组大小,只将实参数组的首地址传递给形参数组名。因此,形参数组可以不指定大小,只需在数组名后跟一个空括号就可以了。
(2)二维数组名作为函数的参数
注意:同一维数组不同的是:二维数组必须指定第2维的大小(即列数要确定),在形参和实参列数相同的情况下,第1维大小可以不同,但是实参数组和形参数组的首地址还是相同的。

(二)指针变量作为函数返回值(指针函数)

  • C语言允许函数的返回值是一个指针(地址),我们将这样的函数称为指针函数。//int *f(int)
  • 注意:

1、用指针作为函数返回值时需要注意的一点是,函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误。

2、这里所谓的销毁并不是将局部数据所占用的内存全部抹掉,而是程序放弃对它的使用权限,弃之不理,后面的代码可以随意使用这块内存。

(三)函数指针

      一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。
 
函数指针的定义形式为:
returnType (*pointerName)(param list);    //int(*f)(int)
returnType 为函数返回值类型,pointerNmae 为指针名称,param list 为函数参数列表。参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称,这一点和函数原型非常类似。注意,( )的优先级高于*,第一个括号不能省略

六、二级指针

  • 如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。

假设有一个 int 类型的变量 a,p1是指向 a 的指针变量,p2 又是指向 p1 的指针变量,它们的关系如下图所示:

将这种关系转换为C语言代码:
int a =100;
int *p1 = &a;
int **p2 = &p1;

  • 指针变量也是一种变量,也会占用存储空间,也可以使用&获取它的地址。C语言不限制指针的级数,每增加一级指针,在定义指针变量时就得增加一个星号*。p1 是一级指针,指向普通类型的数据,定义时有一个*;p2 是二级指针,指向一级指针 p1,定义时有两个*。但是在实际开发中会经常使用一级指针和二级指针,几乎用不到高级指针

七、main函数的形参

    main函数可以不带参数,也可以带参数,这个参数可以认为是 main函数的形式参数。C语言规定main函数的参数只能有两个,习惯上这两个参数写为argc和argv。c语言规定argc(第一个形参)必须是整型变量,argv( 第二个形参)必须是指向字符串的指针数组。加上形参说明后,main函数的函数头应写为:
    main (int argc,char *argv[])
说明: argc是命令行输入的字符串个数(即命令个数);*argv[]是字符串指针数组,其各元素值为命令行中各字符串(命令均按字符串处理)的首地址(即存放指向各字符串第一个元素的指针)。
char *argv[] = {"./a.out","hhh","kkk"}

八、总结

  • 指针(Pointer)就是内存的地址,C语言允许用一个变量来存放指针,这种变量称为指针变量。指针变量可以存放基本类型数据的地址,也可以存放数组、函数及其他指针变量的地址。
  •       程序在运行过程中需要的是数据和指令的地址,变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符:在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址;程序被编译和链接后,这些名字都会消失,取而代之的是它们对应的地址

(1) 指针变量可以进行加减运算,例如p++、p+i、p-=i。指针变量的加减运算并不是简单的加上或减去一个整数,而是跟指针指向的数据类型有关
(2) 给指针变量赋值时,要将一份数据的地址赋给它,不能直接赋给一个整数,例如int *p = 1000;是没有意义的,使用过程中一般会导致程序崩溃。
(3) 使用指针变量之前一定要初始化,否则就不能确定指针指向哪里,如果它指向的内存没有使用权限,程序就崩溃了。对于暂时没有指向的指针,建议赋值NULL。
(4) 两个指针变量可以相减。如果两个指针变量指向同一个数组中的某个元素,那么相减的结果就是两个指针之间的元素个数。
(5) 数组也是有类型的,数组名的本意是表示一组类型相同的数据。在定义数组时,或者和 sizeof、& 运算符一起使用时数组名才表示整个数组,表达式中的数组名会被转换为一个指向数组首地址的指针。
 

猜你喜欢

转载自blog.csdn.net/lishasha5/article/details/83216279