C语言05- 指针

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:&与*

  1. &:取值运算符(reference),是取变量的地址
  2. *:取内存运算符(dereference),是取地址对应的内存
  3. 上面两个运算符互为逆运算

13.1.3:*p容易混淆的地方

  1. *在指针定义的时候,是和类型结合(说明这个指针指向哪种类型)
  2. *在指针使用的时候,是取内存(解应用)

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个方面来理解:

  1. 它是一个变量,所以也占用一定的内存空间(在X86上占用4个字节,X64上占用8个字节)
  2. 它的值是一个内存地址。这个地址可以是其它变量的地址。
  3. 它的地址指向的内存空间具有确定的长度。这是指针与地址的本质区别。

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:指针加减运算

  1. 在指针声明的时候,最好将其初始化为NULL,否则指针将随机指向某个区域,访问没有初始化的指针,行为为未定义而为程序带来预想不到的结果;
  2. 指针释放之后,也应该将指针指向NULL,以防止野指针。因为指针所指向的内存虽然释放了,但是指针依然指向某一内存区域。
  3. 对于指针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:指针数组与数组指针--偏正关系

  1. int *a[10];//指针数组,存放指针的数组(运算符优先级[]优先)
  2. int (*a)[10];//数组指针,存放数组的指针,指向每行10个元素的一维数组的指针
  3. int a[10];//证书数组

13.4:常量指针与指针常量

包含const关键字声明指针的含义:

  1. const int *a;//指针常量,指针指向的变量不能改变值
  2. int const *a;//指针常量,与const int *a等价
  3. int * const a; //常量指针,指针本身不能改变值
  4. 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形式:

  1. 首先用typdef定义出函数指针的类型,然后,通过函数指针类型来定义函数指针
  2. 直接用函数的签名来定义函数指针
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);
}

注意:指针函数不能返回局部变量的指针(地址),只能返回堆上内存的地址,或者函数参数中的内存地址。因为局部变量存放在栈上,当函数运行结束后,局部变量就被销毁了,这个时候返回一个被销毁的变量的地址,调用者得到的就是一个野指针。

注意下面几个定义的区别:

  1. int f();//普通的函数
  2. int *fpi();//指针函数
  3. 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:指针注意事项

  1. 指针在声明的时候最好初始化。
  2. 指针的加减运算移动的是指针所指类型大小。
  3. 当用malloc或new为指针分配内存时应该判断内存分配是否成功,并对新分配的内存进行初始化。
  4. 如果指针指向的是一块动态分配的内存,那么指针在使用完后需要释放内存,做到谁分配谁释放的原则,防止内存泄漏。
  5. 指针在指向的动态内存释放后应该重新置为NULL,防止野指针。

野指针不是NULL指针,是指向“垃圾”内存的指针。野指针是很危险的,它可能会造成不该访问的数据或不该改的数据被访问或者篡改。在应用free或者delete释放了指针指向的内存之后,应该将指针重新初始化为NULL。这样可以防止野指针。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容