C语言小结

C语言小结:

普通变量:数据类型 变量名:直接表示的是01代码所表示的值;

指针变量:根据当前操作系统的地址总线的表示,决定指针变量是多少位的。指针变量保存的是内存单元的地址;而且是连续空间的第一个内存单元的地址值;

数组:数组是比较特殊的,确定一个数组需要两个参数。一个是数组名,另外一个是数组长度。

数组名字是连续内存单元的第一个内存单元的地址。

数据类型:数据类型的作用是用来按照偏移量来取出来对应的数据的。操作系统根据类型来决定取出多少个字节或者是多少个内存单元地址。

执行逻辑:for、while、do...while

数据结构

1、什么是数据结构

把现实生活中大量而复杂的问题以特定的数据类型和特定的存储结构保存到主存储器(内存)中,在此基础之上为实现某个功能而执行的响应的操作,这个相应的操作叫做算法。

从前面一句话中可以体现出来两个要素:个体和个体之间的关系

特定的数据类型:代表了其中的一个个体

特定的存储结构:个体和个体之间的关系是什么样子的

只要解决了这两个问题,那么就认为解决了将现实生活中的事物保存到了计算机中去。

至于如何来进行操作,那么这是算法该做的事情了。

数据结构解决的是数据存储的问题,算法是对存储数据的操作

算法是用来解题的方法和步骤

算法的衡量标准:

1、时间复杂度

时间复杂度不是算法的执行时间,而是算法大概执行了多少次。
    因为在不同的机器上,算法执行时间不一定,所以无法来作为一个参考标准

2、空间复杂度

算法执行过程中大概占据的内存大小

3、难易程度

4、健壮性

最重要的是前两个,但是实际上在编写程序的时候,考虑的是第三个点,每个人都能够来进行实现。

所谓的栈、堆,不能理解成内存中的一块区域,而是考虑分配内存的方式。如果是以压栈、出栈的方式分配的内存,那么叫栈;如果是以堆排序方式分配的内存,那么叫做堆内存。

包括函数调用:真正的原理是在调用的时候,内存是以栈的方式来进行分配内存的。

数据库和数据结构的联系

字段、记录、表:字段是事物的基本属性,记录是一个事物的描述,表是事物的集合,关系是靠键来进行连接的。

我觉得这句话的描述非常之合理。

程序=数据结构+对数据的操作+被计算机执行的语言(c,c++,java,python)

数据结构的实现要用C语言来进行实现,因为有了指针,才有了对应的地址。

最基本的就是链表中的,需要大量使用到。

预备知识:指针、结构体、动态内存的分配和释放

难和不难:一个知识点自己学不会,别人告知了,但是也无法学会;但是对于那些别人告诉你了,就知道怎么去做了,这种就属于简单范畴。

指针开篇:

# include <stdio.h>
int main(void){
	
	int * p;
	int i  = 10;
	int j ;
	j = *p;
	printf("%d\n",j);
	return 0;
}

无论一个指针怎么变化,首先必须要有声明。

上面是声明了一个指针变量,既然是变量,那么也是属于变量的一种。之前在C语言中讲述了变量必须要进行初始化,为什么需要进行初始化,相信已经讲的很清楚了。所以这里不来进行赘述了。

指针是C语言的灵魂,指针的本质是地址,地址是内存单元的编号,对于一个编号来说,由地址总线构成,我们64位的地址总线代表了64个01状态,所以是由64位01代码组合而成的。而硬件只能够识别字节,所以将其转换成为字节之后,就是64/8=8个字节,所以一个指针变量的大小是由8个字节组成的。

内存是CPU可以访问的唯一的一块大容量的存储部件。

CPU通过三根线来访问内存。

地址线就是通过内存单元编号来快速定位到是在内存中哪个位置的;

控制线就是对内存单元中的数据是读还是写

数据线就是数据CPU和内存之间进行数据交互的线路

其中需要注意的是,对于内存单元中的数据的访问。

