对星号*
的总结
在我们目前所学到的语法中,星号*
主要有三种用途:
- 表示乘法,例如
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 = 100, b = 999, temp;
int *pa = &a, *pb = &b;
printf("a=%d, b=%d\n", a, b);
/*****开始交换*****/
temp = *pa; //将a的值先保存起来
*pa = *pb; //将b的值交给a
*pb = temp; //再将保存起来的a的值交给b
/*****结束交换*****/
printf("a=%d, b=%d\n", a, b);
return 0;
}
运行结果:
a=100, b=999
a=999, b=100
---------------------------------------------华丽分割线------------------------------------
int a = 10, *pa = &a, *paa = &a;
double b = 99.9, *pb = &b;
char c = '@', *pc = &c;
//加法运算
pa++; pb++; pc++;
//减法运算
pa -= 2; pb -= 2; pc -= 2;
pa、pb、pc 每次加 1,它们的地址分别增加 4、8、1,正好是 int、double、char 类型的长度;减 2 时,地址分别减少 8、16、2,正好是 int、double、char 类型长度的 2 倍。
---------------------------------------------华丽分割线------------------------------------
#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
第 8 行代码中我们使用了*(arr+i)
这个表达式,arr 是数组名,指向数组的第 0 个元素,表示数组首地址, arr+i 指向数组的第 i 个元素,*(arr+i) 表示取第 i 个元素的数据,它等价于 arr[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 个元素,或者说指向数组的开头。“arr 本身就是一个指针”这种表述并不准确,严格来说应该是“arr 被转换成了一个指针”。如果一个指针指向了数组,我们就称它为数组指针(Array Pointer)。
---------------------------------------------华丽分割线------------------------------------
C语言中没有特定的字符串类型,我们通常是将字符串放在一个字符数组中。
char str[] = "http://c.biancheng.net";字符数组归根结底还是一个数组
除了字符数组,C语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串:
char *str = "http://c.biancheng.net";
char *str;
str = "http://c.biancheng.net";
字符串中的所有字符在内存中是连续排列的,str 指向的是字符串的第 0 个字符;我们通常将第 0 个字符的地址称为字符串的首地址。字符串中每个字符的类型都是
char
,所以 str 的类型也必须是
char *
。
这一切看起来和字符数组是多么地相似,它们都可以使用%s
输出整个字符串,都可以使用*
或[ ]
获取单个字符,这两种表示字符串的方式是不是就没有区别了呢?
有!它们最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。
#include <stdio.h>
int main(){
char *str = "Hello World!";
str = "I love C!"; //正确
str[3] = 'P'; //错误
return 0;
}
这段代码能够正常编译和链接,但在运行时会出现
段错误(Segment Fault)
或者
写入位置错误
。
第4行代码是正确的,可以更改指针变量本身的指向;第3行代码是错误的,不能修改字符串中的字符。在编程过程中如果只涉及到对字符串的读取,那么字符数组和字符串常量都能够满足要求;如果有写入(修改)操作,那么只能使用字符数组,不能使用字符串常量。
最后我们来总结一下,C语言有两种表示字符串的方法,一种是字符数组,另一种是字符串常量,它们在内存中的存储位置不同,使得字符数组可以读取和修改,而字符串常量只能读取不能修改。
---------------------------------------------华丽分割线------------------------------------
数组是一系列数据的集合,无法通过参数将它们一次性传递到函数内部,如果希望在函数内部操作数组,必须传递数组指针
C语言为什么不允许直接传递数组的所有元素,而必须传递数组指针呢?
参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。
对于像 int、float、char 等基本类型的数据,它们占用的内存往往只有几个字节,对它们进行内存拷贝非常快速。而数组是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行内存拷贝有可能是一个漫长的过程,会严重拖慢程序的效率,为了防止技艺不佳的程序员写出低效的代码,C语言没有从语法上支持数据集合的直接赋值。
如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。
- int a =100;
- int *p1 = &a;
- int **p2 = &p1;
&
获取它的地址。C语言不限制指针的级数,每增加一级指针,在定义指针变量时就得增加一个星号
*
。p1 是一级指针,指向普通类型的数据,定义时有一个
*
;p2 是二级指针,指向一级指针 p1,定义时有两个
*
。
如果我们希望再定义一个三级指针 p3,让它指向 p2,那么可以这样写:
- int ***p3 = &p2;
- int ****p4 = &p3;
空指针 NULL
一个指针变量可以指向计算机中的任何一块内存,不管该内存有没有被分配,也不管该内存有没有使用权限,只要把地址给它,它就可以指向,C语言没有一种机制来保证指向的内存的正确性,程序员必须自己提高警惕。char *str = NULL;
NULL 是“零值、等于零”的意思,在C语言中表示空指针。从表面上理解,空指针是不指向任何数据的指针,是无效指针,程序使用它不会产生效果。stdio.h
中定义的一个宏,它的具体内容为:
#define NULL ((void *)0)
(void *)0
表示把数值 0 强制转换为
void *
类型,最外层的
( )
把宏定义的内容括起来,防止发生歧义。从整体上来看,NULL 指向了地址为 0 的内存,而不是前面说的不指向任何数据。
void 指针
void *
表示一个有效指针,它确实指向实实在在的数据,只是数据的类型尚未确定,在后续使用过程中一般要进行强制类型转换。C语言动态内存分配函数 malloc() 的返回值就是
void *
类型,在使用时要进行强制类型转换,请看下面的例子:#include <stdio.h>
int main(){
//分配可以保存30个字符的内存,并把返回的指针转换为 char *
char *str = (char *)malloc(sizeof(char) * 30);
gets(str);
printf("%s\n", str);
return 0;
}
这里重点是让大家理解
void *
,它不是空指针的意思,而是实实在在的指针,只是指针指向的内存中不知道保存的是什么类型的数据。
int a[3][3]={1, 2, 3, 4, 5, 6, 7, 8, 9};
,它的类型是int [3][3]
,长度是 4×3×3 = 36,读者可以亲自测试。
- int a = {1, 2, 3, 4, 5}, *p, i = 2;
p = a; | p = a; | p = a + i; |
*(a+i)
的形式,C语言标准也要求编译器必须具备这种行为。
C语言指针数组(每个元素都是指针)
dataType *arrayName[length];
[ ]
的优先级高于
*
,该定义形式应该理解为:
dataType *(arrayName[length]);
括号里面说明arrayName
是一个数组,包含了
length
个元素,括号外面说明每个元素的类型为
dataType *
。
int (*p)[4] = a;
括号中的*
表明 p 是一个指针,它指向一个数组,数组的类型为
int [4]
,这正是 a 所包含的每个一维数组的类型。
[ ]
的优先级高于
*
,
( )
是必须要加的,如果赤裸裸地写作
int *p[4]
,那么应该理解为
int *(p[4])
,p 就成了一个指针数组,而不是二维数组指
指针数组和二维数组指针的区别
指针数组和二维数组指针在定义时非常相似,只是括号的位置不同:- int *(p1[5]); //指针数组,可以去掉括号直接写作 int *p1[5];
- int (*p2)[5]; //二维数组指针,不能去掉括号