【C语言】数据结构预备知识,指针,数组,结构体,动态内存分配与释放

1 数据结构概述

(教材:严蔚敏、吴伟民,该书程序是伪算法具体的程序是高一凡)学完数据结构之后会对面向过程的函数有一个更深的了解。

1.1 定义

我们如何把现实中大量而复杂的问题以特定的数据类型(单个数据怎样存储?)和特定的存储结构(个体的关系)存到主存储器(内存)中,以及在此基础上为实现某个功能(比如查找某个元素,删除某个元素,对所有元素进行排序)而执行的相应操作,这个相应的操作也叫算法。

(比如班里有15个人,其信息量也许一个数组就搞定了,但是假如10000个,怎么办?内存也许没有这么多连续的空间,所以我们改用链表, you see这就是与存储有关系。又比如,人事管理系统的信息存储,因为存在着上下级的关系,所以数组和链表就无能为力了,这时候我们用树,再比如我们做的是交通图,站和站之间肯定要连通,这时候以上的存储方式又无能为力了,所以我们又有了图。图就是每个结点都可以和其他结点产生联系。所以当我们要解决问题时,首先要解决的是如何把这些问题转换成数据,先保存到我们的主存中,)

数据结构

狭义:
	数据结构是专门研究数据存储的问题 
    数据的存储包含两个方面:个体的存储 + 个体的关系的存储
广义:
	数据结构既包含数据的存储也包含数据的操作
	对存储数据的操作就是算法

算法

狭义:
    算法是和数据的存储方式密切相关
广义:
	算法和数据的存储方式无关
	这就是泛型的思想	

数据的存储结构有几种

线性
	连续存储【数组】
		优点:存取速度很快
		缺点:插入删除元素很慢,空间通常是有限制的
	离散存储【链表】
		优点:空间没有限制,插入删除元素很快	
		缺点:存取速度慢		

线性结构的应用

	栈
	队列

非线性

	树
	图

解题的方法和步骤

衡量算法的标准
	1、时间复杂度
		大概程序要执行的次数,而非执行的时间。
		
	2、空间复杂度
		算法执行过程中大概所占用的最大内存
		
	3、难易程度(主要是应用方面看重)
	
	4、健壮性(不能别人给一个非法的输入就挂掉)

数据结构的地位

	数据结构是软件中最核心的课程

按数据结构的思路理解数据库:
字段(一个属性)
记录(一个事务)
表(一类事务的集合)
外键表示事务与事务之间的关系

	程序 = 数据的存储+数据的操作+可以被计算机执行的语言(已经提供)

(学完数据结构,想用一种语言去实现它,必须有指针,数据结构java版,就胡扯,变味,因为我们要讲链表,就是通过指针链在一起的。比如在java中A aa = new A();本质上,aa是个地址)

2 预备知识

2.1 指针概述

指针的重要性:(内存是可以被CPU直接访问的,硬盘不行。主要靠地址总线,数据总线,控制总线访问内存。内存分为很多格子单元,格子编号范围是0-4G-1。地址总线确定对哪个单元进行操作,数据总线确定是读还是写。)
指针是C语言的灵魂
定义

地址  
    地址就是内存单元的编号
    从0开始的非负整数
    范围:0--FFFFFFFF[0-4G-1](地址线是32位,刚好控制2的32次)
指针:  
    指针就是地址  地址就是指针
    指针变量是存放内存单元地址的变量
    指针的本质是一个操作受限的非负整数(不能加乘除,只能减)
	分类:

2.2 基本类型的指针

# include <stdio.h>
int main(void)
{
	int * p; //p是个变量名字, int * 表示该p变量只能存储int类型变量的地址
	int i = 10;
	int j;

	p = &i;
	*p = i; // 等价于 i = i;
//	j = *p; // 等价于 j = i;
	printf("i = %d, j = %d, *p = %d\n", i, j, *p);	
	return 0;
} 

基本概念

    int i=10;  
    int *p = &i; //等价于 int *p;	p = &i;  

详解这两部操作:
1. p存放了i的地址,所以我们说p指向了i
2. p和i是完全不同的两个变量,修改其中的任意一个变量的值,不会影响另一变量的值
3. p指向i,*p就是i变量本身。更形象的说所有出现*p的地方都可以换成i,所有出现i的地方都可以换成*p

总结:
1、 如何一个指针变量(假定为p)存放了某个普通变量(假定为i)的地址,那我们就可以说:“p指向了i”, 但p与i是两个不同的变量,修改p的值不影响i的值,修改i的值不影响p的值.

2、 *p等价于i 或者说*p可以与i在任何地方互换

3、 如果一个指针变量指向了某个普通变量,则
*指针变量 就完全等价于 该普通变量

