一、结构体:
struct tag {
member-list;
}variable-list;
struct:结构体关键字(不能缺省)
tag:结构体标签,可以省略(匿名结构体类型)
member-list:成员列表
variable-list:变量列表
struct Stu
{
char name[20];
int age;
char id[13];
char gender[5];
};
int main()
{
struct Stu s1;
return 0;
}
(1)匿名结构体类型:省略标签,但是此时创建结构体必须创建在变量列表处
struct
{
char name[20];
int age;
char id[13];
char gender[5];
}s2;
int main()
{
return 0;
}
虽然两个匿名结构体类型的成员变量一模一样,但是仍然是不同的类型
struct
{
char name[20];
int age;
char id[13];
char gender[5];
}s2;
struct
{
char name[20];
int age;
char id[13];
char gender[5];
}*ps;
int main()
{
ps = &s2;
return 0;
}
此时会出现错误:
struct Stu
{
char name[20];
int age;
char sex[5];
int score[3];
};
int main()
{
struct Stu s = { "amao", 10, "女", { 100, 99, 98 } };
printf("%s\n", s.name);
printf("%d\n", s.score[1]);
system("pause");
return 0;
}

2、关于结构体的内存对齐:
(1)
# include<stddef.h>
struct s1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n",sizeof(struct s1));
printf("%d\n", offsetof(struct s1,c1));
printf("%d\n", offsetof(struct s1,i));
printf("%d\n", offsetof(struct s1, c2));
system("pause");
return 0;
}
offsetof()是一个宏返回一个整型,相当于这个成员在这个类型创建的变量起始位置的偏移量
#define offsetof(s,m) (size_t)&(((s *)0)->m)
//把0转换成结构体指针,(s *)0)->m:则0是结构体的地址,通过0地址找到成员m
//&(((s *)0)->m):取出0地址处某个成员的地址
//(size_t)&(((s *)0)->m):把取出的地址强转成size_t
a、 第一个成员在与结构体变量偏移量为0的地址处。
b、其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。 VS中默认的值为8 Linux中的默认值为4。
c、结构体总大小为最大对齐数(每个成员变量都有一个对齐 数)的整数倍。
d、如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
例:
a:
struct s2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct s2));
system("pause");
return 0;
}

b:
c、
struct s2
{
double c1;
char c2;
int i;
};
struct s3
{
char c1;
struct s2 s2;
double d;
};
int main()
{
printf("%d\n", sizeof(struct s3));
system("pause");
return 0;
}

