c语言补充知识点

本文详细介绍了C语言的基础知识,包括关键字extern、局部变量、全局变量、常量、数组、二维数组、函数参数、指针、字符串、运算符、逻辑操作、内存管理以及结构体等。特别强调了指针的使用、内存地址、类型转换和数组作为函数参数时的注意事项。还探讨了函数递归、内存区域以及汉诺塔和青蛙跳台阶问题。
摘要由CSDN通过智能技术生成

extern

用于声明外部符号的关键字,一个.c文件调用另外一个.c文件中的变量或函数时可以用到
使用该变量的源代码中需要引入头文件
然后需要声明一下,格式为 extern 变量类型 变量名
调用函数格式 extern 返回值 函数名(形参类型)

局部变量

作用域为变量声明所在的{}中,声明周期也就在其作用域结束时结束,局部变量不初始化,是随机值

全局变量

全局变量的范围是整个工程,其生命周期是整个程序的生命周期,全局变量不初始化默认是0

常量

c语言中的常量分为以下几种:

  • 字面常量:直接写出来的值,比如就写个3,100,3.14,这几个数字就都是字面常量
  • const修饰的常量:常属性,其修饰的变量就是常变量,不能被改动了,但其属性还是个变量。
  • define定义的标识符常量:define 定义的标识符常量是可以给数组分配大小的
  • 枚举常量:格式:
    enum 枚举类的名字
    { 枚举类型1,
    枚举类型2,
    枚举类型3
    };
    逐个打印,发现枚举类型的值是从0开始递增的整数
    调用单个值的格式:enum 枚举类名 变量名 = 某个枚举类型

数组

一维数组的创建: type_T arr_name [const_n];
//type_t 是数组的元素类型
//const_n 是一个常量表达式,用来指定数组的大小,不能是变量
-不完全初始化:int arr[10] ={1,2,3};容量是10,初始化3个,剩下的都默认初始化为0,用char不完全初始化数组,没被初始化的数组元素也默认为0,不过char arr[10]={‘a’,‘b’};和char arr[10]="ab"这两个不一样,第二个存入数组的实际上是‘a’,‘b’,‘\0’,两者虽然初始化后结果一样,都是a,b,0,0,0,0,0,0,0,0,但方式不同,要注意。字符串后面那个\0,会分配空间,但计算字符串长度时会被忽略。

数组大小必须是常量

例:int n=10;此时n是个变量,下一句int arr[n]={0};会报错,因为不能给一个变量来分配数组大小,即使在变量n加const依然不行。

数组大小的计算

字符串数组的大小:strlen()得出
其他数组的大小:sizeof(数组名)/sizeof(数组元素类型)

数组在内存中的存放

在这里插入图片描述
打印一个大小为10的整形数组地址,发现每个地址之间差四个字节,因为整形的大小就是4个字节,所以可以得出数组是连续排列的。

二维数组的初始化

以整形二维数组为例:三行四列的数组
int arr[3][4]={1,2,3,4,5,6},如果里面的元素仅用逗号隔开,且是不完全初始化,那么数组就会先填满第一行,然后再去填写下一行,没被初始化的位置就是0
int arr[3][4]={{1,2,3},{4,5,6}},如果用大括号括开,那么就相当于强制分行,1,2,3在第一行,第一行共有四个元素,第四个就是0,4,5,6在第二行,第二行也有四个元素,第四个也为0,第三行就全部都是0。
int arr[][]={1,2,3,4,5,6}二维数组是否能像一维数组一样不写下标,由元素来控制数组大小呢?显然这样是会报错的,实际上,二维数组必须要有列的下标,也就是第二个下标,如int arr[][4]这样就可以。
只写列下标,不完全初始化数组举例:
int arr[][4]={{1,2,3},{4,5,6,7},{8,9}};
打印出来是这样的
在这里插入图片描述
说明,只要指定了列数,输出初始化时加大括号就可以把数组分行,不够列数的那些行,都初始化0

二维数组在内存的存储

循环打印一个3行4列的整形二维数组的元素地址,得到如下结果:
在这里插入图片描述
可以看到,第一行四个元素连续排列,然后第二行第一个元素紧接着也连续排列,这样似乎就也是一种一维数组了。
在这里插入图片描述
实际物理存储的二维数组:
在这里插入图片描述

