C 高级编程day01——内存分区

一、数据类型

1.数据类型的概念

  什么是数据类型?为什么需要数据类型?数据类型是为了更好进行内存的管理,让编译器能确定分配多少内存。

  我们现实生活中,狗是狗,鸟是鸟等等,每一种事物都有自己的类型,那么程序中使用数据类型也是来源于生活。

  当我们给狗分配内存的时候,也就相当于给狗建造狗窝,给鸟分配内存的时候,也就是给鸟建造一个鸟窝,我们可以给他们各自建造一个别墅,但是会造成内存的浪费,不能很好的利用内存空间。

  我们在想,如果给鸟分配内存,只需要鸟窝大小的空间就够了,如果给狗分配内存,那么也只需要狗窝大小的内存,而不是给鸟和狗都分配一座别墅,造成内存的浪费。

  当我们定义一个变量,a = 10,编译器如何分配内存?计算机只是一个机器,它怎么知道用多少内存可以放得下10?

  所以说,数据类型非常重要,它可以告诉编译器分配多少内存可以放得下我们的数据

  严谨地说:

  (1)类型是对数据的抽象;

  (2)类型相同的数据具有相同的表示形式、存储格式以及相关操作;

  (3)程序中所有的数据都必定属于某种数据类型;

  (4)数据类型可以理解为创建变量的模具: 固定大小内存的别名;

2.数据类型大致分类

在这里插入图片描述

3.数据类型别名

  有时候会使用到一些类型名很长的数据类型,为了提高效率和精简,就推出了起别名的方法,在定义变量的时候,可以直接用数据类型的别名来定义。

3.1 测试代码:

typedef int u32;

typedef struct stu
{
        char name[21];
        int age;
}S;

int main()
{
        u32 a = 10;

        S s1;
        memset(&s1,0,sizeof(S));
        strcpy(s1.name,"伊丽莎白");
        s1.age = 20;

        printf("a=%d\n",a);
        printf("s1.name=%s,s1.age=%d\n",s1.name,s1.age);
        return 0;
}

  测试结果:使用数据类型的别名定义数据成功。
在这里插入图片描述

3.2 注意事项

  结构体的别名是放在右大括号的右边的,看下面的代码的大写的S,S不是定义的结构体变量,不要混淆了。加了 typedef ,S 是结构体的别名。

typedef struct stu
{
        char name[21];
        int age;
}S;

  当然也可以声明好了数据类型之后,再起别名:

struct stu
{
        char name[21];
        int age;
};
typedef struct stu S;

4.void数据类型

  void字面意思是”无类型”,void* 无类型指针,无类型指针可以指向任何类型的数据

  void定义变量是没有任何意义的,当你定义void b,编译器会报错。因为无法给 void 类型变量分配内存。
在这里插入图片描述
  void真正用在以下两个方面:

  (1)对函数返回的限定;

  (2)对函数参数的限定;

//1. void修饰函数参数和函数返回
void test01(void){
	printf("hello world");
}

4.1 void * 转换

  当然 void 还有其他的用法,比如说 void *,称为泛型指针,或者万能指针。因为 void * 可以不通过强制类型转换就能转换成其他类型指针。普通的数据类型之间的转换要强制转换。

(1)测试代码:

int main()
{
        int a = 10;
        int *p_a = &a;

        char ch = 'C';
        char *p_ch = &ch;
        
        //将char * 类型赋值给 int *
        p_a = p_ch;
       
        return 0;
}

测试结果:普通类型的如果不加强制转换,就会出警告。
在这里插入图片描述
(2)定义一个万能指针,测试

测试代码:

		//将void * 类型赋值给 int *
        p_a = p_void;

        //将void * 类型赋值给 char *
        p_ch = p_void;

编译通过,没有报错:
在这里插入图片描述
(3 )void* 可以指向任何类型的数据,被称为万能指针

void test03(){
	int a = 10;
	void* p = NULL;
	p = &a;
	printf("a:%d\n",*(int*)p);
	
	char c = 'a';
	p = &c;
	printf("c:%c\n",*(char*)p);
}

(4)void* 常用于数据类型的封装

void test04(){
	void * memcpy(void * _Dst, const void * _Src, size_t _Size);
}

5.sizeof 操作符

  sizeof是c语言中的一个操作符,类似于++、–等等。sizeof能够告诉我们编译器为某一特定数据或者某一个类型的数据在内存中分配空间时分配的大小,大小以字节为单位

5.1 基本语法

sizeof(变量);
sizeof 变量;
sizeof(类型);