在上图中,我写了两个,但是我用的是人类能够识别的十进制表示,实际上应该根据具体的数据类型+01代码解读来进行确定的。

重点就在于对里面的01代码的解读,以不同的方式进行解读,造成的影响会有很大不同。

比如说:是以int类型来进行解读,表示的是数字;以字符形式进行解读;以地址编号的形式进行解读等等

得到的结果所表示的意义也就不同。

指针是什么?指针就是地址,地址就是指针。因为地址是从大于等于0的整数,所以指针本身已经是一个大于等于0的整数了。

但是指针是一个操作受限的非负整数。因为指针无法来进行加减乘除。

而在数组那一章节,我们知道*(a+3),这种是对地址的操作,这里的3不是我们理解的3,而是偏移量。

指针变量是存放地址的变量,操作指针变量,就是操作地址

用一个例子来进行详细说明:

# include <stdio.h>
int main(void){
	
	int * p;
	int i  = 10;
    int j;
	p=&i;
	printf("%d\n",*p);
    printf("%d\n",i);
	return 0;
}

对上面的理解用个图来进行表示:

左边的代表的是内存单边的编号,右边的是变量名字,中间代表的是对01代码解读(带H的表示的地址,也就是指针;10代表的是变量i的值)

四段式总结上面的代码:

1、指针变量p表示的是保存了int类型的地址;
2、变量p指向了i,即是保存了变量i的地址;
3、*p和i是等价的。操作*p也就代表了操作变量i;
4、 修改了p的值,并不会影响i的值;修改了i的值,并不会影响p的值。   

对第三局话的理解:*p和i是等价的。而不能够说星号p代表的就是就是变量i的值,只能说代表的是是变量i。举个例子说明下:

int * p;
int i = 10;
p = &i;
*p = 100;

这段话如果理解成了*p就是i的值,那么效果上就是10=100.那么这样子肯定是不行的。所以理解成星号p代表的是变量i

也就是说:

*p = 100 等价于是  i = 100

对第四句的理解:修改p的值,指针p就不指向i的地址了,那么就不会影响到i的值;修改了i的值,因为p变量保存的是i变量的地址值,对i本身保存的值没有影响。所以都不会有影响。

如何通过被调函数修改主函数中普通变量的内容

三个参数:实参是相关变量的地址;形参为该变量的类型作为类型的指针变量;在被调函数中 * 形参名就可以修改主函数中的普通变量值

看一个例子:

# include <stdio.h>

void update(int * p){
	*p = 111;
}

int main(void){
	
	int i = 10;
	printf("%d\n",i);
	update(&i);
	printf("%d\n",i);
	return 0;
}

控制台输出,可以看到结果。

如何通过被调函数修改主函数中一维数组的内容

两个参数:存放数组首元素的指针变量,存放数组长度的整型变量

需要注意一点的是:C语言和java中的for循环是有区别的,变量定义需要放在for循环的外面

看一下例子:

# include <stdio.h>

void showArray(int * p,int len){
	*(p+2)=100;
}

int main(void){
	int a[8] = {1,2,3,4,5,6};
	showArray(a,8);
	printf("%d\n",a[2]);
	return 0;
}

可以看到数组中的内容a[2]发生了改变。

再次研究指针,指针占用的内存空间

# include <stdio.h>
int main(void){
	double i = 6.66;
	double * p = &i;
	printf("%p\n",p);
	return 0;
}

控制台输出:

000000000062FE10

输出%p是以十六进制来输出的,因为十六进制中每位是由4个bit组合而成的,所以原始的二进制代码应该是64位的,也就是64位地址总线所表现出来的状态。

再分析一下,因为变量i是double类型的,所以变量i占用了四个字节。既然变量i占用了四个字节,那么对变量取地址,得到的是几个字节?得到的是8个字节,但是因为计算机中习惯用大端法和小端法来进行表示。因为是连续存储的,所以只取了其中的一个字节作为首要地址,但是本质上根据数据类型操作系统会找到几个连续的字节来找到对应的内存单元,从内存单元中取出来内存单元中存储的值。

