16-C语言:第17天笔记

发布于:2025-07-31 ⋅ 阅读:(22) ⋅ 点赞:(0)

C语言:第17天笔记

内容提要

  • 指针
    • 二级指针
    • main函数原型
    • 常量指针与指针常量
    • 野指针、空指针、空悬指针
    • void与void*的区别
  • 内存管理

指针

二级指针

定义

二级指针(多重指针)用于储存一级指针的地址,需要两次解引用才能访问原始数据。其他多重指针的用法类似,但实际开发中最常见的指针是二级指针。

int a = 10;    // a是普通变量,也就是原始数据
int *p = &a;   // 一级指针,p指向a,解引用1次就可以获取a的值
printf("%d\n", *p); // 10

int **w = &p;  // 二级指针,w指向p,解引用2次就可以获取a的值
printf("%d\n", **w);// 10
-----------------------------------------------------------------
int ***k = &w; // 三级指针,k指向w,解引用3次就可以获取a的值
printf("%d\n", ***k);  // 10   int a1 = ***k;  int *a2 = **k;  int **a3 = *k; int ***a4 = k;
语法
数据类型 **指针变量名 = 指针数组的数组名 | 一级指针的地址
特点

与指针数组的等效性 二级指针与指针数组在某些时候存在等效性,但与二维数组不等效。二维数组名是数组指针类型,如int (*)[3],而非二级指针。

// 指针数组
int arr[] = {11,22,33};
int *arr_[] = {&arr[0],&arr[1],&arr[2]};

// 二级指针接收指针数组
char *str[3] = {"abc","aaa034","12a12"};
char **p = str;  // p:数组首地址,行地址,默认0行  *p:列地址,默认0行0列  **p:列元素




#include <stdio.h>

int main(int argc,char *argv[])
{
    // 字符串数组,字符类型的指针数组
    char *str[3] = {"abc","aaa034","12a12"};
    char **p = str;// str表示数组首地址,其实就是首元素地址  abc 这个字符串
    // p 存储的就是 abc的地址,指向的行,*p 访问到abc的列,默认首列  
    
    // char str[] = "hello";
    // printf("%s\n", str);
    // char str2[20];
    // scanf("%s", str2); // str2就是一个地址

    // 打印字符串
    // for (int i = 0; i < 3; i++)
    // {
    //    printf("%s\n", *p);
    //    p++;
    // }
    
    // 打印字符
    int i = 0;
    while(**p != '\0')
    {
        printf("%-2c",**p);
        (*p)++;
    }
    printf("\n");
    return 0;
}

与二维数组的差异 二维数组名是数组指针类型,直接赋值给二级指针会导致类型不匹配

// 数组指针可以指向一个二维数组
int arr[2][3] = {{1,3,5},{11,33,55}};
int (*p)[3] = arr;  // 数组名默认代表首元素地址,在二维数组中,就是行地址

// 二级指针不等效二维数组
int **k = arr; // 编译报错 arr类型 int(*)[3]   不兼容 k类型 int**
解引用

字符型二级指针 可直接遍历字符串数组,类似一维数组操作:



#include <stdio.h>

void fun1()
{
    // 定义一个字符类型的指针数组(字符串数组)
    char *arr[] = {"orange","apple","grape","banana","kiwi"};
    int len = sizeof(arr) / sizeof(arr[0]); // int len = 5 * 8(指针) / 8(指针) = 5

    for (int i = 0; i < len; i++) printf("%s\n", arr[i]); printf("\n");

}

void fun2()
{
    char *arr[] = {"orange", "apple", "grape", "banana", "kiwi"};
    int len = sizeof(arr) / sizeof(arr[0]);

    // 此时二级指针完全等价于指针数组
    char **p = arr;  // p 指向 arr的首元素,也就是orange

    for (int i = 0; i < len; i++)
    {
        // printf("%s\n", p[i]);    // 下标法
        printf("%s\n", *(p + i));// 指针法
    }
    printf("\n");
}

void fun3()
{
    char *arr[] = {"orange","apple","grape","banana","kiwi"};

    int len = sizeof(arr) / sizeof(arr[0]);

    char **p;

    int i = 0;

    // 遍历数组
    do
    {
        p = arr + i; // arr代表行,+i,此时是行偏移,返回的行地址   p指向字符串
        printf("%s\n", *p);// 对行地址解引用得到列地址   int a = 10; int *p = &a;
        i++;
    } while (i < len);

    printf("\n");
}

int main(int argc,char *argv[])
{
   
    fun1();
    fun2();
    fun3();

    return 0;
}

注意:如果需要一个字符串类型的数组,我们可以选择使用二级指针或者指针数组,此时两者完全等价。

其他类型的二级指针 需要两次解引用访问数据,常用于操作指针数组

#include <stdio.h>

int main()
{
	// 创建一个一维数组
	int arr1[] = {11,22,33,44,55,66}; // 11:0x11
	// 创建一个指针数组
	int *arr[] = {&arr1[0],&arr1[1],&arr1[2],&arr1[3],&arr1[4],&arr1[5]}; // [0]:0x22 --> 0x11
	
	// 用一个二级指针接收指针数组
	int **p = arr;  // p 指向 arr,p存储的arr第一个元素的地址
	
	// 遍历数组
	for(int i=0;i<sizeof(arr)/sizeof(arr[0]);i++){
		printf("%-6d", *p[i]); // 下标法(1.指针偏移,2.对新指针解引用)
		printf("%-6d", **(p+i));// 指针法  p+i  元素地址偏移  元素地址,对元素地址解引用,返回元素值(11..对应的地址)
	}
	printf("\n");
}
总结
类型 本质 内存布局 等效性
二级指针(int** 指向指针的指针 指针的指针 与指针数组等效
指针数组(int*[] 元素为指针的数组 分散的指针 退化为二级指针
二维数组(int[][3] 数组的数组 连续的数据块 数组指针(int(*)[3]

main函数原型

定义

main函数有多种定义格式,main函数也是函数,函数相关的结论对main函数也有效。

main函数的完整写法:

 int main(int argc, char *argv[]){..}
 int main(int argc, char **argv){..}

扩展写法:

 main(){}  等价 int main(){}   // C11之后不再支持 缺省 返回类型
 int main(void){}   等价 int main(){}
 void main(void){}  等价 void main(){}
 int main(int a){}
 int main(int a, int b, int c){}
 ...
说明

① argc,argv是形参,他们俩可以修改

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

③ argc和argv的常规写法

  • argc:存储了参数的个数,默认是1个,也就是运行程序的名字
  • argv:存储了所有参数的字符串形式

④ main函数是系统通过函数指针的回调调用。

演示

代码:

#include <stdio.h>

int main(int argc, char **argv)  // {"abc","aaa"}   对行地址解引用,得到首列地址
{
	// 访问参数个数 argc
	printf("argc=%d\n", argc);
	
	// 遍历参数(每一个参数都是一个字符串常量)
	for(int i=0;i< argc; i++){
		printf("%s,%s\n", argv[i], *(argv+i));
	}
	printf("\n");
}

运行结果:

在这里插入图片描述

常量指针与指针常量

常量类型

① 字面量:直接使用固定值(如:12,hello,orange, 杨家辉三角),符号常量和枚举在编译器转换为了字面量

② 只读常量:用const修饰的变量,初始化之后不可修改。

const int a = 10;  // 只读常量
a = 21;  // 编译报错
常量指针
  • 本质:指向常量数据的指针

  • 语法:

    const 数据类型 *变量名;
    const 数据类型* 变量名;
    
  • 举例:

    const int *p;   // p是常量指针
    
  • 特性:

    • 指向对象的数据不可改变(int a = 10; const int *p = &a; *p = 20;,非法)
    • 指针本身的指向可以改变(int a = 10, b = 20; const int *p = &a; p = &b;,合法)
  • 案例:

    #include <stdio.h>
    
    int main()
    {
    	int a = 10;        // 变量
    	const int *p = &a; // 常量指针
    	
    	// *p = 100;       // 错误,指针指向的数据不可改变
    	printf("%d\n", *p);// 10
    	
    	int b = 20;        // 变量
    	p = &b;            // 正确,指针指向可以改变
    	printf("%d\n", *p);// 20
    }
    
指针常量
  • 本质:指针本身是常量,指向固定地址

  • 语法:

    数据类型* const 变量名;
    数据类型 *const 变量名;
    
  • 特性:

    • 指向对象的数据可以改变(int a = 10; int* const p = &a; *p = 20;,合法)
    • 指针本身的指向不可改变(int a = 10, b = 20; int* const p = &a; p = &b;,非法)
  • 注意:

    定义时必须初始化:

    int a = 10;
    int* const p = &a;  // 正确
    
  • 案例:

    #include <stdio.h>
    
    int main()
    {
    	int a = 10;        // 变量
    	int* const p = &a; // 指针常量
    	
    	*p = 100;          // 正确,指针指向的数据可以改变
    	printf("%d\n", *p);// 100
    	
    	int b = 20;        // 变量
    	// p = &b;            // 错误,指针指向不可改变
    	printf("%d\n", *p);// 100
    }
    
常量指针常量
  • 本质:指针指向和指向对象的数据都不可改变

  • 语法:

    const 数据类型* const 变量名;
    const 数据类型 *const 变量名;
    
  • 举例:

    const int* const p;  // p是常量指针常量
    
  • 特性:

    • 指向对象的数据不可改变(int a = 10; int* const p = &a; *p = 20;,非法)
    • 指针本身的指向不可改变(int a = 10, b = 20; int* const p = &a; p = &b;,非法)
  • 注意:

    定义时需要初始化:

    int a = 10;
    const int *const p = &a; // 正确
    

    简单理解:不管是常量指针、指针常量还是常量指针常量,本质上都是一个赋值受到限制的指针变量。

总结对比
类型 语法 指向可变 数据可变
常量指针 const int *p ✔️
指针常量 int *const p ✔️
常量指针常量 const int *const p
关键点
  1. const* 左侧:修饰数据(常量指针)
  2. const* 右侧:修饰指针(指针常量)
  3. 函数参数优先使用常量指针,提高代码安全性
  4. 指针常量必须初始化,且不可重新指向

野指针、空指针、空悬指针

野指针

定义

指向无效内存区域(比如未初始化、已释放或者越界访问)的指针称之为野指针。野指针会导致未定义(UB)行为。

危害:

  • 访问野指针可能引发段错误(Segmentation Fault)
  • 可能破坏关键内存数据,导致程序崩溃。

产生场景:

  1. 指针变量未初始化

    int *p;    // p未初始化,是野指针
    printf("%d\n", *p); // 危险操作:p就是野指针
    
  2. 指针指向已释放的内存

    int *p = malloc(sizeof(int)); // 在堆区申请1个int大小的内存空间,将该空间地址赋值给指针变量p
    free(p); // 释放指针p指向的空间内存
    printf("%d\n", *p); // 危险操作:p就是野指针
    
  3. 返回局部变量的地址

    int* fun(int a, int b)
    {
        int sum = a + b; // sum就是一个局部变量
        return &sum;  // 将局部变量的地址返回给主调函数
    }
    
    int main()
    {
        int *p = fun(2,3);
        printf("%d\n", *p); // 危险操作:p就是野指针
    }
    

如何避免野指针:

  1. 初始化指针为NULL

  2. 释放内存后立即置指针为NULL

  3. 避免返回局部变量的地址

  4. 使用前检查指针有效性(非空校验,边界检查)。

    int fun(int *pt)
    {
        int *p = pt;
        // 校验指针
        if(p == NULL) // 结果为假   等价于 if(!p)  其实底层: if(p == 0)
        {
            printf("错误!");
            return -1;
        }
        printf("%d\n", *p);
        return 0;
    }
    
空指针

**定义:**值为NULL的指针,指向地址0x000000000000(系统保留,不可访问)

在这里插入图片描述

**作用:**明确表示指针当前不指向有效内存,一般用作指针的初始化。

示例:

int *p = NULL;  // 初始化为空指针

free(p);   // 释放后置空
p = NULL;
空悬指针

**定义:**指针指向的内存已经被释放,但未重新赋值。空悬指针是野指针的一种特例。

示例:

char *p = malloc(100); // 在堆区分配100个char的空间给p
free(p); // 释放指针p指向的内存空间
printf("%p,%d\n", p, *p); // p可以正常输出,*p此时属于危险操作 
// p指向的内存空间被回收,但是p指向空间的地址依然保留,此时这个指针被称作空悬指针

void与void*的区别

定义
  • void: 表示“无类型/空类型”,用于函数返回类型或者参数。

    void func(void);    // 没有返回值也没有参数,一般简写:void func();
    
  • *void:**通用指针类型(万能指针),可指向任意类型数据,但需要强制类型转换后才能解引用。

    void* ptr = malloc(4);  // ptr指向4个字节大小的堆内存空间  
    // 存放int类型数据
    int *p = (int*)ptr;
    *p = 10;
    
    // 存放float类型数据
    float* p1 = (float*)ptr;
    *p = 12.5f;
    
    // 存放char类型数组
    char* p2 = (char*)ptr;
    
    // 以下写法完全错误
    float* ptr = malloc(4);
    int *p = (int*)ptr;  // 此时编译报错,类型不兼容 float* int*
    

    注意:只能是具体的类型(int*,double*,float*,char*...)和void*之间转换

注意事项
  • void不能直接解引用(*ptr 会报错
  • 函数返回void*需要外部接收的时候明确类型(不明确类型,就无法解引用)
示例

#include <stdio.h>

/**
 * 定义一个返回类型为void*类型的指针函数
 */
void* proces_data(void* p)
{
	return p;
}

int main(int argc, char *argv[])
{
	// int类型
	int m = 10;
	int* p_int = &m;
	
	int* result_int = (int*)proces_data(p_int);
	printf("Integer value:%d\n", *result_int);
	
	// double类型
	double pi = 3.1415926;
	double* p_double = &pi;
	
	double* result_double = (double*)proces_data(p_double);
	printf("Double value:%lf\n", *result_double);
	
	// void* p_void = proces_data(p_double);
	// printf("Void value:%lf\n", *p_void);
	// *p_void = 20;
	
	// 注意:void* 修饰的指针是可以进行赋值操作的,但是不能对其解引用
	
	return 0;
}

内存管理【扩展资料】

C进程内存布局

任何一个程序,正常运行都需要内存资源,用来存放诸如变量、常量、函数代码等等。这些不同的内容,所存储的内存区域是不同的,且不同的区域有不同的特性。因此我们需要研究C语言进程的内存布局,逐个了解不同内存区域的特性。

每个C语言进程都拥有一片结构相同的虚拟内存,所谓的虚拟内存,就是从实际物理内存映射出来的地址规范范围,最重要的特征是所有的虚拟内存布局都是相同的,极大地方便内核管理不同的进程。例如三个完全不相干的进程p1、p2、p3,它们很显然会占据不同区段的物理内存,但经过系统的变换和映射,它们的虚拟内存的布局是完全一样的。

  • PM:Physical Memory,物理内存。
  • VM:Virtual Memory,虚拟内存。

在这里插入图片描述

将其中一个C语言含如进程的虚拟内存放大来看,会发现其内部包下区域:

  • 栈(stack)
  • 堆(heap)
  • 数据段
  • 代码段

在这里插入图片描述


虚拟内存中,内核区段对于应用程序而言是禁闭的,它们用于存放操作系统的关键性代码,另外由于 Linux 系统的历史性原因,在虚拟内存的最底端 0x00000000 ~ 0x08048000 之间也有一段禁闭的区段,该区段也是不可访问的。


虚拟内存中各个区段的详细内容:

在这里插入图片描述

栈内存

  • 什么东西存储在栈内存中?
    • 环境变量
    • 命令行参数
    • 局部变量(包括形参)
  • 栈内存有什么特点?
    • 空间有限,尤其在嵌入式环境下。因此不可以用来存储尺寸太大的变量。
    • 每当一个函数被调用,栈就会向下增长一段,用以存储该函数的局部变量。
    • 每当一个函数退出,栈就会向上缩减一段,将该函数的局部变量所占内存归还给系统。
  • 注意: 栈内存的分配和释放,都是由系统规定的,我们无法干预。

在这里插入图片描述

  • 示例代码:
void func(int a, int *p) // 在函数 func 的栈内存中分配
{
    double f1, f2;        // 在函数 func 的栈内存中分配
    ...                   // 退出函数 func 时,系统的栈向上缩减,释放内存
}

int main(void)
{
    int m  = 100;  // 在函数 main 的栈内存中分配
    func(m, &m);  // 调用func时,系统的栈内存向下增长
}

静态数据

C语言中,静态数据有两种:

  • 全局变量:定义在函数外部的变量。
  • 静态局部变量:定义在函数内部,且被static修饰的变量。
  • 示例:
int a; // 全局变量,退出整个程序之前不会释放
void f(void)
{
    static int b; // 静态局部变量,退出整个程序之前不会释放
    printf("%d\n", b);
    b++;
}

int main(void)
{
    f();
    f(); // 重复调用函数 f(),会使静态局部变量 b 的值不断增大
}
  • 为什么需要静态数据?
  1. 全局变量在默认的情况下,对所有文件可见,为某些需要在各个不同文件和函数间访问的数据提供操作上的方便。
  2. 当我们希望一个函数退出后依然能保留局部变量的值,以便于下次调用时还能用时,静态局部变量可帮助实现这样的功能。
  • 注意1:
    • 若定义时未初始化,则系统会将所有的静态数据自动初始化为0
    • 静态数据初始化语句,只会执行一遍。
    • 静态数据从程序开始运行时便已存在,直到程序退出时才释放。
  • 注意2:
    • static修饰局部变量:使之由栈内存临时数据,变成了静态数据。
    • static修饰全局变量:使之由各文件可见的静态数据,变成了本文件可见的静态数据。
    • static修饰函数:使之由各文件可见的函数,变成了本文件可见的静态函数。

数据段与代码段

  • 数据段细分成如下几个区域:
    • .bss 段:存放未初始化的静态数据,它们将被系统自动初始化为0
    • .data段:存放已初始化的静态数据
    • .rodata段:存放常量数据
  • 代码段细分成如下几个区域:
    • .text段:存放用户代码
    • .init段:存放系统初始化代码

在这里插入图片描述

int a;       // 未初始化的全局变量,放置在.bss 中
int b = 100; // 已初始化的全局变量,放置在.data 中

int main(void)
{
    static int c;       // 未初始化的静态局部变量,放置在.bss 中
    static int d = 200; // 已初始化的静态局部变量,放置在.data 中
    
    // 以上代码中的常量100、200防止在.rodata 中
}
  • 注意:数据段和代码段内存的分配和释放,都是由系统规定的,我们无法干预。

堆内存

堆内存(heap)又被称为动态内存、自由内存,简称堆。堆是唯一可被开发者自定义的区段,开发者可以根据需要申请内存的大小、决定使用的时间长短等。但又由于这是一块系统“飞地”,所有的细节均由开发者自己把握,系统不对此做任何干预,给予开发者绝对的“自由”,但也正因如此,对开发者的内存管理提出了很高的要求。对堆内存的合理使用,几乎是软件开发中的一个永恒的话题。

  • 堆内存基本特征:
    • 相比栈内存,堆的总大小仅受限于物理内存,在物理内存允许的范围内,系统对堆内存的申请不做限制。
    • 相比栈内存,堆内存从下往上增长。
    • 堆内存是匿名的,只能由指针来访问。
    • 自定义分配的堆内存,除非开发者主动释放,否则永不释放,直到程序退出。

在这里插入图片描述

  • 相关API:
    • 申请堆内存:malloc() / calloc() int *p = malloc(4);
    • 清零堆内存:bzero()
    • 释放堆内存:free()

在这里插入图片描述

  • 示例:
int *p = malloc(sizeof(int)); // 申请1块大小为 sizeof(int) 的堆内存
bzero(p, sizeof(int));        // 将刚申请的堆内存清零

*p = 100; // 将整型数据 100 放入堆内存中
free(p);  // 释放堆内存

// 申请3块连续的大小为 sizeof(double) 的堆内存
double *k = calloc(3, sizeof(double));

k[0] = 0.618;
k[1] = 2.718;
k[2] = 3.142;
free(k);  // 释放堆内存  
  • 注意:
    • malloc()申请的堆内存,默认情况下是随机值,一般需要用 bzero() 来清零。
    • calloc()申请的堆内存,默认情况下是已经清零了的,不需要再清零。
    • free()只能释放堆内存,并且只能释放整块堆内存,不能释放别的区段的内存或者释放一部分堆内存。
  • 释放内存的含义:
    • 释放内存意味着将内存的使用权归还给系统。
    • 释放内存并不会改变指针的指向。
    • 释放内存并不会对内存做任何修改,更不会将内存清零。

网站公告

今日签到

点亮在社区的每一天
去签到