5.2 sizeof 注意点

  (1)sizeof返回的占用空间大小是为这个变量开辟的大小,而不只是它用到的空间。和现今住房的建筑面积和实用面积的概念差不多。所以对结构体用的时候,大多情况下就得考虑字节对齐的问题了

  (2)sizeof返回的数据结果类型是unsigned int

  (3)要注意数组名和指针变量的区别。

  通常情况下,我们总觉得数组名和指针变量差不多,但是在用sizeof的时候差别很大,对数组名用sizeof返回的是整个数组的大小,而 对指针变量进行操作的时候返回的则是指针变量本身所占得空间,在32位机的条件下一般都是4。而且当数组名作为函数参数时,在函数内部,形参也就是个指针,所以不再返回数组的大小;

5.3 测试

(1)测试代码:

// 1.测试sizeof 的用法
void test_1()
{
        int b;

        printf("(1)sizeof(变量类型):要用括号将变量类型括起来。\n");
        printf("sizeof(int) = %d\n",sizeof(int));

        printf("(2)sizeof 变量名:可以不用括号将变量名括起来。\n");
        printf("sizeof b = %d\n",sizeof b);

        printf("(3)sizeof(变量名):可以用括号将变量名括起来。\n");
        printf("sizeof (b) = %d\n",sizeof (b));
}

// 2.测试sizeof 的返回值
void test_2()
{
        unsigned int a = 4;
        if ( a - 10 > 0 ) { printf("大于0\n"); }
        else { printf("小于0\n"); }

        int b = 5;

        if( sizeof(b) - 10 > 0 )
        {
                printf("大于0\n");
        }
        else
        {
                printf("小于0\n");
        }
}

void test_3(int arr[])
{
        printf("调用函数传给被调用函数的的是指针,所以在别调用函数中,数组名是指针,如果使用sizeof(数组名),其实是计算指针的大小。\n");

        printf("sizeof(arr) = %d\n",sizeof(arr));
}

int main()
{
        test_1();

        test_2();

        int arr[]={ 1,2,3,4,5,6 };

        printf("在调用函数中,sizeof(数组名) 是计算数组的大小:sizeof(arr) = %d\n",sizeof(arr));

        test_3(arr);
		return 0;
}      

(2)测试结果:

  test_1:
在这里插入图片描述
  test_2:这里解释一下为什么会是大于0,因为在计算的时候,10会转换为 unsigned 类型计算,计算结果也是 unsigned 类型,所以会大于0。
在这里插入图片描述

5.4 数组名与指针

  数组做函数参数时,将退化为指针。

  test_3():
在这里插入图片描述

6.变量

6.1 变量的概念

  既能读又能写的内存对象,称为变量;若一旦初始化后不能修改的对象则称为常量

6.2 变量名的本质

  (1)变量名的本质:一段连续内存空间的别名;

  (2)程序通过变量来申请和命名内存空间 int a = 0;

  (3)通过变量名访问内存空间;

  (4)不是向变量名读写数据,而是向变量所代表的内存空间中读写数据

6.3 修改变量的值的两种方式

  直接修改和间接修改。直接修改:使用变量名去修改内存的值;间接修改是通过指针去找到内存,然后修改内存中的值。

void test(){
	
	int a = 10;

	//1. 直接修改
	a = 20;
	printf("直接修改,a:%d\n",a);

	//2. 间接修改
	int* p = &a;
	*p = 30;

	printf("间接修改,a:%d\n", a);
}

二、程序的内存分区模型

1.内存分区

1.1 程序运行之前

  我们要想执行我们编写的c程序,那么第一步需要对这个程序进行编译。

  1)预处理:宏定义展开、头文件展开、条件编译,这里并不会检查语法

  2)编译:检查语法,将预处理后文件编译生成汇编文件

  3)汇编:将汇编文件生成目标文件(二进制文件)

  4)链接:将目标文件链接为可执行程序

1.2 size + 可执行二进制文件

  当我们编译完成生成可执行文件之后,我们通过在linux下size命令可以查看一个可执行二进制文件基本情况:

在这里插入图片描述
  通过上图可以得知,在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为 代码区(text)、数据区(data)和未初始化数据区(bss)3 个部分(有些人直接把data和bss合起来叫做静态区或全局区)

(1)代码区

  存放 CPU 执行的机器指令。通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指t令。另外,代码区还规划了局部变量的相关信息。

(2)全局初始化数据区/静态数据区(data段)

  该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量)和常量数据(如字符串常量)。

(3)未初始化数据区(又叫 bss 区)

  存入的是 全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。

  总体来讲说,程序源代码被编译之后主要分成两种段:程序指令(代码区)和程序数据(数据区)。代码段属于程序指令,而数据域段和.bss段属于程序数据。数据主要是全局变量和静态变量。

1.3 那为什么把程序的指令和程序数据分开

