大家好我是小张同学,今天继续来学习指针。
目录
数组和指针的纠葛与生俱来,也是面试必问题目,下面就来看一下吧。
1. 数组指针、字符串指针、二维数组指针
1.1 定义一个数组指针
前面说过,指针变量存放的是地址,它可以存放普通变量的地址,可以存放另一个指针变量的地址,当然也可以存放数组、结构体、函数的地址。
如果一个指针指向了数组,就称它为数组指针,比如下面的代码就定义了一个指针 p 指向数组 arr:、
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
在这里发现,定义一个元素类型为 int 的数组指针和定义一个指向int变量的指针的写法是一样的!
数组指针是指向数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关,在上面的例子中,arr 数组的元素是 int 类型,所以 p 的类型也要是 int* 类型。
其实指针 p 并不知道它指向的是一个数组,p 只知道它指向的是一个整数,究竟如何使用 p 取决于代码里面怎么写。
1.2 数组名的含义
在上面的代码中,直接将 arr 赋值给指针 p,这是因为数组名 arr 可以被当作是一个指针常量,就是数组第一个元素的地址,在大多数使用数组名的表达式中,数组名就被当作一个指针常量,比如下面这个例子:
int arr[5] = {1, 2, 3, 4, 5};
int value;
value = arr[0]; //也可以写成 value = *arr;
value = arr[1]; //也可以写成 value = *(arr+1);
value = arr[3]; //也可以写成 value = *(arr+3);
虽然 arr[1] 和 *(arr+1) 表示同一个意思,但是从可读性来看,使用下标看起来更舒服
但是存在例外,当使用 sizeof 时,数组名就不是指针常量的意思了,比如在下面的例子中,size1 的值为20,而 size2 的值为4。
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
int size1 = sizeof(arr);
int size2 = sizeof(p);
对于上面这个例子,要明白几点:
- 数组是一系列数据的集合,p仅仅是一个 int* 类型指针,编译器不知道它指向一个整数还是一个数组
- 对 p 使用 sizeof 求得的就是指针变量本身的长度,即编译器并没有把 p 和数组关联起来
- 站在编译器的角度,变量名、数组名都是一个符号,需要和数据绑定起来,变量名表示一个数据,数组名表示一组数据,它们都是有类型的,上面数组 arr 的类型就是 int[5],表示一个拥有5个 int 数据的集合,那么长度就是4*5 = 20个字节,而 p 的类型是 int*,在32位环境中长度为4,在64位环境中长度为8
- 归根结底,arr 和 p的类型不一样,因此使用sizeof求得的结果也不一样
整数、小数、数组、指针等不同的数据类型都是对内存的抽象,用不同的名字来指代不同的内存块,我们在编程的时候,不需要直接面对内存,使用这些名字更方便。
1.3 字符串指针
C语言中没有特定的字符串类型,通常都是将字符串放在一个字符数组中。
char str[] = "sunday";
printf("%s\n", str);
除了使用字符数组,还可以直接使用一个 char* 指针指向字符串,如下:
char *str = "sunday"; //字符串常量
printf("%s\n", str);
第二种方式中,实际是定义了一个数组指针,因为字符串实际上是字符数组,数组中的每个元素都是char类型,因此 str 的类型也必须是 char*,str指向的是字符串的第0个字符
上面两种方式,都可以使用 %s 输出整个字符串,都可以使用 str[ i ] 或者 *(str+i) 的方式获取单个字符,但是两者还是有区别的。
两者在内存中的存储区域不一样,前者存在栈区(可读可写),后者存在常量区(可读不可写)。
因此对于后者,字符串一旦被定义,就不能被修改,
char *str = "sunday";
str[1] = 'A'; //错误
上面这样写,编译是可以通过的,但是运行时会出错,因此如果使用这种方式,可以使用 const 关键字,一旦出现重新赋值修改,在编译阶段就可以发现错误了。
1.4 二维数组指针
二维数组给我们的感觉像是有行有列,但是在内存中,没有行列的概念,就是一块连续的内存,先依次存放 arr[0],然后依次存放 arr[1]。
int(*p)[3] = arr,首先 *p 表明 p 是一个指针,它指向一个数组,数组的类型是 int[3],这正是 arr 所包含的每个一维数组的类型。
对指针进行加减整数运算时,前进或后退的步长和它指向的数据类型有关,p 指向的类型为 int[3],那么 p+1 就前进 3x4=12 个字节,这正好是数组 arr 所包含的每个一维数组的长度,即 p+1 会使指针指向二维数组的下一行。
那么来看一下这个表达式,*(*(p+1)+1)表示第1行第1个数字。
-
首先看内层括号(p+1),表示将p指向arr[1]
-
然后解引用 *(p+1) 会得到指向数组 {3, 4, 5} 的指针
-
*(p+1)+1,会从数组{3, 4, 5}的首地址,向后移动一个位置,此时指向数组中的 4
-
最后*(*(p+1)+1)解引用上一步得到的指针,取出数字4
因此要想访问二维数组的每个数字,就使用 *(*(p+i)+j)即可,但是实际编程中,使用p[ i ][ j ] 看起来更舒服。
2. 指针数组
如果一个数组中的所有元素都是指针,那么就称它为指针数组。
int a = 1;
int b = 2;
int c = 3;
int* arr[3] = {&a, &b, &c};
int **p = a;
int*arr[3], 根据上一篇文章讲的优先级,[] 的优先级比 * 的优先级高,首先arr[3] 说明这是一个数组,包含了 3 个元素,然后int* 表明每个元素的类型都是 int*,并且使用变量 a,b,c的地址进行初始化。
int **p是指向数组 arr 的指针,它的定义形式应该为 int *(*p),括号里面的 * 表示 p 是一个指针,括号外面的 int* 表示 p 指向的数据类型。
在这个例子里面,**(p+i) 和 *arr[i] 可以得到同样的结果。
2.1 字符串数组
char *arr[3] = {
"monday",
"tuesday",
"wednesday"
}
char **p = arr;
char *arr[3]定义了一个指针数组,它的每一个元素类型都是 char*,*arr 就表示一个指向字符的指针,**arr表示一个具体的字符
arr是二级指针,*(arr+i)是一级指针,*(*(arr+i)+j) 才是具体的字符。
2.2 指针数组和二级指针
写到这里,我有个问题,字符串可以当成是字符数组,那么字符串数组实际就是二维字符数组(实际上每个字符串长度可能不一样),定义一个char **p 就可以指向字符串数组,那么是不是意味着定义一个int **p 就可以指向 int 二维数组?
答案是不行的。
int arr[2][3] = {{0, 1, 2}, {3, 4, 5}};
int **p = arr 这种写法是错误的,原因在于类型不匹配,我们可以把arr理解为一个包含两个元素的数组,每个元素的类型都是 int[3],所以定义一个指向该数组的指针时,指针类型应该是和元素的类型有关
而 int** 表示一个指向整型指针的指针,即 int** 应该指向 int*类型,而不是int[3]类型。
char arr[2][3] = {{'a', 'b', 'c'}, {'d', 'e', 'f'}};
同理,如果 arr 是一个普通的 char 二维数组,char **p = arr 这种写法也是错误的,原因也是类型不匹配,char ** 期望指向 char* 类型而不是char[3]类型。
那么为什么字符串数组就可以呢?
一层一层来看,一个字符串可以使用一个 char *指针来指向它。
char *str = "monday";
那么一个字符串数组,实际上就是一个 char* 类型的数组,数组的每个元素都是 char*,那么自然可以使用 char** 来指向这个字符串数组。
而一个 int 二维数组或者 char 二维数组,并没有 int* 指针或者 char* 指针来指向二维数组的每一行,这种情况下就不能使用 int** 来指向这个二维数组。
如果使用了malloc 为每行数据单独分配空间,然后再使用int** 就可以了,这是因为,每一行都有一个int *指向它,而 int** 指向的是一组 int*。
3. 数组和指针的关系总结
3.1 联系
-
数组名作为指针:在大多数表达式中,数组名被视为指向第一个元素的指针
-
指针操作:可以使用指针来访问数组的元素,通过指针的整数运算来实现,如加一表示访问下一个元素
3.2 区别
-
类型和含义:数组是一个数据集合,而指针是一个变量,存储的是一个地址
-
内存分配:声明一个数组时,编译器会为数组保留内存空间,然后数组名为这段空间的起始地址,声明一个指针时,编译器只为指针本身保留内存空间,一个指向整型的指针并不会创建用于存储整型值的内存空间。
-
指针可以被重新赋值以指向不同的地址,数组名不能被重新赋值,因为数组名是一个常量
-
使用sizeof 运算符,对数组得到的是整个数组的大小,对指针得到的是指针本身的大小
这篇文章就结束了,下一篇接着讲指针。