本文是本人在学习 C语言的过程中所积累的对 C语言指针的感悟,可能会有些地方描述不准确,还请指出。本文遵循一般文章结构,从简单到难,从基本概念到抽象总结。适合任何任何学习 C语言的人群。
我们提供的服务有:成都网站设计、做网站、微信公众号开发、网站优化、网站认证、来宾ssl等。为超过千家企事业单位解决了网站和推广的问题。提供周到的售前咨询和贴心的售后服务,是有科学管理、有技术的来宾网站制作公司一、指针的概念指针的值就是某一个变量的内存地址,指针变量就是用来存放某个变量的内存地址的变量,和广义的变量没有什么区别。
在同一CPU构架下,不同类型的指针变量所占用的存储单元长度是相同的。这是因为操作系统的位数与其所能支持的大内存有直接的关系。由于计算机是按照字节寻址的,如在 32 位操作系统下,32位比特位一共能描述 2^32 个状态,一个状态标记大小为 1B(一般定义8位(bit,比特)为一字节),所以一共有 2^32*1B = 4GB。因此 32 位系统所能支持的大内存为 4GB。而对于 64 位操作系统(目前主流操作系统),所能支持的大内存为 2^64*1B = 17179869184GB,这是一个很大的数,基本上可以支持任何现实中任意存在的内存。
而指针最小就是以字节为单位进行指引,因此对于 32 位操作系统,它也要是 32 位的,即 4 字节大小;而对于 64 位操作系统,它就得是 64 位 即 8 字节大小。
我们可以写一个程序验证下:
#includeint main() {int a = 4;
int* p = &a;
printf("%zu\n", sizeof(p));
// 指针变量占据 8 个字节
printf("%p\n", p);
// 变量 a 存放在以内存地址为0x16f85f71c开头的内存单元中
}
输出结果为:
8
0x16f85f71c
在 C/C++语言中,指针一般被认为是指针变量,指针变量的内容存储的是其指向的对象的首地址,指向的对象可以是变量(指针变量也是变量),数组,函数等占据存储空间的实体。
二、指针详解以下的例子由易到难,每次增长一颗星。
(一)★#includeint main() {int a = 0, b = 1;
int *p1 = &a, *p2 = &b;
printf("%p\n%p\n", p1, p2);
return 0;
}
输出为:
0x16bacb718
0x16bacb714
这是一个很简单的例子,程序中创建了两个局部变量,然后利用指针输出它们的地址。其中&
是取地址符,取出来以后赋给两个指针变量p1、p2,并将其打印出来。
细心的话可以发现这两个地址由大到小,相差为 4。这说明栈中的这两个变量恰好相邻,且一个int
类型的变量占用 4B大小空间。而且还说明栈空间的增长方向是由大到小的,当然我们的主题是指针,这里不再赘述。
#includeint main() {int a = 0;
int* p = &a;
printf("通过指针修改前 a 的值为:%d\n", a);
*p = 10;
printf("通过指针修改后 a 的值为:%d\n", a);
return 0;
}
输出为:
通过指针修改前 a 的值为:0
通过指针修改后 a 的值为:10
这里就比上面稍微多了一点东西,就是可以通过指针去修改被该指针所指的变量的值。换句话说,就是间接修改变量的值。其中*p
的含义为解引用,指代的就是变量a
。
#includeint main() {int a = 0, b = 1;
int *p1 = &a, *p2 = &b;
printf("a在%p\n", p1);
printf("b在%p\n", p2);
int **pp1 = &p1, **pp2 = &p2;
printf("a的指针p1在%p\n", pp1);
printf("a的指针p1在%p\n", pp2);
return 0;
}
输出为:
a在0x16da73718
b在0x16da73714
a的指针p1在0x16da73708
a的指针p1在0x16da73700
可以看到,指针变量也有一个内存地址,它指向的是一个指针变量的内存地址。那么,我们同样可以通过二级指针对一级指针作出修改:
#includeint main() {int a = 0, b = 1;
int *p1 = &a, *p2 = &b;
printf("a在%p\n", p1);
printf("b在%p\n", p2);
int **pp1 = &p1, **pp2 = &p2;
printf("a的指针p1在%p\n", pp1);
printf("b的指针p2在%p\n", pp2);
// 交换两个二级指针
int** temp = pp1;
pp1 = pp2;
pp2 = temp;
printf("a的指针p1在%p\n", pp1);
printf("b的指针p2在%p\n", pp2);
return 0;
}
输出为:
a在0x16fa0b718
b在0x16fa0b714
a的指针p1在0x16fa0b708
b的指针p2在0x16fa0b700
a的指针p1在0x16fa0b700
b的指针p2在0x16fa0b708
这里需要提一点,就是当一个变量(前提是一个变量)为表达式左值时,它代表一个变量,当其为右值时,它代表变量的值。关于左值右值的概念,不再赘述。
交换前的指针关系:
交换后的指针关系:
我们还可以尝试通过pp2
来直接修改变量a
的值:
#includeint main() {int a = 0, b = 1;
int *p1 = &a, *p2 = &b;
printf("a在%p\n", p1);
printf("b在%p\n", p2);
int **pp1 = &p1, **pp2 = &p2;
printf("a的指针p1在%p\n", pp1);
printf("a的指针p1在%p\n", pp2);
// 交换两个二级指针
int** temp = pp1;
pp1 = pp2;
pp2 = temp;
printf("a的指针p1在%p\n", pp1);
printf("a的指针p1在%p\n", pp2);
**pp2 = 10;
printf("修改后a的值为:%d\n", a);
return 0;
}
输出为:
a在0x16b1ff718
b在0x16b1ff714
a的指针p1在0x16b1ff708
a的指针p1在0x16b1ff700
a的指针p1在0x16b1ff700
a的指针p1在0x16b1ff708
修改后a的值为:10
其中二级指针pp2
解引用了两次,才与a
等价。这里的例子大量使用了二级指针,即int**
类型的变量。具体来说,指针类型有多种,比如int* p
,int** p
,int*** p
等等。
前面几节都是很简单的概念,接下来几节将会引入数组、字符串等来对指针的使用进行更深的阐述。
(1)关于数组名#includeint main() {int a[3] = {1, 2, 3};
printf("%p\n", a);
printf("%zu\n", sizeof(a));
}
输出为:
0x16fc9b708
12
可以看到,数组名 a 是一个指针类型的值,它实际上指向的是大小为 12B 的连续空间的首地址。数组名其实是一个常量,因此它不能当做左值。当我们修改它的值时,它会提示array type 'int [3]' is not assignable
,意思是这个变量不可被修改。另外,a 的大小为 12B 这表明和普通的指针并不一样,或者a并不是简单的指针。因此,很多参考书上说“数组名本质上就是一个指针”的说法是完全错误的。
我们刚提到,数组名是一个常量,因此我们可以利用一个指针变量来访问内存单元:
#includeint main() {int a[3] = {1, 2, 3};
int* p = a;
for (int i = 0; i< 3; i++) {printf("%d ", *(p + i));
}
return 0;
}
输出为:
1 2 3
其中p+i
的含义是,指针指向了p
所指的内存再向右偏移i
个类型为int
的内存单元。之所以是int
,是因为我们声明的是int
类型的指针。比如:
#includeint main() {int a[3] = {1, 2, 3};
int* p = a;
printf("%p\n", p);
printf("%p\n", p + 1);
printf("%p\n", p + 2);
long *q = a;
printf("%p\n", q);
printf("%p\n", q + 1);
return 0;
}
输出为:
0x16f1c3708
0x16f1c370c
0x16f1c3710
0x16f1c3708
0x16f1c3710
可以看到,因为long
类型占 8 个字节(long
的定义为不少于int
类型的大小,有些计算机系统long
和int
等价,但大部分long
类型都占 8 字节),int
占 4 个字节,所以long
类型的指针偏移的长度为int
类型的两倍。同样,我们可以搞点稍微复杂的事情:
#includeint main() {int a[3] = {1, 2, 3};
printf("数组的首地址为:%p\n", a);
int* p = (int*)(&a + 1);
int* q = a + 3;
printf("此时 p 指向:%p\n", p);
printf("此时 q 指向:%p\n", q);
return 0;
}
输出为:
数组的首地址为:0x16b25f708
此时 p 指向:0x16b25f714
此时 q 指向:0x16b25f714
这说明,&a + 1
的偏移量为3个 int 类型大小。这里就需要格外注意,指针偏移一个单位是参考哪一种类型的变量指针的。这里参考的是int a[3]
,也就是说,a的数据类型为int[3]
,偏移一次当然偏移 12B了,因为int[3]
的大小就是 12B!
我们就可以很简单地预见以下的程序输出:
#includeint main() {int a[3] = {1, 2, 3};
int* p = (int*)(&a + 1);
printf("%d\n", *(p - 1));
return 0;
}
输出为:
3
因为p
指向的是数组最后一个元素的下一个内存单元,又因为p
是int
类型的指针,因此一个偏移量大小为int
,所以p-1
之后,就指向了数组的最后一个元素。
字符串是一个重点,也是一个难点。但是只要掌握指针基本概念,把握字符数组和字符串的区别和联系,也是送分题。
首先得了解下字符串输出的基本原理:
#includeint main() {char c[3] = {'a', 'b', 'c'};
printf("%s\n", c);
return 0;
}
输出为:
abc*/
为什么会有这么奇怪的输出呢?这是因为printf("%s\n", c)
输出字符串时,只要输入一个指针(任何类型的指针都可以),就会一直打印,直到遇到'\0'
为止。比如下面这个程序:
#includeint main() {int a[3] = {100, 101, 102};
printf("%s\n", a);
return 0;
}
输出结果为:
d
这是因为数据存放为大端方式,100
存在到第一个高地址字节空间后,后面三个存放的都是'\0'
,所以就停止打印了。我们可以印证一下:
假设我们想要打印出"abc"
,这就要保证内存单元里面放的是97\98\99\00
。即0x61626300
。大端方式存入内存单元就为:0x00636261
,这个数字 10 进制大小为6513249
,因此:
#includeint main() {int a[3] = {6513249,100,200};
printf("%s\n", a);
return 0;
}
程序输出为:
abc
果然输出了"abc"
,这无疑是一件令人激动的事情!当然,这里的重点是字符串输出,就不再赘述其它了。
经过上面的例子,可以充分地说明输出函数的特性,即只要是一个指针,丢给printf("%s\n", a)
后,就会打印出结果。
所以字符串是使用空字符'\0'
结尾的一组数据,就这么简单。我们可以很容易地构造出一些字符串,比如我们上面通过一些手段构造的"abc"
,以及下面用字符数组构造的字符串(常用手段):
#includeint main() {char string[12] = {'H','e','l','l','o','w','o','r','l','d','!','\0'};
printf("%s\n",string);
char *string1 = "Helloworld!";
printf("%s\n",string1);
return 0;
}
在用字符数组构造字符串时,一定要注意在最后一个内存单元加上’\0’,因为我们不能保证字符串结束后的下一个内存单元放的是不是’\0’,因此字符串可能不会正常终止。
还可以使用字面值常量来创建字符串,比如char *string1 = "Helloworld!"
。这种不需要在在最后一个内存单元加上'\0'
,编译系统会自动加。
为了明白字符串赋值的本质,这里不会使用库函数提供的各种函数,比如strcpy()
,strcat()
等。
#includeint main() {char string[12] = {'H', 'e', 'l', 'l', 'o', 'w',
'o', 'r', 'l', 'd', '!', '\0'};
char string1[12] = {0}; // 初始化与否都可以,因为紧接着我们就要对其赋值
for (int i = 0; i< 12; i++) {string1[i] = string[i];
}
printf("%s\n", string1);
return 0;
}
本例采用了逐个字符赋值的方法来完成对字符串的赋值。我们的主题是指针,那么可不可以像下面那样赋值:
#includeint main() {char string[12] = {'H', 'e', 'l', 'l', 'o', 'w',
'o', 'r', 'l', 'd', '!', '\0'};
char string1[12] = {0}; // 初始化与否都可以,因为紧接着我们就要对其赋值
string1 = string;
printf("%s\n", string1);
return 0;
}
当然不可以,前文已经提到,数组名只一个地址常量,既然是常量当然就不可以被修改,自然就不能当做左值。事实上,上文还提到了通过字面值常量来创造字符串,既然是常量,那么自然也就不能修改,比如:
#includeint main() {char* p = "Helloworld!";
*(p + 1) = 'E';
printf("%s\n", p);
return 0;
}
这个程序的目的是把"Helloworld"改为"HElloworld",那么目的能达到吗?自然不能,编译器会报错bus error
。我们不去管这个错误的具体含义,只需要知道常量字符串是无法修改的。也就是说,我们的指针并不是指在哪里就改哪里,那计算机系统就乱套了!
如果我们迫切的想要修改,可以这样做:
#include#include#includeint main() {char* p = "Helloworld!";
int len = strlen(p);
char* newString = (char*)malloc(sizeof(char) * (len + 1));
for (int i = 0; i< len; i++) {newString[i] = p[i];
}
newString[len] = '\0';
newString[1] = 'E';
printf("%s\n", newString);
return 0;
}
输出结果为:
HElloworld!
过程也很简单,就是申请一块内存空间,先复制过来,然后再修改成想要的样子。
(五)★★★★★开始之前,我们先来谈谈什么是匿名数组。顾名思义,匿名就是藏起来名字的意思。比如:
#includeint main() {int* p = (int[2]){19, 20};
return 0;
}
我们定义了一个匿名的数组,并赋给一个int*
类型的变量 p。匿名类在 Java 中 的作用之一是起到很好的封装性,同样在 C语言中也有这样的作用。这个数组,只能通过 p 访问。你可能注意到了,我并没有用int
类型的指针去描述 p,而是用int*
类型来描述 p。
C语言中的数据类型有哪些?一般来说,有四大类型,分别是基本类型,构造类型,空类型以及我们讲的指针类型。
指针类型是花样最多的一种类型,它通常包括 5 类:int*
,char*
,int**
,int(*)[]
,int*[]
。
换句话说,从语法的角度讲,把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。比如,给出一个例子:
#includeint main() {int a[3] = {1, 2, 3};
int* p;
char* s;
int** q;
int(*pt)[3];
int*[3];
return 0;
}
在这个例子中,指针的类型分别为:int*
,char*
,int**
,int(*)[3]
,int*[3]
。我们经常用到数组指针以及指针数组,因此重点理解它们就是关键。
对于指针数组,描述它的语法为int*p[3]
。首先,它是一个数组,所以得是个p[3]
;其次要是一个指针类型的也就是int*
。所以就产生了int*p[3]
。数组 p 中每一个元素都是一个指针,指向一个一维数组。比如:
#includeint main() {int a[3] = {1,2,3};
int b[3] = {4,5,6};
int c[5] = {7,8,9,10,11};
int *p[3] = {a,b,c};
return 0;
}
为了更明显,不如把它写成int*(p[3])
,但是由于[3]
的结合度非常高,因此这个括号可以去掉。
顾名思义,数组指针就是一个指向数组的指针。我们这样定义:·int(*p)[3]·。首先得是一个指针,因此得是(int*p)
;然后得是一个数组类型的,因此就是(int*p)[3]
。但这样明显歧义了,因此就成了int(*p)[3]
。它指向一个类型为int[3]
的变量。比如:
#includeint main() {int a[3] = {1,2,3};
int (*p)[3] = &a;
}
a 的数据类型为int[3]
,p 的类型为int(*)[3]
。要想让 p指向 a,我们只需要把变量 a 的地址赋值给 p,即int (*p)[3] = &a
。为了更明显,我们可以这样修改一下程序:
#includeint main() {int a[3] = {1,2,3};
int b[4] = {1,2,3,4};
int (*p)[3];
p = &a;
p = &b;
}
可以看到这样的报错:incompatible pointer types assigning to 'int (*)[3]' from 'int (*)[4]' [-Wincompatible-pointer-types]
。意思是从“int (*)[4]”[-Wincompatible-pointer-types] 分配给“int (*)[3]”的不兼容指针类型
。这也从侧面反映了int(*)[3]
是一种确定的指针类类型。
能看到这里,想必你已经功力深厚,返璞归真了,哈哈~
三、总结这里只总结难点:
int*p[3]
,这里的重点是 p 的数据类型为int*[3]
,数组每一个元素都是一个指针,指向int 类型的数组
。int(*p)[3]
,这里的重点是 p 的数据类型为int(*)[3]
,p 是一个指针,指向类型为 int[3]的变量
。全文完,感谢你的阅读。
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