22

C语言指针学习总结

 4 years ago
source link: http://www.cnblogs.com/veeupup/p/12380426.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

这里对 C 语言的指针进行比较详细的整理总结,参考网络上部分资料整理如下。

指针概念

计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用4个字节,char 占用1个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。

我们将内存中字节的编号称为地址(Address)或指针(Pointer)。地址从 0 开始依次增加,对于 32 位环境,程序能够使用的内存为 4GB,最小的地址为 0,最大的地址为 0XFFFFFFFF。

输出一个地址:

int a = 100;
char str[20] = "tanweime.com";
printf("%#X, %p\n", &a, str);

---
运行结果:
0XE42523AC, 0XE4252390

%#X%p 表示以十六进制形式输出,并附带前缀 0X 。a 是一个变量,用来存放整数,需要在前面加 & 来获得它的地址;str 本身就表示字符串的首地址,不需要加 &

C语言中有一个控制符 %p ,专门用来以十六进制形式输出地址,不过 %p 的输出格式并不统一,有的编译器带 0x 前缀,有的不带

一切都是地址

C语言用变量来存储数据,用函数来定义一段可以重复使用的代码,它们最终都要放到内存中才能供 CPU 使用。

数据和代码都以二进制的形式存储在内存中,计算机无法从格式上区分某块内存到底存储的是数据还是代码。当程序被加载到内存后,操作系统会给不同的内存块指定不同的权限,拥有读取和执行权限的内存块就是代码,而拥有读取和写入权限(也可能只有读取权限)的内存块就是数据。

CPU 只能通过地址来取得内存中的代码和数据,程序在执行过程中会告知 CPU 要执行的代码以及要读写的数据的地址。如果程序不小心出错,或者开发者有意为之,在 CPU 要写入数据时给它一个代码区域的地址,就会发生内存访问错误。这种内存访问错误会被硬件和操作系统拦截,强制程序崩溃,程序员没有挽救的机会。

CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。

指针变量

数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量。

在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。指针变量的值就是某份数据的地址,这样的一份数据可以是数组、字符串、函数,也可以是另外的一个普通变量或指针变量。

现在假设有一个 char 类型的变量 c,它存储了字符 'K'(ASCII码为十进制数 75),并占用了地址为 0X11A 的内存(地址通常用十六进制表示)。另外有一个指针变量 p,它的值为 0X11A,正好等于变量 c 的地址,这种情况我们就称 p 指向了 c,或者说 p 是指向变量 c 的指针。

定义指针变量

定义指针变量与定义普通变量非常类似,不过要在变量名前面加星号 * ,格式为:

datatype *name;

或者

datatype *name = value;

* 表示这是一个指针变量, datatype 表示该指针变量所指向的数据的类型 。例如:

int *p1;

p1 是一个指向 int 类型数据的指针变量,至于 p1 究竟指向哪一份数据,应该由赋予它的值决定。再如:

int a = 100;int *p_a = &a;

在定义指针变量 p_a 的同时对它进行初始化,并将变量 a 的地址赋予它,此时 p_a 就指向了 a。值得注意的是,p_a 需要的一个地址,a 前面必须要加取地址符 & ,否则是不对的。

和普通变量一样,指针变量也可以被多次写入,只要你想,随时都能够改变指针变量的值,请看下面的代码:

//定义普通变量
float a = 99.5, b = 10.6;char c = '@', d = '#';
//定义指针变量
float *p1 = &a;char *p2 = &c;
//修改指针变量的值
p1 = &b;p2 = &d;

* 是一个特殊符号,表明一个变量是指针变量,定义 p1、p2 时必须带 * 。而给 p1、p2 赋值时,因为已经知道了它是一个指针变量,就没必要多此一举再带上 * ,后边可以像使用普通变量一样来使用指针变量。也就是说,定义指针变量时必须带 * ,给指针变量赋值时不能带 *

指针变量也可以连续定义,例如:

int *a, *b, *c;  //a、b、c 的类型都是 int*

注意每个变量前面都要带 * 。如果写成下面的形式,那么只有 a 是指针变量,b、c 都是类型为 int 的普通变量:

int *a, b, c;

通过指针变量取得数据

指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:

*pointer ;

这里的 * 称为指针运算符,用来取得某个地址上的数据,请看下面的例子:

#include <stdio.h>
int main(){    
        int a = 15;    
      int *p = &a;    
      printf("%d, %d\n", a, *p);  //两种方式都可以输出a的值    
return 0;}

