枚举的形式就是这样
enum Day
{
MON,
TUES,
WED,
THUR,
FRI,
SAT,
SUN
};
int main()
{
enum Day d = FRI;
return 0;
}
enum在函数main函数内部,enum Day d=FRI,这里的意思是,d就表示enum Day中的FRI。
枚举的这个常量也是能够打印出来的。
#includeenum Day { MON, TUES, WED, THUR, FRI, SAT, SUN }; int main() { printf("%dn", MON); printf("%dn", TUES); printf("%dn", WED); return 0; }
我们打印枚举常量中的前三个常量:MON TUES WED
对应的取值是
所以对应的枚举常量的值是这样的
enum Day
{
MON,//0
TUES,//1
WED,//2
THUR,//3
FRI,//4
SAT,//5
SUN//7
};
但是假如我们把第一个枚举常量初始化为1时,常量对应的结果就从1开始了
#includeenum Day { MON=1, TUES, WED, THUR, FRI, SAT, SUN }; int main() { printf("%dn", MON); printf("%dn", TUES); printf("%dn", WED); printf("%dn", THUR); printf("%dn", FRI); printf("%dn", SAT); printf("%dn", SUN); return 0; }
我们进行编译
枚举的优点有哪些?
我们先讲第二点:
enum Day
{
MON=1,
TUES,
WED,
THUR,
FRI,
SAT,
SUN
};
#define MON 1
其中,#define进行定义的MON是没有类型的。
而由枚举定义的MON是有类型的,类型是enum Day
在c语言编译器上,检测的不够严格
#includeenum Day { MON=1, TUES, WED, THUR, FRI, SAT, SUN }; #define MON 1 int main() { enum Day d = 5; return 0; }
我们还是能够编译成功
但是在c++编译器上
原因是enum枚举的常量是有类型的,同等类型之间才能相互转化,
枚举常量
enum Day
{
MON=1,
TUES,
WED,
THUR,
FRI,
SAT,
SUN
};
这些MON YUES WED叫做枚举常量,枚举常量是不能修改的,有人会说,我们不是把MON改成1了吗?我们这里只是把初始值1赋给了MON而已。
优点4:便于调试:
例如:
#define M 3
int main()
{
int m = M;
}
我们的#define是通过替换来实现的
上图是我们的代码的运行过程,其中,#define的替换发生在预处理阶段
我们的替换是这样的,当我们写出这样一串代码时
#define M 3
int main()
{
int m = M;
}
M还是M本身,在我们开始运行程序,代码进行预处理时,程序就会把M替换成3,相当于
#define M 3
int main()
{
int m = 3;
}
相当于在main函数中,已经没有M,所以我们调试时也无法看见M的取值。
但是假如我们用枚举定义的变量,例如:
enum Day
{
MON=1,
TUES,
M
};
int main()
{
int m = M;
}
这时候,我们M的取值是3,我们经过调试的时候,也是能够看见对应M的取值。这就是枚举对应的调试的优点
联合(共用体)我们先写一串代码:
union un
{
int a;
char c;
};
struct st
{
int a;
char c;
};
int main()
{
union un u;
printf("%d", sizeof(u));
return 0;
}
联合类型的变量u理论上至少应该占用5个字节,我们进行编译,可以发现,他只占用了4个字节。
原因就是在于他们共用了同一块空间
我们进行证明
#includeunion un { int a; char c; }; struct st { int a; char c; }; int main() { union un u; printf("%pn", &u); printf("%pn", &(u.a)); printf("%pn", &(u.c)); return 0;
我们分别打印出联合u的地址,联合中a的地址和联合中c的地址。
我们可以发现,对应的地址的结果相同,证明:联合中的所有变量与联合所占用的空间是相同的。
联合内部的结构是怎样的,我们画图进行描述:
因为a,c,u的地址是相同的,所以说明他们的起始在同一位置
所以至少在第一个空间处,a和c是共存的。
因为联合体的这个特点,所以联合的成员变量一般是不一起使用的。
联合的下一个特点
#includeunion un { int a; char c; }; int main() { union un u; u.a = 0x11223344; u.c = 00; return 0; }
我们进行调试,看u.a对应的地址。
当跳过u.a时,对应的内存也已经被改变。
跳过u.c时,对应的内存也发生了改变。
所以u.c的内容改变也会导致u.a的内容发生改变。
如何判断当前编译器是大端存储还是小端存储:
int check_sys()
{
int a = 1;
return *(char*)&a;
}
int main()
{
int ret = check_sys();
if (ret==1)
printf("小端n");
else
printf("大端n");
return 0;
}
思路是这样的:
1在内存中的大端存储模式是00 00 00 01,小端存储模式是01 00 00 00,我们只需要取出第一个字节的地址然后再解引用判断,假如对应的结果是1就是小端,对应的结果为0就是大端。
这时候,我们就可以用联合的方法了,这种方法能让问题更简单化。
int check_sys()
{
union un
{
char c;
int a;
}u;
u.a = 1;
return u.c;
}
int main()
{
int ret = check_sys();
if (ret==1)
printf("小端n");
else
printf("大端n");
return 0;
}
我们在函数定义部分创建了一个联合u,我们把联合u中的整型a初始化结果为a,内存中的布局就是01 00 00 00,或者 00 00 00 01.然后我们返回u.c,也就是字符类型的数据,对应的是首字节,假如对应的结果为1就是小端存储,对应结果为0就是大端存储。
这里我们也可以使用匿名联合,因为我们只使用一次,可以把un去掉
union
{
char c;
int a;
}u;
接下来,我们求联合体的大小。
union un{
char arr[5];
int i;
};
int main()
{
printf("%d", sizeof(union un));
return 0;
}
这个联合体所占空间的大小是多少呢?
很多人会说占五个字节,我们进行编译
可以发现,占了8个字节,原因是什么呢?
答: 原因是存在对齐,int类型的对齐数是4,char类型的对齐数是1,所以最大的对齐数是4,所以必须为4的倍数,所以结果就是8.
内存中的图像是这样的
后面的空白是浪费掉的三个字节。
总结:1:联合体的大小至少是最大成员的大小
2:当最大成员的大小不是最大对齐数的大小的整数倍时,就要对齐到最大对齐数的整数倍。
动态内存管理我们目前知道的向内存申请空间的方法都有什么呢?
int main()
{
int a = 0;
int arr[10] = 0;
return 0;
}
第一个代表我们向内存申请了一个整型的空间,占四个字节,第二个代表我们向内存申请了一个数组的空间,占40个字节。
这些空间开辟的方法有一个共性的特点:无法改变空间的大小。
学习动态内存管理,我们最需要的就是以下四个函数的理解
我们先介绍malloc函数
malloc
函数的参数是无符号整数size,函数的返回值是void类型的指针。
函数的作用是:开辟内存块。
具体的作用是:开辟size个字节的内存空间到内存块中,返回一个指针指向内存块的起始位置。
因为我们开辟内存块的目的未知,所以我们的返回值的类型一定要是void*的类型。
malloc函数是申请内存的,有没有可能申请失败呢?
答:有可能,假如我们的内存空间有限,当申请的内存过大时,会失败,失败的话就会返回空指针。
我们举一个动态分配使用的例子
#include#include #include #include int main() { int arr[10] = { 0 }; int*p = (int*)malloc(40); if (p == NULL) { printf("%s", strerror(errno)); return 1; } // return 0; }
malloc函数的头文件是#include
我们对代码进行解析:
答:创建一个整型数组,整型数组有10个元素,我们调用malloc函数,malloc是申请内存,我们申请40个字节,因为我们这里用到的是整型,所以我们把malloc函数的返回值强制类型转化为整形指针再赋给整形指针p,因为申请内存会有失败的情况,所以我们要进行判断,判断其是否为空指针,如果为空指针说明我们申请失败,我们讲对应的错误码转化为错误信息打印出来,//位置表示我们成功申请内存。
成功申请内存后,画出图像
p就指向了我们申请空间的首元素的位置。
#include#include #include #include int main() { int arr[10] = { 0 }; int*p = (int*)malloc(40); if (p == NULL) { printf("%s", strerror(errno)); return 1; } int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } for (i = 0; i < 10; i++) { printf("%d", *(p + i)); } return 0; }
编译一下
顺利通过
接下来,我们实验一下内存不足产生错误时的信息
我们使用int的最大值
我们再进行编译:
报告错误信息:没有足够的空间。
malloc函数的特殊点。
首先,内存非为堆区栈区静态区,栈区的元素主要是局部变量和形式参数等再函数内部创建的变量,堆区则包括malloc calloc realloc free等函数申请或释放的空间,注意:栈区的空间,是不能够进行伸缩的,而堆区的空间可以进行伸缩,所以堆区的空间是可以进行变化的。
没有free,并不是说内存空间就不回收了,当程序退出的时候,系统会自动回收内存空间的。
内存泄漏的定义:
通俗的讲,内存泄漏就是我们向系统申请了一部分内存空间,我们用了一部分,但我们没有把剩余部分返还给系统,这剩余的部分既没有使用,又没有回收,就构成了内存泄漏。
变长数组:在c99标准下支持。
int main()
{
int n = 0;
scanf("%d", &n);
int arr[n] = { 0 };
}
如上图代码所示:我们创建一个变量,进行输入,然后我们可以用变量来定义数组的长度,这里就叫做变长数组。
free:释放内存
我们进行应用:
#include#include #include #include int main() { int arr[10] = { 0 }; int*p = (int*)malloc(INT_MAX); if (p == NULL) { printf("%s", strerror(errno)); return 1; } int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } for (i = 0; i < 10; i++) { printf("%d ", *(p + i)); } free(p); return 0; }
因为p指向了我们之前申请的空间,我们free表示释放内存空间,也就是释放p所指向的内存空间,将它返还给操作系统。
我们在free处设置一个断点:
我们进行调试
跳到断点处,p所指向的空间的地址是0x00b78040.
我们使用free释放空间。
可以发现,p仍然指向的是0x00b78040。
我们p所指向的空间已经返回给操作系统,再进行访问的话,就
所以我们为了代码的安全性,我们要把p置为空指针,防止对后续代码产生影响。
malloc函数总结:
如果开辟成功,则返回一个指向开辟好的空间的指针。
如果开辟失败,则返回一个空指针,因此malloc的返回值一定要做检查。
返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候,由使用者自己来决定。
如果size=0,则malloc函数行为未定义,取决于编译器。
free1:free的参数是一个无类型指针,返回值是一个void类型的值
2:假如free释放的空间不是动态内存开辟的,则这种行为未定义。
3:如果free函数的参数是空指针,那么他什么都不用做。
calloccalloc函数的参数有两个,类型都是无符号整型,函数的返回值类型是无类型指针。
calloc函数的作用也是动态内存开辟
第一个参数num表示申请几个元素的空间。
第二个参数size表示申请一个元素所占的字节数。
我们进行实验:开辟十个整型的空间:
#include#include #include #include int main() { int*p = (int*)calloc(10, sizeof(int)); if (p == NULL) { printf("%s", strerror(errno)); } }
注意:我们要判断p是否为空指针,如果为空指针证明申请动态内存失败,我们要把失败的错误信息打印出来。
#include#include #include #include int main() { int*p = (int*)calloc(10, sizeof(int)); if (p == NULL) { printf("%s", strerror(errno)); return 1; } int i = 0; for (i = 0; i < 10; i++) { printf("%d ", *(p + i)); } free(p); p = NULL; return 0; }
我们进行编译:
这里证明:calloc函数在开辟空间的时候,会首先把空间全部初始化为0,然后再把对应的空间的首元素的地址返回。
reallocrealloc函数的作用:调整动态内存大小
realloc有两个参数,第一个参数是无类型指针,第二个参数是无符号整型,第一个参数表示的是要调整的动态内存的首地址。size表示调整之后新内存的大小(单位字节)
我们实现一个realloc函数
int main()
{
int*p=(int*)malloc(40);
if (p == NULL)
{
printf("%sn", strerror(errno));
return 1;
}
realloc(p, 80);
return 0;
}
动态内存开辟时要记得判断是否为空指针,为空指针时要打印错误信息。
这里,我们要追加40个字节的空间,我们原来的空间大小是四十个字节,一共是80个字节。
realloc是怎样实现的
我们画图来进行解释:
这是第一种情况:假如我们原有的动态内存后面有其他被占用的内存,我们就会进行拷贝和调整
我们首先创建一个80个字节的空间,然后我们把之前的四十个字节以及里面的内容拷贝到这八十个字节里去,然后返回这80个字节的空间。 注意:这里老的四十个字节的空间在拷贝完成之后要被释放掉
第二种情况:
我们原有的动态内存的后面没有其他的被占用空间,我们直接在其后面添加一个40个字节的空间,然后我们返回这80个字节对应的首地址。
我们该如何接受realloc函数的返回值呢?
我们能不能用原动态内存的首地址p来接受
p=realloc(p, 80);
最好不要,原因是:原因是假如我们追加的空间过大,我们的函数调用就会失败,就会返回空指针,把空指针传递给指针p,那么我们的指针就连原来的地址都找不到了。
最好的方法,我们创建一个新指针来接受这个返回值。
int*ptr = realloc(p, 80);
if (ptr != NULL)
{
p = ptr;
}
如果ptr不是空指针,我们再把ptr传递给我们的参数p,我们的p才得到的是有效的地址。
#include#include #include #include int main() { int*p=(int*)malloc(40); if (p == NULL) { printf("%sn", strerror(errno)); return 1; } int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i + 1; } int*ptr = (int*)realloc(p, 80); if (ptr != NULL) { p = ptr; } for (i = 0; i < 10; i++) { printf("%d ", *(p + i)); } free(p); p = NULL; return 0; }
我们进行编译:
打印的结果还是相同。
我们进行调试
我们可以发现,在还没有经过判断的时候,对应的p和ptr就已经相同了。
所以证明:我们符合第二种情况:我们原有的动态内存的后面没有其他被占用的内存。
对应的就是这个图像
那么有没有可能产生第一种情况呢?
当我们追加8000个字节时
可以发现,p和ptr对应的结果就不相同。
注意:频繁大量的申请空间,容易产生内存碎片,运行的效率也会大打折扣。
在一定的情况下,realloc也和malloc等价:
int main()
{
realloc(NULL, 40);
malloc(40);
}
这里malloc和realloc时等价的。