然后对内存单元中的值来进行表示。

画个图来进行说明一下:

假如说上面的(前面还有60个01代码)00000000~(前面还有60个01代码)00000111代表的是变量i的内存存储空间,因为内存单元号是操作系统弄分配连续的四个字节,那么当对其进行取地址的时候,得到的是最小的内存单元编号,也就是(前面还有60个01代码)00000000

那么变量p因为是指针,指针是地址,所以存储的就是(前面还有60个01代码)00000000,那么对于后面的七个地址又是如何来进行存储的呢?计算机利用大端法和小端法来表示,即因为这八个字节是连续的,又因为指针p也是doule类型的,所以在寻址的时候,操作系统会根据首地址来找到连续的8个字节的内存单元编号,也就是(前面还有60个01代码)00000000~(前面还有60个01代码)00000111。

所以对内存单元中的01代码来进行取值。

所以对于指针来说,存储的知识变量对应的数据类型占用的连续字节的首地址。

比如上面的变量i,是double类型的,占用了8个字节,只取了第一个内存单元的地址,(前面还有60个01代码)00000000然后保存到指针p中去,当操作指针p的时候,操作系统会根据其数据类型,然后找到数据类型占用的几个单元的连续的存储空间的01代码

本质上是这样子。

所以来验证一下:

# include <stdio.h>
int main(void){
	int a[5] = {1,2,3,4,5,6};
	int * p;
	p = &a[0];
	printf("%p\n",p);
	p = &a[1];
	printf("%p\n",p);
	return 0;
}

查看控制台输出:

000000000062FE00
000000000062FE04

可以看到相差了三个内存单元地址编号。这里只是存储了第一个的内存地址单元编号

上面的得到了验证。

结构体

为什么会出现结构体?什么是结构体?如何来使用结构体?

如果来定义一个事物,其中的属性比较复杂,那么单一的无法来进行表示,所以需要将多个属性来组合在一起。

结构体和java中的类是非常相似的

比如说:

class Student{
    int age;
    int id;
    int showAge(){
       return this.age; 
    }
}

类中包含了属性和对属性的操作

而结构体中只是定义了属性而已,相对来说,结构体的功能是比较弱的。但是这里也体现出来一个好处,我们在C语言中编写程序是以算法为核心的,在面向过程语言中,这种思想非常重要。

struct Student{
    int age;
    int num;
}

尽管来说是比较弱的,但是还是多个属性的结合。也是表示了一种事物具备状态的描述。

所以结构体是用户自己根据实际的需要来定义的一种数据类型。

下面来定义一下一个简单的结构体:

# include <stdio.h>

struct Student{
	int id;
	char name[100];
};

int main(void){
	
	
	return 0;
}

结构体是一种数据类型,是一种模型,而不是具体的变量。地位上等同于是int double这种数据类型,所以声明变量,那么就像和int double一样的声明变量的方式。

这个结构体整体是一个新的数据类型,而不是说Student是一个数据类型。

struct Student{
	int id;
	char name[100];
};

上述中定义的这个结构体叫做数据类型,但是在使用的时候,只需要写上

struct Student s;

这种写法即可。

通过一个具体的案例来进行结合使用

# include <stdio.h>

struct Student{
	int id;
	char name[100];
};

int main(void){
	struct Student s = {1,"guang"};
	printf("%d,%s\n",s.id,s.name);
	return 0;
}

查看控制台输出:

1,guang

还可以通过另外一种方式来进行赋值:

# include <stdio.h>
# include <string.h>  // 这里使用到了strcpy函数,所以需要进行导入
struct Student{
	int id;
	char name[100];
};

int main(void){
	struct Student s;
	s.id=99;
	// 一下两种是给字符串常量赋值的错误写法 
	//s.name="guang";
	//s.name={'a','b','c'};
	strcpy(s.name,"abc");
	printf("%d,%s\n",s.id,s.name);
	return 0;
}