运行结果:

15, 15

假设 a 的地址是 0X1000,p 指向 a 后,p 本身的值也会变为 0X1000, p 表示获取地址 0X1000 上的数据,也即变量 a 的值。从运行结果看, p 和 a 是等价的。

上节我们说过,CPU 读写数据必须要知道数据在内存中的地址,普通变量和指针变量都是地址的助记符,虽然通过 p 和 a 获取到的数据一样,但它们的运行过程稍有不同:a 只需要一次运算就能够取得数据,而 p 要经过两次运算,多了一层“间接”。

假设变量 a、p 的地址分别为 0X1000、0XF0A0,它们的指向关系如下图所示:

qIrQrmy.jpg!web

程序被编译和链接后,a、p 被替换成相应的地址。使用 *p 的话,要先通过地址 0XF0A0 取得变量 p 本身的值,这个值是变量 a 的地址,然后再通过这个值取得变量 a 的数据,前后共有两次运算;而使用 a 的话,可以通过地址 0X1000 直接取得它的数据,只需要一步运算。

也就是说,使用指针是间接获取数据,使用变量名是直接获取数据,前者比后者的代价要高。

指针除了可以获取内存上的数据,也可以修改内存上的数据,例如:

int a = 15, b = 99, c = 222;
int *p = &a; //定义指针变量
*p = b;      //通过指针变量修改内存上的数据
c = *p;      //通过指针变量获取内存上的数据
printf("%d, %d, %d, %d\n", a, b, c, *p);

运行结果:

99, 99, 99, 99

*p 代表的是 a 中的数据,它等价于 a,可以将另外的一份数据赋值给它,也可以将它赋值给另外的一个变量。

* 在不同的场景下有不同的作用: * 可以用在指针变量的定义中,表明这是一个指针变量,以和普通变量区分开;使用指针变量时在前面加 * 表示获取指针指向的数据,或者说表示的是指针指向的数据本身。

也就是说,定义指针变量时的 * 和使用指针变量时的 * 意义完全不同。以下面的语句为例:

int *p = &a;*p = 100;

第1行代码中 * 用来指明 p 是一个指针变量,第2行代码中 * 用来获取指针指向的数据。

需要注意的是,给指针变量本身赋值时不能加 * 。修改上面的语句:

int *p;p = &a;*p = 100;

第2行代码中的 p 前面就不能加 *

指针变量也可以出现在普通变量能出现的任何表达式中,例如:

int x, y, *px = &x, *py = &y;
y = *px + 5;  //表示把x的内容加5并赋给y,*px+5相当于(*px)+5
y = ++*px;  //px的内容加上1之后赋给y,++*px相当于++(*px)
y = *px++;  //相当于y=(*px)++
py = px;  //把一个指针的值赋给另一个指针

关于 * 和 & 的谜题

假设有一个 int 类型的变量 a,pa 是指向它的指针,那么 *&a&*pa 分别是什么意思呢?

*&a 可以理解为 *(&a)&a 表示取变量 a 的地址(等价于 pa), *(&a) 表示取这个地址上的数据(等价于 *pa),绕来绕去,又回到了原点, *&a 仍然等价于 a。

&*pa 可以理解为 &(*pa)*pa 表示取得 pa 指向的数据(等价于 a), &(*pa) 表示数据的地址(等价于 &a),所以 &*pa 等价于 pa。

对星号 * 的总结

在我们目前所学到的语法中,星号 * 主要有三种用途:

int a = 3, b = 5, c; c = a * b;
int a = 100; int *p = &a;
int a, b, *p = &a; *p = 100; b = *p;

指针变量的运算

指针变量保存的是地址,本质上是一个整数,可以进行部分运算,例如加法、减法、比较等,请看下面的代码:

#include <stdio.h>
int main(){
    int    a = 10,   *pa = &a, *paa = &a;
    double b = 99.9, *pb = &b;
    char   c = '@',  *pc = &c;
    //最初的值
    printf("&a=%#X, &b=%#X, &c=%#X\n", &a, &b, &c);
    printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
    //加法运算
    pa++; pb++; pc++;
    printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
    //减法运算
    pa -= 2; pb -= 2; pc -= 2;
    printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
    //比较运算
    if(pa == paa){
        printf("%d\n", *paa);
    }else{
        printf("%d\n", *pa);
    }
    return 0;
}
--------
运行结果:
  &a=0X28FF44, &b=0X28FF30, &c=0X28FF2B
  pa=0X28FF44, pb=0X28FF30, pc=0X28FF2B
  pa=0X28FF48, pb=0X28FF38, pc=0X28FF2C
  pa=0X28FF40, pb=0X28FF28, pc=0X28FF2A
  2686784

