在EIC-AST的第一个Task~
学习内容
0x01 C 语言中的数据类型
C 语言中的常用数据类型
C 语言中整型与字符型数据
首先需要注意的是:
无符号(unsigned)整型数据与一般有符号整型数据的区别。
整型有无符号(unsigned)和有符号(signed)两种类型,在默认情况下声明的整型变量都是有符号的类型(char 有点特别,需要根据具体编译环境确定),如果需声明无符号类型的话就需要在类型前加上 unsigned。
无符号整型和有符号整型的区别就是无符号类型可以存放的正数范围比有符号整型中的范围大一倍,因为有符号类型将最高位储存符号,而无符号类型全都储存数字。
并且:
在 C/C++语言中,int 和 long int 的所占的字节数与编译环境有关。
C 语言标准是这样规定的:int 最少 16 位(2 字节),long 不能比 int 短,short 不能比 int 长,具体位长由编译器开发商根据各种情况自己决定。
在老式的 16 位编译系统上,short、int、long 普遍的长度是 2 字节、2 字节、4 字节。
在 32 位编译系统 x86 处理器上,short、int、long 普遍的长度是 2 字节、4 字节、4 字节。int 占四字节,与 long 相同。
在 64 位编译系统 x64 处理器上:short 占两字节,int 占四字节,long 占 8 字节,long 数据范围变为:-2^63~2^63-1
由此可见 int 类型的数据长度一般是机器位长。在 16 位编译系统中 int 为 16 位,两个字节;32 位编译系统中 int 为 32 位,4 个字节;但是在 64 位编译系统中为了兼容 32 位编译系统,64 位编译系统的 int 也是 4 字节。
现在常用的编译器多认为 int 和 long int 相同,均为 4 字节,short 为 2 字节,char 为 1 字节。
如果只输入 int,它有可能是以上三种形式中的一种。
那么如何得到某个类型在特定平台上的准确大小?
为了得到某个类型或某个变量在特定平台上的准确大小,我们可以使用 sizeof 运算符。通过表达式 sizeof(type) 得到对象或类型的存储字节大小。下面的实例演示了获取 int 类型的大小:
1 |
|
C 语言中的 void 类型
void 类型指定没有可用的值。它通常用于以下三种情况下:
第一种:函数返回为空
C 中有各种函数都不返回值,或者您可以说它们返回空。不返回值的函数的返回类型为空。例如 void exit (int status);
第二种:函数参数为空
C 中有各种函数不接受任何参数。不带参数的函数可以接受一个 void。例如 int rand(void);
3 指针指向 void
类型为 void * 的指针代表对象的地址,而不是类型。例如,内存分配函数void *malloc( size_t size );
返回指向 void 的指针,可以转换为任何数据类型。
C 语言中的 bool(布尔)类型
在此之前的 C 语言中,使用整型 int 来表示真假。在输入时:使用非零值表示真;零值表示假。在输出时:真的值是 1,假的值是 0。
现在,出现了布尔型变量。_Bool
类型长度为 1,只能取值范围为 0 或 1。将任意非零值赋值给_Bool
类型,都会先转换为 1,表示真。将零值赋值给_Bool
类型,结果为 0,表示假。
有如下 example program:
1 | #include <stdio.h> |
运行结果如下:bool1==1, bool2==1, bool3==0, bool4==1, sizeof(_Bool) == 1
C 语言中的数组类型
所有的数组都是由连续的内存位置组成。最低的地址对应第一个元素,最高的地址对应最后一个元素。
C 语言中的指针类型
每个变量都被存放在从某个内存地址(以字节为单位)开始的若干个字节中。“指针”,也称作“指针变量”,大小为 4 个字节(或 8 个字节)的变量,其内容代表一个内存地址。
通过指针,我们能够对该指针指向的内存区域进行读写。
如果把内存的每个字节都想像成宾馆的一个房间,那么内存地址相当于就是房间号,而指针里存放的,就是房间号。
T _ p ; // T 可以是任何类型的名字,比如 int, double ,char 等等。
p 的类型: T _
- p 的类型: T
通过表达式 * p,可以读写从地址 p 开始的 sizeof(T)个字节 - p 等价于存放在地址 p 处的一个 T 类型的变量
- 意思为间接引用运算符
sizeof(T*) 4 字节(64 位计算机上可能 8 字节)
有了指针,就有了自由访问内存空间的手段:
不需要通过变量,就能对内存直接进行操作。通过指针,程序能访问的内存区域就不仅限于变量所占据的数据区域。
在 C 中,用指针 p 指向 a 的地址,然后对 p 进行加减操作,p 就能指向 a 后面或前面的内存区域,通过 p 也就能访问这些内存区域。
C 语言中的字符串类型
C 语言中,字符串有两种形式:
- 用双引号括起来的字符串常量, 如”CHINA” , “C program “。
- 存放于字符数组中,以‘\0’字符(ASCII 码为 0)结尾
存放于字符数组中的字符串常量占据内存的字节数等于字符串中字符数目加 1,多出来的是结尾字符‘\0’。
但是字符串的长度不包含’\0’
用 char 数组存放字符串,数组元素个数应该至少为字符串长度+1,以避免数组越界。
char 数组的内容,可以在初始化时设定,还可以用对数组元素赋值的办法任意改变其中的某个字符。
“” 也是合法的字符串常量,称为“空串”, 空串仍然会占据一个字节的存储空间,存放 ‘\0’。
如果字符串常量中包含双引号,则双引号应写为‘"’。而‘\’字符在 字符串中出现时,须连写两次,变成‘\’,以防止转译。
C 语言中的结构类型
两个同类型的结构变量,可以互相赋值。但是结构变量之间不能用“==”、“!=”、“<”、“>”、“<=”、“>=”进行比较运算。
一般来说,一个结构变量所占的内存空间的大小,就是结构中所有成员变量大小之和。结构变量中的各个成员变量在内存中一般是连续存放的。
并且,一个结构的成员变量可以是任何类型的,包括可以是另一个结构类型:
ASCII 码和 char 类型的关系
char 表示一个字符型数据,其和 int 在 0-255 范围内是等价的。而字符编码采用的是 ASCII 码,所以看起来和 ASCII 有关。在用 char 进行输入、输出时其值可以被当成 ASCII 码,输入、输出函数根据这个码找到相应的字符输入或输出。
0x02 使用格式化输入输出函数 printf()和 scanf()
在 printf 和 scanf 中可以使用以”%”开头的控制符,指明要输入或输出的数据的类型以及格式。
常用的格式控制符如下表所示:
|常用格式控制符|作 用|
|—|—|
|%d|读入或输出 int 变量|
|%c|读入或输出 char 变量|
|%f|读入或输出 float 变量,输出时保留小数点后面 6 位|
|%lf|读入或输出 double 变量,输出时保留小数点后面 6 位|
|%x|以十六进制读入或输出整型变量|
|%lld|读入或输出 long long 变量(64 位整数)|
|%nd|(如%4d,%12d)以 n 字符宽度输出整数,宽度不足时用空格填充|
|%0nd|( 如 %04d,%012d )以 n 字符宽度输出整数,宽度不足时用 0 填充|
|%.nf|(如%.4f,%.3f) 输出 double 或 float 值,精确到小数点后 n 位|
scanf 的进阶使用
1.用 scanf 可以一次读入多个类型不同的变量,只要输入的各项之间用空格分隔即可。
example:
1 |
|
输入:34 k 234.45↙
输出:34 k 234.449997
2.若输入的各项之间没有用空格分隔,则等待输入字符时,不会跳过空格(空格也会被当作字符读入),输入其他类型的数据时,会跳过空格。
example:
1 |
|
输入:34 k 456↙
输出:34 0.000000
原因:c = ' ', 读入f 时,对应输入是'k',导致出错。
3.如果在输入中有 scanf 中出现的非控制字符,则这些字符会被跳过。
example:
1 |
|
输入:12 k,3.75:290↙
输出:12,k,3.750000,290
有关 sprintf()和 sscanf()函数
参考这篇文章:浅析 C 语言中 printf(),sprintf(),scanf(),sscanf()的用法和区别-极客编程-博客园
0x03 分支结构与循环控制结构
分支结构
if 语句常见错误
1.错把赋值符当逻辑运算符来使用:
example:
1 | int a = 0; |
2.互相矛盾的多个条件,如果确实只希望执行其中一个分支,应该用 if 和多个 else if,而不要写多个 if
wrong example:
1 | int a = 0; |
最终会输出 hello
right example:
1 | int a = 0; |
不会输出 hello
switch 语句常见错误
switch 语句在进入某个 case 分支后,会一直执行到第一个碰到的“break;”,即使这个“break;”是在后面的 case 分支里面。如果没有碰到“break;”,则会向下一直执行到 switch 语句末尾的“}”,包括“default:”部分的语句组也会被执行。
因此,在运用 switch 语句时,一定要根据题目加上 break 关键字。
if-else 与 switch-case 语法的区别
if 语句,if else if 语句和 switch case 语句都属于流程控制语句。
在只需要判断一个条件的时候,自然是使用 if 语句方便有效;但是当判断条件很多的时候,我们可以使用多个 if 语句或者 if…else if 语句或者 switch case 语句。
if…else if 语句和多个 if 语句的区别还是很大的,if…else if 在任何一个环节满足条件的时候就将会终止判断,只处理一个满足条件的情况;而对于多个 if 语句,将会对每一个判断条件进行判断,自然而然会导致程序的执行效率降低。在多个判断条件的情况下,使用 if…else if 语句相对于使用多个 if 语句而言,可以减少程序的判断次数,提高效率。
在多个判断条件的情况下,不仅可以使用 if…else if 语句,还可以使用 switch case 语句。一般情况下,当判断条件较多的情况下,使用 switch case 语句的效率会高于使用 if…else if 语句。switch…case 与 if…else if 的根本区别在于,switch…case 会生成一个跳转表来指示实际的 case 分支的地址,而这个跳转表的索引号与 switch 变量的值是相等的。从而,switch…case 不用像 if…else if 那样遍历条件分支直到命中条件,而只需访问对应索引号的表项从而到达定位分支的目的。
因此,当只有分支比较少的时候,if 效率比 switch 高(因为 switch 需要生成跳转表)。若分支比较多,那当然是 switch 更高效也更清晰。
循环结构
for 循环语句结构特点
for 语句结构:
1 | for( 表达式1 ;表达式2;表达式3) { |
for 循环结构里的“表达式 1”和“表达式 3”都可以是用逗号连接的若干个表达式。
for 循环括号内三个语句的执行时间
执行过程:
- 计算“表达式 1”。
- 计算“表达式 2”,若其值为 true,则执行“{ }”中的语句组,然后转到 3);若为 false,则不再执行“{}”中的语句组,for 语句结束,转到 5)。
- 计算“表达式 3”。
- 转到 2)。
- 从 for 语句后面继续往下执行程序。
for 语句的循环控制变量特点
循环控制变量定义在”表达式 1”中,则其只在 for 语句内部起作用,可以不用担心循环控制变量重名。
example:
1 |
|
若情境是并非到达指定次数,而是满足某条件时即停止循环,则适合用 while 语句来实现循环。
while 循环的执行过程
- 判断“表达式”是否为真,如果不为真,则转 4)
- 执行“语句组”
- 转 1)
- while 语句结束,继续执行 while 语句后面的语句。
do……while 循环语句结构特点
1 | do { |
如果希望循环至少要执行一次,就可以使用 do…while 语句。
do……while 循环语句的执行过程
每执行一次循环后,都要判断“表达式”的值是否为真,如果真就继续循环,如果为假,就停止循环。
while 和 do……while 语句的区别
while 先判断后执行,do while 先执行后判断
当不满足循环条件时,while 循环一次都不会执行,do while 循环至少执行一次
break 与 continue 语句
break 语句可以出现在循环体中(for、while、do…while 循环均可),其作用是跳出循环。
并且在多重循环的情况下,break 语句只能跳出直接包含它的那一重循环。
example:
_找兄弟数_:如果两个不同的正整数,他们的和是他们的积的因子,就称这两个数为兄弟数,小的称为弟数,大的称为兄数。先后输入正整数 n 和 m(n < m) , 请在 n 至 m 这 m-n+1 个数中,找出一对兄弟数。如果找不到,就输出“No Solution.”。如果能找到,就找出和最小的那一对;如果有多对兄弟数和相同且都是最小,就找出弟数最小的那一对。
1 |
|
continue 语句可以出现在循环体中(for、while、do…while 循环均可),其作用是立即结束本次循环,并回到循环开头判断是否要进行下一次循环。
在多重循环的情况下,continue 只对直接包含它的那重循环起作用。
example:
找 10 以内的偶数
1 |
|
输出:2,4,6,8,10,
0x04 初始化数组
一维数字数组的初始化
在通过type arrayName [ arraySize ];
语句声明数组之后,需要对数组进行初始化。
在 C 中,我们可以逐个初始化数组,也可以使用一个初始化语句,如下所示:int test[5] = {1, 2, 3, 4, 5};
需要注意的是,声明数组时方括号内的数字代表的是数组长度。数组的元素都是从 0 开始标号的,因此数组的第一个元素是 arrayName[0],最后一个元素是 arrayName[数组的总大小减去 1]。需要牢记这一点以免发生数组越界情况。
例如,有以下程序:
1 |
|
运行结果为: >test 数组的第 1 个元素为 1,
test 数组的第 2 个元素为 2,
test 数组的第 3 个元素为 3,
test 数组的第 4 个元素为 4,
test 数组的第 5 个元素为 5,
大括号 { } 之间的值的数目不能大于我们在数组声明时在方括号 [ ] 中指定的元素数目。
若是大括号{ }之间的值小于我们在数组声明时在方括号中指定的元素数目,则没有值与之对应的数组元素自动赋 0
只能给元素逐个赋值,不能给数组整体赋值。例如给十个元素全部赋 1 值,只能写为:int a[10]={1,1,1,1,1,1,1,1,1,1};而不能写为:int a[10]=1.
如不给可初始化的数组赋初值,则全部元素均为 0 值。
以上规则也适用于 其他类型的数组
例如,有以下程序:
1 |
|
运行结果为: >test 数组的第 1 个元素为 1,
test 数组的第 2 个元素为 2,
test 数组的第 3 个元素为 3,
test 数组的第 4 个元素为 4,
test 数组的第 5 个元素为 5,
test 数组的第 6 个元素为 0,
如果在初始化时省略掉了数组的大小,数组的大小则为初始化时元素的个数。因此,如果我们初始化:int test[] = {1, 2, 3, 4, 5};
那么创建的这个数组,它与前一个例子中所创建的数组是完全相同的。
一维字符数组的初始化
C 语言允许用字符串的方式对数组作初始化赋值。例如:
1 | char c[9]={'c',' ','p','r','o','g','r','a','m'}; |
可写为:
1 | char c[10]={"C program"}; 或去掉{}写为: |
1 | char c[10]="C program"; |
需要注意的是,用字符串给字符数组赋值时由于要添加结束符 ‘\0
‘,数组的长度要比字符串的长度(字符串长度不包括 ‘\0'
)大 1。例如:
char str[] = “C program”;
该数组在内存中的实际存放情况为:
字符串长度为 9,数组长度为 10。
因此我们需要增加一个字符数组长度用来存放\0。
并且,上述这种字符数组的整体赋值只能在字符数组初始化时使用,不能用于字符数组的赋值,字符数组的赋值只能对其元素一一赋值,下面的赋值方法是错误的。
1 | char str[]; |
当对全体元素赋初值时也可以省去长度说明。例如:
1 | char c[]={'c',' ','p','r','o','g','r','a','m'}; |
这时 C 数组的长度自动定为 9。
二维数字数组的初始化
example:
1 | int a[5][3]={{80,75,92},{61,65},{59,63,70},{85,90},{76,77,85}}; |
每个内层的{},初始化数组中的一行。
同样地,二维数组初始化时,如果对每行都进行了初始化,则也可以不给出行数:
example:
1 | int a[][3]={ {80,75,92},{61,65} }; |
a 是一个 2 行 3 列的数组,a[1][2]被初始化成 0。
二维字符数组的初始化
通常情况下,二维数组的每一行分别使用一个字符串进行初始化。 例如:
1 | char c[3][8]={{"apple"},{"orange"},{"banana"}}; |
等价于:
1 | char c[3][8]={"apple","orange","banana"}; |
以上两条初始化语句中,二维数组的第一维大小均可省略。数组 c 的逻辑结构如下所示:
| |0|1|2|3|4|5|6|7|
|–|–|–|–|–|–|–|–|–|
|c[0]|a|p|p|l|e|\0|\0|\0|
|c[1]|o|r|a|n|g|e|\0|\0|
|c[2]|b|a|n|a|n|a|\0|\0|
二维数字数组的存放方式
数组 T a[N][m] 每一行都有 M 个元素
第 i 行的元素就是 a[i][0]、a[i][1]……a[i][m-1]。
同一行的元素,在内存中是连续存放的。
第 j 列的元素的元素,就是 a[0][j]、a[1][j]……a[N-1][j]。
a[0][0]是数组中地址最小的元素。如果 a[0][0]存放在地址 n,则 a[i][j]存放的地址就是:n + i × M× sizeof(T) + j × sizeof(T)
数组越界
数组元素的下标,可以是任何整数,可以是负数,也可以大于数组的元素个数。不会导致编译错误:
example:
1 | int a[10]; |
但运行时很可能会出错!
a[-2] = 5; a[200] = 10; a[10] = 20;int m = a[30];均可能导致程序运行出错!!!
因为可能引起意外修改其他变量的值,导致程序运行结果不正确
可能试图访问不该访问的内存区域,导致程序崩溃
数组越界的程序,用某些编译器编译后可能可以正确运行,换一个编译器编译后就运行错误
最可怕的是,编译器不会报错。
因此,我们在使用数组时,最好能初始化/声明地大一些
0x05 标识符的作用域,结构体
标识符的作用域
C 语言中的全局变量、局部变量、静态变量
要想学习变量的作用域,首先要搞懂 C 中的全局变量、局部变量、静态变量
局部变量:定义在函数内部的变量叫局部变量(函数的形参也是局部变量)
全局变量:定义在所有函数的外面的变量叫全局变量
全局变量在所有函数中均可以使用,局部变量只能在定义它的函数内部使用
静态变量:全局变量都是静态变量。局部变量定义时如果前面加了“static”关键字,则该变量也成为静态变量
静态变量的存放地址,在整个程序运行期间,都是固定不变的
非静态变量(一定是局部变量)地址每次函数调用时都可能不同,在函数的一次执行期间不变
如果未明确初始化,则静态变量会被自动初始化成全 0(每个 bit 都是 0),局部非静态变量的值则随机
作用域
变量名、函数名、类型名统称为“标识符”。一个标识符能够起作用的范围,叫做该标识符的作用域
在一个标识符的作用域之外使用该标识符,会导致“标识符没有定义”的编译错误。使用标识符的语句,必须出现在它们的声明或定义之后
在单文件的程序中,结构、函数和全局变量的作用域是其定义所在的整个文件
函数形参的作用域是整个函数
局部变量的作用域,是从定义它的语句开始,到包含它的最内层的那一对大括号“{}”的右大括号 “}”为止。
for 循环里定义的循环控制变量,其作用域就是整个 for 循环
同名标示符的作用域,可能一个被另一个包含。则在小的作用域里,作用域大的那个标识符被屏蔽,不起作用。
生存期
所谓变量的“生存期”,指的是在此期间,变量占有内存空间,其占有的内存空间只能归它使用,不会被用来存放别的东西。
而变量的生存期终止,就意味着该变量不再占有内存空间,它原来占有的内存空间,随时可能被派做他用。
全局变量的生存期,从程序被装入内存开始,到整个程序结束。
静态局部变量的生存期,从定义它语句第一次被执行开始,到整个程序结束为止。
函数形参的生存期从函数执行开始,到函数返回时结束。非静态局部变量的生存期,从执行到定义它的语句开始,一旦程序执行到了它的作用域之外,其生存期就终止。
使用 struct 定义结构体
我们可以使用“struct”关键字来定义一个“结构”,也就是说定义了一个新的结构数据类型:
定义方式:
1 | struct 结构名 |
example:
1 | struct Student { |
在经过这条语句之后 Student 即成为自定义类型的名字,可以用来定义变量Stuent s1,s2;
使用结构体获得、写入结构体内部的成员变量
一个结构变量的成员变量,可以完全和一个普通变量一样来使用,也可以取得其地址。使用形式:
结构变量名.成员变量名
example:对于以下定义的 StudentEx 与 Date 结构体数据类型
1 | struct Date { |
我们可以运行以下的 main()代码段:
1 | StudentEx stu; |
结构变量的初始化
结构变量可以在定义时进行初始化:
例如对上面的例子,我们可以通过以下语句对结构变量进行初始化
1 | StudentEx stu = { 1234,"Tom",3.78,{ 1984,12,28 }}; |
指向结构变量的指针
定义指向结构变量的指针
方式:结构名 * 指针变量名;
example:
1 | StudentEx * pStudent;//定义了* pStudent为StudentEx类型的指针 |
通过指针,访问其指向的结构变量的成员变量
方式:
1 | 指针->成员变量名 |
example:
对于‘使用结构体获得、写入结构体内部的成员变量’中定义的结构体
可以使用以下语句:
1 | StudentEx Stu; |
0x06 C 语言函数
函数的定义
一般来说函数的定义必须出现在函数调用语句之前,否则调用语句编译出错
函数的定义方式:
1 | 返回值类型 函数名(参数1类型 参数1名称, 参数2类型 参数2名称……) |
如果函数不需要返回值,则“返回值类型”可以写“void”
函数的调用
调用函数语句:函数名(参数1,参数2,……)
return 语句
对函数的调用,也是一个表达式。函数调用表达式的值,由函数内部的 return 语句决定。
return 语句语法如下:return 返回值;
return 语句的功能是结束函数的执行,并将“返回值”作为结果返回。“返回值”是常量、变量或复杂的表达式均可。
如果函数返回值类型为“void”,return 语句就直接写:return ;
需要注意的是,return 语句作为函数的出口,可以在函数中多次出现。多个 return 语句的“返回值”可以不同。在哪个 return 语句结束函数,函数的返回值就和哪个 return 语句里面的“返回值”相等。
0x07 C 语言的指针
C 语言指针的定义
T _ p ; // T 可以是任何类型的名字,比如 int, double ,char 等等。
p 的类型: T _ _p 的类型: T
通过表达式 _ p,可以读写从地址 p 开始的 sizeof(T)个字节
*p 等价于存放在地址 p 处的一个 T 类型的变量
- 间接引用运算符 sizeof(T*) 4 字节(64 位计算机上可能 8 字节)
指针的用法
& : 取地址运算符
&x : 变量 x 的地址(即指向 x 的指针)
对于类型为 T 的变量 x,&x 表示变量 x 的地址(即指向 x 的指针) &x 的类型是 T * 。
example:
1 | char ch1 = 'A'; |
指针的赋值
不同类型的指针,如果不经过强制类型转换,不能直接互相赋值
example:
1 | int * pn, char * pc, char c = 0x65; |
指针的运算
1) 两个 同类型 的指针变量,可以比较大小
若地址 p1<地址 p2,则 p1< p2 值为真。
若地址 p1=地址 p2,则 p1== p2 值为真。
若地址 p1>地址 p2,则 p1 > p2 值为真。
- 两个 同类型 的指针变量,可以相减
运算规则:
两个 T * 类型的指针 p1 和 p2
p1 – p2 = ( 地址 p1 – 地址 p2 ) / sizeof(T)
例:int _ p1, _ p2;
若 p1 指向地址 2000,p2 指向地址 600, 则
p1 – p2 = (1000 – 600)/sizeof(int) = (2000 – 600)/4 = 350
3)指针变量加减一个整数的结果是指针
例如:
p : T * 类型的指针
n : 整数类型的变量或常量
则 p+n : T _ 类型的指针,指向地址: 地址 p + n × sizeof(T)
n+p, p-n , _ (p+n), * (p-n)等同理
- 指针变量可以自增、自减
若 T* 类型的指针 p 指向地址 n
则 p++, ++p : p 指向 n + sizeof(T) p–, –p : p 指向 n - sizeof(T)
5)指针可以用下标运算符“[ ]”进行运算
若 p 是一个 T _ 类型的指针, n 是整数类型的变量或常量
则 p[n] 等价于 _ (p+n)