关于C语言学习的部分遗漏问题解答

ZJ Lv100

浮点数的数值范围

赋值表达式与赋值语句

基础排序方法

汉诺塔问题

数组赋值

动态内存分配


浮点数的数值范围

浮点格式

精度和范围与数据的存储格式密切相关,所以我们先来看一看它们的存储格式:
对于float类型的变量,其底层的存储格式为:

符号位(S) 指数阶码(E) 尾数(F)
1bit 8bit 23bit

对于double类型的变量,底层的存储格式为:

符号位(S) 指数阶码(E) 尾数(F)
1bit 11bit 52bit

存储格式为 (-1)^s * F * 2^E

范围推导

float类型

首先看它的最大的情况,此时S=0,F全部是0,指数域全部是1,此时32bit的值为:0111 1111 1000 0000 0000 0000 0000 0000。

它的精确值为:+1.0*2^128,对应的十进制约为:3.4*10^38

负的无穷大就是当S=1的时候,符号取负即可。

double类型

直接给出的数表示的范围:−1.7*10^308 ∼ +1.7*10^308

总结

正常显示的只显示精度范围内的,但实际浮点类型可以存储相当大的数据,只是不会正常显示出来而已。

90c1d403d3c66315cc03416f9374711


赋值表达式与赋值语句

赋值表达式

简单赋值运算符和表达式:简单赋值运算符记为“=”。

由“= ”连接的式子称为赋值表达式。其一般形式为:

1
变量=表达式

例如:

1
2
3
x=a+b
w=sin(a)+sin(b)
y=i++

赋值表达式的功能是计算表达式的值再赋予左边的变量。赋值运算符具有右结合性。
因此

1
a=b=c=5

可理解为

1
a=(b=(c=5))

在其它高级语言中,赋值构成了一个语句,称为赋值语句。

而在 C 中,把“=”定义为运算符,从而组成赋值表达式。 凡是表达式可以出现的地方均可出现赋值表达式。
例如,式子:

1
x=(a=5)+(b=8)

是合法的。它的意义是把 5 赋予 a,8 赋予 b,再把 a,b 相加,和赋予 x,故 x 应等于 13。
在C语言中也可以组成赋值语句,按照C语言规定,任何表达式在其未尾加上分号就构成为语句。
因此如x=8;a=b=c=5;都是赋值语句。

赋值语句

赋值语句是由赋值表达式再加上分号构成的表达式语句。

其一般形式为:

1
变量=表达式;

在赋值语句的使用中需要注意以下几点:

  1. 由于在赋值符“=”右边的表达式也可以又是一个赋值表达式, 因此,下述形式 变量=(变量=表达式); 是成立的,从而形成 嵌套的情形。 其展开之后的一般形式为:变量=变量=…=表达式; 例如: a=b=c=d=e=5; 按照赋值运算符的右接合性,因此实际上等效于: e=5; d=e; c=d; b=c; a=b;

  2. 注意在变量说明中给变量赋初值和赋值语句的区别。 给变量赋初值是变量说明的一部分,赋初值后的变量与其后的其它同类变量之间仍必须用逗号间隔,而赋值语句则必须用分号结尾。 例如: int a=5,b,c;

  3. 在变量说明中,不允许连续给多个变量赋初值。 如下述说明是错误的: int a=b=c=5 必须写为 int a=5,b=5,c=5; 而赋值语句允许连续赋值。

  4. 注意赋值表达式和赋值语句的区别。 赋值表达式是一种表达式,它可以出现在任何允许表达式出现的地方,而赋值语句则不能。

下述语句是合法的:

1
if((x=y+5)>0) z=x;

这里(x=y+5)是赋值表达式,语句的功能是,若表达式x=y+5大于0则z=x。

下述语句是非法的:

1
if((x=y+5;)>0) z=x;

因为x=y+5;是语句,不能出现在表达式中。


基础排序方法

