C语言_分支语句、循环语句、函数初步

bool类型

bool只有true 和false;在C语言中0是false,其他情况(非0)都为true。
在.c文件中需要引入头文件<stdbool.h>;在.cpp文件中直接使用。
注意:VS2012不完全支持C99标准,不能引入头文件stdbool.h 。但文件后缀为.cpp可以直接使用bool类型。

也可以自己构造bool 类型(但没必要,因为cpp文件下可以直接用)。

构造bool类型代码示例

1
2
3
4
5
6
7
8
9
10
// test.c 文件 注意文件后缀是 C 文件。
#include<stdio.h>
typedef int bool;
#define true 1
#define false 0
int main()
{
bool xtag = true;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
int main()
{
bool x = false;
printf("%d \n",x);//0
++x;
printf("%d \n",x);//1
++x;
printf("%d \n",x);//1
++x;
printf("%d \n",x);//1
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
int main()
{
bool x = false;
printf("%d \n",x);//0
--x;//0 --> -1 转为1
printf("%d \n",x);//1
--x;//1 --> 0
printf("%d \n",x);//0
--x;//0 --> -1 转为1
printf("%d \n",x);//1
return 0;
}

关系表达式

关系表达式运算结果是bool值。关系运算符都是双目运算符,其结合性均为左结合。关系运算符的优先级低于算术运算符,高于赋值运算符。在六个关系运算符中,<<=>>=的优先级相同,高于==!===!=的优先级相同。
需要特别注意:== 才表示等于比较,而 = 表示赋值,大家要注意区分,切勿混淆。

image-20210715132315085

该死的=

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
int main()
{
int age = 0;
scanf("%d",&age);
if(age = 3) //if(age==3) //if(3==age)
{
printf("该上幼儿园了 \n");
}
else
{
printf("hehe \n");
}
return 0;
}

此例中if圆括号内的表达式为"age=3",是一个常量赋值给变量的操作,一定会通过导致表达式结果值为1,所以无论在外输入什么此处判断都会为true。这是因为少打了一个=号造成等值判断误成为了赋值语句。为了规避这个错误,我们应该在使用等值判断语句时尽量把常量放在左边,把待比较的变量放在右边。这样的话,如果写成"3=age"后,编译时期即会报错,而不是把错误延续给运行时期!

逻辑表达式

逻辑表达式运算结果是bool值。

image-20210715132551231

与运算(&&)

又称截断与、简洁与。参与运算的两个表达式都为真时,结果才为真,否则为假。

或运算(||)

又称截断或、简洁或。参与运算的两个表达式只要有一个为真,结果就为真;两个表达式都为假时结果才为假。

非运算(!)

参与运算的表达式为真时,结果为假;参与运算的表达式为假时,结果为真。

优先级

逻辑运算符和其它运算符优先级从低到高依次为:
赋值运算符(=) < &&|| < 关系运算符 < 算术运算符 < 非(!)
&&||低于关系运算符!高于算术运算符

分支语句

双分支语句加几行代码变单分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
int main()
{
int a=0,b=0;
int max=0;
scanf("%d %d",&a,&b);
if(a>b)
{
max = a;
}else
{
max = b;
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
int main()
{
int a=0,b=0;
int max=0;
scanf("%d %d",&a,&b);
max = a;
if(max<b)
{
max = b;
}
return 0;
}

三目运算符替代简单的if语句

1
2
3
4
5
6
7
8
9
#include<stdio.h>
int main()
{
int a=0,b=0;
int max=0;
scanf("%d %d",&a,&b);
max = (a>b)?a:b;
return 0;
}

if 语句在某些情况下可以用条件运算符“?:”来简化表达。“ ? :”是一个三元运算符,其构成的表达式格式为:<表达式1> ? <表达式2> : <表达式3>;执行逻辑:先计算表达式1,若其值为真(或非0),则计算表达式2(不计算表达式3),并将该值作为整个表达式的值;反之,即表达式1 的值为假或为0,则计算表达式3(不计算表达式2),并将该值作为整个表达式的值。

if-else多分支语句

判断字符类别

判别键盘输入字符的类别,是否是数字字符,是否是小写字符,是否是大写字符,还有其它字符。

1
2
3
4
5
//输入字符给变量有两种写法。
char ch;
scanf_s("%c",&ch);//a
ch=getchar();
//以上两种写法是等效的。

相应的头文件<ctype.h>

函数
字符分类
  1. isalnum(char);判断一个字符是否是字母或数字
  2. isalpha(char);判断一个字符是否是字母
  3. islower(char);判断一个字符是否是小写字母
  4. isupper(char);判断一个字符是否是大写字母
  5. isdigit(char);判断一个字符是否是数字
  6. isxdigit(char);判断一个字符是否是十六进制数字字符(0123456789abcdefABCDEF
  7. iscntrl(char);判断一个字符是否是控制字符
  8. isspace(char);判断一个字符是否是空白字符
  9. isblank(char);判断一个字符是否是空格字符(C99
  10. ispunct(char);判断一个字符是否是一个标点符号
字符操作
  1. tolower(char);将字符转换成小写
  2. toupper(char);将字符转换成大写

良好的代码风格

image-20210716113008168

switch多分支结构

函数初步

在结构化程序设计中,函数是将任务进行模块划分的基本单位。通过函数,可以把一个复杂任务分解成为若干个易于解决的小任务。充分体现结构化程序设计由粗到精,逐步细化的设计思想。一个大的程序一般应分为若干个程序模块,每个模块实现一个特定的功能,这些模块称为子程序,在C语言中子程序用函数实现。

什么时候我们认为模块是足够小的:功能是单一的。

image-20210714181921261

按是否系统预定义分两类

编译系统预定义

一类是编译系统预定义的,称为库函数或标准函数,如一些常用的数学计算函数、字符串处理函数、图形处理函数、标准输入输出函数等。这些库函数都按功能分类,集中说明在不同的头文件中。用户只需在自己的程序中包含某个头文件,就可直接使用该文件中定义的函数。

  1. <asserst.h>
  2. <ctype.h>
  3. <math.h>
  4. <stdio.h>
    把函数名字、函数功能记下来。

用户自定义

另一类是用户自定义函数,用户可以根据需要将某个具有相对独立功能的程序定义为函数。
自定义函数有:函数返回类型 + 函数名 + 形参列表 + 函数体构成

函数的命名要求

  1. 拿英文命名函数
  2. 第二个要求:见名知义,不要用汉语拼音,有歧义。

函数的声明、定义注意事项

原则
  1. 需要外部输入的(比如scanf)写到形参中;
  2. 需要打印、输出的,return返回。
该死的形参
形参变量类型名后的标识符要不要省
  1. 函数的声明中形参列表可以省去形参名(标识符),但不能省去类型名。因为虽然函数不识别名称,但必须识别类型;
  2. 而函数定义就要把形参名写全,因为函数体中要操作之;
  3. 函数的调用中,参数前不能加类型名。
形参变量的定义必须每个参数都有一个类型和一个名称

形参变量的定义与局部变量定义是有区别的。局部变量是可以int x,y;这样定义的,但形参定义不可以,必须是一个类型匹配一个名称!

函数声明、定义后的分号

函数声明是一个语句,所以要加分号。但是定义函数完成后花括号后加分号也没影响,因为那是个空语句,但也没必要!

一定要在函数被调用前声明或定义

被调用函数要在调用者调用它之前的区域声明或定义,不然编译是不会通过的!

不允许函数的嵌套定义

C语言中不允许函数的嵌套定义,即在一个函数中定义另一个函数。

函数的调用是允许嵌套的

image-20210716124810268
image-20210716124758735

示例

image-20210714182226592

定义函数时可能会涉及若干个变量,究竟哪些变量应当作为函数的参数?哪些应当定义在函数体内?这有一个原则:
作为一个相对独立的模块,函数在使用时完全可以被看成 “黑匣子”,除了输入输出外,其他部分可不必关心。从函数的定义看出,函数头正是用来反映函数的功能和使用接口,它所定义的是“做什么”,在这部分必须明确“黑匣子”的输入输出部分,输出就是函数的返回值,输入就是参数。因此,只有那些功能上起自变量作用的变量才必须作为参数定义在参数表中;函数体中具体描述“如何做”,因此除参数之外的为实现算法所需用的变量应当定义在函数体内。

形参和实参

形式参数(形参)

只能等到函数被调用时接收传递进来的数据,所以称为形式参数,简称形参。

形式参数是指函数名后括号中定义的变量,形式参数只有在函数被调用的过程中给于赋值(分配存储空间)。函数执行完后形式参数变量就自动释放了,所以形式参数只在函数中可见(作用域)。

实参(实际参数)

调用函数时给出的参数包含了实实在在的数据,所以称为实际参数,简称实参。

实参可以是:常量、变量、表达式或函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。

功能

形参和实参的功能是传递数据,发生函数调用时,实参的值会传递给形参。

形参实参的区别与联系

  1. 形参变量只有在函数被调用时才会分配内存(在stack 中),调用结束后,立刻释放内存,所以形参变量只有在函数内部有效,不能在函数外部使用。
  2. 实参可以是常量、变量、表达式、函数等,无论实参是何种类型的数据,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参,所以应该提前用赋值、输入等办法使实参获得确定值。
  3. 实参和形参在数量上、类型上、顺序上必须严格一致,否则会发生“类型不匹配”的错误。当然,如果能够进行自动类型转换,或者进行了强制类型转换,那么实参类型也可以不同于形参类型。
  4. 函数调用中发生的数据传递是单向的,只能把实参的值传递给形参,而不能把形参的值反向地传递给实参;换句话说,一旦完成数据的传递,实参和形参就再也没有关系,所以,在函数调用过程中,形参的值发生改变并不会影响实参的值。

函数调用中的内存分配

image-20210716123311156

假如内存共有1M空间。我们会把它分解成若干个栈帧(下去查询VS如何设置栈的大小和栈帧的大小),主函数调用时,会把底层的栈帧分配给主函数,如果将要占用很多的空间,我们就得继续往上占用上层的栈帧。每当有一个函数调用,即分配一个栈帧。

1
2
3
int a = 10;
int b = 20;
int* p = &a;//星号在类型和标识符之间时是声明。//p=>&a; //*p(星号在指针变量前是解引用)=>*&a=>a;

传地址交换值–间接改变值

image-20210715152049764

问题1

int tmp = *ap;通过指针指向取x的值并修改tmp值的底层实现是如何的?
mov eax,10;
mov ebx,0x00b3f9f0;
mov [ebx],100h;
直接访问、间接访问。任何一本讲微机原理的书都有讲解。

问题2

image-20210716122234042
仔细观察,发现未曾开辟定义的存储空间中都是随机值"cccccccc",而有两个地址很特殊,就是Swap_p的函数域中ap指针变量的地址之上的两个地址"0X00B3F9F8""01351459"。这两个值是什么值呢?
C语言的面试:指针、编译链接过程、函数调用过程中线程的保护、恢复是怎么实现的。调用函数、现场保护,调用完后要实现现场的恢复。这是区分学的好不好、自学能力强不强的标准。C语言全部讲完后,分模块讲时再说。

函数调用机制

image-20210714182226592

C语言中,先把y入栈,再把x入栈,函数参数入栈的顺序是从右向左的!有些编程语言是从左向右的。

函数调用首先要进行参数传递,参数传递的方向是由实参传递给形参。传递过程是,先计算实参表达式的值,再将该值传递给对应的形参变量。一般情况下,实参和形参的个数和排列顺序应一一对应,并且对应参数应类型匹配(赋值兼容),即实参的类型可以转化为形参类型。而对应参数的参数名则不要求相同。
在示例中int MaxInt(int a,int b),a和b是形参,在main中 x, y 是实参。

查:被调用函数MaxInt return c给主函数中的max变量时,肯定不能直接赋值,而是用临时空间先存放,再取出送给这个max。这个临时空间谁来担当?

多文件结构

循环语句

while语句

示例–打印平方表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
int main()
{
int i = 0, n = 0;
printf("Enter number of entries in table: ");
scanf("%d",&n);
i = 1;
while(i <= n)
{
printf("%10d%10d\n",i,i*i);
++i;
}
printf("\n");
return 0;
}

do-while循环

特点是先执行,后判断。要有一个条件使之退出while才行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//计算一个正整数的位数
#include<stdio.h>
int main()
{
int digits = 0,n;
printf("Enter a nonnegative integer: ");
scanf("%d",&n);
do
{
n/=10;
digits++;
}while(n>0);
printf("The Number has %d digit \n",digits);
return 0;
}

但是do-while有一个漏洞,就是如果上例代码输入了一个"0"值的话,还是会执行一次,最后输出1位。但是实际上0是不占位数的。

for循环

  1. 表达式1只执行一次
  2. 表达式2判断为真才执行循环体
  3. 循环体执行完后才执行表达式3
    特点是:编程的执行顺序和我们编写他的顺序不符合,所以有些人不习惯for语句。

VS和VC++编译器对于for语句中表达式1的区别

VS2012/2019中的for(int i=0;i<10;++i)中i的作用域只在for块内

VC++中i的作用域在块外也有,因此不能重新声明i

VC++中.c文件中的for语句中表达式1不能同时定义、初始化。只能在for外先定义i。

for循环的惯用法

对于向上加(变量自增)或向下减(变量自减)的循环来说,for语句通常是最好的选择。

1
2
3
4
5
6
7
8
9
// 从0 向上加到n-1
for(i = 0; i < n; ++i) ...
// 从1 向上加到n
for(i = 1; i <= n; ++i) ...
// 从n-1 向下减到0
for(i = n-1; i >= 0; --i) ...
//从n 向下减到1
for(i = n; i > 0; --i) ...
//编写的控制表达式中把 i < n 写成 i <= n , 会犯"循环次数差一次" 错误
1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
int main
{
int i = 0;
int n = 5;
for(i = n; i > 0;--i)
{
printf("%d ",i);//执行n次
}
printf("for end: \n");//输出for循环退出后i的值
printf("%d \n", i);
return 0;
}

循环语句圆括号中省略表达式

1
2
3
4
5
6
7
8
9
10
int n=10;
int i=1;
for(;;)//for(int i=0;i<n;++i)
{
if(i>=n)
{
break;
}
++i;
}

对于for省略:如果省略了表达式2,那么是死循环的效果

对于while,省略圆括号内表达式,不可行。

三种死循环

for

1
2
3
4
for(;;)
{
;
}
1
2
3
4
5
6
7
8
9
10
11
//如何把死循环写法改为之前for(int i=0;i<n;++i)的效果
int n=10;
int i=1;
for(;;)//for(int i=0;i<n;++i)
{
if(i>=n)
{
break;
}
++i;
}

while

1
2
3
4
while(1)
{
;
}

do-while

1
2
3
4
do
{
;
}while(1);

0716重点:跳转语句

实际上,break/continue/return都是goto的变种。

break

语句只能用在switch语句和循环语句中,用来跳出switch语句或提前终止循环,转去执行switch语句或循环语句之后的语句

image-20210716141723230

需要注意的是:break语句只能跳出一层循环

continue

语句只能用在循环语句中,用来终止本次循环。当程序执行到continue语句时,将跳过其后尚未执行的循环体语句,开始下一次循环下一次循环是否执行仍然取决于循环条件的判断。continue语句与break语句的区别在于,continue语句结束的只是本次循环,而break结束的是整个循环。

但上面这段话没说明本质。

image-20210716143039440

continue对于for语句

continue对于for语句跳到的是表达式3。如果处理不当就会出问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//打印:1 3 5 7 9
int main()
{
int n=10;
for(int i=0;i<n;++i)
{
if(i&2==0)
{
continue;
}
printf("%d ",i);
//++i;//如果把for圆括号中的表达式3挪下来写到循环块内的最后一句,则此循环将成为死循环。因为continue针对for循环是跳到表达式3的,如果表达式3是空语句则不执行任何语句,徒劳。
}
printf("\n");
return 0;
}
continue对于while和do-while语句

continue对于while和do-while语句,continue跳到的是圆括号内的判断。如果处理不当,更会出问题。

如何将上述for的代码由for改为while循环?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
//错误示范
int main()
{
int n=10;
int i=0;
while(i<n)
{
if(i%2==0)
{
continue;
}
printf("%d ",i);
++i;
}
printf("\n");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
int main()
{
int n=10;
int i=0;
while(i<n)
{
++i;
if(i%2==0)
{
continue;
}
printf("%d ",i);
}
printf("\n");
}

goto

语句标号语句一起使用,所谓标号语句是用标识符标识的语句,它控制程序从goto语句所在的地方转移到标号语句处。

goto语句会导致程序结构混乱,可读性降低,而且它所完成的功能完全可以用算法的三种基本结构实现,因此一般不提倡使用goto语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
int main()
{
int i = 0,j = 0,k = 0;
for(i = 0;i<10;++i)
{
for(j = 0;j<10;++j)
{
for(k = 0;k<10;++k)
{
if(i+j+k == 10)
{
goto input;
}
}
}
}
input:
printf(" main end \n");
return 0;
}
适用场合

在某些特定场合下goto语句可能会显出价值,比如在多层循环嵌套中,要从深层地方跳出所有循环,如果用break语句,不仅要使用多次,而且可读性较差,这时goto语句可以发挥作用。

1
2
3
4
5
#include<stdio.h>
int main()
{

}
注意
  1. goto最好只用它来从上到下跳。不要从下到上跳,因为可能会产生程序的二义性。
  2. 不能在函数间跳转,不能跨越两个函数。只能在本函数的作用域、可见性中跳转。
VS2019

return

语句用于结束函数的执行,返回调用者,如果是主函数,则返回至操作系统(终止程序的执行)。
利用一个return语句可以将一个数据返回给调用者。

return本质上就是goto连带一个数据返回。与goto的区别就是goto不能带一个数据,return可以。

主函数中的return与子函数中的return
  1. 主函数return后,程序结束。子函数return只是本函数结束。
  2. 主函数的return是返回给操作系统。
return与exit函数的区别

在主函数中,exit(1);return 0;的效果是一样的。都是结束程序的执行;但在子函数中就和return语句不一样了,子函数中调用exit()也会直接终止整个程序的执行。

调用exit()函数会直接终止程序的进行,需要引入头文件<stdlib.h>

传递给exit函数的实际参数和main函数的返回值具有相同的含义:两者都说明程序终止时的状态,为了表示正常
终止,传递0,即 exit(0);因为0 有点模糊,所以C语言允许用EXIT_SUCCESS来替代(效果相同)。exit(0);等同于exit(EXIT_SUCCESS);,表示程序正常退出;exit(1);等同于exit(EXIT_FAILURE);,表示程序异常退出。

1
2
3
4
exit(EXIT_SUCCESS); /* normal termination */
exit(EXIT_FAILURE); /* abnormal termination */ //传递EXIT_FAILURE表示异常终止:
EXIT_SUCCESS和EXIT_FAILURE都是定义在<stdlib.h>中的宏。
EXIT_SUCCESS和EXIT_FAILURE的值都分别是01.
返回类型为void

通常,当函数的返回类型为void时, return语句可以省略,如果使用也仅作为函数或程序结束的标志。有些编译器可以写成return void;,但在VS2019中不可以。

总结

都是goto的变种。

空语句

语句可以为空,也就是除了末尾处的分号以外什么符号也没有。

所带来的问题

圆括号后放置空语句

不小心在if、while 或 for 语句的圆括号后放置分号会创建空语句,从而造成if、 while 或 for 语句提前结束。if 语句中,如果在圆括号后放置分号,无论条件表达的值是什么,if 语句执行的动作都一样,都会执行if块内的代码:

if语句

1
2
3
4
if(d == 0)  ;
{
printf("Error: Division by zero \n");
}

while语句

while 语句中,如果在圆括号后放置分号,会产生无限循环:

1
2
3
4
5
6
i = 10;
while(i>0) ;
{
printf("%d ",i);
--i;
}

另一种可能是循环终止,但是在循环终止后只执行一次循环体语句:

1
2
3
4
5
i = 10;
while(--i>0) ;
{
printf("%d ",i);
}

for语句

for 语句中,如果在圆括号后放置分号,会导致只执行一次循环体语句:

1
2
3
4
for(i = 10; i > 0 ; --i) ;
{
printf("value: %d ",i);
}

要注意的地方

该死的分号;

函数后的分号

函数声明语句

函数声明时加分号。

函数定义语句

在大括号后加了分号也没事,因为这是空语句,无大碍。
image-20210716114054328

结构体定义语句

结构体定义结束时,在大括号后必须加分号。表示结束。
image-20210716114227011

逗号表达式

逗号表达式只能写类型一致的声明,int i = 0, float = 2.0是不对的。

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
int main()
{
int a=10,b=20,c-30;
int x=0;
//下面两句看似说明逗号运算符和分号没什么区别
x=a,a=b,c+=10;
x=a;a=b;c+=10;
//但是,如下两句就能体现出了逗号运算符的方便
x = (a+10, a=b, c += 10);//编译通过
x = (a+10; a=b; c += 10);//程序编译不通过,因为分号不可被包在括号里。
}

上例中,x = (a+10, a=b, c += 10);这句表达式如何运算呢?首先执行a+10,但a的值不变;再执行a=b,将b的值赋给,a的值变成20;再让c+=10,c的值变为40。那么x的值会被赋为多少呢?答案是40,因为逗号表达式的值是取最后一条表达式的值。

按照逗号表达式的运行机制,我们可以优化一个事情,就是下面讲到的scanf的代码位置。

scanf_s函数的机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
int main()
{
int n=0,m=0;
printf("Enter integers (0 to erminate)\n");
scanf_s("%d",&n);
while(n!=0)
{
sum = sum + n;
scanf_s("%d",&n);
}
printf("sum %d \n",sum);
return 0;
}

stdin标准输入文件流
stdout标准输出文件流
stderr错误流

标准输入/输入文件流他们都带有缓冲区。stdin从键盘上输入数据的时候,就先把数据放到标准输入文件流的缓冲区中了。stdin还有一个能力,会把缓冲区中的内容回显在屏幕上。如果没有回车,我们就认为这个输入没有结束。如果打了回车,就相当于通知scanf从缓冲区中取值。可以每输入一个数据回车一次后scanf读取此数,接下来输入后面的数据并回车时,就会把前面的缓冲区覆盖掉;也可以使多个数据空格隔开全输入完,再一次性回车,交给scanf依次读取。两种方式最大的不同就在于缓冲区存储的数据不一样多。

这种输入的形式,有个不好的地方,就是while外一个scanf,while内有个scanf。怎么样使之更为简洁呢?就用到了逗号表达式。

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
int main()
{
int n=0,sum=0;
printf("Enter integers (0 to terminate)\n");
while(scanf_s("%d",&n),n!=0)
{
sum = sum + n;
}
printf("sum %d \n",sum);
return 0;
}

while最终要判断的是逗号表达式最后的一个表达式即n!=0。所以这样做不但不影响while的正确判断还简化了代码编写。

0715作业

  1. 两个for循环(二维数组)打印一个乘法口诀表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<stdio.h>
void MultiTable(int n)
{
for(int i=1;i<=n;++i)
{
for(int j=1;j<=i;++j)
{
printf("%d*%d=%d ",j,i,i*j);
}
printf("\n");
}
}
int main()
{
int n = 0;
scanf_s("%d",&n);
MultiTable(n);
}
  1. 仔细观察如何把一维数组转化输出为一个二维平面?–>为N后做准备
    image-20210715163945136
  2. 选做题:仔细观察,n=5,从输出的角度如何打印成这样的效果:
    image-20210716012622369
    (提示:第一行每个数只有1位,每一行都比上一行少一个数)
    如果是整型数
    如果是字符数组:滑动动态窗口方式。
  3. 输入任意顺序的三位数,都能正确找到其中间大小的数。
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
51
#include<stdio.h>
int MaxInt(int x,int y)
{
return x > y ? x : y;
}
//错误写法
int MidInt(int a,int b,int c)
{
a = a > b ? a : b;
return a < c ? a : c;
}
//逻辑法
int MidInt(int a, int b, int c)
{
if(a>=b&&a<=c)
{
return a;
}
else if(b>=a&&b<=c)
{
return b;
}
else
{
return c;
}
}
//先冒泡排序,后输出b
int MidInt(int a, int b, int c)
{
if(a>b){Swap_Int(&a,&b);}
if(b>c){Swap_Int(&b,&c);}
if(a>b){Swap_Int(&a,&b);}
return b;
}
//1 2 3=>2
//1 3 2=>2
//2 3 1=>2
//2 1 3=>2
//3 1 2=>2
//3 2 1=>2
int main()
{
int a,b,c;
int max=0,mid=0;
scanf_s("%d %d %d",&a,&b,&c);
max = MaxInt(a,MaxInt(b,c));
mid = MidInt(a,b,c);
printf("max=%d,mid=%d",max,mid);
return 0;
}
  1. 下去查询VS如何设置栈的大小和栈帧的大小
    image-20210716013306466

image-20210716013335862
6. 下去查:每一个工程的入口函数默认是主函数,怎么设置其他函数为入口?
image-20210716013306466
image-20210716015329536
7. 查:直接访问、间接访问。讲微机原理的书都有讲解。
mov eax,10;
mov ebx,0x00b3f9f0;
mov [ebx],100h;
8. 被调用函数return一个数给主函数中的一个变量时,肯定不能直接赋值,而是用临时空间先存放,再取出送给这个变量。这个临时空间谁来担当?
9. EAX惯用于“累加器”(accumulator),它是很多加法乘法指令的缺省寄存器;还用来存放函数返回值;占用32个2进制位,4个字节。eax的后16位为ax,后16位中,前8位为ah,后8位为al,前16位的访问需要右移。有时EAX也用于程序数据的返回值。
1. EBX惯用于“基地址”(base)寄存器,在内存寻址时存放基地址。多与指针相关。
2. ECX惯用于“计数器”(counter),是重复(REP)前缀指令和LOOP指令的内定计数器。用于循环的计数。
3. EDX:I/O设备的地址编号大于255时,存放设备的端口号。
4. 在进行乘除法运算时,EAX用来存商,EDX用来存余数。
5. 临时量具有常性,只读不可写,Add(x,y)=100;是不可行的,是不能给函数的返回值赋值的。

0716作业

  1. 查:如何产生随机值
    https://blog.csdn.net/w_y_x_y/article/details/80199694
1
2
3
4
这里对程序中用到的产生随机数的函数进行解释。
1、srand()函数:随机数发生器的初始化函数,需要提供一个种子,这个种子会对应一个随机数。如果使用相同的种子,rand() 函数会出现一样的随机数。默认种子数是1,即srand(1)。
2、rand()函数:伪随机数发生器,需要先调用srand初始化,一般用当前日历时间初始化随机数种子,这样每行代码都可以产生不同的随机数。
3、随机数产生的原理:随机数中的变量种子rand初始会赋值给holdrand,然后holdrand和一个公式计算出新的随机数并赋值给holdrand再返回,循环产生随机数,每次得到的结果只与上次随机数的值有关,如果想要每次生成的新随机数和上次随机数没有关联,可以通过每次利用(srand((unsigned)time(0)))改变种子进而初始化holdrand得到随机数。
  1. 做一个简单的计算器,弄一个.h和.cpp文件
    image-20210716150924442
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef
#表明是预处理命令
ifndef意为if not defined sth.
它与ifdef都可用于条件编译

#ifdef 标识符A
程序段1
#else
程序段2
#endif
意为若所指定的标识符A已经被#define命令定义过则编译程序段1,否则编译程序段2

同理
#ifndef 标识符B
程序段1
#else
程序段2
#endif
意为若所指定的标识符B未被#define命令定义过则编译程序段1,否则编译程序段2

C语言_概览

前言及语录

  1. C语言的最根本的一个特点:一定要会上机编程。这是宗旨。不管学的怎么样,程序要会编。程序不会编的话,记那么多规则也是没有用的。即C语言的编程是第一位的要求。
  2. C语言官网C++参考手册:不会了就可以在这里面查!
  3. 注册博客,要求是:把学习内容按照我们的理解写出来,总结自己学到的内容。博客的好处就是避免遗忘。可以翻出来清楚自己学到了哪些,同时能随时补充。
  4. 刷题网站:牛客网、力扣网。学完数组、指针、结构体后即可上此平台刷题。绝大部分的题都是要我们自己去刷的。
  5. 看的第一本C语言书籍:《高质量程序设计指南——C++/C语言》。先看到56页,等学到C++再往后看。

C语言概述部分内容简单地把基础知识、基本的语法、指针、结构体介绍。有的详尽,有的不太详尽。后期分专题分析重要的内容。

计算机基础知识

软件的概念

一系列按照特的那个顺序组织的计算机数据指令的集合。简单地说,软件就是程序加文档的集合。
软件一般被划分为系统软件(如安卓操作系统)、应用软件(如Word、编译器)和介于这两者之间的中间件(如Redis)。

文件

文件由两部分构成:文件名和文件主体。
文件的一种分类是:可执行文件和不可执行文件。

可执行文件

  1. 在Windows操作系统中,扩展名为:*.exe, *.bat, *.com等的文件是可执行文件;
  2. 可执行文件由指令和数据构成。
  3. Linux是靠文件属性来判断是否可执行。

不可执行文件

其内容是由数据构成。

举例

C/C++语言中,*.c/*.cpp源文件(文本–ASCII码字符构成的),*.h头文件(文本),*.i预编译文件(文本),*.s汇编文件,*.o/*.obj二进制目标文件,*.exe可执行文件。

image-20210712155031576

在IDE中编译链接后,工程目录下的Debug中形成了obj文件和exe文件。
达到可执行的底层步骤:预编译、编译、链接。

实际上在编译和链接之间还有汇编这个步骤

进制及其转换

image-20210712180942700
我们学习计算机时,如在调试的时候我们看到的底层是以16进制表达的。我们要理解进制。
进制也就是进位计数制,是人为定义的带进位的计数方法。 对于任何一种进制–X进制,就表示每一位置上的数运算时都是逢X进一位。 如:十进制是逢十进一,十六进制是逢十六进一,二进制就是逢二进一,以此类推,x进制就是逢x进位。我们日常生活中的进制都哪些?
计算机中的进制分为二进制,八进制,十进制,十六进制。
二进制数、八进制数、十六进制数转换为十进制数的规律是相同的。把二进制数(八进制或十六进制数)按位权形式展开多项式和的形式,求其最后的和,就是其对应的十进制数——简称“按权求和”。如:
(10100)2(1 * 2^4 + 0*2^3 + 1 * 2^2 + 0*2^1 + 0*2^0)10(20)10

(245)8(2 * 8^2 + 4 * 8^1 + 5 * 8^0)10(165)10

(1F2)16(1 * 16^2 + 15 * 16^1 + 2 * 16^0)10(498)10

练习:(103)10( )2➔( )8( )16

C语言简介

C语言是一种结构化语言,它有着清晰的层次,可按照模块的方式对程序进行编写,十分有利于程序的调试,且C语言的处理和表现能力都非常的强大,依靠非常全面的运算符和多样的数据类型,可以轻易完成各种数据结构的构建,通过指针类型更可对内存直接寻址以及对硬件进行直接操作,因此既能够用于开发系统程序,也可用于开发应用软件。
1982年成立C标准委员会,建立C语言的标准。
1989年,ANSI发布了第一个完整的C语言标准——ANSIX3.159—1989,简称“C89”。
1999年,在做了一些必要的修正和完善后,ISO (International Standards Organization),发布了新的C语言标准,命名为ISO/IEC 9899: 1999,简称“C99”。
在2011年12月8日,ISO又正式发布了新的标准,称为ISO/IEC9899: 2011,简称为“C11”。

第一个C语言程序

1
2
3
4
5
6
7
// hello.c  		 //注释	// /* 不容许嵌套 */
#include<stdio.h> //预编译处理
int main() //主函数
{
printf("hello word !\n"); //语句
return 0;
}

C源程序的结构特点

  1. 一个C语言源程序可以由一个或多个源文件组成。
  2. 每个源文件可由一个或多个函数组成。
  3. 一个源程序不论由多少个文件组成,都有一个且只能有一个main函数,即主函数。
  4. 源程序中可以有预处理命令(include 命令仅为其中的一种),预处理命令通常应放在源文件或源程序的最前面。
  5. 每一个说明,每一个语句都必须以分号结尾。
  6. 标识符,关键字之间必须至少加一个空格以示间隔。若已有明显的间隔符,也可不再加空格来间隔。

C语言的特点

  1. 与Java和C++一样,C语言是一种强类型语言。即类型不可变性。在C语言中,变量、函数一旦定义了类型,它的类型就不变。
  2. C语言是函数式编译的。即小函数来套函数,一个一个套接。

一段有启发的代码

image-20210712190620491
此段代码的运行结果是:“-10 > 10”

数据类型

基本数据类型

必须记住(包括占用字节数),融于血液中。

整型

  1. char; 存放字符的ASCII码值。占用1字节
  2. short; 短整型,占用2字节
  3. int; 占用4字节
  4. long int; 占用4字节
  5. long long; 占用8字节

浮点

  1. float; 单精度,占用4字节
  2. double; 双精度,占用8字节
  3. long double; 双双精度,当前VS编译器占用8字节,dev编译器占用12字节,将来可能占用16字节

新类型

bool; 占用1字节

无类型 - 也属于基本数据类型

void; 不能定义变量,所以不占用字节

各数据类型的区别

最大的区别在于在定义变量时,它们开辟的空间的字节个数不一样。

关键字sizeof

  1. 计算变量占用的存储空间一种类型在定义变量时所占的存储空间不是函数
  2. 计算出来的数值类型为:unsigned int
  3. 在编译时进行计算,而不是运行时进行计算。

示例:int n = sizeof(char);

1
2
3
4
const int n = 5;
int ar[n] = {12,23,34,45,56};
sizeof(n);//4
sizeof(ar);//等于sizeof(int)*n 即4*5=20

变量、常量、标识符

变量

以某标识符为名字,其数值可以改变,可读可写。共有三个层次。

全局变量

定义在函数(包括主函数)之外的变量。

作用域解析符——::

两个冒号,代表我要用全局变量。常用于全局、局部变量名冲突时区别出全局变量。

局部变量

定义在函数内部的变量。

块内变量

块内可见。块外不可见。

代码示例

image-20210712233203207
这段代码的输出结果为:“b = 0 ”。

全局与局部冲突时的向上就近原则

说明一个规律、道理:当遇到的变量名字出现全局、局部的冲突时,按向上就近原则为主。
image-20210712233841613
这段代码的输出结果为:b = 10。说明了::符的作用。

常量

其值不可改变,只读不写。

字面常量

要注意的是常量不只是有数值,常量也带有类型的意义。比如7/2=3

编程与数学的区别:一个有穷一个无穷;一个有类型一个无类型。

宏常量

用#define定义的常量,叫做宏常量。

1
#define PI 3.14

宏的根本概念是一种替换原则。

  1. 无类型,只是暂时用字符串来表示

  2. 不开辟存储空间

  3. 结尾一般不加分号,如果加上分号则把分号也一起看作是替换的值。

  4. 在预编译时遇到宏常量字符串时起作用,替换。可以在预编译后Debug文件夹下生成的*.i文件中看到。
    image-20210713003504549

    image-20210713003530909
    image-20210713003601640

常变量

用const关键字修饰的变量,称为常变量。只可读取,不可改变。要开辟空间。

枚举常量(enum)

1
2
3
4
5
6
7
8
enum week{
Mon=1,
Tues=2,
Wed=3,
Thurs=4,
Fri=5,
Sat=6,
Sun=7};

枚举常量实际上是一种受到限制的整型量。不可以是小数。

  1. 第一种受限的表现:x不能随便取值,只能从1、2、3、4、5、6、7中赋值。
    image-20210713004400231
  2. 第二种受限的表现:即使从1、2、3、4、5、6、7中赋值也要用定义枚举类的变量名赋值,不可直接用数字。
    image-20210713004708901
  3. 正确写法:
    image-20210713004753195
  4. 其他约束:运算的约束,比如不可以自增自减。
  5. 如果没给第一个变量赋值,则默认第一个变量为0,往后的变量值为依次加1;若没给中间某个变量赋值,则默认为上一个变量值加1。
    image-20210713005317130
    如上图,值依次为:-1、0、1、-2、-1、0、1

字符常量和字符串常量

char ch = 'a';

给变量ch中存放的不是字符'a',而是字符'a'对应的ASCII码值。即编译后转换为"char ch = 97;"

打开内存,查ch处的存储内容。发现存的是97的十六进制形式61

image-20210713105853322

image-20210713105916869
image-20210713105929580
char是整型数据类型,ch存储的是ASCII码97,但输出的形式由我们决定,如果是printf("%d \n", ch);那么就是97。如果是printf("%c \n",ch);那么就是a

关于字符对应的ASCII码,我们重点记忆几个字符即可,比如字符a是97,则可以推出其他字符的值。

特殊符号 - 转义字符及其含义

image-20210713111900791

重点记三个:

  1. \n是换行符(LF),将当前位置移到下一行开头。ASCII码值是10
  2. \r是回车符(CR),将当前位置移到本行开头。ASCII码值是13
  3. \t是水平制表符(HT),跳到下一个TAB位置。ASCII码值是9
  4. \0是空字符,ASCII码值是0所以空字符可以有两种赋值形式:char ch1 = 0;或char ch2 = '\0';
    如果要表示反斜杠字符,可用\\转义。
定界符

单引号是字符的定界符:'a' -> |97|。如果要表示单引号字符,可用\'转义。
双引号是字符串的定界符:"a" -> |97|\0|。如果要表示双引号字符,可用\"转义。

关于空字符(\0)、空格字符(' ')和'0'字符

image-20210713113436447
一定要区分。区分的关键是从ASCII码值来理解。

  1. 空格字符不是空字符空格字符的ASCII码值是48
  2. char chb = 0;char chc = '\0';等效,都是给变量赋ASCII码值0。
  3. 单引号中只有0的时候是字符’0’,其ASCII码值是48。

头文件<ctype.h>

用来确定包含于字符数据中的类型的函数。
image-20210713144419145

标识符

必须是以下划线或字母开头下划线、字母、数字的组合体

_a; a4; _3;均可

变量、函数起名时要见名知义,不要用汉语拼音,因为有同音异词,容易引起歧义。

定义和声明

定义

所谓的定义就是为这个变量分配一块内存并给它取上一个名字,这个名字就是我们经常所说的变量名。但注意,这个名字一旦和这块内存匹配起来,它们就同生共死,终生不离不弃,并且这块内存的位置也不能被改变。一个变量在一定的区域内(比如函数内,全局等)只能被定义一次,如果定义多次,编译器会提示你重复定义同一个变量或对象。

声明

什么是声明:有两重含义,如下:
第一重含义:告诉编译器,这个名字已经匹配到一块内存地址上了。(但是如果只是声明的话,不开辟内存空间)
第二重含义:告诉编译器,我这个名字我先预定了,别的地方再也不能用它来作为变量名。

示例

image-20210712231142222

示例中float pi = 3.14f;f表示此数据为单精度类型,若不带f默认为双精度。

顺序语句、选择语句、循环语句

顺序结构

按照语句出现的先后顺序依次执行。

选择结构

根据条件判断是否执行相关语句。

循环结构

当条件成立时,重复执行某些语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//统计从键盘输入的一行字符的个数。
int main()
{
int num = 0;
printf("input a string\n");
//char ch = '\0';
//scanf_s("%c",&ch);
//ch=getchar();
while (getchar()!='\n')
{
num += 1;
}
printf("%d \n",num);
return 0;
}

其中,getchar();等效于scanf_s("%c",&ch);,前者明显比后者简洁。

函数

通过函数,可以把一个复杂任务分解为若干个易于解决的小任务。充分体现结构化程序设计由粗到精、逐步细化的设计思想。

image-20210713172544505

函数组成部分

1
2
3
4
返回类型 + 函数名称(形参列表)
{
  函数体
}

进程与程序的关系

image-20210713151216654

进程

进程是程序的一次执行。进程是动态的。

比如,在现实世界中,程序可以看作是乐谱,乐谱不会自己弹奏。有一钢琴,人坐下根据乐谱进行弹奏,弹奏乐谱的过程就是进程。

进程非常重要的概念:以时间为单位进行流失,在经过某几个时间点内要把程序全部执行直至结束。时间点有创建、执行、结束。

资源

又如,菜谱给了做饭步骤,菜谱自己不能凭空做出饭。按照菜谱的模式买菜,架火按照菜谱做饭。而做饭要有资源,油、水、调料、火等。

计算机中最重要的两个资源:时间空间

时间针对于CPU,空间针对于内存。

程序被执行时,任何一个进程,将会把用到的存储空间分配给四个区域——代码区(test area)、数据区(data area)、堆区(heap area)、栈区(stack area)。

  1. data区存放程序的全局变量。
  2. heap区对应着malloc和free开辟和释放的存储空间。
  3. stack区存放函数中定义的局部变量。

可见性(作用域)

可见性指标识符能够被使用的范围:只有在作用域内的标识符才可以被使用。此阶段特性针对编译和链接过程。

  1. 函数中定义的标识符,包括形参函数体中定义的局部变量的作用域都只在该函数内,也称作函数域。
  2. 文件作用域也称全局作用域,定义在所有函数之外的标识符,具有文件作用域。作用域为从定义处到整个源文件结束。文件中定义的全局变量和函数都具有文件作用域。

生存期

生命期指的是标识符从程序开始运行时被创建,具有存储空间,到程序运行结束时消亡时释放存储空间的时间段。此阶段针对的是程序的执行过程

  1. 局部变量的生存期是:函数被调用,分配存储空间;函数执行结束,释放空间。stack区。
  2. 全局变量的生存期是:从程序执行前开始,到执行后结束。data区。

代码示例_生存期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
int* fun()
{
int ar[10] = {12,23,34,56,78,89,90,100};
printf("fun \n");
for(int i = 0;i < 10;++i)
{
printf("%d ",ar[i]);
}
printf("\n");
return ar;
}
int main()
{
int* p = fun();
printf("main \n");
for(int i = 0;i < 10;++i)
{
printf("%d ",p[i]);
}
return 0;
}

运行结果_生存期

image-20210713163156464

调用的fun结果正常。而main再遍历ar地址时就失效了,后面的数都是随机数。

此例说明,函数的生存期只有在被调用时才拥有其存储空间,调用结束时,其存储空间将被释放。

思考

数组ar的头地址中的值为何能一直保存?main函数自己调用时,为何除了12正确,其他都是乱的?

做了以下实验:如果让fun函数return ar+1,再在main函数中遍历,发现运行结果是image-20210713164713434

即:第一个数据23正确保留,后面全乱。由此,对程序的生存期又可见一斑。

但在dev-C++环境下不可实现mainfor遍历(运行出错)。这又说明,不同编译器对于生存期的定义也不尽相同

还做了另一实验:将ar数组大小调整至10000。还是输出10个数,输出却正常。

这个例子体现了指针的失效

解释

我们在调用函数时,都会开辟若干个栈帧提供函数中变量的存储空间。在main函数中调用fun()函数时,随即开辟了一些空间存放数组。最后虽然返回了原本数组的首地址并赋给了p指针,但调用结束后变量的生存期殆尽,空间被释放。printf()也是一函数,他的调用难免会覆盖刚才fun()函数占用的栈帧,即残留在栈帧中的数据被重写导致输出达不到预期以为的效果。

而把数组大小调至10000使fun()函数占用的栈帧很大,导致printf()函数等函数正好没有覆盖到刚才某些数组数据占用的空间,最后导致侥幸输出正确。但本质上p指针还是一个失效指针。
image-20210715001423769

动态生命期

动态生命期对应动态内存的分配、管理。

数组

数组是包含给定类型的一组数据,即一组相同类型元素的集合。

运算符

操作数(Operand)

操作数(operand)是程序操纵的数据实体,该数据可以是数值、逻辑值或其他类型。该操作数既可以是常量也可以为变量。

运算符(Operator)

运算符(operator)是可以对数据进行相应操作的符号。如对数据求和操作,用加法运算符'+',求积操作使用乘法运算符'*'等。

根据运算符可操作操作数的个数可分为一元运算符(单目运算符)、二元运算符(双目运算符)和多元运算符(C语言中只有一个三元运算符" ? : ")。

运算符优先级

image-20210715010003149

运算符举例

取模运算符%

在C语言中有很多应用:

  1. 判断是否能够整除某个数;
  2. 判断奇偶数,判别质数;
  3. 计算范围。形成循环。
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
51
52
53
54
55
56
57
58
59
60
61
#include<stdio.h>
int main()
{
//以下均要求每五个数换一行
//从0输出100个数(0~99)
for(int i=0;i<100;++i)
{
if(i%5 == 0 && i!=0)
{
printf("\n");
}
printf("%3d",i);
}
//重点是条件中加一个边界限制条件i!=0,如果没有的话会在输出前多余一行
//输出效果
// 0 1 2 3 4
// 5 6 7 8 9
// ...
// 95 96 97 98 99

//从1输出100个数(1~100)
//如果只改变上述代码中for循环中的int i=1;i<=100会出现以下情况:
// 1 2 3 4
// 5 6 7 8 9
// ...
// 95 96 97 98 99
//100
//所以我们要转变思路,主要转移点就是:先输出,后换行,并限制尾部边界条件,即100输出后不换行。
for(int i=1;i<=100;++i)
{
printf("%3d",i);
if(i%5==0 && i!=100)
{
printf("\n");
}
}
//而先输出数字后换行的模式如果对于0~99来说又失效了,会出现如下情况:
for(int i=0;i<100;++i)
{
printf("%3d",i);
if(i%5==0 && i!=100)
{
printf("\n");
}
}
// 0
// 1 2 3 4 5
// 6 7 8 9 10
// ...
// 96 97 98 99
//可进行如下改进
for(int i=0;i<100;++i)
{
printf("%3d",i);
if((i+1)%5==0 && (i+1)!=100)
{
printf("\n");
}
}
return 0;
}

image-20210714155735069

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//编程输出如上效果
//主要思路:两层for循环,用取余思想
#include<stdio.h>
int main()
{
const int n = 26;
for(int i=0;i<n;++i)
{
int k=i;
for(int j=0;j<n;++j)
{
printf("%d",k);
k=(k+1)%n;//精髓所在
}
printf("\n");
}
return 0;
}
  1. 求最大公约数的辗转相除法(欧几里得算法):gcd(a, b) = gcd(b, a mod b)
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
#include<stdio.h>
int main()
{
int a = 0, b = 0;
int i = 0;
scanf_s("%d %d",&a,&b);
//遍历算法
//i = a < b ? a : b;
//while(i>1)
//{
// if(a%i==0&&b%i==0)
// {
// break;
// }
// --i;
//}
//printf("%d \n",i);
//辗转相除法--35和25:35%25=10;25%10=5;10%5=0
while(b!=0)
{
int c = a % b;
a = b;
b = c;
}
printf("%d \n",a);
return 0;
}

取余和取模的区别

对于整型数a,b来说,取模运算或者求余运算的方法都是求整数商

  1. c = [a/b];
  2. 计算模或者余数:r = a - c * b

求模运算和求余运算在第一步不同:取余运算在取商c的值时,向0方向舍入(fix()函数);而取模运算在计算商c的值时,向负无穷方向舍入(floor()函数)。
例如计算:-7 Mod 4。那么: a = -7; b = 4;
第一步:求整数商c,如进行求模运算c=-2(向负无穷方向舍入);求余c = -1(向0方向舍入);
第二步:计算模和余数的公式相同,但因c的值不同,求模时r = 1,求余时r = -3
归纳:当ab同号时,求模运算和求余运算所得的c的值一致,因此结果一致。当符号异号时,结果不—样。
另外各个环境下%运算符的含义不同,比如c/c++、java为取余,而python则为取模。

+=, *=

1
2
3
int a=3,b=5,c=7;
a *= b + c;
//a = a * (b+c); //36

说明:别看*=里面虽然带乘号,但是它的优先级是很低的。

前置++、后置++

如果没有赋值语句,效果完全一样:先取ab中原数到临时空间eax(cpu的内部寄存器),临时空间数据+1,再把加1后的数值更新到ab中。
image-20210713184548788
如果有赋值语句。则后置是先把值赋给c,再+1回写;而前置是先更新变量值,再赋值到c。
image-20210713184644291

其他说明

  1. 对于C语言程序,++ii++效率是一样的;
  2. 对于GoLang,只有++i,没有i++
  3. 在面向对象语言中如C++、Java、Python,++i效率比较高,i++效率低。至于为何,在C++部分讲述。

左值和右值

按字面意思通俗地说。以赋值符号=为界,=左边的就是左值,=右边就是右值。
更深一层,可以将L-value理解成可寻址。A value (computer science) that has an address;R-value表示可读。

指针

内存(在程序中称为主存DRAM)是计算机中重要的部件之一,它是外存(硬盘)与CPU进行沟通的桥梁。

image-20210714162919263

计算机中所有程序的运行都是在内存中进行,为了有效的使用内存,就把内存以8位二进制(bit)划分为存储单元(也就是 1 字节)。为了有效的访问到内存的每个存储单元,就给内存存储单元进行了编号,这些编号被称为该内存存储单元的地址

image-20210714162932627

存储地址的变量称为指针变量。在C 语言中指针就是地址。

打印地址的方式

%x

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
int main()
{
int a = 10;
printf("%x \n",&a);//输出了:93fcb4,原本总共是8位,前面的00舍去了
printf("%08x \n",&a);//输出了006ffbc4。%后面补上08,8代表宽度,0代表宽度不够的时候补0
printf("%08X \n",&a);//输出了:001AFDA8。x由小写改为大写,代表字母以大写形式输出。
printf("0X%08X \n",&a);//输出了:0X0113F944。%前加上了"0X",完整、完美地表示了这是一个16进制数。
return 0;

}

%p

1
2
3
4
5
6
7
8
#include<stdio.h>
int main()
{
int a = 10;
printf("%p \n",&a);//输出了:012FFBA4
printf("%#p \n",&a);//输出了:0X00AFF8BC,但在VS2019里不支持,VS2012中可以。
return 0;
}

该死的星号*

1
2
3
4
5
6
C/C++ *
int a = 10, b = 20;
int c = a * b;

int * p = &a;//*在此表达式中的作用只是声明、标识。a的地址给的是p而不是*p
*p = 100;//与上面一句中的*作用不同,此处*的作用是指向、解引用。如果p是a的地址,那么*p就是a本身。

上述给了几个表达式。

abcp都是变量。变量最根本的区别在于:类型

要彻底理解星号的不同用处,突破点在于对变量的类型的区分!

int * p = &a;此处*在左侧的作用只是一个声明。声明p是一个int类型的指针变量。就是说,你只要是整型变量,那么我p就可以存放你的地址。此例,a的地址给的是p而不是*p。所以pabc不同之处在于,p是一个int型指针,abc只是int型。因此int p = &a;这句话就是错误的:&a是一个指针,而pint型,类型不匹配,无法存放。

*p = 100;int * p = &a;中的*作用不同,此处*的作用是指向、解引用。也就是说,如果p是a的地址,那么*p就是a本身。此处不要拿“间接访问”来解释,在语法上没有这回事。

指针的两个值

image-20210714170927750

比如:int a = 10; int* p = &a;

一个是本身的值,即本身存储的值,即本身存储的某个地址值。即0x0055f864。此值说明了其指向谁。即&a

另一个是本身存储的地址值的真实内容值,即a

虽然p是一个指针,但它也是一个变量,所以他也有自己的地址,即0x0055f84c

逻辑名称与物理地址

比如:我要到邢同学的宿舍去,邢同学的宿舍就是逻辑名称,而12公寓328是其物理地址。那么,邢同学的宿舍和12公寓328实际上就是同一空间。

对应到内存条上

image-20210714171543412

可以拓展的知识点:小端存放与大端存放

指针类型的sizeof

32位机的指针一概都是4;64位机的指针一概都是8!

因为不管你是char类型、短整型、整型的变量的地址,都只是存放首地址,对于64位机,某一类型的首地址就占用64位bit位,8个字节才能存放;同理,对于32位机,某一类型的首地址就占用32位bit位,4个字节才能存放。

指针的类型

野指针

1
2
3
4
5
6
#include<stdio.h>
int main()
{
int a;
int* p;
}

就像a未初始化,不清楚其值是多少一样。未初始化的p也不知道其指向的是哪个地址。此类指针叫做野指针。

空指针

1
2
3
4
5
6
#include<stdio.h>
int main()
{
int* p=NULL;//此处将用0代替NULL赋值
int* s=nullptr;//此处将用(void*)0代替nullptr赋值
}

image-20210714234134586

失效指针

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
int * fun()
{
int ar[10]={12,23,34,45,56,67,78,89,90,100};
int* p=&ar[0];
return p;
}
int main()
{
inr* ip=fun();
for(int i=0;i<10;++i)
{
printf("%d",*ip);
ip=ip+1;
}
}

运行结果

NULL与nullptr

实际上还是有一些小的区别。

1
2
int* p = NULL;//此处将用0代替NULL赋值
int* s = nullptr;//此处将用(void*)0代替nullptr赋值

结构体

程序开发人员可以使用结构体来封装一些属性,根据原有的类型,设计出新的类型,在C语言中称为结构体类型。
在C语言中,结构体是一种数据类型。

C语言中的类型

char; short; int; long int; long long; float; double; long double; bool;都是基本类型

char*; short*;这种指针类型和int ar[10]; br[20];这种数组类型都叫做结构类型

结构体的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<stdio.h>
struct Student
{
char s_id[10];//学号
char s_name[10];//姓名
char s_sex[5];//性别
int s_age;//年龄
};
int main()
{
//定义结构体变量并初始化
struct Student stud={"202001","tulun","man",15};
//sizeof(stud)?
//使用.(成员选择(对象)运算符)访问结构体变量的成员
printf("id: %s \n",stud.s_id);
printf("name: %s \n",stud.s_name);
printf("sex: %s \n",stud.s_sex);
printf("age: %d \n",stud.s_age);
return 0;
}

结构体的访问形式

1
2
3
4
5
Student  sx = {...};
Student* sp = &sx;

形式一:(*sx).s_id = 202001;//注意,*(取值运算符)优先级低于.(成员选择(对象)运算符),使用必须给*sx加上括号,否则sx直接先和.结合,导致错误。
形式二:sp->s_id = 202001;//成员选择(对象指针)运算符

掌握如何计算结构体的sizeof。

文件

数据流

指程序与数据的交互是以流的形式进行的。进行C语言文件的存取时,都会先进行“打开文件”操作,这个操作就是在打开数据流,而“关闭文件”操作就是关闭数据流。

缓冲区(Buffer)

指在程序执行时,所提供的一块存储空间(在内存中),可用来暂时存放做准备执行的数据。它的设置是为了提高存取效率,因为内存的存取速度比磁盘驱动器快得多。

C语言的文件处理功能依据系统是否设置“缓冲区”分为两种:一种是设置缓冲区,另一种是不设置缓冲区。由于不设置缓冲区的文件处理方式,必须使用较低级别的 I/O 函数(包含在头文件io.hfcntl.h中)来直接对磁盘存取,这种方式的存取速度慢,并且由于不是C的标准函数,跨平台操作时容易出问题。下面只介绍第一种处理方式,即设置缓冲区的文件处理方式。

当使用标准I/O函数(包含在头文件stdio.h中)时,系统会自动设置缓冲区,并通过数据流来读写文件。当进行文件读取时,不会直接对磁盘进行读取,而是先打开数据流,将磁盘上的文件信息拷贝到缓冲区内,然后程序再从缓冲区中读取所需数据,如下图所示:

image-20210713225323314

文件类型

分为文本文件和二进制文件两种。

文本文件是以字符编码的方式进行保存的。二进制文件将内存中数据原封不至文件中,适用于非字符为主的数据。

如果以记事本打开,只会看到一堆乱码。

其实,除了文本文件外,所有的数据都可以算是二进制文件。二进制文件的优点在于存取速度快,占用空间小,以及可随机存取数据。

文件存取方式

包括顺序存取方式和随机存取方式两种。

顺序读取也就是从上往下,一笔一笔读取文件的内容。保存数据时,将数据附加在文件的末尾。这种存取方式常用于文本文件,而被存取的文件则称为顺序文件。

随机存取方式多半以二进制文件为主。它会以一个完整的单位来进行数据的读取和写入,通常以结构为单位。

C语言提供的标准文件

通常把显示器称为标准输出文件,printf就是向这个文件输出数据;

通常把键盘称为标准输入文件,scanf就是从这个文件读取数据。

image-20210713225726024

关键字

const

const在实际编程中用得比较多,const是constant的缩写,意思是“恒定不变的”!它是定义只读变量的关键字,或者说 const 是定义常变量的关键字。(可读,不可写)

const可以修饰变量,数组,指针等;说const定义的是变量,但又相当于常量;说它定义的是常量,但又有变量的属性,所以叫常变量。

用const定义常变量的方法很简单,就在通常定义变量时前面加 const 即可,如:const int a = 10;int const a = 10;而且可以修饰任何类型的变量,包括数组。

那么用const修饰后和未修饰前有什么区别呢?用const定义的变量的值是不允许改变的,即不允许给它重新赋值,即使是赋相同的值也不可以。所以说它定义的是只读变量。这也就意味着必须在定义的时候就给它赋初值。无论是全局常变量还是局部常变量都必须初始化赋值。

注意事项

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
int main()
{
const int a = 10;
int* ip = &a;//这种写法是错误的,会产生二义性,因为这样写的意义何在?你的a到底能变还是不能变!
//有一种办法能让此语句成功编译,就是强转类型:int* ip = (int*)&a;但是强转类型会出乱子。详看下文《一家网络公司的某个试题》。
//还有一种办法能成立,就是把左式声明部分加上const来修饰,即const int* ip = &a;这样做:ip就可以指向a了,但不可以通过ip改变a。而且这样做会导致下面的语句"*ip = 100"报错:“表达式必须是可修改的左值”。因为ip指向的值是拿const修饰的,此处就识别为不可修改了!
*ip = 100;
return 0;
}

总结:const不只能修饰普通类型、数组,还能修饰指针(比如修饰指针的其一种特性——修饰指针的指向能力,达到了保护的效果,就如const int* ip = &a,达到了通过ip只能读取a,不可写的效果)。以此类推,C语言还有:int* const is; const int* const ir;其中const int* const ir;的前一个指针是修饰指向不可改变,后一个const是修饰指针自身不可改变。

一家网络公司的某个试题

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
int main()
{
const int a = 10;
int b = 0;
int* ip = (int*)&a;
*ip = 100;
b = a;
printf("a = %d \n",a);
printf("b = %d \n",b);
printf("*ip => %d \n",*ip);
return 0;
}

*.cpp文件的运行结果如下,此结果是比较符合const这个关键字的功能的。

image-20210715011105297

但是,此运行结果却和调试过程中的显示结果不一致!

image-20210715010757756

前九行运行是符合普通人(没有深刻理解const)的思路的,a通过指针ip被赋值100。但奇怪的是:第十行运行后b=a;这个语句居然是给b赋值了10

另外,c文件的输出结果却是:

image-20210715012709734

这又是怎么一回事?详见下面《解释》的论述。

解释

cpp文件下的结果

*.cpp文件中,const变量和宏变量都是替换机制来实现的,但const是在编译时期替换的,宏变量是在预编译时期替换的。

image-20210715011908929
image-20210715012233908
再来一段汇编代码,发现a=b;这条语句并不是把a取值给b,而是用0Ah(10的十六进制)直接赋给b。

所以,无论输出前对a进行了哪些操作都是徒劳的,因为a早已“偷梁换柱”,所以就会出现了我们单步跟踪结果和最终输出结果不一致的情况。

上例给我们一个启发,不要相信你的眼睛,而要相信你所分析的代码。

C文件下的结果

C文件下的结果居然又成了a=100, b=100, *ip=100。接下来我们来分析:
image-20210715012838790
对于b=a;这个语句,我们可以看出:不像cpp文件那样用0Ah直接赋值,而是先取a的值再赋给b。因为*ip改变了a的值,所以b的值也将受到影响。

总结

对于被const关键字修饰的变量:在cpp文件下是编译时期用具体值替换变量名;而在c文件下,编译时期不替换。于是造成了输出结果的差异。

拓展–汇编与程序的联系

汇编层面下的赋值b=a;
cpu中包含着4个通用数据寄存器:叫做eaxebxecxedx。cpu在进行加减乘除、数据运算时不是在内存中运算的,而是把内存数据拉到寄存器中计算,因为内存的计算速度远比cpu计算速度慢。
所以,就出现了为何b=a;在汇编语言层面上是取出a的值给eax再由eax赋值给b,而不是a直接赋值给b。
image-20210715014252117
DMA方式,也叫做I/O协处理器。当我们内存要移动大块数据时,如果要介入cpu的话太影响运行效率,所以我们大块数据交给DMA(协处理器)进行数据移动,从而把CPU资源腾出来用于计算,提高效率。
汇编层面下的解引用*ip=100;
image-20210715014516247
首先把ip的值放入到寄存器eax中,那么eax的值就相当于a的地址,再往下,把64h(100的十六进制)给了[eax]。此时注意:mov eax,10;mov [eax],10;这两种访问方式不一样。前者是直接访问方式,把10给了eax;后者是间接访问方式,不是把10给了eax,而是把10给了eax所存放的某个地址里。因此,直接访问是一步汇编实现,间接访问是两步汇编实现,所以直接访问效率要高于间接访问。

sizeof关键字

已在上面章节论述。

此处再引入几个有启发的例子。
image-20210714003229156
此题运行后的size还是4,a还是10!可从汇编角度观察究竟!
image-20210715021922182
启示:sizeof只在编译时期计算而不是运行时期计算,相当于直接替换为4。因此++a这个++的动作就失去了意义,编译时就直接略去了++这个操作。因此后面a的值没有变化。
image-20210714003659077
此程序运行后的size大小为8,即double类型所占用的空间。因为在编译时期sizeof就识别到了(a+0.9)这个数值为double类型。
sizeof的本质是只关心你此处的数值最终的类型

sizeof和strlen的差异

image-20210715021416730
实际结果为:len为6,size为7。因为字符串实际占用了7个字节。字符串的最后是要有\0结尾的;而len的计算是:一碰到\0就结束计算。

typedef

typedef是在计算机编程语言中用来为复杂的声明定义简单的别名。它本身是一种存储之类的关键字,与autoexternmutablestaticregister等关键字不能出现在同一个表达式中。

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
typedef unsigned char u_int8;
typedef unsigned short u_int16;
typedef unsigned int u_int32;
typedef unsigned long long u_int64;
int main()
{
u_int8 a;//等效于unsigned char a;
u_int64 x;//等效于unsigned long long x;
return 0;
}

一定要记住:凡是合法的变量名声明、数据声明、指针声明,加一个typedef后我们就可以把变量名转换为类型名,把变量声明定义转换为类型声明定义。

1
2
3
unsigned int UNIT;//原来的变量名声明
typedef unsigned int UNIT;//UNIT由一个全局变量转换成了一个类型!
UNIT a;
1
2
3
int Array[10];
typedef int Array[10];//Array由全局变量转换成了一个“开辟10个空间的整型数组类型”
Array ar,br,cr;

有人这么认为:认为typedef是用替换原则来实现的,如把"Array"替换为"int[10]",即Array ar, br, cr;替换为int[10] ar,br,cr;,这是不对的。它是一种类型的声明概念。

1
2
3
int* PINT;
typedef int* PINT;
PINT p,s;//p,s是什么类型呢?回到typedef int* PINT;,把typedef去掉即可得知——p和s都是int*类型。

上例中,PINT p,s;int* x,y;是有区别的!我们在编译器中发现,x是整型指针变量,而y是整型变量。因为声明时,我们的星号*要和标识符结合,而不是和类型名结合。所以当我们涉及到为多个指针声明时,用到typedef的方式就特别好。
image-20210715121438344

1
2
3
4
5
6
7
8
9
10
struct ListNode
{
int data;
ListNode* next;
};
ListNode* LinkList;//原本是一个“某一结构体类型的指针变量”
typedef ListNode* LinkList;//由全局结构体指针变量转变为了一个类型
//如此定义,那么以下两句就等效
ListNode* p;
LinkList s;
1
2
3
4
5
6
7
8
9
10
11
12
13
//此时,LinkList原本是一个“全局结构体类型的指针变量”
struct ListNode
{
int data;
ListNode* next;
}*LinkList;
//我们也加上一个typedef,现在,LinkList就不再是一个指针变量了,而是一个类型。
typedef struct ListNode
{
int data;
ListNode* next;
}*LinkList;
//实际上,以上这种typedef直接在定义结构体时加在前面的写法和typedef ListNode* LinkList;这种写法也是等价的。
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
//再来一个奇怪的例子:
//首先,我们知道,int int;是不可能这样声明定义变量名的。因为他拿关键字(基本数据类型名)来起名字。
//但如果没拿关键字起名字的话,变量名和类型名可以一样吗?
struct ListNode
{
int data;
ListNode* next;
};
ListNode ListNode;//是可以编译通过的,最多就是警告。
//答案是可以一样!因为此类型名、变量名并非关键字,只要符合标识符的命名规范,是没有任何影响的!

//因此,下例的ListNode原本是一个“结构体类型的变量名”
struct ListNode
{
int data;
ListNode* next;
}ListNode;
//如今我们在结构体定义前加一个typedef,他就可以变为了一个类型名!
typedef struct ListNode
{
int data;
ListNode* next;
}ListNode;
//那么,
typedef ListNode ListNode;//就与之等效。
//这种定义方式常常出现在开源项目中,迷惑调试器,追踪变量时搞混变量名和类型名。为的就是不让你看懂。
1
2
3
4
5
6
7
8
9
10
11
12
13
//如此,理解了typedef的含义。我们可以如下使用
typedef struct ListNode
{
int data;
ListNode* next;
}ListNode,*LinkList;
int main()
{
ListNode a;
LinkList p;
p = &a;
return 0;
}
1
2
//也可用作对枚举类型的定义
typedef enum{OK = 0,ERROR = 1} Status;

static

修饰局部变量

静态关键字对于局部变量来说是延长寿命的。也就是说,函数只有被调用时,局部变量才会被初始化,函数调用结束时会释放掉所有局部变量。如果我们给局部变量加上了static修饰,那么函数在第一次被调用时初始化了局部变量,即使调用结束后,局部变量也不会释放,即局部变量的寿命比函数要长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<stdio.h>
void fun(int x)
{
int a = x;
int b = 0;
++a;++b;
printf("a : %d b = %d \n",a,b);
return;
}
int main()
{
for(int i=10;i>0;--i)
{
fun(i);
}
return 0;
//将输出:
//11 1
//10 1
//9 1
//...
//2 1
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<stdio.h>
void fun(int x)
{
static int a = x;//这一步是定义a并对a初始化,只执行一次。
int b = 0;
++a;++b;
printf("a : %d b = %d \n",a,b);
return;
}
int main()
{
for(int i=10;i>0;--i)
{
fun(i);
}
return 0;
//将输出:
//11 1
//12 1
//13 1
//...
//20 1
//此输出结果说明,虽然fun(i)一直在给x传参赋值给a,但是实际上是无效的,因为声明定义语句已经在第一次调用fun函数进行了,“一劳永逸”了
}

对于第二段代码:在函数被第一次调用时,我们创建a,并把它存放到数据区(data area)。函数结束后,a也不释放。第二次调用时,static int a = x;这一语句不再执行。这一步是定义a并对a初始化,只执行一次。但是写成a = x;就不一样了。a = x;是一句执行语句,在每次函数调用也还是要执行的!

但,static修饰局部变量不改变其作用域。

修饰全局变量

而static修饰全局变量时,不改变其生存期,而改变其可见性(只能在当前cpp文件中可见,即使其他文件中的代码拿extern来修饰这个变量,同一工程的另外cpp文件也不可见)。

extern

extern用在全局变量或函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。

假如我们同一个工程下有两个cpp文件,其中有一cpp文件用到了另一个cpp文件的内容。从理论上讲,两个cpp文件各自编译后形成了各自的obj文件,之后链接到了一个exe文件下(这个exe文件就是以工程为单位生成的),理论上,Test7_10文件中的g_maxfun()可以自然地调用yhp文件中的变量、函数。编译是可以进行的,但是,生成是不通过的。

image-20210715124558882

image-20210715125037821

我们这时就需要在Test7_10.cpp中添加代码了:如下才能运行成功。
image-20210715125337615
如果在一个cpp文件中,我们不想让其他文件用extern来调用自己的内容,我们可以在这些内容前加上static修饰(需要是全局变量)。因为,在全局变量前加static会改变其可见性。(只能在当前cpp文件中可见,即使其他文件中的代码拿extern来修饰这个变量,同一工程的另外cpp文件也不可见)。

而如果一个cpp文件已经拿static修饰了变量名,另一个cpp还用extern来引用的话,就会失败,这时出现了下面要论述的技术,也就是让这个引用失败的机制——名字粉碎技术。

名字粉碎技术

image-20210714175813243

VS2019的使用

创建项目

image-20210712150544136

image-20210712150616048

image-20210712150647588

image-20210712150735990

image-20210712150755672

image-20210712150817409

image-20210712150827045

scanf的注意事项

S2019中不再使用scanf,而是换为了scanf_s
输入数据时,要严格按照双引号内的格式来输入。

为何要用scanf_s替换scanf

scanf不安全。比如我们定义了一个int a = 10; char buff[8];
如果用scanf("%s",buff);输入字符串超过8位时,比如输入yhpingaaaaaaaa,会把a中的信息“冲掉”。(仅限于VC++ 6.0这种比较古老的编译器中)
最后用printf("a=%x",a);查看a时,发现a=61616161。61是十六进制,转为十进制为97,表示字符'a'的ASCII码值。所以此处就体现出了scanf对内存的不安全性。
scanf_s则避免了这个问题,我们可以在输入时加一个参数:scanf_s("%s",buff,8);限制输入字符串的长度。

有启发意义的代码实验

image-20210713192127424

image-20210714143758025

首先我们要清楚16进制与2进制之间的关系,才能清楚存储空间是如何存储数据的。存储空间中对数据的存放是以十六进制形式存储的,接下来是对此的举例解释:

比如int占了4个字节,等于占了32个二进制位,而4个二进制位可以转换为1个16进制位,于是int存储的数据可以由32/4=8位16进制位来表示。比如我们int i = 10;那么在存储空间中就是(0a 00 00 00)16。十进制的10转换成了十六进制的a。

接下来我们再来解释为何输出了"a=61616161"

函数中的变量是存放在栈区的,因为先定义了a,栈底开辟a的空间,a占用4个字节先存放数值12。而后,char类型的buff[8]数组占用a之上的8个字节;scanf输入%s时,不直接存放字符串,而是存放字符串的ASCII码值,比如字符'a'的ASCII码值是91,再转换为十六进制值61。在存放了一部分字符串"yhpingaa"之后,原本在内存中给buff数组存放的空间不足,剩下的"aaaaaa"就由上到下(小端存放方式–高位地址存高位数据,低位地址存低位数据)覆盖到了内存中给a变量开辟的存储空间的4个字节中去了,并且还有可能占用了a空间以外的未知内存资源中,因为int a只能存放4个字节即4个字符值,而剩余a的数目超过了4个。