注意:
指针变量也是变量,只不过它存放的不能是内存单元的内容,只能存放内存单元的地址
普通变量前不能加*
常量和表达式前不能加&

# include <stdio.h>
void f(int i) 
{
	*p = 100;
}
int main(void)
{
	int i = 9;
	f(i);
	printf("i = %d\n", i);
	return 0;
}
//最后输出结果为9,因为局部变量只能在本函数内部使用。
# include <stdio.h>
void f(int * p) //不是定义了一个名字叫做*p的形参, 而是定义了一个形参,该形参名字叫做p,它的类型是int *
{
	*p = 100; //i的地址发送给p,*p就是i,即100赋给i,i变成了100
}

int main(void)
{
	int i = 9;
	f(&i);
	printf("i = %d\n", i);
	return 0;
}	
最后输出结果为100

如何通过被调函数修改主调函数中普通变量的值
Ⅰ 实参为相关变量的地址 &i
Ⅱ 形参为以该变量的类型为类型的指针变量 int * p
Ⅲ 在被调函数中通过 *形参变量名 的方式就可以修改主函数相关变量的值 *p = 100

2.3 指针和数组的关系

指针 和 一维数组

    数组名  
        一维数组名是个指针常量,  
        它存放的是一维数组第一个元素的地址, 
        它的值不能被改变  
        一维数组名指向的是数组的第一个元素,即a指向的是a[0]  
    下标和指针的关系  
        a[i] <<==>> *(a+i)  
# include <stdio.h>
int main(void)
{
    int a[5] = {1,2,3,4,5};
    //a[3] == *(3+a);
    printf("%p\n", a+1);
    printf("%p\n", a+2);
    printf("%d\n", *a+3); //*a+3等价于 a[0]+3
    return 0;
}
# include <stdio.h>
void Show_Array(int * p, int len)
{
    int i = 0;
    for (i=0; i<len; ++i)
        printf("%d\n", p[i]);//输出1,2,3,4,5
    //p[2] = -1;  //若p[0] = -1,则主函数输出a[0]=-1,因为p[0] == *p就是p指向的元素a[0],a[0]变化为-1;
    //若p[2] = -1,则主函数输出a[2]=-1,因为p[2] == *(p+2) == *(a+2) == a[2]
    //结论:p[i]就是主函数的a[i]
}

int main(void)
{
    int a[5] = {1,2,3,4,5};
    Show_Array(a, 5);  //a等价于&a[0], &a[0]本身就是int *类型,a发送给p
    //printf("%d\n", a[2]);
    return 0;
}

假设指针变量的名字为p
则p+i的值是p+i*(p所指向的变量所占的字节数)

指针变量的运算
指针变量不能相加,不能相乘,不能相除
如果两指针变量属于同一数组,则可以相减
指针变量可以加减一整数,前提是最终结果不能超过指针允许指向的范围
p+i的值是p+i*(p所指向的变量所占的字节数)
p-i的值是p-i*(p所指向的变量所占的字节数)
p++ <==> p+1
p-- <==> p-1

举例

如何通过被调函数修改主调函数中一维数组的内容【如何界定一维数组】
    两个参数
        存放数组首元素的指针变量
        存放数组元素长度的整型变量
# include <stdio.h>
int main(void)
{
    double * p;
    double x = 66.6;

    p = &x;  //x占8个子节 1个字节是8位, 1个子节一个地址

    double arr[3] = {1.1, 2.2, 3.3};
    double * q;

    q = &arr[0];
    printf("%p\n", q);  //%p实际就是以十六进制输出
    q = &arr[1];
    printf("%p\n", q);  

    return 0;
}
//输出:0012FF5C 00122FF64 C=12 5C+8=64

指针在32位编译环境中只占4个字节,64位编译环境中占8个字节

通过函数修改实参的值
要想修改一个变量的值,如果函数没有返回值,只能在函数中放入地址用指针:

# include <stdio.h>
void f(int * p);//前置声明

int main(void)
{
    int i = 10;

    f(&i);
    printf("i = %d\n", i);

    return 0;
}

void f(int * p)
{
    *p = 99;
}

更进一步的,无论p是什么类型的变量,要想改写它的值,只需要在函数中放它的地址。

# include <stdio.h>
void f(int ** q);
int main(void)
{
    int i = 9;
    int * p = &i;// int  *p;  p = &i;

    printf("%p\n", p);
    f(&p);
    printf("%p\n", p);

    return 0;
}
//p是int *类型,则&p是int **类型,指针的指针。把p的地址发送给q,*q=p
void f(int ** q)
{
    *q = (int *)0xFFFFFFFF;
}

