1、内存四区
1.1、数据类型的本质
1)数据类型基本概念
- 类型是对数据的抽象
- 类型相同的数据具有相同的表示形式、存储格式、相关的操作
- 程序中使用的数据必定属于某种数据类型
- 数据类型和内存 有关系
- C/C++ 引入数据类型,可以更方便地管理数据
2)数据类型的本质
- 数据类型可以理解为创建变量的模具:固定内存大小的别名
- 数据类型的作用:编译器预算对象(变量)分配的内存空间大小
- 数据类型只是模具,编译器不为类型分配空间,只有根据类型创建的变量才会分配空间
int main()
{int a; //告诉编译器分配 4 个字节int b[10]; //告诉编译器分配 4*10 个字节//类型的本质是固定大小内存的别名printf("sizeof(a)=%d\nsizeof(b)=%d\n",sizeof(a),sizeof(b)); //4 40//打印地址printf("b:%d &b:%d\n",b,&b); //数组名字就是数组首元素地址,数组首地址 两个输出一样的//b 和 &b 的数据类型不一样//b,数组首元素地址,一个元素 4 字节,+1 -> +4//&b,整个数组的首地址,一个数组 4*10=40字节,+1 -> +40printf("b+1:%d &b+1:%d\n",b+1,&b+1); //14678444 14678480//指针类型长度,32位为4, 64位为8char *****************p=NULL;int *q=NULL;printf("%d %d\n",sizeof(p),sizeof(q)); //32位:4 4 64位:8 8return 0;
}
3)数据类型的别名
- typedef 可以给类型取别名,且 typedef 只能给类型取别名
typedef unsigned int u32;//typedef 通常和结构体一起使用
typedef struct myStruct //这里的 myStruct 可写可不写
{int a;int b;
}TMP;
int mian()
{u32 t; //unsigned int t;return 0;
}
4)void 类型(空类型、无类型)
- 函数参数为空,在定义函数的时候,可以使用 void 来修饰:int func(void); 在 C++ 中 void 写不写是一样的,但是在 C 中是存在区别的
- 函数没有返回值,使用 void 来修饰:void func(void);
- 不能定义 void 类型的普通变量:void a; //err
- 可以定义 void 类型的指针*:void* p;
- 主要是因为 void 类型的普通变量不同类型的内存大小不一样,在编译器分配内存空间的时候无法确定分配的内存大小,而在相同的操作系统下,不同类型的指针变量内存大小相同,不影响编译器的内存分配
- void p 万能指针*,常用作函数返回值,或者函数的参数
- 这样可以很灵活,只要是指针就可以使用,例如 molloc 函数的定义:void* molloc(size_t size)
- memcpy 函数,拷贝内存的内容,可以拷贝各种类型的数组,而 strcpy,只能拷贝 char 类型的数组
1.2、变量的使用
- C 语言中,一维数组、二维数组其实也是有数据类型的
- C 语言中,函数也是具有数据类型的,可以通过函数指针进行重定义
1)变量的本质
- 变量:既能度又能写的内存对象。一旦初始化后不能修改的称为称量
- 变量定义形式:
- 类型 标识符1,标识符2,…,标识符n
- 变量的本质是:一段连续内存空间的别名
int main()
{int a;//变量相当于门牌号,内存相当于房间//直接赋值a=10;pringf("a=%d\n",a); //10//间接赋值pringf("&a=%p\n",&a); // a的地址p=&a;pringf("p=%p\n",p); // a的地址*p=22;pringf("a=%d\n",a); //22pringf("*p=%d\n",*p); //22return 0;
}
1.3、内存四区模型
- 四区:栈区、堆区、全局区、代码区(不用管)
1)全局区(静态区):全局变量、静态变量、文字常量
- 全局变量和静态变量,初始化的全局变量和静态变量存储在一起,未初始化的全局变量在另一块
- 全局区相同的常量只存在一份
char *get_str1()
{char *p="abcdef"; //文字常量区return p;
}char *get_str1()
{char *q="abcdef"; //文字常量区return q;
}int main()
{char *p=NULL;char *q=NULL;p=get_str1();//%s,打印指针指向的内存区域的内容//%d,打印 p 本身的值printf("p=%s, p %d\n",p,p); //p=abcdef,p=一串地址q=get_str2();printf("q=%s, q %d\n",q,q); //q=abcdef,q=一串地址,且这个地址和p的地址一样//主函数里面的p和 get_str1里面的p对应不同的内存//全局区相同的常量只存在一份,因此主函数里面的 p 和 q 指向同一块内存return 0;
}
2)栈区
char* get_str()
{ //字符串 "sdskcnckjana" 是存放在全局区char str[]="sdskcnckjana"; //栈区,函数结束,内存销毁,主函数中复制内存的内容,因此复制到的内容是不确定的,可能是原本的内容,也可能是乱码//这里在 char str[]="sdskcnckjana" 之后,会拷贝一份字符串到栈区return str;
}int main()
{char* buffer[128]={0};strcpy(buffer,get_str());printf("%s\n",buffer); //打印的结果:不确定,即乱码,这里还有可能输出 sdskcnckjana,是因为这里拷贝的时候可能 get_char 还没有销毁return 0;
}
char* get_str()
{ //字符串 "sdskcnckjana" 是存放在全局区char str[]="sdskcnckjana"; //栈区,函数结束,内存销毁,主函数中复制内存的内容,因此复制到的内容是不确定的,可能是原本的内容,也可能是乱码printf("%s\n",buffer); //打印的结果:sdskcnckjana//这里在 char str[]="sdskcnckjana" 之后,会拷贝一份字符串到栈区return str;
}int main()
{char* buffer[128]={0};char* p=NULL;p=get_str();printf("%s\n",buffer); //打印的结果:不确定,即乱码return 0;
}
3)堆区
char* get_str()
{ //字符串 "sdskcnckjana" 是存放在全局区char str[]="sdskcnckjana"; //栈区,函数结束,内存销毁,主函数中复制内存的内容,因此复制到的内容是不确定的,可能是原本的内容,也可能是乱码printf("%s\n",buffer); //打印的结果:sdskcnckjana//这里在 char str[]="sdskcnckjana" 之后,会拷贝一份字符串到栈区return str;
}char* strget2()
{char *temp=(char*)malloc(100); //堆区分配空间if(temp==NULL)return NULL;strcpy(tmp,"snjcscsdmkcs");//在这里,字符串"snjcscsdmkcs"存放在全局区,temp存放在栈区,指向一块堆区的内存,strcpy之后,会拷贝一份字符串"snjcscsdmkcs"到temp指向的堆区内存//get_str2函数运行完毕之后,p也会指向temp指向的堆区内存,并且会释放指针temp,但是temp指向的内存不会释放,需要手动释放return temp;
}
int main()
{char* buffer[128]={0};char* p=NULL;p=get_str2();if(!p){printf("%s\n",buffer); //打印的结果:snjcscsdmkcsfree(p); //释放p之前,这块堆内存使用权归p,释放之后,使用权归操作系统,但是内部的内容依然存在,直到下次被写才会改变,并且释放p之后,p依然指向这块堆区域,只是p指向的堆内存可以由系统支配了,所以一般指针释放之后,会将其指向空指针p=NULL;}return 0;
}
1.4、函数调用模型
- 关注重点在于调用的流程和变量的生命周期
- 调用模型是一个栈模型:先调用,后返回
1.5、函数调用变量传递分析
- main 函数调用子函数1,子函数1调用子函数2,那么 main 函数在栈区开辟的内存,子函数1和子函数2都可以使用
- main 函数在堆区开辟的内存,没有释放的时候,子函数1和子函数2都可以使用
- 子函数1在栈区开辟的内存,子函数1和子函数2都可以使用,但是 main 函数无法使用
- 子函数在堆区开辟的内存,没有释放的时候,main 函数、子函数1和子函数2都可以使用
- 全局区存放的变量,生命周期和程序一致,因此无论哪个函数在全局区开辟的内存,所有函数都可以使用
1.6、静态局部变量的使用
int *getA()
{static int a=10; //a是一个局部的静态变量,函数结束,内存不释放,因此只要把地址传出去,就可以通过地址使用这个内存了return &a;
}int main()
{int *p=getA(); //通过地址使用局部静态变量return 0;
}
- 在变量的生命周期之外,只要内存没有释放,就能够通过一定的手段使用对应的内存
1.7、栈的生长方向和内存释放方向
- 栈底,高地址;栈顶,低地址。栈的生长方向:栈底到栈顶,即栈的高地址到低地址,一般描述为从上到下
- 堆的生长方向与栈相反,从低地址到高地址,一般描述为从下到上
- 栈内的数组内部,是从低地址向高地址的,即栈内数组内部,也是从下到上
2、指针强化
2.1、指针也是一种数据类型
- 指针变量也是一种变量,占有内存空间,用来保存内存地址
- 通过星号操作内存
- 在指针声明的时候,星号表示所声明的是指针变量
- 在指针使用的时候,星号表示操作指针所指向的内存空间中的值
- *p 相当于通过地址(p 变量的值)找到一块内存,然后操作内存
int main()
{int a=100;int *p=NULL;int p1=NULL;char *********q=0x1111;//指针指向谁,就把谁的地址赋值给指针p1=&a;//通过星号可以找到指针指向的内存区域,操作的还是内存//星号放在等号的左边,给内存赋值,写内存//星号放在等号的右边,取内存赋值,读内存*p1=22;printf("%d %d\n",sizeof(p),sizeof(q)); //32位系统:4 4return 0;
}
2.2、指针间接赋值
- void* 类型的指针在使用的时候需要转换成实际的类型
int main()
{void* p;char buf[1024]="aancnciwce";p=buf;//void* 类型的指针在使用的时候需要转换成实际的类型printf("%s\n",(char*)p);int a[100]={1,2,3,4};p=a;int i=0;//void* 类型的指针在使用的时候需要转换成实际的类型for(i=0;i<4;i++){printf("%d ",*((int*)p+i));}int b[3]={1,2,3};int c[3];memcpy(c,b,sizeof(b)); //void* 类型的指针转换成了 int*for(i=0;i<3;i++){printf("%d ",c[i]);}char *q=NULL; //#define NULL ((void*)0),这里实际上q没有具体的指向,所以给q指向的内存赋值就会报错/*//加上这样两句,就可以给q 一个具体的指向,再给其指向的内存赋值,就没问题了char str2[100]={0};q=str2;*///给 q 指向的内存区域赋值strcpy(q,"1234"); //errreturn 0;
}
- 分文件编程说明
//防止头文件重复包含
#pragma once//例如,有两个头文件 a.h 和 b.h,且在 a.h 中包含了 b.h,在 b.h 中包含了 a.h,那么会出现头文件包含的死循环,导致文件包含过多的错误//不添加兼容 C++,使用 C++ 语言的程序调用 C 语言的程序的语句,编译的时候不会有问题,但是使用的时候会有问题。添加了之后,可以不做任何改动,直接使用
- 复习:
- 数据类型本质是固定内存大小的别名
- typedef,给数据类型起别名
- 栈和堆,栈是为了效率,堆为了内存分配更加灵活
- 栈的分配和回收由系统进行,堆的分配和回收由程序员进行
- 数组做形参,退化为指针。数组作为形参,丢失长度信息,使用 sizeif(a)/sizeof(a[0]) 无法计算出数组长度
- strcpy(p,“abcdefg”); 实际上不是给指针赋值,而是把字符串 “abcdefg” 拷贝到指针 p 指向的内存空间*
- 字符串,通过首地址可以用 printf 打印出来,而数组不可以,是因为字符串末尾有字符串结束符,而数组没有
- 指针变量和指针指向的内存是两个不同的概念
- 改变指针变量的值,会改变指针的指向,但是不会改变指针指向的内存的内容
- 改变指针指向的内存的内容,不会改变影响到指针变量的值
- 使用指针写内存的时候,一定要确保内存可写
char *buf="nscnscnwsicw"; //指针直接指向文字常量区
buf2[2]='l'; //err,因为这个字符串存放在文字常量区,内容不可改char str[]="nsijacnicwc"; //字符串常量本身存放在文字常量区,但是由于字符数组赋值,会复制一份存放在栈区
str[2]='l'; //OK,由于 str 是存放在栈区的字符数组,因此是可修改的
-
指针是一种数据类型,是指它指向的内存空间的数据类型
- 指针步长 (p++),根据指针所指向的内存空间的数据类型来确定
p++ 等价于 (unsigned char)p+sizeof(a);
-
不允许向 NULL 和未知非法地址拷贝内存
char *p3=NULL;
strcpy(p3,"lll"); //err,如果给 p3 赋值为某一个具体的非法地址,如 0x0001,也会出错,因为这个内存不允许使用
//给 p3 指向的内存区域拷贝内存,但是 p3 为空,没有指向任何有效的内存,因此内存拷贝会出错
2.3、通过指针间接赋值
- 步骤:
- 一般变量和指针变量
- 建立关系
- 通过 * 操作内存
int main()
{int a=100;int *p=NULL;//建立关系,指针指向谁,就把谁的地址赋给指针了p=&a;//通过 * 操作内存;*p=32return 0;
}
- 如果想通过形参改变实参的值,必须地址传递
int get_a()
{int a=10;return a;
}void get_a2(int a)
{a=22;
}void get_a3(int *a)
{*a=33; //通过星号操作内存
}void get_a4(int *a1,int *a2,int *a3,int *a4)
{*a1=33; //通过星号操作内存*a2=44;*a3=55;*a4=66;
}int main()
{int a=get_a();printf("%d\n",a); //输出为:10get_a2(a);printf("%d\n",a); //输出为:10//如果想通过形参改变实参的值,必须地址传递//实参,形参get_a2(a); //在函数调用时,建立关系printf("%d\n",a); //输出为:33int a1,a2,a3,a4;get_a4(&a1,&a2,&a3,&a4);printf("%d %d %d %d\n",a1,a2,a3,a4);return 0;
}
- 间接赋值是指针的最大意义,尤其是配合函数使用的时候
- 二级指针间接赋值
void func1(int *p)
{p=0xaabb;printf("%p\n",p); //0000aabb
}void func2(int **p)
{*p=0xeeff; //需要深入理解printf("%p\n",p); //0000eeff
}int main()
{/*//一个变量应该定义一个什么类型的指针保存它的地址//在原来的基础上再多加一个*int a=10;int *p=&a;int **q=&p;int *********t=NULL;int **********tp=&t;*/int *p=0x1122;printf("%p\n",p); //00001122func1(p); //值传递,传递的是指针变量的值printf("%p\n",p); //000011222func2(&p); //地址传递,传递的是指针变量的地址printf("%p\n",p); //0000eeffreturn 0;
}
2.3、指针作为函数参数的输入输出特性
- 主调函数可以把堆区、栈区、全局数据内存地址传给被调函数
- 被调函数只能返回堆区、全局数据
- 指针作为函数参数具有输入输出特性:
- 输入:主调函数分配内存
- 输出:被调函数分配内存
void func(char* p)
{//给p指向的内存区域拷贝,实际上就是main中的bufstrcpy(p,"ssvcscac");
}void func1(char **p,int *len)
{if(p==NULL)return;char* tmp=(char*)malloc(100);if(tmp==NULL)return;strcpy(tmp,"cscnsncscna");//间接赋值*p=tmp;*len=strlen(tmp);
}int main()
{//输入:主调函数分配内存char buf[100]={0};func(buf);printf("%s\n",buf); //ssvcscacchar *p=NULL;func(p); //err,不能给空或者非法未知内存拷贝//输出:被调用函数分配内存,要想进行内存修改,必须进行地址传递char *p1=NULL;int len=0;func1(&p1,&len);if(p)printf("%s %d\n",p,len); //cscnsncscna 11return 0;
}