C 语言指针学习

C 语言指针学习

目标:复习软考 + 阅读代码 + 理解数据结构;

1. 指针定义

内存区域中的每个字节都对应一个编号,这个编号就是地址。如果在程序中定义了一个变量,那么在对程序中进行编译时,系统就会给这个变量分配内存单元。按变量地址存取值的方式称为直接访问,如printf("%d",&i);另一种存取变量值的方式成为间接访问,即将变量 i的地址存放到另一个变量中,在 C 语言中,指针变量是一种特殊的变量,它用来存放变量地址。

语法格式:

// 类型 *指针变量名;
int *i_printer;
  • 指针与指针变量是两个概念,一个是变量的地址称为该变量的指针

    例如:地址 2000 是变量 i 的指针。如果有一个变量专门用来存放另一变量的地址,那么称它为指针变量

    image-20240729161354770

2. 指针学习

取地址操作符为&,也称引用,通过该操作符我们可以获取一个变量的地址值;

取值操作符为*,也称解引用,通过该操作符我们可以得到一个地址对应的数据;

#include <stdio.h>

int main() {
    int i = 5;
    int *p = &i;
    printf("i=%d\n", i);  // 直接访问
    printf("*p=%d\n", *p);  // 间接访问
}
  • 指针变量前面的*表示该变量为指针型变量.

    float *pointer_1;
    // 注意指针变量名是 pointer_1, 而不是 *pointer_1 ;
    
  • 在定义指针变量时必须指定其类型。需要注意的是,只有整型变量的地址才能放到指向整型变量的指针变量中。例如.下面的赋值是错误的;

    float a;
    int * pointer_1;
    pointer_1 =&a;  // 毫无意义且会出错;
    
  • 如果已执行了语句

    pointer_1 =&a;
    

    那么&* pointer_1的含义是什么呢?

    &*两个运算符的优先级别相同,但要按自右向左的方向结合。因此,&* pointer_1&a相同,都表示变量a的地址,也就是pointer_1.
    *&a的含义是什么呢?
    首先进行&a运算,得到a的地址,再进行运算。&a*pointer_1的作用是一样的,它们都等价于变量a,即*&aa等价.

  • C语言本质上是一种自由形式的语言,这很容易诱使我们把*写在靠近类型的一侧,如int *a这个声明与前面一个声明具有相同的意思,而且看上去更清晰, a 被声明成类型为int*的指针.但是,这并不是一个好习惯,因为类似int *a,b,c的语句会使人们很自然地认为这条语句把所有三个变量声明为指向整型的指针,但事实上并非如此,*实际上是*a的一部分,只对a标识符起作用,但其余两个变量只是普通的整型变量.要声明三个指针变量,正确的语句如下:

    int *a, *b, *c;
    

3. 指针的传递

指针的使用场景通常只有两个,传递与偏移;

#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;

}

image-20240729215316008

main函数开始执行时,系统会为main函数开辟函数栈空间,当程序走到int i时, main 函数的栈空间就会为变量i分配4字节大小的空间。调用change函数时,系统会为change函数重新分配新的函数栈空间,并为形参变量j分配4字节大小的空间.在调用change(i)时,实际上是将i的值赋值给j,我们把这种效果称为值传递(C语言的函数调用均为值传递)。因此,当我们在change 函数的函数栈空间内修改变量 j的值后, change函数执行结束,其栈空间就会释放,j就不再存在, i的值不会改变。

image-20240729220436343

使用指针进行传递

#include <stdio.h>

// 地址传递
void change(int* j) {
    *j = 5;
}

int main() {
    int i = 10;
    printf("before change i=%d\n", i);
    change(&i);  // 传递变量 i 的地址;
    printf("after change i=%d\n", i);
    return 0;

}

image-20240729220743462

image-20240729221229063

我们将变量i地址传递给change 函数时,实际效果是j=&i,依然是值传递,只是这时我们的j是一个指针变量,内部存储的是变量i的地址,所以通过*j就间接访问到了与变量i相同的区域,通过*j=5就实现了对变量i的值的改变.通过单步调试,我们依然可以看到变量j自身的地址是与变量i的地址依然不相等。

4. 指针的偏移

前面介绍了指针的传递。指针即地址,就像我们找到了一栋楼,这栋楼的楼号是B,那么往前就是 A,往后就是 C,所以应用指针的另一个场景就是对其进行加减,但对指针进行乘除是没有意义的,就像家庭地址乘以 5 没有意义那样。在工作中,我们把对指针的加减称为指针的偏移,加就是向后偏移,减就是向前偏移。

#include <stdio.h>

#define N 5

int main() {
    int a[N] = {1, 2, 3, 4, 5};
    int *p;
    int i;
    p = a;
    for (i = 0; i < N; i++) {
        printf("%3d", *(p+i));
    }
    printf("\n---------------\n");
    p=&a[4];
    for (i = 0; i < N; i++) {
        printf("%3d", *(p-i));
    }
    printf("\n");
    return 0;
}

image-20240729223234509

偏移的长度是其基本类型的长度,也就是偏移sizeof(int),这样通过*(p+1)就可以得到元素a[1].

5. 补充

5.1 强制类型转化

在 C 语言中,强制类型转换是一种将一个变量的值转换成另一种数据类型的操作。这种转换可以通过类型转换运算符来实现,主要有以下几种形式:

  • 显示类型转换

    使用圆括号将变量显式地转换成另一种类型。

    // 语法格式;
    type variable_name = (type)expression;
    

    例如:

    int a = 10;
    float b = (float)a; // 将整数a转换为浮点数b
    
  • 静态类型转换

    C99标准中引入了_Static_cast关键字,用于在编译时进行类型检查。

    // 语法格式;
    type variable_name = _Static_cast<type>(expression);
    

    例如:

    int a = 10;
    float b = _Static_cast<float>(a); // 将整数a转换为浮点数b
    
  • 函数类型转换

    将一个函数指针转换为另一个类型的指针。

    // 语法格式;
    type (*function_name)(args) = (type (*)(args))expression;
    

    例如:

    void (*func_ptr)(int) = (void (*)(int))some_other_func_ptr;
    
  • 复合字面量

    C11标准中,可以使用复合字面量进行类型转换。

    // 语法格式;
    type variable_name = (type){expression};
    

    例如:

    int a = 10;
    float b = (float){a}; // 使用复合字面量将整数a转换为浮点数b
    

5.2 sizeof 操作符

在 C 语言中,sizeof 既不是函数,也不是关键字,它是一个预处理操作符。

预处理操作符是编译器在编译源代码之前执行的操作,sizeof 操作符用于确定数据类型或变量在内存中的大小,以字节为单位。

使用 sizeof 的语法如下:

size_t size = sizeof(type);

这里,type 是一种数据类型,例如 intfloatchar 等,或者是一个变量的名称。size_t 是一个无符号整数类型,通常用于存储大小信息。

int a;
size_t size = sizeof(a); // 获取变量a的大小

或者直接获取类型的大小:

size_t size = sizeof(int); // 获取int类型的大小

sizeof 操作符在预处理阶段就被处理了,它不会产生运行时调用,因此效率很高。

6. 指针内存空间

提问:指针也是种数据类型,那么这种数据类型占用多少内存空间?

#include <stdio.h>

// 使用 sizeof 操作符, 计算指针类型的大小;
int main(){
    printf("sizeof int * = %d\n", sizeof(int *));
    printf("sizeof char * = %d\n", sizeof(char *));
    printf("sizeof float * = %d\n", sizeof(float *));
    printf("sizeof double * = %d\n", sizeof(double *));
}

image-20240730112043449

总结:所有指针类型在32位操作系统下是4个字节,64 位操作系统是 8 个字节;

7. 空指针和野指针

空指针:指针变量指向内存中编号为0的空间

用途:初始化指针变量

注意:空指针指向的内存是不可以访问的

示例1:空指针

//空指针
void test01()
{
	int* p = NULL; //NULL本质就是 0

	//访问空指针的存储内容 会报错
	//内存地址编号为 0 ~ 255之间的系统占用的内存,用户不可以访问
	//printf("%d\n", *p); //error 空指针不可以访问
}

野指针:指针变量指向非法的内存空间

示例2:野指针

//野指针
void test02()
{
	//利用指针变量p指向非法内存空间 0x1100
	int* p = 0x1100;

	//printf("%d\n", *p);
}

//注意事项:不要操作未初始化的指针变量
void test03()
{
	int* p; //也属于野指针
	//*p = 100; 
	//printf("%d\n", *p);  //error 非法访问内存
}

总结:空指针和野指针都不是我们申请的空间,因此不要访问。

8. const 修饰指针

const修饰指针有三种情况

  1. const修饰指针 --- 常量指针
  2. const修饰常量 --- 指针常量
  3. const即修饰指针,又修饰常量
// 1、const 修饰的 * 称为常量指针
void test01() {
    int a = 10;
    const int *p = &a;  // *p只读  p可读可写   等价于 int const  *p = &a;
    // *p = 100;  //error 指针指向的值 不可以修改
    int b = 20;
    p = &b;  //success 指针的指向可以修改
    printf("p=%d", *p);
}

int main() {
    test01();
}

image-20240730113307890

// 2、const 修饰的 p 称为指针常量
void test_02(){
    int a =10;
    int* const p = &a;
    *p=30;
    // int b = 20;

    //p = &b; // error  指针的指向不可以修改
    printf("p=%d", *p);

}
int main() {
    test_02();
}

image-20240730113642291

void test_03(){
    int a =10;
    const int* const p = &a; //等价于 int const * const p = &a;
//    *p = 100;  // error 指针指向的值不可以改
    int b = 20;
//    p = &b;  error指针的指向不可以改
}
int main() {
    test_02();
}

技巧:看const右侧紧跟着的是指针还是常量, 是指针就是常量指针,是常量就是指针常量

9. 指针与动态内存申请

在学习 C 语言的数组后都会觉得数组长度固定很不方便,其实 C 语言的数组长度固定是因为其定义的整型、浮点型、字符型变量、数组变量都在栈空间中,而栈空间的大小在编译时是确定的,如果使用的空间大小不确定,那么就要使用堆空间。请看下面例子

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(){
    int i;
    char *p;
    scanf("%d", &i);  // 输入需要申请的空间大小;
    p=(char*) malloc(i); // 使用 malloc 动态申请堆空间
    strcpy(p, "malloc success");
    // strcpy 把src所指向的字符串复制到dest所指向的空间中,'\0'也会拷贝过去
    puts(p);
    free(p); // free 时必须使用 malloc 申请时返回的指针值,不能进行任何便宜;
    printf("free success \n");

    return EXIT_SUCCESS;
}

image-20240730115527709

首先我们来看malloc函数.在执行#include <stdlib.h>,void *malloc(size_t size);时,需要给malloc传递的参数是一个整型变量,因为这里的size_t即为 int;返回值为void*类型的指针,void*类型的指针只能用来存储一个地址而不能进行偏移,因为 malloc并不知道我们申请的空间用来存放什么类型的数据,所以确定要用来存储什么类型的数据后,都会将void*强制转换为对应的类型。在代码中我们用来存储字符,所以将其强制转换为char*类型。
同时需要注意指针本身大小,和其指向的空间大小,是两码事,不能和前面的变量类比去理解!

如下图所示,定义的整型变量 i、指针变量p均在main函数的栈空间中,通过malloc申请的空间会返回一个堆空间的首地址,我们把首地址存入变量p.知道了首地址,就可以通过strcpy函数往对应的空间存储字符数据.

image-20240730121154729

既然都是内存空间,为什么还要分栈空间堆空间呢?栈是计算机系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈操作、出栈操作都有专门的指令执行,这就决定了栈的效率比较高;堆则是C、C++函数库提供的数据结构,它的机制很复杂,例如为了分配一块内存,库函数会按照一定的算法(具体的算法请参考关于数据结构、操作系统的书籍)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能由于内存碎片太多),那么就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后返回。显然,堆的效率要比栈低得多。(这段了解即可)

栈空间由系统自动管理,而堆空间的申请和释放需要自行管理,所以在具体例子中需要通过free函数释放堆空间.free函数的头文件及格式为

#include <stdlib.h>
void free(void *ptr); 

其传入的参数为void类型指针,任何指针均可自动转为void*类型指针,所以我们把p传递给free 数时,不需要强制类型转换。p的地址值必须是malloc当时返回的地址值,不能进行偏移,也就是在mallocfree之间不能进行p=p+1等改变变量p的操作,原因是申请一段堆内存空间时,内核帮我们记录的是起始地址和大小,所以释放时内核用对应的首地址进行匹配,匹配不上时,进程就会崩溃。如果要偏移进而存储数据,那么可以定义两个指针变量来解决.

栈空间与堆空间的区别

栈空间在函数执行完成之后会被释放,堆空间不会因函数执行结束而释放;堆空间只有在执行free操作后才会释放,否则在进程执行中会一直有效。

度过大难,终有大成。

继续努力,终成大器。

posted @ 2024-07-30 21:01  紫青宝剑  阅读(22)  评论(0编辑  收藏  举报