一、程序的内存分段:
当执行程序的运行命令后,操作系统会给程序分配它所需要的内存,并划分成以下内存段供程序使用:
text 代码段:
C代码被翻译成二进制指令后存储在可执行文件中,当可执行文件被操作系统执行时,它会把里面的二进制指令(编译后的代码)加载到这个内存段,它里面的内容决定了程序如何执行,为了避免程序被破坏、修改,所以它的权限是只读。
该内存段分为两个部分:
r-x:二进制指令 r--:常量数据
int num = 10; printf("Hello World\n")
注意:该内存段的内容如果被强制修改会产生段错误(非法使用内存)。【char* p = “heheheh”】
data 数据段:
存储的是初始化过的全局变量(初始化的值不为0)
存储初始化过的静态局部变量(被static修饰过的局部、块变量)
存储在内存段的变量,被const修饰后,就会改存储到text内存段,变成真正的常量。
bss 静态数据段:
存储的是未初始化的全局变量
存储未初始化过的静态局部变量(被static修饰过的局部、块变量)
操作系统把程序被加载到内存后,会把该内存段进行初始化,也就是所有字节赋值为零,所以全局变量的默认值不是随机,而是零。
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函数可以获取当前进程的编号。
二、变量属性和分类
变量的属性
-
作用域:变量的使用范围。
-
存储位置:变量使用那个内存段存储数据,决定了变量在运行期间能否被释放(销毁),能否被修改。
-
生命周期:变量从定义、分内存到内存销毁的时间段。
全局变量:
定义在函数外的变量叫全局变量。
-
作用域:本程序中任何位置都可以使用。
-
存储位置:初始化的全局变量使用的是data内存段,未初始化的全局变量使用的是bss内存段。
-
生命周期:从程序开始执行,到程序执行结束。
局部变量:
定义在函数内的变量叫局部变量。
-
作用域:只能在它所在的函数内使用(从定义的位置开始,到函数结束)。
-
存储位置:使用的是stack内存段。
-
生命周期:当它所在的函数被调用后,执行到局部变量的定义语句时局部变量就会被创建(操作系统会给局部变量的变量名分配一块stack内存),当函数执行结束后,局部变量就被销毁了。
块变量:
定义在if、for、while、do while语句块内的变量叫块变量,就是特殊的局部变量。
-
作用域:只能在它所在的语句块内使用。
-
存储位置:使用的是stack内存段。
-
生命周期:当它所在的函数被调用后,执行到块变量的定义语句时块变量就会被创建(操作系统会给块变量的变量名分配一块stack内存),当出了它所在的大括号,块变量就被销毁了。
#include <stdio.h> int main(int argc,const char* argv[]) { for(int i=0; i<10; i++) { printf("%p\n",&i); } for(int j=0; j<10; j++) { printf("%p\n",&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); do{ printf("%d\n",num); int num = 789; // 此时使用的是块变量,同名的全局变量、局部变量已经被屏蔽 printf("%d\n",num); }while(0); }
全局变量的优点和缺点:
优点:
使用方便,避免了函数之间传参产生的消耗,提高程序的运行速度。
缺点:
程序运行期间全局变量所占用的内存不会被销毁,可能会产生内存浪费。
命名冲突的可能性比较大,可能会与其它文件的全局变量、函数、结构、联合、枚举、宏命名冲突。
总结:
全局变量尽量少用,或者不用。
建议:全局变量首字母大写,局部变量全部小写
三、修饰变量的关键字
<类型限定符> 数据类型 变量名;
typedef
变量被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。
auto
int num;
早期的C语言用它来修饰自动分配、释放内存的变量,也就是局部变量和块变量,但由于代码使用的变量绝大多数都是局部变量和块变量,所以就约定,该关键字不加就代码加,所以该关键字已经没有实用价值了。
在C++11的语法标准中,auto有了新的功能,就是定义自动类型的变量,编译器会根据变量的初始值,自动设置变量的数据类型。 auto num = 1234;
编译指令:g++ xxx.c -std=c++11
注意:虽然auto关键字,已经不再使用,但基本功能还保留着,所以它不能修饰全局变量。
const
const的意思是常量,但实际它只是为变量提供一层保护,被它修饰的变量不能显式修改,但可以隐式修改,也就被它修饰后并不能变成真正的常量。
#include <stdio.h> int main() { const int num = 1234; int* p = (int*)# *p = 6666; printf("%d\n",num); } // 执行结果是:6666
注意:存储在data内存段的变量,被const修饰后就会变成真正的常量,存储位置被修改为text,其实是修改了data段和text段的分界线。
#include <stdio.h> const int num = 1234; int main() { int* p = (int*)# *p = 6666; printf("%d\n",num); } // 执行后会出现段错误
static
static既可以修饰变量,也可以修饰函数,主要有三大功能:
限制作用域:
默认情况下全局变量、函数的作用域是整个程序都可以使用,被static修饰后,就只能在它所在的.c文件内使用。
该功能可以避免全局变量、函数的命令冲突,也能防止全局变量、函数被外部修改、调用,提高代码的安全性。
普通全局变量、函数也叫外部变量、外部函数,被static修饰后就叫做内部变量、内部函数、静态全局变量。
改存储位置:
局部变量、块变量被static修饰后,存储位置就由stack改data、bss,称呼为静态局部变量、静态块变量。
静态局部变量、静态块变量的默认值不再是随机的,而是零。
延长生命周期:
由于静态局部变量、静态块变量的存储位置由stack(动态分配、释放)改为data、bss,所以静态局部变量、静态块变量不会随着函数的执行结束而销毁,而是和全局变量的生成周期一样。
#include <stdio.h> void func(int i) { static int num = 1; num *= i; printf("%d\n",num); } int main() { for(int i=1; i<10; i++) { func(i); } }
注意:
static修饰局部变量、块变量,会改变它们的存储、延长生命周期,但并不会改变它们的作用域。
volatile
int num = 123; ... if(num == num) { } // 默认情况下,比较结果永远为真 volatile int num = 123; ... if(num == num) { } // num被volatile修饰后,比较结果可能不为真
在程序中使用到num变量时,系统会从内存中读取该num的值交给CPU运算,如果之后num变量的值没有发生明显变化,再次使用变量时系统会直接使用上次读取的旧值,而不会再从内存中读取。这编译器对变量读值过程的优化。
volatile 关键字就告诉编译器不要优化变量的读值过程,每使用该变量时,都重新从内存中读取它的值。
什么情况下需要使用volatile关键字:
变量被共享访问,且有多个执行者可以修改它的值,这种情况下变量就应该被volatile修饰。
情况1:多线程编程处理复杂问题时。
情况2:裸机编程、驱动编程时,软硬件共用的寄存器。
register
计算机的存储介质读写速度排序:机械硬盘->固态硬盘->内存条->高级缓存->CPU寄存器
register关键字的作用是申请把变量的存储介质由内存条改为CPU寄存器,一旦申请成功,变量的读写速度、运算速度会大大提高。
#include <stdio.h> int main() { int index = 0; while(index < 0x7fffffff) index++; } /* real 0m0.463s user 0m0.463s sys 0m0.001s */ #include <stdio.h> int main() { register int index = 0; while(index < 0x7fffffff) index++; } /* real 0m0.463s user 0m0.463s sys 0m0.001s */
注意:CPU中的寄存器数量有限,申请不一定成功,只有需要长期大量运算的变量才适合用register关键字修饰。
注意:被register修饰过的变量,不能获取变量的地址。
extern
当使用其它.c文件中的全局变量时,需要像声明函数一样,对其它.c文件全局变量进行声明。
extern 类型 变量名;
注意:声明变量只能解决编译时的问题,如果目标文件最终链接时,变量没有定义,依然会报错。
a.c:(.text+0x12):对‘num’未定义的引用,这种是链接时的错误。
计算机的内存长什么样子:
1、计算机的内存就像是一叠非常厚的"便签",一张便签就相当于一个字节的内存,一个字节有8个二进制位。
2、每一张"便签"的都有自然排序形成的一个编号,计算机根据便签的编号访问、使用"便签"。
3、CPU会有若干个金手指,每根金手指能感知高低两种电流,低电流当作二进制的0,高电流当作二进制的1,我们所说的32位的CPU,指的是CPU有32个金手指用于感知便签的编号:
便签的最小编号 00000000000000000000000000000000 = 0 便签的最大编号 11111111111111111111111111111111 = 4294967295 所以32位的CPU最多能使用 4294967296byte->4194304kb->4096mb->4gb
4、便签的编号也就是内存的地址,是一种无符号整数。
什么是指针:
1、指针(pointer)是一种特殊的数据类型,使用它可以定义指针变量,简称指针。
2、指针变量中存储的是内存的地址,是一种无符号的整数。
3、通过指针变量中记录的内存地址,我们可以读取内存中所存储的数据,也可以向内存中写入数据。
4、一般使用%p以十六进制格式显示内存地址。
如何使用指针:
定义指针变量:
类型* 指针变量名;
int* xxx_p;
char* \ double*
1、指针变量中只记录了内存中某一个字节的地址编号,我们把它当作一个内存块的首地址,当使用指针变量访问内存时具体访问多少个字节,由指针变量的类型决定。
char* p; // 能访问1字节 short* p; // 能访问2字节 int* p; // 能访问4字节
2、普通变量与指针变量的用法不同,为了避免混用,所以指针变量建议一般以p结尾,加以区分。
3、指针变量不能连续定义,一个*只能定义出一个指针变量。
int num1,num2,num3; int* p1,p2,p3; // p1是指针变量,p2、p3是普通的int类型变量 int *p1,p2,p3; int *p1,*p2,*p3; // p1、p2、p3都是指针变量 typedef int* intp; intp p1,p2,p3; // p1、p2、p3都是指针变量
4、指针变量与普通变量一样,默认值是随机的(野指针),为了安全尽量给指针变量初始化,如果不知道该赋什么值,可以先初始化为NULL(空指针)。
int* p1; // 野指针 int* p2 = NULL; // 空指针
给指针变量赋值:
指针变量 = 内存地址。
所谓的给指针变量赋值,就是给指针变量存储一个内存地址,如果该内存地址是非法的,当使用指针变量访问内存是就会出现段错误。
//指向堆内存 int* p = malloc(4); // 把堆内存的地址赋值给指针变量 //指向num所在内存段(data\bss\stack) int* p = # // &计算出变量的内存地址(单目运算符) 注意:num变量的类型必须与p的类型相同
指针变量解引用:
*指针变量
指针变量赋值就是引用一块内存,解引用就是根据指针变量存储的内存地址,去访问内存,具体访问多少个字节由指针变量的类型决定。
如果指针变量中存储的是非法内存地址,该动作会出现段错误,要从指针变量赋值的步骤去解决。
int num = 1234; int* p = # //*p <=> num; printf("%d",*p); *p = 2345; printf("%d\n",num);
int main() { const int num = 1234; int* p = (int*)# *p = 6666; printf("%d\n",num); }
证明指针变量存储的就是一个整数:
void func(int addr)//long 建议用long类型因为是8字节,指针可能是8字节{ *(int*)addr = 100; printf("%#x\n",addr); } int main(int argc,const char* argv[]){ int num = 0; printf("%p\n",&num); func(&num); printf("%d\n",num); }
为什么要使用指针:
1、函数之间需要共享变量
函数之间的命名空间是互相独立,并且是以赋值方式单向传参的(值传递),所以传参无法解决共享变量的问题
全局变量虽然可以在函数之间共享,但过多使用全局变量可能会造成命名冲突和内存浪费。
使用数组还需要额外传递长度
当函数需要返回两个以上的参数(返回值)时,就需要共享变量了(输出型参数)
虽然函数之间命名空间是相互独立的,但是所使用的地址空间是同一个,所以指针可以解决
#include <stdio.h> //函数通过指针共享变量 int scanf = 100;//定义全局的可能命名冲突 void func(int* p) { printf("%d %p\n",*p,p); *p = 100; printf("%d %p\n",*p,p); } int main(int argc,const char* argv[]) { int num = 0; printf("%d %p\n",num,&num); func(&num); printf("%d\n",num); }
#include <stdio.h> #include <time.h> //说明想要获取多个返回值,可以借助指针返回 int _time(int* p) { *p = 1234; return 1234; } int main(int argc,const char* argv[]) { /* time_t sec = 0; time_t s = time(&sec); */ time_t sec = 0; time_t s = _time(&sec); printf("%lu %lu\n",s,sec); }
2、使用指针变量可以提高函数的传参效率
函数之以赋值方式传参的,也就是内存的拷贝,把一个变量的内存内容拷贝给别一个变量,当变量的字节数比较大时(大于4字节),传参效率就很低,而传递变量的地址,只需要拷贝4|8字节内存。
#include <stdio.h> void func(long double f) { } int main() { long double f = 3.14; for(int i=0; i<1000000000; i++) { func(f); f++; } } /* real 0m5.527s user 0m5.523s sys 0m0.004s */ void func(long double* f) { } int main() { long double f = 3.14; for(int i=0; i<1000000000; i++) { func(&f); f++; } } /* real 0m2.553s user 0m2.553s sys 0m0.000s */
3、使用堆内存时必须与指针变量配合
堆内存无法取名字,标准库、操作系统提供的内存分配接口函数的返回值都是内存地址,所以必须使用变量配合才能使用堆内存。
void *malloc(size_t size); void *calloc(size_t nmemb, size_t size); void *realloc(void *ptr, size_t size);
注意:由于使用指针变量具有一定的危险,所以除了以上情况,不要轻易使用指针。【学海无涯】
使用指针要注意的问题:
空指针:
指针变量中如果存储的是NULL,那么它就是空指针,因此操作系统规定程序不能访问该内存,只要访问就一定会产生段错误,同时也是返回值是指针类型的函数执行错误的标志。
int* func(void) { if(条件) { return NULL; // 返回NULL表示函数执行有误 } }
如何避免空指针产生的段错误?
对来历不明的指针进行解引用前,要先判断是否是空指针。
1、当指针变量接收了函数的返回值,判断是否是空指针,既能避免访问空指针产生的段错误,也能知道该函数执行是否失败、出错。
2、如果设计的函数参数是指针变量,那么调用者传递实参就可能是空指针,对指针变量解引用前要先判断。
if(NULL == p) // 正确写法 { } if(p == NULL) // 错误写法,当少写一个等号时,就变成了给p赋值为空指针 { } if(!p) //大多数系统的NULL是0,但有少数系统的NULL是1 { } 【vim /usr/include/stdio.h 查找 :/NULL】 【find /usr/include -name stddef.h】
野指针:
指针变量中存储的地址,无法确定是否是合法的内存地址,这种指针变量被称为野指针。
对野指针解引用的后果:
1、一切正常,指针变量恰好存储的是空闲的内存地址,概率不高。
2、段错误,存储的是非法的内存地址。
3、脏数据,存储的是其它变量的内存地址。
如何避免野指针产生的错误:
野指针无法判断出来,但所有的野指针都是人为制造出来的,所以要想避免野指针产生的错误,只能不制造野指针。
如何不制造野指针:
1、定义指针变量时一定要初始化,要么赋值一个合法的内存地址,要么初始化NULL。
2、不返回局部变量、块变量的地址,当函数执行完毕后,局部变量和块变量就被销毁。
3、与堆内存配合的指针变量,当堆内存被释放、销毁,该指针变量要及时的赋值为NULL。
#include <stdio.h> //说明函数不能返回栈内存的地址 int* func(void) { int num = 10; int* p = # //return #还会有点警告 return p; } int main(int argc,const char* argv[]) { int* p = func(); printf("%d\n",*p); printf("hehehe\n"); printf("%d\n",*p);//返回的栈内存被上面的函数修改了 }
野指针比空指针的危害更大:
野指针产生的错误具有隐藏性、潜伏性、随机性,所以野指针比空指针危害更大。
作业:
1、实现一个函数,用于交换两个int变量的值。并调用它实现一个数组排序函数
#include <stdio.h> void swap(int* p1,int* p2) { if(NULL == p1 || NULL == p2) return; int tmp = *p1; *p1 = *p2; *p2 = tmp; } void sort(int arr[],int len) { for(int i=0; i<len-1; i++) { for(int j=i+1; j<len; j++) { if(arr[i] > arr[j]) { swap(&arr[i],&arr[j]); } } } } int main() { int num1 = 3, num2 = 7; printf("%d %d\n",num1,num2); swap(&num1,&num2); printf("%d %d\n",num1,num2); }
2、实现一个函数,用于计算两整数最大公约数和最小公倍数。
#include <stdio.h> // 最大公约数据使用返回值,最小公倍数存储在调用者提供的指针所指向的内存 int max_min(int num1,int num2,int* result) { if(NULL == result) return -1; /* int max = 1; for(int i=2; i<=num1; i++) { if(0 == num1%i && 0 == num2 % i) max = i; } for(int i=num1*num2; i>=num1; i--) { if(0==i%num1 && 0==i%num2) { *result = i; } } // *result = num1*num2/max; return max; */ for(int i=num1*num2; i>=num1; i--) { if(0 == i%num1 && 0 == i%num2) *result = i; } return num1 * num2 / *result; } int main() { int min; int max = max_min(4,6,&min); printf("%d %d\n",max,min); }