这里是赋值方式。这里还有一种取出来值的方式,那就是通过指针来进行访问。

# include <stdio.h>
# include <string.h>
struct Student{
	int id;
	char name[100];
};

int main(void){
	struct Student s;
	s.id=99;
	strcpy(s.name,"abc");
	struct Student * p = &s;
	printf("%d,%s\n",s.id,s.name);
	printf("%d,%s\n",p->id,p->name); // p->id等价于*(p).id,又等价于s.id,这是在后面使用过程中的一种规范。
	return 0;
}

查看控制台输出:

99,abc
99,abc

所以使用结构体就有了两种最常用的方式:

struct Student s = {1,"guang"};

struct Student * p = &s;

第一种:s.id
第二种:p->id    

对于p->id的再解释:表示的是p变量所指向的这个结构体变量中的id成员

注意事项:

结构体变量不能够加减乘除,但是能够相互赋值;

结构体变量和结构体指针变量作为形参赋值问题:

下面来通过两个例子分别来对上面的两个注意事项来进行说明,最终我们会得出来一些使用上的结论:

第一个注意事项:

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

struct Student{
	int id;
	char name[100];
};
// 定义一个输出函数
void g(struct Student s);
int main(void){
	struct Student s;
	s.id=99;
	strcpy(s.name,"abc");
	printf("%d,%s\n",s.id,s.name);
	g(s);
	return 0;
}
void g(struct Student s){
	printf("%d,%s\n",s.id,s.name);
}

首先从定义的输出函数中入手,可以发现,在调用g()函数的时候,需要将整个结构体的字节数都给发送过去,整个结构体的字节个数肯定是大于104个字节了。

所以相比来说,如果发送指针过去,指针只有八个字节,那么效率上就变快了。之前看到过这段话好久了,但是一直没有好的例子来证明,所以这里来进行说明下。所以使用g()函数的时候,不推荐这种方式来进行使用,因为消耗内存。

所以将g()函数修改成g1()函数,改成以指针方式来进行输出:

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

struct Student{
	int id;
	char name[100];
};
void g(struct Student s);
void g1(struct Student * s);
int main(void){
	struct Student s;
	s.id=99;
	strcpy(s.name,"abc");
	printf("%d,%s\n",s.id,s.name);
	f(&s);
	printf("%d,%s\n",s.id,s.name);
	g1(&s);
	return 0;
}

void g(struct Student s){
	printf("%d,%s\n",s.id,s.name);
}

void g1(struct Student * s){
	printf("%d,%s\n",s->id,s->name);
}

转换成这种功能来进行使用是比较方便的。首先是节省了内存,体现出来指针强大的地方。

第二个注意事项:

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

struct Student{
	int id;
	char name[100];
};
// 定义一个修改函数
void f(struct Student * p);
int main(void){
	struct Student s;
	s.id=99;
	strcpy(s.name,"abc");
	printf("%d,%s\n",s.id,s.name);
	f(&s);
	printf("%d,%s\n",s.id,s.name);
	return 0;
}

void f(struct Student * p){
	p->id = 11;
	strcpy((*p).name,"guang");
}

查看控制台输出结果:

99,abc
11,guang

动态内存分配

为什么需要使用动态内存分配?如何来使用动态内存分配?

首先来举一个例子:

# include <stdio.h>
int main(void){
	
	int a[5] = {1,2,4,5};
	
	return 0;
}

对于上面的代码来说,这个数组是属于静态内存分配的。在程序运行阶段中,我们无法对这个数组的大小来进行修改了,但是有时候我们又需要来进行修改,所以静态内存无法做到。这里就相当于是ArrayList中的add方法,尽管我们指定了初始化容量大小,但是随着我们的add慢慢添加,底层的数组在进行动态扩充。如果容量不够了,再次分配新的内存来使用,将原来旧的值放入到新的数组中去。

所以大多数是为了程序在使用过程中,能够动态的来进行使用内存,扩充我们的内存。

