复习C语言进阶2

2023/10/08:

动态内存管理:

int val = 20;                  在栈空间上开辟四个字节

char arr[10] = {0};        在栈空间上开辟10个字节的连续空间


动态内存开辟原型:

空间开辟函数malloc

void* malloc (size_t size);

函数的作用是向堆区申请一块连续的空间,并返回指向这段空间的指针

  • 如果开辟成功则返回开辟空间的指针
  • 如果开辟失败则返回一个NULL指针,所以malloc以后记得要判断是否为NULL指针,如果是空指针则表示开辟空间失败。
  • 返回值是void*,因为malloc函数不知道使用者要开辟什么类型的空间,所以决定权交给使用者来决定。
  • 参数size_t size,则表示需要开辟多大的空间,以字节为单位。

空间释放函数free:

void free (void* ptr);

  • free函数用来释放动态开辟的内存,如果动态内存开辟完空间,到程序结束都没有释放,那就会造成内存泄漏。
  • 所以malloc和free函数是配对使用的。
  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
  • 如果参数 ptr 是NULL指针,则函数什么事都不做。

养成一个良好的习惯:

if (p == NULL)
	{
		perror("calloc fail");
		return;
	}
#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* ptr = (int*)malloc(10 * sizeof(int));
	if (NULL != ptr)//判断ptr指针是否为空
	{
		for (int i = 0; i < 10; i++)
		{
			ptr[i] = 0;
		}
	}
	free(ptr);//释放ptr所指向的动态内存
	ptr = NULL;
	return 0;
}

几个注意的地方:

  1. malloc之后必须用别的类型的指针强转。
  2. 必须判断指针是不是空的,空的就得perror接上return
  3. 必须free
  4. free完之后必须让指针变为空指针。

calloc

void* calloc (size_t num, size_t size);

函数的功能和malloc的区别在于,calloc开辟的空间会把每个字节都初始化为0

  • num代表要开辟的个数。
  • size代表每个个数的大小是多大。

realloc

  • realloc函数的出现让动态内存管理更加灵活。
  • 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。
  • realloc 函数就可以做到对动态开辟内存大小的调整。

void* realloc (void*ptr, size_t size);

  • ptr 是要调整的内存地址
  • size 调整之后新大小
  • 返回值为调整之后的内存起始位置。
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

int main()
{
	int* ptr = (int*)malloc(100);
    if (ptr == NULL)
    {
        perror("malloc fail");
        return;
    }
    //扩展容量
    ptr = (int*)realloc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)
    if(ptr == NULL)
    {
    	perror("malloc fail");
    	return;
    }
    free(ptr);
    return 0;
}
    int* p = (int*)realloc(ptr, 1000);
    if (p == NULL)
    {
        perror("realloc fail");
        return;
    }
    ptr = p;
    free(ptr);
    ptr = p;

用下面的方法更好:

  • malloc了空间的ptr指针作为接收返回值
  • 如果申请失败realloc则会返回空指针
  • 把空指针赋值给ptr不仅原本来的数据没了,还会造成内存泄漏

所以,新创建一个指针变量来接收realloc的返回值,如果是NULL指针直接返回,如果不是则把新指针给ptr。


常见的动态内存错误

对NULL指针的解引用操作


对动态开辟空间的越界访问


动态开辟内存忘记释放——内存泄露

void test()

{

        int *p = (int*)malloc(100);

        if(NULL != p)

        {

                *p = 20;

        }

}

  • 在test函数内部动态开辟了一次内存空间,由于疏忽忘记释放,所以会一直消耗内存
  • 在test函数调用完毕后,直接被销毁,虽然动态开辟的空间还在,但却永远也找不到位置。

解决方案:

int* p = (int*)malloc(100);

if (NULL != p)

{

        *p = 20;

}

free(p);

p = NULL;


不完全释放空间

指针p不该乱动!!!

void test()
{
	int *p = (int *)malloc(100);
	p++;
	free(p);//p不再指向动态内存的起始位置
}

  解决方案:

方案1:

*(p + i) = i;

printf("%d ",*(p + i));

方案2:

p[i] 

这两种方法不对p++所以不会把p指向的位置转移走。


对非动态内存空间进行释放

void test()
{
	int a = 10;
	int *p = &a;
	free(p);
}


笔试题

void GetMemory(char *p)
{
	p = (char *)malloc(100);
}
void Test(void)
{
	char *str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}
int main()
{
	Test();
	return 0;
}