(1)防止程序的指令有意或者无意被修改

  程序被load到内存中之后,可以将数据和代码分别映射到两个内存区域。由于数据区域对进程来说是可读可写的,而指令区域对程序来讲说是只读的,所以分区之后呢,可以将程序指令区域和数据区域分别设置成可读可写或只读。这样可以防止程序的指令有意或者无意被修改;

(2)可以节省大量的内存

  当系统中运行着多个同样的程序的时候,这些程序执行的指令都是一样的,所以只需要内存中保存一份程序的指令就可以了,只是每一个程序运行中数据不一样而已,这样可以节省大量的内存。比如说之前的Windows Internet Explorer 7.0运行起来之后, 它需要占用112 844KB的内存,它的私有部分数据有大概15 944KB,也就是说有96 900KB空间是共享的,如果程序中运行了几百个这样的进程,可以想象共享的方法可以节省大量的内存。

1.4 程序运行之后

  程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,操作系统把物理硬盘程序load(加载)到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区

(1)代码区(text segment)

  加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。

(2)未初始化数据区(BSS)

  加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。

(3)全局初始化数据区/静态数据区(data segment)

  加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。

(4)栈区(stack)

  栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。

(5)堆区(heap)

  堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。

2.分区模型

2.1 栈区

  由系统进行内存的管理。主要存放函数的参数以及局部变量。在函数完成执行,系统自行释放栈区内存,不需要用户管理。

(1)测试代码:

int *myFunc()
{
        int a=10;
        return &a;
}
void test_1()
{
        int *p = myFunc();
        printf("*p = %d\n",*p);
        printf("*p = %d\n",*p);
}
int main()
{
        test_1();
        return 0;
}

  (2)测试结果:

  1)在编译的时候会警告 myFunc 函数返回的是一个局部变量的地址
在这里插入图片描述
  2)运行结果:打印同一个地址对应的内存中的值两次,两次结果不相同。第一次结果是正确的,第二次是不正确的。
在这里插入图片描述

  再来查看代码:在 myFunc() 中 a 是局部变量,存放在栈区中,myFunc() 执行完了之后,a变量对应的内存就被释放掉了。理应来说其他的函数不能使用 a 这个变量了,因为a已经被释放掉,a 变量对应的内存中存放的数据是不确定的。

int *myFunc()
{
        int a=10;
        return &a;
}

  但是在 test_1() 函数中依然操作a变量地址对应的内存,那为什么在第一次打印的时候,结果是正确的呢?这样是由于系统帮忙优化了,将保留了a对应的内存的值保留了一次,或者说暂时保留了。但是一般来说你只能操作一次已经被释放的内存,但是我们一般不怎么做,不安全。

void test_1()
{
        int *p = myFunc();
        printf("*p = %d\n",*p);
        printf("*p = %d\n",*p);
}
栈区注意事项

  不要返回局部变量的地址,局部变量在函数执行之后就被释放了,释放的内存就没有操作权限了,如果继续操作,结果未知

2.2 堆区

  由编程人员手动申请,手动释放,若不手动释放,程序结束后由系统回收,生命周期是整个程序运行期间。使用malloc或者new进行堆的申请。

(1)测试代码:

int *myFunc()
{
        int *arr = (int *)malloc(sizeof(int)*5);
        if(arr == NULL) return ;

        int i;
        for(i=0; i<5; i++)
        {
                arr[i] = 100+i;
        }

        return arr;
}

void test_1()
{       
        int *ret = myFunc();

        int i;
        for(i=0; i<5; i++)
        {
                printf("ret[%d] = %d\n",i,ret[i]);
        }
        free(ret);
}
int main()
{
        test_1();  
        free(arr);     
        return 0;
}

(2)测试结果:
在这里插入图片描述

堆区分配内存的注意事项

  给指针进行开辟内存的时候,传入的函数中不要用同等级的指针,需要传入指针的地址,函数中接收这个地址后再进行开辟内存。

  也就是说如果一个函数想要开辟内存,但是开辟内存的操作在另一个函数中,需要调用那个包含了开辟内存操作的函数(被调用函数)去开辟内存,不在本函数开辟开辟内存。

  如果调用函数将地址传给被调用函数的是一级指针的地址,那么被调用函数(开辟空间操作的函数)的参数应该是二级指针。这种情况常常发生在字符串中。下面来看代码

  (1)第一次测试:

void allocateSpace(char *p)
{
        char *tmp = malloc(sizeof(char)*100);
        
        memset(tmp,0,sizeof(tmp));
 
        strcpy(tmp,"helloworld");

        p = tmp;
}