数组作为函数的参数

一般传入函数的数组名是数组的首元素地址(即arr = arr[0]),此外还需要在调用前算好数组的大小,并一起传入函数中。
数组名是首元素地址(有两个例外)
1.sizeof(数组名),计算的是整个数组的字节大小
2.&arr,取地址数组名,取出的是整个数组的地址
整个数组的地址,就是说取到的地址是数组作为一个整体,它的首地址,虽然地址的值和数组首元素的起始地址是一样的,但如果你对首元素地址指针+1,可以得到下一个元素的地址,但你如果对 取地址数组名 这样的方式取到的地址加一,你会发现地址增加了整个数组的大小这么多,即取到了数组结束后的那个内存位置的起始地址。代码示例如下:
在这里插入图片描述
在这里插入图片描述
可以看到,最后的地址变化值为FDF0~FE18,F0-E1=32,再加个8,就是40,而数组的字节大小刚好是10个整形,10*4byte=40,所以,取地址符加数组名的组合,取到的是整个数组的一个首地址。

字符串

由双引号引起来的一串字符称为字符串。注:字符串的结束标志是一个\0的转义字符,在计算字符串的时候\0是结束标志,不能算作字符串内容。
“”是空字符串

  • 字符串中有多个字符,所以可以用字符数组来保存字符串

第一种方法
char arr1[] =“abc”;
printf(“%s”,arr1);//这样便可以打印出abc

第二种方法:char arr2[]={‘a’,‘b’,‘c’};直接输出会显示abc烫烫烫,这是因为arr1是字符串,最后会补一个\0,而用三个单独的字符后面不会补\0,计算机就随机打印abc后面的一些值,而\0是字符串的结束标志,此时如果在arr2数组中加一个\0的ASCII码0,就可以正常打印abc。

注:计算机存储数据的时候,都是保存的二进制码,那#,v,$这样的字符就没办法保存了,所以就用一种通用的编码格式来代表每一种字符,这就是ASCII码
在这里插入图片描述
ASCII码常用的一些值:

  • 48-57:数字0~9
  • 65-90:大写字母A~Z
  • 97-122:小写字母a~z

strlen()

功能:计算字符串的长度,计算长度时会把\0字符串结束标志符当作计算停止的标志,但不会把\0计算在长度内。

转义字符

把原来的意思转变了,\n原本的n字母加了斜杠\后变成了换行字符

  • \\:用来表示一个反斜杠,防止他被解释为一个转义序列符
  • \'当用字符时想用‘作为字符内容,那么可以在’前加上\,使其在字符中只表示’ \"也是相同的道理
  • \t水平制表符 就是字符表示一个tab键
  • strlen计算带转义符的字符串长度时,不管转移后的字符有多长(比如\t水平制表符就看起来占好几个长度),都算作一个长度。
  • \ddd,表示1~3个八进制数字。比如\32,\132,等转义字符,是把斜杠后面的数字按每个位,以八进制转换为十进制,如32 就是 381+280=26然后找到其对应的ASCII值,就是最终的字符 ->,在计算字符串长度时\32,\132长度也算作1个长度
  • \xdd,表示两个十六进制数字。如\x61 转换成十进制,是6161+1160=97,找到ASCII对应的值是小写a,所以打印出一个小写a,在计算字符串长度时\x61长度也算作1个长度

操作符

算术操作符+ - * / %

1.除了%操作符之外,其他的几个操作符可以用于整数和浮点数
2.对于/操作符如果两个操作数都为整数,执行整数除法,而只要有浮点数执行的就是浮点数除法
3.%操作符的两个操作数必须为整数,返回的是整除之后的余数。

移位操作符 >> <<

计算机保存的是补码,所以下述移位操作符移动的都是补码
移位操作后赋值给其他变量,其他变量的值是移位后的值,被移位的变量值不变。

算术右移

右边丢弃,左边补原符号位,原来符号位也跟着一起移动,负数就在左边补1,正数就补0

逻辑右移

右边丢弃,左边补0

左移

左边丢弃,后面补0

位操作符 & | ^

先换成2进制位
& 按位与
| 按位或
^ 按位异或,相同为0,不同为1

