6.C语言指针

5 阅读12分钟

指针

实现变量值的交换

通过函数传参处理交换
#include <stdio.h>

void swap(int a, int b){
    a = a + b;
    b = a - b;
    a = a - b;
    printf("swap里:a=%d, b=%d, &a=%p, &b=%p\n", a, b, &a, &b);
}

int main(){
    int a = 10;
    int b = 20;
    swap(a, b);
    printf("main里:a=%d, b=%d, &a=%p, &b=%p\n", a, b, &a, &b);
}

执行结果:

PS C:\Users\86176\Desktop\嵌入式\c语言\ccode\class5> gcc .\t1.c -o t1.exe
PS C:\Users\86176\Desktop\嵌入式\c语言\ccode\class5> .\t1.exe
swap里:a=20, b=10, &a=000000CAE35FFBC0, &b=000000CAE35FFBC8
main里:a=10, b=20, &a=000000CAE35FFBFC, &b=000000CAE35FFBF8

❌️上面方法是不行的,因为函数传参实质是值copy,函数中的a b 与 main 里的 a b 已经不是同一个了。这时候就要用到指针

指针是什么

int a;  // 定义了一个int类型的变量a。实际上是被系统分配了一个4字节的的内存单元
a = 100;  // 将数值100写入到a变量所关联的内存单元中
int b = a;  // 从a变量所关联的内存单元中取出数值,写入到b所关联的内存单元中

C语言中,任何一个变量,都是有两层含义的:

  • 代表该变量的存储单元(地址) 左值(lvalue)
  • 代表该变量的存储单元中的数据(数值) 右值(rvalue)

我们对变量的访问无非就是两种情况:

  • 把一个数值写入到变量名关联的内存单元中(write)
  • 从变量名关联的内存单元中读出数值(read)

系统将变量名与变量的内存空间的地址进行了关联,访问变量数据时,系统实际上是通过变量的内存地址进行的访问。

可不可以直接通过地址来访问变量? ---> 指针。

指针的概念

地址:系统把内存以一个字节为单位划分成很多份并进行编号,这个编号就是内存地址。C语言中,可以认为指针就是一个有类型的地址。一个变量的首地址,成为该变量的“指针”。它标志这该变量的内容从哪里开始的。

  • 指针变量:

    • 指针变量也是个变量,但是它是用来保存一个对象的地址的。
  • 指针变量的定义:

    语法:类型说明符 * 指针变量名{=初始化};

    • 类型说明符:该指针所指向的那个内存空间的类型
      int a = 10;
      int * p;  // 定义空指针
      p = &a;
      int * x = &a;  // 定义一个指向变量a地址的指针
      // 称之为:指针x指向了变量a
      

      注意:32位处理器中地址都是32位,所以指针变量被分配的内存也是32位(4字节),如果是64位处理器中,指针变量被分配到的内存是64位(8字节)。void * 被称为空指针,也是万能指针。

    • 指针类型的作用:
      • 指针类型是:在指针变量与整数进行加减时,其实际地址与整数之间变化的倍率关系,从示例可以看出int指针+1后,地址移动了4字节,char指针+1后,地址移动了1字节
      #include <stdio.h>
      
      int main(){
          int a = 10;
          char b = 'a';
      
          int * pa = &a;
          char * pb = &b;
      
          printf("pa=%p\n", pa);
          printf("pa+1=%p\n", pa+1);
          printf("pb=%p\n", pb);
          printf("pb+1=%p\n", pb+1);
      
          return 0;
      }
      
      // 执行结果:
      pa=00000032D27FFC5C
      pa+1=00000032D27FFC60
      pb=00000032D27FFC5B
      pb+1=00000032D27FFC5C
      
      • 确定指针在运行加减运算的时候的步长是多少
      • 确定指针在对空间操作时,每次操作的空间是多大。
    • 如何获取地址
      • 通过取地址符:&
        • &对象名 表示获取该对象的地址
      • 有些对象名字本身就是他的地址
        • 如:数组、函数名

      注意:这种方式获取到的地址,是有类型的,类型是该空间的类型。 这里的地址类型要加*,如 int *, char *

    • 如何访问指针指向的空间的值

      需要用到指向运算符:解运算符:* 语法:*地址 <===> 地址对应的那个变量

      int a = 10;
      
      &a // 获取a的地址
      *(&a)  // 获取a的地址的变量
      // *& 可以约去
      *(&a) <==> a
      
      注意:*要与乘号相同,注意区分,如 (*p)*2
      • 代码:
      #include <stdio.h>
      
      int main(){
          int a = 10;
      
          int* p = &a;
          printf("a=%d, *p=%d\n", a, *p);
      
          *p = 20;
          printf("a=%d, *p=%d\n", a, *p);
      
          int b = *p;
          printf("a=%d, *p=%d, b=%d\n", a, *p, b);
          printf("&a=%p, p=%p, &b=%p\n", &a, p, &b);
      }
      
      结果: 
      a=10, *p=10
      a=20, *p=20
      a=20, *p=20, b=20
      &a=000000EFF2BFF7C4, p=000000EFF2BFF7C4, &b=000000EFF2BFF7C0
      

交换a/b值的代码(指针)

#include <stdio.h>

void swap(int*, int*);

int main(){
    int a = 10, b = 50;
    printf("a=%d, b=%d\n", a, b);
    swap(&a, &b);

    printf("a=%d, b=%d\n", a, b);
    return 0;
}

void swap(int* a, int* b){
    int t = *a;
    *a = *b;
    *b = t;
}

指针与数组

数组元素与普通变量一样,也有自己的地址,数组元素间地址是连续的

一维数组与指针的关系

我们可以用一个指针来表示已存在的数组,且数组名就是数组首元素的地址

int a[10];

// 以下这两种写法是一样的,因为在表达式中,a就是&a[0]
int* p = &a[0];
int* p2 = a;

// 下面也是等价的
a[0] = 100;
*p = 100;
// *p ===> *a ==> *(&a[0]) ==> a[0]

// 一维数组,第i项的地址就是a+i(因为a是第0项的地址&a[0], a每次加1就是将地址往后移动1个单位,
// 每个单位的长度取决于a的类型,如a是int* ,每个单位就是4,
// a是char* 每个单位就是1。所以&a[i]就是a+i)
&a[i] ===> &a[0] + i  ==> a+i
a[i] ===> *(a+i)

注意:数组名虽然可以作为指针使用,但是作为指针使用时,可以进行加减运算,不能进行赋值运算

二维数组与指针的关系

二维数组可以看做是一个一维数组,只不过它里面的每一项又是一个数组。

int a[3][4];

// 通过上面的一维数组,我们可知
a[i] = *(a+i)

// 所以:(注意:下方的 a[i] 其实相当于a[i]数组的第0项的地址:&a[i][0]a[i][j] ==> *(a[i]+j) ==> *(*(a+i)+j)
#include <stdio.h>

int main(){
    char a[3] = {'a', 'b', 'c'};
    char b[2][3] = {{'1','2','3'}, {'4','5','\0'}};

    printf("sizeof(a)=%ld, sizeof(a+1)=%ld, *(a+1)=%c\n", sizeof(a), sizeof(a+1), *(a+1));
    printf("sizeof(b)=%ld, sizeof(b+1)=%ld, *(b+1)=%s\n", sizeof(b), sizeof(b+1), *(b+1));

    return 0;
}

结果:
sizeof(a)=3, sizeof(a+1)=8, *(a+1)=b
sizeof(b)=6, sizeof(b+1)=8, *(b+1)=45

一定要注意:在表达式中,数组a退化为它本身第0项的地址,即 a ==> &a[0];非表达式中,单独一个a并不是&a[0], 而是表示数组本身。

int a[10];

// 非表达式中,这里a就是指数组本身,数组长度为10,所以结果为10
sizeof(a) ==> 10

// 表达式中,这里表达式a+1中,a表示 &a[0], &a[0]+1 就是 &a[1], 是个指针
// 这里指针的长度为864位系统是8,32为系统是4)
sizeof(a+1) ==> 8

指针与字符串

在C语言中,并没有内置字符串类型,C语言的字符串是通过char*指针来实现的

// 定义了一个char*类型指针,其值保存为 "ABCDEFG" 的首地址,字符串会保存在.rodata里(只读区/常量区)
char* str1 = "ABCDEFG"; 

代码:

#include <stdio.h>

int main(){
    char* str1 = "ABCDEFGHI";
    printf("str1=%s, sizeof(str1)=%ld, *str1=%c\n", str1, sizeof(str1), *str1);

    char str2[] = {"ABCDEFGHI"};
    printf("str2=%s, sizeof(str2)=%ld, *str2=%c\n", str2, sizeof(str2), *str2);

    return 0;
}

结果:
str1=ABCDEFGHI, sizeof(str1)=8, *str1=A
str2=ABCDEFGHI, sizeof(str2)=10, *str2=A
  • char* 格式定义字符串:
    • 格式:char* str1 = "ABCDEFGHI";
    • 这种方式会把字符串的值存储在 .rodata (只读区/常量区)中
    • 数据只读,不能更改。
  • 字符数组格式定义字符串:
    • 格式:char str2[] = {"ABCDEFGHI"};
    • 这种方式会把字符串的值存储在“栈”中。并且会多存储一个字符 \0,这是字符串的结尾标识符

NULL指针和野指针

NULL指针

NULL指针也叫空指针,其实是系统定义的第一个宏,表示不指向任何空间

  • 语法: 类型说明符 * 指针变量名 = NULL;
    char * pointer = NULL;
    // NULL ===> (void*)0
    // NULL指针也是空指针,地址为0的地方,不能被访问。访问会报错
    
  • 作用:防止程序中出现定义了指针却又未初始化时,去访问该指针出现数据异常的情况
    int *p;
    // 上面可替换为:
    int *p = NULL;
    

野指针

野指针不是NULL指针,而是指向了一个不知道的地方

  • 形成的原因:
    • 定义了指针,未进行初始化配置。如:int *p;
    • 指针在被free和delete后没有置空(=NULL),让人误认为该指针还是个合法的指针

注意:一定不要定义野指针,如果不知道一个指针指向何处,就让他置为空 NULL

指针常量和常量指针

指针常量

是一个常量,其次才是个指针。他是常量就说明这个指针的地址不能修改。但是他的值可以修改。

  • 语法: 类型说明符 * const 指针变量名;
    char *const s1;  // 就是定义了一个指针常量
    

最典型的指针常量就是数组名;不能改变它的地址,可以改变他的内容;

char a = 'a';
char b = 'b';
char * pointer1 = &a;
char *const pointer2 = &b;  // 定义指针常量 
pointer2 = &a;              // ❌️pointer2是个指针常量,不能修改指针

总结:指针常量,指针本身的空间只读。指针指向的内容可读可写。

常量指针

是一个指针变量,但是其指向的地址空间是个常量,指针可以修改,但是他的值是常量,不能修改。

  • 语法:const 类型说明符 * 指针变量名;
    // 下方两种方式一样
    const char * str1;
    char const * str1;
    

指针常量和常量指针的区别:看const修饰的是谁,修饰的是指针变量名,就是就是指针常量,反之则是常量指针

数组指针和指针数组

数组指针

指向一个数组空间的指针

int a[10];

typeof(a) *p;  // 定义了一个指针p,他的类型是typeof(a), 他是个数组指针
===> int[10] *p;  // 这种写法不符合C语言语法规范
===> int (*p)[10];  // 这里定义了一个指向拥有10个int类型元素的数组空间的指针。

因为*的结合性比较低,所以要用括号括起来,表示定义的是一个指针 注意:二维数组名就是一个数组指针,因为a=&a[0], 所以数组指针的用法就是二维数组的用法

int a[2][4];

int (*p)[4] = a;  // 定义一个数组指针p

p+1 ==> &a[1]
*(p+1) ==> a[1] ==> &a[1][0]

*(*(p+1)+j) ==> *(&a[1][0] + j) ==> a[i][j]

指针数组

指针数组,首先是一个数组,其次他的元素是指针类型

int * p[10];  // 定义了一个数组,其元素有10个,元素类型是 int*类型

main的参数

#include <stdio.h>

/*
    argc: 传入参数的数量
    argv:传入参数的值(文件名也算)
 */
int main(int argc, const char *argv[]){

    for (int i=0; i<argc; i++){
        printf("第%d个元素:%s\n", i, argv[i]);
    }
    return 0;
}

结果:
第0个元素:D:\ccode\class5\main.exe
第1个元素:12个元素:23个元素:34个元素:45个元素:56个元素:67个元素:78个元素:8

二级指针与多级指针

二级指针就是一个指针指向另一个指针

  • 指针变量的地址:
    int a = 10;
    int * p = &a;  // 一级指针
    int ** p2 = &p;  // 二级指针,存储的值是个一级指针
    
    • 解引用:
    **p2 ==> *p ==> a
    
  • 指针数组的地址:
    • 如果一个数组的元素是指针,那个该数组就是个二级指针,同理,如果一个数组的元素是二级指针,那么该数组就是个三级指针
    int a = 2, b = 3, c = 4;
    int *arr[] = {&a, &b, &c};
    // arr的元素是一级指针,所以arr是二级指针 
    arr[0]  ==> int*
    arr ==> &arr[0] ==> int**
    

几级指针就用几个*,解引用也要解几次
注意:并不是地址一模一样就是同一个指针,他还有类型区别

int a[2][4];

&a[0] 的地址与 &a 一样,但是他们不是同一个指针,他们的类型不同

&a[0]  ==> int (*)[4]  类型
&a ==> int (*)[2][4] 类型

函数指针与指针函数

函数指针

是一个指针,保存函数地址的变量。保存的地址是函数的地址。

  • 函数指针的定义: 定义:指向的函数的返回值类型 (*函数指针变量名)(指向函数的形参列表);
    int sum(int a, int b){
        return a+b;
    }
    
    int (*sum_p)(int, int) = sum;
    
  • 函数指针的引用: 因为函数名本身就是函数的地址,所以解不解变量都可以
    • (*函数指针变量名)(函数的形参);
    • 函数指针的变量名(函数的形参);
    int sum(int a, int b){
        return a+b;
    }
    
    int (*sum_p)(int, int) = sum;  // 赋值sum的时候不需要加参数
    
    // 两种格式写法都可以
    (*sum_p)(1, 19);
    sum_p(1, 19);
    
  • 用法:
    • 线程池

    • 回调函数

      • 用户把调用函数的地址作为一个参数传入函数,以便该函数在处理相对应的事件的时候,可以灵活的使用不同的方法

指针函数

指针作为一个函数返回值类型的函数

int *sum();

指针作为函数的参数

由于形参不能改变实参(形不改实),被调函数想要传递参数给主函数一般只能return返回。除此之外还有两种方式:

  • 访问全局变量
  • 指针作为函数的参数(就像上面的swap函数)