2.4 结构体(C++中用类也能实现)

为什么会出现结构体
		为了表示一些复杂的数据,而普通的基本类型变量无法满足要求
	
什么叫结构体
		结构体是用户根据实际需要自己定义的复合数据类型
        结构体相当于java中的类,但是里面没有方法
	
如何使用结构体
		两种方式:
			struct Student st = {1000, "zhangsan", 20}
			struct Student * pst = &st;
			
			1.
				st.sid
			2.
			 	pst->sid
			 	表示pst所指向的结构体变量中的sid这个成员

使用结构体变量的第一种方式:

# include <stdio.h>
# include <string.h>

struct Student
{	
	int sid;
	char name[200];
	int age;
}; //分号不能省
//定义了一个新的数据类型,叫struct Student,里面有三个成员sid,name,age
int main(void)
{
	struct Student st = {1000, "zhangsan", 20};
	//用struct Student数据类型定义了一个名为st的新变量。
	printf("%d  %s  %d\n", st.sid, st.name, st.age);

	st.sid = 99;
	//st.name = "lisi";  //error,只有java能这样写
	strcpy(st.name, "lisi");
	st.age = 22;
	printf("%d  %s  %d\n", st.sid, st.name, st.age);

	//printf("%d %s %d\n", st);  //error
	return 0;
}    	

32位系统,内存中无论指针指向的变量类型是什么类型,指针变量都只占4个字节,比如int类型占4个字节,分4个格子,一个字节一个编号,那么就有4个编号,地址总线是32位,每个编号需要32根线,32根线能确定4G个不同状态,编号为0,线就是0,0,……0,编号为1,线就是0,0,……1,每个格子能存32位数据,也就是4个字节,指针变量指向的只是第一个编号的格子,

使用结构体变量的第二种方式:

# include <stdio.h>
# include <string.h>

struct Student
{	
	int sid;
	char name[200];
	int age;
}; //分号不能省

int main(void)
{
	struct Student st = {1000, "zhangsan", 20};
	//st.sid = 99;  //第一种方式

	struct Student * pst;
	pst = &st;
	pst->sid = 99;  //第二种方式  pst->sid 等价于 (*pst).sid  而(*pst).sid等价于 st.sid,  所以pst->sid 等价于 st.sid

	return 0;
}

注意事项:
结构体变量不能加减乘除,但可以相互赋值
普通结构体变量和结构体指针变量作为函数参数的传递

(病毒就是靠访问正在运行的那些程序所占用的内存。Java中规定局部变量必须初始化,因为这些变量一开始都是垃圾值,但是属性不是必须初始化的,因为已经默认初始化为0)

# include <stdio.h>
# include <string.h>

struct Student
{	
	int sid;
	char name[200];
	int age;
}; //分号不能省

//前置声明不能漏,不然系统不知道f,g,g2是什么
void f(struct Student * pst);
void g(struct Student st);
void g2(struct Student *pst);

int main(void)
{
	struct Student st;  //已经为st分配好了内存,但是里面是垃圾数字,需要通过指针修改变量的值

	f(&st);
	g2(&st);

	//printf("%d %s %d\n", st.sid, st.name, st.age);

	return 0;
}

//这种方式耗内存 耗时间 不推荐,结构体变量直接赋值,相当于直接传了大约208个字节,而指针只占用4个字节。
void g(struct Student st)
{
	printf("%d %s %d\n", st.sid, st.name, st.age);	
}

void g2(struct Student *pst)
{
	printf("%d %s %d\n", pst->sid, pst->name, pst->age);	
}

void f(struct Student * pst)
{
	(*pst).sid = 99;
	strcpy(pst->name, "zhangsan");
	pst->age = 22;
}

2.5 动态内存分配和释放

(使用malloc分配的就是动态内存,不使用的就是静态内存。动态分配的内存一定要手动释放,否则造成内存泄露。)
(java中A aa = new A();其实就是 A *p = (A *)malloc(sizeof(A)))

动态内存分配和释放:

动态构造一维数组
假设动态构造一个int型数组
int *p = (int *)malloc(int len);
1、 malloc只有一个int型的形参,表示要求系统分配的字节数
2、 malloc函数的功能是请求系统len个字节的内存空间,如果请求分配成功,则返回第一个字节的地址,如果分配不成功,则返回NULL
3、 malloc函数能且只能返回第一个字节的地址,所以我们需要把这个无任何实际意义的第一个字节的地址(俗称干地址)转化为一个有实际意义的地址,因此malloc前面必须加(数据类型 *),表示把这个无实际意义的第一个字节的地址转化为相应类型的地址。如:
int *p = (int *)malloc(50);
表示将系统分配好的50个字节的第一个字节的地址转化为int *型的地址,更准确的说是把第一个字节的地址转化为四个字节的地址,这样p就指向了第一个的四个字节,p+1就指向了第2个的四个字节,p+i就指向了第i+1个的4个字节。p[0]就是第一个元素, p[i]就是第i+1个元素
double *p = (double *)malloc(80);
表示将系统分配好的80个字节的第一个字节的地址转化为double *型的地址,更准确的说是把第一个字节的地址转化为8个字节的地址,这样p就指向了第一个的8个字节,p+1就指向了第2个的8个字节,p+i就指向了第i+1个的8个字节。p[0]就是第一个元素, p[i]就是第i+1个元素
free(p)
释放p所指向的内存,而不是释放p本身所占用的内存

# include <stdio.h>
# include <malloc.h>

int main(void)
{
	int a[5] = {4, 10, 2, 8, 6};//静态分配内存

//用指针动态分配内存,模拟数组	
	int len;
	printf("请输入你需要分配的数组的长度: len = ");
	scanf("%d", &len);
	int * pArr = (int *)malloc(sizeof(int) * len);
//sizeof(int)是整型的字节数,若len=5,则(sizeof(int) * len)=20,20表示malloc函数请求操作系统为程序分配20字节的空间。(int *)是强制转换,无论一个变量占几个字节,malloc函数只能返回第一个字节地址,这个地址是无意义地址(干地址),所以前面需要强制转换,告诉编译器第一个地址是什么类型(int or double……)的地址
//	*pArr = 4;  //类似于 a[0] = 4; 
//	pArr[1] = 10; //类似于a[1] = 10;
//	printf("%d %d\n", *pArr, pArr[1]);

	//我们可以把pArr当做一个普通数组来使用
	for (int i=0; i<len; ++i)
		scanf("%d", &pArr[i]);

	for (i=0; i<len; ++i)
		printf("%d\n", *(pArr+i));

	
	free(pArr);  //把pArr所代表的动态分配的20个字节的内存释放

	return 0;
}

跨函数使用内存:
8、下列程序中,能够通过调用函数fun,使main函数中的指针变量p指向一个合法的整型单元的是

A) main()
  {  int *p;
     fun(p);} 
  int fun(int *p)
  { int s; 
    p=&s;
  }
B) main()
   { int *p;
     fun(&p);}
  int fun(int **q)
  {  int s;
     *q=&s;
  }
C) #include <stdlib.h>
   main()
   {  int *p;  //p变量地址发送给q,则*q=q
      fun(&p);}
   int fun(int **q)
   {  *q=(int *)malloc(4);  //p指向4个字节,函数执行完后,这4个字节没有消失,因为内存没释放
   }
D) 
   #include <stdlib.h>
   main() 
  {  int  *p;
     fun(p);}
   int fun(int *p)
   {  p=(int *)malloc(sizeof(int));
   }

注释: 这个题很有意思。考查了:

  1. 指针的指针的使用 2) 动态内存分配与自动变量的内存分配。
    动态分配的内存必须调用free()函数才能释放,而自动变量一旦跳出它的代码作用范围,就会由编译器自动释放掉。
    让我们先看:
    A) 选项无论fun()中p的值如何变化,都不会影响到主函数中p的值,因为它是值传递
    B) 选项倒是把p的地址&p传递给了fun()函数,但遗憾的是,由于s是个自动变量,当推出fun()函数后,s变量所占内存单元会被会被释放掉,此时主函数中的p还是没法指向一个合法的int型单元
    C) 选项fun()的形参 int **p;表明p是个指向指针变量的指针变量,即是个指针的指针。 而主函数中的 int *p; 表明p只是个指针变量,但&p则指向了p,&p也是个指向指针变量p的指针变量,实参和形参类型一致。 fun()的功能是使实参p指向了一个int型变量, 又由于该int型变量是由malloc()动态分配的,所以推出fun()函数并不会影响实参p的指向, 故C是对
    D) 选项犯了和A同样的错误。 真想不到二C还会考到这个知识,哈哈!
# include <stdio.h>
# include <malloc.h>

struct Student
{
	int sid;
	int age;
};

struct Student * CreateStudent(void);
void ShowStudent(struct Student *);

int main(void)
{
	struct Student * ps;

	ps = CreateStudent();
	ShowStudent(ps);

	return 0;
}

void ShowStudent(struct Student * pst)
{
	printf("%d %d\n", pst->sid, pst->age);
}

struct Student * CreateStudent(void)
{
	struct Student * p = (struct Student *)malloc(sizeof(struct Student));
	p->sid = 99;
	p->age = 88;
	return p;
}
//输出99 88
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值