赋值操作符

=

复合赋值操作符

+= , -= , *= , /= , &= , ^= , |= , >>=, <<=,计算顺序是左操作数和右操作数先进行左操作符的运算,结果再赋回给左操作数。

单目操作符

  • !逻辑反操作
  • - 负值
  • + 正值
  • & 取地址
  • sizeof 操作数的类型长度(以字节为单位),sizeof()括号内放的值是不参与运算的,即使里面有运算,其运算结果对外面的变量没有影响,sizeof返回的值是无符号值,如果一个变量要和sizeof进行运算,先要变成无符号数,如:int a=-1; if(a>sizeof(sizeof(int))),sizeof求int结果应为4,而a是-1,但此时要先将a转换为无符号数,-1的原码00000000 00000000 00000000 00000001,反码11111111 11111111 11111111 11111110
    补码11111111 11111111 11111111 11111111,这个补码转换为无符号数远大于4,所以这个if中判断的结果是a>>sizeof(int)
    比如b=0; int c=sizeof(b=a+5);printf(“%d\n”,b);打印出的b的值依然是0
  • ~ 对一个数的二进制按位取反,包括符号位,取反后的值是补码,如果要打印需要减1,符号位不变再取反
  • – 前置,后置-- (前后置和加加相同)
  • ++前置,后置++ 前置先加加,再使用,后置先使用,后加加
  • *间接访问操作符(解引用操作符)
  • (类型) 强制类型转换

逻辑操作符

逻辑与 &&
逻辑或 ||
和按位与,按位或不相同的是,逻辑与只关注左右两边是真还是假,非零即为真,零即为假,逻辑与运算如果左右都为真,结果就为1,逻辑或左右有一个真,即为真

逻辑与运算的套路

如果有多个逻辑与,第一个逻辑与左边的值如果为假,那么后面的所有值不管是什么都不计算。

int main()
{
int i=0,a=0,b=2,c=3,d=4;
i=a++ && ++b && d++;
printf("a=%d\n b=%d\n c=%d\n d=%d\n",a,b,c,d);
//打印结果1 2 3 4
return 0;
}

因为a为0,且a++是后置++,先运算,后自增,所以a是以0去参与逻辑与运算的,所以后面的++b,d++都不计算,只有a逻辑与算完后自增了1

int main()
{
int i=0,a=1,b=2,c=3,d=4;
i=a++ || ++b || d++;
printf("a=%d\n b=%d\n c=%d\n d=%d\n",a,b,c,d);
//打印结果2 2 3 4
return 0;
}

同样的代码,a改为1,逻辑与改为逻辑或,可以看到逻辑或一旦第一个值为1,后面的值也不会进行计算了

原码,反码,补码

只要是整数,内存中存储的都是二进制的补码
正数–原码,反码,补码,三码统一
负数-- 反码等于原码符号位不变,其他位取反,补码是反码+1
计算机在内存中保存的都是补码形式

大端,小端问题

大端模式:是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端模式:是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中

三目运算符

exp1?exp2:exp3;
判断exp1真假,为真,整个表达式结果为exp2的值,为假,整个表达式结果为exp3的值

逗号表达式

exp1,exp2,…expn;
逗号表达式,就是用逗号隔开的多个表达式。逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。

int a=1;
int b=2;
int c=(a>b,a=b+10,a,b=a+1)
//     a>b无结果,a=2+10,a表达式无结果,b=12+1,最后的表达式就是c的结果
c算出来是13

.和->操作符

  • .操作符用于 结构体变量.成员
  • ->操作符用于 结构体指针->成员

隐式类型转换

整形提升

c的整形提升就是说在进行整形运算时,表达式中的字符和短整型操作数在使用之前被转换为普通整形,这种转换称为整形提升。

  • 例:
    char a,b,c;

    a=b+c;
    b和c的值被提升为普通整形,然后再执行加法运算,加法运算完成之后,结果将会被截断,然后再存储于a中
整形提升是按照变量的数据类型的符号位来提升的

例:

