自定义类型和C语言中的动态内存管理

自定义类型:结构体、枚举、联合
一、结构体:
1、结构体的声明
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;
}

此时会出现错误:


(2)结构体的应用
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


(2)内存对齐规则【vs下默认对齐数为8;Linux下默认对齐数是4】
A、

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. 使用方便,一次可以定义多个常量

三、联合:
1、

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就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得 也没多高了,反正你跑不了要用做偏移量的加法来寻址)

猜你喜欢

转载自blog.csdn.net/xuruhua/article/details/80800668