C语言-指针:
在已有的数据类型后面加上一个星号(char*、short*、int*…),这就是一个指针。
指针是一种新的数据类型,学习一个新的数据类型我们应该从:数据宽度、数据声明方式、数据赋值方式、数据运用操作这些方面来了解这个新类型。
指针的声明
char* a;
short* b;
int* c;
float* d;
double* e;
struct student* f;
char** a;
short*** b;
int**** c;
float***** d;
double****** e;
struct student******* f;
1、指针的标准写法:变量类型* 变量名
2、任何类型都可以带* 加上*以后就是新的类型
3、*可以是任意多个
? 指针的赋值
struct A{
int x;
int y;
int z;
};
void test(){
char* a;
short** b;
int*** c;
struct A* d;
a=(char*)1;
b=(short**)2;
c=(int***)3;
d=(struct A*)4;
}
1、在赋值的时候要在变量前面用小括号括起来,然后在括号内填入类型
2、类型在填写时我们的星号也要和声明时类型的星号一样
3、指针的宽度永远是4字节、无论类型是什么,无论有几个*
指针的+、-运算
?
++、–运算
void test(){
char* a;
short* b;
int* c;
char** d;
short** e;
int** f;
a=(char*)1;
b=(short*)2;
c=(int*)3;
d=(char**)4;
e=(short**)5;
f=(int**)6;
printf("%d,%d,%d,%d,%d,%d\n",a,b,c,d,e,f);
a++;
b++;
c++;
d++;
e++;
f++;
printf("%d,%d,%d,%d,%d,%d\n",a,b,c,d,e,f);
a--;
b--;
c--;
d--;
e--;
f--;
printf("%d,%d,%d,%d,%d,%d\n",a,b,c,d,e,f);
}
?
+、-运算
void test(){
char* a;
short* b;
int* c;
char** d;
short** e;
int** f;
a=(char*)1;
b=(short*)2;
c=(int*)3;
d=(char**)4;
e=(short**)5;
f=(int**)6;
printf("%d,%d,%d,%d,%d,%d\n",a,b,c,d,e,f);
a=a+5;
b=b+5;
c=c+5;
d=d+5;
e=e+5;
f=f+5;
printf("%d,%d,%d,%d,%d,%d\n",a,b,c,d,e,f);
a=a-5;
b=b-5;
c=c-5;
d=d-5;
e=e-5;
f=f-5;
printf("%d,%d,%d,%d,%d,%d\n",a,b,c,d,e,f);
}
1、指针类型的变量可以加、减一个整数,但是不能乘或者除
2、指针类型变量与其他整数相加或者相减时:
? 指针 + N = 指针 + N * (去掉一个*后类型的宽度)
? 指针 - N = 指针 - N * (去掉一个*后类型的宽度)
?
指针的加减
编译器限制了两个指针不能相加,但是可以相减
void test(){
int x,y,z,l;
char* a;
char* b;
char** e;
char** f;
short* c;
short* d;
short** g;
short** h;
a=(char*)1;
b=(char*) 2;
e=(char**) 3;
f=(char**) 7;
c=(short*) 5;
d=(short*) 9;
g=(short**) 7;
h=(short**) 19;
x=b-a;
y=f-e;
z=d-c;
l=h-g;
printf("%d,%d,%d,%d,",x,y,z,l);
}
1、两个指针进行相减时,先进行数字的相减,再将结果除以数据宽度(去*后)
2、相减只能是通类型的相减(int和char之间不能相减,星号数不同不能相减)
3、两指针相减后得到是int型的数据,不能用char*类型装
?
指针比较
指针是可以进行比较的
void test(){
char* a;
char* b;
if (a>b){
printf("1");
}
else{
printf("2");
}
}
不同类型的指针进行比较会出现警告,但是还是能正常运行(vc++2010)
struct A{
int x;
int y;
};
void test1(){
struct A** x;
x=(struct A**)100;
x++;
printf("%d\n",x);
x=x+2;
printf("%d\n",x);
x=x-3;
printf("%d\n",x);
}
void test2(){
struct A** x;
struct A** y;
int z;
x=(struct A**)200;
y=(struct A**)100;
z=x-y;
printf("%d\n",z);
}
void test3(){
struct A* x;
x=(struct A*)100;
x++;
printf("%d\n",x);
x=x+2;
printf("%d\n",x);
x=x-3;
printf("%d\n",x);
}
void test4(){
struct A* x;
struct A* y;
int z;
x=(struct A*)200;
y=(struct A*)100;
z=x-y;
printf("%d\n",z);
}
int main(){
test1();
test2();
test3();
test4();
return 0;
}
?
类型转换
主要是编译器设置,看编译器。
1、基本类型之间都可以转换,但是结构体和其它类型之间不可以转换
2、(int*)可以转换成(char *),指针之间是可以转换的,结构体的指针也是可以转换的
?
地址符号:&
&(地址符号):可以取任何一个变量的地址
&a的类型就是a的类型 + *
void test(){
char a=10;
short b=20;
}
void test(){
char a = 10;
short b =20;
int c =30;
char* pa = &a;
short* pb = &b;
int* pc = &c;
char** ppa = &pa;
short** ppb = &pb;
int** ppc =&pc;
}
每一个存储单元都对应了一个唯一的物理地址!!!
这里相当于有两个变量:地址 和 内存单元 我们将数据放入到内存单元中,而这个内存单元有其对应的唯一的地址。指针就是变量的地址!我们可以通过找到地址来获取对应的数据(内存单元信息)。
void test(){
int p = 10;
int******* p7;
int****** p6;
int***** p5;
int**** p4;
int*** p3;
int** p2;
int* p1;
p1 = &p;
p2 = &p1;
p3 = &p2;
p4 = &p3;
p5 = &p4;
p6 = &p5;
p7 = &p6;
}
void test(){
int x,y;
int* px;
int** px2;
int*** px3;
int**** px4;
int***** px5;
x = 10;
px = &x;
px2 = &px;
px3 = &px2;
px4 = &px3;
px5 = &px4;
y = *(*(*(*(*(px5)))))
}
? 指针的进一步探索:*x
char* x 对应的数据类型是 char
short* x 对应的数据类型是 short
int* * x 对应的数据类型是 int *
*x 的意思就是取x中的值
void function(){
int arr[5]={1,2,3,4,5};
for(int k=0;k<5;k++){
printf("%d\n",*(p+k));
}
}
2、通过char指针遍历数据
3、通过short指针遍历数据
4、通过int指针遍历数据
?
数组作为参数
? 数组作为参数传入函数中,实质上传入的是一个基址(数组中第一个元素的堆栈地址)。在函数内部调用传入的数组参数时,是通过偏移实现的。通过在第一个数组元素的地址上进行相加来偏移到后面的数组元素的位置,实现对元素的操作。
结合数组本身的特点,数组是一块连续分配内存的存储单元,所以存储的数据是连续存放的。我们在将数组作为参数传入函数时,为了提高效率并节省资源,所以我们只传入一个数组的基址地址,后续操作通过在基址地址上做偏移来实现。这样既节省了空间,也提高了效率。
不同类型的数组作为参数其实现本质是一样的,不同点在于偏移量的改变(char型,每次偏移都只偏移1位;short型则需偏移两位,int型偏移为四位)。
?
通过指针遍历数组
? 把数组的基址给到指针,然后通过指针的偏移来一个一个遍历数据。
当我用宽字节的指针遍历窄字节的数组时,我的偏移量会发生改变,会导致读取到的数据和预期不同。
如:我们设置的是char型的数组,我们用short指针进行遍历,我们一次会读取到两个字节的数据,然后两个字节拼凑后转换成十进制数。(char:01,02,03,04—>1,2,3,4)(short:0102,0304—>513,1027)
void test(){
char [100] = {
0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x07,0x09,
0x00,0x20,0x10,0x03,0x03,0x0c,0x00,0x00,0x44,0x00,
0x00,0x33,0x00,0x47,0x0c,0x0e,0x00,0x0d,0x00,0x11,
0x00,0x00,0x00,0x02,0x64,0x00,0x00,0x00,0xaa,0x00,
0x00,0x00,0x64,0x10,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x02,0x00,0x74,0x0f,0x41,0x00,0x00,0x00,
0x01,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x0a,0x00,
0x00,0x02,0x74,0x0f,0x41,0x00,0x06,0x08,0x00,0x00,
0x00,0x00,0x00,0x64,0x00,0x0f,0x00,0x00,0x0d,0x00,
0x00,0x00,0x23,0x00,0x00,0x64,0x00,0x00,0x64,0x00
}
}
------------------
#include <stdio.h>
char a[100] = {
0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x07,0x09,
0x00,0x20,0x10,0x03,0x03,0x0c,0x00,0x00,0x44,0x00,
0x00,0x33,0x00,0x47,0x0c,0x0e,0x00,0x0d,0x00,0x11,
0x00,0x00,0x00,0x02,0x64,0x00,0x00,0x00,0xaa,0x00,
0x00,0x00,0x64,0x10,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x02,0x00,0x74,0x0f,0x41,0x00,0x00,0x00,
0x01,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x0a,0x00,
0x00,0x02,0x74,0x0f,0x41,0x00,0x06,0x08,0x00,0x00,
0x00,0x00,0x00,0x64,0x00,0x0f,0x00,0x00,0x0d,0x00,
0x00,0x00,0x23,0x00,0x00,0x64,0x00,0x00,0x64,0x00
};
void find(){
char* q;
int* p;
int i;
int j;
char b[4];
q=&a[0];
for(i=0;i<97;i++){
for(j=0;j<4;j++){
b[j]=*(q+i+j);
}
p=(int*)&b[0];
if(*(p)==0x64){
printf("%x\n",p);
}
}
}
int main(){
find();
getchar();
}
? 指针数组
从数据角度来说,指针与普通的数据类型没有区别。指针也能存储任意数据,但是指针与普通数据类型还是有区别的。普通数据类型的数值无法直接赋值给指针类数据,如果需要赋值就需要用到强制转换。
指针的出现主要是为了存储地址,赋值时可以直接将变量的地址赋给指针。这样在编程时结合地址就能更方便的操作,而指针数组是一个连续地址连续分配的用于存储地址等变量的存储空间。
指针是一种变量,常用于存储数据地址,但它本身作为变量也有存储地址。
int* arr[5] = {0};
arr[0] = (int*)10;
arr[1] = (int*)11;
arr[2] = (int*)12;
arr[3] = (int*)13;
arr[4] = (int*)14;
? 结构体指针
结构体指针还是和指针的本质一样,指向某个变量的地址。
struct student
{
int a;
int b;
int c;
}
struct student a;
struct student* ps;
s.a = 10;
s.b = 20;
s.c = 30;
ps = &s;
printf("%d\n",ps->a);
ps->a = 11;
printf("%d\n",ps->a);
?
多级指针(指针的指针)
char* p1;
printf("%d\n",*p1);
所以读取一个指针的反汇编过程就是先读取变量p1的值,然后再通过*****再次读取。
char* p1;
printf("%d\n",*(p1+0));
这里的反汇编代码是和直接用星号是一样的。但是过程会不一样。
在有指针读取数据时进行加减的情况下,编译器会先读取p1所存储的值并跳转到新地址,然后用新地址进行运算,最终的计算结果就是最后跳转的地址。
char* p1;
printf("%d;%d\n",*(p1+0),p1[0]);
printf("%d;%d\n",*(p1+2),p1[2]);
这里可以看出指针其实还可以有多种表达形式,不同的表达形式对应的反汇编代码是一样的,所以编译器执行的操作也是一样的。
**p1 == (p1+0) == p1[0]
char** p2;
printf("%d\n",*p2);
printf("%d\n",*(*p2));
printf("%d\n;%d\n",*(*(p2+2)+3),p2[2][3]);
》》》printf(“%d\n”,*p2);
只有一个星号的时候,会发现即使是**char****类型,反汇编代码也依旧一样。
》》》printf(“%d\n”,*( *p2));
两个星号时,反汇编代码就发生了不同,多了一步读取为地址的操作。
》》》printf(“%d\n;%d\n”,*( *(p2+2)+3),p2[2] [3]);
在对于多级指针的运算特点进行分析:首先把p2作为一个地址把对应的数据读取出来,再进行+2运算,此时是char**类型的加法运算所以 +2 * 4;之后是char*类型的加法运算。
用数组也能够进行同样的操作。
char*** p3;
printf("%d\n",*(*(*p3)));
printf("%d\n;%d\n",*(*(*(p3+1)+2)+3),p3[1][2][3]);
这时候就能理解并总结出,读取一个n级指针中的数值,需要经过n次mov操作。
与p2原理一样。
?
数组指针
学习数组指针主要是与指针数组区别。数组指针是通过指针指向数组中的元素。
int (*a)[6];
a = (int (*)[6])10;
printf("%d",a);
a++;
printf("%d",a);
数组指针的宽度:数组指针、多级指针、结构体指针,这些其宽度都是4个字节,可用sizeof进行判定。
数组指针分配存储空间大小的判定和结构体的大小判定相似。上述声明的数组指针中,是int类型的且大小为6的数组,所以4*6=24。
int arr[16]={1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
int (*px)[3];
px = (int (*)[3])arr;
printf("%d,%d,%d,%d,%d",px,*px,*(*px),*px+1,*(*(px+1)+1));
编译器没有为px分配空间,px和* px所指向的是同一存储空间,但是*让其代表的意义不同。
px所指向的是整个数组指针,在运算过程中关于px的加减都会乘以数组指针的大小;* px只考虑指针类型。如上述所示:* (* (px+1)+1) —> 先进行px+1* 3* 4=px+12 、12+1* 4=16
即:在px存储的数组的首地址的基础上进行偏移16个字节的偏移。
?
多维数组指针
多维数组指针和一维数组指针没有太大的区别。理解了基本的一维数组的概念后在对多维指针数组的运算规则加以理解就好了。
char code[]={
0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x07,0x09,
0x00,0x20,0x10,0x03,0x03,0x0c,0x00,0x00,0x44,0x00,
0x00,0x33,0x00,0x47,0x0c,0x0e,0x00,0x0d,0x00,0x11,
0x00,0x00,0x00,0x02,0x64,0x00,0x00,0x00,0xaa,0x00,
0x00,0x00,0x64,0x10,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x02,0x00,0x74,0x0f,0x41,0x00,0x00,0x00,
0x01,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x0a,0x00,
0x00,0x02,0x74,0x0f,0x41,0x00,0x06,0x08,0x00,0x00,
0x00,0x00,0x00,0x64,0x00,0x0f,0x00,0x00,0x0d,0x00,
0x00,0x00,0x23,0x00,0x00,0x64,0x00,0x00,0x64,0x00
};
int (*px)[5];
px = (int (*)[5])code;
printf("%x\n",*(*(px+2)+2));
#把数组code的首地址存入数组指针px中
char (*py)[2][3];
py = (char (*)[2][3])code;
printf("%x\n",*(*(*(px+2)+3)+4));
int (*pz)[2][3][4];
py = (int (*)[2][3][4])code;
printf("%x\n",*(*(*(*(px+2)+2)+2)+2));
数组指针不一定非要指向数组,指针的本质就是指向地址。在使用指针的时候也可以任意指向更灵活的使用指针。在使用不同类型的数组指针指向数组时,注意指针的类型宽度,这会决定在对指针指向地址偏移后取该地址下的几个字节的数据。
? 函数指针
int Function(int x,int y){
return x+y;
};
int (*px)(char,int);
px =Function;
int x=px(1,2);
函数指针的宽度为4,因为函数内部的变量可以任意类型任意数量,所以函数的大小不能确定,以至于函数指针不能进行++、+、–、-这些操作。能进行的就是比较操作。
函数指针的意义不在于将指针指向函数地址,然后使用指针调用函数的便捷。函数指针指向的可以是数据区也可以是代码区,又因为函数指针的类型,所以用函数指针指向数据区的具有函数功能的硬编码数据也是可以执行的。这也就是壳。函数指针会在加载dll中非常有用。
#include <stdio.h>
#include <string.h>
unsigned char fun[]={
0x55,
0x8B,0xEC,
0x81,0xEC,0xC0,0x00,0x00,0x00,
0x53,
0x56,
0x57,
0x8D,0xBD,0x40,0xFF,0xFF,0xFF,
0xB9,0x30,0x00,0x00,0x00,
0xB8,0xCC,0xCC,0xCC,0xCC,
0xF3,0xAB,
0x8B,0x45,0x08,
0x03,0x45,0x0C,
0x5F,
0x5E,
0x5B,
0x8B,0xE5,
0x5D,
0xC3
};
int main(){
int x;
int (*PFun)(int,int);
PFun = (int (__cdecl *)(int,int))&fun;
x = PFun(1,2);
printf("%d\n",x);
getchar();
}
上述代码就是将函数隐藏在数据区,因为C++2010编译器的限制,所以没能成功,显示出现了违规行为。但是从反汇编代码上看,是完全没有问题的。算是一个函数指针的小应用。
|