int main()
{
	char a = 3;
	//00000000 00000000 00000000 00000011 3的二进制
	//截断为char 00000011 挑最低位的截断
	char b = 127;
	//00000000 00000000 00000000 01111111 127的二进制
	//截断为 01111111
	char c = a+b;
	//00000011 -a
	//01111111 -b
	//a和b如何相加
	//看符号位来补高位,a和b都是正数,所以高位都补0
	//00000000 00000000 00000000 00000011 -a
	//00000000 00000000 00000000 01111111 -b
	//00000000 00000000 00000000 10000010 a+b结果
	//再一次截断  10000010 -c
	printf("%d\n",c);
	//打印的是整形,所以要再一次整形提升,看符号位是1,所以补1
	//11111111 11111111 11111111 10000010 -整形提升后的c,是补码形式
	//打印的值是原码,所以我们想看看打印的数,要转换为原码看
	//11111111 11111111 11111111 10000001 反码是补码减1
	//10000000 00000000 00000000 01111110 原码 是-126
	return 0;
}

==,+这些符号都会进行整形提升,如果是用char类型进行运算,先观察符号位,然后提升成32位整形,运算时的变量和其原本的值是不相等的
char c =1;
printf(“%d\n”,sizeof©); // 1
printf(“%d\n”,sizeof(+c)); // 4
printf(“%d\n”,sizeof(!c)); //1
+c因为参与了加法运算,被整形提升为int型

算术转换

long double
double
float
unsigned long int
long int
unsigned int
int
下面的和上面的进行运算时,先转换成上面的,然后再进行运算

一些关键字

register

格式:register 变量类型 变量名;
由于计算机中寄存器数量有限,该语句不能保证变成寄存器变量,而是由编译器决定是否可以,该语句起到一个建议的作用。

static

  • 当 static修饰局部变量时,局部变量的生命周期变长
  • 当static修饰全局变量时,改变了变量的作用域,让静态的全局变量只能在自己所在的源文件内部使用,出了源文件就无法使用。
  • static修饰函数,改变了函数的链接属性,普通的函数具有外部链接属性,使用static修饰后,外部链接属性就变成了内部链接属性。

struct结构体

为了表达复杂对象,需要将多个基本类型组合起来表示,于是便有结构体类型。
声明格式:
struct 结构体名
{
char name[20];
short num;

};
struct tag
{
member-list;
}variable-list;
variable-list是变量名,写在这个位置创建出来的也是结构体变量,和在main函数中用 struct Stu a1这样的方式创建出的变量相同,都是结构体变量,只不过写在variable-list这个位置的是全局变量。
主函数中调用:
struct 结构体名 变量名={按照顺序和类型依次初始化值}
通过 结构体变量名.结构体中的类型 这样的格式就能使用和修改结构体里的各项值了,注意,要修改char数组类型(也就是字符串类型)需要用到strcpy()函数,因为直接用结构体变量.成员=“xxx”;这样不行,是因为char数组的名字是一个地址,所以不能直接用一个字符串赋值给一个地址,修改char数组里面的字符串,所以必须要用strcpy()库函数,记得加头文件string.h。

  • struct指针格式: struct 结构体名* 指针名=&结构体对象名;
  • 使用指针去调用结构体对象的值
    (*指针名).属性1;
    (*指针名).属性2;
    就是解引用让指针找到指向的对象然后根据结构体.操作符获取值
    还有一种方法:就是使用箭头
    指针名->结构体中的属性1
    指针名->结构体中的属性2
    指针指向结构体变量,而箭头则可以直接指向其中的属性,这方法更常用且直观。

typedef类型定义/类型重定义

就是将类型名重定义为一个别名,下次直接用别名就能代表这个类型
typedef用于结构体
typedef struct Stu
{
char name[20];
short age;
char tele[13]
char sex[5];
}Student;
再调用struct Stu 这个结构体类型去创建变量时,就用Student stu1这样的格式就可以了

结构体传参

void function(struct Stu* s)
{
调用结构体中的变量时可以 s->结构体中的成员名
}
传指针给函数更加节省空间

union

volatile

define

除了标识符常量,还可以定义宏–带参数
例:用宏定义一个两数求最大值的功能
#define MAX(X,Y) (X>Y?X:Y)
使用时int max = MAX(a,b);//此处的MAX(a,b)被替换为后面的(X>Y?X:Y)

指针

