高级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*)# *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’未定义的引用,这种是链接时的错误。