数组,指针
总是有人认为数组就是指针,指针就是数组,两者好像完全是一样的东西。之前的我也曾幼稚的这样认为过。其实,事
实并非这样,指针就是指针,数组就是数组,两者是完全不同的东西。我们之所以会认为数组就是指针,指针就是数
组,无非就是因为他们都可以“以指针的形式”和”以数组的形式“进行访问。下边我们分别来讲解数组和指针。
(一)数组:
int a[5]; 我相信所有人都知道这是一个数组,里边包含了5个元素。
数组的定义及初始化:
int n = 4; int arr[n] = {1,2,3,4};以上数组的定义方法是初学者最容易犯的错误。因为n是一个变量,而数组的元素个数必须是一个常量。
那我们如果用const修饰的量,会不会出错??在.c文件中,const修饰的量是只读变量,注意仍然是变量,而在.cpp
文件中,const修饰的就是常量,我们可以用来定义数组个数。
但是,如果你这样子做就是可以的,如下:
#define n 10 int arr[n] = {0};因为在预编译阶段标识符n就会被替换成10.
编译器通常不会为普通const只读变量分配内存,而是将他们保存在符号表(关于符号表,之后的博客中我将详
细解析)中,const只读变量在程序运行过程中只有一份拷贝,而宏在预编译阶段会进行替换。
注意:1.数组一旦给定,a就与这块内存匹配不可改变,也就是a++,--a,这些都是不对的。
在博客strlen和sizeof中,有提过求数组大小,这里再说一次。
sizeof(a)的值在32位下是20.
sizeof(a[0])在32位系统下是4.
sizeof(a[5])在32位系统下是4.并没有出错。一定要记住,sizeof是关键字而不是函数,函数求值是在运行的时
候,关键字求值是在编译的时候,虽然不存在a[5]这个元素,但是这里并没有真正去访问它。
2.只有在两种场合下,数组名并不发生降级:当数组名作为sizeof的操作数或是单目操作符&的操作数时。
二维数组:二维数组其实就是一维数组。int b[2][3];这里b可以看做是包含2个元素的数组,每个元素又是一个包含
3个元素的数组,在内存中的存储方式如图。
3.数组名能否作为左值和右值呢??数组名不能作为左值。当数组名是左值时,编译器会认为数组名是数组首
元素的地址,而这个地址开始的一块内存是一个整体,我们只能访问数组中的某个元素,而不能把数组当成一个
整体进行访问。数组名可以作为右值,作为右值时,表示的数组首元素的地址。
4.a和&a的区别:在strlen和sizeof那篇博客中,关于这个我们已经做过整理和区分,这里给出例题来理解一下。
int main() { int a[5] = { 1,2,3,4,5 }; int *ptr = (int *)(&a + 1); printf("%d %d", *(a + 1), *(ptr - 1)); return 0; }我们可以仔细分析一下这道题目,看看最后输出的结果到底是什么呢??
答案是2 5. &a+1是指向数组的指针,指向数组元素5后边的那块内存,强制类型转换,转换成int *,所以结果是5.
如果没有进行强制类型转换,第二个结果还是1.
(二)指针
指针常量:请看下边一段代码。
int main() { int a = 10; *0x0018ff44 = 20; return 0; }注:++6.0下第一个变量的地址就是0x0018ff44.
上边这段代码到底能不能将a的值变为20呢??答案是否定的,因为编译器认为0x0018ff44是一个十六进制数字,是
无法进行进行解引用。所以,使用指针常量的时候一定要进行强制类型转换。
*(int *)0x0018ff44 = 20;这样就可以将a的值改为20.
接着看下边的例子:
int main() { int *p = (int *)0x0018ff44; *p = null; p = null; return 0; }
看完这段代码,我相信肯定有人会认为*p = null;这句编译不通过。其实null的ascii是0.这下就应该知道了吧。
我们继续在vc++6.0下调试这段代码,发现当执行到*p = null;时,p = 0x00000000.这到底是为什么呢??因为p
的地址是0x0018ff44 ,p里边保存的地址也是0x0018ff44 (自身的地址),所以*p就是p(在这段程序中是这样),
所以改变*p也就改变了p。
在《深度解剖》这本书中对这个问题作出了特别详细的解释,有兴趣的读者可以自行。
指针变量是存放变量的地址的变量。
一说到指针变量,我们的脑海里应该会闪现出在三个和它密切相关的“人物”------指针变量的内容、指针变量的指
向、指针变量的地址。
char ch = 'a'; char *cp = &ch;我们来通过左值和右值来理解指针变量。
&ch 作为右值,&ch是ch的地址,也就同指针变量cp中存储的变量一样。作为左值是非法。
cp作为右值,表示cp的值,作为左值表示cp所代表的那块内存。
&cp作为右值,表示指针变量cp的地址,作为左值非法。
*cp作为右值,表示cp所指向的空间的内容,作为左值表示cp所指向的那段空间。
*cp+1作为右值,表示的是字符‘b’,作为左值非法。
*(cp+1)作为右值,cp的下一段空间的内容,作为左值,表示cp的下一段内存。
++cp作为右值,表示cp的下一段内存的内容,作为左值非法。
cp++作为右值,表示cp的内容,作为左值非法。
*++cp作为右值,表示ch后边的内存地址的值,作为左值表示ch后边那块内存位置。
*cp++作为右值ch的内容,作为左值,表示ch这段空间。
注:++的优先级大于*。
++*cp作为右值,表示cp指向的内存的内容加1,作为左值,非法。
(*cp)++作为右值,表示cp指向的内存的值,作为左值,非法。
注:(*cp)++与*cp++不一样。
++*++cp作为右值,表示cp下一段内存指向的内存的内容加1,作为左值非法。
++*cp++作为右值,表示cp指向的内存空间的值加1,作为左值非法。
指针之间的运算:
指针-指针:是两指针之间的元素的个数,单位是个数,不是字节。指针-指针的前提是两指针指向同一块内存;指针
类型相同。看下边一个例子:
int main() { int arr[10] = {0}; int n = &arr[10] - &arr[0]; printf("%d",n); return 0; }注:以上代码可以正确求出数组的大小。&arr[10],仅取地址,不会产生越界,如果改变arr[10],就会出错。
说到这里,肯定会有人说,那可不可以用数组的最后一个元素的地址减去数组第一个元素之前的那个地址来求数组大
小呢?答案是不可以。因为数组的前边是不会预留空间,而数组末尾会预留空间。所以,不要让指针指向-1号下标。
指针和数组讲到这里,想必大家也应该理解了吧。接下来,我将会对比着指针和数组,再来分析一些重要内容。
(三)指针,数组对比分析:
看以下的例子:
char arr [10] = "abcdef"; char *ap = arr+2;注意:图中是ap不是cp
ap就是&arr[2];
*ap就是arr[2];
ap[0]就是arr[2];注意:c的下标引用和间接访问表达式是一样的。
ap+6就是&arr[8];
*ap+6为arr[2]+6;注意,间接访问的优先级高于加法运算符。
*(ap+6)表示的是arr[8];
ap[6]也是arr[8];
&ap是表示存放ap的内存地址。
ap[-1]就是arr[1].
ap[9]实则是表达式arr[11],但是arr数组总共只有10个元素,ap[9]这个表达式越过了数组的右界,but编
译器一般检测不出来,所以如果在程序中写出这样的表达式,会出现意想不到的结果。
看下边的代码:
int main() { int i,arr[10]; for(i = 0;i<=12;i++) { printf("love you"); arr[i] = 0; } return 0; }
这个程序中数组发生了越界,导致程序死循环。当然这只是在vs编译环境下。我们通过下边的图片看看为什么死循环
注意:栈与其他数据结构不同,栈是先入栈的是在高地址。
如果在vs下,刚好越过了预留空间,改变了i的值,导致死循环。
如果是在vc++6.0下,arr[9]与i是挨着的。只要数组一越界。就会出问题。
如果在linux下的gcc编译器中运行,貌似arr[9]与i也是挨着。(我将循环判断条件改成i<=10,依然死循环)
指针数组与数组指针:
首先,我们必须明确指针数组是一个数组,每个元素都是一个指针。数组指针是一个指针,指向的是一个数组。
对于指针数组,我们并不陌生,main函数里的一个参数就是指针数组。比如给定指针数组int *arr[5];
而数组指针呢?
注意注意:对于以上的指针数组,他的类型到底是什么??应该是int(*)[5],指针变量是p,知道这一点很重要,
强制类型转换时会用到。
千万不要认为二维数组就是一个二级指针。二维数组其实就是一个一维数组,每个元素又是一个一维数组。
下边一个关于二维数组传参的例子:
void fun(int arr[2][3]) { ; } int main() { int arr[2][3]; fun(arr); return 0; }实参里的arr,是数组首元素的地址,是一个指向大小为3的数组的指针。所以,在形参里的那个第二维大小是不可省
略的。,除了代码中给出的形参外,形参还可以是int[][3];或者是int (*arr)[3].牢记int arr[][].和int**arr这两个都是不可
以的。
关于声明和定义:
定义:就是创建了一个对象,并为这个对象分配了一块内存,并取了名字。
声明:(1)告诉编译器,这个名字已经匹配到一块内存了,下边的代码就不要用这个名字了。
(2)告诉编译器,这个名字已经预定,别的地方不要使用它了。
从上边的学习中,我们已经知道,数组和指针是不一样的东西,所以定义为数组就要声明为数组,定义为指针就要声
明为指针,其他的都是不对的,下边我们来一一分析一下。
定义为数组,声明为指针:
比如:
int arr[] = "abcdef";在另一个文件中有如下声明:
extern char *arr;如果我们在声明的这个文件中输出arr,将会出现什么结果呢??程序崩溃,图解如下:
而声明的指针只占4个字节,他看到的就是0x61 0x52,0x63,0x64,他可能不是一个有效的地址。所以这样不对。
定义为指针,声明为数组:
定义:char* p = "abcdef";
声明:extern char str[];
所以在声明的那个文件中我们看到的str[0]就是0x00,str[1]就是0x12......是不会得到正确结果的。但是如果我们想要
输出abcdef该怎样输出呢??
printf("%s",*(char **)str);
或者:printf("%s",(char *)*(int *)str);将str转为int*,解引用,再强转。
分析到这里,所以指针就是指针,数组就是数组,不要乱用。
下边整理出几道比较重要的例题:
例1:
int a[5][5]; int (*p)[4] = null; p = (int (*)[4])a; printf("%d %p",&p[4][2]-&a[4][2],&p[4][2]-&a[4][2]);分析一下这个程序输出的结果是什么。。
当以%d格式输出时,结果是4,这个就利用了指针相减那个知识点;当以%p输出时,会输出什么呢??它会将-4变成
无符号数输出,因为地址并没有负数。
-4原码:10000000 00000000 00000000 00000100
反码: 11111111 11111111 11111111 11111011
补码:11111111 11111111 11111111 11111100
所以%p格式输出 ff ff ff fc
例2:
int a[4] = {1,2,3,4}; int *ptr1 = (int *)(&a +1); int *ptr2 =((int)a+1); printf("%x %x",ptr1[1],*ptr2);程序输出结果会是什么??下边图解:
所以程序输出结果:4 20 00 00 00(小端模式)
其实,(int)a+1是将a转换成整形再加1.
上一篇: 降温了!好冷啊!