内存和内存地址

要想了解指针,首先要了解内存和内存地址,我们去电脑城常说要买4G,8G,16G内存的电脑,这是内存的大小。此外呢,计算机中还有地址线/数据线,这个地址线的数量就对应了你计算机是32位计算机还是64位计算机,假设是32根地址线,每根地址线正电表示1,负电表示0,一共32位可以构成一个二进制数,这个二进制数表示地址,所以不考虑正负,因此32位中无符号位,那么内存地址从0000 0000 0000 0000 0000 0000 0000 0000(32个0位)到1111 1111 1111 1111 1111 1111 1111 1111就有232个内存编号,每一个内存编号去对应一个内存空间,这个内存空间可以看作顺序排列的一个个单元格,每个格子代表一个内存空间,既然是一个个内存空间,那么肯定有大小,假设一个内存空间为8位1比特(实际上每个内存空间也是按照一个字节来划分的),那么所有的内存空间的大小就为232*8bit,化成GB就是4GB,也就是我们买电脑时说要买4GB内存电脑的由来.
但实际情况比这个要复杂,32位机器其地址线可能也大于32位,64位机器地址线远没有64位,首先是用不到这么多地址,其次地址太长也会对寻址造成麻烦,该内容后续学习计组还需要补充。

指针变量

有一种变量是用来存放地址的,这种变量就叫做指针变量。
写法 :
int a =10; //定义一个int型变量
int* p = &a;//取地址符&取出a的地址,然后把值赋给指针变量p
*p =20;//*p解引用后就是a的值,被修改为20
p则是使用解引用操作符,找到指针指向的变量的值
解释:a作为一个新定义的int型变量,它有自己的地址和值,而定义的指针变量也是一个变量,它也有自己的地址和值,而它的值就是a的地址,它自己也有一个内存地址,*p实际上是通过p中的值(也就是a的地址)找到a后解除引用后得到的a自己的值。

指针变量的大小

指针变量的大小可以这样考虑,指针变量保存的是一个地址,那32位平台保存的就是一个32位的地址,那么就是32个bit位,转换成字节就是4字节,因此,在32位平台上,一个指针的大小就是32bit/8 =4字节,在64位平台大小就是64bit/8=8字节

指针的一些疑问

1.如果说一个指针只是保存地址,地址都是32位或64位,那么为什么不同类型的变量有不同类型的指针呢?固定一个指针类型然后保存地址不行吗?
答:在进行解引用操作时会出现问题,如果一个int型变量,用char* 指针去取地址保存,保存的地址依然是int型变量的起始地址,但是一旦解引用操作,修改指针指向变量的值时,int指针解引用会修改4个字节的值,也就是int型变量的长度,这是正确的,但如果是char指向该int型变量,再解引用修改值,就只会修改第一个字节的值(大端小端问题,可能是手写出来的32位中的前8位或后8位)。总的来说,无论什么类型的指针变量,指到的都是所指变量的首地址,但每种指针可以操控的变量长度不同,导致这样操作会出问题。

第二个原因,在对指针进行加减数操作时,不同类型的指针也会有不同的结果,假设pa是一个int指针,对它进行+1,printf(“%p\n”,pa);和printf(“%p\n”,pa+1);可以发现,pa指向的变量和pa+1指向的变量地址相差4个字节,而用char指针去指向int类型的变量,再对char* 指针+1,两者指向的变量地址就只相差1个字节,也就是说,指针类型决定了指针走一步走多远
int* p;p+1–>4
char* p;p+1–>1
double* p;p+1–>8
这就是指针变量的类型必须和指向的变量类型对应的原因。

野指针

概念:野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)
成因:1.没有初始化,指针会被随机在内存中找个值赋值,如果你再解引用给它指向的地址内容赋值,就很危险
2.指针越界访问,如果一个数组,有10个元素,你非要循环10+次,每次让指针指向的地址自增,一旦指向数组这10个元素后面的值,就是越界了,就算野指针。
3.指针指向内存空间释放

int * test()
{							//这段问题在于,函数调用是创建的临时变量,在函数调用完成后就会被销毁
	int a =10;				//返回给操作系统去使用,但返回的地址被赋给了指针p,修改指针p的值
	return &a;				//而这个地址指向的内存空间已经不属于你的这个程序了,不能修改
}
int main()
{
int *p =test();
*p = 20;
return 0;
}