B、那么为什么要存在内存对齐呢?
a、 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能 在某些地址处取某些特定类型的数据,否则抛出硬件异常。
b、性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的 内存访问仅需要一次访问
结构体的内存对齐是拿空间来换取时间的做法。那在设计结构体的时候,我们既要满足对齐,又要节省空间,应该让占用空间小的成员尽量集中在一起。
C、设置对齐数
# pragma pack(对齐数)//设置默认对齐数
# pragma pack(对齐数) //恢复默认对齐数
d、结构体传参:
值传递:形参是实参的一份临时拷贝,需要拷贝数据,比较浪费时间,浪费空间。
传地址:参数压栈只用压4个字节
# include<window.h>
struct S {
int data[1000];
int num;
};
struct S s = { { 1, 2, 3, 4 }, 1000 }; // 结 构 体传 参
void print1(struct S s) {
printf("%d\n", s.num);
} // 结 构 体 地址 传 参
void print2(struct S* ps) {
printf("%d\n", ps->num);
}
int main() {
int i = 0;
int time1 = 0;
int time2 = 0;
int start = 0;
int end = 0;
start = GetTickCount();
for (i = 0; i < 9999; i++)
{
print1(s); // 传 结 构 体,更安全,改变形参不会影响实参
}
end = GetTickCount();
time1 = end - start;
start = GetTickCount();
for (i = 0; i < 9999; i++)
{
print2(&s); // 传 地址,参数压栈只需要四个字节,效率高
}
end = GetTickCount();
time2 = GetTickCount();
printf("time1=%d,time2=%d\n", time1,time2);
system("pause");
return 0;
}
3、位段:节省空间,不存在内存对齐。位段其实也是结构体,只是在成员的后面加了一个冒号和数字。这些数字表示比特位。
A、
struct A
{
int _a : 2;
int _b : 3;
int _c;
int _d : 30;
};
int main()
{
printf("%d\n", sizeof(struct A));
return 0;
}
a. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家 族)类型
b. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟 的。
c. 位段涉及很多不确定因素,位段是不跨平台的【如何使用空间是不确定的,剩下的空间是省略还是继续使用不确定。不同的编译器可能采用不同的方式】,注重可移植的程序应该避免使用位段。
注意:给出的数据的大小不能大于当前类型的大小。
B、
位段的跨平台问题
a. int位段被当成有符号数还是无符号数是不确定的。
b. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写 成27,在16位机器会出问题。)
c. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。 【大小端的问题】
d. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段 剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
1、
enum Color // 颜 色
{ RED,
GREEN,
BLUE
};
2、枚举的特点:
enum Color // 颜 色
{
RED,
GREEN,
BLUE
};
//同下:标识符常量
//# define RED 0
//# define GREEN 1
//# define BLUE 2
int main(){
printf("%d\n", RED);
printf("%d\n", GREEN);
printf("%d\n", BLUE);
system("pause");
return 0;
}
默认从0 开始,一次递增1,当然在定义的时候也可以赋初值。
枚举常量可以和标识符常量定义同样的数据,但是枚举具有一些优于标识符常量的优点:
a、增加代码的可读性和可维护性
enum Option
{
ADD = 1,
SUB,
MUL,
DIV,
};
b. 很#define定义的标识符比较枚举有类型检查,更加严谨。
在c++中以下代码会出现错误:Color c和2的类型不符
enum Color // 颜 色
{
RED,
GREEN,
BLUE
};
int main(){
enum Color c = 2;
//改为:enum Color c = BLUE;
system("pause");
return 0;
}
c. 防止了命名污染(封装)
d. 便于调试 (#define定义的变量不能调试,但是枚举变量可以调试)
e. 使用方便,一次可以定义多个常量
union UN
{
char c;
int i;
double d;
};
int main()
{
union UN un;
printf("%d\n", sizeof(un));
printf("%p\n", &un);
printf("%p\n", &(un.c));
printf("%p\n", &(un.i));
printf("%p\n", &(un.d));
return 0;
}
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
2、联合也存在内存对齐(联合的总大小是最大对齐数的整数倍)
union UN
{
int i;
char c[5];
};
int main()
{
union UN un;
printf("%d\n", sizeof(un));
return 0;
}
char c[5]对齐数为2,大小为5;int i和
char c[5]的最大对齐数为4,4的最小倍数为8,所以总大小为8.
b、对于以下代码:最大对齐数为4,short[7]的对齐数为2,打小为14,所以总大小为16
union UN
{
int i;
short s[7];
};
联合大小的计算:联合的大小至少是最大成员的大小。当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
3、可以通过修改联合里面的一个参数的地址,修改另一个参数。
union Un
{
int i;
char c;
};
int main()
{
union Un un;
un.i = 0x11223344;
un.c = 0x55;
printf("%p\n", un);
return 0;
}
小端:低位字节序的内容放到低地址处;
大端:高位字节序的内容放在低地址处。
4、将 long 类 型 的 IP 地址 , 转转 为 点 分 十 进 制 的 表 示 形式
//192.168.10.1点分十进制
union IP
{
unsigned long num;//4个字节
struct
{
unsigned char c1;
unsigned char c2;
unsigned char c3;
unsigned char c4;
}ip;//4个字节
};
int main()
{
union IP myip;
myip.num = 213441237;
printf("%d.%d.%d.%d\n", myip.ip.c1, myip.ip.c2, myip.ip.c3, myip.ip.c4);
return 0;
}
四、C语言中的动态内存管理:
1、存在动态内存分配的原因:声明周期结束时,自动回收。但是也会代来一些问题
2、动态内存开辟相关的函数(在C++中叫做操作符):在堆上开辟空间
(1)malloc和free必须成对使用(空间不释放会造成内存泄漏)
void* malloc (size_t size);
size:要传的字节数 void*表示会把申请好的空间返回
void free (void* ptr);
应用:
int main()
{
int *px = (int *)malloc(34 * sizeof(int));
if(NULL==px){
return ;//return结束的是函数,不是整个程序
// exit(EXIT_FAILURE);结束整个程序,若是在一个函数中,则应该exit
}
//使用
int i = 0;
for (i = 0; i < 34; i++)
{
*(px + i) = i;
printf("%d ", i);
}
free(px);//必须进行回收
return 0;
}
malloc开辟空间失败,返回NULL,所以动态内存开辟返回的函数,必须检测是否为空。
(2)calloc:需要进行初始化,初始化为全0
void* calloc (size_t num, size_t size);
num:元素的个数 size:每个元素的长度
int *px = (int *)calloc(34 ,sizeof(int));
(3)realloc:重新开辟空间(扩大或者缩小空间)
void* realloc (void* ptr, size_t size);
size:新大小 ptr:要调整的空间
A、直接在后面进行扩容,
看以下代码:
int main()
{
int *px = (int *)malloc(80);
if (NULL == px){
return;//return结束的是函数,不是整个程序
// exit(EXIT_FAILURE);结束整个程序,若是在一个函数中,则应该exit
}
//使用
int i = 0;
for (i = 0; i < 34; i++)
{
*(px + i) = i;
printf("%d ", i);
}
px = (int *)realloc(px, 1500);
free(px);//必须进行回收
return 0;
}
若是扩容失败,会返回空指针,使原来的空间也找不到了,产生内存泄漏。改正:用临时指针把空间先存起来
int main()
{
int *px = (int *)malloc(80);
int *tmp = NULL;
if (NULL == px){
return;//return结束的是函数,不是整个程序
// exit(EXIT_FAILURE);结束整个程序,若是在一个函数中,则应该exit
}
//使用
int i = 0;
for (i = 0; i < 34; i++)
{
*(px + i) = i;
printf("%d ", i);
}
tmp = (int *)realloc(px, 1500);
if (tmp != NULL)//非空则使用当前的地址
{
px = tmp;
}
free(px);//必须进行回收
//px释放之后还记得原空间的地址,所以对内存空间进行释放之后,
//指针没有发生变化,还是保存原来的值,应该在释放指针之后,让指针指向空
px = NULL;
return 0;
}
B、后面的容量不够进行扩容,则重新开辟一块空间。将原有的数据拷贝到新空间,销毁原来的旧空间。
3、常见的动态内存错误:
(1)对空指针进行解引用
void test() {
int *p = (int *)malloc(INT_MAX/4);
*p = 20; // 如 果 p 的 值 是 NULL , 就 会 有 问题
free(p);
}
解决方式,在使用前进行判空
(2)对动态开辟空间的越界访问:此时只开辟了20个字节的空间,却要使用25个字节的空间,越界了
int *px = (int *)malloc(80);
int *tmp = NULL;
if (NULL == px){
return;
}
int i = 0;
for (i = 0; i < 33; i++)
{
*(px + i) = i;
printf("%d ", i);
}
(3)对非动态开辟内存使用free释放
int array[10] = { 0 };
int *px = array;
int i = 0;
for (i = 0; i < 10; i++)
{
*(px + i) = i;
}
free(px);
此时会听见编译器发出咚的一声响声
(4)使用free释放一块动态开辟内存的一部分
int *px = malloc(80);
int i = 0;
for (i = 0; i < 10; i++)
{
*px++ = 1;//赋值前10个字节的空间为1
}
free(px);//此时释放会失败,因为,px++此时指向第11个元素的位置,
//并没有指向原来的空间,此时释放会出现问题
(5)对同一块动态内存多次释放(一般自己回收自己开辟的空间)
(6)动态内存开辟忘记释放
int *p = (int *)malloc(100);
if(NULL != p) {
*p = 20;
}
(7)即使malloc和free成对出现也有可能会造成错误
void Test()
{
int *px =(int *) malloc(80);
int flag = 1;
if (px == NULL)
{
printf("%s\n",strerror(errno));
exit(EXIT_FAILURE);
}
if (0 != flag)
return;//程序从return处释放,没有给free()己会释放
free(px);
}
int main()
{
Test();
//出了函数后不能找到这块空间,不能释放
getchar();
return 0;
}
4、经典的几道改错题
(1)程序会崩溃,不会输出任何内容(两个问题,一个问题在代码注释中说明了;另一个问题是没有释放动态内存)
void GetMemory(char *p) {
p = (char *)malloc(100); //堆空间里开辟100个字节的空间
} //出了函数的作用域,p就销毁了,此时str与p没有任何关系
void Test(void) {
char *str = NULL;//创建str
GetMemory(str);//传值,变量为指针变量,此时p是str的一份临时拷贝
strcpy(str, "hello world");//此时str仍然为一个空指针,把一个字符串拷贝到空指针中
//会发生错误
printf(str);
}
改正:
A、改为传地址并且free:
void GetMemory(char **p) {
*p = (char *)malloc(100);
}
void Test(void) {
char *str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
B、return p;
char* GetMemory(char *p) {
p = (char *)malloc(100);
//char *p=(char *)malloc(100);//采用这种方式,函数中不传参
return p;
}
void Test(void) {
char *str = NULL;
str=GetMemory(str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
(2)程序有输出,但是输出的内容是随机值。返回栈空间的地址
char *GetMemory(void) {
char p[] = "hello world"; //函数结束之后,p中的内容就不存在了
return p; //返回p,即返回数组首元素的地址
}
void Test(void) {
char *str = NULL;
str = GetMemory();//str可以记住空间的地址,但是空间的内容已经不在了,空间不属于你
printf(str);
}
int *test()
{
int a=22;
return &a;
}
int main()
{
int *p=test();
return 0;
}
(3)没有free
void GetMemory2(char **p, int num) {
*p = (char *)malloc(num);
}
void Test(void) {
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
(4)
void Test(void) {
char *str = (char *)malloc(100);
strcpy(str, "hello");
free(str);//释放之后置为空指针
if (str != NULL) //空指针!=NULL,会发生错误
{
strcpy(str, "world");
printf(str);
}
}
五、柔性数组:结构体中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』 成员。
struct S
{
int a;
int arr[];//柔性数组
//int arr[0];//柔性数组
};
柔性
柔性数组的特点:
a、结构中的柔性数组成员前面必须至少一个其他成员。
b、sizeof 返回的这种结构大小不包括柔性数组的内存。
struct S
{
int a;
int arr[];//柔性数组
//int arr[0];//柔性数组
};
int main()
{
printf("%d\n", sizeof(struct S));
system("pause");
return 0;
}
c、包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应 该大于结构的大小,以适应柔性数组的预期大小。
struct S
{
int a;
int arr[];//柔性数组
//int arr[0];//柔性数组
};
int main()
{
struct S* px =(struct S*) malloc(sizeof(struct S)+100*sizeof(int));
int i = 0;
for (i = 0; i < 100; i++)
{
px->arr[i] = i;
}
for (i = 0; i < 100; i++)
{
printf("%d ", px->arr[i]);
}
free(px);
system("pause");
return 0;
}

方式二:
struct S
{
int num;
int *pStr;
};
int main()
{
struct S* px = (struct S*)malloc(sizeof(struct S));
px->pStr = malloc(100 * sizeof(int));
int i = 0;
for (i = 0; i < 100; i++)
{
px->pStr[i] = i;
}
for (i = 0; i < 100; i++)
{
printf("%d ", px->pStr[i]);
}
free(px->pStr);
free(px);
px = NULL;
system("pause");
return 0;
}
动态
malloc动态开辟内存不能太频繁,会使内存碎片增多,这是动态开辟内存相对柔性数组的缺点;由于内存的局部性原理,柔性数组采用连续存储的方式效率高。
柔性数组的优点:
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把 整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结 构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把 结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指 针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得 也没多高了,反正你跑不了要用做偏移量的加法来寻址)