C语言:指针详解

C语言:指针详解

1:指针的基本概念

1:什么是指针

指针是一个变量,其值为另一个变量的内存地址。换句话说,指针"指向"另一个变量的存储位置。指针的类型决定了它所指向的变量类型。通过指针,我们可以直接访问和操作内存中的数据,这使得指针在C语言中具有非常强大的功能。

2:为什么要引入指针

  • 为函数修改实参提供支持
  • 为动态内存管理提供支持
  • 为动态数据结构(链表,队列等)提供支持。
  • 为内存访问提供另一种途径。
  • 使程序更高效简洁

3:指针的作用

  1. 直接操作内存:指针允许程序员直接访问和操作内存地址,从而实现高效的内存管理。
  2. 动态内存分配 :通过指针可以动态分配和释放内存,例如使用malloccallocfree等函数。
  3. 实现复杂数据结构:指针是实现链表、树、图等复杂数据结构的基础。
  4. 函数参数传递:通过指针可以将变量的地址传递给函数,从而实现对变量的直接修改。
  5. 数组操作:数组名本质上是一个指向数组首元素的指针,通过指针可以方便地操作数组。

4:指针的类型

C 语言中的指针类型非常丰富,主要包括:

  • 基本数据类型的指针(如 int *char * 等)。
  • 数组指针(如 int (*p)[5])。
  • 指针数组(如 int *arr[5])。
  • 函数指针(如 int (*p)(int, int))。
  • 指针函数(如 int *p())。
  • 多级指针(如 int **p)。
  • void 指针(如 void *p)。
  • 结构体指针(如 struct Point *p)。
  • 常量指针与指针常量(如 const int *pint *const p)。
  • 空指针(如 NULL)。
  • 野指针。

2:指针的声明与初始化

1: 指针的声明

指针的声明格式为:

c 复制代码
数据类型 *指针变量名;

例如:

c 复制代码
int* p;//声明一个指向 int 类型的指针变量p

这里, int表示指针所指向的变量类型,* 表示这是一个指针变量,p是变量名。

2: 指针的初始化

c 复制代码
int a = 10;
int* p = &a;//将变量 a 的地址赋给指针p 

3:指针的操作

1 :取地址操作符 &

取值操作符 & 用于获取变量的内存地址 。例如:

c 复制代码
int a = 10;
int *p = &a;  // 获取变量a的地址并赋值给指针p

2 :解引用操作符 *

解引用操作符*用于访问指针所指向的内存中的数据。例如:

c 复制代码
int a = 10;
int *p = &a;
printf("%d", *p);  // 输出10,即指针p所指向的内存中的数据

3 :指针的算术运算

运算类型 说明 示例
指针+整数 指针指向值按照所指向的类型大小向后移整数个单位 int* p = arr; p = p+2;
指针-整数 指针所指向的值按照所指向的类型大小向前移整数个单位 int* p = arr; p = p -2;
指针-指针 两个指针相减的结果是他们之间的元素个数 int* p = arr; int* p2 = arr +1; int dis = p2 -p1;

4: 指针的比较:规则与应用

在C语言中,指针之间的比较是一种常见的操作,通常用于判断指针是否指向同一个地址,或者用于比较指针在内存中的相对位置。指针之间的比较操作包括相等性比较(==!=)和大小比较(<><=>=)。

4.1 :指针的相等性比较
4.1.2:指针的相等性比较(== !=

指针的相等性比较用于判断两个指针是否指向同一个地址。这种比较操作在以下几种情况下非常有用:

1、判断指针是否为空

c 复制代码
int *p = NULL;
if (p == NULL) {
    printf("Pointer is NULL\n");
}

2、判断两个指针是否指向同一个变量:

c 复制代码
int a = 10, b = 20;
int *p1 = &a, *p2 = &b;
if (p1 == p2) {
    printf("p1 and p2 point to the same address\n");
} else {
    printf("p1 and p2 point to different addresses\n");
}

3、判断指针是否指向数组的某个特定位置:

c 复制代码
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr + 2;
if (p == arr + 2) {
    printf("p points to the third element of the array\n");
}
4.1.3:规则
  • 类型兼容性 :进行相等性比较的两个指针必须是兼容的类型,或者其中一个可以隐式转换为另一个的类型。例如,int *char * 是不兼容的类型,但 void * 可以与任何类型的指针进行比较。
  • 空指针比较 :空指针(NULL)可以与任何类型的指针进行比较,结果为布尔值。
4.2 :指针的大小比较
4.2.1:大小比较(< > <= >=)

指针的大小比较 用于判断两个指针再内存中的相对位置。

1、判断指针是否在某个范围内:

c 复制代码
int arr[5];
int* p = arr + 2;
if(p >= arr && p <= arr +5)
{
    printf("p is within the array bounds\n");
}

2、遍历数组

c 复制代码
int arr[5] = {1,2,3,4,5};
int* p = arr;
while(p > arr+5)
{
    printf("%d\n",*p);
    p++;
}

3、比较两个指针的相对位置

c 复制代码
int arr[5] = {1, 2, 3, 4, 5};
int *p1 = arr + 1;
int *p2 = arr + 3;
if (p1 < p2)
{
    printf("p1 is before p2 in memory\n");
}
4.2.2:规则
  • 类型兼容性:进行大小比较的两个指针必须是兼容的类型,或者其中一个可以隐式转换为另一个的类型。
  • 同一数组或对象:两个指针必须指向同一数组或同一对象的元素,或者指向数组或对象的末尾位置(即数组或对象的最后一个元素的下一个位置)。如果两个指针指向不同的数组或对象,比较结果是未定义的。
  • 空指针 :空指针(NULL)可以与任何指针进行大小比较,但结果通常没有实际意义。
4.3:指针比较的合法性
4.3.1合法的指针比较

1、同一数组或对象的指针比较

c 复制代码
int arr[5] = {1, 2, 3, 4, 5};
int *p1 = arr;
int *p2 = arr + 3;
if (p1 < p2) 
{
    printf("p1 is before p2 in memory\n");
}

2、指针与空指针的比较:

c 复制代码
int *p = NULL;
if (p == NULL)
{
    printf("Pointer is NULL\n");
}

3、指针与数组末尾位置的比较

c 复制代码
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr + 5; // 指向数组末尾位置
if (p > arr) 
{
    printf("p is beyond the array bounds\n");
}
4.3.2非法的指针比较

1、指向不同数组或对象的指针比较:

c 复制代码
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5}; // 定义一个整型数组
    int *p1 = arr;                // p1指向数组的首元素
    int *p2 = arr + 3;            // p2指向数组的第四个元素

    // 比较两个指针的大小
    if (p1 < p2)
    {
        printf("p1 is before p2 in memory\n");
        // 如果p1小于p2,输出这条信息
    } 
    else 
    {
        printf("p1 is not before p2 in memory\n");
        // 否则,输出这条信息
    }

    return 0;
}

2、未初始化的指针比较:

c 复制代码
int *p1, *p2;
if (p1 == p2) { // 未定义行为
    printf("p1 and p2 point to the same address\n");
}
4.4:指针比较的应用场景
4.4.1:数组遍历

指针比较常用于数组遍历,判断指针是否超出数组范围。例如:

c 复制代码
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
while (p < arr + 5)
{
    printf("%d\n", *p);
    p++;
}
4.4.2:字符串处理

指针比较也常用于字符串处理,判断指针是否到达字符串末尾。例如:

c 复制代码
char str[] = "Hello, World!";
char *p = str;
while (*p != '\0')
{
    printf("%c", *p);
    p++;
}
printf("\n");
4.4.3:动态内存管理

指针比较用于动态内存管理,判断指针是否指向已分配的内存块。例如:

c 复制代码
int *arr = (int *)malloc(5 * sizeof(int));
if (arr == NULL) {
    printf("Memory allocation failed\n");
} 
else 
{
    int *p = arr;
    while (p < arr + 5)
    {
        *p = 0;
        p++;
    }
    free(arr);
}

4:指针数组与数组指针

在C语言中,"指针数组"和"数组指针"是两个容易混淆的概念,但它们的含义和用途完全不同。它们的区别主要在于类型定义和使用方式。

1:指针数组(Array of Pointers)

1.1:定义

指针数组 首先 他是数组 是存放指针的数组

1.2:声明

指针数组的声明格式如下:

c 复制代码
类型名 *数组名[数组长度];

例如:

c 复制代码
int *arr[10]; // 声明一个包含10个指针的数组,每个指针指向int类型的变量
1.3:使用

指针数组的每个元素都是一个指针,可以分别初始化和使用。例如:

c 复制代码
int a = 10, b = 20, c = 30;
int *arr[3];//声明一个指针数组 其包含三个指针 int 类型的指针
arr[0] = &a;//& 取 a 的地址
arr[1] = &b;
arr[2] = &c;

printf("%d %d %d\n", *arr[0], *arr[1], *arr[2]); // 输出10 20 30  *arr[0] 解引用arr[0] 取到arr[0] 所指的值
1.4:应用场景

• 字符串数组:可以使用指针数组来存储字符串数组。

c 复制代码
#include <stdio.h>

int main() {
    char *str[3] = {"Hello", "World", "CSDN"};
    // 打印数组中的每个字符串
    for (int i = 0; i < 3; i++) 
    {
        printf("%s\n", str[i]);
    }
    // 打印 strArray[1]
    printf("str[1]: %s\n", str[1]);
    return 0;
}

2:数组指针(Pointer to an Array)

2.1:定义

数组指针 就是 指针 指向数组的指针 存放数组的地址

2.2:声明

数组指针的声明格式如下:

c 复制代码
类型名 (*指针变量名)[数组长度];

例如:

c 复制代码
int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5] = &arr;// 声明一个指针 p ,指向一个包含 5 个 int 类型元素的数组
2.3:使用

通过数组指针可以访问数组的元素。例如:

c 复制代码
 // 输出数组arr的第三个元素,即3
int main()
{
    int  arr[5] = {1,2,3,4,5};
    int (*pa)[5] = &arr;
    int i = 0;
    for(i = 0;i < 5;i++)
    {
        //输出数组 arr 的元素
        printf("%d",(*pa)[i]);
        printf("%d",*(pa+i);
        printf("%d",*(*pa + i));       
    }
}
2.4:应用场景

• 多维数组:可以使用数组指针来操作多维数组。

通过指针引用多维数组

表 示 形 式 含义 地 址
a 二维数组名,指向一维数组a[0],即0行首地址
a[0],*(a+0), *a 0行0列元素地址
a+1,&a[1] 1行首地址
a[1],*(a+1) 1行0列元素a[1] [0]的地址 2008
[1]+2,*(a+1)+2,&a[1] [2] 1行2列元素a[1] [2] 的地址 2012
(a[1]+2), * ((a+1)+2), a[1] [2] 1行2列元素a[1][2]的值 元素值为13
c 复制代码
#include <stdio.h>
void aver(int* p,int row,int col)
{
    int sum = 0;
    int ave = 0;
    int count =0;
    for (int i = 0; i < row; i++)//行
    {
        for (int j = 0; j < col; j++)//列
        {
            printf("%d\t",  *((p+i*col)+j));
            sum += *((p+i*col)+j);
            count++;//计数
        }  
        printf("\n");     
    }
    // ave = sum /count;
    printf("aver = %d\n",ave = sum /count);
}
int main(int argc, char const *argv[])
{
    int arr[3][4] = {10,20,30,40,12,23,34,65,56,76,54,45};
    int* p = arr[0];//指针指向第一行首元素位置
    int row = sizeof(arr)/sizeof(arr[0]); //数组的行数
    int col = sizeof(arr[0])/sizeof(arr[0][0]);//数组的列数
    aver(p,row,col);
    return 0;
}

• 函数参数:可以将数组指针作为函数参数传递,以操作多维数组。

c 复制代码
#include <stdio.h>
void change(int (*p)[3],int len)
{
    for (int i = 0; i < len; i++)
    {
        for (int j = i+1; j < len; j++)
        {
            int temp = *(p[i]+j);
            *(p[i]+j)= *(p[j]+i); 
            *(p[j]+i) = temp;
        }        
    }
}

void my_printf(int (*p)[3],int len)
{
    for (int i = 0; i < len; i++)
    {
        for (int j = 0; j < len; j++)
        {
            printf("%-3d \t", *(*(p+i)+j));
        }
       printf("\n");
    }
}

int main(int argc, char const *argv[])
{
    
    int arr[][3] = {1,2,3,4,5,6,7,8,9}; 
    int len = sizeof(arr)/sizeof(arr[0]);  
    printf("转置前:\n");
    my_printf(arr,len);
   
    change(arr,len); 
    printf("转置后:\n");  
    my_printf(arr,len);
    return 0;
}

3:指针数组与数组指针的区别

特性 指针数组(Array of Pointers) 数组指针(Pointer to an Array)
声明 type *arr[size]; type (*arr)[size];
类型 数组,每个元素是一个指针 指针,指向一个数组
用途 存储多个指针,可以指向不同对象 指向一个数组,用于操作整个数组
解引用方式 *arr[i] (*arr)[i]
内存布局 连续存储指针,指针指向其他位置的数据 指向一个连续的数组
3.1:声明格式

• 指针数组:

c 复制代码
int *p[5]= {1,2,3,4,5};  // 声明一个包含5个指针的数组

• 数组指针:

c 复制代码
 int (*p)[5] = {1,2,3,4,5}; // 声明一个指向包含5个int的数组的指针

5:函数指针与指针函数

在 C 语言中,指针函数和函数指针是两个容易混淆但功能不同的概念。它们在声明、使用和用途上都有显著的区别。

1:函数指针(Function Pointer)

函数指针是一个指针,它指向一个函数的入口地址。通过函数指针,可以在运行时 动态地调用不同的函数。

1.1:声明

函数指针的声明格式如下:

c 复制代码
返回值类型 (*变量名)(形式参数列表)

示例

c 复制代码
int (*p)[3];
int (*p)(int a ,int b)
1.2:声明一个函数指针并初始化:

①:定义的同时赋值

c 复制代码
int (*func)(int, int) = add;  // 声明一个函数指针,指向 add 函数

②:先定义,后赋值

c 复制代码
//定义一个普通的函数
int add(int a,int b)(return a + b);
//定义一个函数指针
int(*p)(int a,int b);
//给函数赋值
p = add;

注意:

  • 函数指针指向的函数要和函数指针定义的返回值类型,形参列表对应,否则编译会报错

  • 函数指针是 指针,但不能进行指针运算 如p++等,没有实际意义

  • 函数指针作为形参可以形成回调

  • 函数指针作为形参,函数调用时的实参只能是与之对应的函数名。不能带小括号() 带了表示调用 不是赋值

  • 函数指针的形参列表中的变量名可以省略

1.3:使用

通过函数指针调用函数:

c 复制代码
int result = func(5, 3);  // 调用 add 函数
printf("Result: %d\n", result);  // 输出 8

完整代码示例

c 复制代码
#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int main() {
    int (*func)(int, int) = add;  // 声明并初始化函数指针

    int result = func(5, 3);  // 通过函数指针调用 add
    printf("Result of add: %d\n", result);  // 输出 8

    func = subtract;  // 改为指向 subtract 函数
    result = func(5, 3);  // 通过函数指针调用 subtract
    printf("Result of subtract: %d\n", result);  // 输出 2

    return 0;
}

输出结果

Result of add: 8
Result of subtract: 2

2:指针函数(Pointer Function)

指针函数是一个返回指针的函数。换句话说,它是一个函数,其返回值是一个指针。

2.1:声明

指针函数的声明格式如下:

c 复制代码
指针类型 函数名(形参列表)
2.2:示例

假设我们有一个函数create_array,它动态分配一个整数数组并返回指向该数组的指针:

c 复制代码
int* create_array(int size) 
{
    int* arr = (int*)malloc(size * sizeof(int));
    if (arr == NULL) 
    {
        return NULL;  // 内存分配失败
    }
    return arr;  // 返回指向动态分配数组的指针
}
2.3:使用
c 复制代码
int* get(int a)
{
    int *b = &a;
    return b;
}
int main()
{
 	int* a = get(5);
    printf("%d\n",*a);
}

案例:

c 复制代码
//指针函数
//有若干个学生的成绩,每个学生有四门成绩
//再用户输入学生序号后能输出改学生的全部成绩,用指针函数来实现

//定义一个函数,传入学生的序号,返回学生的所有课程成绩
// p 二维数组
// n 二维数组的索引
// return 学生成绩 (行号对应的列数组)
float *search(float (*p[4]),int n)
{
    //定义一个指针,用来接收查询到的某个学生的所有课程
    float *pt;//
    pt = *(p+n);//*p[n],p[n]
}

int main()
{
    float score[][4] = {{50,43,56,23},{65,7556,56},{11,22,34,65},{55,66,77,88}};
    int m;
    float *p;
    printf("输入学生序号(0~2)\n");
    scanf("%d",&m);
    printf("第%d个娃考了%d分",m);
    //用来接收某个学生的所有成绩
    p = search(score,m);
    
    //遍历成绩
    for(int i = 0;i < 4;i++)
    {
        printf("%-5.2f\t",*(p+i));
    }
    printf("\n");
    return 0;
}

3:区别总结

特性 函数指针(Function Pointer) 指针函数(Pointer Function)
声明格式 返回值类型 (*变量名)(形式参数列表) 指针类型 函数名(形参列表)
用途 指向一个函数,可以通过指针调用函数 返回一个指针,通常用于动态分配或返回指针
示例 int (*p)(int a ,int b) int* p(int a,int b)
调用方式 p(int a,int b) int* p = p(int a,int b)
返回值 指向函数的指针 返回一个指针
3.1:使用场景
3.1.1:函数指针

• 回调函数:

• 在事件处理、多态实现或算法选择中使用函数指针。

• 例如,标准库中的qsort函数使用函数指针作为比较函数。

• 动态函数调用:

• 在运行时根据条件选择调用不同的函数。

3.1.2:指针函数

• 动态内存分配:

• 返回动态分配的内存块的指针。

• 例如,malloccalloc返回指向动态分配内存的指针。

• 链表或树的节点创建:

• 返回指向新创建的节点的指针。

4:总结

• 函数指针是一个指针,指向一个函数,可以通过指针调用函数。

• 指针函数是一个函数,返回一个指针。

• 它们的声明和使用方式不同,用途也不同。理解它们的区别可以帮助你更灵活地使用 C 语言。

6:字符数组和字符指针

1:字符串的实现

再c语言中,表示一个字符串有以下两种形式:

①:用字符串数组存放一个字符串。

②:用字符指针指向字符串。

1.1:案例:两种实现方式
c 复制代码
//字符串的两种实现方式
//第一种:使用 字符数组 实现字符串
void str_test1()
{
    //定义一个伪字符串
    char str[] = "I LOVE YOU";
    printf("%s\n".str);
}
//第二种:使用 字符指针变量 指向字符串
void str_test2()
{
    //定义一个伪字符串
    char *str = "I LOVE YOU";
    printf("%s\n",str);
   
}
int main()
{
     str_test1();
     str_test2();
}

2:字符数组与字符指针的联系

2.1:字符数组:

由元素构成,每个元素存放一个字符

只能对字符数组中的各个元素赋值,不能用赋值语句对整个字符数组赋值

2.2字符指针:

字符类型的指针变量,存放的是地址,也能作为函数

c 复制代码
#include <stdio.h>
//字符数组和字符指针的联系
int main()
{
    char str1[] = "你好,xb";
    char *str2 = "你好,xb";//我们将数据类型为char的指针变量称为字符指针
    
    //测试赋值
    //srt1 =  "你好,xw";不能对字符数组整体赋值 ,如果要赋值 ,使用string 下的 strcpy()
    str2 =  "你好,xw";
    
    printf("%s\n%s\n",str1,str2);
    
    char a[] =  "I LOVE YOU";
    char *b =  "I LOVE YOU";
    //
    printf("%c,%c,%c,%c\n",a[2],*(a+2),b[2],*(b+2));
}

3:字符串作为形式参数

3.1:实参与形参都可以是字符数组
c 复制代码
void fun(char str[],int len){......}
void main()
{
    char str[] = "I LOVE YOU";
    fun(str,sizeof(str) /sizeof(str[0]);
}
3.2实参用字符数组,形参用字符指针(再函数内部不能对字符串中的字符做修改)
c 复制代码
void fun(char *str,int len){}//(str已经是一个常量了不能修改)
void main()
{
    char *str = "I LOVE YOU";
   fun(str,sizeof str /sizeof(str[0]);
}
3.3:实参是指针类型,形参是字符数组
c 复制代码
void fun(char str[],int len){
    str[2] = 'A';
}//(str已经是一个常量了不能修改)
void main()
{
    char *str = "I LOVE YOU";
   fun(str,sizeof(str) /sizeof(str[0]);
}
  • 字符数组在创建的时候会在内存中开辟内存空间 ,内存空间可以存放内存数据,字符指针再创建的时候需要依赖于字符数组在内存开辟的内存空间中,存放的是数组元素的地址。字符指针的创建依赖于字符数组,字符数组可以独立存在,而字符指针不能独立存在。

  • 字符数组可以初始化,但不能赋值;字符指针可以初始化,也可以赋值。

3.4:案例

一、

c 复制代码
#include <stdio.h>
//字符指针作为函数参数
//用函数调用实现字符串的复制以及长度计算
//定义一个函数,实现字符串的拷贝,返回字符串长度
//source 拷贝的源字符串
//target 需要保存拷贝数据的目标字符串
//return 字符串的大小

int str(char *source,char *target)
{
    int i = 0;
    while(source[i] != '\0')
    {
        *(target+i) = *(source + i);
        i++;
        
    }
    target[i] = '\0';
   
    return i;

}

int main(int argc,char *argv[])
{
    char source[20],target[20];
    printf("输入字符串:");
    scanf("%s",source);
    int len = str(source,target);
    //int len = sizeof(source) / sizeof(source[0]);
    printf("%s%s,%d",source,target,len);
    return 0;
}

二、

c 复制代码
#include <stdio.h>

//字符指针作为函数参数
//给定一个字符串,截取 start 到 end之间的字符串,含头不含尾
//定义一个函数,实现字符串的截取
//@param source 源字符串
//start 开始截取的位置
//end 截取结束的位置
//target 截取后的字符串

int str_split(char *source,char *target,int start,int end)
{
    int i = 0,k =0;
    while(source[i] != '\0')
    {
        if(i >= start && i <= end )
        {
            target[k] = source[i];
            k++;
        }
        i++;
    }
    return k;

}

int main(int argc,char *argv[])
{
    char *source = "sdfafdsfgfer";
    char target[20];
    int len = str_split(source,target,2,5);
    printf("%-20s,%-20s,%-20d\n",source,target,len);

    return 0;
}
3.5main函数原型
  • argc,argv是形参,他们俩可以修改

  • main函数的扩展写法有些编译器不支持,编译报警

  • argc和argv的常规写法:

    • argc:存储了参数个数,它至少为 1

    • argv:存储了所有参数的字符串形式

  • 参数通过控制台输入

  • main函数是系统通过函数指针的回调形式调用的

7:野指针、空指针、空悬指针

1:野指针

1.1:定义

指向一块位置区域(已经销毁或者访问受限的内存区域外的已经存在或者不存在的内存区域)的指针称为野指针。野指针是危险的。

1.2:危害
  • 引用野指针,相当于访问了非法的内存,常常会导致段错误,也有可能编译运行不报错。

  • 引用野指针,可能会破环系统的关键数据,导致系统崩溃等严重后果

2:野指针产生的场景:
2.1:遍历未初始化,通过指针访问该变量
c 复制代码
int a;
int* p = &a;//野
pritnf("%d\n",*p);//野
2.2:指针未初始化
c 复制代码
int *p;//野
printf("%d\n",p);//野
2.3:指针指向的内存空间被(free)回收了
c 复制代码
int* p = malloc(4);
*p = 12;

free(p);
printf("%d\n",*p);//野
2.4:指针函数中 直接返回了局部变量的地址
c 复制代码
int* get_num()
{
    int a = 15;
    int* p = &a;//此时p对应的是一个局部变量
    return 0;
}
main()
{
    int* p = get_num();//此时p是野指针
}
3:如何避免野指针:
  • 指针变量要及时初始化,如果暂时没有对应的值,建议赋初值NULL。

  • 数组操作(主要是遍历和指针运算)时,注意数组的长度,避免越界

  • 指针指向的内存空间被回收,建议给这个指针变量赋值为NULL。

c 复制代码
int* p = (int* )malloc(10);
free(p);
p = NULL;
  • 指针变量使用之前要检查它的有效性(非空校验)
c 复制代码
int* p = NULL;
//if(p==NULL)
if(!p)
{
    return -1;
}

2:空指针

很多情况下,我们不可避免的会遇到野指针,比如刚定义的指针无法立即为其分配一块恰当的内存,又或者指针指向的内存已经被释放了等等。一般的做法是将这些危险的野指针指向一块确定的内存,比如零地址内存(NULL)。

1:定义:

空指针即保存了零地址的指针(赋值为NULL的指针),也就是指向零地址的指针。(NULL是空常量,它的值是0,这个NULL一般存放在内存0x0000 0000的位置,这个地址只能存NULL,不能被其他程序修改)

案例:

c 复制代码
// 1. 刚定义的指针,让其指向零地址以确保安全
char* p1 = NULL;
int* p2 = NULL;
// 2. 被释放了内存的指针,让其指向零地址以确保安全
char *p3 = malloc(100);
free(p3);
p3 = NULL;
int sum = 0;

3:空悬指针

在C语言中,悬空指针指的是指向已删除(或释放)的内存位置的指针。如果一个指针指向的内存已经被释放,但指针本身并未重新指向其他有效的内存地址,那么这个指针就变成了悬空指针。悬空指针会引发不可预知的错误,并且如果一旦发生,就很难定位,因此在编程中尽量避免使用悬空指针。

c 复制代码
// 2. 被释放了内存的指针,让其指向零地址以确保安全
char *p3 = malloc(100);
free(p3);
printf("%p,%c\n",p3,*p3);// 此时地址依然可以访问,但是地址对应的原本数据不可访问

4:void 与void* 的区别

4.1:定义:

void:是空类型,是数据类型的一种

void*:是指针类型,是指针类型的一种,可以匹配任意类型的指针,类似与通配符,又被叫做万能指针。

4.2:void:
4.2.1:说明:

void作为返回值类型使用,表示没有返回值;作为形参,表示形参列表为空,在调用的时候不能给实参

案例:

c 复制代码
// 函数定义
void fun(void){..} // 等效于 void fun(){..}
// 函数调用
fun();
4.3:void*:
4.3.1:说明:
  • void是一个指针类型,但该指针的数据类型不明确,无法通过解引用获取内存中的数据,因为 void 不知道访问几个内存单元。
  • void*是一种数据类型,可以作为函数 返回值类型 ,也可以作为 形参类型
  • void*类型的变量在使用之前必须强制类型转换,明确它能够访问几个自己的内存空间
4.3.2:举例:
c 复制代码
#include <stdio.h>
#include <stdlib.h>
// 函数定义
void* fun(void* p) // 指针函数(返回值类型是指针的函数,此时返回的是不明确类型,需要外部强转)
{
    int *p;
    // double *p;
    // long *p;
    // char *p;
    return p;
}
// 函数调用
void main()
{
    int m = 10;
    int *p = &m;
    void* a = fun(p);// 这种接收方式,实际上没有意义,推荐:int *a = (int*)fun(p);
    printf("%p\n",a);// 可以正常打印,打印出一个地址
    *a = 10;// 编译报错,void*变量不能解引用访问数据
    int *w = (int*)a;
    *w = 10;// 编译和运行正常,void*变量a在使用前已经强制类型转换了,数据类型明确了,访问的内存
    单元明确了。
}

注:

void*作为返回值类型,这个函数可以返回任意类型的指针

void*作为形参类型,这个函数在调用时,可以给任意类型的指针

void类似于通配符,不能对 void 类型的变量解引用(因为不明确数据类型,所以无法确定内存单元的大小)

void*在间接访问(解引用)前要强制类型转换,但不能太随意,否则存和取的数据类型不一致

8:动态内存分配

1:malloc

1.1:说明:
  • 原型: void* malloc(size_t size);
  • 功能:分配一块指定大小的内存,返回指向该内存的指针。
  • 返回值:成功时返回指向分配内存的指针,失败时返回 NULL 。
  • 注意:分配的内存未初始化,内容是未定义的。
1.2:案例:
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main()
{
    int* p = (int*)malloc(8*sizeof(int));
    if(!p)//if(p == NULL)
    {
        printf("申请失败");
        printf("%s\n",strerror(errno));
    }
    else
    {
        int i = 0;
        for(i = 0; i < 10;i++)
        {
            *(p+i) = i;
            
        }
        for(i = 0;i < 10;i++)
        {
            printf("%d",*(p+i));
        }
    }
    free(p);
    p = NULL;
    return 0;
}

2:calloc

2.1:说明:
  • 原型: void* calloc(size_t num, size_t size);
  • 功能:分配一块足够存储 num 个大小为 size 的元素的内存,并将内存初始化为零。
  • 返回值:成功时返回指向分配内存的指针,失败时返回 NULL 。
  • 注意:分配的内存会被初始化为零。
2.2:案例:
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main()
{
    //在堆内申请10块int类型的内存大小
    int* p = (int*)calloc(10,sizeof(int));
    //如果申请失败 返回-1
    if(p == NULL)
    {
        printf("申请失败");
     	return -1;
    }
    else
    {
        int i = 0;
        for(i = 0;i < 10;i++)
        {
            *(p+i) = i;
        }
        for(i = 0; i < 10;i++)
        {
            printf("%d",*(p+i));
        }
    }
    free(p);
    p = NULL;
    return 0;
}

3:realloc

3.1:说明:
  • 原型: void* realloc(void* ptr, size_t size);
  • 功能:重新分配一块内存,调整其大小为 size 。如果 ptr 是 NULL ,行为等同于 malloc(size) 。
  • 返回值:成功时返回指向新分配内存的指针,失败时返回 NULL ,原始内存保持不变。
  • 注意:如果内存块被移动, realloc 会复制原始内存的内容到新位置。
3.2:案例:
c 复制代码
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>

int main()
{
    int* p =(int*)malloc(10);
    //int* p = (int*)realloc(p,50);
    
    
    //第一种情况
    //申请了10块空间
    //用了5 块
    //发现不够用
    //需要在加  少于5个空间的时候
    //用recalloc
    //返回的地址仍然是 定义的 p的地址

    //第二种情况
    //需要再加超出5 个地址的时候
    //超出的空间就属于被非法访问
    //这个时候  realloc就会 重新开辟一块包含空间
    //这个空间 里边 包含着之前开辟的 p
    //如下图
    //这个时候超过五个空间的部分
    //如果没有赋值
    //系统就会自动 赋随机值
    //返回新的地址, 旧的地址 ralloc 会释放
    if(p ==NULL)//if(!p)
    {
        printf("申请失败");
        printf("%s",strerror(errno));

    }
    else
    {
        int i = 0;
        for(i = 0;i < 5;i++)
        {
            *(p+i) = i;
        }
    }

    int* ptr = realloc(p, 50);
    if(ptr != NULL)
    {
        ptr = p ;
        int i = 0;
        for(i = 5;i < 10;i++)
        {
            *(ptr+i) = i;
        }
        for(i = 0;i < 10;i++)
        {
            printf("%d",*(ptr+i));
        }
    }
    free(ptr);
    ptr = NULL;
    return 0;
}

4:free

4.1:说明
  • 原型: void free(void* ptr);
  • 功能:释放之前分配的内存块。 ptr 必须是之前通过 malloc 、 calloc 或 realloc 分配的内存块的指针返回值:无返回值。
  • 注意:释放内存后, ptr 指向的内存不再属于程序,应将 ptr 设置为 NULL ,避免悬空指针

5:注意

1: 检查内存分配是否成功:

在使用 malloc 、 calloc 或 realloc 后,必须检查返回值是否为 NULL ,以确保内存分配成功。

2.:释放内存:
  • 使用 free 释放动态分配的内存,避免内存泄漏。
  • 释放内存后,应将指针设置为 NULL ,避免悬空指针
3.:内存初始化:

malloc 分配的内存内容是未定义的,如果需要初始化为零,可以使用 calloc 或手动初始化。

4: realloc 的使用:
  • 如果 realloc 返回 NULL ,原始内存仍然有效,需要手动释放。
  • 如果内存块被移动, realloc 会复制原始内存的内容到新位置

9:内存操作

1:常用的内存操作函数

1:内容填充(memset)
  • 头文件: #include <string.h>
  • 函数原型: void* memset(void* s,int c,size_t n)
  • 函数功能:填充s开始的堆内存空间前n个字节,使得每个字节值为c
  • 函数参数:

​ void* s:代操作内存首地址

​ int c:填充的字节数据

​ size_t n:填充的字节数

  • 返回值:返回s
  • 注意:c常常设置为0,用于动态内存初始化

案例:

c 复制代码
#include <stdio.h>
#include <string.h>
/*void *memset(void *s,int c,size_t n)
*s:指向要填充的内存区域的指针
*c: 要填充的值(无符号值)
*n: 要填充的字节数
*/   
int fun1()
{
    //初始化数组
    int arr[] = {1,2,3,4,5};
    
    //将arr 中的前三个元素初始化为0
    memset(arr,0,3*sizeof(int));
    int len = sizeof(arr) / sizeof(arr[0]);
    for(int i = 0;i < len;i++)
    {
        printf("%d",arr[i]);
    }
    printf("\n");
    return 0;
}
    
//初始化数组
int fun2()
{
    int arr[10];
    //将arr中的元素都初始化为0
    memset(arr,0,sizeof(arr));
    int i = 0;
    for(i = 0;i < 10;i++)
    {
        printf("%d",arr[i]);
    }
    printf("\n");
    return 0;
}
//设置特定值
int main()
{
	char str[10];
   	//将字符串初始化为 *
    memset(str,'*',sizeof(str)-1);
    str[9] = '\0';
    printf("%s\n",str);
    fun1();
    fun2();
    return 0;
}
2:内容拷贝(memcpy || memmove)
  • 头文件: string.h

  • 函数原型:

    c 复制代码
    void* memcpy(void* dest,const void* src,size_t n) 

    适合目标地址与源地址内存无重叠的情况

    c 复制代码
    void* memmove(void* dest,const void* src,size_t n)
  • 函数功能:拷贝src开始的堆内存空间前n个字节,到dest对应的内存中。

  • 函数参数:

​ void* dest:目标内存首地址

​ void* src:源内存首地址

​ size_t n:拷贝的字节数

  • 返回值:返回dest
  • 注意:内存申请了几个内存空间,就访问几个内存空间,否则数据不安全
  • 注意:memcpy与memmove一般情况下是一样的,更建议使用memmove进行内存拷贝;
  • 因为memmove函数是从自适应(从后往前或者从前往后)拷贝,当被拷贝的内存和目的地的内存有重叠时,数据不会出现拷贝错误。而memcpy函数是从前往后拷贝,当被拷贝的内存和目的地内存有重叠时,数据会出现拷贝错误。

案例:

c 复制代码
#include <stdio.h>
#include <string.h>
int main(int argc,char *argv[])
{
    // 创建源空间和目标空间
    int src[4] = {11,22,33,44};
    int dest[6] = {111,222,333,444,555,666};
    
    // 将src中的数据拷贝dest中
    //dest+1 ->222  src+1 -> 22
    memmove(dest+1,src+1,2 * sizeof(int));
    // 测试输出
    printf("源数组-src:\n");
    //遍历数组
    for(int i = 0;i < 4;i++)
        printf("%-5d",src[i]);
    printf("\n目标数组-dest:\n");
    for(int i = 0;i < 6;i++)
        printf("%-5d",dest[i]);
    printf("\n");
    return 0;
}
3:内存比较(memcmp)
  • 头文件: #include <string.h>

  • 函数原型:

    c 复制代码
     int memcmp(void *dest,const void *src,size_t n)
  • 函数功能:比较src和dest所代表的内存前n个字节的数据

  • 函数参数:

​ void* dest:目标内存首地址

​ const void* src:源内存首地址

​ size_t n:比较的字节数

  • 返回值:

​ 0 :数据相同

​ >0 :dest中的数据大于src(正值)

​ <0 :dest中的数据小于src (负值)

  • 注意:n一般和src,dest的总容量一致;如果不一致,内存比较的结果就不确定了。
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc,char *argv[])
{
    // 申请内存
    int* src = (int*)malloc(3*sizeof(int));
    int* dest = (int*)calloc(4,sizeof(int));
    if(!src || !dest)
    {
        perror("内存申请失败!");
        return -1;
    }
    // 对使用malloc申请的空间清零
    memset(src,0,3*sizeof(int));
    
    //初始化src he dest的指向区域
    *src = 65;
    *(src+1) = 66;
    *dest = 70;
    *(dest+1) = 5;
    
    //使用memcmp比较src 和dest的前8个字节
    int result = memcmp(dest,src,2 * sizeof(int));
    
    //将src 和 dest 转化为char* 类型的指针a和b
    char *a = (char*)dest;
    char *b = (char*)src;
    
    //用memcmp 比较a 和 b的前一个字节
    int result2 = memcmp(b,a,sizeof(char));
    printf("%d,%d\n",result,result2);
    
    //释放申请的src dest内存
    free(src);
    free(dest);

    //将src dest 置空
    src = NULL;
    dest = NULL;
    return 0;
}
4:内存查找(memchr || memrchr)
  • 头文件: #include <string.h>

  • 函数原型:

    c 复制代码
    void *memrchr(const void *s, int c, size_t n);
    void *memchr(const void *s, int c, size_t n);

    函数功能:

    • memchr: 在 s 指向的内存区域的前 n 个字节中==正向查找==最后一次出现的字符 c 。如果找到匹配的字符,返回指向该字符的指针;如果未找到,则返回 NULL 。

    • memrchr: 在 s 指向的内存区域的前 n 个字节中==反向查找==最后一次出现的字符 c 。如果找到匹配的字符,返回指向该字符的指针;如果未找到,则返回 NULL 。

  • 函数参数:

​ const void *s:代操作内存首地址

​ int c:待查找的字节数据

​ size_t n:查找的字节数

  • 返回值:返回查找到的字节数据地址
  • 注意:如果内存中没有重复数据,memchr和memrchr结果是一样的;如果内存中有重复数据,memchr和memrchr结果就不一样

案例(memchr)

c 复制代码
#include <stdio.h>
#include <string.h>
int main(int argc, char const *argv[])
{
    //初始化字符串
    char ch[] = "hello world";

    //定义要查找的字符
    char ch_s = 'o';

    //计算字符串的长度(不包括\0)
    int len = strlen(ch);

    //用 menchr 查找'o'在字符串的位置
    char * s = (char*)memchr(ch,ch_s,len);

    //找到了打印位置
    if(s != NULL)
    {
        printf("%d",*s);
        printf("%c的位置在:%ld\n",ch_s,(long)(s-ch));
    }
    else
    {
        printf("没找到%c!\n",ch_s);
    }


    return 0;
}

案例(memrchr)

c 复制代码
#include <stdio.h>
#include <string.h>
#include <strings.h>

int main(int argc, char const *argv[])
{
    //初始化字符串
    char ch[] ="hello world";

    //定义要查找的字符
    char char_s = 'o';

    //计算字符串的长度(不包括\0)
    int len = strlen(ch);

    //用memrchr函数查找字符
    char* s = (char*)memrchr(ch,char_s,len);
    if(s != NULL)
    {
        printf("%c的位置在%ld\n",char_s,(long)(s-ch));
    }
    else
    {
        printf("没找到%c\n",char_s);
    }
    return 0;
}

注意

est 置空

src = NULL;

dest = NULL;

return 0;

}

#### 4:内存查找(memchr || memrchr)

- 头文件: #include <string.h>

- 函数原型:

  ```c
  void *memrchr(const void *s, int c, size_t n);
  void *memchr(const void *s, int c, size_t n);

函数功能:

  • memchr: 在 s 指向的内存区域的前 n 个字节中==正向查找==最后一次出现的字符 c 。如果找到匹配的字符,返回指向该字符的指针;如果未找到,则返回 NULL 。

  • memrchr: 在 s 指向的内存区域的前 n 个字节中==反向查找==最后一次出现的字符 c 。如果找到匹配的字符,返回指向该字符的指针;如果未找到,则返回 NULL 。

  • 函数参数:

​ const void *s:代操作内存首地址

​ int c:待查找的字节数据

​ size_t n:查找的字节数

  • 返回值:返回查找到的字节数据地址
  • 注意:如果内存中没有重复数据,memchr和memrchr结果是一样的;如果内存中有重复数据,memchr和memrchr结果就不一样

案例(memchr)

c 复制代码
#include <stdio.h>
#include <string.h>
int main(int argc, char const *argv[])
{
    //初始化字符串
    char ch[] = "hello world";

    //定义要查找的字符
    char ch_s = 'o';

    //计算字符串的长度(不包括\0)
    int len = strlen(ch);

    //用 menchr 查找'o'在字符串的位置
    char * s = (char*)memchr(ch,ch_s,len);

    //找到了打印位置
    if(s != NULL)
    {
        printf("%d",*s);
        printf("%c的位置在:%ld\n",ch_s,(long)(s-ch));
    }
    else
    {
        printf("没找到%c!\n",ch_s);
    }


    return 0;
}

案例(memrchr)

c 复制代码
#include <stdio.h>
#include <string.h>
#include <strings.h>

int main(int argc, char const *argv[])
{
    //初始化字符串
    char ch[] ="hello world";

    //定义要查找的字符
    char char_s = 'o';

    //计算字符串的长度(不包括\0)
    int len = strlen(ch);

    //用memrchr函数查找字符
    char* s = (char*)memrchr(ch,char_s,len);
    if(s != NULL)
    {
        printf("%c的位置在%ld\n",char_s,(long)(s-ch));
    }
    else
    {
        printf("没找到%c\n",char_s);
    }
    return 0;
}

注意

memrchr 是 POSIX 标准的一部分,而不是 C 标准库的一部分。所以在某些编译器下不支持,如果你的编译器不支持 最好使用标准C库函数 memchr

相关推荐
小梁不秃捏2 小时前
深入浅出Java虚拟机(JVM)核心原理
java·开发语言·jvm
我不是程序猿儿3 小时前
【C】识别一份嵌入式工程文件
c语言·开发语言
王亭_6663 小时前
VSCode集成deepseek使用介绍(Visual Studio Code)
ide·vscode·编辑器·deepseek·openrouter
软件开发技术局4 小时前
撕碎QT面具(8):对控件采用自动增加函数(转到槽)的方式,发现函数不能被调用的解决方案
开发语言·qt
周杰伦fans5 小时前
C#中修饰符
开发语言·c#
yngsqq5 小时前
c# —— StringBuilder 类
java·开发语言
赔罪5 小时前
Python 高级特性-切片
开发语言·python
爱健身的小范6 小时前
记录一下VScode可以使用nvcc编译,但VS不行的解决方案
ide·vscode·编辑器
翻滚吧键盘6 小时前
vscode复制到下一行
ide·vscode·编辑器
avi91116 小时前
[AI相关]Unity的C#代码如何简写
unity·c#·语法糖