冒泡排序(两两互换)

冒泡排序是交换排序中一种简单的排序方法。

它的基本思想是对所有相邻记录的关键字值进行比效,如果是逆顺(a[j]>a[j+1]),则将其交换,最终达到有
序化;

其处理过程为:

(1)将整个待排序的记录序列划分成有序区和无序区,初始状态有序区为空,无序区包括所有待排序的记
录。

(2)对无序区从前向后依次将相邻记录的关键字进行比较,若逆序将其交换,从而使得关键字值小的记录向
上”飘浮”(左移),关键字值大的记录好像石块,向下“堕落”(右移)。 每经过一趟冒泡排序,都使无序区中
关键字值最大的记录进入有序区,对于由n个记录组成的记录序列,最多经过n-1趟冒泡排序,就可以将这n个记
录重新按关键字顺序排列。

原始的冒泡排序算法 对由n个记录组成的记录序列,最多经过(n-1)趟冒泡排序,就可以使记录序列成为
有序序列,第一趟定位第n个记录,此时有序区只有一个记录;第二趟定位第n-1个记录,此时有序区有两个记
录;以此类推,算法框架为: for(i=n;i>1;i—) { 定位第i个记录;}

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include<stdio.h>

int main()
{
int data[5];
printf("请输入数据:\n");
int i,j;
for(i = 0;i < 5;i ++)
{
scanf("%d",&data[i]); //输入数据
}

for(i = 0;i < 5;i ++) //采用双重循环
{
for(j = 0;j < 5 - 1 - i;j++)
{
if(data[j] > data[j+1]) //这里默认采用升序来排列
{
int temp; //定义一个临时值来存储数据,以此来达到交换数值的目的
temp = data[j];
data[j] = data[j+1];
data[j+1] = temp;
}
}
}

printf("排序好的数组为:\n");
for(i = 0;i < 5;i ++) //用for循环来遍历并输出排序完成的数组
{
printf("%d ",data[i]);
}
return 0;
}

选择排序

一、什么是选择排序?

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是:第一次从待排序的中数据元素选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。

二、选择排序思路

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

三、排序过程

例:定义一个数组 int a[8] = {9,3,7,2,6,1,5,8},要求利用选择排序的方法将数组从小到大排序。

排序的次数:因为每排好一个元素,那么所需要排的元素个数减一,直到排到倒数第二个元素停止,将倒数第二个元素也排好后,整体数组排序就完成了。所以排序的次数 = 元素个数 - 1。(冒泡排序的排序次数与该排序的排序次数计算方法相同)

9,3,7,2,6,1,5,8

第一次排序:假设首元素作为整体元素数据最小值,然后从该元素的后一个元素开始每个元素都与该最小值进行比较,假如有比该元素小的值,就用一个变量去记住下标值,最后比较完成后,把两个元素互换位置即可。

第一次排序结果:

1,3,7,2,6,9,5,8

第二次排序:因为第一次排序选择的是将首元素作为最小值,最终经过互换位置,首元素排序完成,第二次排序就不需要排序首元素,只需要排序除首元素以外的元素,然后在依照第一次排序的原理进行排序。

第二次排序结果:

1,2,7,3,6,9,5,8

然后根据第一次排序和第二次排序的原理,最终的排序结果为:{1,2,3,5,6,7,8,9}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>

void arr_out(int a[8])//输出函数
{
int i = 0;
for(i = 0;i < 8;i++)
{
printf("%d ",a[i]);
}
printf("\n");
}

void arr_sort(int *p,int n)
{
int i,j;
int min = 0;
for(i = 0;i < n - 1;i++)//排序次数
{
min = i;
for(j = i + 1;j < n;j++)
{
if(p[j] < p[min])
{
min = j;//记录交换的元素下标值
}
}
if(i != min)
{
int temp = p[i];
p[i] = p[min];
p[min] = temp;
}
}
}

