5.1.0 内存四区模型

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

内存四区

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

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

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

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

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

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

全局区详解

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

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 的意思就是打印地址所指向的内容。

栈区详解

#include <stdlib.h>
#include <stdio.h>

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;
}

内存栈区调试

#include <stdio.h>
#include <stdlib.h>

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

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 的意思不是清空内存,而是解除这个指针的绑定关系(解除和这块内存的绑定关系),原先只有指针自己能调用,(取消绑定后)现在谁都可以用。

#include <stdio.h>
#include <stdlib.h>

/**
* 运行流程说明:
* 首先在栈区创建了指针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;

#include <stdio.h>
#include <stdlib.h>

//51
void main() {
int num = 10;
int* p = &num;
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");
}

指针与数组练习

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

#include <stdio.h>
#include <stdlib.h>

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");
}

指针的间接赋值

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

#include <stdio.h>
#include <stdlib.h>

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

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;
}

指针数组

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

#include <stdio.h>
#include <stdlib.h>

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 改变类型名
注意:结构体也是一种数据类型,是我们自定义的类型。

#include <stdio.h>
#include <stdlib.h>

/**
* 结构体就是自定义的数据类型语法: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函数。

#include <stdio.h>
#include <stdlib.h>

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;

结构体变量之间赋值

#include <stdio.h>
#include <stdlib.h>

/**
* 我们定义结构体的时候不开辟内存。
* 在声明变量的时候才会开辟内存 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");
}

结构体静态数组-栈区

#include <stdio.h>
#include <stdlib.h>

/**
* 结构体定义数组-在栈区开辟
* 注意类比
*/
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");
}

结构体动态数组-堆区

#include <stdio.h>
#include <stdlib.h>

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

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");
}

结构体嵌套数组

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

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


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函数

for (int i = 0; i < strlen(Name); i++) {        // 如果你不知道大小可以采用strlen函数
pMyStruct->hello[i] = Name[i];
}

结构体赋值第一部分

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

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 <stdio.h>
#include <stdlib.h>

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语言实现界面

#include <stdio.h>
#include <stdlib.h>

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语言没有类的概念,我们做一些属性的时候,只能是往结构体里保存。

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

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代表两个字符串不相同。