C语言进阶(一)—— 内存分区:变量和内存分布

本文介绍了C语言中的数据类型,包括数据类型的概念、typedef别名、void的使用,以及sizeof操作符。此外,文章还详细讨论了变量的本质和内存分区模型,包括栈和堆的特性,以及函数调用过程中的宏函数和调用惯例。
摘要由CSDN通过智能技术生成

1. 数据类型

1.1 数据类型概念

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

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

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

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

1.2 数据类型别名

typedef使用

//#define _CRT_SECURE_NO_WARNINGS  //VS不建议使用传统库函数,如果不用这个宏,会出现一个错,编号:C4996
#include<stdio.h>   // std 标准 i input  输入   o  output 输出 
#include<string.h>  // strcpy   strcmp  strcat  strstr
#include<stdlib.h>  // malloc  free


//1、typedef使用  简化结构体关键字 struct
//struct Person
//{
//    char name[64];
//    int age;
//};
//typedef struct Person  myPerson;

//主要用途  给类型起别名
//语法  typedef  原名  别名
typedef struct Person
{
    char name[64];
    int age;
}myPerson;

void test01()
{
    struct Person p1 = { "张三", 19 };

    myPerson p2 = { "李四", 20 };
}


// 2、区分数据类型
void test02()
{
    //char * p1, p2;  //p1是char *  而 p2 是char

    typedef char * PCHAR;
    PCHAR p1, p2;

    char *p3, *p4; // p3 和 p4都是char *
}


//3、提高代码移植性
typedef int MYINT; //typedef long long MYINT; 只需要替换 long long 就可以了
void test03()
{
    MYINT a = 10;

    MYINT a2 = 10;
}


//程序入口
int main1(){
    test01();

    system("pause"); // 按任意键暂停  阻塞功能

    return EXIT_SUCCESS; //返回 正常退出值  0
}

1.3 void数据类型

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

void定义变量是没有任何意义的,当你定义void a,编译器会报错。

void真正用在以下两个方面:

  • 函数返回的限定;

  • 函数参数的限定;

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>

//1、无类型是不可以创建变量的
void test01()
{
    //void a = 10; //编译器直接报错,因为不知道给a分配多少内存空间
}


//2、可以限定函数返回值
void func()
{
    //return 10;
}

void test02()
{
    //func();
    //printf("%d\n", func());
}


//3、限定函数参数列表
int func2(void)
{
    return 10;
}
void test03()
{
    //printf("%d\n", func2(10));
}


//4、void *  万能指针
void test04()
{
    void * p = NULL;

    int  * pInt = NULL;
    char * pChar = NULL;

    //pChar = (char *)pInt;
    pChar = p; //万能指针  可以不需要强制类型转换就可以给等号左边赋值

    printf("size of void *   = %d\n", sizeof(p));

}

int main(){
    //test02();
    //test03();
    test04();
    system("pause");
    return EXIT_SUCCESS;
}

1.4 sizeof操作符

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

基本语法:
sizeof(变量);
sizeof 变量;
sizeof(类型);

sizeof 注意点

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

  • sizeof返回的数据结果类型是unsigned int

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

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>

//1、sizeof本质, 是不是一个函数??? 不是函数,只是一个操作符,类似+-*/
void test01()
{
    //对于数据类型 ,sizeof必须用()去使用,但是对于变量,可以不加()
    printf("size of int = %d\n", sizeof(int));

    double d = 3.14;

    printf("size of d = %d\n", sizeof d );

}

//2、sizeof的返回值类型是什么 ?  unsigned int 无符号整型
void test02()
{
    //unsigned int a = 10;
    //if (a - 20 > 0) //当unsigned int和int类型数据做运算,编译器会将数据类型都转为unsigned int
    //{
    //    printf("大于 0 \n");
    //}
    //else
    //{
    //    printf("小于 0 \n");
    //}


    if ( sizeof(int) - 5 > 0 )
    {
        printf("大于 0 %u \n", sizeof(int)-5);
    }
    else
    {
        printf("小于 0 \n");
    }

}

//3、sizeof可以统计数组长度
//当数组名作为函数参数时候,会退化为指针,指向数组中第一个元素
void calculateArray( int arr[] )
{
    printf("arr的数组长度: %d\n", sizeof(arr));
}

void test03()
{
    int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8 };

    //printf("arr的数组长度: %d\n", sizeof(arr));

    calculateArray(arr);
}

int main(){
    //test01();
    //test02();
    test03();

    system("pause");
    return EXIT_SUCCESS;
}

1.5 数据类型总结

  • 数据类型本质是固定内存大小的别名,是个模具,C语言规定:通过数据类型定义变量

  • 数据类型大小计算(sizeof);

  • 可以给已存在的数据类型起别名typedef

  • 数据类型的封装(void 万能类型);

2. 变量

1.1 变量的概念

既能读又能写的内存对象,称为变量;

若一旦初始化后不能修改的对象则称为常量。

变量定义形式: 类型 标识符, 标识符, … , 标识符

1.2 变量名的本质

  • 变量名的本质:一段连续内存空间的别名

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

  • 通过变量名访问内存空间;

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

修改变量的两种方式:直接、间接

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>

void test01()
{
    int a = 10;
    //直接修改
    a = 20;
    printf("a = %d\n", a);
    //间接修改
    int * p = &a;
    *p = 100;

    printf("a = %d\n", a);

}

//对于自定义数据类型
struct Person
{
    char a; // 0 ~ 3
    int b;  // 4 ~ 7
    char c; // 8 ~ 11
    int d;  // 12 ~ 15
};
void test02()
{
    struct Person p1 = { 'a', 10, 'b', 20 };

    //直接修改  d 属性
    p1.d = 1000;

    //间接修改  d 属性
    struct Person * p = &p1;
//    p->d = 2000;

    //printf("%d\n", p);
    //printf("%d\n", p+1);

    char * pPerson = p;

    printf("d = %d\n", *(int*)(pPerson + 12));
    printf("d = %d\n",  *(int*)((int*)pPerson +3) );
}

int main(){

    //test01();
    test02();
    system("pause");
    return EXIT_SUCCESS;
}

3. 程序的内存分区模型

3.1 内存分区

3.1.1 运行之前

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

预处理:宏定义展开、头文件展开、条件编译,这里并不会检查语法
编译:检查语法,将预处理后文件编译生成汇编文件
汇编:将汇编文件生成目标文件(二进制文件)
链接:将目标文件链接为可执行程序

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

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

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

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

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

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

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

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

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

3.1.1 运行之后

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

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

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

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

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

类型

作用域

生命周期

存储位置

auto变量

一对{}内

当前函数

栈区

static局部变量

一对{}内

整个程序运行期

初始化在data段,未初始化在BSS段

extern变量

整个程序

整个程序运行期

初始化在data段,未初始化在BSS段

static全局变量

当前文件

整个程序运行期

初始化在data段,未初始化在BSS段

extern函数

整个程序

整个程序运行期

代码区

static函数

当前文件

整个程序运行期

代码区

register变量

一对{}内

当前函数

运行时存储在CPU寄存器

字符串常量

当前文件

整个程序运行期

data段

注意:建立正确程序运行内存布局图是学好C的关键!!

3.2 分区模型

3.2.1 栈区

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

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>

//栈 注意事项 ,不要返回局部变量的地址
int * func()
{
    int a = 10;
    return &a;
}

void test01()
{
    int * p = func();

    //结果已经不重要了,因为a的内存已经被释放了,我们没有权限去操作这块内存
    printf("a = %d\n", *p);
    printf("a = %d\n", *p);
}


char * getString()
{
    char str[] = "hello world";
    return str;
}

void test02()
{
    char * p = NULL;
   //不要返回局部变量的地址,因为局部变量在函数执行之后就释放了,我们没有权限取操作释放后的内存
    p = getString();
    printf("%s\n", p); //乱码,每次不一样
}

int main(){

    //test01();
    test02();
    system("pause");
    return EXIT_SUCCESS;
}

3.2.2 堆区

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

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>

int * getSpace()
{
    int * p = malloc(sizeof(int)* 5);

    if (p == NULL)
    {
        return NULL;
    }

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

}

void test01()
{
    int * p = getSpace();

    for (int i = 0; i < 5;i++)
    {
        printf("%d\n", p[i]);
    }

    //手动在堆区创建的数据,记得手动释放
    free(p);
    p = NULL;

}


//注意事项
//如果主调函数中没有给指针分配内存,被调函数用同级指针是修饰不到主调函数中的指针的
void allocateSpace( char * pp )
{
    char * temp =  malloc(100);
    if (temp == NULL)
    {
        return;
    }
    memset(temp, 0, 100);
    strcpy(temp, "hello world");
    pp = temp;

}

void test02()
{
    char * p = NULL;
    allocateSpace(p);

    printf("%s\n", p);
}


void allocateSpace2(char ** pp)
{
    char * temp = malloc(100);
    memset(temp, 0, 100);
    strcpy(temp, "hello world");
    *pp = temp;
}

void test03()
{
    char * p = NULL;

    allocateSpace2(&p);

    printf("%s\n", p);

    free(p);
    p = NULL;

}



int main(){
    //test01();
    //test02();
    test03();
    system("pause");
    return EXIT_SUCCESS;
}
#include <stdlib.h>
void *calloc(size_t nmemb, size_t size);
功能:
在内存动态存储区中分配nmemb块长度为size字节的连续区域。calloc自动将分配的内存 置0。
参数:
nmemb:所需内存单元数量
size:每个内存单元的大小(单位:字节)
返回值:
成功:分配空间的起始地址
失败:NULL
#include <stdlib.h>
void *realloc(void *ptr, size_t size);
功能:
重新分配用malloc或者calloc函数在堆中分配内存空间的大小。
realloc不会自动清理增加的内存,需要手动清理,如果指定的地址后面有连续的空间,那么就会在已有地址基础上增加内存,如果指定的地址后面没有空间,那么realloc会重新分配新的连续内存,把旧内存的值拷贝到新内存,同时释放旧内存。
参数:
ptr:为之前用malloc或者calloc分配的内存地址,如果此参数等于NULL,那么和realloc与malloc功能一致
size:为重新分配内存的大小, 单位:字节
返回值:
成功:新分配的堆内存地址
失败:NULL

3.2.3 全局/静态区

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

注意:

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

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

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

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>

int v1 = 10;//全局/静态区
const int v2 = 20; //常量,一旦初始化,不可修改
static int v3 = 20; //全局/静态区
char *p1; //全局/静态区,编译器默认初始化为NULL

//那么全局static int 和 全局int变量有什么区别?

void test(){
    static int v4 = 20; //全局/静态区
}

//1、静态变量
static int a = 10; //特点:只初始化一次,在编译阶段就分配内存,属于内部链接属性,只能在当前文件中使用

void test01()
{
    static int b = 20; //局部静态变量,作用域只能在当前test01中

    //a 和 b的生命周期是一样的
}

//2、全局变量

extern int g_a = 100; //在C语言下 全局变量前都隐藏加了关键字  extern,属于外部链接属性

void test02()
{
    extern int g_b;//告诉编译器 g_b是外部链接属性变量,下面在使用这个变量时候不要报错

    printf("g_b = %d\n", g_b);

}

int main(){
    //test();
    //test01();
    test02();

    system("pause");
    return EXIT_SUCCESS;
}
static 和 extern 区别
static 静态变量:编译阶段分配内存,只能在当前文件内使用,只初始化一次
extern 全局变量:C语言下默认的全局变量前都隐藏的加了该关键字

const修饰的变量
全局变量:直接修改失败;间接修改失败 原因放在常量区,受到保护
局部变量:直接修改失败;间接修改成功 原因放在栈上;伪常量不可以初始化数组

字符串常量是否可修改?字符串常量优化:
不同的编译器可能有不同的处理方式
ANSI没有指定出标准
有些编译器可以修改字符串常量,有些不可以
有些编译器将相同的字符串常量看成同一个

字符串常量地址是否相同?
tc2.0,同文件字符串常量地址不同。
Vs2013,字符串常量地址 同文件和不同文件都相同
Devc++、QT同文件相同,不同文件不同。

3.2.4 总结

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

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

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

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

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

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

3.3 函数调用流程

3.3.1 宏函数

#define MYADD( x, y) ((x) + (y)) //注意:保证运算完整性
在一定程度上会比普通函数效率高,普通函数会有入栈和出栈的时间开销
将比较频繁短小的函数 写为宏函数,直接跑源码
优点:以空间换时间

3.3.2 调用惯例

主调函数和被调函数都必须有一致的约定,才可以正确的调用函数,这个约定我们称为调用惯例;
调用惯例包含的内容:出栈方、参数的传入顺序、函数名称的修饰;
c和c++下默认的调用惯例为 cdecl
注意: _cdecl不是标准的关键字,在不同的编译器里可能有不同的写法,例如gcc里就不存在_cdecl这样的关键字,而是使用__attribute__((cdecl)).

调用惯例

出栈方

参数传递

名字修饰

cdecl

函数调用方

从右至左参数入栈

下划线+函数名

stdcall

函数本身

从右至左参数入栈

下划线+函数名+@+参数字节数

fastcall

函数本身

前两个参数由寄存器传递,其余参数通过堆栈传递。

@+函数名+@+参数的字节数

pascal

函数本身

从左至右参数入栈

较为复杂,参见相关文档

3.4 栈的生长方向和内存存放方向

栈底 --- 高地址

栈顶 --- 低地址

高位字节数据 --- 高地址

低位字节数据 --- 低地址

小端对齐

//1. 栈的生长方向
void test01(){

    int a = 10;
    int b = 20;
    int c = 30;
    int d = 40;

    printf("a = %d\n", &a);
    printf("b = %d\n", &b);
    printf("c = %d\n", &c);
    printf("d = %d\n", &d);

    //a的地址大于b的地址,故而生长方向向下
}

//2. 内存生长方向(小端模式)
void test02(){
    
    //高位字节 -> 地位字节
    int num = 0xaabbccdd;
    unsigned char* p = &num;

    //从首地址开始的第一个字节
    printf("%x\n",*p);
    printf("%x\n", *(p + 1));
    printf("%x\n", *(p + 2));
    printf("%x\n", *(p + 3));
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

心上学事上练

感谢您的支持,让我们一起成长

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值