您的位置:首页 > 编程语言 > C语言/C++

读书笔记《c语言深度解剖》(6)

2010-03-02 20:39 211 查看
今天看了第四章,数组和指针。
第四章:数组和指针
1.将数值存储到指定的内存地址

前面几个小节没什么好记录的,这一个小节需要注意的是,必须做强制转换
int *p = (int*) 0xFFFFFF;
*p = 10;
2.数组
当我们定义一个数组a的时候,编译器根据元素类型和数组大小分配一块确定大小的内存,并把这块内存命名为a。名字a一旦被命名就不能被改变(数组不能被作为左值)。a[0],a[1]为每一个元素,并非元素的名字,数组中的元素是没有名字的。
int a[3];
sizeof(a);     //12
sizeof(&a);    //4。书上解释为取数组a的首地址。
sizeof(a[0]);  //4
sizeof(a[3]);  //4。因为sizeof是关键字不是函数,函数求值是在运行的时候,而关键字sizeof求值是在编译的时候。这里并没有去访问a[3]这个元素,而是根据元素类型来确定。
sizeof(&a[0]); //元素a[0]的首地址
这里牵涉到&a[0]和&a的区别,前者是首元素的首地址,后者是数组的首地址,两者的值相同,但是所代表的含义不同。
3.数组名a作为左值和右值

数组名a作为右值的时候,其意义与&a[0]一样,代表的是数组首元素的地址,而不是数组的首地址。这时两回事。而且编译器并没有专门用一块内存空间来存储数组的地址,这点和指针不同。(指针的值是个地址,指针本身也有地址)
数组名a不能做为左值!我们无法把数组当做一个整体来访问。
4.以指针的形式访问和以下标的形式访问
char *p  = "abcdef";  //A
char a[] = "123456";  //B
数组和指针都可以既用指针形式访问,和用下标形式访问。p指向一块静态区的首地址,这块内存地址没有名字,访问这块区域是属于完全匿名访问。数组a本身是在栈上,要访问数组a中的元素,先通过数组名a找到这块内存,然后根据偏移量找到具体的值,这时具名+匿名的访问方式
5.a和&a的区别
首先说明a是数组首元素的地址,和&a[0]一样。而&a是数组的首地址。a和&a的值相同,但是含义却不一样。来看一个例子:
int main() {
int a[5] = {1,2,3,4,5};
int *ptr = (int*)(&a+1);
printf("%d, %d/n", *(a+1), *(ptr-1));
return 0;
}
上面已经说了&a是整个数组的首地址,后面加1,等于是再加上sizeof(a)的值,即&a+5*sizeof(int)。这时候已经越界了,然后再强制转换为int*,赋给ptr。
而a是数组首元素的地址,后面加1,等于是再加上sizeof(int)的值,即a[1]的首地址。
最后,由于ptr指向的是a[5],ptr-1指向的是a[4]。
所以这个程序输出2 5
6.指针和数组的定义和声明
6.1 定义为数组,声明为指针
在文件1中,有定义:char a[] = "ABCDEFG";在文件2中,有声明:extern char *a;
首先要说的是,这个是错误的!
对于大多数编译器来说,在文件1中,a占7个byte;在文件2中,会把a当做一个指针来处理,占4个byte。而且,在这4个byte中,分别存储的是A,B,C,D的ASCII码值。由这四个ASCII码值组成的地址并不是我们所想要的数组的首元素的地址。而真正的数组a的首元素的首地址在文件一中定义数组的时候被编译器保存到了某个不知道的地方。
6.2 定义为指针,声明为数组
在文件1中,有定义:char *p = "abcdefg";在文件2中,有声明:extern char p[];
如上面的分析一样,在文件1中,指针p中存储的是一块静态区域的地址,假设这块地址为0x00FF0000。而在文件2中,把p当做数组来访问,按照char类型取出p[0],p[1],p[2],p[3]分别为0x00,0xFF,0x00,0x00,并不是我们所想要的a,b,c,d。而且如果更改了p[0]等等,那么本来静态区域的地址也会丢失。
7.指针数组和数组指针
指针数组:是一个数组,数组里面都是指针。
数组指针:是一个指针,指向了一个数组。
char *p1[10];      //指针数组。因为[]的优先级比*高,所以p1首先是一个数组,然后,这个数组里的元素都是指向char的指针。
char (*p2)[10];    //数组指针。()的优先级比[]高,所以p2首先是一个指针,这个指针指向一个数组,这个数组里的成员类型是char。
char (*)[10] p3;   //也是数组指针。char (*)[10]是指针类型,p3是指针变量名。
有一个例子:
int main() {
char a[5] = {'a','b','c','d','e'};
char (*p3)[5] = &a;    //=号左右两边一样,都是指向整个数组的指针。p3+1向后移动5个字节。
char (*p4)[5] = a;     //=号左右两边不同,右边是指向单个元素的指针。编译器给出警告。p4+1向后移动5个字节。
char (*p5)[3] = &a;    //p5+1向后移动3个字节。
char (*p6)[10] = &a;   //p6+1向后移动10个字节。
}
8.地址的强制转换
struct test{
//......
//......
}*p;
假设p的值为0x00FF0000。求下面三个表达式的值:

p+0x01 分析:就是p加上一个整数,即为p+sizeof(struct test);
(unsigned int)p+0x01 分析:p的值已知为0x00FF0000,这本来是一个地址,现在被转换为一个int型,所以结果就为int型加上1,为0x00FF0001
(unsigned int*)p+0x01 分析:p被转换成为一个int型指针,所以结果为p加上sizeof(unsigned int),为0x00FF0004;
还有一个例子:
int main() {
int a[4] = {1,2,3,4};
int *p1 = (int*)(&a + 1);
int *p2 = (int*)(int(a) + 1);
printf("%d, %d/n", p1[-1], *p2);
return 0;
}
下面来分析一下:由前面小节的知识知道,p1指向的是一个越界地址,即a[3]的末尾,a[4]的首地址。所以p1[-1]被解析成*(p1-1),所以打印出4
我们知道,a是数组首元素的首地址,在第4行被转换成了一个整数,然后加1。我们又知道数组中都是int型,每个元素有4个byte。于是p2指向的是元素a[0]中四个byte中的第2个(第1个是a)。所以,p2指向的内存块包括a[0]的后3个byte和a[1]的第一个byte。具体打印的值,还依据系统的采用的是大端还是小端的存储模式。
9.二维数组的布局
只需记住二维数组的存储也是线性的,编译器总是把二维数组看成一个一维数组,而一维数组的每一个元素又是一个数组。这里还碰到了一个逗号表达式的知识:用逗号运算符连接的式子叫做逗号表达式,它的值是最右边的表达式的值。
int a = 3;
a *= 5, a + 2;          //表达式值为17
if (a=fun(x,y), a>0) {  //先执行a=fun(x,y),再判断
//.....
}
还有一个例子:
int main() {
int a[5][5];
int (*p)[4];
p = a;
return 0;
}
求&p[4][2] - &a[4][2]
首先由弄清楚所求的是什么。因为[]的优先级大于&的优先级,所以,这个问题求的是p[4][2]和a[4][2]两个地址之差。
首先,&a[4][2]很简单,即a中第22个元素的地址。那么&p[4][2]呢?根据定义,p是一个数组指针,大小为4,类型为int,所以p+1相当于把p向后移动了4*sizeof(int)个byte。所以p[4]相当于把p移动了4*4*sizeof(int)个byte。所以p[4][2]在p[4]的基础上又移动了2*sizeof(int)个byte,即a中第18个元素的地址。所以答案为-4。
10.数组参数和指针参数
要记住:c语言中,当一维数组作为函数参数时,编译器总是把它解析为一个指向数组首元素首地址的指针。(这样做的原因是,在值传递的时候,拷贝整个数组无论是在空间还是时间上,开销都是非常大的)一个例子:
void fun(char a[10]) {     //可以写成char a[]。
int s = sizeof(a);     //这里s为4,因为a被解析成了指针。
}
int main() {
int b = "asdffg";
fun(b);
return 0;
}
指针作为函数参数时,也要注意,传递进去的是指针的拷贝:这里有个例子:
void fun(char *p, int num) {
p = (char*)malloc(sizeof(char)*num);
}
int main() {
char *a = NULL;
fun(a,10);
strcpy(a, "asd");   //此时a仍然为NULL,p所指的内存泄露
free(a);
return 0;
}
有两种方法可以解决这个问题,一个是return一个指针,另一个是使用二级指针。
如果是二维数组作为参数,诸如void fun(char a[3][4])。因为上面说了,我们可以把a[3][4]看成一个一维数组a[3],然后编译器会将这个一维数组解析成一个指针,所以void fun(char a[3][4])可以改写为:void fun(char (*p)[4])。同时,也可以写成void fun(char a[][4]),只能省略第一维,之后的无论多少维都不能省。因为上面那条规则不是递归的,只能把第一维解析成指针。
如果我们把这里的()去掉,即void fun(char *p[4]),则函数参数是一个一维指针数组,按照上面的规则可以改写为void fun(char **a),即二级指针。
11.函数指针
char*(*fun1)();  //fun1是函数指针,指向一个函数
char**fun2();    //fun2是函数,返回一个char**
一个经典的例子:(*(void(*)())0)();
首先,void(*)()是一个函数指针类型;
其次,将0转化为函数指针类型,即一个函数存在于首地址为0的一段区域内;
然后,用*取出这段内存中的内容,也就是这个函数。
最后,()调用这个函数。
函数指针数组也就是将函数指针放在一个数组里面,形如:char* (*p[3])(void)。分析:首先p是一个数组,数组的元素是指针,这些指针指向某种类 型的函数。可以这样使用函数指针数组:
int main() {
char* (*p[3])();
p[0] = fun1;      //ok
p[1] = &fun2;     //ok;加上&符号也可以。
return 0;
}

函数指针数组的指针形如,char* (*(*p)[3])()。分析,首先p是一个指针,指向一个含有3个元素的数组。这个数组的每个元素都是一个函数指针。我们可以这样使用函数指针数组的指针:
int main() {
char* (*a[3])();
char* (*(*p)[3])();
p = &a;
a[0] = fun1;     //a的用法和上面一样。
a[1] = &fun2
p[0][0]();       //这里只能是p[0][i],因为p是一个指向数组的指针,p[1][i]会向后移动整个数组的长度,出现越界。
p[0][1]();
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: