小引
在指针的学习阶段,有不少同学都十分畏惧这个物什,甚至“谈指针色变”。其实对指针的不理解,其实本质上是对内存的不理解,本篇博客就从零开始、系统的来讲解指针这个话题。
首先我们应该明确以下的一些基础常识:
- 指针是一个变量,用来存放地址,地址唯一标识一块内存地址。
- 指针的大小是固定的
4 或 8
个字节。(32
或64
位平台) - 指针是有类型的,指针的类型决定了指针加减整数运算的步长,以及指针解引用时的权限。
字符指针
顾名思义,就是指向字符变量的指针,指针内存放的内容是一个字符。它的一般写法如下:
int main(){
char ch = 'w';
char *ptr = &ch;
*ptr = 'w';
return 0;
}
如果此时对
ptr
进行strlen
,就会内存访问越界,属于未定义行为,结果不可预期。因为指针指向的是一个字符而不是字符串。
指向单个字符的使用方式简单明了,但是相对于下面的这种常见的指向字符串的使用方式,就相形见绌了。
int main(){
char *ptr_1 = "this is a C string";
printf("%s\n",ptr_1);
return 0;
}
这样也可以间接的表现为指针指向了字符串
。
代码中char *ptr_1 = "this is a C string";
语句看似是将字符串放入了指针内,但其本质是将这个字符串的首地址存放到了指针内。
如果此时对
ptr_1
进行strlen
,就会正确的打印出字符串的个数,因为此时指针的指向不再是单个字符,而是字符串。
其实编译器无法识别
指针的指向是字符数组 还是字符串,所以它的指向需要使用者来定义保证。
请看代码:
int main(){
char str1[] = "This is a C string.";
char str2[] = "This is a C string.";
char *str3 = "This is a C string.";
char *str4 = "This is a C string.";
if(str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if(str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
运行结果为:
- 这里
str3
和str4
指针指向的是同一个常量字符串。C/C++
语法上会把常量字符串存储到单独的一个内存区域,当几个不同的指针同时指向同一个字符串时,他们实际会指向同一块内存(这个字符串的首地址)。所以这几个指针其实是同一个指针。 - 但是用相同的常量字符串对不同的数组进行初始化时就会在内存中开辟出不同的区域。指向其各自首地址的指针也就是不同的指针了。
所以str1和str2不同,str3和str4不同。
数组指针
它的本质是指针,指针的指向是一个 数组
。它的定义如下:
int (*arr)[20];
因为
[]
的优先级高于*
,而()
的优先级高于[]
。所以加上圆括号()
改变优先级顺序。(不加圆括号的情况下面会提及)
先进行括号( )
内的操作,这里变量名arr
与*
结合就明确了他是一个指针变量
,括号操作符完成再与其他元素进行结合,它指向了一个大小为10个整型元素的数组。
【二维数组也是一个数组指针】
- 对于
arr
和&arr
:
arr
是数组名,数组名表示数组首元素的地址
&arr
就是数组指针,指针的指向是整个数组。
请看如下代码:
int main(){
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
运行结果为:
二者输出内容相同,这就说明数组名
和& 数组名
是等价的吗?其实不然,请看如下代码:
int main(){
int arr[10] = { 0 };
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
printf("arr+1 = %p\n", arr+1);
printf("&arr+1 = %p\n", &arr+1);
return 0;
}
运行结果为:
这里对数组名 + 1
和& 数组名 + 1
操作发现二者出现了区别,说明它们有着本质上的差别。
arr
指向数组首元素,&arr
是数组指针,指向数组,二者指向的地址是相同的但类型不同。
arr + 1
是指向数组首元素之后一个元素的指针。
而&arr + 1
是指向下一个数组,跨过了整个数组,步距为数组元素的大小之和。
与数组指针相混淆的有另一个名词:指针数组
。
指针数组
int *p1[10];
它的本质是数组,数组中的所有元素都是 指针
。
【二级指针也是一个指针数组】
int *arr1[10]; //整型指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5]; //二级字符指针的数组
具体举例:
const char *arr[] = {
"hehe",
"haha",
"xixi"
};
arr
数组中的三个字符串也就是三个字符数组,数组可以隐式转换为指针,所以就可以使得arr
的类型设为char *
的字符指针类型,所以arr
中三个元素的类型都是char *
。- 加上
const
关键字修饰本数组是为了确保数组内内容不发生改变。同时因为是三个字符串常量,其本身也不会发生改变。
这样就定义好了一个指针数组。
一维数组传参
1. 整型数组
对于数组:
int arr[10] = { 0 };
使用以下函数进行传参:
// 情况 1
void test(int arr[])
正确
编译器会把函数的参数名arr
识别为*arr
的指针,与其数组大小无关,所以数组大小的参数省略书写也是完全可以的。
// 情况 2
void test(int arr[10])
正确
这是最直观的传参方式,实参和形参变量的类型、大小都相同,很容易理解和书写,是非常常见的一种写法。
// 情况 3
void test(int *arr)
正确
和情况1同义,编译器会把传入函数的数组参数隐式转化为指针*arr
,数组元素也会转换成数组的移动,所以这种写法也是可以的。
所以以上三种方法都是正确的,内涵也是相同的,读者使用时可以根据具体情况选择不同的表现形式,提高代码可读性。
2. 指针数组
对于数组:
int *arr2[20] = { 0 };
使用以下函数进行传参:
// 情况一
void test2(int *arr[20])
正确
传参方式直观,实参与形参参数类型、大小均相同,便于理解。
// 情况二
void test2(int **arr)
正确
在这段代码中,可以把int*
看作一个整体,将它typedef
成为一个类型t
那么实参中的数组定义就可以看成t arr2[20];
了,调用函数传参就可以看成t *arr
,就与上面所讲的一维整型数组传参如出一辙了。这样传参也是正确的。
所以指针数组可以当做二级指针来传参。
二维数组传参
对于数组:
int arr[3][5] = {0};
使用以下函数进行传参:
// 情况 1
void test(int arr[3][5])
正确
同样是最直观的写法,类型大小均相同,不多赘述。
// 情况 2
void test(int arr[][])
- [×]
不正确
因为对于一个二维数组,两个[]
中第一个数是行数
,第二个数列数
,行数可以目前不知道,但是每一行有多少元素,即列数,必须要知道,否则给定一串元素就不知道如何划分行列数,这样才会方便运算。
// 情况 3
void test(int arr[][5])
正确
二维数组传参第一个数字可以省略,如若给定一部分数据,就可以根据列数计算出行数,这些工作都是自动完成的,所以C语言规定二维数组行数可以省略,但列数不可省。
【小结】:
二维数组传参,函数形参只能省略第一个[]
的数字,第二个[ ]中的数字不可省略。
对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
// 情况一
void test(int (*arr)[5])
正确
这样的写法是一个数组指针,可以正确传参。
而除此之外,以下的其他写法都是错误的:
// 情况二
void test(int* arr[5])
- [×]
不正确
这样的写法是一个指针数组,而二维数组的传参应该是一个数组指针,错误。
// 情况三
void test(int *arr)
- [×]
不正确
作为一级指针传入数组会造成实参与形参类型不符,错误。
// 情况四
void test(int **arr)
- [×]
不正确
和第二种情况是等效的,正是因为指针数组可以当做二级指针来传参,两者可以互相转化,错误。
【小结】:二维数组可以作为 数组指针 传参。
一级指针传参
常见的传参方式如下:
void print(int *p, int sz){
int i = 0;
for(i = 0;i < sz;i++){
printf("%d\n",*(p + i));
}
}
int main(){
int arr[10] = {1,2,3,4,5,6,7,8,9};
int *p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
print(p,sz); //一级指针p,传给函数print
return 0;
}
Q:那么当一个函数的参数部分为一级指针的时候(例如int *
),函数能接收什么参数?
int*
类型的一级整型指针- 数组元素类型为
int
的一维数组
二级指针传参
常见的传参方式如下:
void test(int** ptr){
printf("num = %d\n", **ptr);
}
int main(){
int n = 10;
int *p = &n;
int **pp = &p;
test(pp); //二级指针pp传给函数test
test(&p);
return 0;
}
Q:当函数的参数是二级指针时(例如int **
),可以接收什么参数?
- 二级指针
- 指针数组