如何规避野指针

1.指针初始化:如果不知道如何初始化,就给这个指针赋值NULL,NULL实际上是 (void ) 0 0强制转换为void
2.小心指针越界
3.指针指向的空间释放时要置为NULL
4.指针使用之前检查其有效性 (if p!=NULL)使用时判断指针是否为空指针,如果不空再使用

指针运算

指针加减整数

实际上是指针指向的地址在增减,增减的幅度取决于是指向什么类型的指针,int指针+1,实际上地址增加四个字节,char指针+1,地址增加1个字节

#define N_VALUES 5
float values[N_VALUES];
float *vp;
for(vp=values[0];vp<&values[N_VALUES];)
{
	*vp++=0;//后置++运算符的规则是先使用,后++,所以解引用操作符和赋值操作符先起作用,把*vp=0先完成,然后vp指针的值+1,指向数组中下一个浮点数元素
}

指针相减

#include<stdio.h>
int main()
{
	int arr[10]={1,2,3,4,5,6,7,8,9,10};
	printf("%p\n",&arr[9]);//0061FE9C
	printf("%p\n",&arr[0]);//0061FE78  两个取地址值相差36
	printf("%d\n",&arr[9]-&arr[0]);//但两者相减,打印出是9
	return 0;
}

所以由此可得,指针相减,得到的是两个指针之前包含的元素个数

指针的关系运算

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较

二级指针

int a=10;
int* pa = &a;//一级指针
int** ppa = &pa;//二级指针
在这里插入图片描述
**ppa ,*ppa二级指针解引用,两个星号指向的是一级指针指向的变量,一个星号指向的是一级指针内的地址值

指针数组和数组指针

  • 指针数组 :是一个数组,存放的变量是指针
  • 数组指针:是一个指针

分支和循环

if相关的几种语句

  • 单分支语句
    if(表达式)
    语句;
    如果表达式为真,那么执行语句,如果表达式不为真,那就什么都不执行
  • 双分支语句
    if(表达式)
    语句1;
    else
    语句2;
    如果表达式为真,那执行语句1,如果表达式不为真,那就执行else下的语句2,无论如何得执行一个
  • 多分支语句
    if(表达式1)
    语句1;
    else if(表达式2)
    语句2;
    else
    语句3;
    可以实现多个选择,else if的情况有多少都可以,最终也是必须执行一个

if,else if,else之后的语句如果是单行,直接缩进即可,如果是多行,需要大括号括起来。

悬空的else

else会找离他最近的未匹配的if匹配,如果一个else上面有两个未匹配的if,而else想和前一个if匹配,就需要把第二个if用大括号括起来

在写if的判断条件时,很容易将赋值语句=当作判断相等语句==,一定要注意

switch case

switch 语句用于实现多分支的情况
判断switch中的值只能用整形变量
switch(整形表达式)
{
case后面的值必须是整形常量表达式,不能用变量,且case匹配到值后会顺序执行到最后一个情况,所以要想只输出一个结果,需要在后面加break
case 1 操作;break;
case 2 操作; break;

default:操作;break;
}
case和default没有严格顺序,但一般把default放最后

getchar(),putchar(),EOF

  • getchar()获取键盘输入的字符 当getchar输入一个ctrl+z时,获取到一个EOF,获取到EOF可以当作判断循环结束的条件
    getchar()和scanf()都是输入函数,它们都是会监视一个叫做输入缓冲区的内存空间,当输入缓冲区没有东西时,就会等待。我们输入时,一般会按一下回车(\n)表示输入结束,scanf会把第一个回车或空格前的输入内容读走,然后把空格或\n以及之后的内容留下,此时如果后面再用getchar()时,输入缓冲区里就会有之前输入剩下来的值,而不是我们想要的新输入getchar函数的内容。解决办法,在scanf()后面加个
    while((ch=getchar())!=‘\n’)
    {
    ;//这是个空语句
    }
    代码意思就是只要读到的不是\n,那就一直读,直到清空输入缓冲区,
  • putchar()输出字符
  • EOF end of file 值是-1

