字符指针
顾名思义,字符指针是指向char类型的指针变量。由指针基础中我们得知,不同类型的指针变量前进的距离以及解引用的权限存在不同。字符指针+1,指针前进一个字节,借助这个特性,当我们需要逐个字节访问数据的时候,可以使用字符指针变量,简单的使用方法如下所示。
int main(){
const char* pstr = "hello world";
printf("%s\n", pstr);
return 0;
}
其中,字符串"hello world",是将其首地址存放在了pstr指针变量中,而不是将整个字符串存放在指针变量中。因此,观察如下代码。
int main(){
char str1[] = "hello world.";
char str2[] = "hello world";
const char* str3 = "hello world";
const char* str4 = "hello world";
if (str1 == str2)
printf("0\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("0\n");
return 0;
}
显而易见,由于数组的创建会开辟出不同的内存块,即使使用相同的字符串进行初始化,因此str1和str2不同。而不同的字符指针指向同一个字符串时,由于常量字符串存储到单独的一个内存区域,因此他们实际会指向同一块内存,所以str3和str4相同。
运行结果如下所示:
数组指针
与数组指针相对的是指针数组,其本质是数组。
int a[10];
// 整型数组。存放的数据类型是整型
int* a[10];
// 一级整型指针数组。存放的数据类型是int*
int** a[10];
// 二级整型指针数组。存放的数据类型是int**
数组指针,本质则是指针,用于存放数组的地址。
因为 [] 的优先级要高于 *号, 所以使用括号让*先于变量结合。
一种较为少用的使用方法如下:
int a[10] = {1,2,3,4,5,6,7,8,9,0};
int (*p)[10] = &a;
// 对比
int* pa = a;
从中不难发现一个不同,&数组名的数据类型是int (*)[10],而数组名的数据类型是int*,区别在于,数组名代表的是数组首元素地址,&数组名代表的是整个数组地址,通过如下代码可发现他们的不同之处。
int main(){
int a[10] = { 0 };
printf("a = %p\n", a);
printf("&a = %p\n", &a);
printf("a+1 = %p\n", a + 1);
printf("&a+1= %p\n", &a + 1);
return 0;
}
运行结果如下图所示:
所以数组指针,是一个指向数组地址的指针。int (*p)[10] = &a中,数组指针p指向的是一整个数组a。
数组指针常用情况是在二维数组的使用中,例如打印二维数组。
void print2(int a[3][5], int c, int r) {
for(int i = 0; i < c; i++) {
for (int j = 0; j < r; j++) {
printf("%d ", a[i][j]);
}
printf("\n");
}
printf("\n");
}
void print22(int (*p)[5], int c, int r) {
for (int i = 0; i < c; i++) {
for (int j = 0; j < r; j++) {
printf("%d ", *(*(p + i) + j));
//p+i 指向 第i行
//*(p+i)相当于拿到第i行,又相当于第i行首元素地址
}
printf("\n");
}
printf("\n");
}
int main() {
int a[3][5] = { {1,2,3,4,5,},{6,7,8,9,10},{11,12,13,14,15 } };
print2(a, 3, 5);
print22(a, 3, 5);
return 0;
}
print2() 使用数组下标对二维数组进行打印,print22() 使用 int (*p)[5] 接收二维数组a的首元素地址,也就是第一行元素的地址,通过对 (p + i) 解引用拿到第i行的元素,接着对 *(p + i) + j) 进行解引用,拿到第i行第j列的元素。
通过上面讲解,可轻松识别如下代码:
int a[5];
// a是一个数组,存放5个元素,每个元素的类型是int
int *p1[10];
// p1是一个数组,存放10个元素,每个元素的类型是int*
int (*p2)[10];
// p2是一个指向数组的指针,指向的数组有10个元素,每个元素的类型是int
int (*p3[10])[5];
// p3是一个数组,存放10个元素,每个元素的类型是int(*)[5]
数组参数和指针参数
把握首要原则:类型匹配原则
一维数组传参
如果我们要创建一个test()函数,分别传递给如下两个一维数组a1和a2作为参数,形参部分该如何写呢?
int a1[10] = { 0 };
int* a2[10] = { 0 };
test(a1);
test2(a2);
第一种方法是形参写成数组形式:
void test1(int arr[]){} // 接收a1,形参部分数组大小省略
void test12(int arr[10]){} // 接收a1,不过不建议使用
void test2(int* arr[]){} // 接收a2
形参部分数组大小可省略的一个原因在于,C语言在传递数组时,不会传递整个数组,而是传递数组的首元素地址,在一维数组中便是传递第一个元素。
第二种方法是形参写成指针形式:
void test1(int *arr){} // 接收a1
void test2(int **arr){} // 接收a2
二维数组传参
当传参对象变为二维数组时,形参同样有两种写法。
int a3[3][4] = {0};
test3(a3);
第一种方法是形参写成数组形式:
void test3(int arr[3][4]){} // 接收a3,形参部分数组大小完整
void test32(int arr[][4]){} // 接收a3,形参部分数组省略行数
void test33(int arr[][]){} // err,该方法省略列数,报错
与一维数组传参不同之处在于,形参部分的数组行可省略,列不可省略
第二种方法是形参写成指针形式:
void test3(int (*arr)[4]){} // 接收a3
void test3(int *arr){} // err
void test3(int* arr[5]){} // err
void test3(int **arr){} // err
为什么只有形参类型为 int (*arr)[4]) 才能接受成功呢?
本质是因为二维数组传参时,数组名表示的是首元素地址,也就是第一行元素的地址,该元素的数据类型是 int (*)[4] ,也就是传递了一个一维数组,因此形参的类型必须要跟实参传递的类型相匹配。
一级指针传参
与数组传参相比,指针传参相对简单,但是判断标准仍是类型是否相互匹配。
void test(int *p){} // 接收地址
如给上述形参类型为一级指针的test()函数传参,实参可写为如下几种常见形式:
int main(){
int a = 0;
int* p = &a;
int arr[10] = {0};
test(&a); // 变量的地址
test(p); // 指针变量p中存放变量a地址
test(arr); // 数组名表示数组首元素的地址
return 0;
}
二级指针传参
与一级指针类似,常见情况如下:
void test(int **pp){} // 二级指针接收一级指针的地址
int main(){
int a = 0;
int* p = &a;
int** pp = &p;
int* arr[10] = {0};
test(&p); // 一级指针的地址
test(pp); // 二级指针变量pp中存放一级指针变量p地址
test(arr); // 首元素的地址是一级指针的地址(首元素的类型是int*)
return 0;
}
总结
- 字符指针是指向char类型的指针变量,因为其前进距离和解引用权限的特殊性,通常在逐个字节访问数据的时候进行使用。
- 指针数组是存放指针的数组,数组指针是指向数组的指针,前者本质是数组,后者本质是指针。数值指针通常用于二维数组。
- 数组和指针传参,尽管有些复杂,但是最重要的地方在于,都要把握的形参和实参类型相互匹配的原则。