对C语言来说数组和指针二者存在千丝万缕的关系,所以贴在下面以方便随时回顾。
C语言☞数组
指针为什么如此重要?我的信念是:正是指针使C威力无穷。有些任务用其他语言也可以实现,但C能够更有效地实现;有些任务无法用其他语言实现,如直接访问硬件,但C却可以。要想成为一名优秀的C程序员,对指针有一个深入 而完整的理解是先决条件。
理解了内存后,理解指针就容易多了。
#include
int main()
{
int a = 10; //在内存中开辟一块空间
int* ptr = &a; //取出a变量的地址,a变量占用4个字节的空间,
// 这里将a的4个字节的第一个的地址存放在p变量中,p就是一个指针变量
return 0;
}
对于上面的代码,我们说 ptr “指向”a。ptr和&a的区别是ptr是变量,而&a是常量。或者说,ptr是可修改的左值,而&a是右值。
相信读者已经很熟悉如何声明 int 类型和其他类型的变量,那么如何声明指针变量?
我们都知道,变量有不同的类型,整型,字符型,浮点型等。如此说来,是变量应该就有类型,那么指针变量的类型是怎样的呢?
int* pi; //pi是指向int类型变量的指针
char* pc; //pc是指向char类型变量的指针
float* pf //pf是指向float类型变量的指针
pc指向的值(* pc)是char类型。pc本身是什么类型?
我们描述它的类型是“指向char类型的指针”
所以,指针实际上是一个新类型。
这里可以看到:
int* 类型的指针是为了存放 int 类型变量的地址。
char* 类型的指针是为了存放 char 类型变量的地址。
float* 类型的指针是为了存放 float 类型变量的地址。
声明指针
上面的代码就是一些指针的声明示例。
声明指针变量时必须指定指针所指向变量的类型,因为不同的变量类型占用不同的存储空间,一些指针操作要求知道操作对象的大小。另外,程序必须知道存储在指定地址上的数据类型。例如,long和float可能占用相同的存储空间,但是它们存储数字却大相径庭。
那指针类型的意义又是什么?
在上面的代码中,对pc定义的时候强制类型转换为char类型,通过打印它们的地址可以看出pc到pc+1隔了1个字节,而pi到pi+1之间隔了4个字节。
pc + n --> pc + n* sizeof (char)
pi + n --> pi + n* sizeof (int)
总结:
指针类型决定了在解引用指针的时候能访问几个字节。
比如:char* 的指针解引用就只能访问一个字节,而int* 的指针的解引用就能访问四个字节。
指针的类型决定了指针向前或者向后走一步有多大(距离)。
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有,明确限制的)。
野指针就像是一条野狗,是非常危险的存在,因为假如你被野狗咬了,你也得不到赔偿。
#include
int main()
{
int* pt; //第一行
*pt = 5; //第二行
printf("%d\n", *pt);
return 0;
}
为何不行?第二行的意思是把5存储在pt指向的位置。但是pt未被初始化,其值是一个随机值,所以不知道5将存储在何处。这可能会擦写数据或代码,或者导致程序崩溃。
切记:创建一个指针时,系统只分配了存储指针本身的内存,并未分配存储数据的内存。因此,在使用指针之前,必须先用已分配的地址初始化它。
Run-Time Check Failure #2 - Stack around the variable ‘arr’ was corrupted.
这段话翻译过来的意思是:运行时检查失败#2-变量“arr”周围的堆栈已损坏。
通过排查,了解到可能是数组“arr”变量可能存在堆栈溢出或内存访问越界的问题。
问题原因是数组里输入的字符超过了这个数组的范围,故导致了访问越界,报错的产生 。编译器不会显示出此类问题,在代码运行过后才会发出一个警告。
遇到此类问题,如若存在数组变量,则需再三检查数组下标访问的合法性。
当指针指向的范围超出数组arr的范围时,p就是野指针。
#include
int main()
{
int* p = NULL;
int a = 10;
p = &a;
if (p != NULL)
{
*p = 20; //检查指针有效性
}
printf("%d\n", *p);
return 0;
}
这步if判断非常关键,是一个程序员严谨思维的体现。
可以对指针进行哪些操作?C提供了一些基本的指针操作。
像上面的例子中,初始化一个数组为全0。递增指向数组元素的指针可以让该指针移动至数组的下一个元素。
可以使用 + - 运算符把指针与整数相加相减,该整数将乘以指针指向类型的大小(以字节为单位),然后把结果与初始地址相加相减。
#include
//strlen 的模拟实现
int my_strlen(char* s)
{
char* p = s;
while (*p != '\0')
{
p++;
}
return p - s; //计算出字符串的元素个数
}
int main()
{
char s[] = "abcdef";
int len = my_strlen(&s);
printf("%d\n", len);
return 0;
}
两个指针指向同一块空间,指针 - 指针得到的是指针和指针之间的元素个数。
#include
int main() //初始化数组为全0
{
int arr[5];
int* p;
for (p = &arr[0]; p < &arr[5];)
{
*p = 0;
p++;
}
return 0;
}
其实,细心的小伙伴会发现我在比较时,取出了数组后面的空间(&arr[5])。其实,这是合法的。
标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针进行比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
#include
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("arr = %p\n", arr);
printf("&arr[0] = %p\n", &arr[0]);
return 0;
}
从运行结果可以看出,数组名和数组首元素的地址是一样的。
结论:数组名表示的是数组首元素的地址。
除了下面两种情况:
1、sizeof(数组名),计算的是整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。
2、&数组名,取出的是数组的地址。&数组名,数组名表示整个数组。
#include
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p + i);
}
return 0;
}
从上面的运行结果可以看出,p+i 其实计算的是数组arr 下标为 i 的地址。
那我们就可以直接通过指针来访问数组了。
arr[ i ] 和 *(arr + i)二者是等价的
其实,arr[ i ] = * (arr + i) = * (i + arr) = i [ arr ],虽然后两种形式不常见,但语法是正确的
我们在处理数组的函数实际上用指针作为参数,但是在编写这样的函数时,可以选择是使用数组表示法还是指针表示法。
int b = 20;
*ppa = &b;
//等价于 pa = &b;
***ppa = 30;
//等价于*pa = 30;
//等价于a = 30;
指针和多维数组有什么关系?为什么要了解它们的关系?
int zippo[4][2];
数组名 zippo 是该数组首元素的地址。在本例中,zippo 的首元素是一个内含两个 int 值的数组,所以 zippo 是这个内含两个 int 值的数组的地址。
因为zippo 是数组首元素的地址,所以 zippo 的值和 &zippo [ 0 ] 的值相同。而 zippo[ 0 ] 本身是一个内含两个整数的数组,所以 zippo[ 0 ] 的值和它首元素的地址(即 &zippo[ 0 ] [ 0 ] 的值 )相同。
简而言之,zippo[ 0 ] 是一个占用一个 int 大小对象的地址,而 zippo 是一个占用两个 int 大小对象的地址。由于这个整数和内含两个整数的数组都开始于同一个地址,所以 zippo 、zippo[ 0 ] 和zippo[ 0 ] [ 0 ] 三者的值是相同的。
解引用一个指针(在指针前使用 * 运算符)或在数组名后使用带下标的[ ] 运算符,得到引用对象代表的值。
因为 zippo[ 0 ] 是该数组首元素( zippo[ 0 ] [ 0 ] )的地址,所以*(zippo[ 0 ] )表示存储在zippo[ 0 ] [ 0 ] 上的值。
与此类似, * zippo 代表该数组首元素(zippo[ 0 ] )的值,但是 zippo[ 0 ] 本身是一个int类型的值。该值的地址是&zippo[ 0 ] [ 0 ] ,所以 * zippo 就是 &zippo[ 0 ] [ 0 ] 。**zippo 与 *&zippo[ 0 ] [ 0 ] 等价,相当于 zippo[ 0 ] [ 0 ] .
简而言之,zippo 是地址的地址,必须解引用两次才能获得原始值。
大家看文字可能不太理解的话,下面将以视图来演示数组地址、数组内容和指针之间的关系。
可以用数组表示法或指针表示法来表示一个二维数组元素:
zippo[ m ][ n ] == *( * zippo + m) + n )
C把数组名解释为该数组首元素的地址。换言之,数组名与指向该数组首元素的指针等价。概括的说,数组和指针的关系十分密切。如果 arr 是一个数组,那么表达式 arr [ i ] 和 *(arr)+ i
等价。
后记
指针的用法灵活巧变,指针的使用会贯穿整个C语言,这只是初阶的指针,后期可能还会更新指针进阶的内容。