C语言指针介绍
入门指针
内存的地址——计算机中所有的数据都必须放在内存中,为了正确地访问这些数据,必须为每个字节都编上号码。而这些编号就是地址,也就是指针。
怎么输出一个指针呢?
#include <stdio.h>
int main()
{
int a = 100;
char str[20] = "I love C";
printf("%#X, %#X\n", &a, str); //%p也可
return 0;
}
定义并使用它!
如果一个变量(假设为c)存储了一份数据的指针,我们就称它为指针变量(假设为p)。 术语称, p 指向了 c或者说 p 是指向变量 c 的指针。
//定义普通变量
float a = 11.1, b = 22.2;
char c = '#', d = '!';
//定义指针变量
float *p1 = &a;
char *p2 = &c;
//修改指针变量的值
p1 = &b; p2 = &d;
小伙伴们有木有发现,定义它的时候需要*,而再次使用他的时候不用再加上*了!非常省劲~
指针不仅能取得地址,还能获得数据!
其实特别简单,还需要使用的时候加上*即可,像这样!
1. #include <stdio.h>
2.
3. int main()
4. {
5. int a = 15;
6. int *p = &a;
7. printf("%d, %d\n", a, *p);
8. return 0;
9. }
10.//输出的a和*p都是15
(终于加上行号了,O(∩_∩)O哈哈~)我们捋一捋哈定义加*,访问地址不加*,访问数据加*。
C语言谜题之 * 和 &
假设有一个 int 类型的变量 a,pa 是指向它的指针,那么*&a 和&*pa 分别是什么意思呢? (会考选择题哦~~)
*&a 就是a,怎么理解呢? a距离&比较近先运算,&a得到地址,根据第3条,再加上星号就是访问数据啦。
&*pa ,这次星号比较近,可以理解为&(*pa),*先取得 pa 指向的数据(等价于 a),在加上&就是地址啦。
第一个等价于 a,第二个等价于pa
指向数组的指针
进阶指针
1.数组指针
也就是指向数组的指针。大家都知道如果定义数组arr
int arr[] = { 2000, 15, 10, 11, 252 };
int *p = arr;
在以上例子中,arr、p、&arr[0]三者是等价的,他们都指向第0个元素。有同学认为arr本身就是一个指针,其实更准确来说arr被转换成了一个指针。
arr在什么时候会转换为指针呢?
- 对数组的引用arr[i]在编译时被编译器改写成*(a+i)的形式。
- 数组作为函数形参,一个数组被编译器修改成指向数组第一个元素的指针。
有了数组指针,我们就有两种方案访问数组元素
- 使用下标
也就是采用 arr[i] 的形式访问数组元素。如果 p 是指向数组 arr 的指针,那么也可以使用 p[i] 来访问数组元素, 它等价于 arr[i]。 - 使用指针
也就是使用 *(p+i) 的形式访问数组元素。另外数组名本身也是指针,也可以使用 *(arr+i) 来访问数组元素,它等价 于 *(p+i)。
关于数组指针的谜题
假设 p 是指向数组 arr 中第 n 个元素的指针,那么 *++p、(*p)++ 分别是什么意思呢?
*++p 等价于 *(++p),会先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 *(p+1),所以会 获得第 n+1 个数组元素的值。
(*p)++ 就非常简单了,会先取得第 n 个元素的值,再对该元素的值加 1。假设 p 指向第 0 个元素,并且第 0 个元素的值为 99,执行完该语句后,第 0 个元素的值就会变为 100。
2.字符串指针
字符串一共有两种表示方法:
- 字符数组
char str[] = "http://c.biancheng.net";
printf("%s\n", str);
- 指针指向字符串(本节讲解)
char *str = "http://c.biancheng.net";
printf("%s\n", str);
并且两者还有区别:它们最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其 他数据)只有读取权限,没有写入权限。 什么意思呢?请看以下代码:
1. #include <stdio.h>
2. int main(){
3. char *str = "Hello World!";
4. str = "I love C!"; //正确
5. str[3] = 'P'; //错误,常量不能读入
6. return 0;
7. }
下面是C语言数组
#include<stdio.h>
int main(){
char str[20] = "c.biancheng.net";
char *s1 = str;
char *s2 = str+2;
char c1 = str[4];
char c2 = *str;
char c3 = *(str+4);
char c4 = *str+2;
char c5 = (str+1)[5];
int num1 = *str+2;
long num2 = (long)str;
long num3 = (long)(str+2);
}
s1 | c.biancheng.net |
---|---|
s2 | biancheng.net |
c1 | a |
c2 | c |
c3 | a |
c4 | e |
c5 | c |
num1 | 101 |
num2 | 2686736 |
num3 | 2686738 |
3.指针变量作为函数参数
1. int max(int intArr[], int len){
2. int i, maxValue = intArr[0]; //假设第0个元素是最大值
3. for(i=1; i<len; i++){
4. if(maxValue < intArr[i]){
5. maxValue = intArr[i];
6. }
7. }
8. return maxValue;
9. }
10.
int intArr[]都不会创建一个数组出来,编译器也不会为它们分配内存,实际的数组是不存在的,它们最终还是会转换为 int *intArr 这样的指针。这就意味着,两种形 式都不能将数组的所有元素“一股脑”传递进来,大家还得规规矩矩使用数组指针。 因为 intArr 仅仅是一个指针,而不是真正的数组,所以必须要额外增加一个参数来传递数组长度。
4.指针作为函数返回值
#include <stdio.h>
int *func(){ int n = 100;
return &n;
}
int main(){
int *p = func(), n;
printf("c.biancheng.net\n");
n = *p;
printf("value = %d\n", n);
return 0;
}
输出结果:
c.biancheng.net
value = -2
可以看到,现在 p 指向的数据已经不是原来 n 的值了,它变成了一个毫无意义的甚至有些怪异的值。该段代码仅仅是在 *p 之前增加了一个函数调用(printf()),这一细节的不同却导致运行结果有天壤之别,究竟是为什么呢?
前面我们说函数运行结束后会销毁所有的局部数据,这个观点并没错,大部分C语言教材也都强调了这一点。但是, 这里所谓的销毁并不是将局部数据所占用的内存全部抹掉,而是程序放弃对它的使用权限,弃之不理,后面的代码可以随意使用这块内存。对于上面的两个例子,func() 运行结束后 n 的内存依然保持原样,值还是 100,如果使用及时也能够得到正确的数据,如果有其它函数被调用就会覆盖这块内存,得到的数据就失去了意义。
高级指针
1.二级指针
如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。
int a =100;
int *p1 = &a;
int **p2 = &p1; //二级指针
2.空指针NULL以及void指针
简单来说,void是实实在在的指针,NULL是空指针
你记得malloc函数吗?
char *str = (char*)malloc(sizeof(char)*30);
char *str = NULL;
动态内存分配函数malloc()的返回值就是void*类型,所以用到了强制类型转换(char *),这就是一个实实在在的例子。NULL在C语言中表示空指针,不指向任何数据。
3.指针数组
如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。
1. #include <stdio.h>
2. int main(){
3. int a = 16, b = 932, c = 100;
4. //定义一个指针数组
5. int *arr[3] = {&a, &b, &c};//也可以不指定长度,直接写作 int *parr[]
6. //定义一个指向指针数组的指针
7. int **parr = arr;
8. printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);
9. printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));.
10. return 0;
11. //结果为
12. //16,932,100
13. //16,932,100
14. }
其中,parr+i 表示第 i 个元素的地址, * (parr+i) 表示获取第 i 个元素的值(该元素是一个指针), **(parr+i) 表示获取第 i 个元素指向的数据。
4.二维数组指针
也就是指向二维数组的指针。
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 会使得指针指向数组的上一行。
*(p+1)+1 表示第 1 行第 1 个元素的地址。如何理解呢?
*(p+1)单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个 元素的地址,因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针; 就像一维数组的名字,在定义时或者和 sizeof、& 一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针。
在( * (p+1)+1)的基础上加星,就表示第 1 行第 1 个元素的值。很明显,增加一个 * 表示取地址上的数据。
5.指针数组和二维数组指针的区别
int *(p1[5]); //指针数组,可以去掉括号直接写作 int *p1[5];
int (*p2)[5]; //二维数组指针,不能去掉括号
6.函数指针
即指向函数的指针。
一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。我们可以把函数的这个首地址赋予一个指针变量,使指针变量指向函数所在的 内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。
int max(int a,int b){
//功能
}
int main(){
int (*pmax)(int a,int b) = max;//定义函数指针
maxval = (*pmax)(x,y);//调用
}
总结
int *p; | p 可以指向 int 类型的数据,也可以指向类似 int arr[n] 的数组。 |
---|---|
int **p; | p 为二级指针,指向 int * 类型的数据。 |
int (*p)(); | p 是一个函数指针,指向原型为 int func() 的函数。 |
int (*p)[n]; | p 为二维数组指针。 |
int *p[n]; | p 为指针数组。[ ] 的优先级高于 *,所以应该理解为 int *(p[n]); |
int *p(); | p 是一个函数,它的返回值类型为 int *。 |