C语言05- 指针
13:指针
指针是C语言中的精华,也是C语言程序的重点和难点。
13.1:指针定义与使用
指针其实就是一个变量,和其他类型的变量一样。与其他变量的不同就在于它的值是一个内存地址,指向内存的某一个地方。
指针是一种存放另一个变量的地址的变量。
为什么指针在x86占4个字节,x64占8个字节?x86支持32位寻址(x86系统对32位系统定义,在32位系统中,cpu、寄存器一次最多处理的最大整数是4个字节32位,而且地址总线也是32位),因此可以支持最大2^32=4GB的虚拟内存空间;x64也是如此,一次最大处理为8个字节64位。
//指针的定义方式为:
//类型名 *指针名;
//比如:
char *pch;
指针声明
char *pch;
int *pi;
float *pf;
double *pd;
//那么如何取得一个变量的地址呢?可以在变量前面加&运算符。比如:
char c = ‘c’;
//那么c在内存中的地址为:&c。
void main(void) {
char c=’a’;
char *pch = &c;
printf(“%p,%p\n”, pch, &c);
}
//所以,可以给字符指针做如下赋值:
char c = ‘c’;
char *pch = &c;
//这个时候,pch的值就是c的地址了。如果要通过指针访问对应的变量的值,可以使用*运算符。比如:
char c = ‘c’;
char *pch = &c;
printf(“c=%c\n”, *pch);
13.1.1:解引用
*解引用:通过指针(存放的内存地址),找到相应的内存和里面存放的数据。
char c=‘a’;
char *s = “helloworld”;//此处的*s是用于定义指针s
char *p = &c;//此处的*p是用于定义指针p,实际上是(char*) p=&c
*p += 1;//此处的*p是解引用(即c)
printf(“%c\n”, *p);//此处的*p是解引用
void func(char *p) {}//此处*p是定义指针
func(&c);//传指针
func(p);//传指针
13.1.2:&与*
- &:取值运算符(reference),是取变量的地址
- *:取内存运算符(dereference),是取地址对应的内存
- 上面两个运算符互为逆运算
13.1.3:*p容易混淆的地方
- *在指针定义的时候,是和类型结合(说明这个指针指向哪种类型)
- *在指针使用的时候,是取内存(解应用)
13.1.4:指针类型与互相转换,sizeof(p),sizeof(*p)
指针的类型可以有如下几种:
char *p;//指向内存单元1个字节
short *p;//指向内存单元2个字节
int *p;//指向内存单元4个字节
float *p;//指向内存单元4个字节
double *p;//指向内存单元8个字节
//指针作为变量,存放在内存中,占有内存空间,因此也是有长度的:
sizeof(p) = 4 or 8 //计算指针的长度,在x86 4字节,x64平台64位程序中8个字节
sizeof(*p)//计算指针对应类型的长度
例子:在X86平台计算下面的值
char c = 'a';
char *p1=&c;
int x=10;
int *p2=&x;
char buf[]="hello world";
char *str="hello world";
sizeof(p1)=4//指针的长度
sizeof(p2)=4//指针的长度
sizeof(*p1)=1//指针对应类型char的长度
sizeof(*p2)=4//指针对应类型int的长度
sizeof(buf)=12//buf是数组,sizeof计算的是数组的长度,该数组通过"hello world"初始化,包含最后的'\0',共12个字节
sizeof(str)=4//指针的长度
sizeof(*buf)=1//指针对应类型char的长度
sizeof(*str)=1//指针对应类型char的长度
typedef struct _S {
int a;
char c;
}S, *PS;
S s;
S *ps=&s;
sizeof(ps)=4//指针的长度
sizeof(*ps)=8//指针对应类型S的长度
指针的类型可以互相转换:
//下面的代码将字符类型的指针转化为int*类型指针,p所指的内存宽度由1个字节变为了4个字节
char c;
int *p = (int *) &c;
int a;
int *p1 = &a;
char *p2 = (char *)p1;
//下面的代码把int*类型指针转化为字符类型指针,内存宽度p2所指的内存宽度由p1的4个字节变为了1个字节。
int a = 0x12345678;
char *p1 = (char *)&a;
int *p2 = &a;
printf("*p1=0x%x,*p2=0x%x\n", *p1,*p2);
指针含义可以分为3个方面来理解:
- 它是一个变量,所以也占用一定的内存空间(在X86上占用4个字节,X64上占用8个字节)
- 它的值是一个内存地址。这个地址可以是其它变量的地址。
- 它的地址指向的内存空间具有确定的长度。这是指针与地址的本质区别。
13.1.5:void* 类型指针
void *类型指针,其他类型指针隐式转换成该类型,不能直接来取值,必须先转换为特定类型再做取值
13.1.6:判断系统是低位优先还是高位优先
bool is_little_endian() {
int x = 0x1;
char *c = (char *)&x;
if (*c == 1) {//强转
return true;
} else {
return false;
}
}
int main(void) {
if (is_little_endian()) {
printf("little_endian");
} else {
printf("big_endian");
}
return 0;
}
13.2:指针加减运算
- 在指针声明的时候,最好将其初始化为NULL,否则指针将随机指向某个区域,访问没有初始化的指针,行为为未定义而为程序带来预想不到的结果;
- 指针释放之后,也应该将指针指向NULL,以防止野指针。因为指针所指向的内存虽然释放了,但是指针依然指向某一内存区域。
- 对于指针p,指针的加法运算p = p + n中,p向前移动的位置不是n个字节,而是n * sizeof(*p)个字节,指针的减法运算与此类似。
//有关于*p++,(*p)++,++*p,++(*p),*++p;的区别
int main(int argc, char* argv[]) {
int a=1;
int *p=&a;
int res=0;
//一,*p++
res = *p++;
printf("res:%d,p:%p,&a:%p\n",res,p,&a);
//res:1,p:0x7ffeef0a5920,&a:0x7ffeef0a591c
//*p++,由于单目运算符是右结合律,所以p先与++结合,即等价于:*(p++)
//由于p++在表达式里是先取值,再加1,因此等价于:res = *p; p= p+1;这里的1是1个单位长度,这里由于p是int; 所以,是4个字节的长度
//二,(*p)++
a=1;
p = &a;
res = (*p)++;
printf("res:%d,p:%p,&a:%p,a:%d\n",res,p,&a,a);
//res:1,p:0x7ffee049d91c,&a:0x7ffee049d91c,a:2
//这里(*p)++等价于先将*p的值给了res,然后,再将(*p)进行加1
//即等价于:
//res = *p;
//*p = *p+1,因为*p即为a,所以也就是a=a+1,a的值将变为2
//三,++(*p)
a=1;
p = &a;
res= ++(*p);
printf("res:%d,p:%p,&a:%p\n",res,p,&a);
//res:2,p:0x7ffee44f591c,&a:0x7ffee44f591c
//++(*p),也就是先把*p对应的值加1,再把加1后的*p的值传给res
//也就是等价于:
//*p=*p+1,因为*p就是a,所以也就是a=a+1
//res = *p;
//四,*++p
a=1;
p=&a;
res = *++p;
printf("res:%d,p:%p,&a:%p\n",res,p,&a);
//res:-520980177,p:0x7ffee0f27920,&a:0x7ffee0f2791c
//*++p,先将p的地址加1,然后再将p的值给res,即:
//p = p+1;
//res = *p;等到的res值是不固定的,取决于指向的内存
//五,++*p
a=1;
p=&a;
res = ++*p;
printf("res:%d,p:%p,&a:%p\n",res,p,&a);
//res:2,p:0x7ffeef2ed91c,&a:0x7ffeef2ed91c
//++*p等价于++(*p),参见上面的三
return 0;
}
13.3:指针与数组
数组名所代表的值就是数组的首地址,一旦定义了数组之后,数组名所代表的值就不能再改变。
//数组名就是一个常量指针
int a[10];
//a就是一个常量指针(int *const a)不能再用其它的值赋值给a。因为a是常量。
/*
数组名虽然代表了数组的首地址虽然a与&a值一样,都是数组的首地址,但是,a与&a的含义并不一样。
对于一维数组来说:
int a[10];
&a+1中的1代表的是整个数组的长度10*sizeof(int);
a+1中的1代表的是一个元素的长度sizeof(int)。
&a[0]+1中的1也代表的是一个元素的长度。
对于多维数组来说:
int a[5][10];
a和&a都是数组a[5][10]的首地址。那么它们有什么不同呢?实际上,它们代表的类型不同。a是int a[10]的类型,而&a则是a[5][10]的类型。大家知道,指针运算中的“1”代表的是指针类型的长度。所以a+1和&a+1中的1代表的长度分别为a的类型a[10]即sizeof (int) * 10 和&a的类型a[5][10]即sizeof (int)*10*5。所以a 的首地址为1310392,那么a + 1和&a + 1的地址为:
a + 1 = 1310392 + sizeof(int) * 10 = 1310392 + 4 * 10 = 1310432
&a + 1 = 1310392 + sizeof(int) * 10 * 5 = 1310392 + 4 * 10 * 5 = 1310592
更抽象点的说,如果定义一个多维数组int a[M1][M2][…][Mn],那么a + 1 = a首地址+M2*M3*…*Mn *sizeof(int);而&a + 1 = a首地址 + M1*M2*…*Mn*sizeof (int)。
&a[0][0]:就是数组第一个元素的地址,它的内存字节是4个字节。因此&a[0][0]+1,此处的1代表的长度是4个字节。
*/
数组作为参数在函数里退化为指针
int a[10]//sizeof(a)=40,a代表数组
void func(int a[], int n) {
//此时数组作为参数退化为指针,所以sizeof(a)=4
int b[10];
printf("a:%d, b:%d", sizeof(a), sizeof(b));//4,40
}
13.3.1:指针数组与数组指针--偏正关系
- int *a[10];//指针数组,存放指针的数组(运算符优先级[]优先)
- int (*a)[10];//数组指针,存放数组的指针,指向每行10个元素的一维数组的指针
- int a[10];//证书数组
13.4:常量指针与指针常量
包含const关键字声明指针的含义:
- const int *a;//指针常量,指针指向的变量不能改变值
- int const *a;//指针常量,与const int *a等价
- int * const a; //常量指针,指针本身不能改变值
- const int *const a;//常量指针与指针常量
区别技巧:把一个声明从右往左读
char *const p; (我们把"*"读成pointer to)
p is a const point to char
const char *p
p is a point to const char
13.5:函数指针与指针函数
13.5.1:函数指针
函数指针:指向函数的指针,这个指针变量存放的是函数的地址
函数名,就是函数的首地址。定义函数指针有2形式:
- 首先用typdef定义出函数指针的类型,然后,通过函数指针类型来定义函数指针
- 直接用函数的签名来定义函数指针
void print_int(int x) {
printf("hello, %d\n", x);
}
typedef void (*F)(int x);//此处定义了一个函数指针类型F
int main(void) {
int a=100;
void (*f1)(int x);
f1=print_int;//f1是指针定义出来的函数指针,把函数print_int赋值给f1
f1(a);
F f2=print_int; // f2是通过函数指针类型F定义出来的函数指针,把print_int赋值给f2。
f2(a);
print_int(a);
return 0;
}
13.5.2:指针函数
指针函数:返回指针的函数。
将get_memory()设置成为一个返回指针的函数:
//char *get_memory();
int main(void) {
char *p = NULL;//p是指针,做为实参,初始值为NULL
p=get_memory();//通过该函数,为p分配一块内存。如何定义get_memory函数?
strcpy_s(p, 100,”hello world”);
printf(“%s\n”, p);
free(p);
p=NULL;
return 0;
}
char *get_memory() {
return (char *)malloc(100);
}
注意:指针函数不能返回局部变量的指针(地址),只能返回堆上内存的地址,或者函数参数中的内存地址。因为局部变量存放在栈上,当函数运行结束后,局部变量就被销毁了,这个时候返回一个被销毁的变量的地址,调用者得到的就是一个野指针。
注意下面几个定义的区别:
- int f();//普通的函数
- int *fpi();//指针函数
- int (*pfi)();//函数指针
13.5.3:不能返回局部变量的指针或引用,只能返回值
函数一定不要返回局部变量的指针或者引用。
//如下面的代码:
char \*func(void) {
char c = ‘A’;
char \*p = &c;
return p;
}
char &func(void) {
char c='A';
return c;
}
//在上面func函数中,我们将局部变量c的地址当做一个指针返回,那么在main函数中,我们是不能够再次使用或者访问这个指针所指的内存的。因为局部变量c的生命周期只存在于函数func运行期间。一旦func结束运行之后,那么c就被销毁了,c的地址就是一个无效的内存地址,因此,当在main函数中执行了:
pc=func();
//pc指向的内存是无效的内存,因此pc是一个野指针,试图访问一个野指针,其后果是未定义的,程序有可能崩溃,有可能访问的是垃圾值。
int main(void) {
char * pc = NULL;
pc = func();
printf(“%c”, *pc);
return 0 ;
}
13.6:二级指针
二级指针:指向指针的指针,即该指针的值是另外一个一级指针的地址。
char c;
char *pch = &c;//pch为一级指针
char **ppch = &pch;//ppch为二级指针,存放这一级指针的地址
printf(“%c”, **ppch);
printf(“%p,%p,%p”, pch, ppch, *ppch);
13.7:指针注意事项
- 指针在声明的时候最好初始化。
- 指针的加减运算移动的是指针所指类型大小。
- 当用malloc或new为指针分配内存时应该判断内存分配是否成功,并对新分配的内存进行初始化。
- 如果指针指向的是一块动态分配的内存,那么指针在使用完后需要释放内存,做到谁分配谁释放的原则,防止内存泄漏。
- 指针在指向的动态内存释放后应该重新置为NULL,防止野指针。
野指针不是NULL指针,是指向“垃圾”内存的指针。野指针是很危险的,它可能会造成不该访问的数据或不该改的数据被访问或者篡改。在应用free或者delete释放了指针指向的内存之后,应该将指针重新初始化为NULL。这样可以防止野指针。