指针

指针

指针的定义
指针是一种特殊的数据类型,用于存储内存地址。指针可以指向任何数据类型(如整数、字符、数组、结构体等),并允许对该内存地址进行间接访问,从而操作或获取存储在该地址处的数据。按变量地址存取变量值的方式称为"直接访问",如printf("%d",i);、scanf("%d",&i);等,另一种存取变量值的方式称为"简介访问",即将变量i的地址存放到另一个变量中(指针)。 指针变量的定义格式如下:
基本类型 *指针变量名
举例如下:
int *ptr; // 声明一个指向整数类型的指针变量
char *ptr_char; // 声明一个指向字符类型的指针变量
float *ptr_float; // 声明一个指向浮点数类型的指针变量

int num = 10;
int *ptr_num = # // 声明并初始化一个指向整数 num 的指针

ptr = NULL; // 将指针 ptr 初始化为 NULL
在C语言中,指针和指针变量是密切相关但又不同的概念:
  1. 指针(Pointer)
    • 指针是一种特殊的数据类型,用于存储内存地址。
    • 指针本身并不存储实际的数据,而是存储一个地址,该地址指向内存中的某个位置,该位置存储着真正的数据。
    • 通过指针可以实现对所指向内存位置的间接访问,可以读取或修改该位置存储的数据。
  2. 指针变量(Pointer Variable)
    • 指针变量是声明为指针类型的变量,用于存储指针。
    • 它是存储指针值的具体容器,即指针变量中存储的是一个内存地址,这个地址指向某个数据的存储位置。
    • 指针变量本身也占用内存空间,通常是根据指针类型的大小来确定的。
总的来说,指针是一种抽象的概念,表示内存中的某个位置,而指针变量则是具体的存储指针值的变量。指针变量用于存储指针,使得我们能够在程序中方便地使用指针进行内存操作。 在32位的系统内指针的寻址范围是4字节,对于64位的系统,指针的寻址范围是8字节sizeof(i_pointer)=8
取地址操作符和取值操作符(指针本质)
取地址操作符位&,也称引用,通过该操作符可以获取到一个变量的地址值;取值操作符为*,也称解引用,通过该操作符可以得到地址值对应的数据。
#include <stdio.h>

int main(){
    int i=5;
    int *p=&i;
    printf("i=%d\n",i); //直接访问 
    printf("*p=%d\n",*p); //间接访问 
    return 0;
}  
读者需要注意: 1、指针变量前面的"*"表示该变量为指针型变量,而指针变量名是"p",不是"*p"。 2、在定义指针变量时必须指定其类型,只有整型变量的地址才能放到指向整型变量的指针变量中。
#include <stdio.h>

int main(){
    float a;
    int *pointer_1;
    pointer_1=&a;
    printf("%c",pointer_1); 
    return 0;
} 
上述例子会提示错误:[Error] cannot convert 'float' to 'int' in assignment(无法将类型转换) &*pointer_1语句的含义:"&"和"*"两个运算符的优先级别相同,但是要按照自右向左的方向结合,因此&*pointer_1与&a相同,都表示变量a的地址。 *&a语句的含义:首先进行取地址(&)运算,再进行(*)运算,*&a和*pointer_1作用一样。 声明多个指针需要养成好习惯
int *a,*b,*c
指针的传递
王道的龙老师总结出来,指针的使用场景通常只有两个,即传递和偏移,读者应时刻记住只有在这两种场景下使用指针。 下面的例子使用自定的change方法修改变量i的值
#include <stdio.h>

void change(int j)
{
    j=5;    
}
int main(){
    int i=10;
    printf("before change i=%d\n",i);
    change(i);
    printf("after change i=%d\n",i);
    return 0;
} 
通过编译可以发现变量i的值并没有发生改变 当函数change()被调用时,参数j是按值传递的,这意味着传递给change()函数的是变量i的值的拷贝,而不是i本身的内存地址。因此,change()函数内部对参数j的任何修改都只会影响到j的副本,而不会影响到main()函数中的变量i。 可以通过以下方式来理解为什么变量i的值不会发生改变:
  1. 内存分配:在函数调用时,会为参数j分配内存空间,并将变量i的值复制到这个新的内存空间中。这个新的内存空间是change()函数的局部变量,与main()函数中的变量i不同。

  2. 修改参数:在change()函数中,虽然对参数j的值进行了修改,但实际上是修改了change()函数内部局部变量的值,而不是main()函数中的变量i的值。这是因为参数j只是main()函数中变量i的一个拷贝,对其进行的修改不会影响到原始的变量i

  3. 栈帧:在函数调用时,每个函数都会有自己的栈帧,用于存储局部变量、参数等信息。当函数返回时,栈帧被销毁,其中的局部变量也随之消失。因此,change()函数中对参数j的修改只在函数执行期间有效,函数返回后,其影响范围就结束了。

综上所述,虽然change()函数内部对参数j的值进行了修改,但这个修改只在函数内部有效,不会影响到main()函数中的变量i。 对代码进行修改,实现对变量i数值的修改

#include <stdio.h>

void change(int *j)
{
    *j=5; //间接访问得到变量i的值 
}
int main(){
    int i=10;
    printf("before change i=%d\n",i);
    change(&i);
    printf("after change i=%d\n",i);
    return 0;
} 
执行程序可以发现变量i的值确实发生了变化,这是因为将变量i的地址传递给change函数时,实际效果是j=&i,依然是值传递,但是此时j是指针变量,内存存储的是变量i的地址,所以通过*j间接访问到了与变量i相同的区域,但是变量j的地址和变量i的地址依然是不相同的。
指针的偏移
前面介绍了指针的传递,指针即地址,指针的另一个场景就是对其进行加减(偏移)
#include <stdio.h>
#define N 5
int main() {
    int a[N]={1,2,3,4,5};
    int *p;
    int i;
    p=a; //将数组a的首地址传递给指针变量p,需保证等号两边的数值类型一样
    for(i=0;i<N;i++)
    {
        printf("%3d",*(p+i)); //正序输出
    }
    printf("\n\n");
    p=&a[4]; //让p指针指向数组a的最后一个元素地址
    for(i=0;i<N;i++)
    {
        printf("%3d",*(p-i)); //倒序输出
    }
    return 0;
}
上述代码可以实现数组的正序和倒序输出,编译器在编译时,数组取下标的操作正是转换为指针的偏移来完成的。偏移的长度是其基类型的长度,也就是偏移sizeof(int),这样通过*(p+1)就可以得到元素a[1]。
指针和一维数组
一维数组在函数调用进行传递时,数组的长度子函数无法识别,这是由于一维数组名中存储的是数组的首地址。
#include <stdio.h>
#include <string.h>

void change(char *d)
{
    *d='H';
    d[1]='E';
    *(d+2)='L';
}

int main() {
    char c[10]="hello";
    change(c);
    puts(c);
    return 0;
}
数组名c存储了一个起始地址(可在clion内存视图内查看),子函数在进行调用时起始是对指针指向的初始地址进行修改,子函数内定义了不同修改的方法,第一个方法为指针法,通过下标获取数组元素并进行修改的方法称为下标法。
指针与动态内存申请
在前面的学习中,每次使用数组都需要定义它的长度,其实是因为定义的整型、浮点型、字符型变量和数组变量都在栈空间中,而栈空间的大小在编译时是确定的,如果使用的空间大小不确定,那么就要使用堆空间。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main() {
    int i;
    char *p;
    scanf("%d",&i);
    p=(char*) malloc(i); //请求内存空间,并强制转换类型
    strcpy(p,"malloc susses");
    puts(p);
    free(p);
    return 0;
}
malloc函数返回值是void*类型的指针,它只能用来存储地址而不能进行偏移,由于malloc函数并不知道用户申请的空间用来存放什么类型的数据,所以用户在确定存放的数据类型后,都需要将void*类型进行转换,上述代码用于存放字符,所以强制转换为char*类型。 在使用指针时,需要注意指针本身的大小和其指向空间的大小,这是为了确保程序的正确性和稳定性。下面是一些需要注意的原因:
  1. 内存越界访问:如果指针本身的大小不足以容纳其指向的内存空间的大小,那么在使用指针进行间接访问时,可能会导致内存越界访问。这种情况会导致未定义的行为,可能会破坏其他数据或导致程序崩溃。

  2. 缓冲区溢出:当指针指向的内存空间大小小于实际要存储的数据大小时,如果不加以检查就进行写入操作,可能会导致缓冲区溢出。这会破坏程序的内存布局,可能会影响到程序的其他部分,甚至导致安全漏洞。

  3. 指针算术运算:在进行指针算术运算时,指针的大小和指向空间的大小决定了指针加减操作的有效范围。如果指针的大小与指向空间的大小不匹配,可能会导致算术运算产生错误的结果。

  4. 类型安全性:指针本身的大小通常取决于其所指向的数据类型。因此,如果将指针用于不兼容的数据类型,可能会导致类型错误或数据解释错误,从而引发程序错误。

栈空间与堆空间的差异
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

char *print_stack()
{
    char c[17]="I am print_stack";
    puts(c);
    return c;
}

char *print_malloc()
{
    char *p;
    p=(char*) malloc(20);
    strcpy(p,"I am print_malloc");
    puts(p);
    return p;
}

int main() {
    char *p;
    p=print_stack();
    printf("p=%s\n",p);
    p=print_malloc();
    puts(p);
    //printf("p=%s\n",p);
    return 0;
    return 0;
}

上述代码执行的结果为

I am print_stack
p=(null)
I am print_malloc
I am print_malloc
第二次打印出现异常的原因是print_stack()函数在执行完成后,栈空间会被释放,字符数组c原本空间会分配给其他函数使用,所以在打印时无法获取原本栈空间的数据,而print_malloc()函数的字符串存放在堆空间中,只有执行free操作才会释放。 在C语言中,堆空间和栈空间是两种不同的内存管理方式,它们用于存储程序运行时所需的数据和变量,但它们有着不同的特点和用途。
  1. 栈空间(Stack Space)
    • 栈空间是由操作系统自动分配和管理的,用于存储函数的局部变量、函数的参数值以及函数调用过程中的一些临时数据。
    • 栈空间的大小在程序运行前就已经确定,并且是有限的,通常比较小。这是因为栈空间的分配是静态的,由操作系统在程序运行时确定。
    • 栈空间的分配和释放速度比较快,因为它采用了一种简单高效的先进后出(LIFO)的数据结构。
    • 栈空间的生命周期与所在函数的执行周期相关联,函数执行结束后,栈上的内存将会被自动释放。
  2. 堆空间(Heap Space)
    • 堆空间是由程序员手动分配和释放的,用于存储程序运行时动态分配的内存,如使用malloc()calloc()realloc()等函数分配的内存块。
    • 堆空间的大小不固定,可以根据程序的需要动态地进行分配和释放。堆空间的大小受到系统内存的限制,通常比栈空间大得多。
    • 堆空间的分配和释放速度较慢,因为它涉及到更复杂的内存管理和分配算法,如内存碎片整理等。
    • 堆空间的生命周期不受限于函数的执行周期,需要手动释放分配的内存,否则可能会导致内存泄漏。
总的来说,栈空间适合存储局部变量和临时数据,生命周期短暂且大小固定;而堆空间适合存储动态分配的数据,生命周期较长且大小不确定。程序员需要根据具体的需求选择合适的内存管理方式,以确保程序的正确性和性能。