使用:

# include <stdio.h>
# include <malloc.h>
int main(void){
	
	int a[5] = {1,2,4,5};
	int len;
	printf("请输入您要给数组分配的大小:\n");
	scanf("%d",&len);
	int * p = (int *)malloc(sizeof(int)*len);
	return 0;
}

解释上面的一段代码:int * p = (int )malloc(sizeof(int)len);为什么需要加上(int *)来进行强制转换

malloc(sizeof(int)*len)是向操作系统申请这么多的字节个数来供程序来进行读写。

又因为malloc函数只会返回向操作系统申请的malloc(sizeof(int)*len)内存的第一个地址。但是对于第一个地址来说,对于程序来说是没有任何的含义的。为什么?

假设请求分配的是8个字节,那么对于八个字节来说,对于操作系统来说,可以认为是double类型的一个数据类型占用的,也可以是2个int数据类型占用的,也可以是8个char数据类型占用的。所以对于操作系统来说,这里是存在着很大的问题的,因为不知道是哪种数据类型。而且保存到了指针变量里面之后,指针变量在进行操作的时候,无法确定是否存在着下一个地址或者说是不知道有几个元素的存在,按照上面所说的,如果是double,可能就只有一个;如果是int,那么就有两个;如果是char,就有8个,所以有歧义。

而加上了(int *)来进行强制类型转换,那么指针变量就知道在操作的时候,该去操作多少个内存单元中的01代码了。

但是我们用指针指向,不一定都需要来进行使用。因为这里只需要获取得到数组中连续空间的首地址即可。

举个例子:

# include <stdio.h>
# include <malloc.h>
int main(void){
	
	int a[5] = {1,2,4,5};
	int len;
	printf("请输入您要给数组分配的大小(至少大于等于2):\n");
	scanf("%d",&len);
	int * p = (int *)malloc(sizeof(int)*len);
	*p=666;
	*(p+1)=888;
	printf("数组中第一个和第二个元素的值是:%d,%d\n",p[0],p[1]);
	return 0;
}

查看控制台输出:

请输入您要给数组分配的大小(至少大于等于2):
5
数组中第一个和第二个元素的值是:666,888

对于静态内存分配来说,内存在作用域结束之后,就会被操作系统自动回收;但是对于动态内存来说,是不会被回收的,需要开发人员手动的来进行回收。

所以需要开发人员手动的来进行释放,使用free函数:

# include <stdio.h>
# include <malloc.h>
int main(void){
	
	int a[5] = {1,2,4,5};
	int len;
	printf("请输入您要给数组分配的大小(至少大于等于2):\n");
	scanf("%d",&len);
	int * p = (int *)malloc(sizeof(int)*len);
	*p=666;
	*(p+1)=888;
	printf("数组中第一个和第二个元素的值是:%d,%d\n",p[0],p[1]);
	free(p);
	return 0;
}

释放完成之后,就是告知操作系统,这块之前分配的操作系统不需要再来进行使用了,那么回收掉这块内存之后,后来的程序就可以向操作系统申请,然后使用了。

跨函数使用内存

首先来举两个例子来进行说明:

改变p变量指向变量的值,如下面的代码:

# include<stdio.h>

void f(int *);
int main(void){
	int i = 10;
	int * p =&i;
	f(p);
	printf("%d\n",*p);
	
	return 0;
}

void f(int * point){
	int j = 20;
	point = &j;
}

再来一个:

# include<stdio.h>

void f(int *);
int main(void){
	int i = 10;
	int * p =&i;
	f(p);
	printf("%d\n",*p);
	
	return 0;
}

void f(int * point){
	int j = 20;
	*point = j;
}

再来一个:

# include<stdio.h>

void f(int **);
int main(void){
	int i = 10;
	int * p =&i;
	f(&p);
	printf("%d\n",*p);
	
	return 0;
}

void f(int ** point){
	int j = 20;
	*point = &j;
}

这里具有相当高的迷惑性,为什么?这里会让我们误以为产生了一个效果:main函数中调用f()函数,main函数中的指针指向了f()函数中的内存,尽管在f()函数从调用到出栈。

所以有着一定的迷惑性。

正确的理解方式是:在函数调用过程中,如果调用了函数,那么在函数在栈空间中,方法中的局部变量的分配都是在栈空间的,只要方法一旦出栈,就会被OS进行回收。所以对于静态分配的变量来说,都是如此。

在百度问答上找到这样的一段提问代码:

为什么程序中输出*p的值为5?i为局部变量,当f();结束时i的空间应该被释放掉,i的值应为随机值, q = &i 等价于 p=&i,p的值不是应该为随机值吗?

# include <stdio.h>void f(int **q)
{
int i = 5;
*q = &i;
}int main(void)
{
int * p; f(&p);
printf("%d\n", *p); return 0;
}

第一种解答方式:

释放只是空间可以作为其他使用,其他程序使用后内存内容就不一样了,看修改的程序:

#include<stdio.h>
void f(int **q){
int i = 15;
*q = &i;
}
void ff(int **q){
int i = 12;
*q = &i;
}

int main(void){
int * p;
int * xx;
f(&p);
ff(&xx);
printf("%d\n", *p);
getchar();
return 0;
}

从这里可以理论上分析,得到的结果(*p)应该是15,但是结果是12。

因为ff函数覆盖了f函数,同时i变量的地址覆盖了。

#include<stdio.h>void f(int **q){
int i = 15;
*q = &i;
}
void ff(char **q){
char j = 12;
*q = &j;
}
int main(void){
int * p;
char * xx;
f(&p);
ff(&xx);
printf("%d\n", *p);
getchar();
return 0;
}

这段代码的出来的结果,是201326607,这个是一个垃圾结果。

第二种解答方式:

i是局部变量,也就意味着i是栈上的内存 栈上的内存不会被释放 只会被重复利用
f()函数结束后,i的内容可能无效,并不一定无效,这取决于你后续的操作是否覆盖了i的内存
你这里面仅仅是f()执行完立马输出,并没有其他操作覆盖i的内存 所以输出了5

但是无论怎么说,跨函数使用内存,如果调用的方法中的变量是静态分配的内存空间,那么将会导致方法一旦出栈了,栈空间的所有的变量数据都将会被回收了。所以在方法A调用方法B的时候,方法B在出栈了之后,方法A中就无法再次来使用方法B在内存中分配的内存空间了。但是这也仅限于是方法中的变量静态内存分配。

下面来看一下方法中的变量动态分配的,还在原来的基础之上来进行修正:

#include <stdio.h>
# include <malloc.h> 
void f(int **q){
	int * p = (int *)malloc(sizeof(int));
	*p = 123456;
	*q = p;
}
void ff(int **q){
int i = 12;
*q = &i;
}

int main(void){
int * p;
int * xx;
f(&p);
ff(&xx);
printf("%d\n", *p);
getchar();
return 0;
}

结果是123456

再对第二个来进行修改:

#include<stdio.h>
# include <malloc.h>
void f(int **q){
	int * p = (int *)malloc(sizeof(int));
	*p = 123456;
	*q = p;
}
void ff(char **q){
char j = 12;
*q = &j;
}
int main(void){
int * p;
char * xx;
f(&p);
ff(&xx);
printf("%d\n", *p);
getchar();
return 0;
}

对于这个来说,结果也是123456。不会因为方法结束,导致里面的内存被重复利用。

再者,动态分配的内存没有进行释放,即使是在方法中创建的,因为没有进行手动释放内存,那么在方法结束了,那么方法中开辟的内存空间也不会被释放,需要手动进行释放。

所以说以后不要相信静态内存分配,是相信动态内存分配。

typtedef给数据类型起别名的操作

posted @ 2021-08-09 00:50  写的代码很烂  阅读(75)  评论(0编辑  收藏  举报