7.8 KiB
一、数组与指针间的转换
- 数组与指针混合运算时
- 在 C 程序中,除了指针可以进行加减法之外,数组也经常参与其中,但是 C 语言不认为数组是基本数据类型,因此没有像“指针运算”那样发明“数组运算”。而我们又确确实实可以写出数组参与运算的代码。
int a[3] = {100, 200, 300};
int *p=a;
int *q;
q = p + 1;
p = a + 1; //a移动一步长,步长与元素类型相关
-
要点:
-
任何数组的名字 a,除了在其定义语句和 sizeof 语句之外,均代表其首元素a[0]的地址。
-
数组 a 的首元素是 a[0],因此其中的 p = a + 1 实际上等价于 p = &a[0] + 1,而又由于 a[0]的数据类型是 int 型,因此地址&a[0]的类型就是 int *。
- 指针与下标运算符相互作用时
- 当指针指向的目标是一片连续的内存空间时
int array[5] = {1,2,3,4,5};
int *p = array; // 指针p与数组名字array所代表的地址相同
array[1]; // 编译器一遇到这条语句马上将之转化为其本来面目:*(array+1)
*(p+1); // p[1]
// 结论如下
// array[x] == *(array+x);
// *(p+x) == p[x];
- 结论:在使用数组名字访问数据元素是数组会自动转换为指针形式访问,当指针指向一片连续内存空间时指针取目标就可以使用指针名加下标,如:p[x];
二、指针数组
- 数组元素为指针的数组称为指针数组
char *p1,*p2,*p3;
char *str[3] = {p1,p2,p3}; // 表示定义了一个数组其元素为3个指针并初始化。
- 常用与在数组中保存多个字符串。
三、数组指针
- 指向数组地址的指针称为数组指针
char str[3];
char *p = str; // 指针指向str数组的基地址
char (*q)[3]; // 表示定义了一个指针,可以指向一个长度为3的char型数组地址
q = &str;
char str1[2][3];
char (*k)[3] = str1;// 指针指向str1数组的基地址
- 常用在二维数组传参中
#include <stdio.h>
void fun(int *p, int (*q)[], int **k)
{
}
int main(int argc, char *argv[])
{
int array[2][3];
int array1[2][5];
int arr[2];
int *ptr;
fun(arr,array,&ptr); // array == &array[0] array[0] == &array[0][0]
fun(arr,array1,&ptr);
return 0;
}
练习
- 定义指针指向下列的目标并将代码补充完整
int main(int argc, char *argv[])
{
int a[3] = {1,2,3};
int b[2][5]= {1,2,3};
char *s[5]= {"hahaha","xixi","ooo"};
p1 = a;
printf("%d\n", p1);
p2 = b;
printf("%d\n", p2);
p3 = s;
printf("%s\n", p3);
p4 = &b[0];
printf("%d\n", p4);
p5 = &b[1][3];
printf("%d\n", p5);
int p6 = &a;
printf("%d\n", p6);
int p7] = &b;
printf("%d\n", p7);
p8 = &s;
return 0;
}
四、数组、指针与函数
数组参数
-
核心语法:当数组在函数中被当做参数传递时,系统会将其自动转化为指针,具体而言,会将其转化为一个指向数组首元素的指针。
-
示例:
int a[3] = {100,200,300};
void f1(a); // &a[0]; int *
-
说明:
-
数组a作为参数传给函数f1()。
-
实参a在传递进函数后,系统随即将其转化为一个指向a[0]的指针,即一个 int* 指针,指向首元素100。
-
此时,若定义函数f1(),则可以有如下两种写法,它们是完全等价的:
// 写法一:
void f1(int a[3]) // 或可以写成 void f1(int (a[3]) )
{
...
}
// 写法二:
void f1(int *a) // 或可以写成 void f1(int (*a) )
{
...
}
数组与指针表示字符串时的区别
数组是一片连续的内存,这片内存中保存的数据是字符串,可以对这个片内存的数据进行修改。
指针是一个变量,只存储数据字符串的地址,而字符串数据本身是常量,不能通过指针对目标进行修改,但是可以改变指针指向的目标。
char *p = "hello"; // 局部变量;"hello"存放在.rodata段中;指针p指向的目标在.rodata中
char str1[] = "hello";// 局部变量;"hello"存放在.rodata段中;str是栈空间的一片连续内存存放的是"hello"
printf("p: %s\n", p);
printf("str: %s\n", str1);
str1 = "abcd"; // 错误,将“abcd”字符串赋值给数组str1时需要一个一个的进行。
str1[0] = 'a'; // 栈内存可以进行读写访问
printf("str: %s\n", str1);
p = "abc"; // 可以修改p的指向
*p = 'a'; // 不能对常量区(.rodata)内存进行写访问
printf("p: %s\n", p);
复杂数组参数1
以上示例的数组可以为任意类型的数组,比如:
int b[3][4]; // &b[0] int [4]
int f2(b);
此时,二维数组b被当做参数传给了函数f2(),逻辑跟一位数组完全一样,唯一的不同只是数组b的首元素不再是普通的int,而是int [4],函数f2()的定义也可以有两种写法:
// 写法一:
void f2(int b[3][4]) // 或可以写成 void f2(int (b[3]) [4] )
{
...
}
// 写法二:
void f2(int (*b)[4]) // 此处小圆括号(*b)不能省略
{
...
}
此例与上述数组a和函数f1()完全等同,看不出来的同学注意将数组b中的元素类型 int [4] 视为一个整体,等同于数组a中的元素类型 int。
复杂数组参数2
继续讲数组参数进行变形,比如:
char *c[3]; // &c[0] char *
int f3(c);
此时,数组c被当做参数传给了函数f3(),逻辑跟之前的两个例子完全一样,唯一的不同只是数组c的首元素是 char *,函数f3()的定义也可以有两种写法:
// 写法一:
void f3(char *c[3])// 或可以写成 void f3(char * (c[3]) )
{
...
}
// 写法二:
void f3(char **c) // 或可以写成 void f3(char * (*c) )
{
...
}
总结
-
任何数组成为参数被传递时,都一律会被转化为一个指针,一个指向其首元素的指针,系统这么做是因为要提高数据传递的效率,但这同时给编程开发者提了个醒 —— 与普通按值传递不同,数组传的都是地址,形参都可以直接访问实参。
-
数组作为函数参数与返回值时实际是传递数组的基地址与返回数组的基地址,此时这个数组实际是一个指针,如下示例代码中my_strcat与my_strcat1的返回值均是一个数组,参数也是数组此时实际传递的是一个指针。
/* 将字符串str2追加到str1的末尾,返回追加后的地址 */
char *my_strcat(char str1[], char str2[])
{
int i;
for (i = 0; str1[i] != '\0'; i++)
{
}
for (int j = 0; str2[j] != '\0'; j++)
{
str1[i+j] = str2[j];
}
return str1;
}
char *my_strcat1(char *str1, char *str2)
{
int i;
for (i = 0; str1[i] != '\0'; i++)
{
}
for (int j = 0; str2[j] != '\0'; j++)
{
str1[i+j] = str2[j];
}
return str1;
}
五、复杂声明
-
分析复杂声明的步骤
-
从左至右找第一个非关键字标识符
-
以这个标识符为中心,逐个与()、[]、*结合
-
找到被小括号括起来的部分(从左到右原则)
-
跟后缀操作符集合((), [])结合
-
()后缀表示是一个函数,分析其返回值及参数类型
-
[]后缀表示是一个数组,分析其元素类型
-
跟前缀(*)结合
-
*前缀表示是一个指针,分析指向的目标类型
-
示例
char *(*fun)(int);
char *(*fun[10])(int);
int *(*fun(int))[3];
char *(*fun(char *(*p)(char *)))[2](int);
