指针是C/C++的精华,如果没能很好的掌握指针,那么C/C++也等于没学,想要学好指针又离不开数组的学习
这里有一些数组和指针的整理
首先让我们来看三个问题:
一.指针
1.1指针的内存布局
举一个栗子:
int *p;
此处定义了一个指针,类型为“int*”,占用了四个字节的空间(所有指针的大小都在内存中占用了4个字节的空间),然后把这个空间命名为p,这4个字节只能储存某个内存的地址,即使你存入别的任何数据都将被当作地址处理。
1.2“*”解引用操作
“*”访问地址所指向的内容, *p即为访问指针p所指向的内存空间。
1.3 int *p = NULL 和*p = NULL的区别
int *p = NULL;
通过编译器可以看到p的值为0x00000000。这句换的意思是在定义一个指针变量p,其指向的内存里面保存的是int类型的数据,并且把p的值设置为0x0000000,而不是把*p的值设置为0x00000000。
int i = 10;
int *p = &i;
*p = NULL;
通过编译器可以发现p指向的内存由原来的10变为0了;而p本身的值,即内存地址本身并没有改变,而指针指向的内存空间内容改变了。
NULL被宏定义为0:
#define NULL 0
1.4 如何将数值存储到指定的内存地址
int *p = (int *)0x12ff7c;
*p = 0x100;
将地址0x12ff7c赋值给指针变量p的时候必须强转 。设置内存地址的时候需注意内存地址一定是一个你有权力去访问的合法地址。你可以这样做:
int i=0;
变量i所在的内存肯定是可以被访问的,然后可以在编译器的watch窗口上观察&i的值解能知道它的内存地址了。得到这个地址后可以把代码“int i = 0;”删掉。
*(int *) 0x12ff7c = 0x100;
这行代码和上面两行代码没有本质区别。先将0x12ff7c强制转换,告诉编译器这个地址上将存储一个int类型的数据;然后通过钥匙“*”向这块内存写入一个数据。
二.数组
三.指针与数组之间的关系
指针和数组之间没有任何关系
指针就是指针,在32位系统下,永远只占4个byte,其值为某一个内存地址。指针可以指向任何地方,但不是任何地方你都可以通过这个指针变量访问到。
数组就是数组,其大小与元素的类型和个数有关。定义数组时必须指定某元素的类型和个数。数组可以存任何类型的数据,但是不能存函数。
3.1 以指针的形式访问和以下标的形式访问
3.1.1以指针的形式访问和以下标的形式访问指针
char *p = "abcdef";
这里定义了一个指针变量p,p本身在栈上占用4个byte,p里存储的是一块内存的首地址。这块内存在静态区,其空间大小为7个byte,这块内存也没有名字。对这块内存的访问完全是匿名的访问。如果我们要读取字符“e”,我们有两种方式:
1)以指针的形式:*(p+4)。先取出p里存储的地址值,假设为0x0000FF00,然后加上4个字符偏移量(偏移量4代表的是4个元素,而不是4个byte。刚好为char类型数据1个字符的大小就为1个byte,记住这个偏移量的单位是元素的个数而不是byte数),得到新的地址0x0000FF04,然后取出0x0000FF04地址上的值。
2)以下标的形式:p[4]。编译器总是把下标的形式的操作解析为以指针的形式的操作。p[4]这个操作会被解析成:先取出p里存储的地址值,然后加上中括号中4个元素的偏移量,计算出新的地址,然后从新的地址中取出值。也就是说以下标形式访问在本质上与以指针的形式访问没有区别,只是写法上不同罢了。
3.1.2以指针的形式访问和以下标的形式访问数组
char a[] = "123456";
这里定义了一个数组a,a拥有7个char类型的元素,其空间大小为7。数组a本身在栈上面。对a的元素的访问必须先根据数组的名字a找到数组首元素的首地址,然后根据偏移量找到相应的值。这是一种典型的“具名+匿名”访问。比如现在需要读取字符‘5’,我们有两种方式:
1)以指针的形式:*(a+4)。a这时候代表的是数组首元素的首地址,假设为0x0000FF00,然后加上4个字符的偏移量,得到新的地址0x0000FF04,最后取出新地址0x0000FF04上的值。
2)以下标的形式:a[4]。编译器总是把下标的形式的操作解析为以指针的形式的操作。a[4]这个操作会被解析成: a作为数组首元素的首地址,然后加上中括号中4个元素的偏移量,计算出新的地址,然后从新的地址中取出值。
由上面的分析不难看出,指针和数组根本就是两个不一样的东西。只是它们都可以“以指针形式”或“以下标形式”进行访问。一个是完全的匿名访问,一个是典型的具名+匿名访问。
提示:不到非不得已,尽量别使用printf函数,它会使你养成只看结果不问为什么的坏习惯,有时候可以通过watch窗口来查看。
3.2 a和&a的区别
int a[5] = {1,2,3,4,5};
int *ptr = (int *)(&a+1);
printf("%d,%d",*(a+1),*(ptr-1));
指针加减运算,注意:数组名什么时候表示整个数组的大小
3.3 指针和数组的定义与声明
3.3.1 定义为数组,声明为指针
文件1
char a[100];
文件2
extern char *a;
这里文件1定义了数组a,文件二中声明它为指针。这是错误的
这里说一下定义与声明的区别:(1)定义分配内存,而声明没有。(2)定义只能出现一次,而声明能出现多次。这里extern告诉编译器a这个名字已经在别的文件中被定义了,下面的代码使用的名字a是别的文件中定义的。再回顾到前面对于
3.3.2定义为指针,声明为数组
文件1
char *p="abcdef";
文件2
extern char p[];
3.4 指针和数组的对比
四. 指针数组和数组指针
4.1指针数组和数组指针的内存布局
指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身决定。它是“储存指针的数组”的简称。
数组指针:首先它是一个指针,它指向一个数组。在32位系统下永远是4个字节,它指向的数组占多少个字节是不知道的。它是“指向数组的指针”的简称。
下面哪一个是数组指针,哪一个有事指针数组?
int *p1[10];
int (*p2)[10];
“[]”的优先级高于“*”,p1先与[]结合,构成一个数组的定义,数组名为p1,int*修饰的是数组的内容,即数组的每个元素都是int类型的指针。那么我们知道这是一个数组,其包含10个指向int类型数据的指针,即指针数组。
“()”的优先级高于[],“*”号和p2构成一个指针的定义,指针变量为p2,int修饰的是数组的内容,即数组的每个元素。数组在这里并没有名字,是个匿名数组。那现在我们清楚p2是一个指针,它指向一个包含10个int类型数据的数组,即数组指针。
4.2 int (*)[10] p2
int (*) [10] p2;
int (*)[10]是指针类型,p2是指针变量。数组指针的原型的确如此,只不过为了方便与好看把指针变量p2前移了。私下里可以这么理解,但是编译器不会这么想。
4.3 再论a与&a之间的区别
自我调适理解。
int main()
{
char a[5] = {'A','B','C','D'};
char (*p3)[5] = &a;//p3是一个指针,指向数组a
char (*p4)[5] = a;//p4是数组指针,类型是char(*)[5],a是数组首地址,类型是char*,赋值出错
return 0;
}
int main()
{
char a[5] = {'A','B','C','D'}
char (*p3)[3] = &a;//char(*)[5]类型的值不能用于初始化char(*)[3]的值
char (*p4)[3] = a;//出错
return 0;
}
int main()
{
char a[5] = {'A','B','C','D'}
char (*p3)[10] = &a;char(*)[5]类型的值不能用于初始化char(*)[10]的值
char (*p4)[10] = a;//出错
return 0;
}
当理解完上面三段代码时,相信你对数组和指针的理解又更深了一步,我们又可以分别看看三段代码中p3+1和p4+1的值又是多少呢?
4.4 地址的强制转换
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;
}
ptr1[-1]被解析成*(ptr1-1),其值是0x4,这个好理解。
关键是*ptr2,要想知道*ptr2的值,这里需要引入大小端模式。如果不知道当前系统是什么模式,那就要想办法测试一下。这里介绍一种方法:
int checkSystem()
{
union check
{
int i ;
char ch;
}c;
c.i = 1;
return(c.ch==1);
}
如果当前系统这个函数返回0则为大端模式,如果返回1则为小端模式。
大端模式 ,*ptr2的值为0x100
小端模式, *ptr2的值为0x2000000
4.5 多维数组与多级指针
超过二维的数组和超过二级的指针其实并不多用。如果很好的理解了二级数组与二级指针,那么二维以上的也就不是什么问题了。所以这里重点讨论二维数组和二级指针。
4.5.1 二维数组
4.5.1.1 假象中的二维数组布局
4.5.1.2 现实中的二维数组布局
数组以下标的方式来访问其中的某个元素:a[i][j]。编译器总是将二维数组看成是一个一维数组,而一维数组的每一个元素又都是一个数组。a[3]这个一维数组的三个元素分别是a[0],a[1],a[2]。每个元素的大小为sizeof(a[0]),即
sizeof(char)*4。由此可以计算出a[0],a[1],a[2]三个元素的首地址分别为
a[0] &a[0],
a[1] &a[0]+1*sizeof(char)*4,
a[2] &a[0]+2*sizeof(char)*4。
亦即a[i]的首地址为&a[0]+i*sizeof(char)*j。
这时候再来考虑a[i]中的内容,就本例而言,a[i]内有4个char类型的元素,其每个元素的首地址分别为&a[i]
&a[i]
&a[i]+1*sizeof(char)
&a[i]+2*sizeof(char)
&a[i]+3*sizeof(char)
即a[i][j]的首地址为 &a[i]+j*sizeof(char)。
再把&a[i]的值用a表示,得到a[i][j]元素的首地址为:a+i*sizeof(char)*j+j*sizeof(char)。
同样,也可以换成以指针的形式表示:*(*a(a+i)+j)。
#include
int main ()
{
int a[3][2] = {(0,1),(2,3),(3,4)};
int *p;
p = a[0];
printf("%d",p[0]);
return 0;
}
打印出的结果是多少呢?如果你的答案是0的话,那么恭喜你,你答错了!!!
花括号里嵌套的是小括号,而不是花括号!这里花括号里嵌套了逗号表达式。这个赋值就相当于
int a[3][2]={1,3,5};
所以本栗的答案是1!!!
4.5.1.3&p[4][2] - &a[4][2]的值为多少?
再举一个栗子:
int a[5][5];
int (*p)[4];
p = a;
问&p[4][2] - &a[4][2]的值为多少?
当数组名a作为右值时,代表的是数组首元素的首地址,这里把数组a看做是包含了5个int类型元素的一维数组,里面再存储了一个一维数组。如此,则a在这里代表的是a[0]的首地址。a+1表示的是一维数组a的第二个元素。a[4]表示的是一维数组a的第5个元素,而这个元素里又存了一个一维数组。所以&a[4][2]表示的是
&a[0][0]+4*5*sizeof(int)+2*sizeof(int)。
根据定义,p是指向一个包含4个元素的数组的指针。也就是说p+1表示的是指针向后移动了一个“包含4个int类型元素的数组”。这里1的单位是p指向的空间,即4*sizeof(int)。所以p[4]表示的是&p[0]+4*4*sizeof(int)。由于p被初始化&a[0],那么&p[4][2]表示的是&a[0][0]+4*4*sizeof(int)+2*sizeof(int)。
重点就是明白数组指针p所指向的内存到底是什么。解决这类问题的最好办法就是画内存布局图。
4.6 数组参数与指针参数
参数分为形参和实参
形参:声明或定义函数时的参数
实参:在调函数时主调函数传递过来的实际值。
4.6.1 一维数组参数
void fun (char a[10])
{
int i = sizeof(a);
char c = a[3];
}
int main
{
char b[10] ="abcdefgh";
fun(b);
return 0;
}
看此时数组b是否真的传递到函数内部呢?如果真的传递进去了,那么i的值应该是10,但是我们测试后发现i的值为4,因为在C语言中,有这样一条规则:
C语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。
在C语言中,所以非数组形式的数据实参均以传值形式(对实参做一份拷贝并传给被调用的函数,函数不能修改作为实参的实际变量的值,而只能修改传递给它的那份拷贝)调用。
如果拷贝整个数组,空间和时间上开销都很大,效率较低。同样,函数的返回值不能是一个数组,而只能是是指针。
函数本身没有类型,只有函数的返回值有类型,
void fun (char *p)
{
int i = sizeof(p);
char c = p[3];//char c = *(p+3);
}
int main
{
char b[10] ="abcdefgh";
fun(b);
return 0;
}
实际传递的数组大小与函数形参指定的数组大小没有关系。
void fun (char a[1])
{
int i = sizeof(a);
char c = a[3];
}
int main
{
char b[100] ="abcdefgh";
fun(b);
return 0;
}
void fun (char a[0])
{
int i = sizeof(a);
char c = a[3];
}
int main
{
char b[100] ="abcdefgh";
fun(b);
return 0;
}
4.6.2 一级指针参数
4.6.2.1 能否把指针变量本身传递给一个函数
void fun (char *p)
{
int i = sizeof(p);
char c = p[3];//char c = *(p+3);
}
int main
{
char *p2 ="abcdefgh";
fun(p2);
return 0;
}
p2是main函数内的一个局部变量,它只在main函数内部有效。(main函数内的变量不是全局变量,而是局部变量,只不过它是生命周期和全局变量一样长。全局变量一定是定义在函数外部的),既然p2是局部变量,那么fun函数肯定无法使用p2的真身。对实参p2做一份拷贝(假设拷贝名为_p2)并传递给被调用的函数。那么传递到函数内部的就是_p2的而非p2本身。所以fun函数实际运行时,用到的都是_p2这个变量而非p2本身。
再举一个栗子
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的地址,是一个值,在函数内部,用钥匙(“*”)来开锁:*(&str),其值就是str。所以malloc分配的内存地址是真正赋值给了str本身。
拓展:看下面的两段代码,运行后会产生什么样的结果
char * GetMemory (void)
{
char p[] = "hello word";
return p;
}
int main()
{
char *str = NULL;
str = GetMemory();
printf(str);
return 0;
}
void GetMemory (char **p, int num)
{
*p = (char *)malloc(num*sizeof(char));
}
int main()
{
char *str = NULL;
GetMemory(str, 10);
strcpy(str,"hello");
return 0;
}
4.6.3 二维数组参数与二维指针参数
void fun(char a[3][4]);
void fun(char (*p)[4]);//括号不能省略,保证把p解析为一个指向包含4个char类型数据元素的数组(一维数组a[3])。若把括号去掉,声明“void fun(char *P[4])”可以改写成“void fun (char **p)”
void fun(char a[][4]);//为什么第二维的维数不可省略?
二维数组参数和二维指针参数的等效关系整理一下:
这里需要注意的是:C语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素的首地址的指针这条规则并不是递归的,也就是所只有一维数组才是如此,当数组超过一维时,将第一维改写为指向数组首元素首地址的指针之后,后面的维数也不可改写。比如:a[3][4][5]
4.7 函数指针
4.7.1 函数指针的定义
顾名思义,函数指针就是函数的指针,它是一个指针,指向一个函数。看栗子:
A),char *(*fun1)(char *p1,char *p2);
B),cahr * *fun2(char *p1, char *p2);
C),char * fun3 (char *p1, char *p2);
C):fun3是函数名,p1,p2是参数,其返回值类型为char*类型
B):与C)位移的区别就是返回值类型为char**,是一个二级指针
A):fun1是函数名吗?回忆一下数组指针
int (*p)[10] ;//int (*)[10] p
这里fun1不是函数名,而是一个指针变量, 它指向一个函数。这个函数有两个指针类型的参数,函数的返回值也是一个指针。
改写一下或许更加有利于理解:
char *(*)(char *p1, char *p2) fun1
4.7.2函数指针的使用
4.7.2.1 函数指针使用的栗子
#include
#include
char *fun(char *p1,char *p2)
{
int i = 0;
i = strcmp(p1,p2);
if(0==i)
{
return p1;
}
else
{
return p2;
}
}
int main()
{
char *(*pf)(char *p1,char *p2);
pf = &fun;
(*pf)("aa","bb");
return 0;
}
通过(*pf)取出存在这个地址上的函数,然后调用它
4.7.2.2 *(int*)&p
void Function()
{
}
int main ()
{
void (*p)();
*(int*)&p=(int)Function;
(*p)();
return 0;
}
void (*p)();
这行代码定义了一个指针变量p,p指向一个函数,这个函数的参数和返回值都是void。
&p是求指针变量p本身的地址,这是一个32位的二进制常数
(int*)&p表示将地址强制转换成指向int类型数据的指针。
(int)Function表示将函数的入口地址强制转换成int类型的数据
*(int*)&p=(int)Function;表示将函数的入口地址赋值给指针变量p。
函数指针和普通的指针没什么区别,只是指向的内容不同而已。
函数指针的好处在于,可以将实现同一功能的多个模块同一起来标识,这样一来更容易后期的维护,系统结构更加清晰。或者归纳为:便于分层设计,利于系统抽象,降低耦合度以及使接口与实现分开。
4.7.3(*(void (*) ())0) ()
void(*)(),可以明白这是一个函数指针类型。这个函数没有参数,也没有返回值。
(void(*)())0,这是将0强制转换成函数指针类型,0是一个地址,也就是说有一个函数存在首地址为的一段区域内。
(*(void(*)())0),这是取地址开始的一段内存里面的内容,其内容就是保存在首地址为的一段区域内的函数。
(*(void (*) ())0) (),这是函数调用
4.7.4 函数指针数组
把函数的地址存到一个数组中,这个数组就是函数指针数组。
char * (*pf[3])(char *p);
定义一个函数指针数组。这是一个数组,数组名为pf,数组内储存了3个指向函数的指针。这些指针指向一些返回值类型为指向字符的指针、参数为一个指向字符的指针的函数。
函数指针数组用途:转移表
4.7.5 函数指针数组的指针
指向函数指针数组的指针,这个指针指向一个数组,这个数组里面存的都是指向函数的指针。
char*(*(*pf)[3])(char *p);
pf是一个指针,这个指针指向一个包含了3个元素的数组,这个数组里存的是指向函数的指针;这些指针指向一些返回类型为指向字符的指针,参数为一个指向字符的指针的函数。