快速入门 C 语言 [上] (非常详细!!)...
目录
1. 基本数据类型
2. 变量
2.1 定义格式 和 命名规范
2.2 格式化输入和输出(scanf 和 printf)
编辑
2.3 作用域和生命周期
3. 常量
4. 字符串+转义字符+注释
5. 操作符
5.1 双目操作符
5.1.1 算数操作符
5.1.2 移位操作符
5.1.3 位操作符
5.1.4 赋值操作符
5.1.5 关系操作符
5.1.6 逻辑操作符
5.1.7 下标引用、函数调用和结构成员
5.2 单目操作符
5.2.1 不改变原操作数
5.2.2 改变原操作数
5.3 三目运算符
5.4 逗号表达式
5.5 操作符的属性
6. 整形提升和类型转换
7. 分支语句和循环语句
7.1 分支语句(选择结构)
7.1.1 if语句
7.1.2 switch语句
7.2 循环语句
7.2.1 while
7.2.2 for
7.2.3 do...while
7.2.4. goto语句
7.3. 一段有趣的代码
1. 基本数据类型
也叫 内置类型 ,即编程语言自带的基本数据类型,无需额外定义或导入,直接就能定义变量,如下:
1:char //字符数据类型
典型的比如 ASCII 字符表:
第一部分:(看看就行)
第二部分:常用的就三小段:字符0 ~ 9,A ~ Z,a ~ z;只需记住0,A,a对应的十进制,往后递增加1,大小写间的十进制差是32。并且 char 类型(字符类型)的数据在参与表达式运算时用其对应的十进制数,所以也把它被归为 整形 一类,即数学中的整数。
接着是:每个 char 类型数据的大小为1个字节(byte)。
字节是计算机划分数据存储的基本单位,1个字节又划分成8个比特位(bit),即8个0或1组成的串, 比如,ASCII表用二进制表示就是:00000000 ~ 01111111;那么8个比特位能表示的数据范围就是 00000000~11111111,即0~255,所以是不是说 char 可表示的十进制数据范围就是0~255?
其实不是的。
char 可定义变量的有效数据范围是:-128 ~ 127;0~255指 unsigned(无符号的) char 的范围,这是一种 无符号型的数据,char是有符号的。现在你只需要知道的是:不同类型可定义变量的有效数据范围是不同的,范围外的就叫 “溢出”。
同属整形家族还有:
2. short //短整型,2byte == 16bit,范围:-2^15 ~ 2^15 - 1,即-32768 ~ 32767
unsigned short 范围 0 ~ 2^16,即0~65535
3. int //整形,定义整数常用,4byte == 32bit,范围:-2^31 ~ 2^31 -1,即:正负21亿多
unsigned int 范围 0~2^32,即42亿多;但是经常使用的是:size_t(无符号整形),注意 其在32(x86)位系统下,4个字节;在64(x64)位系统下,8个字节。
4. long //长整形,在 32(x86)位系统上,通常是 4 byte;在 64(x64)位系统上, 通常是 8 byte
5. long long //更长的整形,8byte
然后是浮点数,和整形的规则不同,现在大家把它们当 普通小数类型 来用就行
6. float //单精度浮点数,4byte
7. double //双精度浮点数,8byte
8. 指针类型(后面讲)
2. 变量
2.1 定义格式 和 命名规范
定义格式:数据类型 变量名;
形象的解释一下就是:拿着图纸造房子 ——> 不同的设计图纸对应不同的数据类型,造出来的房子是一个个实体呀,属于开了物理空间的,其大小就对应着设计图纸的参数,也就是数据类型的大小,比如int有4个字节,char只有一个字节; 至于这个房子里放什么,可能随着时间一直在变化,所以称 这个房子 就是个变量;但不管你放什么,都不能超过其大小,比如这个房子的大小就200平,你偏要往里修个1000平的游泳池,放不下呀,也就是我们前面说的 “溢出”了;还有,这个房子 最后叫什么名,也不是设计图纸要管的事,属于开发商的自定义,即 变量名。
【***.c文件叫源文件,就是写代码的地方;一切的字母和符号都要用 英文输入法;“ ; ” 是一条语句结束的标志,不可遗漏】
比如,在 test.c 源文件中:
int main()//程序执行入口,只能有一个,一定要写!!!
{
char a, b = 'b', c, d;//定义并局部初始化;定义多个变量用逗号隔开;单个字符的使用要用单引号' '
int age = b-18;//定义并赋值(=)初始化,这是好的编程习惯;编译器默认向上查找变量b,就是说:一定要:先定义,再使用!!!
float weight = 53.5f;//小数默认为double类型,定义float类型可带后缀f
return 0;//返回执行结果
}
命名规范有以下要求:
1.只能由字母(包括大写和小写)、数字和下划线( _ )组成。
2.不能以数字开头。
3.长度不能超过63个字符。
4.变量名中区分大小写的。
5.变量名不能使用关键字(语言本身预先设定好的),比如:
2.2 格式化输入和输出(scanf 和 printf)
包含头文件:#include<stdio.h>
(关于<头文件(***.h)>,现在你只要知道是:C语言本身已经写好的东西(比如下面的scanf和printf)都是被打包好的,你安装C编译环境时,它们就被下载到你的本地PC上;你要用的时候,就要告诉编译器去哪里找,方式就是:#include<****.h>)
输入:scanf
(如果你用Visual Studio,加上:#define _CRT_SECURE_NO_WARNINGS 1 否则错误信息会建议你使用 scanf_s,但这个是微软自己搞的,不属于官方原生)
从stdin(标准输入流,就是你的键盘输入,其实被放到一个类似文件的管理系统中,我们在屏幕上看到的输入就是从这个系统里读取出来,再呈现给我们)读取数据,并根据参数格式将其存储到附加参数(指针变量)所指向的位置,也就是我们定义好的变量。
比如:
关于这个字符序列(C字符串):
空白字符: 该函数将读取并忽略下一个非空白字符之前遇到的任何空白字符(空白字符包括空格、换行符\n和制表符\t。
非空白字符,除了格式说明符(%): 都会导致scanf函数从流中(键盘)读取下一个字符,将其与该非空白字符进行比较,如果匹配,则丢弃该字符,继续读下一个字符。如果字符不匹配,则函数失败,返回并保留流的后续字符未读。
格式说明符: 由初始百分比符号(%)组成的序列表示格式说明符,用于指定要从流中检索的数据的类型和格式,并将其存储到附加参数所指向的位置。
常用的有:%c —— 字符类型
%d —— 有符号十进制整数
%u —— 无符号十进制整数
%f —— 十进制单精度浮点数
%lf —— 十进制双精度浮点数
%s —— C字符串
%x —— 无符号十六进制整数(0~9,a~f,A~F),一般以0x或0X开头(0是数字零)
%o —— 无符号八进制整数(0~7)
至于什么意思,看下面的例子:
也就是说,我们的输入根本就不是我们以为的整型,浮点型,而都是文本字符,程序是根据 格式说明符 来处理读到的字符,修改变量内容的。
scanf 格式说明符的完整原型其实是下面这个:
%[*][width][length]说明符
其中:* 表示一个“忽略”操作,即从stdin中读取字符,但是不对变量内容进行修改
width:宽度,指定当前读取操作中要读取的最大字符数(加上终止符\0),遇到空格字符提前结束读取(可选)
length:指的是数据类型的长度修饰符,用于指定数据类型的大小,以便于正确地读取或打印数据;不同的 length
修饰符影响与类型相关的转换。其通常和说明符搭配使用:
(黄色行表示C99引入的说明符和子说明符)
现在给大家举个测试例子:
大家平常的使用场景,一般用不到[*][width][length],直接 "%+常用说明符" 就行,但这些丰富的用法总得见一见,学一学,用一用。
接着是格式化输出:printf
和scanf不同,其作用是:把C字符串写到stdout(标准输出流),即 屏幕上。
关于这个C字符串,有两种情况:
情况一:不包含格式说明符(%):
也可以输出中文字符:printf("你好,我的名字是***")
情况二:包含格式说明符(%),则其后面的附加参数(变量的值)将被格式化并插入到结果字符串中,以替换它们各自的说明符。常用格式说明符和scanf的一样,这里再加几个:
%p —— 指针(地址)
%e(小写)和 %E(大写)—— 科学计数法,通常对浮点数使用。
比如:12345.6789 科学计数法表示为:1.23456789 x 10^4
%e格式输出就是:1.234568e+04 【默认保留6位小数,并且四舍五入;e+04表示将小数点向右移动 4 位,就是 12345.680000,较原数产生了精度损失】
-0.0034 科学计数法表示为:-3.4 x 10^-3
%E格式输出就是:-3.400000E-03 【默认保留6位小数,并且四舍五入;E-03表示将小数点向左移动 3 位,就是 -0.003400,较原数没有精度损失】
如下示例:
int main()//执行入口
{
int age = 18;
double weight = 56.5;
const char* name = "ZhangSan";//常量字符串的定义初始化,先学着用
printf("Name=%s;Age=%d;Weight=%lf\n", name, age, weight);
printf("addree(age)=%p\n", &age);
double d1 = 12345.6789;
double d2 = -0.0034;
printf("%e, %E\n", d1, d2);
return 0;
}
输出:
(至于为什么地址是一串数字编号,我们后面再看,现在你只要知道是什么就行)
printf的格式说明符遵循以下原型:
%[flags][width][.precision][length]说明符
和scanf一样,末尾的说明符字符是最重要的组成部分,其它的都是子说明符,属于可选可不选。
1:先来看[width]:(每个输出只能二选一)
number:要打印的最小字符数。如果要打印的值比这个数字短,结果将用空格填充。即使结果更大,值也不会被截断。
*: 宽度不是在格式字符串中指定的,而是作为必须格式化的参数之前的附加整数值参数指定的。
比如:
并且通过num1的输出可以发现:默认右对齐
如果要左对齐,就要添加 flags 标志。
2:[flags](可任意组合使用)
2.1 如果不写入任何符号,则在值之前插入空白,不是空格
2.2 ‘ - ’:左对齐
2.3 ‘+’:即使是正数,也强制在结果前加上正号(+),因为默认情况下,只有负数前面有-号
比如:输入一个整数,要求输出占20个字符,左对齐,带正负号
int num = 0;
scanf("%d", &num);
printf("num=%-+20d\n", num);//或者printf("num=%+-*d\n", 20, num);
如果就是要求默认的右对齐,但是不够的字符要指定用零(0)而不是空格左填充,就要用符号:
2.4 ‘0’ 字符零,修改为如下:
printf("num=%+020d\n", num);//或者printf("num=%0+*d\n", 20, num);
(注意:左对齐时,指定填充就失效了)
2.5 ‘#’:
2.5.1 与o、x或X说明符一起使用时,对于不同于零的值,值的前面分别加0、0x或0X
比如:
int num4 = 100;
printf("%#o, %#x, %#X\n", num4, num4, num4);
输出:0144, 0x64, 0X64
2.5.2 如果与 g 或 G 一起使用时,即使后面没有更多数字,它也会强制写入输出包含小数点;说明符‘g'和'G'的意思是:使用最短形式表示浮点数。
比如:
double num5 = 100.00000;
printf("%lf, %g, %#g\n", num5, num5, num5);
float num6 = 100.12056;
printf("%lf, %G, %#G\n", num6, num6, num6);
输出: 'g'说明符会根据数值的大小自动选择使用 常规小数形式(%f
)或 科学计数法(%e
),以确保输出尽可能简洁。具体来说,%g
会根据浮点数的大小和精度选择最合适的格式,去除不必要的尾随零和小数点。
3. [.precision] 精度 (每个输出只能二选一)
.number
对于整数说明符(d, u, o, x, X):效果相当于[flags]的'0'指定左填充;精度为0表示无操作。
(常用)对于浮点数(l, lf, e, E): number就是小数点后要打印的位数(默认是6位)
对于'g'和’G'说明符:有效数字的最大数目
对于's'说明符:这是要打印的最大字符数。默认情况下,将打印所有字符,直到遇到结束的\0字符。 如果指定的周期没有明确的精度值,则假定为0
.*:精度不是在格式字符串中指定的,而是作为必须格式化的参数之前的附加整数值参数指定的
看下面的例子:
int num7 = 10;
printf("%.0d,%.1d,%.*u\n", num7, num7, 10, num7);
float num8 = 67.34508;
printf("%f, %.2f; %E, %.*E\n", num8, num8, num8, 3, num8);
printf("%g, %.1g, %.*g, %.4g\n", num8, num8, 2, num8, num8);//有效数字是从左往右第一个非零数开始
const char* str = "hello world!";
printf("%s, %.s, %.3s, %.20s", str, str, str, str);
输出:
4:[length] 长度
和前面scanf的[length]用法大致一致,此处不再赘述,贴一张参考表格:
2.3 作用域和生命周期
变量分为:局部变量(变量所在的局部范围) 和 全局变量(整个项目代码)
举个例:
int global = 10;//全局变量
int main()
{
int local = 20;//局部变量
printf("global=%d, local=%d\n", global, local);
return 0;
}
总结一下就是:{ } 就叫一个局部域,这个域里定义的变量就叫局部变量,这些变量只能在这个域里使用(作用域),出了这个域,这些变量就会被销毁,即所占空间还给操作系统,不受你的代码程序管理了,从创建到销毁的时间段就叫做这个变量的生命周期;不在任何{}的变量,就是全局的,可被任意一个局部域使用,且其生命周期是程序结束时。
其次,一个局部域可以通过{ }被不断划分成多级局部域;并且同级域内不能重定义(重名),全局域也是如此;不同级局部域之间可以重名,因为生命周期不同。
如下示例:
最后,当局部变量和全局变量冲突时(重名),局部优先。
int agc = 10;
int main()
{
int agc = 20;
printf("agc=%d\n", agc);//输出:agc=20
return 0;
}
3. 常量
不同于变量,比如:年龄,体重,薪资等; 生活中的有些值是不变的,比如:圆周率,身份证号码,血型,......
C语言中的常量和变量的定义的形式有所差异,分为以下以下几种:
1. 字面常量:比如:3.14,100,'a',"hello world!",......
2. const 修饰的常变量:本质还是变量,但是不能直接通过变量名修改其内容,比如:
修饰局部变量:
int main()
{
const int a = 10;//局部变量
a = 20;//报错
return 0;
}
修饰全局变量:它会默认具有内部链接属性,即变量只在定义它的源文件中可见,其他源文件无法引用,看对比:
3. #define 定义的标识符常量:就一个词 “直接替换”,比如:
#define NUM1 10
int main()
{
#define NUM2 20
int a = NUM1 + NUM2;//处理成:int a = 10 + 20;
printf("%d\n", a);//输出:20
return 0;
}
4. 枚举常量:整形常量,关键字(enum),可穷举,比如:
enum Colour//颜色
{
Red = 2,
Green = 4,
Yellow = 8
};
int main()
{
enum Option//选项
{
Stop,//默认为0,往下递增+1
Pass,
Caution
};
printf("%d :> %d\n%d :> %d\n%d :> %d\n", Red, Stop, Green, Pass, Yellow, Caution);
return 0;
}
输出:
这里只是简单介绍让大家能看懂,学着用;实际的运用场景是更丰富,更灵活的,需要不断的项目代码积累量,才能体会。
4. 字符串+转义字符+注释
关于 字符串,前面已经介绍过并使用过了,这里就简单提一下:
用双引号 " " 引起来的一串连续字符,结束标志是'\0'
定义一个常量字符串:const char* str = "*********";
输出:printf("%s\n", str); 或者 printf(str);
一个常用的库函数:strlen,返回字符串的长度,即字符数,不包含‘\0'
包含头文件:#include<string.h>
如下代码:
int main()
{
const char* str = "hello world!";
size_t len_str = strlen(str);
printf(str);
printf("\nlen_str=%u\n", len_str);
return 0;
}
输出:
下面,我们来看 转义字符:顾名思义就是转变意思
常见的有:
注:
三字符序列是 C 语言标准中为支持某些不包含全套 ASCII 字符集的键盘而引入的一组字符组合,这些组合会被编译器解释为其他符号。例如,??)
被解释为 ]
,而 ??(
被解释为 [
。为了防止这些三字符序列在代码中意外地被编译器解释为其他符号,C 语言提供了 \?
这个转义字符。虽然三字符序列在现代编译器中已经很少使用,但 \?
作为一个转义字符仍然存在,以避免与这些序列发生冲突。
看下面的例子:
int main()
{
char c1 = '\'';
printf("%c\n", c1);
const char* str1 = "\"hello\", \"world\"\f";
printf(str1);
printf("\nD:\code\test.c\n");
printf("D:\\code\\test.c\n\n");
int i = 0;
for (; i < 10; i++)
{
printf("\a");
}
//\b将输出光标向左移动一个字符,但不会删除字符,但是会被紧接着的输出覆盖
printf("start test char of '\\b'!\b\n");
printf("start test char of '\\b'!\b\b\b\n");
printf("start test char of '\\b'!\b\b\b\b\b\b\b****\n\n");
printf("start test char of '\\r'!\n");
printf("start test char of '\\r'!");
printf("\rstart test\r char of '\\r'!\n\n");//覆盖
printf("\130\t\x30\test\n");//补空格
return 0;
}
输出:
(注意:输出的都是单个字符)
注释:
其实在前面的示例中小编就一直在用,目的之一就是:有些代码需要标注一下是干啥的;或者代码太多,避免逻辑混乱;再或者你要把代码给别人看/用,减少他人的理解成本;...... 这绝对是个利己利他,调高效率的编程好习惯!
另一个常见用途是,有的代码需要拿来做测试,但是发行版本又不需要,那么此时可以不用删除,注释掉就行。
注释有两种风格(C/C++都能用):
C语言风格的注释: /*xxxxxx*/
缺陷:不能嵌套注释,即每个/*配对最近的*/
C++风格的注释: //xxxxxxxx
可以注释一行也可以注释多行
再简单举个例子:
int main()
{
/*
int age = 10;
const char* name = "zhangsan";
double weight = 56.8;
*/
//接下来是一段测试代码,探究scanf和printf
int a = 10;//定义变量a并初始化为10
scanf("%2d", &a);//只读取2个字符,当整数处理
printf("%-+10.6o\n", a);//输出:左对齐,带正负号,占10位,精度为6,以八进制输出
//......
return 0;
}
当然,这里只是演示,告诉大家怎么用,实际中的注释应该尽量言简意赅,重点突出!
5. 操作符
是由操作数(如变量、常量)和操作符组成的一段代码,就叫 表达式,它会被计算并返回一个值;单独的一个操作数也可以被视为一个表达式,这种表达式的计算结果就是操作数本身。
5.1 双目操作符
即,需要左右两个操作数。
5.1.1 算数操作符
+(加) -(减) *(乘) / %(取余数)
%操作符只能用于整数间,比如:5%2 = 1;其它的整数,浮点数都行。
对于 / 操作符:如果两个操作数都为整数,执行整数除法 取模(商),比如:5/2=2
只要有一个操作数是浮点数,则执行正常的浮点数相除,比如:5/2.0=2.5
5.1.2 移位操作符
<< 左移操作符
>> 右移操作符
移位操作符的操作数只能是整数,位指 “比特位”
需要预备知识:《C语言---数据的存储》点2.1和点2.2
先看:<< 左移操作符
代码验证一下:
int main()
{
int num1 = 10;
int num2 = num1 << 2;
printf("num1=%d, num2=%d\n", num1, num2);
return 0;
}
输出:
接着看:>> 右移操作符
代码验证一下,你当前的编译器是逻辑右移,还是算数右移:
int main()
{
int num3 = -1;
int num4 = num3 >> 2;
printf("num3=%d, num4=%d\n", num3, num4);
return 0;
}
小编当前演示示例是算数右移:
警告: 对于移位运算符,不要移动负数位,这个是标准未定义的。
5.1.3 位操作符
这里的位,同样是指 “比特位”。两个操作数也必须是整数。
&:按位与,有0就是0
| :按位或,有1就是1
^:按位异或,相同为0,相异为1
需要预备知识:《C语言---数据的存储》点2.1和点2.2
如下示例:
代码验证一下:
int main()
{
int num5 = -1;
int num6 = 2;
int num7 = num5 & num6;
int num8 = num5 | num6;
int num9 = num5 ^ num6;
printf("num5=%d, num6=%d, num7=%d, num8=%d, num9=%d", num5, num6, num7, num8, num9);
return 0;
}
输出:
运用示例:实现两个整数的交换
int main()
{
int a = 0;
int b = 0;
scanf("a=%d, b=%d", &a, &b);
//思路一:创建中间临时变量tmp
int tmp = a;
a = b;
b = tmp;
//要求:不能创建其它变量
//思路二:运用加减法
a = a + b;
b = a - b;//b=a+b-b=a
a = a - b;//a=a+b-a=b
//(重点!!!)思路三:利用 ^
a = a ^ b;
b = a ^ b;//b=a^b^b = a ^ 0=a;特性1:两个相同值^为0,并与顺序无关;特性2:任何值与0异或,都不变
a = a ^ b;//a=a^b^a=b^0=b
printf("a=%d, b=%d\n", a, b);
return 0;
}
5.1.4 赋值操作符
=
前面就一直在用,覆盖变量原先的值
double f1 = 34.5;
f1 = 70.0;
int a = 1;
int b = 2;
int c = 3;
a = c = b * a + 4;//连续赋值,从右往左
//但是同样的语义,可以写成:(更加清晰爽朗而且易于调试,推荐)
c = b * a + 4;
a = c;
复合赋值符:
+=:a += b 就是 a = a + b;
同理还有: -=,*=,/=,%=,>>=,<<=,&=,!=,^=
5.1.5 关系操作符
> 大于 >=大于或等于
<小于 <=小于或等于
!=不等于 ==等于
关系比较的结果:0为假,非零为真
如下代码:
int main()
{
int a = 10;
int b = 20;
printf("%d, %d, %d, %d, %d, %d\n", a > b, a >=b, a < b, a<=b, a != b, a == b);
return 0;
}
输出:
5.1.6 逻辑操作符
&& 逻辑与:左右两个表达式同时为真(非0)才返回真,否则为假(0)
|| 逻辑或 :左右两个表达式只要一个为真,就返回真
(从左往右)
如下代码:
int main()
{
int c = 20;
int d = 30;
int e = c < d && c == 20;
int f = c >= d && d == 30;
int g = e || f;
int h = f || d <= c;
printf("%d, %d, %d, %d\n", e, f, g, h);
return 0;
}
输出:
5.1.7 下标引用、函数调用和结构成员
分别在数组,函数,结构体部分讲解。
5.2 单目操作符
即,只需要一个操作数。
5.2.1 不改变原操作数
- 负值:将正值变负,负值变正
+ 正值:不能把负值变正,表示该数值为正,为了代码的可读性或语义明确性,但较少使用
! 逻辑反操作:真变假,假变真
~ 对一个数的二进制按位取反:0变1,1变0
sizeof 操作数的类型长度(以字节为单位)
(类型) 强制类型转换
&(取地址)和 *(解引用)在 指针 部分细讲。
如下代码:
int main()
{
int a = 10;
int _a = -a;
int b = -20;
int _b = +b;
int c = _a > b;
int c1 = !c;
int c2 = !c1;
int d = 1;//二进制:0~1
int _d = ~d;//二进制:1~0,输出十进制为:-2
printf("%d, %d; %d, %d; %d, %d, %d; %d, %d\n", a, _a, b, _b, c, c1, c2, d, _d);
//常用:sizeof,注意我的书写形式
int e = sizeof(char);
double f = 3.14;
int g = sizeof(f);
printf("%d, %d\n", e, g);
printf("%d, %d, %d\n", sizeof(int), sizeof(a), sizeof a);
//强制类型转换(相近类型: 整形,浮点型,指针)
int num1 = 10;
double num2= (double)(num1 / 4);
double num3 = (double)num1 / 4;
printf("%lf, %lf\n", num2, num3);
return 0;
}
输出:
5.2.2 改变原操作数
前置/后置: ++ , -- ;每次+/- 1
直接来看例子:
int main()
{
int a = 10;
int b = ++a;//前置++:a先加1:a = a +1==11;然后再把a赋值给b,为11
printf("a=%d, b=%d\n", a, b);
int c = b++;//后置++:先使用,把b赋值给c, 为11;b再加1:b = b+1==12
printf("c=%d, b=%d\n", c, b);
//前置/后置--,也是同样的道理
int d = --c;
printf("d=%d, c=%d\n", d, c);//10, 10
int e = d--;
printf("e=%d, d=%d\n", e, d);//10, 9
return 0;
}
输出:
上点难度(如果对运算符的优先级和结合性存疑,先去看一下点5.5):
//输出结果是什么?
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && --b || d++;//从左往右依次判断:表达式"a++":先使用,a==0,为假,a再加1==1,那么(a++ && --b)为假;接着看"d++",为真,所以i=1
printf("i=%d, a=%d, b=%d, c=%d, d=%d\n", i, a, b, c, d);//1,1, 2, 3, 5
i = --d && a-- || ++c;//"--d"先减1==4,再用,为真;"a--"先用==1,为真,那么(--d && a--)为真,后面的不再判断,i = 1, a再减1==0
printf("i=%d, a=%d, b=%d, c=%d, d=%d\n", i, a, b, c, d);//1, 0, 2, 3, 4
//计算
i = c++ + b-- * --d - ++a;//c==3,先用其+(b--*i), 再c加1==4;其中,b-- * --d ==6,b==1, d==3;则(c++ + b-- * i)为9;++a为1,那么i=9-1==8
printf("i=%d, a=%d, b=%d, c=%d, d=%d\n", i, a, b, c, d);//8, 1, 1, 4, 3
return 0;
}
输出:
5.3 三目运算符
也叫 条件表达式: exp1 ? exp2 : exp3;
如果表达式exp1为真,则执行并返回表达式exp2的结果,否则返回exp3的结果。
如下代码:
int main()
{
int a = 10;
int b = 20;
int c = a >= b ? a = b : a += 1;
printf("c=%d, a=%d, b=%d\n", c, a, b);
int d = a <= b && a != b ? a = b : b - 1;
printf("d=%d, a=%d, b=%d\n", d, a, b);
return 0;
}
输出:
5.4 逗号表达式
就是用 ( ) 括起来,并用逗号隔开多个表达式:(exp1, exp2, exp3, …, expN);
从左向右依次执行,整个表达式的结果是最后一个表达式的结果。
如下代码:
int main()
{
int a = 10;
int b = 20;
int c = (a--, b + a, --b, a += a, b);//(10, 20+9==29, 19, a = a+a==9+9==18, 19) == 19
printf("a=%d, b=%d, c=%d\n", a, b, c);
return 0;
}
输出:
5.5 操作符的属性
复杂表达式的求值有三个影响的因素:
1. 操作符的优先级。即,先算哪个后算哪个,比如a+b*c,先算乘再算加
2. 操作符的结合性。比如: 判断 a!=b,从左往右;赋值 a=b,从右往左
3. 是否控制求值顺序。比如:(a+b)*c,先算括号里的加,再算括号外的乘
点3是我们自己控制的,C语言只规定了点1和2,如下表:
两个相邻的操作符先执行哪个?取决于他们的优先级;如果两者的优先级相同,取决于他们的结合性。
但是还是会产生一些问题表达式:不能确定唯一的计算路径!
比如:a*b + c*d + e*f
在计算的时候,由于*比+的优先级高,只能保证,*的计算是比+早,但是优先级并不 能决定第三个*比第一个+早执行 ,因此此表达式的计算路径可能是:
1:
a*b
c*d
a*b + c*d
e*f
a*b + c*d + e*f
2:
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f
如果,abcdef都是单个变量/常量值且互不影响,那么不管哪个顺序都不会影响最终结果;
但是,如果是复合表达式呢?
比如:c + --c;
操作符的优先级只能决定自减--的运算在+的运算的前面,但是我们并没有办法得 知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
同样的还有:int i = 10; int ret = i-- - --i * ( i = -3 ) * i++ + ++i;
int i = 1; int ret = (++i) + (++i) + (++i);
那么,怎么算,就要看具体的编译器了,我们来对比一下VS2022和gcc:
int main()
{
int c = 10;
int d = c + --c;
printf("c=%d, d=%d\n", c, d);
int i1 = 10;
int ret1 = i1-- - --i1 * (i1 = -3) * i1++ + ++i1;
printf("i1=%d, ret1=%d\n", i1, ret1);
int i2 = 1;
int ret2 = (++i2) + (++i2) + (++i2);
printf("i2=%d, ret2=%d\n", i2, ret2);
return 0;
}
输出对比:
所以,我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
6. 整形提升和类型转换
已经总结在小编的另一篇文章:《C语言---数据的存储》点击直达 (到这,你已经可以通篇学习此链接文章了)
7. 分支语句和循环语句
程序的执行流程有3种结构,分别是:顺序结构(从上往下,依次执行),选择结构(条件判断),循环结构
7.1 分支语句(选择结构)
7.1.1 if语句
语法结构:
//无分支
if (表达式)
{
语句;
}
//单分支
if (表达式)
{
语句1;
}
else
{
语句2;
}
//多分支
if (表达式1)
{
语句1;
}
else if (表达式2)
{
语句2;
}
//......
/*
else if (表达式n)
{
语句n;
}
*/
else
{
语句;
}
注意:1:条件判断,即为表达式的真假,0表示假,非0表示真,真则执行语句块{ };并且之后的所有分支都不进行判断执行。
2:如果不加 { }, 则表达式为真后 只默认执行一条语句!
如下示例代码:
int main()
{
int opt1 = 0;
printf("请选择:1(继续) / 0(结束):>");
scanf("%d", &opt1);
if (1 == opt1)//"=="判断是否相等
{
int opt2 = 0;
printf("请选择:1(好好学习) / 2(有米,不慌) / 3(摆烂):>");
scanf("%d", &opt2);
if (1 == opt2)
{
printf("好Offer!\n");
}
else if (2 == opt2)
{
printf("继承家产!\n");
}
else
{
printf("要去卖红薯了哟!\n");
}
}
return 0;
}
3. else是和它离的最近的if匹配的,所以适当的使用{}可以使代码的逻辑更加清楚,是种好的代码风格。否则如下示例:
int main()
{
int a = 0;
int b = 2;
if (a == 1)
if (b == 2)
printf("hehe\n");
else
printf("haha\n");
return 0;
}
事与愿违,没有输出!
7.1.2 switch语句
语法结构:
switch (整型表达式)
{
case 整形常量表达式://其结果和switch()中的表达式结果进行匹配;如果匹配就执行;不匹配就下一个case继续比较
空 / 一条 / 多条语句;
break;//跳出switch分支结构,如果不加,将往下执行所有case分支,直到遇到break或结束
//如果表达式的值与所有的case标签的值都不匹配,那么没有任何一个case分支被执行
//如果你并不想忽略掉不匹配所有标签的表达式的值时,可以加一条default子句
default://位置是任意的(一般都放在末尾),但只能有一条,
//......
break;//好的编程习惯,最后一条语句都加break
}
如下示例:
int main()
{
int opt = 0;
printf("请选择:1(好好学习) / 2(有米,不慌) / 3(摆烂):>");
scanf("%d", &opt);
switch (opt)
{
case 1:
printf("好Offer!\n");
break;
case 2:
printf("继承家产!\n");
break;
case 3:
printf("要去卖红薯了哟!\n");
break;
default://可选
//......
break;
}
return 0;
}
或者:
int main()
{
int day = 0;
scanf("%d", &day);
switch (day)
{
case 1:
case 2:
case 3:
case 4:
case 5:
printf("Weekday:Live like an ox and a horse!\n");
case 6:
case 7:
printf("Rest days:You've survived another week!");
default:
//......
break;
}
return 0;
}
小练习:
int main()
{
int n = 1;
int m = 2;
switch (n)//n==1
{
case 1:
m++;//m==3
case 2:
n++;//n==2
case 3:
switch (n)//switch允许嵌套使用
{
case 1:
n++;
case 2:
m++;//m==4
n++;//n==3
break;
}
case 4:
m++;//m==5
break;
default:
--n;
break;
}
printf("m = %d, n = %d\n", m, n);
return 0;
}
7.2 循环语句
7.2.1 while
语法结构:
while (表达式)//为真(非0)就继续,假(0) 就结束 while { }
{
//被循环执行的语句;
//......
//break; 语句:直接提前跳出 一层while循环
//......
//continue; 语句:终止本次循环,跳到while()表达式部分
}
如下代码:
#include<stdlib.h>
#include<time.h>
#define Money 100 //假设你有100元
#define UnitPrice 7 //假设每张彩票单价7元
int main()
{
//随机数种子
srand(time(NULL));//包含头文件:stdlib.h,time.h
int opt1 = 0;
printf("请选择:0【退出】,1【继续】:>");
scanf("%d", &opt1);
if (1 == opt1)
{
int money = Money;
int flag = 1;//标记是否因为余额不足而退出循环
int opt2 = 0;
printf("请输入你要购买的彩票号(整数0~5):>");
while (scanf("%d", &opt2))//scanf返回读到的数据个数
{
int answer = rand() % 6;//产生0~5的随机数,可自定义
if (opt2 == answer)
{
printf("恭喜你,中奖500万!");
break;
}
//没有中
int opt3 = 0;
printf("很遗憾,没有中!【当前余额:%d】\n接下来,你是要好好学习成为大牛【1】;还是当菜鸟,继续买彩票【0】请选择:>", money-=UnitPrice);
scanf("%d", &opt3);
if (1 == opt3)
{
printf("汗水会助力每一个梦!");
break;
}
//继续买彩票
if (money < UnitPrice)
{
printf("余额不足,还是好好学习吧!\n");
flag = 0;
break;
}
printf("请输入你要购买的彩票号(整数0~5):>");
}
//如果中彩票或者成为大牛
if (1 == flag)
{
printf("人生巅峰,迎娶白富美!\n");
}
}
return 0;
}
有兴趣的话,你可以拷贝到你的编译环境下玩一下,运气好的话,可能一次就中,但也可能一次不中,尽管只要在 规定范围的6个随机数里猜一个。而现实生活中,规则更复杂,数字是组合的,且随机性更大...... 可想而知,能中几十万,几百万的概率得有多低了。
在Debug(调试模式)或 Release(可发行模式)下运行完代码后,在当前项目目录下会产生 ***.exe可执行文件
双击就可执行,也可发给别人运行。
7.2.2 for
语法结构:
for (表达式1; 表达式2; 表达式3)
{
//循环语句......:
//break; 语句:直接跳出一层for循环
//continue;语句:终止本次循环,跳过其后的循环语句,直接到for(......)
}
//同样:如果不写{},默认只执行紧跟着的一条语句
说明:表达式1为初始化部分,用于初始化循环变量
表达式2为条件判断部分,用于判断循环是否继续,真(非0)就继续,假(0)就终止
表达式3为调整部分,用于循环条件的调整
第一次执行for(......),先执行表达式1,再执行表达式2判断,表达式3不执行;第二次及其以上执行for (......),表达式1不执行,先执行表达式3,再执行表达式2判断。
比如:
//打印1~n的素数
int main()
{
int n = 0;
printf("请输入打印范围(1~n) :>");
scanf("%d", &n);
int i = 0;
for (i = 1; i <= n; ++i)
{
//素数判断:大于1的自然数,除1和它本身不能被任何数整除
if (i > 1)
{
int j = 0;
for (j = 2; j < i; j++)
{
if (i % j == 0)
{
break;//不是素数
}
}
//执行到这,有两种情况:1:for正常结束,j == i,为素数; 2:中途break,不是素数
if (j == i)
{
printf("%d ", i);
}
}
}
}
测试输出:
接着来看一些for循环的变种写法:
for (; ; )//三个表达式都为空:死循环
{
//......
}
int i = 0;
for (; i < 10; )
{
i += 3;//调整部分还可以放到{ }中
}
int k = 0;
for (i = 1, k = 6;i < 3, k >= 1 ; i++, --k)//每一部分都可以是 逗号分隔的组合表达式,即逗号表达式
{
printf("%d ", k);
}
......
总之就是:非常灵活。
现在,如果要把7.2.1 的示例代码改成for结构,只需要修改一个地方:
for (; scanf("%d", &opt2); )
{
//......
}
7.2.3 do...while
语法结构:
do
{
//循环语句;
//break; 语句:直接跳出
//continue;语句:跳到表达式判断部分
} while (表达式);//真(非0)继续循环;假(0)终止
先至少执行一次循环语句之后,再while判断是否继续循环。
比如,现在来写个猜数字的小游戏:
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int main()
{
int input = 0;
srand(time(NULL));
do
{
//打印菜单
printf("**********************************\n");
printf("*********** 1.play **********\n");
printf("*********** 0.exit **********\n");
printf("**********************************\n");
//输入
printf("请选择>:");
scanf("%d", &input);
int random_num = rand() % 100 + 1;//生成1~100的随机数
int num = 0;
switch (input)
{
case 1:
//开始游戏逻辑
while (1)
{
printf("请输入猜的数字>:");
scanf("%d", &num);
if (num > random_num)
{
printf("猜大了\n");
}
else if (num < random_num)
{
printf("猜小了\n");
}
else
{
printf("恭喜你,猜对了\n");
break;
}
}
break;
case 0:
break;
default:
printf("选择错误,请重新输入!\n");
break;
}
} while (input);
return 0;
}
7.2.4. goto语句
适用场景:在深度嵌套中,一次可跳出多层循环,到达任意指定的位置
比如:
下一篇:
[60 天备战 2024 年 11 月软考高级系统架构设计师 - 第 37 天:系统安全设计 - 数据保护与合规性]
推荐阅读
-
快速入门 C 语言 [上] (非常详细!!)...
-
学习C语言编写小游戏:通过一个简单实例快速入门!
-
C语言的文件IO操作,非常详细!!
-
C语言几种循环结构—零基础小白快速入门必学
-
C语言指针详解(经典,非常详细)
-
SSM三大框架基础面试题-一、Spring篇 什么是Spring框架? Spring是一种轻量级框架,提高开发人员的开发效率以及系统的可维护性。 我们一般说的Spring框架就是Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块。比如Core Container中的Core组件是Spring所有组件的核心,Beans组件和Context组件是实现IOC和DI的基础,AOP组件用来实现面向切面编程。 Spring的6个特征: 核心技术:依赖注入(DI),AOP,事件(Events),资源,i18n,验证,数据绑定,类型转换,SpEL。 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。 数据访问:事务,DAO支持,JDBC,ORM,编组XML。 Web支持:Spring MVC和Spring WebFlux Web框架。 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。 语言:Kotlin,Groovy,动态语言。 列举一些重要的Spring模块? Spring Core:核心,可以说Spring其他所有的功能都依赖于该类库。主要提供IOC和DI功能。 Spring Aspects:该模块为与AspectJ的集成提供支持。 Spring AOP:提供面向切面的编程实现。 Spring JDBC:Java数据库连接。 Spring JMS:Java消息服务。 Spring ORM:用于支持Hibernate等ORM工具。 Spring Web:为创建Web应用程序提供支持。 Spring Test:提供了对JUnit和TestNG测试的支持。 谈谈自己对于Spring IOC和AOP的理解 IOC(Inversion Of Controll,控制反转)是一种设计思想: 在程序中手动创建对象的控制权,交由给Spring框架来管理。IOC在其他语言中也有应用,并非Spring特有。IOC容器实际上就是一个Map(key, value),Map中存放的是各种对象。 将对象之间的相互依赖关系交给IOC容器来管理,并由IOC容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。在实际项目中一个Service类可能由几百甚至上千个类作为它的底层,假如我们需要实例化这个Service,可能要每次都搞清楚这个Service所有底层类的构造函数,这可能会把人逼疯。如果利用IOC的话,你只需要配置好,然后在需要的地方引用就行了,大大增加了项目的可维护性且降低了开发难度。 Spring中的bean的作用域有哪些? 1.singleton:该bean实例为单例 2.prototype:每次请求都会创建一个新的bean实例(多例)。 3.request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。 4.session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。 5.global-session:全局session作用域,仅仅在基于Portlet的Web应用中才有意义,Spring5中已经没有了。Portlet是能够生成语义代码(例如HTML)片段的小型Java Web插件。它们基于Portlet容器,可以像Servlet一样处理HTTP请求。但是与Servlet不同,每个Portlet都有不同的会话。 Spring中的单例bean的线程安全问题了解吗? 概念用于理解:大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例bean存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 有两种常见的解决方案(用于回答的点): 1.在bean对象中尽量避免定义可变的成员变量(不太现实)。 2.在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal(线程本地化对象)中(推荐的一种方式)。 ThreadLocal解决多线程变量共享问题(参考博客):https://segmentfault.com/a/1190000009236777 Spring中Bean的生命周期: 1.Bean容器找到配置文件中Spring Bean的定义。 2.Bean容器利用Java Reflection API创建一个Bean的实例。 3.如果涉及到一些属性值,利用set方法设置一些属性值。 4.如果Bean实现了BeanNameAware接口,调用setBeanName方法,传入Bean的名字。 5.如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader方法,传入ClassLoader对象的实例。 6.如果Bean实现了BeanFactoryAware接口,调用setBeanClassFacotory方法,传入ClassLoader对象的实例。 7.与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。 8.如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执postProcessBeforeInitialization方法。 9.如果Bean实现了InitializingBean接口,执行afeterPropertiesSet方法。 10.如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。 11.如果有和加载这个Bean的Spring容器相关的BeanPostProcess对象,执行postProcessAfterInitialization方法。 12.当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy方法。 13.当要销毁Bean的时候,如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。 Spring框架中用到了哪些设计模式? 1.工厂设计模式:Spring使用工厂模式通过BeanFactory和ApplicationContext创建bean对象。 2.代理设计模式:Spring AOP功能的实现。 3.单例设计模式:Spring中的bean默认都是单例的。 4.模板方法模式:Spring中的jdbcTemplate、hibernateTemplate等以Template结尾的对数据库操作的类,它们就使用到了模板模式。 5.包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 6.观察者模式:Spring事件驱动模型就是观察者模式很经典的一个应用。 7.适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式、Spring MVC中也是用到了适配器模式适配Controller。 还有很多。。。。。。。 @Component和@Bean的区别是什么 1.作用对象不同。@Component注解作用于类,而@Bean注解作用于方法。 2.@Component注解通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用@ComponentScan注解定义要扫描的路径)。@Bean注解通常是在标有该注解的方法中定义产生这个bean,告诉Spring这是某个类的实例,当我需要用它的时候还给我。 3.@Bean注解比@Component注解的自定义性更强,而且很多地方只能通过@Bean注解来注册bean。比如当引用第三方库的类需要装配到Spring容器的时候,就只能通过@Bean注解来实现。 @Configuration public class AppConfig { @Bean public TransferService transferService { return new TransferServiceImpl; } } <beans> <bean id="transferService" class="com.kk.TransferServiceImpl"/> </beans> @Bean public OneService getService(status) { case (status) { when 1: return new serviceImpl1; when 2: return new serviceImpl2; when 3: return new serviceImpl3; } } 将一个类声明为Spring的bean的注解有哪些? 声明bean的注解: @Component 组件,没有明确的角色 @Service 在业务逻辑层使用(service层) @Repository 在数据访问层使用(dao层) @Controller 在展现层使用,控制器的声明 注入bean的注解: @Autowired:由Spring提供 @Inject:由JSR-330提供 @Resource:由JSR-250提供 *扩:JSR 是 java 规范标准 Spring事务管理的方式有几种? 1.编程式事务:在代码中硬编码(不推荐使用)。 2.声明式事务:在配置文件中配置(推荐使用),分为基于XML的声明式事务和基于注解的声明式事务。 Spring事务中的隔离级别有哪几种? 在TransactionDefinition接口中定义了五个表示隔离级别的常量:ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,Mysql默认采用的REPEATABLE_READ隔离级别;Oracle默认采用的READ_COMMITTED隔离级别。ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。ISOLATION_SERIALIZABLE:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 Spring事务中有哪几种事务传播行为? 在TransactionDefinition接口中定义了八个表示事务传播行为的常量。 支持当前事务的情况:PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)。 不支持当前事务的情况:PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。 其他情况:PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。 二、SpringMVC篇 什么是Spring MVC ?简单介绍下你对springMVC的理解? Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。 Spring MVC的工作原理了解嘛? image.png Springmvc的优点: (1)可以支持各种视图技术,而不仅仅局限于JSP; (2)与Spring框架集成(如IoC容器、AOP等); (3)清晰的角色分配:前端控制器(dispatcherServlet) , 请求到处理器映射(handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器(ViewResolver)。 (4) 支持各种请求资源的映射策略。 Spring MVC的主要组件? (1)前端控制器 DispatcherServlet(不需要程序员开发) 作用:接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度。 (2)处理器映射器HandlerMapping(不需要程序员开发) 作用:根据请求的URL来查找Handler (3)处理器适配器HandlerAdapter 注意:在编写Handler的时候要按照HandlerAdapter要求的规则去编写,这样适配器HandlerAdapter才可以正确的去执行Handler。 (4)处理器Handler(需要程序员开发) (5)视图解析器 ViewResolver(不需要程序员开发) 作用:进行视图的解析,根据视图逻辑名解析成真正的视图(view) (6)视图View(需要程序员开发jsp) View是一个接口, 它的实现类支持不同的视图类型(jsp,freemarker,pdf等等) springMVC和struts2的区别有哪些? (1)springmvc的入口是一个servlet即前端控制器(DispatchServlet),而struts2入口是一个filter过虑器(StrutsPrepareAndExecuteFilter)。 (2)springmvc是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2是基于类开发,传递参数是通过类的属性,只能设计为多例。 (3)Struts采用值栈存储请求和响应的数据,通过OGNL存取数据,springmvc通过参数解析器是将request请求内容解析,并给方法形参赋值,将数据和视图封装成ModelAndView对象,最后又将ModelAndView中的模型数据通过reques域传输到页面。Jsp视图解析器默认使用jstl。 SpringMVC怎么样设定重定向和转发的? (1)转发:在返回值前面加"forward:",譬如"forward:user.do?name=method4" (2)重定向:在返回值前面加"redirect:",譬如"redirect:http://www.baidu.com" SpringMvc怎么和AJAX相互调用的? 通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象。具体步骤如下 : (1)加入Jackson.jar (2)在配置文件中配置json的映射 (3)在接受Ajax方法里面可以直接返回Object,List等,但方法前面要加上@ResponseBody注解。 如何解决POST请求中文乱码问题,GET的又如何处理呢? (1)解决post请求乱码问题: 在web.xml中配置一个CharacterEncodingFilter过滤器,设置成utf-8; <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> (2)get请求中文参数出现乱码解决方法有两个: ①修改tomcat配置文件添加编码与工程编码一致,如下: <ConnectorURIEncoding="utf-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/> ②另外一种方法对参数进行重新编码: String userName = new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8") ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码。 Spring MVC的异常处理 ? 统一异常处理: Spring MVC处理异常有3种方式: (1)使用Spring MVC提供的简单异常处理器SimpleMappingExceptionResolver; (2)实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器; (3)使用@ExceptionHandler注解实现异常处理; 统一异常处理的博客:https://blog.csdn.net/ctwy291314/article/details/81983103 SpringMVC的控制器是不是单例模式,如果是,有什么问题,怎么解决? 是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方案是在控制器里面不能写成员变量。(此题目类似于上面Spring 中 第5题 有两种解决方案) SpringMVC常用的注解有哪些? @RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。 @RequestBody:注解实现接收http请求的json数据,将json转换为java对象。 @ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。 SpingMvc中的控制器的注解一般用那个,有没有别的注解可以替代? 一般用@Controller注解,也可以使用@RestController,@RestController注解相当于@ResponseBody + @Controller,表示是表现层,除此之外,一般不用别的注解代替。 如果在拦截请求中,我想拦截get方式提交的方法,怎么配置? 可以在@RequestMapping注解里面加上method=RequestMethod.GET。 怎样在方法里面得到Request,或者Session? 直接在方法的形参中声明request,SpringMVC就自动把request对象传入。 如果想在拦截的方法里面得到从前台传入的参数,怎么得到? 直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样。 如果前台有很多个参数传入,并且这些参数都是一个对象的,那么怎么样快速得到这个对象? 直接在方法中声明这个对象,SpringMVC就自动会把属性赋值到这个对象里面。 SpringMVC中函数的返回值是什么? 返回值可以有很多类型,有String, ModelAndView。ModelAndView类把视图和数据都合并的一起的。 SpringMVC用什么对象从后台向前台传递数据的? 通过ModelMap对象,可以在这个对象里面调用put方法,把对象加到里面,前台就可以拿到数据。 怎么样把ModelMap里面的数据放入Session里面? 可以在类上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key。 SpringMvc里面拦截器是怎么写的: 有两种写法,一种是实现HandlerInterceptor接口,另外一种是继承适配器类,接着在接口方法当中,实现处理逻辑;然后在SpringMvc的配置文件中配置拦截器即可: <!-- 配置SpringMvc的拦截器 --> <mvc:interceptors> <!-- 配置一个拦截器的Bean就可以了 默认是对所有请求都拦截 --> <bean id="myInterceptor" class="com.zwp.action.MyHandlerInterceptor"></bean> <!-- 只针对部分请求拦截 --> <mvc:interceptor> <mvc:mapping path="/modelMap.do" /> <bean class="com.zwp.action.MyHandlerInterceptorAdapter" /> </mvc:interceptor> </mvc:interceptors> 注解原理: 注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池 三、Mybatis篇 什么是MyBatis? MyBatis是一个可以自定义SQL、存储过程和高级映射的持久层框架。 讲下MyBatis的缓存 MyBatis的缓存分为一级缓存和二级缓存,一级缓存放在session里面,默认就有, 二级缓存放在它的命名空间里,默认是不打开的,使用二级缓存属性类需要实现Serializable序列化接口, 可在它的映射文件中配置<cache/> Mybatis是如何进行分页的?分页插件的原理是什么? 1)Mybatis使用RowBounds对象进行分页,也可以直接编写sql实现分页,也可以使用Mybatis的分页插件。 2)分页插件的原理:实现Mybatis提供的接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql。 举例:select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10 简述Mybatis的插件运行原理,以及如何编写一个插件? 1)Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、 Executor这4种接口的插件,Mybatis通过动态代理, 为需要拦截的接口生成代理对象以实现接口方法拦截功能, 每当执行这4种接口对象的方法时,就会进入拦截方法, 具体就是InvocationHandler的invoke方法,当然, 只会拦截那些你指定需要拦截的方法。 2)实现Mybatis的Interceptor接口并复写intercept方法, 然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可, 记住,别忘了在配置文件中配置你编写的插件。 Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理不? 1)Mybatis动态sql可以让我们在Xml映射文件内, 以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能。 2)Mybatis提供了9种动态sql标签:trim|where|set|foreach|if|choose|when|otherwise|bind。 3)其执行原理为,使用OGNL从sql参数对象中计算表达式的值, 根据表达式的值动态拼接sql,以此来完成动态sql的功能。 #{}和${}的区别是什么? 1)#{}是预编译处理,${}是字符串替换。 2)Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值(有效的防止SQL注入); 3)Mybatis在处理${}时,就是把${}替换成变量的值。 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? Hibernate属于全自动ORM映射工具, 使用Hibernate查询关联对象或者关联集合对象时, 可以根据对象关系模型直接获取,所以它是全自动的。 而Mybatis在查询关联对象或关联集合对象时, 需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 Mybatis是否支持延迟加载?如果支持,它的实现原理是什么? 1)Mybatis仅支持association关联对象和collection关联集合对象的延迟加载, association指的就是一对一,collection指的就是一对多查询。 在Mybatis配置文件中, 可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 2)它的原理是,使用CGLIB创建目标对象的代理对象, 当调用目标方法时,进入拦截器方法, 比如调用a.getB.getName, 拦截器invoke方法发现a.getB是null值, 那么就会单独发送事先保存好的查询关联B对象的sql, 把B查询上来,然后调用a.setB(b), 于是a的对象b属性就有值了, 接着完成a.getB.getName方法的调用。 这就是延迟加载的基本原理。 MyBatis与Hibernate有哪些不同? 1)Mybatis和hibernate不同,它不完全是一个ORM框架, 因为MyBatis需要程序员自己编写Sql语句, 不过mybatis可以通过XML或注解方式灵活配置要运行的sql语句, 并将java对象和sql语句映射生成最终执行的sql, 最后将sql执行的结果再映射生成java对象。 2)Mybatis学习门槛低,简单易学,程序员直接编写原生态sql, 可严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发, 例如互联网软件、企业运营类软件等,因为这类软件需求变化频繁, 一但需求变化要求成果输出迅速。但是灵活的前提是mybatis无法做到数据库无关性, 如果需要实现支持多种数据库的软件则需要自定义多套sql映射文件,工作量大。 3)Hibernate对象/关系映射能力强,数据库无关性好, 对于关系模型要求高的软件(例如需求固定的定制化软件) 如果用hibernate开发可以节省很多代码,提高效率。 但是Hibernate的缺点是学习门槛高,要精通门槛更高, 而且怎么设计O/R映射,在性能和对象模型之间如何权衡, 以及怎样用好Hibernate需要具有很强的经验和能力才行。 总之,按照用户的需求在有限的资源环境下只要能做出维护性、 扩展性良好的软件架构都是好架构,所以框架只有适合才是最好。 MyBatis的好处是什么? 1)MyBatis把sql语句从Java源程序中独立出来,放在单独的XML文件中编写, 给程序的维护带来了很大便利。 2)MyBatis封装了底层JDBC API的调用细节,并能自动将结果集转换成Java Bean对象, 大大简化了Java数据库编程的重复工作。 3)因为MyBatis需要程序员自己去编写sql语句, 程序员可以结合数据库自身的特点灵活控制sql语句, 因此能够实现比Hibernate等全自动orm框架更高的查询效率,能够完成复杂查询。 简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系? Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。 在Xml映射文件中,<parameterMap>标签会被解析为ParameterMap对象, 其每个子元素会被解析为ParameterMapping对象。 <resultMap>标签会被解析为ResultMap对象, 其每个子元素会被解析为ResultMapping对象。 每一个<select>、<insert>、<update>、<delete> 标签均会被解析为MappedStatement对象, 标签内的sql会被解析为BoundSql对象。 什么是MyBatis的接口绑定,有什么好处? 接口映射就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置. 接口绑定有几种实现方式,分别是怎么实现的? 接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加 上@Select@Update等注解里面包含Sql语句来绑定, 另外一种就是通过xml里面写SQL来绑定,在这种情况下, 要指定xml映射文件里面的namespace必须为接口的全路径名. 什么情况下用注解绑定,什么情况下用xml绑定? 当Sql语句比较简单时候,用注解绑定;当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多 MyBatis实现一对一有几种方式?具体怎么操作的? 有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成; 嵌套查询是先查一个表,根据这个表里面的结果的外键id, 去再另外一个表里面查询数据,也是通过association配置, 但另外一个表的查询通过select属性配置。 Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别? 能,Mybatis不仅可以执行一对一、一对多的关联查询, 还可以执行多对一,多对多的关联查询,多对一查询, 其实就是一对一查询,只需要把selectOne修改为selectList即可; 多对多查询,其实就是一对多查询,只需要把selectOne修改为selectList即可。 关联对象查询,有两种实现方式,一种是单独发送一个sql去查询关联对象, 赋给主对象,然后返回主对象。另一种是使用嵌套查询,嵌套查询的含义为使用join查询, 一部分列是A对象的属性值,另外一部分列是关联对象B的属性值, 好处是只发一个sql查询,就可以把主对象和其关联对象查出来。 MyBatis里面的动态Sql是怎么设定的?用什么语法? MyBatis里面的动态Sql一般是通过if节点来实现,通过OGNL语法来实现, 但是如果要写的完整,必须配合where,trim节点,where节点是判断包含节点有 内容就插入where,否则不插入,trim节点是用来判断如果动态语句是以and 或or 开始,那么会自动把这个and或者or取掉。 Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式? 第一种是使用<resultMap>标签,逐一定义列名和对象属性名之间的映射关系。 第二种是使用sql列的别名功能,将列别名书写为对象属性名, 比如T_NAME AS NAME,对象属性名一般是name,小写, 但是列名不区分大小写,Mybatis会忽略列名大小写,
-
go语言Socket编程-Socket编程 什么是Socket Socket,英文含义是插座、插孔,一般称之为套接字,用于描述IP地址和端口。可以实现不同程序间的数据通信。 Socket起源于Unix,而Unix基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用:Socket,该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。 套接字的内核实现较为复杂,不宜在学习初期深入学习,了解到如下结构足矣。 套接字通讯原理示意 在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。 常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。 网络应用程序设计模式 C/S模式 传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。 B/S模式 浏览器(Browser)/服务器(Server)模式。只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。 优缺点 对于C/S模式来说,其优点明显。客户端位于目标主机上可以保证性能,将数据缓存至客户端本地,从而提高数据传输效率。且,一般来说客户端和服务器程序由一个开发团队创作,所以他们之间所采用的协议相对灵活。可以在标准协议的基础上根据需求裁剪及定制。例如,腾讯所采用的通信协议,即为ftp协议的修改剪裁版。 因此,传统的网络应用程序及较大型的网络应用程序都首选C/S模式进行开发。如,知名的网络游戏魔兽世界。3D画面,数据量庞大,使用C/S模式可以提前在本地进行大量数据的缓存处理,从而提高观感。 C/S模式的缺点也较突出。由于客户端和服务器都需要有一个开发团队来完成开发。工作量将成倍提升,开发周期较长。另外,从用户角度出发,需要将客户端安插至用户主机上,对用户主机的安全性构成威胁。这也是很多用户不愿使用C/S模式应用程序的重要原因。 B/S模式相比C/S模式而言,由于它没有独立的客户端,使用标准浏览器作为客户端,其工作开发量较小。只需开发服务器端即可。另外由于其采用浏览器显示数据,因此移植性非常好,不受平台限制。如早期的偷菜游戏,在各个平台上都可以完美运行。 B/S模式的缺点也较明显。由于使用第三方浏览器,因此网络应用支持受限。另外,没有客户端放到对方主机上,缓存数据不尽如人意,从而传输数据量受到限制。应用的观感大打折扣。第三,必须与浏览器一样,采用标准http协议进行通信,协议选择不灵活。 因此在开发过程中,模式的选择由上述各自的特点决定。根据实际需求选择应用程序设计模式。 简单的C/S模型通信 Server端:Listen函数 func Listen(network, address string) (Listener, error) network:选用的协议:TCP、UDP, 如:“tcp”或 “udp” address:IP地址+端口号, 如:“127.0.0.1:8000”或 “:8000” Listener 接口: type Listener interface { Accept (Conn, error) Close error Addr Addr } Conn 接口: type Conn interface { Read(b byte) (n int, err error) Write(b byte) (n int, err error) Close error LocalAddr Addr RemoteAddr Addr SetDeadline(t time.Time) error SetReadDeadline(t time.Time) error SetWriteDeadline(t time.Time) error } 参看 [<u>https://studygolang.com/pkgdoc</u>](https://studygolang.com/pkgdoc) 中文帮助文档中的demo: 示例代码:TCP服务器.go package main import ( "net" "fmt" ) func main { // 创建监听 listener, err:= net.Listen("tcp", ":8000") if err != nil { fmt.Println("listen err:", err) return } defer listener.Close // 主协程结束时,关闭listener fmt.Println("服务器等待客户端建立连接...") // 等待客户端连接请求 conn, err := listener.Accept if err != nil { fmt.Println("accept err:", err) return } defer conn.Close // 使用结束,断开与客户端链接 fmt.Println("客户端与服务器连接建立成功...") // 接收客户端数据 buf := make(byte, 1024) // 创建1024大小的缓冲区,用于read n, err := conn.Read(buf) if err != nil { fmt.Println("read err:", err) return } fmt.Println("服务器读到:", string(buf[:n])) // 读多少,打印多少。 }
-
学习 C 语言的必读文章,教你如何快速入门!
-
C 语言快速入门】带你了解 C 语言,零基础入门①。
-
[C语言]超详细讲解,让你C语言入门成功(一)