5.1.0 内存四区模型

image.png

操作系统把物理硬盘代码 load 到内存。
操作系统把C代码分成四个区。
操作系统找到Main函数入口执行。

内存四区

一个由C/C++编译的程序占用的内存分成以下几个部分:
栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。只有汇编语言可以操控栈区。

堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能操作系统回收。
注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。

数据区:主要包括静态全局区和常量区,如果要站在汇编的角度细分的话,还可以分为很多小区。

全局区(静态区)(static):全局变量和静态变量的存储放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。常量区也存在于全局区(他们统称)。

常量区:常量字符串就是放在这里的,程序结束后由系统释放。

代码区:存放函数体的二进制代码。由操作系统管理,函数指针最强大的地方就是可以操作代码区的数据。

全局区详解

```c
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include

char pMem1(); // 函数指针
char
pMem2();
char* pMem3();

void main(void) {
char p1 = NULL; //声明两个指针变量
char
p2 = NULL;
char* p3 = NULL;
p1 = pMem1(); // 接收函数指针传出的指针变量值
p2 = pMem2();
p3 = pMem3();

printf(“p1 = %s p1 = %p\n”, p1, p1);
printf(“p2 = %s p2 = %p\n”, p2, p2);
printf(“p3 = %s p3 = %p\n”, p3, p3);
system(“pause”);
}

char pMem1() {
/**
两个不同的函数定义了一个相同的字符串,地址一样吗?
是一样的,实际上它存储的是字符串的地址,栈区运行完毕就自动销毁了,实际上这两个函数是一样的只是函数名不一样
同样的东西,为什么要创建两次呢?设计者也不是傻子。
例如有一个货物放在哪里不动了,这个时候他们两个人分别去记录这个货物的位置。
栈区上的变量执行完毕变量就销毁了
/
char
p1 = “Hello World”; // 存储在全局区
return p1; // 返回一个指针变量
}

// 函数调用完毕,内存销毁
char pMem2() {
/**
Hello Arvin 这个字符串怎么传入到这个指针的呢?指针变量在32Bit下是4字节,64Bit下是8字节。
指针变量存储的只存储的是它的地址,变量实际在全局区。
/
char* p2 = “Hello World”;
return p2;
}

char pMem3() {
// 如果货物不一样,那么他们的地址就不一样。
char
p3 = “Hello Arvin”;
return p3;
}
```
总结:
全局区相同字符串常量合并。
注意三点:
栈区调用完毕就销毁了。
指针指向谁就把谁的地址赋值给指针。
%s 的意思就是打印地址所指向的内容。

栈区详解

```c
#include
#include

char* p_str();

void main() {

char* p = NULL;
p = p_str();

printf(“P = %s\n”, p); // 数组的首地址

/*
那么p打印这个地址所队友的内存空间内容是不是 Hello 答案是否因为已经被栈销毁了
说明:p_str在栈区创建一块100字节的空间,”Hello” 还是存放在全局区。
p_str是一个数组,复制了全局变量的内容,拷贝了一份在栈上和地址无关。
当p_str函数执行完毕后,将p_str的地址返回给Main的p,然后p_str会自动销毁。所以无法输出获取 Hello。
/
printf(“\n”);
system(“pause”);
}

char* p_str() {
char str[100] = “Hello”; // 4字节
printf(“str = %s\n”,str);
return str;
}
<br />内存栈区调试<br />c
#include
#include

/*
两个函数声明同一个变量,这两个变量有关系吗? :没有
*/

char* p_str2();

void main(void) {

char* p = NULL;
p = p_str2();

printf(“p = %s\n”, p);
printf(“p = %p\n”, p);

// 在这里再打一个断点运行查看内存
printf(“\n”);
system(“pause”);
}

char* p_str2() {
char str[100] = “Hello”;
printf(“str = %s\n”, str);
printf(“str = %p\n”, str);
// 查看栈销毁,在这里打一个断点
return str;
}
```
所以在栈上开辟一个数组是很危险的,所以我们一般在堆上开辟数组。堆是由我们控制的所以在使用完成后要手动释放。


堆区详解

strcpy 函数的作用

原型声明:charstrcpy(char dest,const char* src);
功能:把从src地址开始且含有NULL结束符的字符串复制到以dest开始的地址空间。
说明:stc和dest所指内存区域不可以重叠,且dest必须有足够的空间来容纳src的字符串。
返回指向dest的指针。

堆区不会自动释放需要手动释放。
Free 的意思不是清空内存,而是解除这个指针的绑定关系(解除和这块内存的绑定关系),原先只有指针自己能调用,(取消绑定后)现在谁都可以用。

```c
#include
#include

/*
运行流程说明:
首先在栈区创建了指针p,现在指针p为NULL;
然后执行phead函数,它入栈,创建了一个指针tmp(存储堆区的地址)。在堆区创建了一个100字节大小的空间。
strcpy拷贝Hello到堆区。然后将tmp(堆区地址)返回给main函数中创建的p,栈区销毁phead指针函数。
p接收到堆区的地址,进行判断,如果不为NULL,就打印出它指针指向的地址的内容。
然后解除指针变量p和堆内存的绑定关系。将指针变量p置空。
/

char* phead();

void main(void) {
char* p = NULL;
p = phead();
printf(“p = %p\n”, p);
printf(“p = %s\n”, p);
if (p != NULL) { // 如果p接收到的不为NULL,就打印出它指针指向地址的内容
printf(“%s\n”, p);
// 断点
free(p); // 解除指针变量p和堆内存的绑定关系。
p = NULL; // 将指针变量p置空
}
// 断点
system(“pause”);
}

char phead() {
char
tmp = (char*)malloc(100); // 在堆上开辟了100字节的空间
if (tmp == NULL) { // 如果tmp为空那么就返回一个空的值。
return NULL;
}
strcpy(tmp, “Hello”); // strcpy拷贝函数,我们将Hello拷贝到tmp指向的空间(我们在堆里开辟的空间)
printf(“tmp = %p\n”, tmp);
printf(“tmp = %s\n”, tmp);
// 断点
return tmp;
}
```

5.1.2 指针运算(重点)

指针的加减法,它是处理多级指针包括数组的时候非常重要。它决定了你在指针这里能走多远。严格来说,指针很少有人走到头。至少要掌握二级指针到三级指针(否则你将很难明白C++语言中的引用)。
void *p是一个万能指针。

指针的数据类型决定了指针的解析方式,同时也决定了指针的步长。
指针+1等于指针加上所指向类型的大小。
编译器对指针+1的操作:p+sizeof(p)1;

```c
#include
#include

//51
void main() {
int num = 10;
int* p = #
printf(“%p %p\n”, p, p + 1); // 指针是4|8字节,指针+1 = +4字节
printf(“%d %d\n”, p, p + 1); // 证实指针+1 = +4 注意:指针+1 等于指针所指向类型的大小

char str = ‘a’;
char p_str = &str;
// 指针+1在编译器实际是:p+sizeof(
p)*1
printf(“%p %p\n”, p_str, p_str + 1); // 这里指针+1 = +1字节 因为char是一个字节
printf(“%d %d\n”, p_str, p_str + 1);

/*
既然指针都是4|8字节,那么为什么要有类型呢?注意:void p 是万能指针。
其实指针变量的类型决定了解析的方式,声明什么类型的变量就要用什么类型的指针变量。
如果用错了会出现错误,类型不单单决定了步长,还决定了解析方式。
步长:+1加多少就是步长。指针的类型决定了指针的步长(往前走几个字节)
类型相当于模具,变量相当于做出的模型,如果模具不一样做做出来的模具肯定会出现问题。
/

printf(“\n”);
system(“pause”);
}
```

指针与数组练习

指针也是一种数据类型,指针的数据类型是指它所指向的内存空间的数据类型。
指针的空间和指针所指向的内存空间是不同概念。
下面实例比较复杂建议多看内存和多思考。
```c
#include
#include

void main() {
char str[] = “Hello”;

// 字符串是以\0结尾的所以是6个字节 或者智能去判断
/for (int i = 0; i < sizeof(str); i++) {
printf(“%c”, str[i]);
}
printf(“\n\n”);
/

/ 我们采用指针的方式打印 / // 这里断点调试,注释上面for内容
for (char p_str = str; p_str < str + 3; p_str++) {
printf(“%c”,
p_str);
}
/*
for(指针绑定地址,指针小于数组字符串,p_str++(步长))
数组+1是加了本身一个元素的大小
p_str+1是加了指向类型的大小
指针变量和数组的起始点是一样的,步长也是一样的
所以值也一样
*/

printf(“\n”);
system(“pause”);
}
```

指针的间接赋值

如果想通过形参改变实参的内存内容(值)必须地址传递。
指针间接赋值的三个条件
两个变量、建立关系、通过*操作内存
```c
#include
#include

/*
问题:怎么通过形参改变一个函数内实参的值?从外部更改函数内部的值
函数的参数有副本机制(重新开辟一个内存),除了数组。
采用指针的方式从外部修改内部的值
*/

int num(int a);
/ 我们采用指针的方式从外部修改内部的值 /
void pnum(int *a);

void main() {
int a = 20;
printf(“main a = %p\n”, &a);
int b = num(a); // 我们传入num的a的值为20 那么输出是10 还是 20:答案是:10 (创建副本)

printf(“b = %d\n”, b);

int c = 100;
pnum(&c);
printf(“c = %d\n”, c);

printf(“\n”);
system(“pause”);
}

int num(int a) {
a = 10;
printf(“num a = %p\n”, &a);
return a;
}

void pnum(int a) {
a = 10;
}
```

指针数组

指针数组实际上就是指针类型的数组。
当然还有一种叫数组指针,实际上就是指向数组的指针。
注意:指针数组是指针类型的数组,数组指针是指向数组的指针
```c
#include
#include

void main(void) {
int a = 1;
int b = 2;
int c = 3;

int d[4] = { 1,2,3,4 };

for (int i = 0; i < 4; i++) {
printf(“%d\n”, d[i]);
}

char *pa[] = { “a”,”b”,”c” }; // 指针数组 “a”在指针常量区,”a”存储的实际上是地址
// 当然还有一种叫数组指针,实际上就是指向数组的指针
// 注意:指针数组是指针类型的数组,数组指针是指向数组的指针
// 断点
for (int i = 0; i < 3; i++) { // 这里+1是加了一个地址的长度
printf(“%s\n”, pa[i]); // 这里i也是一个地址的长度
}
printf(“\n”);
system(“pause”);
}
```

5.1.3 结构体基本操作

结构体类型定义
结构体变量定义
结构体变量初始化
Typedef 改变类型名
注意:结构体也是一种数据类型,是我们自定义的类型。
```c
#include
#include

/*
结构体就是自定义的数据类型语法:struct 类型名好比我们的int float。
*/

/ 结构体类型定义第一种方式 /
struct Test {
char face[10];
int EyesSize;

};

/ 结构体变量定义第一种方式 (声明赋值二合一) /
struct Test test = { “Hello”,20 }; // 和定义 int a;一样,但是结构体是struct Test == int

/ 结构体类型定义第二种方式 /
struct Test02 {
char face[10];
int EyesSize;
}test02 = {“World”,15}; // 直接在下面定义变量

/ 结构体类型定义第三种 /
struct {
char face[10];
int EyesSize;
}test03,test04; // 匿名结构体,这个结构体只能使用在下面定义好的变量

/ 结构体类型定义第四种 /
typedef struct Test05 {
char face[10];
int EyesSize;
}Test05; // tyoedef 是别名,它将 struct Test05 替换成 Test05 这样我们就可以少写一个

Test05 test05 = { “Love”,520 };

void main() {

printf(“\n”);
system(“pause”);
}
```

结构体初始化

注意:
Test结构体它声明的时候是不开辟内存的,你必须初始化对象才开辟内存。
箭头运算符 :用于指针来调用变量的一种方式。
test.face = { “Hello” }; 这样的化会出错,它不是指针(不能往指针的地址里拷贝字符串)。所以我们要用到Strcpy函数。
```c
#include
#include

typedef struct Test {
char face[10];
int EyesSize;
char* str;
}Test;

void main(void) {
Test test;
// test.face = { “Hello” }; 这样的化会出错,它不是指针(不能往指针的地址里拷贝字符串)。所以我们要用到Strcpy函数
strcpy(test.face,”Hello”);
test.str = “World”;
test.EyesSize = 15;
for (char p = test.str; p < test.str + 6; p++) {
printf(“%c”,
p);
}

Test test02 = { “Hello”,15,”Arvin”};
printf(“%s %d\n”, test.face, test.EyesSize);
printf(“%s %d %s\n”, test02.face, test02.EyesSize, test02.str);

Test test01;
/ 箭头运算符 :用于指针来调用变量的一种方式 /
Test* p_test = NULL; // 这个指针指向我们的结构体
p_test = &test01; // 初始化一个结构体的对象
strcpy(p_test->face, “Hello”);
p_test->EyesSize = 100; // 指针通过箭头运算符调用结构体里的内容
p_test->str = “World”;
printf(“%s %d %s\n”, p_test->face, p_test->EyesSize, p_test->str);

/*
报错:Test结构体它声明的时候是不开辟内存的,你必须初始化对象才开辟内存。
*/

printf(“\n”);
system(“pause”);
}
```
如果你在p_test->EyesSize = 100;非要用的方式也不是不可以:
(
p_test).EyesSize = 100;

结构体变量之间赋值

```c
#include
#include

/*
我们定义结构体的时候不开辟内存。
在声明变量的时候才会开辟内存 ModelFace modelFace = { “Hello”,50 };
注意:赋值完成后它们就没有关系了
*/

typedef struct ModelFace {
char face[10];
int EyesSize;
}ModelFace;

void main56(void) {
ModelFace modelFace = { “Hello”,50 };

/ 结构体变量之间可以相互赋值 /
ModelFace modeF = modelFace;

printf(“%s %d\n”, modeF.face, modeF.EyesSize);

printf(“\n”);
system(“pause”);
}
```

结构体静态数组-栈区

```c
#include
#include

/*
结构体定义数组-在栈区开辟
注意类比
/
typedef struct ModelFace {
char face[10];
int EyesSize;
}ModelFace;

void main() {
int a[3] = { 1,2,3 };

/ 结构体数组有两种定义方式 /
ModelFace modelFace[3] = { “Gun”,100,”Liyun”,50,”Note”,10 };
ModelFace modelF[3] = { {“Fun”,100},{“Miun”,50},{“Mote”,10} };

for (int i = 0; i < 3; i++) {
printf(“ModeFace:%d %s %d\n”, i, modelFace[i].face, modelFace[i].EyesSize);
printf(“ModeF :%d %s %d\n”, i, modelF[i].face, modelF[i].EyesSize);
}

printf(“\n”);
system(“pause”);
}
```

结构体动态数组-堆区

```c
#include
#include

/*
结构体动态数组-堆区
注意类比
/

struct ModelFace {
char face[10];
int EyesSize;
};

void main() {

int a[3] = { 0 };
int pa = (int)malloc(3 * sizeof(int)); // 开辟堆区内存
free(pa);
pa = NULL;

/ 结构体动态数组 /
struct ModelFace modelFace[3] = { 0 };
struct ModelFace p_str = (struct ModelFace)malloc(3 * sizeof(struct ModelFace));
if (p_str == NULL) {
return NULL;
}
for (int i = 0; i < 3; i++) {
strcpy(p_str[i].face, “Arvin”);
p_str[i].EyesSize = 10 + i;
}
for (int i = 0; i < 3; i++) {
printf(“modelFace %d :%s %d\n”, i, p_str[i].face, p_str[i].EyesSize);
}
if (p_str != NULL) {
free(p_str);
p_str = NULL;
}

printf(“\n”);
system(“pause”);
}
```

结构体嵌套数组

```c
#define _CRT_SECURE_NO_WARNINGS
#include
#include

/*
结构体嵌套指针
如果结构体内部有个指针能否直接给指针拷贝内容:答案是可以的
/

typedef struct ModelFace {
char* face;
int EyesSize;
}ModelFace;

void main59(void) {
printf(“请输入您的姓名:”);
char Mname[10] = { 0 };
int age = 0;
scanf(“%s”,Mname);
char
name = NULL;
name = (char)malloc(10);
strcpy(name, Mname);
printf(“name:%s\n”, name);
if (name != NULL) {
free(name);
name = NULL;
}
/
结构体 /
ModelFace modelFace;
modelFace.face = (char
)malloc(10);
strcpy(modelFace.face, Mname);
modelFace.EyesSize = 18;
printf(“ModeFace:name:%s age:%d”, modelFace.face, modelFace.EyesSize);
if (modelFace.face != NULL) {
free(modelFace.face);
modelFace.face = NULL;
}
printf(“\n”);
system(“pause”);
}
```

结构体赋值细节

在程序流程时如果你不知道大小可以采用strlen函数
c<br />for (int i = 0; i < strlen(Name); i++) { // 如果你不知道大小可以采用strlen函数<br />pMyStruct->hello[i] = Name[i];<br />}<br />
结构体赋值第一部分
```c
#define _CRT_SECURE_NO_WARNINGS
#include
#include

typedef struct MyStruct {
char hello[10];
int a; //第二次加个一个int类型测试
}MyStruct;

void main() {
int a;
printf(“a:%d\n”, sizeof(a)); // 4字节,int是系统做好的类型

MyStruct myStruct;
printf(“myStruct:%d\n”, sizeof(myStruct)); // 10字节,我们自定义变量 加上int为16字节(因为结构体内存对齐)
strcpy(myStruct.hello, “A”);
printf(“myStruct.hello:%d\n”, sizeof(myStruct.hello));
printf(“myStruct.a:%d\n”, sizeof(myStruct.a)); // 4字节,为什么会消失2字节?

/*
结构体在开辟内存的时候,可能因为变量不同。系统会不确定分配多少内存。
所以会尽可能的分配大一点的内存,按照结构体里最大类型的2的倍数进行分配。
/

MyStruct pMyStruct = &myStruct;
printf(“
pMyStruct:%d\n”, sizeof(*pMyStruct)); // 16字节 指向我们的内存:myStruct。
printf(“pMystruct:%d\n”, sizeof(pMyStruct)); // 4|8字节,指针的内存是固定的
strcpy(pMyStruct->hello, “Hello”);
pMyStruct->a = 50;
printf(“pMystruct:%s %d\n”, pMyStruct->hello, pMyStruct->a);

char name[10];
printf(“输入你要的pMyStruct的值:”);
scanf(“%s”, name);
// 需要注意的是我们不能通过 char name{1,2,3};来修改,它存在于栈区,我们只能修改”字符串”
strcpy(pMyStruct->hello, name);
// 当然你可以char* name = Hello,然后传入进去
printf(“pMyStruct:%s %d\n”, pMyStruct->hello, pMyStruct->a);

// 如果非要传入{}可以有以下方法
char Name[10] = { ‘H’,’e’,’l’,’l’,’o’ };
// 栈区数组赋值
for (int i = 0; i < strlen(Name); i++) { // 如果你不知道大小可以采用strlen函数
pMyStruct->hello[i] = Name[i];
}
printf(“pMyStruct:%s %d\n”, pMyStruct->hello, pMyStruct->a);

// 如果我们把采用strcpy还可以采用
char* pName = “ZhangSan”;
if (pMyStruct == NULL) {
return NULL;
}
for (int i = 0; i < 9; i++) {
pMyStruct->hello[i] = pName[i];
}
printf(“pMyStruct:%s %d\n”, pMyStruct->hello, pMyStruct->a);
pName = “LiSi”;
for (pMyStruct = pName; pMyStruct < pName + 5; pMyStruct++) {
printf(“pMyStruct: %s\n”, pMyStruct->hello);
}

printf(“\n”);
system(“pause”);
}
```

C语言赋值细节二

```
#include
#include

MyStruct* Test();

typedef struct MyStruct {
char* Hello;
}MyStruct;

void main(void) {

MyStruct myStruct;
MyStruct* pMyStruct = &myStruct;

/*
字符串是不动的常量,在内存中是以下的表现:
const char H = “Arvin”
它在全局区是不可更改的,我们通过指针地址将它赋值给pMySruct,说白了就是地址的转移。
/
char* H = “Arvin”;
pMyStruct->Hello = H;

/*
I是在栈区的数组。
pMyStruct是在Main函数中的所以下面不会出问题。
如果在外部函数中这样,需要讲值传递置堆区。
栈区是先进后出的模型运行的,Main函数先进,所以当Test进入,出去时候将值传给了Main还是可以正常运行的。
此处涉及主调函数、被调函数、函数调用模型有关系。
*/
char I[10] = { ‘1’,’2’,’3’,’4’,’5’};
pMyStruct->Hello = I;

printf(“%s\n”, pMyStruct->Hello);

printf(“\n”);
system(“pause”);
}
```

C语言实现界面

```c

include
#include

void main(void) {
puts(“\n”);
puts(“\n”);
puts(“\t\t —————————————-“);
puts(“\t\t| Leve1:1 |”);
puts(“\t\t| Leve1:2 |”);
puts(“\t\t| Leve1:3 |”);
puts(“\t\t| Leve1:4 |”);
puts(“\t\t| Leve1:5 |”);
puts(“\t\t —————————————-“);

printf(“\n”);
system(“pause”);
}
```
Dos窗口的大小为:宽80字符高25字符
一个\t的是8个字符。

puts和printf的区别和putchar的区别

printf、pytchar和puts函数均为输出函数。printf函数可以输出各种不同类型的数据,putchar函数只能输出字符数据,而puts函数可输出字符串数据。
puts(s)的作用与语句printf(“%s”,s)的作用基本相同,puts函数只能输出字符串,不能输出1数值或进行格式变换,puts函数在输出字符它自带换行。

结构体保存数据

C语言没有类的概念,我们做一些属性的时候,只能是往结构体里保存。
```c
#define _CRT_SECURE_NO_WARNINGS
#include
#include

typedef struct Info {
char Name[10];
char Gender[10];
double Height;
unsigned short Age;
}Info;

void main(void) {
Info info = { 0,0,0,0 }; // 初始化,如果字符类型没有初始化可能就直接报错
// 断点调试

/ 值指向 info 的数组指针 /
Info* pInfo = &info;

/ 写入数据 /
printf(“请输入您的姓名:”);
scanf(“%s”, pInfo->Name);
printf(“您的姓名是:%s\n”, pInfo->Name);

printf(“请输入你的性别:”);
scanf(“%s”, pInfo->Gender);
printf(“您的性别为:%s\n”, pInfo->Gender);

if ((!strcmp(pInfo->Gender,”男”)) || (!strcmp(pInfo->Gender,”男生”))) { // 这个函数比较特殊,需要反转一下
printf(“请输入您的身高:”);
scanf(“%lf”, &pInfo->Height); // 注意是 LF
printf(“您的身高为:%g\n”, pInfo->Height);

printf(“请输入您的年龄:”);
scanf(“%u”, &pInfo->Age);
printf(“你的年龄为:%u\n”, pInfo->Age);
}

printf(“\n”);
system(“pause”);
}
```

strcmp 的作用

比较字符串,0代表两个相同,非0代表两个字符串不相同。