从运算结果可以看出:pa、pb、pc 每次加 1,它们的地址分别增加 4、8、1,正好是 int、double、char 类型的长度;减 2 时,地址分别减少 8、16、2,正好是 int、double、char 类型长度的 2 倍。

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

数组指针

数组(Array)是一系列具有相同类型的数据的集合,每一份数据叫做一个数组元素(Element)。数组中的所有元素在内存中是连续排列的,整个数组占用的是一块内存。以 int arr[] = { 99, 15, 100, 888, 252 }; 为例,该数组在内存中的分布如下图所示:

J3EBFnf.jpg!web

定义数组时,要给出数组名和数组长度,数组名可以认为是一个指针,它指向数组的第 0 个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址。以上面的数组为例,下图是 arr 的指向:

QbIb2mV.jpg!web

#include <stdio.h>
int main(){
    int arr[] = { 99, 15, 100, 888, 252 };
    int len = sizeof(arr) / sizeof(int);  //求数组长度
    int i;
    for(i=0; i<len; i++){
        printf("%d  ", *(arr+i) );  //*(arr+i)等价于arr[i]
    }
    printf("\n");
    return 0;
}
----
运行结果:
  99  15  100  888  252

第 5 行代码用来求数组的长度,sizeof(arr) 会获得整个数组所占用的字节数,sizeof(int) 会获得一个数组元素所占用的字节数,它们相除的结果就是数组包含的元素个数,也即数组长度。

第 8 行代码中我们使用了 *(arr+i) 这个表达式,arr 是数组名,指向数组的第 0 个元素,表示数组首地址, arr+i 指向数组的第 i 个元素,*(arr+i) 表示取第 i 个元素的数据,它等价于 arr[i]。

arr 是 int* 类型的指针,每次加 1 时它自身的值会增加 sizeof(int),加 i 时自身的值会增加 sizeof(int) * i

我们也可以定义一个指向数组的指针,例如:

int arr[] = { 99, 15, 100, 888, 252 };int *p = arr;

arr 本身就是一个指针,可以直接赋值给指针变量 p。arr 是数组第 0 个元素的地址,所以 int *p = arr; 也可以写作 int *p = &arr[0]; 。也就是说,arr、p、&arr[0] 这三种写法都是等价的,它们都指向数组第 0 个元素,或者说指向数组的开头。

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

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

反过来想,p 并不知道它指向的是一个数组,p 只知道它指向的是一个整数,究竟如何使用 p 取决于程序员的编码。

更改上面的代码,使用数组指针来遍历数组元素:

#include <stdio.h>
int main(){
    int arr[] = { 99, 15, 100, 888, 252 };
    int i, *p = arr, len = sizeof(arr) / sizeof(int);
    for(i=0; i<len; i++){
        printf("%d  ", *(p+i) );
    }
    printf("\n");
    return 0;
}

引入数组指针后,我们就有两种方案来访问数组元素了,一种是使用下标,另外一种是使用指针。

1) 使用下标

也就是采用 arr[i] 的形式访问数组元素。如果 p 是指向数组 arr 的指针,那么也可以使用 p[i] 来访问数组元素,它等价于 arr[i]。

2) 使用指针

也就是使用 (p+i) 的形式访问数组元素。另外数组名本身也是指针,也可以使用 (arr+i) 来访问数组元素,它等价于 *(p+i)。

关于数组指针的谜题

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

p++ 等价于 (p++),表示先取得第 n 个元素的值,再将 p 指向下一个元素,上面已经进行了详细讲解。

++p 等价于 (++p),会先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 *(p+1),所以会获得第 n+1 个数组元素的值。

(*p)++ 就非常简单了,会先取得第 n 个元素的值,再对该元素的值加 1。假设 p 指向第 0 个元素,并且第 0 个元素的值为 99,执行完该语句后,第 0 个元素的值就会变为 100。

字符串指针

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

