目录
一、指针变量的定义和使用
1.定义指针变量
2.通过指针变量取得数据
3.关于*和&
二、指针变量运算(加法、减法、比较运算)
三、数组指针(指向数组的指针)
假设 p 是指向数组 arr 中第 n 个元素的指针,那么 *p++、*++p、(*p)++ 分别是什么意思呢?
四、字符串指针(指向字符串的指针)
字符数组&&字符串常量
五、数组灵活多变的访问形式
六、指针变量作为函数参数
七、指针作为函数返回值
八、二级指针(指向指针的指针)
九、空指针NULL与void指针
1.空指针NULL
2.void 指针
十、数组≠指针
1.数组在什么时候会转换为指针
2.关于数组和指针可交换性的总结
十一、C语言指针数组(每个元素都是指针)
十二、函数指针(指向函数的指针)
十三、总结
所谓指针,也就是内存的地址;所谓指针变量,也就是保存了内存地址的变量。
需要注意的是,虽然变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符,但在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址。
数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量。
在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。指针变量的值就是某份数据的地址,这样的一份数据可以是数组、字符串、函数,也可以是另外一个普通变量或者指针变量。
假设有一个char类型的变量c,它存储了字符'K',并占用了地址为0x11A的内存(地址通常用十六进制表示)。另外有一个指针变量p,它的值为0x11A,正好等于变量c的地址,这种情况我们就称p指向了c,或者说p是指向变量c的指针。
定义指针变量与定义普通变量非常类似,不过要在变量名前面加星号*
,格式为:
datatype *name; 或者 datatype *name = value;
其中,*表示这是一个指针变量,datatype表示该指针变量所指向的数据的类型。
*
是一个特殊符号,表明一个变量是指针变量,定义指针变量时必须带*
。而给指针变量赋值时,因为已经知道了它是一个指针变量,就没必要多此一举再带上*
,后边可以像使用普通变量一样来使用指针变量。也就是说,定义指针变量时必须带*
,给指针变量赋值时不能带*
。
int *p1;//p1 是一个指向 int 类型数据的指针变量
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 赋值时,因为已经知道了它是一个指针变量,就没必要多此一举再带上*,后边可以像使用普通变量一样来使用指针变量。也就是说,定义指针变量时必须带*,给指针变量赋值时不能带*。
注:需要强调的是,p1、p2 的类型分别是float*和char*,而不是float和char,它们是完全不同的数据类型,要引起注意。
指针变量也可以连续定义:
int *a, *b, *c; //a、b、c 的类型都是 int*
注意每个变量前面都要带*。如果写成下面的形式,那么只有 a 是指针变量,b、c 都是类型为 int 的普通变量:
int *a, b, c;
指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:
*pointer; 其中,*称为指针运算符,用来取得某个地址上的数据,
*
在不同的场景下有不同的作用:*
可以用在指针变量的定义中,表明这是一个指针变量,以和普通变量区分开;使用指针变量时在前面加*
表示获取指针指向的数据,或者说表示的是指针指向的数据本身。
//通过指针变量取得数据
#include
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,它们的指向关系如下图所示:
程序被编译和链接后,a、p 被替换成相应的地址。使用 *p 的话,要先通过地址 0XF0A0 取得变量 p 本身的值,这个值是变量 a 的地址,然后再通过这个值取得变量 a 的数据,前后共有两次运算;而使用 a 的话,可以通过地址 0X1000 直接取得它的数据,只需要一步运算。也就是说,使用指针是间接获取数据,使用变量名是直接获取数据,前者比后者的代价要高。
//指针修改内存上的数据
#include
int main(){
int a = 15, b = 99, c = 222;
int *p = &a;//定义指针变量
*p = b;//通过指针变量修改内存上的数据
c = *p;//通过指针变量获取内存上的数据
printf("%d, %d, %d, %d\n",a,b,c,*p);
return 0;
}
//运行结果:15,15
//解析:p指向a,这时*p值为15,*p=b,内存上的值变为99,此时a中的值也变为99。最后c=*p,c中的值也变为99
定义指针变量时的 * 和使用指针变量时的 * 区别:
int *p = &a;//*用在指针变量的定义中,表明这是一个指针变量,以和普通变量区分开;
*p = 100;//在指针变量前面加*表示获取指针指向的数据
需要注意的是,给指针变量本身赋值时不能加*,如:
int *p;
p = &a;//如果写成*p = &a,报错!!!
*p = 100
//通过指针交换两个变量的值
#include
int main(){
int a = 100, b = 999, temp;
int *pa = &a, *pb = &b;
printf("a=%d,b=%d\n", a, b);
//开始交换
temp = *pa;
*pa = *pb;
*pb = temp;
//结束交换
printf("a=%d,b=%d\n", a, b);
return 0;
}
//运行结果:
a=100,b=999
a=999,b=100
int a;
int *p = &a;
*&a表示什么?*&a可以理解为*(&a),&a表示取变量a的地址(即p),*(&a)表示取这个地址上的数据(即*p),绕来绕去,*&a仍然等于a。
&*p表示什么?&*p可以理解为&(*p),*p表示取得p指向的数据(即a),&(*p)表示数据的地址(即&a),所以等于p。
星号*
主要有三种用途:
①表示乘法,例如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
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=0X98FB00,&b=0X98FAD8,&c=0X98FAC3
pa=0X98FB00,pb=0X98FAD8,pc=0X98FAC3
pa=0X98FB04,pb=0X98FAE0,pc=0X98FAC4
pa=0X98FAFC,pb=0X98FAD0,pc=0X98FAC2
-858993460
数组(Array)是一系列具有相同类型的数据的集合,每一份数据叫做一个数组元素(Element)。数组中的所有元素在内存中是连续排列的,整个数组占用的是一块内存。以int arr[] = { 99, 15, 100, 888, 252 };
为例,该数组在内存中的分布如下图所示:
定义数组时,要给出数组名和数组长度,数组名可以认为是一个指针,它指向数组的第 0 个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址。以上面的数组为例,下图是 arr 的指向:
数组名的本意是表示整个数组,也就是表示多份数据的集合,但在使用过程中经常会转换为指向数组第 0 个元素的指针,所以上面使用了“认为”一词,表示数组名和数组首地址并不总是等价。初学者可以暂时忽略这个细节,把数组名当做指向第 0 个元素的指针使用即可。
如果一个指针指向了数组,我们就称它为数组指针(Array Pointer),定义一个指向数组的指针例子如下:
int arr[] = {23, 56, 4, 34, 45 };
int *p = arr;
arr 本身就是一个指针,可以直接赋值给指针变量 p。arr 是数组第 0 个元素的地址,所以int *p = arr;也可以写作int *p = &arr[0];。也就是说,arr、p、&arr[0] 这三种写法都是等价的,它们都指向数组第 0 个元素,或者说指向数组的开头。
数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关,上面的例子中,p 指向的数组元素是 int 类型,所以 p 的类型必须也是int *
。
//①使用指针的方式遍历数组元素
#include
int main(){
int arr[] = { 23, 56, 4, 34, 45 };
int len = sizeof(arr) / sizeof(int);//求数组长度,sizeof(arr) 会获得整个数组所占用的字节数,sizeof(int) 会获得一个数组元素所占用的字节数,它们相除的结果就是数组包含的元素个数,也即数组长度。
int i;
for (i = 0; i < len; i++){
printf("%d ", *(arr + i));//*(arr+i)等价于arr[i],arr 是数组名,指向数组的第 0 个元素,表示数组首地址, arr+i 指向数组的第 i 个元素,*(arr+i) 表示取第 i 个元素的数据,它等价于 arr[i]。
}
printf("\n");
return 0;
}
//运行结果:23 56 4 34 45
//②使用数组指针来遍历数组元素
#include
int main(){
int arr[] = { 23, 56, 4, 34, 45 };
int i, *p = arr, len = sizeof(arr) / sizeof(int);
//求数组的长度时不能使用sizeof(p) / sizeof(int),因为 p 只是一个指向 int 类型的指针,编译器并不知道它指向的到底是一个整数还是一系列整数(数组),所以 sizeof(p) 求得的是 p 这个指针变量本身所占用的字节数,而不是整个数组占用的字节数。
for (i = 0; i < len; i++){
printf("%d ", *(p + i));
}
printf("\n");
return 0;
}
//运行结果:23 56 4 34 45
//③让 p 指向数组中的第三个元素
#include
int main(){
int arr[] = { 23, 56, 4, 34, 45 };
int *p = &arr[2];//也可以写做int *p = arr +2,这里指向第三个元素
printf("%d,%d,%d\n",*(p-2),*p,*(p+2));
return 0;
}
//运行结果:23,4,45
引入数组指针后,我们就有两种方案来访问数组元素了,一种是使用下标,另外一种是使用指针。
(1) 使用下标
也就是采用 arr[i] 的形式访问数组元素。如果 p 是指向数组 arr 的指针,那么也可以使用 p[i] 来访问数组元素,它等价于 arr[i]。
(2) 使用指针
也就是使用 *(p+i) 的形式访问数组元素。另外数组名本身也是指针,也可以使用 *(arr+i) 来访问数组元素,它等价于 *(p+i)。
不管是数组名还是数组指针,都可以使用上面的两种方式来访问数组元素。不同的是,数组名是常量,它的值不能改变,而数组指针是变量(除非特别指明它是常量),它的值可以任意改变。也就是说,数组名只能指向数组的开头,而数组指针可以先指向数组开头,再指向其他元素。
//*p++
#include
int main(){
int arr[] = { 23, 56, 4, 34, 45 };
int i, *p = arr, len = sizeof(arr) / sizeof(int);
for (i = 0; i < len; i++){
printf("%d ", *p++);
}
printf("\n");
return 0;
}
//运行结果:23 56 4 34 45
//解析:*p++ 应该理解为 *(p++),每次循环都会改变 p 的值(p++ 使得 p 自身的值增加),以使 p 指向下一个数组元素。该语句不能写为 *arr++,因为 arr 是常量,而 arr++ 会改变它的值,这显然是错误的。
//*++p
#include
int main(){
int arr[] = { 23, 56, 4, 34, 45 };
int i, *p = arr, len = sizeof(arr) / sizeof(int);
for (i = 0; i < len; i++){
printf("%d ", *++p);
}
printf("\n");
return 0;
}
//运行结果:56 4 34 45 -858993460
//解析:*++p 等价于 *(++p),会先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 *(p+1),所以会获得第 n+1 个数组元素的值。最后一个值为随机值。
//(*p)++
#include
int main(){
int arr[] = { 23, 56, 4, 34, 45 };
int i, *p = arr, len = sizeof(arr) / sizeof(int);
for (i = 0; i < len; i++){
printf("%d ", (*p)++);
}
printf("\n");
return 0;
}
//运行结果:23 24 25 26 27
//解析:(*p)++ 就非常简单了,会先取得第1个元素的值,再对该元素的值加 1。这里第1个元素的值为 23,执行完该循环语句后,第 1个元素的值依次会变为 24 25 26 27。
C语言没有字符串类型,通常是将字符串放在一个字符数组中。使用指针的方式输出字符串:
#include
#include
int main(){
char str[] = "www.baidu.com";
char *pstr = str;
int len = strlen(str), i;
//使用*(pstr+i)
for (i = 0; i < len; i++){
printf("%c", *(pstr + i));
}
printf("\n");
//使用pstr[i]
for (i = 0; i < len; i++){
printf("%c", pstr[i]);
}
printf("\n");
//使用*(str+i)
for (i = 0; i < len; i++){
printf("%c",*(str+i));
}
printf("\n");
return 0;
}
//运行结果:
www.baidu.com
www.baidu.com
www.baidu.com
除了字符数组,C语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串,例如:
char *str = "www.baidu.com";
或者
char *str;
str = "www.baidu.com";
//字符串中的所有字符在内存中是连续排列的,str 指向的是字符串的第 0 个字符;我们通常将第 0 个字符的地址称为字符串的首地址。字符串中每个字符的类型都是char,所以 str 的类型也必须是char *。
//输出字符串:使用%s输出整个字符串,使用*或[ ]获取单个字符
#include
#include
int main(){
char *str = "www.baidu.com";
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;
}
//运行结果:
www.baidu.com
www.baidu.com
www.baidu.com
在编程中如果只涉及到对字符串的读取,那么字符数组和字符串常量都能满足需求;如果有写入(修改)操作,那么只能使用字符数组,不能使用字符串常量。
获取用户输入的字符串就是一个典型的写入操作,只能使用字符数组,不能使用字符串常量,请看下面的代码:
#include
int main(){
char str[30];
gets(str);
printf("%s\n", str);
return 0;
}
//运行结果:
www.baidu.com
www.baidu.com
总结:C语言有两种表示字符串的方法,一种是字符数组,另一种是字符串常量,它们在内存中的存储位置不同,使得字符数组可以读取和修改,而字符串常量只能读取不能修改。
#include
int main(){
char str[20] = "www.baidu.com";
char *s1 = str;
char *s2 = str + 4;
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);
printf("s1=%s\n", s1);
printf("s2=%s\n", s2);
printf("c1=%c\n", c1);
printf("c2=%c\n", c2);
printf("c3=%c\n", c3);
printf("c4=%c\n", c4);
printf("c5=%c\n", c5);
printf("num1=%d\n", num1);
printf("num2=%ld\n", num2);
printf("num3=%ld\n", num3);
return 0;
}
//运行结果:
s1=www.baidu.com
s2=baidu.com
c1=b
c2=w
c3=b
c4=y
c5=i
num1=121
num2=17824436
num3=17824438
//解析:
(1) str既是数组名称,也是一个指向字符的指针;指针可以参加运算,指针加1相当于数组下标加1。
printf() 输出字符串时,要求给定一个起始地址,并从这个地址开始输出,直到遇见NUL(\0)停止。s1 为字符串str第1个字符的地址,s2 为第4个字符的地址,所以 printf() 的结果分别为 www.baidu.com 和 baidu.com。
(2) 数组元素的访问形式可以看做 address[offset],address 为起始地址,offset 为偏移量,所以:
c1 = str[4] 表示以地址 str 为起点,向后偏移4个字符,为b;
str表示第0个字符的地址,*str表示第0个字符,即c2=w;因为指针可以参加运算,所以 str+4 表示第4个字符的地址,c3 = *(str+4) 表示第4个字符,即b。
字符与整数运算时,先转换为整数(字符对应的ASCII码)。对于c4,*str为字符w(同c2),w对应的ASCII为119,所以c4=119+2=121,转化为字符对应为y。
c5 = (str+1)[5] 表示以地址 str+1 为起点,向后偏移5个字符,等价于str[6],为i。
(3) 字符与整数运算时,先转换为整数(字符对应的ASCII码)。对于 num1,*str+2 == w+2 == 119+2 == 121,即num1为121;num2和num3分别为字符串str的首地址和第2个元素的地址。
在C语言中,函数的参数不仅可以是整数、小数、字符等具体的数据,还可以是指向它们的指针。用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。
//使用指针变量作参数交换两个变量的值
#include
void swap(int *p1, int *p2){
int temp;//临时变量
temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main(){
int a = 10, b = 20;
swap(&a, &b);
printf("a=%d,b=%d\n",a,b);
return 0;
}
//运行效果:a=20,b=10
//解析:调用 swap() 函数时,将变量 a、b 的地址分别赋值给 p1、p2,这样 *p1、*p2 代表的就是变量 a、b 本身,交换 *p1、*p2 的值也就是交换 a、b 的值。函数运行结束后虽然会将 p1、p2 销毁,但它对外部 a、b 造成的影响是“持久化”的,不会随着函数的结束而“恢复原样”。
C语言允许函数的返回值是一个指针(地址),我们将这样的函数称为指针函数。
//定义函数 strlong(),用来返回两个字符串中较长的一个
#include
#include
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;
}
//运行结果:
www.baidu.com
www.ali.com
Longer string:www.baidu.com
用指针作为函数返回值时需要注意的一点是,函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误。
指针可以指向一份普通类型的数据,例如 int、double、char 等,也可以指向一份指针类型的数据,例如 int *、double *、char * 等。如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。
假设有一个 int 类型的变量 a,p1是指向 a 的指针变量,p2 又是指向 p1 的指针变量,它们的关系如下图所示:
指针变量也是一种变量,也会占用存储空间,也可以使用 &
获取它的地址。C语言不限制指针的级数,每增加一级指针,在定义指针变量时就得增加一个星号 *
。p1 是一级指针,指向普通类型的数据,定义时有一个 *
;p2 是二级指针,指向一级指针 p1,定义时有两个*
。将这种关系转换为C语言代码:
int a =100;
int *p1 = &a;
int **p2 = &p1;
//如果我们希望再定义一个三级指针 p3,让它指向 p2,那么可以这样写:
int ***p3 = &p2;
//四级指针也是类似的道理:
int ****p4 = &p3;
想要获取指针指向的数据时,一级指针加一个*
,二级指针加两个*
,三级指针加三个*
,以此类推,请看代码:
#include
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=0X75FB70,p3=0X75FB70
&p1=0X75FB7C,p2=0X75FB7C,*p3=0X75FB7C
&a=0X75FB88,p1=0X75FB88,*p2=0X75FB88,**p3=0X75FB88
//解析:以三级指针 p3 为例来分析上面的代码。***p3等价于*(*(*p3))。*p3 得到的是 p2 的值,也即 p1 的地址;*(*p3) 得到的是 p1 的值,也即 a 的地址;经过三次“取值”操作后,*(*(*p3)) 得到的才是 a 的值。
假设 a、p1、p2、p3 的地址分别是 0X00A0、0X1000、0X2000、0X3000,它们之间的关系可以用下图来描述(方框里面是变量本身的值,方框下面是变量的地址。):
一个指针变量可以指向计算机中的任何一块内存,不管该内存有没有被分配,也不管该内存有没有使用权限,只要把地址给它,它就可以指向,C 语言没有一种机制来保证指向的内存的正确性,程序员必须自己提高警惕。
很多初学者会在无意间对没有初始化的指针进行操作,这是非常危险的,请看下面的例子:
#include
int main(){
char *str;
gets(str);
printf("%s\n", str);
return 0;
}
这段程序没有语法错误,但在Windows下编译连接会有警告,没有对指针进行初始化!我们知道,未初始化的局部变量的值是不确定的,C 语言并没有对此作出规定,不同的编译器有不同的实现,大家不要直接使用未初始化的局部变量。上面的代码中, str 就是一个未初始化的局部变量,它的值是不确定的,究竟指向哪块内存也是未知的,大多数情况下这块内存没有被分配或者没有读写权限,使用 gets() 函数向它里面写入数据显然是错误的。
C语言中可以对没有初始化的指针赋值为 NULL ,例如:char *str = NULL;NULL 是 “ 零值、等于零 ” 的意思,在 C 语言中表示空指针。从表面上理解,空指针是不指向任何数据的指针,是无效指针,程序使用它不会产生效果。注意区分大小写,null 没有任何特殊含义,只是一个普通的标识符。
//给str赋值NULL
#include
int main(){
char *str = NULL;
gets(str);
printf("%s\n", str);
return 0;
}
//貌似也会报错
注意,C 语言没有规定 NULL 的指向,只是大部分标准库约定成俗地将 NULL 指向 0 ,所以不要将 NULL 和 0 等同起来,例如这种写法是不专业的:int *p = 0; 而应该坚持写为:int *p = NULL;
注意 NULL 和 NUL 的区别:NULL 表示空指针,是一个宏定义,可以在代码中直接使用。而 NUL 表示字符串的结束标志 ’\0’ ,它是 ASCII 码表中的第 0 个字符。 NUL 没有在C语言中定义,仅仅是对 ’\0’ 的称呼,不能在代码中直接使用。
void 用在函数定义中可以表示函数没有返回值或者没有形式参数,用在这里表示指针指向的数据的类型是未知的。也就是说, void * 表示一个有效指针,它确实指向实实在在的数据,只是数据的类型尚未确定,在后续使用过程中一般要进行强制类型转换。C 语言动态内存分配函数 malloc() 的返回值就是 void * 类型,在使用时要进行强制类型转换。
#include
#include
int main(){
//分配可以保存30个字符的内存,并把返回的指针转换为char *
char *str = (char *)malloc(sizeof(char) * 30);
gets(str);
printf("%s\n",str);
return 0;
}
//运行结果:
www.baidu.com
www.baidu.com
注意:void * ,它不是空指针的意思,而是实实在在的指针,只是指针指向的内存中不知道保存的是什么类型的数据。
数组和指针不等价,数组是另外一种类型。数组和指针不等价的一个典型案例就是求数组的长度,这个时候只能使用数组名,不能使用数组指针:
#include
int main(){
int a[6] = { 0, 1, 2, 3, 4, 5 };
int *p = a;
int len_a = sizeof(a) / sizeof(int);
int len_p = sizeof(p) / sizeof(int);
printf("len_a=%d,len_p=%d\n",len_a,len_p);
return 0;
}
//运行结果:len_a=6,len_p=1
数组是一系列数据的集合,没有开始和结束标志,p 仅仅是一个指向 int 类型的指针,编译器不知道它指向的是一个整数还是一堆整数,对 p 使用 sizeof 求得的是指针变量本身的长度。也就是说,编译器并没有把 p 和数组关联起来,p 仅仅是一个指针变量,不管它指向哪里,sizeof 求得的永远是它本身所占用的字节数。
数组名的本意是表示一组数据的集合,它和普通变量一样,都用来指代一块内存,但在使用过程中,数组名有时候会转换为指向数据集合的指针(地址),而不是表示数据集合本身,这在前面的例子中已经被多次证实。
数据集合包含了多份数据,直接使用一个集合没有明确的含义,将数组名转换为指向数组的指针后,可以很容易地访问其中的任何一份数据,使用时的语义更加明确。
C语言标准规定,当数组名作为数组定义的标识符(也就是定义或声明数组时)、sizeof 或 & 的操作数时,它才表示整个数组本身,在其他的表达式中,数组名会被转换为指向第 0 个元素的指针(地址)。
(1) 用 a[i] 这样的形式对数组进行访问总是会被编译器改写成(或者说解释为)像 *(a+i) 这样的指针形式。
(2) 指针始终是指针,它绝不可以改写成数组。你可以用下标形式访问指针,一般都是指针作为函数参数时,而且你知道实际传递给函数的是一个数组。
(3) 在特定的环境中,也就是数组作为函数形参,也只有这种情况,一个数组可以看做是一个指针。作为函数形参的数组始终会被编译器修改成指向数组第一个元素的指针。
(4) 当希望向函数传递数组时,可以把函数参数定义为数组形式(可以指定长度也可以不指定长度),也可以定义为指针。不管哪种形式,在函数内部都要作为指针变量对待。
注意区别:
数组指针:指的是指针指向的是一个数组;
指针数组:表示的是数组的每个元素都是指针。
如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。指针数组的定义形式一般为:
dataType *arrayName[length]; 其中,[ ]的优先级高于*,该定义形式应该理解为:
dataType *(arrayName[length]); 括号里面说明arrayName是一个数组,包含了length个元素,括号外面说明每个元素的类型为dataType *。
除了每个元素的数据类型不同,指针数组和普通数组在其他方面都是一样的,下面是一个简单的例子:
include
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
//解析:
1. arr 是一个指针数组,它包含了 3 个元素,每个元素都是一个指针,在定义 arr 的同时,我们使用变量 a、b、c 的地址对它进行了初始化,这和普通数组是多么地类似。
2. parr 是指向数组 arr 的指针,确切地说是指向 arr 第 0 个元素的指针,它的定义形式应该理解为int (*parr),括号中的表示 parr 是一个指针,括号外面的int 表示 parr 指向的数据的类型。arr 第 0 个元素的类型为 int ,所以在定义 parr 时要加两个 *。
3. 第一个 printf() 语句中,arr[i] 表示获取第 i 个元素的值,该元素是一个指针,还需要在前面增加一个 * 才能取得它指向的数据,也即 *arr[i] 的形式。
4. 第二个 printf() 语句中,parr+i 表示第 i 个元素的地址,(parr+i) 表示获取第 i 个元素的值(该元素是一个指针),*(parr+i) 表示获取第 i 个元素指向的数据。
指针数组还可以和字符串数组结合使用,请看下面的例子:
#include
int main(){
char *str[3] = {
"google.com",
"hello world",
"C Language"
};
printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
return 0;
}
//运行结果:
google.com
hello world
C Language
//需要注意的是,字符数组 str 中存放的是字符串的首地址,不是字符串本身,字符串本身位于其他的内存区域,和字符数组是分开的。也只有当指针数组中每个元素的类型都是char *时,才能像上面那样给指针数组赋值,其他类型不行。
//为了便于理解,可以将上面的字符串数组改成下面的形式,它们都是等价的。
#include
int main(){
char *str0 = "google.com";
char *str1 = "hello world";
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;
}
//运行结果:
google.com
hello world
C Language
指针数组和二维数组指针的区别
指针数组和二维数组指针在定义时非常相似,只是括号的位置不同:
int *(p1[5]); //指针数组,可以去掉括号直接写作 int *p1[5];
int (*p2)[5]; //二维数组指针,不能去掉括号
一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。
函数指针的定义形式为:
returnType (*pointerName)(param list);
其中,returnType 为函数返回值类型,pointerNmae 为指针名称,param list 为函数参数列表。参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称,这一点和函数原型非常类似。
注意( )的优先级高于*,第一个括号不能省略,如果写作returnType *pointerName(param list);就成了函数原型,它表明函数的返回值类型为returnType *。
//用指针来实现对函数的调用
#include
//返回两个数中较大的一个
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;
}
//运行结果:
Input two numbers:10 30
Max value: 30
//解析:第 13 行代码对函数进行了调用。pmax 是一个函数指针,在前面加 * 就表示对它指向的函数进行调用。注意( )的优先级高于*,第一个括号不能省略。
指针(Pointer)就是内存的地址,C语言允许用一个变量来存放指针,这种变量称为指针变量。指针变量可以存放基本类型数据的地址,也可以存放数组、函数以及其他指针变量的地址。
程序在运行过程中需要的是数据和指令的地址,变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符:在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址;程序被编译和链接后,这些名字都会消失,取而代之的是它们对应的地址。
(1) 指针变量可以进行加减运算,例如p++
、p+i
、p-=i
。指针变量的加减运算并不是简单的加上或减去一个整数,而是跟指针指向的数据类型有关。
(2) 给指针变量赋值时,要将一份数据的地址赋给它,不能直接赋给一个整数,例如int *p = 1000;
是没有意义的,使用过程中一般会导致程序崩溃。
(3) 使用指针变量之前一定要初始化,否则就不能确定指针指向哪里,如果它指向的内存没有使用权限,程序就崩溃了。对于暂时没有指向的指针,建议赋值NULL
。
(4) 两个指针变量可以相减。如果两个指针变量指向同一个数组中的某个元素,那么相减的结果就是两个指针之间相差的元素个数。
(5) 数组也是有类型的,数组名的本意是表示一组类型相同的数据。在定义数组时,或者和 sizeof、& 运算符一起使用时数组名才表示整个数组,表达式中的数组名会被转换为一个指向数组的指针。
参考文献:C语言中文网