int main()
{
int a[8] = {0};
int i = 0;
for(i = 0;i < 8;i++)
{
scanf("%d",&a[i]);
}

arr_sort(a,8);//排序函数
arr_out(a);//输出函数

return 0;

}

汉诺塔问题

==(递归)==

问题背景

汉诺塔问题是一个经典的问题。汉诺塔(Hanoi Tower),又称河内塔,源于印度一个古老传说。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,任何时候,在小圆盘上都不能放大圆盘,且在三根柱子之间一次只能移动一个圆盘。问应该如何操作?

3962f9684ac18a8b980c9b2f2ce63bb6

问题分析

思路:

1
2
3
4
5
使用的语言:C语言
使用的编译器:vs2019
参考书籍:谭浩强第四版
主要使用的知识:函数的递归
代码实现的思路主要分为三步:

假设总共需要移动n个盘子

F(n)=2F(n-1)+1 可以推到出需要移动2^n-1^次

1
2
3
将A柱上的n-1个盘子借助C柱移向B柱
将A柱上仅剩的最后一个盘子移向C柱
将B柱上的n-1个盘子借助A柱移向C柱

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

void move(int x, int y)
{
printf("%c->%c\n", x, y);
}
void hanoi(int n, char a, char b, char c)
{
if (n == 1)
{
move(a, c);
}
else
{
hanoi(n - 1, a, c, b);//将A座上的n-1个盘子借助C座移向B座
move(a, c);//将A座上最后一个盘子移向C座
hanoi(n - 1, b, a, c);//将B座上的n-1个盘子借助A座移向C座
}
}
//move中的实参与hanoi函数中的形参相对应,而hanoi函数中形参a,b,c所对应的值也是在有规律的变化
int main()
{
int n = 0;
scanf("%d", &n);
hanoi(n, 'A', 'B', 'C');
return 0;
}

数组赋值

直接赋值

数组赋值可以在定义的时候直接赋值,例:

1
2
char arr[]='hello';
char arr[10]='world';

循环赋值

在定义数组后,用循环进行对数组中的值进行赋值,例:

下标赋值

1
2
3
4
5
int a[10];
for(i=0;i<10;i++)
{
scanf("%d",&a[i]);
}

指针赋值

指针赋值结束后定义的指针会出现偏移而不是最开始指向的数组头地址,用p = &a[0];将指针改回去

1
2
3
4
5
6
int a[10];
int *p=a; //int *p=&a[0];
for(i=0;i<10;i++)
{
scanf("%d",p++);
}

动态内存分配

此处挪用CSDN博主的文章

malloc()

malloc()找到可用内存中一个大小适合的块。
内存是匿名的;

也就是说,malloc()分配了内存,但没有为它指定名字。
然而,它却可以返回那块内存第一个字节的地址
因此,可以把那个地址赋值给一个指针变量,并使用该指针来访问那块内存。
因为char代表一个字节,所以传统上曾将malloc()定义为指向char的指针类型。

然而,ANSIC标准使用了一个新类型:指向void的指针。这一类型被用作“通用指针”。
函数malloc()可用来返回数组指针、结构指针等等,因此一般需要把返回值的类型指派为适当的类型。

在ANSIC中,为了程序清晰应对指针进行类型指派,但将void 指针值赋值给其他类型的指针并不构成类型冲突。
如果malloc()找不到所需的空间,它将返回空指针。
我们使用malloc()来创建一个 数组。可以在程序运行时使用malloc()请求一个存储块,另外还需要一个指针来存放该块在内存中的位置。

例如,如下代码:

1
2
double * ptd;
ptd = (double * ) malloc (30 * sizeof(double));

这段代码请求30个double类型值的空间,并且把ptd指向该空间所在位置。

注意:ptd是作为指向一个double类型值的指针声明的,而不是指向30个double类型值的数据块的指针。

记住:数组的名字是它第一个元素的地址。

因此,如果令ptd指向一个内存块的第一个元素,就可以像使用数组名一样使用它。
也就是说,可以使用表达式ptd[0]来访问内存块的第一个元素,pd[1]来访问第二个元素,依此类推。
正如前面所学,可以在指针符号中使用数组名,也可以在数组符号中使用指针。

现在,创建一个数组有三种方法:
1.声明一个数组,声明时用常量表达式指定数组维数,然后可以用数组名访问数组元素。
2.声明一个变长数组,声明时用变量表达式指定数组维数,然后用数组名来访问数组元素(这是C99的一个特性)。
3.声明一个指针,调用malloc(),然后使用该指针来访问数组元素。

使用第二种或第三种方法可以做一些用普通的数组声明做不到的事:

创建一个动态数组(dynamic array),即一个在程序运行时才分配内存并可在程序运行时选择大小的数组。

例如,假定n是一个整数量。在C99之前,不能这样做:

1
double item[n]:/*如果n是一个变量,C99之前不允许这样做*/

然而,即使在C99之前的编译器中,也可以这样做:

1
ptd =(double*)malloc(n*sizeofdouble));/*可以*/

这行得通,而且正如您将看到的那样,这样做比使用一个变长数组更灵活。

一般地,对应每个malloc()调用,应该调用一次free()。
函数free()的参数是先前malloc()返问的地址,它释放先前分配的内存。

这样,所分配内存的持续时间从调用malloc()分配内存开始,到调用free()释放内存以供再使用为止。

设想malloc()和free()管理着一个内存池。
每次调用malloc()分配内存给程序使用,每次调用free()将内存归还到池中,使内存可被再次使用。

注意

  • free()的参数应是一指针,指向由malloc()分配的内存块;
  • 其他方式(例如声明一个数组)分配的内存是不能使用free()去释放的。

在头文件stdlib.h中有malloc()和free()的原型。(我不知道,哈哈)

通过使用malloc(),程序可以在运行时决定需要多大的数组并创建它。

程序清单12.14 举例证明了这可能。

它把内存块地址赋给指针ptd,接着以使用数组名的方式使用ptd。

程序还调用了exit()函数。该函数的原型在 stdlib.h 中,用来在内存分配失败时结束程序。

  • 值 EXIT_FAILURE 也在这个头文件中定义。标准库提供了两个保证能够在所有操作系统下工作的返回值:

    EXIT SUCCESS(或者,等同于0)指示程序正常终止;

    EXIT_FAILURE指示程序异常终止。

另外,有些操作系统,包括UNIX、Linux 和Windows,能够接受其他的整数值。

free()

在编译程序时,静态变量的数量是固定的:在程序运行时也不改变。

自动变量使用的内存数量在程序执行时自动增加或者减少。
但被分配的内存所使用内存数量只会增加,除非您记得使用free()。

如果不适当使用,容易造成内存泄漏(memory leak)

calloc()

内存分配还可以使用calloc()。

典型的应用如下:

1
2
long * newmen;
newmen = (long *) calloc (100, sizeof( long));

与 malloc()类似,calloc()在ANSI以前的版本中返回一个 char 指针,在ANSI中返回一个void指针。
如果要存储不同类型,应该使用类型指派运算符。
这个新函数接受两个参数,都应是无符号的整数(在ANSI中是size_t类型)。

第一个参数是所需内存单元的数量,第二个参数是每个单元以字节计的大小

在这里,long使用4个字节,因此这一指令建立了100个4字节单元,总共使用400个字节来存储。

使用 sizeof(long)而不是 4 使代码更具可易移植性。
它可在其他系统中运行,这些系统中 long不是4字节而是别的大小。

函数calloc()还有一个特性:相当于空数组初始化
它将块中的全部位都置为0(然而要注意,在某些硬件系统中,浮点值0不是用全部位为0来表示的)。

动态内存分配与变长数组

变长数组(Variable-Length Array,VLA)malloc()在功能上有些一致。例如,它们都可以用来创建一个大小在运行时决定的数组:

1
2
3
4
5
6
7
8
9
10
11
int valmal(){
int n;
int * pi;
scanf("%d", &n);

pi = (int *) malloc(n*sizeof(int));
int ar[n];//变长数组

pi[2] = ar[2] = -5;

}

一个区别在于 VLA 是自动存储的。

自动存储的结果之一就是 VLA 所用内存空间在运行完定义部分之后会自动释放。

在本例中,就是函数 vlamal()终止的时候。因此不必使用 free()。

另一方面,使用由 malloc()创建的数组不必局限在一个函数中。

例如,函数可以创建一个数组并返回指针,供调用该函数的函数访问。
接着,后者可以在它结束时调用 free()。
free()可以使用不同于 malloc()指针的指针变量:必须一致的是指针中存储的地址。

VLA 对多维数组来说更方便。
您可以使用 malloc()来定义一个二维数组,但语法很麻烦。
如果编译器不支持 VLA 特性,必须固定一维的大小,正如下面的函数调用:

1
2
3
4
5
6
7
8
9
10
11
int n = 5;
int m = 6;
int ar2[n][m];//n*m 的变长数组
int(* p2) [6];//在c99之前可以使用
int(* p3) [m];//要求变长数组支持

p2 = (int (*)6) malloc (n * 6 * sizeof(int));// n*6 数组
p3 = (int (*)[m]) malloc (n * m * sizeof(int));// n*m 数组
//上面的表达式也要求变长数组支持

ar2[1][2] = p2[1][2] = 12;

有必要查看一下指针声明。函数malloc()返回一个指针,因此p2必须是适当类型的指针。
下面的声明:

1
int(* p2) [6];//在c99之前可以使用

表明p2指向一个包含6个int值的数组。
这意味着p2将被解释为一个由6个整数构成的元素,p2[ i ][ j ]将是一个int值。

第二个指针声明使用变量来指定p3所指数组的大小。
这意味着p3将被看作一个指向 VLA 的指针,这正是代码不能在C90标准中运行的原因。

内存类与动态内存分配

您可能正在为存储类和动态内存分配之间的联系感到疑惑。

我们来看一个理想模型。
可以认为程序将它的可用内存分成了三个独立的部分:
一个是具有外部链接的、具有内部链接的以及具有空链接的静态变量的:
一个是自动变量的:
另一个是动态分配的内存的。
(静态变量):
在编译时就已经知道了静态存储时期存储类变量所需的内存数量,存储在这一部分的数据在整个程序运行期间都可用。

这一类型的每个变量在程序开始时就已存在,到程序结束时终止。

(动态变量):
然而,一个自动变量在程序进入包含该变量定义的代码块时产生,在退出这一代码块时终止。

因此,伴随着程序对函数的调用和终止,自动变量使用的内存数量也在增加和减少。

典型地,将这一部分内存处理为一个堆栈。

这意味着在内存中,新变量在创建时按顺序加入,在消亡时按相反顺序移除。

(动态内存分配):
动态分配的内存在调用malloc()或相关函数时产生,在调用free()时释放。

由程序员而不是一系列固定的规则控制内存持续时间,因此内存块可在一个函数中创建,而在另一个函数中释放。(malloc 可以跨函数调用)

由于这点,动态内存分配所用的内存部分可能变成碎片状,也就是说,在活动的内存块之间散布着未使用的字节片。

不管怎样,使用动态内存往往导致进程比使用堆栈内存慢。

  • Title: 关于C语言学习的部分遗漏问题解答
  • Author: ZJ
  • Created at : 2024-09-10 12:00:00
  • Updated at : 2024-12-11 01:22:23
  • Link: https://blog.overlordzj.cn/2024/09/10/C语言/基础部分/遗漏问题解答/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments