C语言-内存、类型限定符

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

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

text 代码段:

​ C代码被翻译成二进制指令后存储在可执行文件中,当可执行文件被操作系统执行时,它会把里面的二进制指令(编译后的代码)加载到这个内存段,它里面的内容决定了程序如何执行,为了避免程序被破坏、修改,所以它的权限是只读。
​ 该内存段分为两个部分:
r-x:二进制指令
r--:常量数据
注意:该内存段的内容如果被强制修改会产生段错误(非法使用内存)。

data 数据段:

​ 存储的是初始化过(初始化的值非零)的全局变量
​ 存储在该内存段的变量,被const修饰后,就会改存储到text内存段,变成真正的常量。

bss 静态数据段:

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

heap 堆:

​ 该内存段由程序员手动调用内存管理函数(malloc/free),进行分配、释放,它的分配释放受程序员的控制,适合存储一些需要长期使用的数据。
​ 它的大小不受限制,理论上能达到物理的上限,所以适合存储大量的数据。
​ 该内存段无法取名字,也就是无法与标识符建立联系,必须与指针配合使用。

stack 栈:

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

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

静态内存:

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

size ./a.out
   text	   data	    bss	    dec	    hex	filename
   3884	    312	     96	   4292	   10c4	./a.out

动态内存:

heapstack两个内存段,会随着程序的执行,而动态变化。
​ 当程序运行时,/proc/程序编号/maps文件里记录程序执行过程中内存的使用情况,程序运行结束这个文件就消失了。
​ 使用ps aux命令查看所有进程的编号,getpid()函数可以获取当前进程的编号。

二、变量属性和分类

变量的属性

  • 作用域:变量的使用范围。
  • 存储位置:变量使用那个内存段存储数据,决定了变量在运行期间能否被释放(销毁),能否被修改。
  • 生命周期:变量从定义、分配内存到内存销毁的时间段。

全局变量:

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

  • 作用域:本程序内任何位置都可以使用。
  • 存储位置:初始化的全局变量使用的是data内存段,未初始化的全局变量使用的是bss内存段。
  • 生命周期:从程序开始执行,到程序执行结束。

局部变量:

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

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

块变量:

​ 定义在ifforwhiledo while语句块内的变量叫局部变量,就是特殊的局部变量。

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

注意:全局变量、局部变量、块变量可以同名,不会造成命名冲突,局部变量会屏蔽同名的全局变量,块变量会屏蔽同名的全局变量、局部变量。
解决: 一般为了解决全局变量与局部变量命名冲突问题,全局变量一般首字母大写,局部变量一般全部小写

全局变量的优点和缺点:

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

缺点

  1. 程序运行期间全局变量所占用的内存不会被销毁,可能会产生内存浪费。
  2. 命名冲突的可能性比较大,可能会与其它文件的全局变量、函数、结构、联合、枚举、宏命名冲突。
#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;

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() {
    const int num = 10; 
    int *p = (int *)&num;                                               
    *p = 88; 
    //num = 88;
    printf("%d\n",num);
}

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

static

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

限制作用域:

​ 默认情况下全局变量、函数的作用域是整个程序都可以使用,被static修饰后,就只能在它所在的.c文件内使用。
​ 该功能可以避免全局变量、函数的命令冲突,也能防止全局变量、函数被外部修改、调用,提高代码的安全性。
​ 普通全局变量、函数也叫外部变量、外部函数,被static修饰后就叫做内部变量、内部函数、静态全局变量。

改存储位置:

​ 局部变量、块变量被static修饰后,存储位置就由stackdata、bss,称呼为静态局部变量、静态块变量。
​ 静态局部变量、静态块变量的默认值不再是随机的,而是零。

延长生命周期:

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

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

volatile

​ 在程序中使用到变量时,系统会从内存中读取该变量的值交给CPU运算,如果之后该变量的值没有发生明显变化,再次使用变量时系统会直接使用上次读取的旧值,而不会再从内存中读取。这编译器对变量读值过程的优化。
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’未定义的引用,这种是链接时的错误。

上一篇:数组、函数及位运算

下一篇:指针

posted @ 2024-05-05 19:02  sleeeeeping  阅读(77)  评论(0)    收藏  举报