for循环

while循环如果需要调整循环条件,只需要调整三个地方,循环变量初始化的值,while后的()循环跳出条件,循环变量自增,但当代码很多时,这三条语句相隔很远,修改起来比较麻烦,于是for循环便解决了这个问题,把三个需要更改的地方都放在最开头,便于修改。
注意:

  • 不可在for循环体内修改循环变量,防止for循环失去控制
  • 建议for循环的循环控制变量采用前闭后开区间的写法

for循环的变种

  • for(;😉:死循环,不要随便省略判断条件,会造成意想不到的错误

一个面试题

#include<stdio.h>
int main()
{
	int i=0;
	int k=0;
	for(i=0,k=0;k=0;i++,k++)
		k++;
	return 0;
}

上述的代码在for循环的判断条件中,第二个循环次数判断的地方由于只是个赋值语句,k=0,而在c语言中,0代表假,非0代表真,此时给k赋值为0就代表假,所以这个循环一次都不会运行。
如果此时给k赋值一个非0的数,那这个循环就是死循环。
(注意判断相等==和赋值=!!!!)

计算n的阶乘时犯的错误

#include<stdio.h>
int main(){
	int n=0;
	scanf("%d",&n);
	int sum=0;
	int i=0;
	for(i=1;i<=n;i++){
		if(i=1)
		{
			sum=1;
			printf("first loop%d\n",sum); 
		}
		else{
			sum=sum*i;
			printf("normal loop%d\n",sum);
		}
	}
	printf("final answer %d\n",sum);
	return 0;
} 

这段代码一开始运行后,光标一直在闪动,但没有输出,后面在每个关键点加了输出,发现一直在first loop死循环,仔细检查发现if(i=1)这里,使用了赋值语句,造成i恒等于1,赋值和判断相等有一次犯了错。。还有计算阶乘时如果n输入100,会直接输出0,因为int变量有最大取值,出现了溢出

判断100~200中是否有素数的代码优化

最初的代码实现:

int main()
{	
	int count;
	for(int i=100;i<201;i++)
	{
		//判断i是否为素数
		int j=0;
		for(j=2;j<i;j++)
		{
			if(i%j==0)
				break;
		} 
		if(j==i)
		{
		printf("找到一个素数:%d\n",i);
		count++;
		}
	}
	printf("共有%d个素数",count);
	return 0;
}

可以看到,内部的循环每次都要从2循环到这个数本身这么多次,然而判断素数可以有这样一直方法:假设一个数a,不是素数,那么他一定可以由两个数b和c相乘得到,而b和c的其中一个数,一定小于等于开平方a,最多就等于开平方a,比如16=28,2小于16开平方,16=44,4等于开平方16.
所以,内层循环的上限可以降低为要被判断的数开平方,就需要用到sqrt()这个函数,修改后的代码实现如下:

int main()
{	
	int count;
	for(int i=100;i<201;i++)
	{
		//判断i是否为素数
		int j=0;
		for(j=2;j<=sqrt(i);j++)
		{
			if(i%j==0)
				break;
		} 
		if(j>sqrt(i))
		{
		printf("找到一个素数:%d\n",i);
		count++;
		}
	}
	printf("共有%d个素数",count);
	return 0;
}

头文件里要加#include<math.h>
此时还可以再优化,第一个for循环里,100~200的数中,偶数不可能是素数,所以for循环还可以再优化,如下

int main()
{	
	int count;
	for(int i=101;i<201;i+=2)
	{
		//判断i是否为素数
		int j=0;
		for(j=2;j<=sqrt(i);j++)
		{
			if(i%j==0)
				break;
		} 
		if(j>sqrt(i))
		{
		printf("找到一个素数:%d\n",i);
		count++;
		}
	}
	printf("共有%d个素数",count);
	return 0;
}

不得不说代码优化还是挺有学问的,一点点就可以优化许多时间

srand()和rand()

头文件#include<time.h>
rand()可以返回一个随机数,大小为0~32267
但是rand()每次都需要一个起点,然后根据这个起点去变换随机值,如果不设置,那么每次随机生成的那几个数都是一样的,所以需要使用srand()函数去设置随机数起点,srand(unsigned int),传入的值必须是个无符号整形,于是我们考虑到传入时间戳,因为时间戳是一直变化的,srand((unsigned int)time(NULL));这句话就在主函数最开头调用一次就能保证每次随机数起点不一样,否则每次调用时间差不多,随机值也差不多。

时间戳

时间戳的值是现在的时间(到秒)减去1970年1月1日0时0分0秒得出的值换成秒,头文件#Include<time.h>,time函数格式:time_t time(time_t* timer); time_t实际上是个long类型

goto语句

可以跳转到某一行,但一般不建议使用,容易破坏程序。
但有些场景还是会使用到goto语句,比如深层循环要跳出时,一个goto就可以跳出,不然可能需要多个break

关机语句

c语言中,system()函数是执行系统命令的
system(“shutdown -s -t 60”);
一分钟后关机指令
system()函数的头文件为stdlib.h

strcmp()函数

比较两个字符串是否相等的专用函数,相等返回0,头文件string.h

函数

c语言中函数的分类

1.库函数
c语言中常用的库函数有:
- IO函数
- 字符串操作函数
- 字符操作函数
- 内存操作函数
- 时间/日期函数
- 数学函数
- 其他库函数
2.自定义函数

memset()函数

格式:void* memset(void* ptr,int value,size_t num);
传入一个value,从指针ptr指向的位置开始num个数,替换为value
头文件string.h

函数传值

函数传值,如果只是要获取一个数值结果,那可以直接传参数的类型,但是要修改变量,比如交换两整形数的值,只是传两个整形进去是无法改变的,因为传参会把变量复制出来,赋给函数参数列表中的两个临时变量,函数运行完后就销毁了,原本那两个变量还是没改变,因此,要传指针进函数。
swap(int* x,int * y);//函数声明接受指针
*x,*y解引用去修改里面的值
调用时:swap(&a,&b);//传地址进函数

形参和实参

形参就是函数声明时括号里的变量,实参是实际调用时传进函数的值,实参在传入时必须是确定的值。当实参传给形参的时候,形参其实是实参的一份临时拷贝,对形参的修改是不会改变实参的,这种调用叫做传值调用
但传入的是指针就不一样了,可以建立函数外部变量和函数内部的联系,

传数组进函数

数组名实际上是一个指针,但要想在函数中调用数组中的元素,传入整个数组这样太浪费空间,于是c语言的办法是,传入数组名,也就是首地址。然后再加数组的大小,先算好再传入函数,这样知道了大小后,在函数中操作数组就方便了

函数的声明

如果在一个源文件中,先写主函数再写其它函数,调用时编译器会提示warning函数未定义,所以一般函数直接写在主函数前。而函数声明真正使用到的地方是在头文件(.h)中声明,头文件写声明,源文件写实现,主函数源代码中要用该函数需要#include"头文件.h"

避免多次引用头文件

include<stdio.h>引用原理是把stdio.h文件的所有代码拷贝过来,如果在一个项目中,像stdio.h这类库函数,甚至是我们自己写的一些函数,都会被多次调用,这样就有代码重复的可能性,所以通过在头文件加入以下语句,让引入的头文件只被调用一次
#ifndef 大写文件名__H
#define 大写文件名__H
头文件代码…
#endif

关于一直递归会停止有关知识点(内存空间,栈,堆,静态区)

函数,变量这些都是需要内存分配空间才可以执行的,那么在内存中,也有一些存放区域的类别
一共有三种,栈区,堆区,静态区
局部变量,函数形参,函数调用----存放在栈区
动态分配的内存,就比如malloc/free,realloc,calloc等分配出来的内存被放在堆区
全局变量,static修饰的变量,则放置在静态区
递归函数不断调用自己,其函数形参和函数内部的一些语句则一直在栈区申请内存,而栈区是有大小的,如果递归一直在栈区申请空间,最终会造成栈溢出(stack overflow)

递归的必要条件

1.存在限制条件,当满足这个条件时,递归就不再继续
2.每次递归调用之后越来越接近这个限制条件
不是说满足这个条件就不会栈溢出,写个会嵌套10000次的递归调用,虽然满足两个条件,但还是会报错栈溢出。

汉诺塔问题和青蛙跳台阶问题

(后续补充)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值