当前位置: 首页 > article >正文

系统编程(指针,内存基础)

指针复习

指针区分

指针变量
  • 指针变量是一种特殊的变量,它存储的是另一个变量的内存地址,而不是数据值本身。
  • 在C/C++中,指针变量的声明方式通常是在变量类型前加上一个星号(*),例如:int *ptr;,这里ptr是一个指向int类型数据的指针变量。
  • 指针变量可以通过取地址运算符(&)来存储另一个变量的内存地址。例如:int a = 5; int *ptr = &a;,这里ptr存储了变量a的内存地址。
  • 通过指针变量访问它所指向的数据值,需要使用解引用运算符(*)。例如:*ptr将访问ptr所指向的内存地址中的数据值。
  • 指针可以进行一些特定的运算,例如指针加减运算(用于遍历数组或动态内存分配)、指针比较运算(比较两个指针是否指向相同的内存地址)等。
  • 一个未指向任何有效内存地址的指针被称为空指针,通常用NULL(在C++11及以后标准中,推荐使用nullptr)来表示。例如:int *ptr = NULL;
  • 指针常用于动态内存分配,通过标准库函数如malloc(在C中)或new(在C++中)来分配内存,并通过指针来访问这块内存。
    int *p;
    double a = 10;
    int b = 11;
变量指针

变量指针在编程中有多种用途,包括但不限于:

  1. 动态内存管理:通过指针,我们可以动态地分配和释放内存,以满足程序运行时的内存需求。
  2. 数组和字符串处理:指针在数组和字符串处理中发挥着重要作用,通过指针可以方便地遍历和修改数组或字符串的元素。
  3. 函数参数传递:在C/C++中,函数参数默认是按值传递的。但是,通过使用指针作为函数参数,我们可以实现按引用传递,从而允许函数修改传入的变量值。
  4. 数据结构实现:指针是实现链表、树、图等复杂数据结构的关键工具。通过指针,我们可以建立元素之间的关联关系,从而实现数据的高效访问和操作
    p = &b;
数组指针

数组指针本质上是一个指针,但它与普通指针的区别在于它指向的是一个数组的首元素地址,并且它隐含了数组的大小信息(尽管在某些情况下,这个大小信息可能不是显式给出的,但程序员在使用时需要知道或假设它)。

在C语言中,数组指针的定义通常使用如下形式:

类型 (*指针变量名)[数组大小];
数组指针的用途
  1. 访问二维数组:数组指针在访问二维数组时非常有用。由于二维数组在内存中实际上是按一维数组的方式连续存储的,因此可以使用数组指针来遍历和访问二维数组的元素。
  2. 函数参数传递:当需要将一个二维数组作为函数参数传递时,可以使用数组指针。这样可以避免在函数内部创建数组的副本,从而提高效率。
  3. 动态内存分配:虽然数组指针本身并不直接用于动态内存分配,但可以使用指针和动态内存分配函数(如malloc)来创建一个动态数组,并使用数组指针来访问这个动态数组。
    // 数组指针和二维数组等效
    // 指针数组和二级指针等效---形参和实参数据类型对应的角度
    int(*p3)[2];
    int arr6[2] = {0};
    p3 = &arr6;
    int arr7[3][2] = {{1,2}, {4,5}, {6, 7}};
    p3 = arr7;
指针数组

指针数组本身是一个数组,但其元素都是指针变量。这些指针变量可以指向相同类型的数据,也可以指向不同类型的数据(但在实际使用中,为了操作的便利性和安全性,通常指向相同类型的数据)。指针数组的定义形式为:“类型名 *数组标识符[数组长度]”。例如,int *ptr_array[5]表示定义了一个包含5个整型指针的数组。

指针数组的特点
  1. 元素类型相同:指针数组中的元素都是指针变量,且这些指针变量通常指向相同类型的数据。
  2. 灵活性:由于指针数组中的元素是指针,因此可以通过这些指针灵活地访问和操作数据。
  3. 节省内存:在处理字符串等变长数据时,使用指针数组可以节省内存空间,因为不需要为每个字符串分配相同长度的字符数组。
指针数组的应用
  1. 指向字符串:指针数组常用于指向多个字符串,这样可以使字符串处理更加方便和灵活。例如,可以使用指针数组来存储一个字符串数组,并通过指针来访问和修改这些字符串。
  2. 作为函数参数:指针数组可以作为函数的参数传递,这样函数可以接收一个指向多个数据的指针数组,并对其进行操作。
  3. 处理二维数组:虽然指针数组和数组指针都可以用于处理二维数组,但指针数组在处理行长度不固定的二维数组时更具优势。通过为每个行分配一个指针,并让这些指针指向相应的数据行,可以灵活地处理不同行长度的二维数组。
指针数组的应用
  1. 指向字符串:指针数组常用于指向多个字符串,这样可以使字符串处理更加方便和灵活。例如,可以使用指针数组来存储一个字符串数组,并通过指针来访问和修改这些字符串。
  2. 作为函数参数:指针数组可以作为函数的参数传递,这样函数可以接收一个指向多个数据的指针数组,并对其进行操作。
  3. 处理二维数组:虽然指针数组和数组指针都可以用于处理二维数组,但指针数组在处理行长度不固定的二维数组时更具优势。通过为每个行分配一个指针,并让这些指针指向相应的数据行,可以灵活地处理不同行长度的二维数组。
#include <stdio.h>

int main() {
    char *str_array[3] = {"Hello", "World", "!"}; // 定义一个包含3个字符指针的数组

    // 使用指针数组访问和打印字符串
    for (int i = 0; i < 3; i++) {
        printf("%s ", str_array[i]);
    }
    printf("\n");

    return 0;
}
函数指针
函数指针的定义

函数指针本质上是一个指针,但它与普通指针的区别在于它指向的是一个函数的入口地址,而不是一个变量的地址。函数指针的定义形式为:“返回类型 (*指针变量名)(参数列表)”。这里的“返回类型”指的是函数返回值的类型,“指针变量名”是函数指针的名称,“参数列表”是函数参数的类型列表。

例如,定义一个指向返回整型值、接受两个整型参数的函数的指针,可以这样写:

int (*func_ptr)(int, int);

这里的func_ptr就是一个函数指针,它可以指向任何返回整型值、接受两个整型参数的函数。

函数指针的赋值

给函数指针赋值时,可以将其指向一个具体的函数。在C语言中,函数名实际上就是该函数的地址,因此可以直接使用函数名来给函数指针赋值。例如:

int add(int a, int b) 
{
    return a + b;
}
 
func_ptr = add; // 将add函数的地址赋给func_ptr

函数指针的调用

通过函数指针调用函数时,需要先对指针进行解引用,然后传递相应的参数。例如:

	int result = (*func_ptr)(3, 4); // 调用add(3, 4),result的值为7

或者,由于函数指针的特殊性,也可以省略解引用的步骤,直接这样调用:

int result = func_ptr(3, 4); // 同样调用add(3, 4),result的值为7
函数指针的用途
  1. 回调函数:函数指针常用于实现回调函数。回调函数是指将一个函数作为参数传递给另一个函数,并在适当的时候调用前者。这种机制使得程序更加灵活和可扩展。
  2. 动态函数调用:通过函数指针,可以在运行时根据需要动态地选择调用哪个函数,从而实现动态函数调用。
  3. 函数表:可以将多个函数指针存储在一个数组中,形成一个函数表。通过索引访问函数表中的函数指针,可以实现不同的功能调用。


int fun9(const void* a, const void* b)
{
    puts("fun9");
    return *(double*)b-*(double*)a;
}

//定义函数指针变量p
    int(*p5)(const void*, const void *);
    p5 = fun9;
   //函数回调
   qsort(arr10, 5, sizeof(double), fun9);

   for(int i = 0; i<5; i++)
   {
        printf("%lf, ", arr10[i]);
   }
   puts(""); 
	#include <stdio.h>
 
// 定义一个函数指针类型,用于指向接受两个int参数并返回int结果的函数
typedef int (*Operation)(int, int);
 
// 实现加法运算的函数
int add(int a, int b) {
    return a + b;
}
 
// 实现减法运算的函数
int subtract(int a, int b) {
    return a - b;
}
 
// 实现乘法运算的函数
int multiply(int a, int b) {
    return a * b;
}
 
// 实现除法运算的函数(注意处理除以零的情况)
int divide(int a, int b) {
    if (b == 0) {
        printf("Error: Division by zero!\n");
        return 0; // 或者可以选择返回一个特殊的错误代码
    }
    return a / b;
}
 
// 定义一个执行运算的函数,它接受一个操作(函数指针)和两个操作数
void performOperation(Operation op, int num1, int num2) {
    int result = op(num1, num2);
    printf("Result: %d\n", result);
}
 
int main() {
    int num1 = 10;
    int num2 = 5;
 
    // 使用加法运算
    performOperation(add, num1, num2);
 
    // 使用减法运算
    performOperation(subtract, num1, num2);
 
    // 使用乘法运算
    performOperation(multiply, num1, num2);
 
    // 使用除法运算
    performOperation(divide, num1, num2);
 
    // 尝试除以零(演示错误处理)
    performOperation(divide, num1, 0);
 
    return 0;
}

在这个例子中,Operation 是一个函数指针类型,它指向接受两个 int 参数并返回 int 结果的函数。addsubtractmultiplydivide 是实现了四种基本运算的函数。performOperation 函数接受一个 Operation 类型的函数指针和两个整数作为参数,并使用提供的函数指针来执行运算。

main 函数中,我们演示了如何使用 performOperation 函数来执行不同的运算。通过传递不同的函数指针(addsubtractmultiplydivide),我们可以灵活地改变 performOperation 函数的行为。

存储方式

代码段(Code Segment)

  • 定义:代码段是程序的只读部分,存储的是程序的指令,即代码。
  • 特点:代码段是静态的,在程序运行期间不会变化。它是只读的,防止运行时修改代码。每个程序有且只有一个代码段。
  • 存储内容:包含所有的函数实现(包括main函数和其他用户定义的函数)和常量(如字符串常量)。

数据段(Data Segment)

  • 定义:数据段是用来存储全局变量和静态变量的区域。
  • 细分:数据段通常被进一步细分为已初始化的数据段(Initialized Data Segment)和未初始化的数据段(BSS Segment,Block Started by Symbol)。
    • 已初始化的数据段:存放程序中初始化的全局变量和静态变量。
    • 未初始化的数据段(BSS段):存放未初始化的全局变量和静态变量,编译器会自动将这些变量初始化为零。
  • 特点:数据段的变量在程序执行期间一直存在,并且可以被多个函数访问和修改。

堆区(Heap)

  • 定义:堆区是程序运行时可以动态分配内存的区域。
  • 特点:堆内存由程序员显式管理,程序员负责分配(如使用malloc函数)和释放(如使用free函数)内存。堆的内存是动态分配的,大小可变,且在程序运行期间可以随时分配和释放。
  • 问题:若忘记释放内存可能导致内存泄漏。

栈区(Stack)

  • 定义:栈区是程序在执行过程中用于存放函数的局部变量、函数调用的参数、返回地址等的内存区域。
  • 特点:栈由操作系统自动管理,局部变量和函数调用信息会随着函数调用入栈,函数结束后出栈。栈内存是自动管理的(由编译器和操作系统),无需程序员显式分配和释放。栈内存通常比较小,并且是后进先出(LIFO, Last In First Out)的结构。
  • 问题:栈的大小是有限的,若超过这个大小会导致栈溢出(Stack Overflow)。

各区域之间的关系

  • 代码段与其他区域:代码段存储的是程序的指令,这些指令在运行时可能会访问数据段、堆区或栈区的变量。
  • 数据段与堆区、栈区:数据段存储的是全局变量和静态变量,这些变量在程序执行期间一直存在。而堆区和栈区则用于存储程序运行时动态分配的内存。堆区由程序员管理,适合存储需要长时间存在的数据;栈区由操作系统管理,适合存储函数调用的局部变量和参数。
  • 堆区与栈区:它们都是动态内存区域,但管理方式不同。堆区需要程序员显式分配和释放内存,而栈区则由操作系统自动管理。此外,栈区内存通常较小且是LIFO结构,适合存储短期使用的数据;堆区内存大小可变,适合存储长期使用的数据。

http://www.kler.cn/a/470160.html

相关文章:

  • C++编程基础之override关键字
  • 【STM32+CubeMX】 新建一个工程(STM32F407)
  • 鸿蒙的APP真机调试以及发布
  • WebSocket底层原理及 java 应用
  • uniapp打包到宝塔并发布
  • 创建型模式2.抽象工厂模式
  • 深入解析希尔排序:原理、实现与优化
  • web系统漏洞攻击靶场
  • 力扣-数据结构-11【算法学习day.82】
  • ros2笔记-2.5.3 多线程与回调函数
  • Vue 项目自动化部署:Coding + Jenkins + Nginx 实践分享
  • 掌握销售‘先机’,HubSpot邮件跟踪软件让销售更智能
  • 激活城市数字化文化产业“新质生产力”,虚拟数字人化身城市代言人
  • 【机器学习】机器学习的基本分类-自监督学习-变换预测(Transformation Prediction)
  • RedisTemplate执行lua脚本及Lua 脚本语言详解
  • 20250103在Ubuntu20.04.5的Android Studio 2024.2.1.12中跑通Hello World
  • 了解什么是JavaEE(什么是JavaEE)
  • PHP语言的并发编程
  • 一个使用 Nginx 进行反向代理和负载均衡的示例配置
  • gozero实现对接开放平台分贝通中新建费用报销的sdk设计与解决方案
  • CAD随机球体插件专业版V1.3版本更新
  • 大数据组件(三)快速入门实时计算平台Dinky
  • XHR readyState:深入了解XMLHttpRequest的状态管理
  • 《Vue进阶教程》第三十五课:自动脱ref
  • C语言基础:指针(常量指针和指针常量)
  • js -音频变音(听不出说话的人是谁)