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

高级c语言(一)

一、程序的内存分段:(进程映像)

当执行程序的运行命令后,操作系统会给程序分配它所需要的内存,并划分成以下内存段供程序使用:

text 代码段:

C代码被翻译成二进制指令后存储在可执行文件中,当可执行文件被操作系统执行时,它会把里面的二进制指令(编译后的代码)加载到这个内存段,它里面的内容决定了程序如何执行,为了避免程序被破坏、修改,所以它的权限是只读。

该内存段分为两个部分:

r-x:二进制指令 r--:常量数据

注意:该内存段的内容如果被强制修改会产生段错误(非法使用内存)。

data 数据段:

存储的是初始化过(初始化的值非零)的全局变量

存储在该内存段的变量,被const修饰后,就会改存储到text内存段,变成真正的常量。

bss 静态数据段:

存储的是未初始化的全局变量

操作系统把程序被加载到内存后,会把该内存段进行初始化,也就是所有字节赋值为零,所以全局变量的默认值不是随机,而是零。

heap 堆:

该内存段由程序员手动调用内存管理函数(malloc/free),进行分配、释放,它的分配释放受程序员的控制,适合存储一些需要长期使用的数据。

它的大小不受限制,理论上能达到物理的上限,所以适合存储大量的数据。

该内存段无法取名字,也就是无法与标识符建立联系,必须与指针配合使用。

stack 栈:

存储的是局部变量、块变量

该内存段会随着程序的执行自动的分配(定义局部变量、块变量)、释放(函数执行完毕自动释放局部变量、块变量),虽然使用比较方便,但它的释放不受程序员控制,长期使用的数据不能存储在栈内存中。

该内存的大小有限,在终端执行: ulimit -s 可以查看当前系统栈内存的使用上限,我们使用虚拟机ubuntu的栈内存使用上限是8192kb,一旦超过这个限制就会产生段错误。可以使用ulimit -s <size> 命令设置栈内存的使用上限。

静态内存:

当程序完成编译 text、data、bss 三个内存段的大小就确定,在程序运行期间大小不会有任何变化,可以使用size命令查看程序的这三个内存段的大小。

sunll@:~/标准C语言$ size ./a.out
   text    data     bss     dec     hex filename
   3884     312      96    4292    10c4 ./a.out
动态内存:

heap、stack两个内存段,会随着程序的执行,而动态变化。

当程序运行时,/proc/程序编号/maps 文件里记录程序执行过程中内存的使用情况,程序运行结束这个文件就消失了。

使用ps aux 命令查看所有进程的编号,getpid函数可以获取当前进程的编号。

练习:

1、使用size命令,查看各大内存段大小,全局变量的存放在哪里。

2、全局变量的默认值。

3、使用ulimit -s,查看内存的使用上限,修改栈内存的使用上限。

4、根据进程的编号,查看对应的maps文件。

二、变量属性和分类

变量的属性
  • 作用域:变量的使用范围。

  • 存储位置:变量使用那个内存段存储数据,决定了变量在运行期间能否被释放(销毁),能否被修改。

  • 生命周期:变量从定义、分配内存到内存销毁的时间段。

全局变量:

定义在函数外的变量叫全局变量。

  • 作用域:本程序内任何位置都可以使用。

  • 存储位置:初始化的全局变量使用的是data内存段,未初始化的全局变量使用的是bss内存段。

  • 生命周期:从程序开始执行,到程序执行结束。

局部变量:

定义在函数内的变量叫局部变量。

  • 作用域:只能在它所在的函数内使用(从定义的位置开始,到函数结束)。

  • 存储位置:使用的是stack内存段。

  • 生命周期:当它所在的函数被调用后,执行到局部变量的定义语句时局部变量就会被创建(操作系统会给局部变量的变量名分配一块stack内存),当函数执行结束后,局部变量就被销毁了。

块变量:

定义在if、for、while、do while语句块内的变量叫局部变量,就是特殊的局部变量。

  • 作用域:只能在它所在的语句块内使用。

  • 存储位置:使用的是stack内存段。

  • 生命周期:当它所在的函数被调用后,执行到块变量的定义语句时块变量就会被创建(操作系统会给块变量的变量名分配一块stack内存),当出了它所在的大括号,块变量就被销毁了。

int main()
{
    for(int i=0; i<10; i++)
    {   
        printf("%p\n",&i);
    }   
    //printf("%d\n",i);     //   i已经被销毁,无法使用
    for(int j=0; j<10; j++)                                            
    {   
        printf("%p\n",&j);
    }
}
    //  i j 的地址编号相同的,但是循环变量i离开了for循环后已经被销毁了,j的地址相同只是刚好重新使用同一个内存而已
#include <stdio.h>
​
int num = 123;
​
int main(int argc,const char* argv[])
{
    printf("%d\n",num); 
    int num = 456;
    printf("%d\n",num);
    for(int i=0;i<1;i++)
    {   
        printf("%d\n",num);
        int num = 789;
        printf("%d\n",num);
    }   
    printf("%d\n",num);                                                
}
​

注意:全局变量、局部变量、块变量可以同名,不会造成命名冲突,局部变量会屏蔽同名的全局变量,块变量会屏蔽同名的全局变量、局部变量。

解决: 一般为了解决全局变量与局部变量命名冲突问题,全局变量一般首字母大写,局部变量一般全部小写

全局变量的优点和缺点:
优点:

使用方便,避免了函数之间传参产生的消耗,提高程序的运行速度。

缺点:

程序运行期间全局变量所占用的内存不会被销毁,可能会产生内存浪费。

命名冲突的可能性比较大,可能会与其它文件的全局变量、函数、结构、联合、枚举、宏命名冲突。

#include <stdio.h>
​
int scanf;  //  全局变量,很容易起命名冲突
int main()
{
    int scanf;  //  局部变量 不容易起冲突
}
总结:

全局变量尽量少用,或者不用。

三、修饰变量的关键字——类型限定符

<类型限定符> 数据类型 变量名;

typedef
typedef int num;
num n1; //n1 就是int类型

变量名被typedef修饰后,就会变成定义它的数据类型,此时该名字不是变量名而是类型名,之后就可以使用这种新的数据类型定义变量、数组了,该功能是为了给复杂的数据类型重新定义一个简短的类型名。

由于无符号整型使用比较麻烦,所以标准库中为我们定义一些简短的无符号整型的类型名,就使用typedef定义的,实现在stdint.h头文件里。

typedef signed char     int8_t;
typedef short int       int16_t;
typedef int             int32_t;
typedef long long int   int64_t;
​
typedef unsigned char       uint8_t;
typedef unsigned short int  uint16_t;
typedef unsigned int        uint32_t;
typedef unsigned long long int  uint64_t;

注意:在之后的学习过程中,如果遇到一些xxx_t的数据类型,都使用typedef重定义,例如:time_t,size_t pid_t。

注意:后续在定义结构时,可以使用typedef缩短结构类型名

auto
auto int num;

早期的C语言用它来修饰自动分配、释放内存的变量,也就是局部变量和块变量,但由于代码使用的变量绝大多数都是局部变量和块变量,所以就约定,该关键字不加就代码加,所以该关键字已经没有实用价值了。

在C++11的语法标准中,auto有了新的功能,就是定义自动类型的变量,编译器会根据变量的初始值,自动设置变量的数据类型。 ​ auto num = 1234; // num int类型

auto f = 3.14; // f double类型

编译指令:g++ xxx.c -std=c++11

注意:虽然auto关键字,已经不再使用,但基本功能还保留着,所以它不能修饰全局变量。

const
const int num;

const的意思是常量,但实际它只是为变量提供一层保护,被它修饰的变量不能显式修改,但可以隐式修改,也就被它修饰后并不能变成真正的常量。

#include <stdio.h>

int main(int argc,const char* argv[])
{
    const int num = 10; 
    int* p = (int*)&num;                                               
    *p = 88; 
    //num = 88;
    printf("%d\n",num);
    printf("%d\n",num);
}

注意:存储在data内存段的变量,被const修饰后就会变成真正的常量,存储位置被修改为text,其实是修改了data段和text段的分界线。如果就算隐式修改也会段错误

static

static既可以修饰变量,也可以修饰函数,主要有三大功能:

限制作用域:

默认情况下全局变量、函数的作用域是整个程序都可以使用,被static修饰后,就只能在它所在的.c文件内使用。

该功能可以避免全局变量、函数的命令冲突,也能防止全局变量、函数被外部修改、调用,提高代码的安全性。

普通全局变量、函数也叫外部变量、外部函数,被static修饰后就叫做内部变量、内部函数、静态全局变量。

改存储位置:

局部变量、块变量被static修饰后,存储位置就由stack改data、bss,称呼为静态局部变量、静态块变量。

静态局部变量、静态块变量的默认值不再是随机的,而是零。

延长生命周期:

由于静态局部变量、静态块变量的存储位置由stack(动态分配、释放)改为data、bss,所以静态局部变量、静态块变量不会随着函数的执行结束而销毁,而是和全局变量的生成周期一样。

注意:

static修饰局部变量、块变量,会改变它们的存储、延长生命周期,但并不会改变它们的作用域。

volatile
int num1 = 10;
printf("%d\n",num1);
num1+10;
num1*100;
volatile int num;	//	告诉编译器不要做取值优化

在程序中使用到num变量时,系统会从内存中读取该num的值交给CPU运算,如果之后num变量的值没有发生明显变化,再次使用变量时系统会直接使用上次读取的旧值,而不会再从内存中读取。这编译器对变量读值过程的优化。

volatile 关键字就告诉编译器不要优化变量的读值过程,每使用该变量时,都重新从内存中读取它的值。

int num = 10;
if(num == num)
{
    //	一定成立
}

volatile int num = 20;
if(num == num)
{
    //	有可能不成立
}

什么情况下需要使用volatile关键字:

变量被共享访问,且有多个执行者可以修改它的值,这种情况下变量就应该被volatile修饰。

情况1:多线程编程处理复杂问题时。

情况2:裸机编程、驱动编程时,软硬件共用的寄存器。

register

计算机的存储介质读写速度排序:机械硬盘->固态硬盘->内存条->高级缓存->CPU寄存器

register关键字的作用是申请把变量的存储介质由内存条改为CPU寄存器,一旦申请成功,变量的读写速度、运算速度会大大提高。

注意:CPU中的寄存器数量有限,申请不一定成功,只有需要长期大量运算的变量才适合用register关键字修饰。

注意:被register修饰过的变量,不能获取变量的地址。

extern

当使用其它.c文件中的全局变量时,需要像声明函数一样,对其它.c文件全局变量进行声明。

extern 类型 变量名; 

注意:声明变量只能解决编译时的问题,如果目标文件最终链接时,变量没有定义,依然会报错。

a.c:(.text+0x12):对‘num’未定义的引用,这种是链接时的错误。

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

相关文章:

  • 基于Python+Django+Vue3+MySQL实现的前后端分类的商场车辆管理系统
  • 解决表格出现滚动条样式错乱问题
  • 简述 synchronized 和 java.util.concurrent.locks.Lock 的异同?
  • 2024 kali操作系统安装Docker步骤
  • Rust学习(二):rust基础语法Ⅰ
  • 出海攻略,如何一键保存Facebook视频素材
  • Mybatis续
  • 36.贪心算法3
  • Android 内置应用裁剪
  • Java集合面试(上)
  • Kafka+PostgreSql,构建一个总线服务
  • k8s 微服务 ingress-nginx 金丝雀发布
  • ESRGAN——老旧照片、视频帧的修复和增强,提高图像的分辨率
  • ozon买家网址是什么,跨境电商ozon买家网址
  • YOLOv8的GPU环境搭建方法
  • 一个基于 laravel 和 amis 开发的后台框架, 友好的组件使用体验,可轻松实现复杂页面(附源码)
  • 一对一,表的设计
  • Nginx中return和rewrite的区别
  • python 实现entropy熵算法
  • c++ static(详解)
  • Snowflake怎么用?
  • 系统架构设计师 云原生架构篇
  • 字符设备驱动 — 4 异常与中断
  • 【Elasticsearch系列七】索引 crud
  • 【Java】网络编程-地址管理-IP协议后序-NAT机制-以太网MAC机制
  • 爬虫逆向学习(六):补环境过某数四代