void test_2()
{
        char *str = NULL;

        allocateSpace(str);

        printf("str: ==%s==\n",str);
}

int main()
{
        test_2();
        return 0;
}

  其实上面的代码就相当于:把一个实参传给了形参,形参并不会改变实参的值,之前说过形参和实参操作的是两片不同的内存。所以要传的是实参的地址。最后str 的值依旧是空。

  把 char * 看成一种数据类型,就是实参传给形参了。所以解决办法就是要传实参的地址,被调用函数的参数改为接收实参的地址,就是要加上一个 * 号。

void allocateSpace(int p)
{
 
}

void test_2()
{
        int str;

        allocateSpace(str);

        printf("str: ==%s==\n",str);
}

  (2)分析完了可能出现的结果,那么就来验证一下,查看测试结果:和分析的一样,是空值。
在这里插入图片描述

  (3)那么我们就按照分析来,修改代码,把 char * 看成一种数据类型,然后将实参的地址传给被调用函数,被调用函数的参数改为接收实参的地址,就是要再加上一个 * 号。最终目的是让形参操作的是实参的内存。

  测试代码:

void allocateSpaceMix(char **p)
{       
        char *tmp = malloc(sizeof(char) * 100);
        
        memset(tmp,0,sizeof(tmp));
        
        strcpy(tmp,"helloworld");
        
        *p = tmp;

}

void test_3()
{       
        char *str = NULL;
        
        allocateSpaceMix(&str);
        printf("str: ==%s==\n",str);
}

  测试结果:
在这里插入图片描述
  (4)注意修改的细节:记得在调用函数中传的是地址所以用取地址符;p 是二级指针,tmp 是一级指针,p = tmp 这样赋值不对,*p 才是一级指针。二级指针存放的是一级指针的地址。

allocateSpaceMix(&str);
*p = tmp;

  (5)也可以这样修改,反正就是,要想改变实参的值,传实参的地址进去,然后接收实参的地址。指针也看成是一种数据类型,这样就不容易弄混乱。在给字符串开辟堆空间要注意。

void allocateSpaceMix(char **p)
{       
        *p = malloc(sizeof(char) * 100);
        
        memset(*p,0,sizeof(*p));
        
        strcpy(*p,"helloworld");
}

2.3 全局/静态区

  全局静态区内的变量在编译阶段已经分配好内存空间并初始化。这块内存在程序运行期间一直存在,它主要存储全局变量、静态变量和常量==。在程序运行前就已经分配好内存了。

(1)注意事项

  1)这里不区分初始化和未初始化的数据区,是因为静态存储区内的变量若不显示初始化,则编译器会自动以默认的方式进行初始化,即静态存储区内不存在未初始化的变量。

  2)全局静态存储区内的常量分为常变量和字符串常量,一经初始化,不可修改。静态存储内的常变量是全局变量,与局部常变量不同,区别在于局部常变量存放于栈,实际可间接通过指针或者引用进行修改,而全局常变量存放于静态常量区则不可以间接修改。

  3)字符串常量存储在全局/静态存储区的常量区。

(2)static 和extern 区别

  1)static 特点:在运行前分配内存,程序运行结束 生命周期结束。

  2)extern 可以提高变量作用域,告诉编译器用extern 声明的变量不要报错,是外部连接属性,可以在其他的文件中找到。

2.4 常量

(1)const修饰的变量

  1)全局变量:直接修改 失败 ,间接修改 语法通过,运行失败,受到常量区保护
  2)局部变量:直接修改 失败 , 间接修改 成功,放在栈上

(2)字符串常量是否可修改?字符串常量优化

  ANSI C中规定:修改字符串常量,结果是未定义的。

  ANSI C并没有规定编译器的实现者对字符串的处理,例如:

  1).有些编译器可修改字符串常量,有些编译器则不可修改字符串常量。
  2).有些编译器把多个相同的字符串常量看成一个(这种优化可能出现在字符串常量中,节省空间),有些则不进行此优化。如果进行优化,则可能导致修改一个字符串常量导致另外的字符串常量也发生变化,结果不可知。

  所以尽量不要去修改字符串常量!

2.5 总结

  在理解C/C++内存分区时,常会碰到如下术语:数据区,堆,栈,静态区,常量区,全局区,字符串常量区,文字常量区,代码区等等,初学者被搞得云里雾里。在这里,尝试捋清楚以上分区的关系。

  (1)数据区包括:堆,栈,全局/静态存储区。

  (2)全局/静态存储区包括:常量区,全局区、静态区。

  (3)常量区包括:字符串常量区、常变量区。

  (4)代码区:存放程序编译后的二进制代码,不可寻址区。

  可以说,C/C++内存分区其实只有两个,即代码区和数据区。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值