#include <stdio.h>
#include <string.h>
int main(){
    char str[] = "tanweime";
    int len = strlen(str), i;
    //直接输出字符串
    printf("%s\n", str);
    //每次输出一个字符
    for(i=0; i<len; i++){
        printf("%c", str[i]);
    }
    printf("\n");
    return 0;
}

除了字符数组,C语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串,例如:

char *str = "tanweime";

或者:

char *str;str = "tanweime";

字符串中的所有字符在内存中是连续排列的,str 指向的是字符串的第 0 个字符;我们通常将第 0 个字符的地址称为字符串的首地址。字符串中每个字符的类型都是 char ,所以 str 的类型也必须是 char *

下面的例子演示了如何输出这种字符串:

#include <stdio.h>
#include <string.h>
int main(){
    char *str = "tanweime";
    int len = strlen(str), i;
   
    //直接输出字符串
    printf("%s\n", str);
    //使用*(str+i)
    for(i=0; i<len; i++){
        printf("%c", *(str+i));
    }
    printf("\n");
    //使用str[i]
    for(i=0; i<len; i++){
        printf("%c", str[i]);
    }
    printf("\n");
    return 0;
}

这一切看起来和字符数组是多么地相似,它们都可以使用 %s 输出整个字符串,都可以使用 *[ ] 获取单个字符,这两种表示字符串的方式是不是就没有区别了呢?

有!它们最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。

内存权限的不同导致的一个明显结果就是,字符数组在定义后可以读取和修改每个字符,而对于第二种形式的字符串,一旦被定义后就只能读取不能修改,任何对它的赋值都是错误的。

我们将第二种形式的字符串称为字符串常量,意思很明显,常量只能读取不能写入。

指针作为函数参数

在C语言中,函数的参数不仅可以是整数、小数、字符等具体的数据,还可以是指向它们的指针。用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。

像数组、字符串、动态分配的内存等都是一系列数据的集合,没有办法通过一个参数全部传入函数内部,只能传递它们的指针,在函数内部通过指针来影响这些数据集合。

有的时候,对于整数、小数、字符等基本类型数据的操作也必须要借助指针,一个典型的例子就是交换两个变量的值。

#include <stdio.h>
void swap(int *p1, int *p2){
    int temp;  //临时变量
    temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}
