1. 指针运算
1.1 算术运算
- 加减
+
、-
指针与整数相加:表示指针指向下个变量。
指针与整数相减:表示指针指向上个变量。
指针与指针相减:两个指针的元素间隔个数。
int arr[]={100,101,102,103,104,105};
int* p = arr;
int* q;
for(int i=0;i<5;++i){
q = p+i;
printf("%d\n",*q);
}
for(int i=0;i<5;++i){
p = q-i;
printf("%d\n",*p);
}
printf("q-p=%d\n",q-p);
- 自增自减
++
、--
指针能够算术运算,必然能够自增自减。
int arr[]={1,2,3,4,5};
int* p = arr;
for(int i=0;i<5;++i){
printf("%d\n",*p++);
}
for(int i=0;i<5;++i){
printf("%d\n",*p--);
}
*p++
/*p--
操作说明
- 操作数是指针
- 自增自减
++
、--
优先级高于解引用*
计算过程
- 运算
++
/--
,返回的是p
的值(地址),然后p
自加/自减。 - 运算
*
,获取p
指向的值。
等价于
*p;
p=p+1;
自增自减 | 相当于 |
---|---|
*p++ |
*(p++) |
*p-- |
*(p--) |
*++p
/*--p
操作说明
- 操作数是指针
- 前缀自增自减
++
、--
和解引用*
的结合律是自右向左。 - 前缀自增自减
++
、--
在解引用*
的右边,优先计算。
计算过程
- 运算
++
/--
,p
自加/自减,返回的是p
自加/自减后的值(地址)。 - 运算
*
,获取p
指向的值。
自增自减 | 相当于 |
---|---|
*++p |
*(++p) |
*--p |
*(--p) |
++*p
/--*p
操作说明
-
*
操作数是指针,前缀自增自减++
、--
操作数是指针指向的值。 - 前缀自增自减
++
、--
和解引用*
的结合律是自右向左。 - 解引用
*
在前缀自增自减++
、--
的右边,优先计算。
计算过程
- 运算
*
,获取p
指向的值。 - 运算
++
/--
,p
指向的值自加/自减。
自增自减 | 相当于 |
---|---|
++*p |
++(*p) |
--*p |
--(*p) |
如果一个表达式里有多个运算符,则先进行优先级比较,先执行优先级高的运算符;如果优先级相同,那就看结合性,根据结合方向来做运算。
- 问题
指针与指针可以相加吗?
在不同数组中可以执行上面的操作吗?
试一下编译下面程序
#include <stdio.h>
int main () {
int arr[]={100,101,102,103,104,105};
for(i=0;i<5;++i){
printf("%p\n",arr++);
}
return 0;
}
- 分析
分析下面三段代码
代码一
#include <stdio.h>
int main () {
int arr[] = {1,2,3,4,5};
int* p = arr;
printf("*p++ = %d\n",*p++);
printf("p index = %d\n",p - arr);
for(int i=0;i<5;++i){
printf("%d ",arr[i]);
}
printf("\n");
return 0;
}
代码二
#include <stdio.h>
int main () {
int arr[] = {1,2,3,4,5};
int* p = arr;
printf("*++p = %d\n",*++p);
printf("p index = %d\n",p - arr);
for(int i=0;i<5;++i){
printf("%d ",arr[i]);
}
printf("\n");
return 0;
}
代码三
#include <stdio.h>
int main () {
int arr[] = {1,2,3,4,5};
int* p = arr;
printf("++*p = %d\n",++*p);
printf("p index = %d\n",p - arr);
for(int i=0;i<5;++i){
printf("%d ",arr[i]);
}
printf("\n");
return 0;
}
++
/--
一定会改变变量的值,前两种改变指针的值,后一种改变元素的值。
字符串的简化遍历
while(*p){
printf("%c\n",*p++)
}
1.2 比较运算符
==
、!=
、<
、<=
、>
、>=
本质是比较内存中的地址。
#include <stdio.h>
int main () {
int arr[]={100,101,102,103,104,105};
int* p = arr;
for(int i=0;i<5;++i){
printf("%p\n",p++);
}
return 0;
}
数组中的元素地址线性递增。
1.3 单位长度
从上面可以看到,指针的加1减1,地址并非加1减1。
int iarr[] = {1,2,3,4,5,6};
int* p = iarr;
for(int i=0;i<5;++i){
printf("%p\n",p++);
}
char carr[] = {1,2,3,4,5,6};
char* q=carr;
for(int i=0;i<5;++i){
printf("%p\n",p++);
}
- 应用范围
指针的算术运算表示在一片连续空间上的移动。
指针的比较运算也是用于一片连续空间的地址比较。
常用于数组等连续内存。 - 数组遍历的
while
写法
int arr[] = {1,2,3,4,5};
int* p = arr;
while(p<arr+5){
printf("%d ",*p++);
}
2. 指针类型
- 无论指向什么类型,所有指针的大小都是一样的,都是地址的大小。
char* str;
short* ps;
int* pn;
long* pl;
long long* pll;
float* pf;
double* pd;
long double* pld;
- 指针类型转换
指向不同类型的指针不能直接相互赋值(特例void*
),需要强制类型转换。
char* str = "abcd";
int* p = str;
指针类型转换没有改变指针内的地址,也没有改变指针指向的值,只是改变了移动的单位长度。
#include <stdio.h>
int main(){
char* str = "abcdef";
int* p=(int*)str;
p++;
char* q = (char*)p;
printf("%c\n",*q);
}
-
void
类型的指针
void*
是一种很特别的指针,表示指向未知类型的指针,并不指定它是指向哪一种类型的数据,而是根据需要转换为所需数据类型。
int n = 0;
int* p = &n;
void* q = p;
int* k = (int*) q;
指针小结
1. 指针(pointer)是什么?
指针是存放地址的变量,为什么不直接称作地址变量?
No. | 比较项 | C语言指针 | 钟表指针 |
---|---|---|---|
1 | 指针类型 |
char* int*
|
时针、分针、秒针 |
2 | 指针单位长度/刻度 |
±1 移动字节数 |
度过的时间 |
2. 指针作用
- 函数需要多个返回值时,作为返回值参数。
- 传入数组后,对数组做操作。
- 较大数据结构体传入时做参数。
- 动态申请内存。
- 避免使用未初始化指针、空指针和野指针。
3. 二维指针
3.1 二维指针 vs 一维指针
指针是变量,变量有地址,指针也有地址。(三段论)
int n=10;
int *p = &n; // *p是指向int变量的指针
int* *pp = &p; // **p是指向int指针的指针
printf("&p:%p p:%p *p:%d",&p,p,*p);
printf("pp:%p *pp:%p **pp:%d",pp,*pp,**pp);
3.2 数组指针 vs 指针数组
3.2.1 数组指针
指向一个数组指针称为数组指针。
int n = 0;
int* p = &n;
int arr[] = {1,2,3,4,5,6};
int* q = arr;
3.2.2 指针数组
指针是一个类型,也可以组成一个数组,这样的数组称为指针数组。指针数组里面存放的是地址,指针数组是多个指针变量集合。
#include <stdio.h>
int main(){
int a = 1;
int b = 2;
int c = 3;
int* p[] = {&a,&b,&c};
for(int i=0;i<3;++i){
printf("%d\n",*p[i]);
}
for(int i=0;i<3;++i){
printf("%d\n",**(p+i));
}
}
[]
的优先级高于*
,那么p
先和[]
结合,说明这是一个数组。再和int*
结合,说明这个数组里的每个元素都是一个指针,每个元素都能保存一个地址。
3.3 二维数组 vs 指针数组
int arr[][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
for(int i=0;i<3;++i){
for(int j=0;j<4;++j){
printf("%d ",arr[i][j]);
}
printf("\n");
}
int* parr[] = {arr[0],arr[1],arr[2]};
for(int i=0;i<3;++i){
for(int j=0;j<4;++j){
printf("%d ",parr[i][j]);
}
printf("\n");
}
指针数组与二维数组在遍历元素上是相同的。但是存在本质区别。arr[0],arr[1],arr[2]
是数组名,parr[0],parr[1],parr[2]
是指针,所以只是形式上的一直,内存表示上是不同。
数组名与指针
- 相同点:访问数组元素方式。
- 不同点:数组名是值,指针是变量。
可以通过取地址&
和sizeof()
的方式查看数组名与指针。
变量名字代表里面存放的值。数组名表示的是一组变量的公用名。
3.4 二维指针 vs 二维数组
int arr[] = {1,2,3,4,5};
int *p = arr; // OK
int* parr[] = {arr,arr+1,arr+2,arr+3,arr+4,arr+5};
int* *q = parr; // OK
int arr[][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
int** p = arr; // Warning
int* parr[] = {arr[0],arr[1],arr[2]};
int** q = arr; // OK
- 指针数组数组名可以直接赋值给二维指针。
- 二维数组数组名不可以直接赋值给二维指针。
- 二维指针操作与指针数组可以认为完全一致;二维数组操作与指针数组部分一致(访问元素上一致;但是指针数组内存放地址可以修改,二维数组数组名表示地址不能修改);
4. 常量指针 vs 指针常量
4.1 常量指针const int *p
可以写作int const *p
,p
是int*
类型,const
修饰的是*p
,所以*p
是常量,表示p
指向的地址里的值不可修改,也就是说,*p
里的值不能再重新赋值了,但是可以修改p
指向的地址。
int a = 10;
int b = 20;
const int *p = &a;
p = &b; // 可以
*p = 100; // 错误
4.2 指针常量int * const p
p
是int*
类型,那么const
修饰的是p
,所以p
是常量,表示p
指向的地址不可修改,即p
不能再指向别的地方了,但是可以修改p
指向的这个地址里的值。
int a = 10;
int b = 20;
int * const p = &a;
p = &b; // 错误
*p = 100; // 允许
4.3 常量指针常量const int * const p
p
是int*
类型,两个const
分别修饰了p
和*p
, 所以p
和*p
都是常量,表示p
指向的地址不可修改,同时p
指向的地址里的值也不可修改。
int a = 10;
int b = 20;
const int *const p = &a;
p = &b; // 错误
*p = 100; // 错误
自由的代价,是永远的警惕。-- C Primer Plus
你定义了一个指针,那就一定要知道这个指针指向的什么地方,而且你要保证这个指针是真实有效的,否则我就用程序崩溃来惩罚你。
No. | 例子 | 名称 | 指向的值 | 地址 |
---|---|---|---|---|
1 |
const int *p /int const *p
|
常量指针 | 不可改变 | 可改变 |
2 | int* const p |
指针常量 | 可改变 | 不可改变 |
3 | const int * const p |
常量指针常量 | 不可改变 | 不可改变 |
*
之前的const
修饰指向的变量,*
之后的const
修饰指针。
问题
- 常量指针和指针常量在什么地方使用?
- 下面的
q
是什么类型指针?
const int *p,*q;
int const *p,*q;
int* const p,*q;
const int * const p,*q;
0地址
#include <stdio.h>
int main(){
int *p = 0;
printf("%d\n",*p);
}
0
地址是内存中不能访问的地址。在C语言中,标准库定义NULL
表示0
地址。
通常用来表示如下:
- 指针没有初始化
- 返回指针无效
补充
1. 字符串数组与字符串指针数组
从一维到二维
1 字符串数组
字符串数组,可以看成二维字符数组,只是初始化可以使用字符串方式。
char arr[12][10] = {"January","February","March","April",
"May","June","July","August",
"September","October","November","December"};
简化
char arr[][10] = {"January","February","March","April",
"May","June","July","August",
"September","October","November","December"};
2 字符串指针数组
char* arr[12] = {"January","February","March","April",
"May","June","July","August",
"September","October","November","December"};
简化
char* arr[] = {"January","February","March","April",
"May","June","July","August",
"September","October","November","December"};
3 字符串数组与字符串指针数组的区别
- 大小的区别
char arr1[][10] = {"January","February","March","April",
"May","June","July","August",
"September","October","November","December"};
char* arr2[] = {"January","February","March","April",
"May","June","July","August",
"September","October","November","December"};
printf("sizeof(arr1)=%d\n",sizeof(arr1));
printf("sizeof(arr2)=%d\n",sizeof(arr2));
- 二维指针的区别
#include <stdio.h>
int main () {
char arr1[][10] = {"January","February","March","April",
"May","June","July","August",
"September","October","November","December"};
char* arr2[] = {"January","February","March","April",
"May","June","July","August",
"September","October","November","December"};
char** p1 = arr1;
for(int i=0;i<12;++i){
printf("%s\n",p1[i]);
}
char** p2 = arr2;
for(int i=0;i<12;++i){
printf("%s\n",p2[i]);
}
return 0;
}
2. main()
参数与返回值
1 main()
参数
main()
的完全形式是这样的。
int main(int argc,char* argv[])
或者
int main(int argc,char** argv)
其中,argc
是命令与参数数量。argv
是命令和参数组成的字符串数组。argv[0]
是命令本身。其余是参数。
int main(int argc,char* argv[]){
for(int i=0;i<argc;++i){
printf("%d:%s",i,argv[i]);
}
return 0;
}
2 main()
返回值
main()
返回值是与调用程序交互的,返回程序执行状态。通常0
表示执行成功,非零表示执行失败。
int main(){
int n;
printf("请输入一个整数:");
scanf("%d",&n);
return n;
}
在终端执行程序后,接着执行echo $?
可以看到返回值。
在标准库内定义两个常量EXIT_SUCCESS
和EXIT_FAILURE
表示成功与失败。