转:C语言深度剖析二
分类: C/C++
1.关于指针的数据类型:
指 针变量 p 里存储的任何数据都将被当作地址来处理。可以这么理解:一个基本的数据类型(包括结构体等自定义类型)加上“*”号就构成了一个指针类型的模子。这个模子 的大小是一定的,与“*”号前面的数据类型无关。 “*”号前面的数据类型只是说明指针所指向的内存里存储的数据类型。编译器会把存在指针变量中的任何数据当作地址来处理。
2.关于NULL:
注意NULL就是NULL,它被宏定义为 0。NUL是ASCII码表的第一个字符,表示的是空字符,其ASCII码值为 0。其值虽然都为0,但表示的意思完全不一样。同样,NULL和0表示的意思也完全不一样。
3.关于数组:
当我们定义一个数组a时,编译器根据指定的元素个数和元素的类型分配确定大小(元素类型大小*元素个数)的一块内存,并把这块内存的名字命名为a。a[0],a[1]等为a的元素,但并非元素的名字。数组的每一个元素都是没有名字的。
4.关于int a[5]:
如:int a[5];sizeof(a[5])
函数求值是在运行的时候,而关键字 sizeof求值是关键字,它是在编译的时候。虽然并不存在a[5]这个元素,但是这里也并没有去真正访问 a[5],而是仅仅根据数组元素的类型来确定其值。所以这里使用 a[5]并不会出错。
5.关于&a[0]和&a的区别:
a[0]是一个元素,a是整个数组,虽然&a[0]和&a的值一样,但其意义不一样。前者是数组首元素的首地址,而后者是数组的首地址。
6.关于a和&a的区别:
a,&a的值是一样的,但意思不一样,a 是数组首元素的首地址,也就是 a[0]的首地址,&a是数组的首地址,a+1是数组下一元素的首地址,即a[1]的首地址,&a+1是下一个数组的首地址。
例:int main()
{
char a[5]={'A','B','C','D'};
char (*p3)[5] = &a;
char (*p4)[5] = a;
return 0;
}
由于a和&a的意义不同,则p3正确。p4这个定义的“=”号两边的数据类型就不一致了。左边的类型是指向整个数组的指针,右边的数据类型是指向单个字符的指针。
7.关于数组名作左值与右值:
如:x=y
左值:在这个上下文环境中,编译器认为 x 的含义是x 所代表的地址。这个地址只有编译器知道,在编译的时候确定,编译器在一个特定的区域保存这个地址,我们完全不必考虑这个地址保存在哪里。
右值:在这个上下文环境中,编译器认为 y 的含义是y 所代表的地址里面的内容。这个内容是什么,只有到运行时才知道。
a作为右值时其意义与&a[0]是一样,代表的是数组首元素的首地址,而不是数组的首地址。
a不能作为左值,因为编译器会认为数组名作为左值代表的意思是a的首元素的首地址,但是这个地址开始的一块内存是一个总体,我们只能访问数组的某个元素而无法把数组当一个总体进行访问。所以我们可以把a[i]当左值,而无法把a当左值。
8.关于指针和数组的关系:
数组就是数组,指针就是指针,它们是完全不同的两码事!他们之间没有任何关系。
9.以指针的形式访问和以下标的形式访问指针和数组:
以指针的形式访问和以下标的形式访问指针:
例:char *p = “abcdef”;
以指针的形式:*(p+4)。
以 下标的形式:p[4]。编译器总是把以下标的形式的操作解析为以指针的形式的操作。p[4]这个操作会被解析成:先取出p里存储的地址值,然后加上中括号 中4个元素的偏移量,计算出新的地址,然后从新的地址中取出值。也就是说以下标的形式访问在本质上与以指针的形式访问没有区别,只是写法上不同罢了。
以指针的形式访问和以下标的形式访问数组:
例:char a[] = “123456”;
以指针的形式:*(a+4)。a这时候代表的是数组首元素的首地址。
以下标的形式:a[4]。同访问指针。
由上可知:指针和数组根本就是两个完全不一样的东西。只是它们都可以“以指针形式”或“以下标形式”进行访问。一个是完全的匿名访问,一个是典型的具名+匿名访问。
10.关于指针的操作:
对指针进行加1操作,得到的是下一个元素的地址。指针变量与一个整数相加减并不是用指针变量里的地址直接加减这个整数。这个整数的单位不是byte而是元素的个数。
例:struct Test
{
int Num;
char *pcName;short sDate;
char cha[2];
short sBa[4];
}*p;
假设p = 0x100000,则:
p+0x1的值为:0x100000+sizof(Test)*0x1 = 0x100014。
(unsigned int*)p + 0x1的值为:0x100000+sizof(unsigned int)*0x1 = 0x100004。
例:int main()
{
int a[4]={1,2,3,4};
int *ptr1=(int *)(&a+1);
int *ptr2=(int *)((int)a+1);
printf("%x,%x",ptr1[-1],*ptr2);
return 0;
}
假设a = 0x00,则:
分析:
ptr1:&a + 1 = a + sizeof(a)*1 = 0x10,将&a+1的值强制转换成int*类型,赋值给int* 类型的变量ptr,ptr1肯定指到数组a的下一个int类型数组了。ptr1[-1]被解析成*(ptr1-1),即ptr1往后退4个byte。所以 其值为0x10 - 0x04 = 0x0c。
ptr2: (int)a+1的值是元素a[0]的第二个字节的地址。转换成整型就是简单的数学运算了。然后把这个地址强制转换成int*类型的值赋给ptr2, 也就是说*ptr2的值应该为元素 a[0]的第二个字节开始的连续4个byte的内容,也就是说是a[0]的后三个字节和a[1]的第一个字节。这连续 4 个byte里到底存了什么东西呢?也就是说语素a[0],a[1]里面的值到底怎么存储的。这就要看系统的大小端模式了。
11.关于指针数组和数组指针:
指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身决定。它是“储存指针的数组”的简称。
数组指针:首先它是一个指针,它指向一个数组。在32位系统下永远是占4个字节,至于它指向的数组占多少字节,不知道。它是“指向数组的指针”的简称。
例:Aint *p1[10];
Bint (*p2)[10];
理 解:这里需要明白一个符号之间的优先级问题。“[]”的优先级比“*”要高。p1先与“[]”结合,构成一个数组的定义,数组名为 p1,int *修饰的是数组的内容,即数组的每个元素。那现在我们清楚,这是一个数组,其包含 10个指向 int类型数据的指针,即指针数组。至于 p2就更好理解了,在这里“ () ”的优先级比“[]”高, “*”号和 p2构成一个指针的定义,指针变量名为 p2,int修饰的是数组的内容,即数组的每个元素。数组在这里并没有名字,是个匿名数组。那现在我们清楚 p2是一个指针,它指向一个包含10个int类型数据的数组,即数组指针。
12.关于指针函数和函数指针:
指针函数:首先它是一个函数,是指带指针的函数,即本质是一个函数。返回类型是某一类型的指针。
其定义格式如下所示:
返回类型标识符 *返回名称(形式参数表){}
函数指针:首先它是一个指针,是指向函数的指针变量,因而“函数指针”本身首先应是指针变量,只不过该指针变量指向函数。
其定义格式如下所示:
数据类型标志符 (*指针变量名)(参数){}
分析:同样可以用优先级来分析。
13.关于二维数组:
例:char a[3][4];
实际上内存不是表状的,而是线性的。平时我们说内存地址为 0x0000FF00也是指从内存零地址开始偏移0x0000FF00个byte。编译器总是将二维数组看成是一个一维数组, 而一维数组的每一个元素又都是一个数组。
a[i]的首地址为:&a[0]+ i*sizof(char)*4
a[i][j]的首地址为:&a[i]+j*sizof(char)
a[i][j]元素的首地址为:a+ i*sizof(char)*4+ j*sizof(char),以指针的形式表示:*(*(a+i)+j)。(因为a+i为第i个数组的首地址,则*(a+i)为第i个数组的首元素地址)
例:#include <stdio.h>
int main(int argc,char * argv[])
{
int a [3][2]={(0,1),(2,3),(4,5)};
int *p;
p=a [0];
printf("%d",p[0]);
}
问打印出来的结果是多少?
分 析:答案为1,不是0!花括号里面嵌套的是小括号,而不是花括号!这里是花括号里面嵌套了逗号表达式!其实这个赋值就相当于 int a[3][2]={ 1, 3, 5};而二维数组的初始化为:int a[3][2] = {{1,2}, {3,4}, {5,6}};
14.关于数组内存计算:
例:int main()
{
int a[5][5];
int (*p)[4];
p=a;
printf("a_ptr=%#p,p_ptr=%#p\n",&a[4][2],&p[4][2]);
printf("%p,%d\n",&p[4][2] - &a[4][2],&p[4][2] - &a[4][2]);
return 0;
}
分 析:&p[4][2] - &a[4][2]的值为-4.&a[4][2]表示的是&a[0][0]+4*5*sizeof(int) + 2*sizeof(int)。&p[4][2]表示的是&a[0][0]+4*4*sizeof(int)+2* sizeof(int)。
15.关于多级指针:
与一级指针不同的是,一级指针保存的是数据的地址,二级指针保存的是一级指针的地址。
16.关于数组作参数:
例:char b[10] = “abcdefg”;
fun(b[10]);
分析:这里数组越界了,这个b[10]并不存在。但在编译阶段,编译器并不会真正计算b[10]的地址并取值,所以在编译的时候编译器并不认为这样有错误。
无 法向函数传递一个数组,C语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。在 C 语言中,所有非数组形式的数据实参均以传值形式(对实参做一份拷贝并传递给被调用的函数,函数不能修改作为实参的实际变量的值,而只能修改传递给它的那份 拷贝)调用。同样的,函数的返回值也不能是一个数组,而只能是指针。需要明确一个概念::函数本身是没有类型的,只有函数的返回值才有类型。
17.关于指针作参数:
例:void GetMemory(char * p, int num)
{
p = (char *)malloc(num*sizeof(char));
}
int main()
{
char *str = NULL;
GetMemory(str,10);
strcpy(str,”hello”);
free(str) ;//free并没有起作用,内存泄漏
return 0;
}
分 析:无法把指针变量本身传递给一个函数,在运行 strcpy(str,”hello”)语句的时候发生错误。 这时候观察 str的值, 发现仍然为NULL。也就是说 str本身并没有改变,我们malloc的内存的地址并没有赋给str,而是赋给了_str。而这个_str是编译器自动分配和回收的,我们根本就无法使 用。
那怎么办? 两个办法:
第一:用 return。
char * GetMemory(char * p, int num)
{
p = (char *)malloc(num*sizeof(char));
return p;
}
int main()
{
char *str = NULL;
str = GetMemory(str,10);
strcpy(str,”hello”);
free(str) ;
return 0;
}
第二:用二级指针。
void GetMemory(char ** p, int num)
{
*p = (char *)malloc(num*sizeof(char));
return p;
}
int main()
{
char *str = NULL;
GetMemory(&str,10);
strcpy(str,”hello”);
free(str) ;
return 0;
}
这样的话传递过去的是 str的地址,是一个值。在函数内部,用钥匙(“*”)来开锁:*(&str),其值就是str。所以malloc分配的内存地址是真正赋值给了str本身。
18.关于二维数组和二维指针作函数参数:
例:void fun(char a[3][4]);
分析:C 语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。也就是说我们可以把这个函数声明改写为:
void fun(char (*p)[4]);
同样,作为参数时,一维数组“[]”号内的数字完全可以省略:void fun(char a[][4]);不过第二维的维数却不可省略。
注意:如果把上面提到的声明 void fun (char (*p)[4])中的括号去掉之后, 声明 “void fun(char *p[4])”可以改写成:void fun(char **p);
数组参数等效的指针参数
数组的数组:char a[3][4]数组的指针:char (*p)[10]
指针数组: char *a[5]指针的指针:char **p
这里需要注意的是:C语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。这条规则并不是递归的,也就是说只有一维数组才是如此,当数组超过一维时,将第一维改写为指向数组首元素首地址的指针之后,后面的维再也不可改写。
19.关于函数指针:
例:char* (*fun1)(char* p1,char* p2);------A
char* *fun2(char* p1,char* p2);------B
char* fun3(char* p1,char* p2);------C
分析:A既是函数指针也是指针函数,返回值类型都为为char*。B是指针函数,返回值类型为char**,是个二级指针。C是指针函数,返回值类型为char*。
函数指针的使用例子:
char * fun(char * p1,char * p2)
{
......
return p1;
}
int main()
{
char* (*pf)(char * p1,char * p2);
pf = &fun;
(*pf) ("aa","bb");
return 0;
}
分析:给函数指针赋值时,可以用&fun或直接用函数名fun。这是因为函数名被编译之后其实就是一个地址,所以这里两种用法没有本质的差别。
20.关于(*(void(*) ())0)():
分析:
第一步:void(*) (),可以明白这是一个函数指针类型。这个函数没有参数,没有返回值。
第二步:(void(*) ())0,这是将 0强制转换为函数指针类型,0是一个地址,也就是说一个函数存在首地址为0的一段区域内。
第三步:(*(void(*) ())0),这是取0地址开始的一段内存里面的内容,其内容就是保存在首地址为0的一段区域内的函数。
第四步:(*(void(*) ())0)(),这是函数调用。
21.关于函数指针数组:
例:char* (*pf[3])(char * p);
分析:这是定义一个函数指针数组。它是一个数组,数组名为 pf,数组内存储了3 个指向函数的指针。这些指针指向一些返回值类型为指向字符的指针、参数为一个指向字符的指针的函数。
函数指针数组的使用:
#include <stdio.h>
#include <string.h>
char * fun1(char * p){}
char * fun2(char * p){}
char * fun3(char * p){}
int main()
{
char * (*pf[3])(char * p);
pf[0] = fun1; // 可以直接用函数名
pf[1] = &fun2; // 可以用函数名加上取地址符
pf[2] = &fun3;
pf[0]("fun1");
pf[0]("fun2");
pf[0]("fun3");
return 0;
}
22.关于函数指针数组的指针:
例:char* (*(*pf)[3])(char * p);
分析:pf这个指针指向一个包含了3个元素的数组;这个数字里面存的是指向函数的指针;这些指针指向一些返回值类型为指向字符的指针、参数为一个指向字符的指针的函数。
函数指针数组的指针的使用:
#include <stdio.h>
#include <string.h>
char * fun1(char * p){}
char * fun2(char * p){}
char * fun3(char * p){}
int main()
{
char * (*a[3])(char * p);
char * (*(*pf)[3])(char * p);
pf = &a;
a[0] = fun1;
a[1] = &fun2;
a[2] = &fun3;
pf[0][0]("fun1");
pf[0][1]("fun2");pf[0][2]("fun3");
return 0;
}
浙公网安备 33010602011771号