指针入门

基础知识

内存地址

  • 字节: 字节是内存容量的一个单位, byte ,一个字节 byte8个位 bit
  • 地址: 系统为了方便区分每一个字节的数据,而对内存进行了逐一编号,而该编号就是内存地址。

C-pointer1

基地址

  • 单字节的数据: char 它所在地址的编号就是该数据的地址
  • 多字节的数据:int 它拥有4个连续的地址的编号 ,其中地址值最小的称为该量的地址

C-pointer2

取地址符号

每一个变量其实都对应了一片内存,因此都可以通过 & 取地址符号将其地址获得。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int i ;
double d;
float f ;
char c ;

printf("i的地址为:%p\n" , &i);
printf("d的地址为:%p\n" , &d);
printf("f的地址为:%p\n" , &f);
printf("c的地址为:%p\n" , &c);


//运行结果:
i的地址为:0x7ffec726bc58
d的地址为:0x7ffec726bc60
f的地址为:0x7ffec726bc5c
c的地址为:0x7ffec726bc57

C-pointer3

注意:

  • 虽然不同的数据类型所占用的内存空间不同,但是他们的地址所占用的内存空间(地址的大小= 指针的大小)是恒定的,由系统的位数来决定 32位 / 64位
  • 不同的地址他从表面上看似乎没有什么差别,但是由他们所代表的内存的尺寸是不一样的(由内存中所存放的数据类型相关),因此我们在访问这些地址的时候需要严格区分它们的逻辑关系。

指针的基础

  1. 概念:&a 就是 a 的地址,实质上也可以理解为他是一个指针,用来指向 a 的地址。专门用来存放地址的一个变量,因此指针的大小是恒定的,由系统的位数来决定。

  2. 指针的定义语法

    1
    2
    3
    4
    int a ; // 定义一片内存名字叫 a , 约定好该内存用来存放 整型数据
    int * p ; // 定义一片内存名字叫 p , 约定好该内存用来存放 整型数据的地址
    char * p1 ; // 定义一片内存名字叫 p1 , 约定好该内存用来存放 字符数据的地址
    double * p2 ; // 定义一片内存名字叫 p2 , 约定好该内存用来存放 双精度数据的地址

    注意:

    指针的类型,并不是用来决定该指针的大小,而是用来告诉编译器如果我们通过该指针来访问内存时需要访问的内存的大小尺寸

  3. 指针的赋值以及初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int a = 100 ;
    int * p = &a ; // 定义并初始化

    double d = 1024.1234 ;
    double * p1 = &d ;// 定义并初始化

    float f ;
    float * p2 ;// 只定义,没有初始化
    p2 = &f ; // 给指针赋值

    C-pointer4

    注意:指针自己也有一个地址

    注意:

    不同类型的指针,应该用来指向与其相对应的类型的变量的地址。

  4. 指针的索引

    通过指针获得它所指向的数据(解引用/取目标

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    int a = 100 ;
    int * p = &a ;

    *p = 250 ; // *p <==> a

    printf("*p:%d\n" , *p) ;
    printf("a:%d\n" , a) ;

    //运行结果
    *p:250
    a:250

野指针

  1. 概念:指向一块未知内存的指针, 被称为野指针。

    1
    int * p ;   //仅定义了,但没有初始化也没有给指针赋值

    C-pointer5

  2. 危害:

    • 引用野指针的时候,很大概率我们会访问到一个非法内存,通常会出现段错误(Segmentation fault (core dumped))并导致程序崩溃。
    • 更加严重的后果,如果访问的时系统关键的数据,则有可能造成系统崩溃
  3. 产生原因:

    • 定义时没有对他进行初始化
    • 指向的内存被释放,系统已经回收后,该指针并没有重新初始化
    • 指针越界
  4. 如何防止:

    • 定义时记得对他进行初始化
    • 绝对不去访问被回收的内存地址,当我们释放之后应该重新初始化该指针。
    • 确认所申请的内存的大小,谨防越界

空指针

在很多的情况下,我们一开始还不确定一个指针需要指向哪里,因此可以让该指针先指向一个不会破坏系统关键数据的位置,而这个位置一般就是 NULL (空)。因此指向该地址的指针都称之为空指针。

C-pointer6

概念: 空指针就是保存了地址值为零的一个地址, 也就零地址NULL

1
2
3
4
int * p1 = NULL ; // 定义一个指针, 并初始化为空指针( 指向 NULL )
*p1 = 250 ; // 段错误 , 该地址不允许写入任何东西

printf("%p ‐‐ %d \n" , NULL , NULL ); // (nul‐‐0)

指针运算

  • 指针的加法:意味着地址向上移动若干个目标 (指针的类型)
  • 指针的减法:意味着地址向下移动若干个目标 (指针的类型)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
long l ;     //long int
long *p = &l ;

int i ;
int * p1 = &i;

printf("p:%p\n" , p );
printf("p+1:%p\n" , p+1 );

printf("p1:%p\n" , p1 );
printf("p1+1:%p\n" , p1+1 );


运行结果:
p:0x7fffcb862610
p+1:0x7fffcb862618
p1:0x7fffcb86260c
p1+1:0x7fffcb862610

C-pointer7

注意:
指针在加减运算的过程中, 加/减的大小取决于该指针他自己的类型,与它所执行的数据实际的类型没有关系。


指针进阶

char 型指针

从本质上来看,字符指针其实就是一个指针而已,只不过该指针用来指向一个字符串/字符串数组

char * msg = "Hello" ;

多级指针

  • 如果一个指针 p1 它所指向的是一个普通变量的地址,那么 p1就是一个一级指针
  • 如果一个指针 p2 它所指向的是一个指针变量的地址,那么 p2就是一个二级指针
  • 如果一个指针 p3 它所指向的是一个指向二级指针变量的地址,那么 p3就是一个三级指针
1
2
3
4
int a = 100 ;
int * p1 = &a ; // p1是一个一级指针
int ** p2 = &p1 ; // p2是一个二级指针
int *** p3 = &p2 ; // p3是一个三级指针

C-pointer8

指针的拆解方法:
对于任何的指针都可以分为两部分:
第一部分: 说明他是一个指针 (* p )
第二部分: 说用它所指向的内容的类型 (* p)以外的东西

1
2
3
4
5
6
7
8
9
10
11
char *p1; // 第一部分:* p1 ,第二部分:char ,说明p1 指向的类型为char

char **p2; // 第一部分:* p2 ,第二部分: char * , 说明p2 指向的类型为char *

int **p3 ; // 第一部分:* p3 ,第二部分:int * 说明p3 指向的类型为int *

char (*p4)[3]; // 第一部分:* p4 , 第二部分: char [3] , 说明p4 指向一个拥有3个元素的char 数组

char (*p5)(int, float); // 第一部分:* p5, 第二部分:char (int , float),说明该指针指向一个 拥有char类型返回, 并需要 一个int 和 float 参数的函数

void *(*p6)(void *); // 第一部分:* p6, 第二部分:void * (void *),说明p6 指向一个 拥有 void * 返回并需要一个void * 参数的函数,函数指针)
  • 以上指针 p1 p2 p3 p4 p5 p6 本质上都是指针,因此它们的大小都是 8字节(64位系统)
  • 以上指针 p1 p2 p3 p4 p5 p6 本质上都是指针,唯一的不容是它们所指向的内容的类型不同

void 型指针

  1. 概念: 表示该指针的类型暂时是不确定的

  2. 要点

    • void 类型的指针,是没有办法直接索引目标的。必须先进行强制类型转换。
    • void 类型指针,无法直接进行加减运算。
  3. void 关键字的作用

    • 修饰指针,表示该指针指向了一个未知类型的数据。
    • 修饰函数的参数列表,则表示该函数不需要参数。
    • 修饰函数的返回值,则表示该函数没有返回值。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void * p = malloc(4) ; // 使用malloc 来申请 4个字节的内存, 并让p来指向该内存的入口地址

    *(int *)p = 250 ; // 先使用(int*) 来强调p是一个整型地址 ,然后再解引用
    printf("*p:%d\n", *(int*)p);// 输出时也应该使用对应的类型来进行输出

    *(float*)p = 3.14 ;
    printf("*p:%f\n", *(float*)p);

    int * a ;
    char * b ;
    float * f ;

    void * k ;

    以上写法 void * p ,在实际开发中不应该出现。以上代码只是为了说明语法问题。

const 指针

const 修饰指针有两种效果:

  • 常指针:修饰的是指针本身,表示该指针变量无法修改

    char * const p ;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    char arr [] = "Hello" ;
    char msg [] = "world" ;
    char * const p = arr ;

    // p = msg ; // 错误,p 被const 所修饰,说明P是一个常量 ,他的内容(所指向的地址)无法修改

    *(p + 1 ) = 'E' ; // p所指向的内容是可以通过p 来修改 (只要保持P所指向的地址不变即可)

    printf("%s\n" , p ); //输出结果为:HEllo
  • 常目标指针:修饰的是指针所指向的目标,表示无法通过该指针来改变目标的数据

    char const * p ;
    const char * p ;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    char arr [] = "Hello" ;
    char msg [] = "world" ;

    const char * p1 = arr ;
    p1 = msg ; // p1 的指向是可以被修改的
    // *(p1+1) = 'V' ; // 错误,常目标指针, 不允许通过该指针来它所指向的内容

    *(msg+1) = 'V' ; // 虽然p1不能修改所指向的内容, 但是内容本身是可以被修改的

    printf("%s\n" , p1 ); // 输出结果为:wVrld

总结:

  • 常指针并不常见
  • 常目标指针,在实际开发过程中比较常见,用来限制指针的权限为 只读

C-pointer9