Bootstrap

指针(C语言)从0到1掌握指针,为后续学习c++打下基础

目录

一,指针

二,内存地址和指针

 1,什么是内存地址

2,指针在不同系统下所占内存

三,指针的声明和初始化以及类型

1,指针的声明

 2,指针 的初始化

1, 初始化方式优点及适用场景

4,指针的声明初始化类型

四,野指针(永远都要避免) 

1,野指针的定义

2,野指针产生的原因

1,指针没有初始化

2,释放内存后未置空 

3.局部变量超出作用域 

3,野指针的危害 

4,如何避免野指针

 五,取地址符和解引用

1,取地址符&

2,解引用 *

六,指针的算术运算

1. 指针加法

2. 指针减法 

 3. 指针自增/自减

 4. 指针与整数的比较

七,指针与数组

1,定义

2,初始化

3,指针数组与字符串

八,指针与函数

1. 函数参数传递指针

2. 函数返回指针 

3. 函数指针 

4,注意事项(必看)

九,多级指针

1. 多级指针的定义与初始化

1,定义

2,初始化

2. 多级指针的使用场景

1,动态二维数组

2,函数参数传递

3. 多级指针的注意事项


一,指针

在C语言中,指针是一种特殊的数据类型,用于存储另一个变量的内存地址。这使得程序可以直接操作计算机的物理内存位置,从而实现高效的内存管理和灵活的数据结构设计。


二,内存地址和指针

1,每个变量存储在内存中的唯一位置。

2,指针是存储内存地址的特殊变量。

int var = 10;
int *ptr = &var;  // ptr存储var的地址

 1,什么是内存地址

内存是计算机用于存储数据和程序的地方,它被划分成一个个连续的存储单元,每个存储单元都有一个唯一的编号,这个编号就是内存地址。内存地址就像图书馆里书架上的格子编号一样,通过它可以准确地找到和操作存储在相应位置的数据。CPU 通过内存地址来访问和读写内存中的数据,数据在内存中的存储、读取和修改等操作都依赖于内存地址来确定具体位置。

2,指针在不同系统下所占内存

指针占用的内存大小取决于所使用的计算机系统的架构和编译器等因素:

以下是一个简易输出在64位32位的情况下所占用字节。

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() 
{
    int* intPtr;                   
    size_t ptrSize = sizeof(intPtr);
    printf("int指针占用的字节数: %zu 字节\n", ptrSize);
    return 0;
}

在 32 位系统中,指针通常占用 4 个字节的内存空间。这是因为 32 位系统的地址总线是 32 位的,它能够表示的地址范围是2的32次方个不同的地址,所以需要 4 个字节来存储一个地址。

在 64 位系统中,指针一般占用 8 个字节的内存。因为 64 位系统的地址总线是 64 位,可表示的地址范围是2的64次方个,所以需要 8 个字节来存储一个地址。


三,指针的声明和初始化以及类型

1,指针的声明

指针是一种变量,它存储的是内存地址。通过指针,我们可以直接访问和操作内存中的数据。定义指针的一般形式为。

数据类型 *指针变量名;

 1.这里定义了一个名为p的指针变量,它可以指向一个int类型的数据。 

int *p;

 2,指针 的初始化

1, 指针在定义后可以进行初始化,使其指向一个已存在的变量。

int num = 10;
int *p = &num;

2,里&是取地址运算符,它获取变量num的内存地址,并将其赋值给指针p,此时p就指向了变量num。也可以先定义指针,再进行赋 

int num = 10;
int *p;
p = &num;

3,这是最安全的初始化方式之一,将指针初始化位null(空指针),表示指针不知想任何有效的内存地址。null是一个特殊的指针值,通常定义位0。

  int* p = NULL;  // 初始化为NULL
  if(p == NULL) 
  {
     printf("p是NULL\n");
  }

4,通过动态内存分配函数(如malloc、callocrealloc)分配内存,并将指针初始化为分配的内存地址。这种方式适用于需要在运行时动态分配内存的场景(仅展示malloc)。 

#include <stdio.h>
#include <stdlib.h>
int main() 
{
    int* p = (int*)malloc(sizeof(int));  // 动态分配一个整型变量的内存
    if (p != NULL) 
    {
        *p = 10;  // 使用分配的内存
        printf("%d\n", *p);
        free(p);  // 释放分配的内存
    }
    else 
    {
        printf("失败\n");
    }
    return 0;
}

1, 初始化方式优点及适用场景

初始化方式优点适用场景
初始化为NULL避免野指针错误不确定指针指向
初始化为动态分配的内存动态管理内存需要动态分配内存
初始化为变量的地址简单直观,方便操作变量需要通过指针修改变量

4,指针的声明初始化类型

在C语言中,指针的初始化不单单只有int类型可以初始化,还有以下:

int *p1;         // 整型指针
char *p2;        // 字符指针
float *p3;       // 浮点指针
double *p4;      // 双精度指针
void *p5;        // 无类型指针

四,野指针(永远都要避免) 

野指针是C语言中一个非常危险且常见的问题,它是指向一个无效、未分配或已经被释放的内存地址的指针。野指针的存在可能导致程序出现不可预知的行为,甚至崩溃。以下是关于野指针的详细解释,包括定义、产生原因、危害以及预防方法

1,野指针的定义

野指针是指向未知或无效内存区域的指针。它不是NULL指针,而是指向已经被释放或从未被分配的内存的指针。野指针的值是随机的,因此它可能指向任何内存位置,这使得野指针的使用非常危险。

2,野指针产生的原因

1,指针没有初始化

任何指针变量刚被创建时不会自动成为 NULL 指针,其缺省值是随机的。如在 C 语言中 int *p; 这样声明一个指针后,如果不对其初始化就使用,它就是野指针。

int *p;  // 未初始化,p是一个野指针
*p = 10;  // 随机地址赋值,可能导致程序崩溃

2,释放内存后未置空 

当使用 free 或 delete 等操作释放了动态分配的内存后,如果没有将指针设置为 NULL,而是继续使用该指针,那么它就会变成野指针。例如在 C 语言中,int *p = (int *)malloc(sizeof(int)); free(p); 执行完 free 后,p 就成为野指针。

int *p = (int*)malloc(sizeof(int)); // 分配内存
free(p);  // 释放内存,但 p 仍指向原地址
// 此时 p 成为野指针,访问 *p 会导致未定义行为

3.局部变量超出作用域 

不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。例如在函数内部定义一个局部变量,然后返回指向该局部变量的指针,当函数结束后,该局部变量的内存被释放,指针就成了野指针

int* createDanglingPointer() 
{
    int localVar = 10;
    return &localVar; // 返回局部变量的地址
}// 函数结束后,localVar 内存被释放,返回的指针变为野指针

3,野指针的危害 

问题表现
程序崩溃访问已释放的内存可能导致段错误
数据损坏野指针可能意外修改其他有效内存区域的数据。
安全问题攻击者可能利用野指针篡改程序逻辑
调试困难野指针引发的错误具有随机性,难以复现和定位。

4,如何避免野指针

 1,初始化指针:在声明指针时,如果可能的话,立即将其初始化为 NULL 或一个有效的内存地址。

2,释放内存后设置指针为 NULL:在释放了指针所指向的内存后,立即将指针设置为 NULL,以防止野指针的产生。

3,避免数组越界:确保数组访问在有效的索引范围内。使用循环和条件语句来检查索引是否在有效范围内。

4,避免返回局部变量地址:确保返回的指针指向堆内存或静态存储区的数据。

5,使用工具进行内存检查:使用如 Valgrind 这样的内存检查工具可以帮助发现野指针和其他内存相关的问题。(Valgrind、Clang Static Analyzer)


 五,取地址符和解引用

1,取地址符&

去支付是指针的核心要在,没有去支付就算不的它是一个指针。

int age = 25;
int *ptr = &age;

2,解引用 *

指针是一个变量,它存储的是另一个变量的内存地址。解引用操作就是通过指针访问其所指向的内存地址中的值。在 C 语言中,使用星号 * 来进行解引用操作

printf("%d", *ptr);  // 输出25
*ptr = 30;           // 修改实际变量值

六,指针的算术运算

 指针的算术运算是C语言中一个非常重要的概念,它允许我们通过指针来访问和操作数组、字符串等数据结构。指针的算术运算包括加法、减法和自增/自减运算。

1. 指针加法

指针加法是指将一个整数加到指针上,结果是一个新的指针,它指向原指针所指向的地址加上该整数乘以指针类型所占字节数的内存位置。

#include <stdio.h>
int main() 
{
    int arr[5] = { 10, 20, 30, 40, 50 };
    int* p = arr;  // 指针p指向数组arr的首元素
    // 指针p加上2
    p = p + 2;
    printf("指针移动后所指向的值: %d\n", *p);  // 输出30
    return 0;
}

2. 指针减法 

指针减法是指将一个整数从指针中减去,结果是一个新的指针,它指向原指针所指向的地址减去该整数乘以指针类型所占字节数的内存位置。

#include <stdio.h>
int main() 
{
    int arr[5] = { 10, 20, 30, 40, 50 };
    int* p = arr + 4;  // 指针p指向数组arr的最后一个元素
    // 指针ptr减去1
    p = p - 1;
    printf("指针移动后所指向的值: %d\n", *p);  // 输出40
    return 0;
}

 3. 指针自增/自减

指针自增(p++)和自减(p--)运算符用于将指针移动到下一个或上一个元素的位置。这相当于在指针上加或减1。

#include <stdio.h>
int main() 
{
    char str[] = "Hello";
    char* c = str;
    while (*c != '\0') 
    {
        putchar(*c);
        c++;        // 逐个访问字符
    }
}

 4. 指针与整数的比较

指针可以与整数进行比较,但这种比较通常没有意义,因为指针的值是内存地址,而整数是数值。唯一有意义的比较是指针与NULL的比较,这可以用来检查指针是否为空。

#include <stdio.h>
int main() {
    int* p = NULL;
    if (p == NULL) 
    {
        printf("NULL\n");
    }
    return 0;
}

七,指针与数组

在 C 语言中,指针数组是一种非常实用的数据结构,它结合了指针和数组的特性。下面将从定义、初始化、使用场景、注意事项等方面详细介绍指针数组。

1,定义

指针数组是一个数组,数组中的每个元素都是一个指针。

数据类型 *数组名[数组大小];

2,初始化

指针数组可以在定义时进行初始化,也可以在后续的代码中逐个赋值。

#include <stdio.h>
int main() 
{
    int num1 = 10, num2 = 20, num3 = 30;
    int* ptrArray[3] = { &num1, &num2, &num3 };
    for (int i = 0; i < 3; i++) 
    {
        printf("ptrArray[%d] 指向的值: %d\n", i, *ptrArray[i]);
    }
    return 0;
}

3,指针数组与字符串

指针数组在处理字符串时非常有用,因为 C 语言中的字符串实际上是字符数组,我们可以使用指针数组来存储多个字符串。

#include <stdio.h>
int main() 
{
    char* strArray[] = { "Hello", "World", "C Language" };
    for (int i = 0; i < 3; i++) 
    {
        printf("strArray[%d]: %s\n", i, strArray[i]);
    }
    return 0;
}

如上述示例所示,指针数组可以方便地存储和操作多个字符串,比二维字符数组更加灵活。

八,指针与函数

在 C 语言中,指针和函数有着紧密的联系,它们相互配合可以实现许多强大的功能。下面从函数参数传递指针、函数返回指针、函数指针这三个方面详细介绍指针与函数的关系。

1. 函数参数传递指针

在 C 语言里,函数参数传递分为值传递和地址传递(指针传递)。值传递只是将实参的值复制给形参,在函数内部对形参的修改不会影响到实参;而地址传递是将实参的地址传递给形参,这样函数内部就可以通过指针直接操作实参所指向的内存空间,从而修改实参的值。

#include <stdio.h>
// 交换两个整数的值,使用指针作为参数
void swap(int* a, int* b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}
int main() 
{
    int x = 10, y = 20;
    printf("交换前: x = %d, y = %d\n", x, y);
    swap(&x, &y);
    printf("交换后: x = %d, y = %d\n", x, y);
    return 0;
}

2. 函数返回指针 

函数可以返回一个指针,这样可以将函数内部动态分配的内存地址或者某个变量的地址返回给调用者。需要注意的是,返回的指针必须指向有效的内存区域,避免返回局部变量的地址,因为局部变量在函数执行结束后会被销毁,其地址将变得无效。

#include <stdio.h>
#include <stdlib.h>
// 动态分配内存并存储一个整数,返回指向该内存的指针
int* createInt(int value) 
{
    int* ptr = (int*)malloc(sizeof(int));
    if (ptr != NULL) 
    {
        *ptr = value;
    }
    return ptr;
}
int main() 
{
    int* numPtr = createInt(100);
    if (numPtr != NULL) 
    {
        printf("动态分配内存中存储的值: %d\n", *numPtr);
        free(numPtr);  // 释放动态分配的内存
    }
    return 0;
}

3. 函数指针 

函数指针是指向函数的指针变量,它可以存储函数的入口地址,通过函数指针可以调用相应的函数。函数指针的定义形式为:返回类型(*指针名)(参数列表);

#include <stdio.h>
// 定义两个函数
int add(int a, int b) 
{
    return a + b;
}
int subtract(int a, int b) 
{
    return a - b;
}
int main() 
{
    // 定义一个函数指针,指向返回值为int,参数为两个int类型的函数
    int (*funcPtr)(int, int);
    // 让函数指针指向add函数
    funcPtr = add;
    printf("add函数调用结果: %d\n", funcPtr(5, 3));
    // 让函数指针指向subtract函数
    funcPtr = subtract;
    printf("subtract函数调用结果: %d\n", funcPtr(5, 3));
    return 0;
}

4,注意事项(必看)

  1. 内存管理:当函数返回指针时,要确保返回的指针指向有效的内存区域,并且在不再使用时及时释放动态分配的内存,防止内存泄漏。
  2. 空指针检查:在使用函数返回的指针或函数指针之前,最好进行空指针检查,避免对空指针进行操作导致程序崩溃。
  3. 函数指针类型匹配:函数指针的类型必须与所指向的函数的返回类型和参数列表完全匹配,否则会导致编译错误或未定义行为。

九,多级指针

多级指针,也就是指针的指针,在 C 语言里是一个较为高级且强大的特性。下面会从多级指针的定义、初始化、使用场景、注意事项等方面进行详细介绍。

1. 多级指针的定义与初始化

1,定义

多级指针是指指向指针的指针,常见的有二级指针、三级指针等。

数据类型 *指针变量名;    //一级指针
数据类型 **指针变量名;   //二级指针
数据类型 ***指针变量名;  //三级指针

2,初始化

多级指针的初始化需要关联到一个已存在的指针。

#include <stdio.h>
int main() 
{
    int num = 10;
    int *p = &num;
    int **pp = &p;
    printf("通过二级指针访问num的值: %d\n", **pp);
    return 0;
}

2. 多级指针的使用场景

1,动态二维数组

在 C 语言里,可以借助二级指针来动态创建二维数组。

#include <stdio.h>
#include <stdlib.h>
int main() 
{
    int rows = 3, cols = 4;
    int** matrix;
    // 为行指针分配内存
    matrix = (int**)malloc(rows * sizeof(int*));
    if (matrix == NULL) 
    {
        fprintf(stderr, "内存分配失败\n");
        return 1;
    }
    // 为每一行分配内存
    for (int i = 0; i < rows; i++) 
    {
        matrix[i] = (int*)malloc(cols * sizeof(int));
        if (matrix[i] == NULL) 
        {
            fprintf(stderr, "内存分配失败\n");
            return 1;
        }
    }
    // 初始化二维数组
    for (int i = 0; i < rows; i++) 
    {
        for (int j = 0; j < cols; j++) 
        {
            matrix[i][j] = i * cols + j;
        }
    }
    // 输出二维数组
    for (int i = 0; i < rows; i++) 
    {
        for (int j = 0; j < cols; j++) 
        {
            printf("%d ", matrix[i][j]);
        }
        printf("\n");
    }
    // 释放内存
    for (int i = 0; i < rows; i++) 
    {
        free(matrix[i]);
    }
    free(matrix);
    return 0;
}

2,函数参数传递

在函数中使用多级指针作为参数,可以修改调用函数中的指针变量。

#include <stdio.h>
#include <stdlib.h>
void allocateMemory(int** ptr) 
{
    *ptr = (int*)malloc(sizeof(int));
    if (*ptr != NULL) 
    {
        **ptr = 100;
    }
}
int main() 
{
    int* p = NULL;
    allocateMemory(&p);
    if (p != NULL) 
    {
        printf("分配内存中存储的值: %d\n", *p);
        free(p);
    }
    return 0;
}

3. 多级指针的注意事项

  1. 内存管理:当使用多级指针进行动态内存分配时,必须注意内存的分配和释放顺序。一般来说,要先释放内层指针指向的内存,再释放外层指针指向的内存,避免内存泄漏。例如在上面动态二维数组的例子中,要先释放每一行的内存,再释放存储行指针的内存。

  2. 查空指针检:在使用多级指针之前,要进行空指针检查,防止对空指针进行解引用操作,从而避免程序崩溃。比如在分配内存时,如果返回 NULL,就需要进行错误处理。

  3. 理解解引用操作:多级指针涉及多次解引用操作,要清楚每次解引用所访问的对象。例如,二级指针 **pp 需要两次解引用才能访问到最终的数据,理解这个过程有助于正确使用多级指针。

 

 

 

 

 

 

 

;