把str传参传的是str的地址吗?不是的,传的是str指向的地址罢了,一级指针变量的地址应该用二级指针变量接受,所以传指针用指针接收的话,接收到的只是地址,即p指向0x00000000 NULL;把p指向的地址改了之后跟str指向的地址没关系。


内存开辟:

  • 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数分配的局部变量、函数参数、返回数据、返回地址等。
  • 堆区(heap):一般由使用者分配释放, 若使用者不释放,程序结束时可能由OS回收 
  • 数据段(静态区)(static)存放全局变量静态数据。程序结束后由系统释放
  • 代码段:存放函数体(类成员函数和全局函数)的二进制代码。以及只读常量。
     


通讯录的实现



自定义类型:结构体,枚举,联合

结构体

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

声明

struct tag
{
        member-list;
}variable-list;

匿名结构体


结构的自引用:


struct Node
{
        int data;
        struct Node* next;
};


结构体变量的定义和初始化:


内存对齐:

如何计算?
首先得掌握结构体的对齐规则:
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值
VS中默认的值为8
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

为什么存在内存对齐?
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

结构体的内存对齐是拿空间来换取时间的做法。

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。

struct S1
{
        char c1;
        int i;
        char c2;
};
struct S2
{
        char c1;
        char c2;
        int i;
};

S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。


修改默认对齐数:

#pragma pack(n)


结构体传参

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()
{
        print1(s); //传结构体
        print2(&s); //传地址
        return 0;

}

传地址更好!


位段:

位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 int、unsigned int 或signed int、char (属于整形家族)
2.位段的成员名后边有一个冒号和一个数字
位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的

struct A
{
        int _a:2;
        int _b:5;
        int _c:10;
        int _d:30;
};

A就是一个位段类型。


位段的内存:

这个图简洁明了:


枚举:

 enum Day//枚举类型enum Day
{
        Mon,//枚举常量
        Tues,
        Wed,
        Thur,
        Fri,
        Sat,
        Sun
};


枚举的优点

我们可以使用#define 定义常量,为什么非要使用枚举?
枚举的优点:
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量


枚举的使用:

enum Color//颜色
{
        RED=1,
        GREEN=2,
        BLUE=4
};
enum Color clr = GREEN;       //只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。

 



联合(共用体)

这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间

//联合类型的声明
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));
  1. 这样一个联合变量的大小,至少是最大成员的大小
  2. 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍

联合的内存调用:

union Un
{
		int i;
		char c;
};

int main()
{
	
	union Un un;
	un.i = 1;
	// 下面输出的结果是一样的吗?
	printf("%x\n", &(un.i));
	printf("%x\n", &(un.c));
	//下面输出的结果是什么?
	un.i = 0x11223344;
	un.c = 0x55;
	printf("%x\n", un.i);
	return 0;
}

有2点:

  1. un.i与un.c的地址相同,但是读取的步长不一样,一个是int一个是char
    1. un.i = 0x11223344的时候,小端存储44 33 22 11,
    2. un.c = 0x55的时候,小端存储55,会把44替换掉。



预处理与define:

程序的翻译环境和执行环境

在ANSI C的任何一种实现中,存在两个不同的环境。

  • 第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
  • 第2种是执行环境,它用于实际执行代码。
     

翻译环境

  • 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
  • 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
  • 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。


运行环境:

程序执行的过程:
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序
的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数。

3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止。


预定义符号

__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义


预处理: 

预处理宏:

  • LINE 表示正在编译的文件的行号

  • __FILE__表示正在编译的文件的名字__DATE__表示编译时刻的日期字符串,例如: “25 Dec 2007”

  • TIME 表示编译时刻的时间字符串,例如: “12:30:55”

  • STDC 判断该文件是不是定义成标准 C 程序 我的vs2013不是定义的标准c语言
     


#define

  • 宏优点
  • 1代码复用性
  • 2提高性能

  • 宏缺点
  • 1 不可调试(预编译阶段进行了替换),
  • 2无类型安全检查
  • 3可读性差,容易出错



#define 定义标识符

#define MAX 1000  注意不要加上
#define reg register //为 register这个关键字,创建一个简短的名字


#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。

#define 定义宏

把参数替换到文本中,这种实现通常称为宏

宏的申明方式:
#define name( parament-list ) stuff

其中的parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。

注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

例子:#define SQUARE( x ) x * x

但是,这个宏存在一个问题:
SQUARE(4+1)!=25

因为这样的:4+1*4+1=9

所以必须再加一个括号

#define SQUARE( x ) (x) * (x)

又有这种情况:

DOUBLE(x) (x) + (x)

10 * DOUBLE(5);

10*(5)+(5)

解决办法是在宏定义表达式两边加上一对括号就可以了

DOUBLE( x) ( ( x ) + ( x ) ) 


#define 替换规则


在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。


注意:
宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
 


#undef NAME


//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。


条件编译

#if 与 if

条件编译是C语言中预处理部分的内容,它是编译器编译代码时最先处理的部分,

条件编译里面有判断语句,比如 #if 、#else 、#elif 及 #endif

它的意思是如果宏条件符合,编译器就编译这段代码,否则,编译器就忽略这段代码而不编译,如

#define A 0 //把A定义为0

#if (A > 1)

printf( "A > 1"); //编译器没有编译该语句,该语句不生成汇编代码

#elif (A == 1)

printf( "A == 1"); //编译器没有编译该语句,该语句不生成汇编代码

#else

printf( "A < 1"); //编译器编译了这段代码,且生成了汇编代码,执行该语句

#endif

而 if 语句则不然,if 是 C 语言中的关键字,它根据表达式的计算结果来觉定执行那个语句,它里面的每个分支都编译了的, 如

#define A 0

if (A > 1)

printf( "A > 1"); //编译器编译该语句,但因为A == 0 未执行

else if(A == 1)

printf( "A == 1"); //编译器编译该语句,但因为A == 0 未执行

else

printf( "A < 1"); //编译器编译该语句,因为A == 0 故执行

作为一个编译“开关”(常用来注释代码),比如:

#if(条件满足)

执行代码 1

#else

执行代码 2

#endif

#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif

假如编译时,确实满足条件(结果非0时),则生成的程序文件(.exe文件)中不会有执行代码2的。如果用普通if语句,生成的程序文件就会有执行代码2,这个区别看看生成文件大小就可以知道。如果你的条件在程序编译前就已经确定了,那就用#if;如果条件需要在程序运行过程中才能判断,则用if。


文件包含


我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于#include 指令的地方一样。
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。

  • 本地文件包含#include "filename"
  • 库文件包含#include <filename.h>
  • 对于库文件也可以使用 “”的形式包含,但是这样做查找的效率就低些

或者

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif


结合动态内存管理,文件操作来完成

通讯录


contact.c

#define _CRT_SECURE_NO_WARNINGS 
#include"contact.h"

void InitContact(Contact* pc)
{
	assert(pc);
	pc->count = 0;
	memset(pc->data, 0, sizeof(pc->data));
	FILE* pfr = fopen("C:\\Users\\lxy\\Desktop\\c.txt","rb");
	if (NULL == pfr)
	{
		perror(" ");
		return;
	}
	int i = 0;
	PeoInfo tmp = { 0 };
	while (fread(&tmp, sizeof(PeoInfo), 1, pfr) == 1)
	{
		pc->data[pc->count] = tmp;
			pc->count++;

	}
	fclose(pfr);


	
}

void AddContact(Contact* pc)
{
	assert(pc);
	if (pc->count == MAX)
	{
		printf("通讯录已满\n");
		return;
	}
	printf("请输入名字\n");
	scanf("%s", pc->data[pc->count].name);
	printf("请输入年龄\n");
	scanf("%d", &(pc->data[pc->count].age));
	printf("请输入性别\n");
	scanf("%s", pc->data[pc->count].sex);
	printf("请输入电话\n");
	scanf("%s", pc->data[pc->count].tele);
	printf("请输入地址\n");
	scanf("%s", pc->data[pc->count].addr);
	printf("存储完毕\n");
	pc->count++;
}

void ShowContact(Contact* pc)
{
	int i = 0;
	for (i = 0; i < pc->count; i++)
	{
		printf("%-20s\t%-3s\t%-10s\t%-12s%\t%-30s\n","姓名","年龄","性别","电话","地址");
		printf("%-20s\t%-3d\t%-10s\t%-12s%\t%-30s\n",  pc->data[i].name, 
													pc->data[i].age, 
													pc->data[i].sex, 
													pc->data[i].tele, 
													pc->data[i].addr);

	}

}
void SearchContact(Contact* pc)
{
	assert(pc);
	char name[20] = { 0 };
	printf("请输入查找的名字");
	scanf("%s", name);
	int pos = FindByName(pc,name);
	if (-1 == pos)
	{
		printf("不存在");
		return;
	}
	else
	{
		printf("%-20s\t%-3s\t%-10s\t%-12s%\t%-30s\n", "姓名", "年龄", "性别", "电话", "地址");
		printf("%-20s\t%-3d\t%-10s\t%-12s%\t%-30s\n", pc->data[pos].name,
			pc->data[pos].age,
			pc->data[pos].sex,
			pc->data[pos].tele,
			pc->data[pos].addr);
	}


}

void ModifyContact(Contact* pc)
{
	assert(pc);
	char name[20] = { 0 };
	printf("请输入修改的名字");
	scanf("%s", name);
	int pos = FindByName(pc, name);
	if (-1 == pos)
	{
		printf("不存在");
		return;
	}
	else
	{
		printf("请输入名字\n");
		scanf("%s", pc->data[pos].name);
		printf("请输入年龄\n");
		scanf("%d",&(pc->data[pos].age));
		printf("请输入性别\n");
		scanf("%s", pc->data[pos].sex);
		printf("请输入电话\n");
		scanf("%s", pc->data[pos].tele);
		printf("请输入地址\n");
		scanf("%s", pc->data[pos].addr);
		printf("修改完毕\n");	
	}
}







int FindByName(Contact* pc, char* name)
{
	assert(pc);
	assert(name);
	int i = 0;
	for (i = 0; i < pc->count; i++)
	{
		if (0 == strcmp(name, (pc->data)[i].name))
			return i;
	}
	return -1;
}



void  DelContact(Contact* pc)
{
	char name[20] = { 0 };
	assert(pc);
	if (pc->count == 0)
	{
		printf("空通讯录,无法删除");
		return;
	}
	printf("输入删除的名字");
	scanf("%s", name);
	if (-1 == FindByName(pc, name))
	{	
		printf("没找到");
		return;
	}
	if (-1 != FindByName(pc, name))
	{
		int i = 0;
		for (i = FindByName(pc, name); i < pc->count-1; i++)
		{
			(pc->data)[i] = (pc->data)[i + 1];
		}
		pc->count--;
		printf("删除成功");
	}
}
int cmp(void* p1, void* p2)
{
	assert(p1);
	assert(p2);
	return strcmp(((PeoInfo*)p1)->name, ((PeoInfo*)p2)->name);

}


void SortContact(Contact* pc)
{
	qsort(pc->data, pc->count, sizeof(pc->data[0]), cmp);
	printf("成功");

}
void SaveContact(Contact* pc)
{
	assert(pc);
	FILE* pfw = fopen("C:\\Users\\lxy\\Desktop\\c.txt","wb");
	if (NULL == pfw)
	{
		perror(" ");
		return;
	}
	int i = 0;
	for (i = 0; i < pc->count; i++)
	{
		fwrite(pc->data+i, sizeof(PeoInfo), pc->count, pfw);

	}
	fclose(pfw);
	pfw = NULL;

}


test.c

#define _CRT_SECURE_NO_WARNINGS 
#include"contact.h"
void menu()
{
	printf("******************************************\n");
	printf("******    1.add         2.del      *******\n");
	printf("******    3.search      4.modify   *******\n");
	printf("******    5.show        6.sort     *******\n");
	printf("******    0.exit                   *******\n");
	printf("******************************************\n");

}

int main()
{
	int input = 0;
	Contact con;
	InitContact(&con);
	do
	{
		menu();
		printf("请选择\n");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			AddContact(&con);
			break;
		case 2:
			DelContact(&con);
			break;
		case 3:
			SearchContact(&con);
			break;
		case 4:
			ModifyContact(&con);
			break;
		case 5:
			ShowContact(&con);
			break;
		case 6:
			SortContact(&con);
			break;
		case 0:
			printf("exit\n");
			SaveContact(&con);
			break;
		default:
			printf("选择错误,请重新选择");
			break;
		}


	} while (input);
	



}

 


contact.h

#define _CRT_SECURE_NO_WARNINGS 
#pragma once
#define MAX 100
#define DEFAULT_SIZE 100
#include<stdio.h>
#include<string.h>
#include<assert.h>
#include<stdlib.h>
typedef struct PeoInfo
{
	char name[20];
	int age;
	char sex[10];
	char tele[12];
	char addr[30];
}PeoInfo;

typedef struct Contact
{
	PeoInfo data[100];
	int count; 

}Contact; 

void InitContact(Contact* pc);

void AddContact(Contact* pc);
void ShowContact(Contact* pc);
void SearchContact(Contact* pc);
void DelContact(Contact* pc);
void ModifyContact(Contact* pc);
void SortContact(Contact* pc);
void SaveContact(Contact* pc);

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值