int main(){
    int a = 66, b = 99;
    swap(&a, &b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

调用 swap() 函数时,将变量 a、b 的地址分别赋值给 p1、p2,这样 p1、 p2 代表的就是变量 a、b 本身,交换 p1、 p2 的值也就是交换 a、b 的值。函数运行结束后虽然会将 p1、p2 销毁,但它对外部 a、b 造成的影响是“持久化”的,不会随着函数的结束而“恢复原样”。

需要注意的是临时变量 temp,它的作用特别重要,因为执行 *p1 = *p2; 语句后 a 的值会被 b 的值覆盖,如果不先将 a 的值保存起来以后就找不到了。

这就好比拿来一瓶可乐和一瓶雪碧,要想把可乐倒进雪碧瓶、把雪碧倒进可乐瓶里面,就必须先找一个杯子,将两者之一先倒进杯子里面,再从杯子倒进瓶子里面。这里的杯子,就是一个“临时变量”,虽然只是倒倒手,但是也不可或缺。

用数组作函数参数

数组是一系列数据的集合,无法通过参数将它们一次性传递到函数内部,如果希望在函数内部操作数组,必须传递数组指针。下面的例子定义了一个函数 max(),用来查找数组中值最大的元素:

#include <stdio.h>
int max(int *intArr, int len){
    int i, maxValue = intArr[0];  //假设第0个元素是最大值
    for(i=1; i<len; i++){
        if(maxValue < intArr[i]){
            maxValue = intArr[i];
        }
    }
   
    return maxValue;
}
int main(){
    int nums[6], i;
    int len = sizeof(nums)/sizeof(int);
    //读取用户输入的数据并赋值给数组元素
    for(i=0; i<len; i++){
        scanf("%d", nums+i);
    }
    printf("Max value is %d!\n", max(nums, len));
    return 0;
}
--------
  运行结果:
12 55 30 8 93 27↙
Max value is 93!

参数 intArr 仅仅是一个数组指针,在函数内部无法通过这个指针获得数组长度,必须将数组长度作为函数参数传递到函数内部。数组 nums 的每个元素都是整数,scanf() 在读取用户输入的整数时,要求给出存储它的内存的地址, nums+i 就是第 i 个数组元素的地址。

用数组做函数参数时,参数也能够以“真正”的数组形式给出。例如对于上面的 max() 函数,它的参数可以写成下面的形式:

int max(int intArr[], int len){
    int i, maxValue = intArr[0];  //假设第0个元素是最大值
    for(i=1; i<len; i++){
        if(maxValue < intArr[i]){
            maxValue = intArr[i];
        }
    }
    return maxValue;
}

int intArr[] 虽然定义了一个数组,但没有指定数组长度,好像可以接受任意长度的数组。

实际上这两种形式的数组定义都是假象,不管是 int intArr[6] 还是 int intArr[] 都不会创建一个数组出来,编译器也不会为它们分配内存,实际的数组是不存在的,它们最终还是会转换为 int *intArr 这样的指针。这就意味着,两种形式都不能将数组的所有元素“一股脑”传递进来,大家还得规规矩矩使用数组指针。

int intArr[6] 这种形式只能说明函数期望用户传递的数组有 6 个元素,并不意味着数组只能有 6 个元素,真正传递的数组可以有少于或多于 6 个的元素。

需要强调的是,不管使用哪种方式传递数组,都不能在函数内部求得数组长度,因为 intArr 仅仅是一个指针,而不是真正的数组,所以必须要额外增加一个参数来传递数组长度。

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

参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。

对于像 int、float、char 等基本类型的数据,它们占用的内存往往只有几个字节,对它们进行内存拷贝非常快速。而数组是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行内存拷贝有可能是一个漫长的过程,会严重拖慢程序的效率,为了防止技艺不佳的程序员写出低效的代码,C语言没有从语法上支持数据集合的直接赋值。

除了C语言,C++、Java、Python 等其它语言也禁止对大块内存进行拷贝,在底层都使用类似指针的方式来实现。

指针作为返回值

C语言允许函数的返回值是一个指针(地址),我们将这样的函数称为指针函数。下面的例子定义了一个函数 strlong(),用来返回两个字符串中较长的一个

#include <stdio.h>
#include <string.h>
char *strlong(char *str1, char *str2){
    if(strlen(str1) >= strlen(str2)){
        return str1;
    }else{
        return str2;
    }
}
int main(){
    char str1[30], str2[30], *str;
    gets(str1);
    gets(str2);
    str = strlong(str1, str2);
    printf("Longer string: %s\n", str);
    return 0;
}

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

#include <stdio.h>
int *func(){
    int n = 100;
    return &n;
}
int main(){
    int *p = func(), n;
    n = *p;
    printf("value = %d\n", n);
    return 0;
}

前面我们说函数运行结束后会销毁所有的局部数据,这个观点并没错,大部分C语言教材也都强调了这一点。但是,这里所谓的销毁并不是将局部数据所占用的内存全部抹掉,而是程序放弃对它的使用权限,弃之不理,后面的代码可以随意使用这块内存。对于上面的两个例子,func() 运行结束后 n 的内存依然保持原样,值还是 100,如果使用及时也能够得到正确的数据,如果有其它函数被调用就会覆盖这块内存,得到的数据就失去了意义。

二级指针

指针可以指向一份普通类型的数据,例如 int、double、char 等,也可以指向一份指针类型的数据,例如 int 、double 、char * 等。

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

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

QVNZ32A.jpg!web

将这种关系转换为C语言代码:

int a =100;
int *p1 = &a;
int **p2 = &p1;

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

如果我们希望再定义一个三级指针 p3,让它指向 p2,那么可以这样写:

int ***p3 = &p2;

四级指针也是类似的道理:

int ****p4 = &p3;

实际开发中会经常使用一级指针和二级指针,几乎用不到高级指针。

想要获取指针指向的数据时,一级指针加一个 * ,二级指针加两个 * ,三级指针加三个 * ,以此类推,请看代码:

#include <stdio.h>
int main(){
    int a =100;
    int *p1 = &a;
    int **p2 = &p1;
    int ***p3 = &p2;
    printf("%d, %d, %d, %d\n", a, *p1, **p2, ***p3);
    printf("&p2 = %#X, p3 = %#X\n", &p2, p3);
    printf("&p1 = %#X, p2 = %#X, *p3 = %#X\n", &p1, p2, *p3);
    printf(" &a = %#X, p1 = %#X, *p2 = %#X, **p3 = %#X\n", &a, p1, *p2, **p3);
    return 0;
}
------
  100, 100, 100, 100
&p2 = 0XE19322F8, p3 = 0XE19322F8
&p1 = 0XE1932300, p2 = 0XE1932300, *p3 = 0XE1932300
 &twa = 0XE193230C, p1 = 0XE193230C, *p2 = 0XE193230C, **p3 = 0XE193230C

以三级指针 p3 为例来分析上面的代码。 ***p3 等价于 *(*(*p3))p3 得到的是 p2 的值,也即 p1 的地址; ( p3) 得到的是 p1 的值,也即 a 的地址;经过三次“取值”操作后, ( ( p3)) 得到的才是 a 的值。

假设 a、p1、p2、p3 的地址分别是 0X00A0、0X1000、0X2000、0X3000,它们之间的关系可以用下图来描述:

iaEBV33.jpg!web

方框里面是变量本身的值,方框下面是变量的地址。

指针数组

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

dataType *arrayName[length];

[ ] 的优先级高于 * ,该定义形式应该理解为:

dataType *(arrayName[length]);

括号里面说明 arrayName 是一个数组,包含了 length 个元素,括号外面说明每个元素的类型为 dataType *

除了每个元素的数据类型不同,指针数组和普通数组在其他方面都是一样的,下面是一个简单的例子:

#include <stdio.h>
int main(){
    int a = 16, b = 932, c = 100;
    //定义一个指针数组
    int *arr[3] = {&a, &b, &c};//也可以不指定长度,直接写作 int *parr[]
    //定义一个指向指针数组的指针
    int **parr = arr;
    printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);
    printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));
    return 0;
}
------
  运行结果:
  16, 932, 100
    16, 932, 100

指针数组还可以和字符串数组结合使用,请看下面的例子:

#include <stdio.h>
int main(){
    char *str[3] = {
        "tanwei",
        "谭巍",
        "C Language"
    };
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
    return 0;
}

需要注意的是,字符数组 str 中存放的是字符串的首地址,不是字符串本身,字符串本身位于其他的内存区域,和字符数组是分开的。

也只有当指针数组中每个元素的类型都是 char * 时,才能像上面那样给指针数组赋值,其他类型不行。

为了便于理解,可以将上面的字符串数组改成下面的形式,它们都是等价的。

#include <stdio.h>
int main(){
    char *str0 = "tanwei";
    char *str1 = "谭巍";
    char *str2 = "C Language";
    char *str[3] = {str0, str1, str2};
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
    return 0;
}

指针与二维数组

二维数组在概念上是二维的,有行和列,但在内存中所有的数组元素都是连续排列的,它们之间没有“缝隙”。以下面的二维数组 a 为例:

int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };

从概念上理解,a 的分布像一个矩阵:

但在内存中,a 的分布是一维线性的,整个数组占用一块连续的内存:

YvmyA3N.jpg!web

C语言中的二维数组是按行排列的,也就是先存放 a[0] 行,再存放 a[1] 行,最后存放 a[2] 行;每行中的 4 个元素也是依次存放。数组 a 为 int 类型,每个元素占用 4 个字节,整个数组共占用 4×(3×4) = 48 个字节。

C语言允许把一个二维数组分解成多个一维数组来处理。对于数组 a,它可以分解成三个一维数组,即 a[0]、a[1]、a[2]。每一个一维数组又包含了 4 个元素,例如 a[0] 包含 a[0][0]、a[0][1]、a[0][2]、a[0][3]。

假设数组 a 中第 0 个元素的地址为 1000,那么每个一维数组的首地址如下图所示:

iqyq6zQ.png!web

为了更好的理解指针和二维数组的关系,我们先来定义一个指向 a 的指针变量 p:

int (*p)[4] = a;

括号中的 * 表明 p 是一个指针,它指向一个数组,数组的类型为 int [4] ,这正是 a 所包含的每个一维数组的类型。

[ ] 的优先级高于 *( ) 是必须要加的,如果赤裸裸地写作 int *p[4] ,那么应该理解为 int *(p[4]) ,p 就成了一个指针数组,而不是二维数组指针。

对指针进行加法(减法)运算时,它前进(后退)的步长与它指向的数据类型有关,p 指向的数据类型是 int [4] ,那么 p+1 就前进 4×4 = 16 个字节, p-1 就后退 16 个字节,这正好是数组 a 所包含的每个一维数组的长度。也就是说, p+1 会使得指针指向二维数组的下一行, p-1 会使得指针指向数组的上一行。

数组名 a 在表达式中也会被转换为和 p 等价的指针!

下面我们就来探索一下如何使用指针 p 来访问二维数组中的每个元素。按照上面的定义:

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

2) *(p+1) 表示取地址上的数据,也就是整个第 1 行数据。注意是一行数据,是多个数据,不是第 1 行中的第 0 个元素,下面的运行结果有力地证明了这一点:

#include <stdio.h>
int main(){
    int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
    int (*p)[4] = a;
    printf("%d\n", sizeof(*(p+1)));
    return 0;
}
---
  16

3) *(p+1)+1 表示第 1 行第 1 个元素的地址。如何理解呢?

*(p+1) 单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址,因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针;就像一维数组的名字,在定义时或者和 sizeof、& 一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针。

4) *(*(p+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)
#include <stdio.h>
int main(){
    int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
    int(*p)[4];
    int i,j;
    p=a;
    for(i=0; i<3; i++){
        for(j=0; j<4; j++) printf("%2d  ",*(*(p+i)+j));
        printf("\n");
    }
    return 0;
}

指针数组和二维数组指针的区别

指针数组和二维数组指针在定义时非常相似,只是括号的位置不同:

int *(p1[5]);  //指针数组,可以去掉括号直接写作 int *p1[5];int (*p2)[5];  //二维数组指针,不能去掉括号

指针数组和二维数组指针有着本质上的区别:指针数组是一个数组,只是每个元素保存的都是指针,以上面的 p1 为例,在32位环境下它占用 4×5 = 20 个字节的内存。二维数组指针是一个指针,它指向一个二维数组,以上面的 p2 为例,它占用 4 个字节的内存。

函数指针

一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。

函数指针的定义形式为:

returnType (*pointerName)(param list);

returnType 为函数返回值类型,pointerNmae 为指针名称,param list 为函数参数列表。参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称,这一点和函数原型非常类似。

注意 ( ) 的优先级高于 * ,第一个括号不能省略,如果写作 returnType *pointerName(param list); 就成了函数原型,它表明函数的返回值类型为 returnType *

#include <stdio.h>
//返回两个数中较大的一个
int max(int a, int b){
    return a>b ? a : b;
}
int main(){
    int x, y, maxval;
    //定义函数指针
    int (*pmax)(int, int) = max;  //也可以写作int (*pmax)(int a, int b)
    printf("Input two numbers:");
    scanf("%d %d", &x, &y);
    maxval = (*pmax)(x, y);
    printf("Max value: %d\n", maxval);
    return 0;
}

总结

指针(Pointer)就是内存的地址,C语言允许用一个变量来存放指针,这种变量称为指针变量。指针变量可以存放基本类型数据的地址,也可以存放数组、函数以及其他指针变量的地址。

程序在运行过程中需要的是数据和指令的地址,变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符:在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址;程序被编译和链接后,这些名字都会消失,取而代之的是它们对应的地址。

定 义 含 义 int *p; p 可以指向 int 类型的数据,也可以指向类似 int arr[n] 的数组。 int **p; p 为二级指针,指向 int * 类型的数据。 int *p[n]; p 为指针数组。[ ] 的优先级高于 ,所以应该理解为 int (p[n]); int (*p)[n]; p 为二维数组指针。 int *p(); p 是一个函数,它的返回值类型为 int *。 int (*p)(); p 是一个函数指针,指向原型为 int func() 的函数。

1) 指针变量可以进行加减运算,例如 p++p+ip-=i 。指针变量的加减运算并不是简单的加上或减去一个整数,而是跟指针指向的数据类型有关。

2) 给指针变量赋值时,要将一份数据的地址赋给它,不能直接赋给一个整数,例如 int *p = 1000; 是没有意义的,使用过程中一般会导致程序崩溃。

3) 使用指针变量之前一定要初始化,否则就不能确定指针指向哪里,如果它指向的内存没有使用权限,程序就崩溃了。对于暂时没有指向的指针,建议赋值 NULL

4) 两个指针变量可以相减。如果两个指针变量指向同一个数组中的某个元素,那么相减的结果就是两个指针之间相差的元素个数。

5) 数组也是有类型的,数组名的本意是表示一组类型相同的数据。在定义数组时,或者和 sizeof、& 运算符一起使用时数组名才表示整个数组,表达式中的数组名会被转换为一个指向数组的指针。

欢迎访问我的 博客github !


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK