learn_C_deep_9 (汇编角度理解return的含义、const 的各种应用场景、volatile 的基本理解与实验证明)

简介: learn_C_deep_9 (汇编角度理解return的含义、const 的各种应用场景、volatile 的基本理解与实验证明)

return 关键字


       不知道我们大家是否有一个疑惑:我们下载一个大型游戏软件,都要花几个小时去下载,但是一旦我们游戏连输,想要删除这个软件的时候,它仅仅只需要十几秒,这是为什么呢?今天我们就来带着这个疑惑,一起来解决这个问题。


计算机中,释放空间是否真的要将我们的数据全部清零?


       在计算机中,释放空间并不一定要将其中的数据全部清零。释放空间,也就是删除文件,计算机并不会立即清零或删除文件的内容。实际上,计算机操作系统通常只是将这些文件对应的磁盘空间标记为可重用,然后在需要存储新的数据时将其覆盖。因此,即使删除了文件,它的内容可能仍然存在于硬盘或其他存储设备中,只要未被覆盖就可以恢复。

        总结:当你删除一个文件时,计算机只需要简单地将文件所在的存储空间标记为可用,不需要进行实际的数据传输,因此删除数据的速度较快。


下面来看一段代码

#include <stdio.h>
char* show()
{
  char str[] = "hello world";
  return str;
}
int main()
{
  char* s = show();
  printf("%s\n", s);
  return 0;
}


       这段代码主要涉及到两个问题:局部变量的生命周期和内存安全性。

首先,我们来看局部变量的生命周期。在函数 show() 中,变量 str 是定义在函数体内的局部变量。局部变量的生命周期只在函数体内,一旦函数执行完毕就会被销毁。因此,在 return str; 语句执行完毕之后,变量 str 所占用的内存空间就被释放了。


接着,我们看内存安全性的问题。在 show() 函数中,我们将作为返回值的变量 str 的地址返回给了调用者。由于变量 str 所在的内存空间已经被释放,因此返回的指针 s 指向的内存空间已经不再被保证安全。在 main() 函数中,我们调用了 printf() 函数输出了 s 所指向的内存空间中的字符串,由于该内存空间可能已经被其他程序或者系统使用,因此会导致未知的错误。这一点也是常说的“野指针”问题。


我们来详细了解一下其中的释放过程


总结:本代码中return语句不可返回指向"栈内存"的"指针",因为该内存在函数体结束时被自动销毁。


这里很奇怪呀?我们刚刚不是说函数调用完后会释放栈帧,里面的数据x经过printf函数应该就会被覆盖,但是我们这里为什么还能打印它呢?  -   这里就要介绍一下return关键字


       return 语句是 C 语言中的一个关键字,用于结束当前函数的执行并返回一个值或不返回值。在大多数情况下,return 语句用于向调用者返回一个函数执行结果。

       return 语句有多种不同的用法和语法结构,其中最常见的用法是:

```

c return expression;

```

其中 expression 可以是一个常量、变量、表达式或者其他函数调用的返回值,这个值会成为函数的返回值被返回给调用者。

       我们上面的代码返回的是x的值,函数栈帧内return关键字将x的值保存在寄存器中,通过寄存器将x的值带回给main函数中的y。如果是x的地址,它也会被返回,只不过不能打印其中的值。编译器会提出警告。


const 关键字


       const是C语言中的一个关键字,它的作用是修饰变量,表示该变量的值是不可直接修改的。这意味着,使用const关键字声明的变量在程序运行期间一旦赋值就不能再被修改。const关键字可以用于修饰基本数据类型、结构体、指针等类型的变量。


       使用const关键字有以下好处:


1. 程序的可读性更好,使用const关键字可以明确告诉其他程序员该变量是一个常量,不应该被修改。

2. 程序更加安全,使用const关键字可以避免在程序中意外地修改一个应该是常量的变量,提高了程序的健壮性。

3. 编译器可以利用const关键字优化程序,例如在一些情况下编译器可以将常量直接嵌入到代码中,提高了程序的执行效率。


const 修饰的只读变量 - - - 不可直接被修改!



不可以直接被修改,但可以被间接修改 - 通过地址进行修改


结论:const修饰的变量并非是真的不可被修改的常量。


const修饰的变量,可以作为数组定义的一部分吗?


const int n = 100;

int arr[n];

 

这里可以看我写的另一篇文章,里面有介绍到。


总结:在vs2013(标准C)下直接报错了,但是在gcc(GNU扩展)下,可以。但我们一切向标准看齐,不可以。


const只能在定义的时候直接初始化,不能二次赋值。为什么?


  const关键字的作用是告诉编译器该变量是一个常量,不应该被修改。因此,使用const关键字声明的变量在程序运行期间一旦赋值就不能再被修改。


       为了让编译器能够实现这个目标,const关键字在编译时会对该变量进行一些优化,使得该变量的值在程序运行期间不可修改。如果允许在程序运行期间对该变量进行二次赋值,那么编译器就无法保障该变量的值不会被修改,这与const关键字的含义相违背。


       因此,const只能在定义的时候直接初始化,不能二次赋值的原因是为了保证程序的健壮性和安全性。如果确实需要在程序运行期间动态地修改一个变量的值,应该使用普通的变量而不是使用const修饰的变量。


const修饰指针


先来介绍一下左值和右值的概念


       在计算机编程中,左值(lvalue)和右值(rvalue)是表达式的两种类型。

 左值表示的是被赋值的对象,可以出现在“=”的左边,也可以在表达式的任何一个操作数中。左值可以出现在多个操作中,并且能够被改变。


       右值表示的是一个可以赋值给左值的值,右值可以出现在表达式的任何一个操作数中,但是不能被改变。右值通常是一个临时值,用于计算表达式,并且当表达式执行完毕后,其值就会被丢弃。


指针变量也存在左值和右值。


在C语言中,指针是一种特殊的变量,它存储的是一个内存地址,可以用来访问那个地址中存储的数据。在定义指针变量时,我们可以使用const关键字来决定指针和指针指向的数据是否可以被修改。


       1. const int* p;

       这里的const作用于指针指向的数据,表示p指向的数据是不可修改的。也就是说,我们可以通过p指针读取这个常量数据,但是不能通过p指针修改这个数据。比如:p本身可以被修改(比如p++),但是p指向的int类型变量是不可修改的(比如*p=10是不合法的)。


       2. int const* p;

       这个定义和上面的定义是等价的,const关键字位置不同但含义相同。


       3. int* const p;

       这里的const作用于指针本身,表示p指针本身是不可修改的。也就是说,我们不能通过改变p指针的值来让它指向其他的地址,但是可以通过p指针修改这个地址中存储的数据。比如:p本身不可以被修改(比如p++是不合法的),但是p指向的int类型变量是可以被修改的(比如*p=10是合法的)。


       4. const int* const p;

       这个定义中有两个const关键字,一个作用于指针本身,一个作用于指针指向的数据。表示p指针本身和p指向的数据都是不可修改的,也就是说,p指针只能指向某一块地址,而且这块地址中存储的数据也不能被修改。比如:p本身不可以被修改(比如p++是不合法的),并且p指向的int类型变量也是不可修改的(比如*p=10是不合法的)。


       const int* p1 = &a;

       int* q1 = p1;

这里将const int*类型的指针p1赋值给了int*类型的指针q1,这样做是不安全的。因为p1指向的是一个不可修改的常量int类型变量,如果通过q1指针去修改p1所指向的变量,就会引发未定义行为。正确的做法是将指针类型强制转换为非const类型,即: int* q1 = (int*)p1;



       int* const p2 = &b;

       int* q2 = p2;


这里将int* const类型的指针p2赋值给int*类型的指针q2,这样做是安全的。因为p2指向的是一个可以修改的int类型变量,同时p2本身也是不可修改的。而且,将const类型的指针赋值给非const类型的指针也是安全的。所以这段代码是没有问题的,不需要做改动。


const修饰函数的参数


在C语言中,我们也可以使用const关键字来修饰函数的参数,这表示函数不会修改被修饰的参数的值。 函数中的参数可以分为形参和实参,形参是函数中定义的变量,实参是函数调用时传递给函数的值。使用const关键字修饰形参时,表示函数中不能修改这个形参的值。如果函数试图修改被const修饰的形参,编译器会报错。


       下面是一个使用const修饰函数参数的例子:

void print_array(const int* arr, int n)
{
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}
int main(void)
{
    int arr[] = { 1, 2, 3, 4, 5 };
    print_array(arr, 5);
    return 0;
}

在这个例子中,print_array函数的第一个参数是const int*类型,表示这个指针指向的是一段不可修改的内存,函数中不能修改这段内存对应的值。第二个参数是普通的int类型,表示数组的长度。 在函数内部,我们使用了一个for循环来遍历数组,并使用printf函数打印数组中的每个元素。 因为我们将第一个参数声明为const int*类型,所以在函数中不能修改这个指针所指向的值。如果函数尝试修改这个指针所指向的值,编译器会报错。


       这有助于保护数组中的值不被意外修改,提高程序的健壮性。


函数在传参的时候有没有形成临时变量?


       在C语言中,函数参数传递采用的是值传递或者地址传递方式。当我们调用函数时,会将实参的值复制一份,然后传递给函数,而函数中定义的形参则是一个新的变量。这个过程中,确实会生成一个临时变量来存储实参的值。


修饰函数返回值

#include <stdio.h>
//告诉编译器,告诉函数调用者,不要试图通过指针修改返回值指向的内容
const int* test()
{
  static int g_var = 100;
  return &g_var;
}
int main()
{
  int* p = test(); //有告警
  // warning C4090: “初始化”: 不同的“const”限定符
  //const int *p = test(); //需要用const int*类型接受
  *p = 200; //这样,【在语法/语义上】,限制了,不能直接修改函数的返回值
  printf("%d\n", *p);
  return 0;
}


这个代码段主要是为了演示如何通过const关键字来限制函数返回变量的修改。


       首先,我们声明了一个名为test的函数,该函数返回一个指向静态int变量g_var的指针。在函数返回类型前加上const关键字,告诉编译器和调用者不要尝试通过指针修改返回值指向的内容,这个关键字可以保证函数返回值的安全性。


       接下来,在main函数中,使用指针p来接收test函数的返回值。由于p是一个非常量指针,因此对p指向的内容进行修改不会引发编译器警告。但是,我们在这里试图通过指针p修改test函数返回的指向g_var的指针,这是不合法的。


       为了避免这种情况,我们需要使用const int*类型来声明指针p,这样编译错误会在编译时抛出而不是在运行时出现。这种方法可以在语法/语义层面上防止对函数返回值的意外修改,保证程序的稳定性和安全性。


volatile关键字


       在 C 语言中,关键字 volatile 用于告诉编译器某个变量的值可能随时会被意外地改变,因此编译器在操作该变量时不应该进行优化或者缓存。

volatile 的主要作用是:


       1. 防止编译器针对该变量进行优化。由于编译器在处理代码时会尽可能地优化代码,包括对内存访问的优化,因此有时候编译器可能会把对某个变量的访问缓存到寄存器中,这样虽然可以提高速度,但可能会导致程序读取的不是最新的值。使用 volatile 关键字可以让编译器强制每次都重新从内存中获取该变量的值,避免了这种问题。

       2. 保证程序正确处理约束条件。在一些特殊情况下,某个变量的值可能会因为外部因素(比如硬件中断或者运行环境等)改变,然而编译器并不能意识到这种情况。使用 volatile 关键字可以确保程序使用的是最新的值,从而能够正确处理约束条件。

#include <stdio.h>
int pass = 1;
int main()
{
  while (pass) { //思考一下,这个代码有哪些地方,编译器是可以优化的。
  }
  return 0;
}

从下面的汇编代码看,由于pass的值一直没有改变,编译器已经对代码进行处理,以及对内存进行优化,cpu的寄存器每次读取变量不需要直接从内存中获取,cpu读取值每次都是直接寄存器中读取,这样优化提高了运行速度。


       汇编代码中,仅仅在42行将pass的值放入寄存器,之后再没有进行这样的操作,之后都是直接读取存储在寄存器的值,然后无限跳转,导致循环。


当我们加入volatile后,编译器就没有对代码进行处理,以及对内存进行优化。在每次循环的时候,都将pass的值放入寄存器,然后无限跳转,导致循环。


结论: volatile 忽略编译器的优化,保持内存可见性。

相关文章
|
9月前
|
算法 vr&ar 网络架构
汇编实验4(99乘法表,整数分解,素数环,迷宫问题)【栈传参,递归,寻址方式】
汇编实验4(99乘法表,整数分解,素数环,迷宫问题)【栈传参,递归,寻址方式】
72 0
|
12月前
【8086汇编】《汇编语言(第三版)》实验一
需要用到的指令✨✨ 查看、修改CPU中寄存器的内容:R命令 查看内存中的内容:D命令 修改内存中的内容:E命令(可以写入数据、指令,在内存中,它们实际上没有区别) 将内存中的内容解释为机器指令和对应的汇编指令:U命令 执行CS:IP指向的内存单元处的指令:T命令 以汇编指令的形式向内存中写入指令:A命令
超详细汇编注释 操作系统实验二 操作系统的引导(哈工大李治军)(二)
超详细汇编注释 操作系统实验二 操作系统的引导(哈工大李治军)(二)
128 0
超详细汇编注释 操作系统实验二 操作系统的引导(哈工大李治军)(二)
|
存储 Ubuntu Linux
超详细汇编注释 操作系统实验二 操作系统的引导(哈工大李治军)(一)
超详细汇编注释 操作系统实验二 操作系统的引导(哈工大李治军)(一)
175 0
超详细汇编注释 操作系统实验二 操作系统的引导(哈工大李治军)(一)
|
存储 SQL 缓存
深入汇编指令理解Java关键字volatile
深入汇编指令理解Java关键字volatile
105 0
汇编实验三
实验3 数据传送指令练习 一、实验目的: 1.熟悉IBM PC机的数据传送指令的使用。 2. 掌握堆栈的作用。 3.掌握堆栈指示器SP和堆栈操作指令PUSH、POP。 二、实验内容: 1.将数据段中偏移地址为0000H的连续三个字节内容传送至该段偏移地址为0003H的连续三个字节单元中。要求使用直接寻址、寄存器间接寻址方式。
127 0
汇编实验三
|
Ruby
汇编实验2 寻址方式练习
实验目的: 1.理解存储器分段及寻址方式的意义 2.熟练掌握立即寻址、寄存器寻址、直接寻址、寄存器间接寻址、寄存器相对寻址、基址变址寻址、相对基址变址寻址等几种寻址方式。 3.复习巩固DEBUG中的R、D、E命令。 4.掌握用A命令编制程序,U命令进行反汇编,用G、T命令执行程序。
209 0
汇编实验2 寻址方式练习
|
存储
汇编实验一 DEBUG调试工具的使用
一、实验目的 1.学习使用DEBUG程序的各种命令。 2.了解计算机取指令、执行指令的工作过程。 3.掌握用DEBUG调试工具自编程序的方法。
305 0
汇编实验一 DEBUG调试工具的使用
|
开发工具
专接本汇编开发工具【Masm for Winodws 集成实验环境】安装细则
专接本汇编开发工具【Masm for Winodws 集成实验环境】安装细则
268 0
专接本汇编开发工具【Masm for Winodws 集成实验环境】安装细则
汇编(三)段地址、偏移地址、内存单元、寄存器、CS、IP、代码段、debug、实验题
段地址、偏移地址、内存单元、寄存器、CS、IP、代码段、debug、实验题、汇编、汇编编程、win10怎么使用debug
9869 0