首页
前端面试题
前端报错总结
电子书
更多
插件下载
Search
1
JavaScript基础(二)操作符 流程控制
42 阅读
2
HTML基础
20 阅读
3
Vue基础
17 阅读
4
wctype.h
14 阅读
5
Vue2(知识点)
13 阅读
默认分类
HTML CSS
HTML基础
CSS
HTML5 CSS3
javaScript
javaScript基础
javaScript高级
Web APIs
jQuery
js小总结
WEB开发布局
Vue
PS切图
数据可视化
Git使用
Uniapp
c语言入门
标准库
嵌入式
登录
Search
liuxiaobai
累计撰写
108
篇文章
累计收到
12
条评论
首页
栏目
默认分类
HTML CSS
HTML基础
CSS
HTML5 CSS3
javaScript
javaScript基础
javaScript高级
Web APIs
jQuery
js小总结
WEB开发布局
Vue
PS切图
数据可视化
Git使用
Uniapp
c语言入门
标准库
嵌入式
页面
前端面试题
前端报错总结
电子书
插件下载
搜索到
41
篇与
的结果
2023-09-20
数据类型
数据类型C 语言的每一种数据,都是有类型(type)的,编译器必须知道数据的类型,才能操作数据。所谓“类型”,就是相似的数据所拥有的共同特征,那么一旦知道某个值的数据类型,就能知道该值的特征和操作方式。基本数据类型有三种:字符(char)、整数(int)和浮点数(float)。复杂的类型都是基于它们构建的。字符类型字符类型指的是单个字符,类型声明使用char关键字。char c = 'B';上面示例声明了变量c是字符类型,并将其赋值为字母B。C 语言规定,字符常量必须放在单引号里面。在计算机内部,字符类型使用一个字节(8位)存储。C 语言将其当作整数处理,所以字符类型就是宽度为一个字节的整数。每个字符对应一个整数(由 ASCII 码确定),比如B对应整数66。字符类型在不同计算机的默认范围是不一样的。一些系统默认为-128到127,另一些系统默认为0到255。这两种范围正好都能覆盖0到127的 ASCII 字符范围。只要在字符类型的范围之内,整数与字符是可以互换的,都可以赋值给字符类型的变量。char c = 66; // 等同于 char c = 'B';上面示例中,变量c是字符类型,赋给它的值是整数66。这跟赋值为字符B的效果是一样的。两个字符类型的变量可以进行数学运算。char a = 'B'; // 等同于 char a = 66; char b = 'C'; // 等同于 char b = 67; printf("%d\n", a + b); // 输出 133上面示例中,字符类型变量a和b相加,视同两个整数相加。占位符%d表示输出十进制整数,因此输出结果为133。单引号本身也是一个字符,如果要表示这个字符常量,必须使用反斜杠转义。char t = '\'';上面示例中,变量t为单引号字符,由于字符常量必须放在单引号里面,所以内部的单引号要使用反斜杠转义。这种转义的写法,主要用来表示 ASCII 码定义的一些无法打印的控制字符,它们也属于字符类型的值。\a:警报,这会使得终端发出警报声或出现闪烁,或者两者同时发生。\b:退格键,光标回退一个字符,但不删除字符。\f:换页符,光标移到下一页。在现代系统上,这已经反映不出来了,行为改成类似于\v。\n:换行符。\r:回车符,光标移到同一行的开头。\t:制表符,光标移到下一个水平制表位,通常是下一个8的倍数。\v:垂直分隔符,光标移到下一个垂直制表位,通常是下一行的同一列。\0:null 字符,代表没有内容。注意,这个值不等于数字0。转义写法还能使用八进制和十六进制表示一个字符。\nn:字符的八进制写法,nn为八进制值。\xnn:字符的十六进制写法,nn为十六进制值。char x = 'B'; char x = 66; char x = '\102'; // 八进制 char x = '\x42'; // 十六进制上面示例的四种写法都是等价的。整数类型简介整数类型用来表示较大的整数,类型声明使用int关键字。int a;上面示例声明了一个整数变量a。不同计算机的int类型的大小是不一样的。比较常见的是使用4个字节(32位)存储一个int类型的值,但是2个字节(16位)或8个字节(64位)也有可能使用。它们可以表示的整数范围如下。16位:-32,768 到 32,767。32位:-2,147,483,648 到 2,147,483,647。64位:-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807。signed,unsignedC 语言使用signed关键字,表示一个类型带有正负号,包含负值;使用unsigned关键字,表示该类型不带有正负号,只能表示零和正整数。对于int类型,默认是带有正负号的,也就是说int等同于signed int。由于这是默认情况,关键字signed一般都省略不写,但是写了也不算错。signed int a; // 等同于 int a;int类型也可以不带正负号,只表示非负整数。这时就必须使用关键字unsigned声明变量。unsigned int a;整数变量声明为unsigned的好处是,同样长度的内存能够表示的最大整数值,增大了一倍。比如,16位的signed int最大值为32,767,而unsigned int的最大值增大到了65,535。unsigned int里面的int可以省略,所以上面的变量声明也可以写成下面这样。unsigned a;字符类型char也可以设置signed和unsigned。signed char c; // 范围为 -128 到 127 unsigned char c; // 范围为 0 到 255注意,C 语言规定char类型默认是否带有正负号,由当前系统决定。这就是说,char不等同于signed char,它有可能是signed char,也有可能是unsigned char。这一点与int不同,int就是等同于signed int。整数的子类型如果int类型使用4个或8个字节表示一个整数,对于小整数,这样做很浪费空间。另一方面,某些场合需要更大的整数,8个字节还不够。为了解决这些问题,C 语言在int类型之外,又提供了三个整数的子类型。这样有利于更精细地限定整数变量的范围,也有利于更好地表达代码的意图。short int(简写为short):占用空间不多于int,一般占用2个字节(整数范围为-32768~32767)。long int(简写为long):占用空间不少于int,至少为4个字节。long long int(简写为long long):占用空间多于long,至少为8个字节。short int a; long int b; long long int c;上面代码分别声明了三种整数子类型的变量。默认情况下,short、long、long long都是带符号的(signed),即signed关键字省略了。它们也可以声明为不带符号(unsigned),使得能够表示的最大值扩大一倍。unsigned short int a; unsigned long int b; unsigned long long int c;C 语言允许省略int,所以变量声明语句也可以写成下面这样。short a; unsigned short a; long b; unsigned long b; long long c; unsigned long long c;不同的计算机,数据类型的字节长度是不一样的。确实需要32位整数时,应使用long类型而不是int类型,可以确保不少于4个字节;确实需要64位的整数时,应该使用long long类型,可以确保不少于8个字节。另一方面,为了节省空间,只需要16位整数时,应使用short类型;需要8位整数时,应该使用char类型。整数类型的极限值有时候需要查看,当前系统不同整数类型的最大值和最小值,C 语言的头文件limits.h提供了相应的常量,比如SCHAR_MIN代表 signed char 类型的最小值-128,SCHAR_MAX代表 signed char 类型的最大值127。为了代码的可移植性,需要知道某种整数类型的极限值时,应该尽量使用这些常量。SCHAR_MIN,SCHAR_MAX:signed char 的最小值和最大值。SHRT_MIN,SHRT_MAX:short 的最小值和最大值。INT_MIN,INT_MAX:int 的最小值和最大值。LONG_MIN,LONG_MAX:long 的最小值和最大值。LLONG_MIN,LLONG_MAX:long long 的最小值和最大值。UCHAR_MAX:unsigned char 的最大值。USHRT_MAX:unsigned short 的最大值。UINT_MAX:unsigned int 的最大值。ULONG_MAX:unsigned long 的最大值。ULLONG_MAX:unsigned long long 的最大值。整数的进制C 语言的整数默认都是十进制数,如果要表示八进制数和十六进制数,必须使用专门的表示法。八进制使用0作为前缀,比如017、0377。int a = 012; // 八进制,相当于十进制的10十六进制使用0x或0X作为前缀,比如0xf、0X10。int a = 0x1A2B; // 十六进制,相当于十进制的6699有些编译器使用0b前缀,表示二进制数,但不是标准。int x = 0b101010;注意,不同的进制只是整数的书写方法,不会对整数的实际存储方式产生影响。所有整数都是二进制形式存储,跟书写方式无关。不同进制可以混合使用,比如10 + 015 + 0x20是一个合法的表达式。printf()的进制相关占位符如下。%d:十进制整数。%o:八进制整数。%x:十六进制整数。%#o:显示前缀0的八进制整数。%#x:显示前缀0x的十六进制整数。%#X:显示前缀0X的十六进制整数。int x = 100; printf("dec = %d\n", x); // 100 printf("octal = %o\n", x); // 144 printf("hex = %x\n", x); // 64 printf("octal = %#o\n", x); // 0144 printf("hex = %#x\n", x); // 0x64 printf("hex = %#X\n", x); // 0X64浮点数类型任何有小数点的数值,都会被编译器解释为浮点数。所谓“浮点数”就是使用 m * be 的形式,存储一个数值,m是小数部分,b是基数(通常是2),e是指数部分。这种形式是精度和数值范围的一种结合,可以表示非常大或者非常小的数。浮点数的类型声明使用float关键字,可以用来声明浮点数变量。float c = 10.5;上面示例中,变量c的就是浮点数类型。float类型占用4个字节(32位),其中8位存放指数的值和符号,剩下24位存放小数的值和符号。float类型至少能够提供(十进制的)6位有效数字,指数部分的范围为(十进制的)-37到37,即数值范围为10-37到1037。有时候,32位浮点数提供的精度或者数值范围还不够,C 语言又提供了另外两种更大的浮点数类型。double:占用8个字节(64位),至少提供13位有效数字。long double:通常占用16个字节。注意,由于存在精度限制,浮点数只是一个近似值,它的计算是不精确的,比如 C 语言里面0.1 + 0.2并不等于0.3,而是有一个很小的误差。if (0.1 + 0.2 == 0.3) // falseC 语言允许使用科学计数法表示浮点数,使用字母e来分隔小数部分和指数部分。double x = 123.456e+3; // 123.456 x 10^3 // 等同于 double x = 123.456e3;上面示例中,e后面如果是加号+,加号可以省略。注意,科学计数法里面e的前后,不能存在空格。另外,科学计数法的小数部分如果是0.x或x.0的形式,那么0可以省略。0.3E6 // 等同于 .3E6 3.0E6 // 等同于 3.E6布尔类型C 语言原来并没有为布尔值单独设置一个类型,而是使用整数0表示伪,所有非零值表示真。int x = 1; if (x) { printf("x is true!\n"); }上面示例中,变量x等于1,C 语言就认为这个值代表真,从而会执行判断体内部的代码。C99 标准添加了类型_Bool,表示布尔值。但是,这个类型其实只是整数类型的别名,还是使用0表示伪,1表示真,下面是一个示例。_Bool isNormal; isNormal = 1; if (isNormal) printf("Everything is OK.\n");头文件stdbool.h定义了另一个类型别名bool,并且定义了true代表1、false代表0。只要加载这个头文件,就可以使用这几个关键字。#include <stdbool.h> bool flag = false;上面示例中,加载头文件stdbool.h以后,就可以使用bool定义布尔值类型,以及false和true表示真伪。字面量的类型字面量(literal)指的是代码里面直接出现的值。int x = 123;上面代码中,x是变量,123就是字面量。编译时,字面量也会写入内存,因此编译器必须为字面量指定数据类型,就像必须为变量指定数据类型一样。一般情况下,十进制整数字面量(比如123)会被编译器指定为int类型。如果一个数值比较大,超出了int能够表示的范围,编译器会将其指定为long int。如果数值超过了long int,会被指定为unsigned long。如果还不够大,就指定为long long或unsigned long long。小数(比如3.14)会被指定为double类型。字面量后缀有时候,程序员希望为字面量指定一个不同的类型。比如,编译器将一个整数字面量指定为int类型,但是程序员希望将其指定为long类型,这时可以为该字面量加上后缀l或L,编译器就知道要把这个字面量的类型指定为long。int x = 123L;上面代码中,字面量123有后缀L,编译器就会将其指定为long类型。这里123L写成123l,效果也是一样的,但是建议优先使用L,因为小写的l容易跟数字1混淆。八进制和十六进制的值,也可以使用后缀l和L指定为 Long 类型,比如020L和0x20L。int y = 0377L; int z = 0x7fffL;如果希望指定为无符号整数unsigned int,可以使用后缀u或U。int x = 123U;L和U可以结合使用,表示unsigned long类型。L和U的大小写和组合顺序无所谓。int x = 123LU;对于浮点数,编译器默认指定为 double 类型,如果希望指定为其他类型,需要在小数后面添加后缀f(float)或l(long double)。科学计数法也可以使用后缀。1.2345e+10F 1.2345e+10L总结一下,常用的字面量后缀有下面这些。f和F:float类型。l和L:对于整数是long int类型,对于小数是long double类型。ll和LL:Long Long 类型,比如3LL。u和U:表示unsigned int,比如15U、0377U。u还可以与其他整数后缀结合,放在前面或后面都可以,比如10UL、10ULL和10LLU都是合法的。下面是一些示例。int x = 1234; long int x = 1234L; long long int x = 1234LL unsigned int x = 1234U; unsigned long int x = 1234UL; unsigned long long int x = 1234ULL; float x = 3.14f; double x = 3.14; long double x = 3.14L;溢出每一种数据类型都有数值范围,如果存放的数值超出了这个范围(小于最小值或大于最大值),需要更多的二进制位存储,就会发生溢出。大于最大值,叫做向上溢出(overflow);小于最小值,叫做向下溢出(underflow)。一般来说,编译器不会对溢出报错,会正常执行代码,但是会忽略多出来的二进制位,只保留剩下的位,这样往往会得到意想不到的结果。所以,应该避免溢出。unsigned char x = 255; x = x + 1; printf("%d\n", x); // 0上面示例中,变量x加1,得到的结果不是256,而是0。因为x是unsign char类型,最大值是255(二进制11111111),加1后就发生了溢出,256(二进制100000000)的最高位1被丢弃,剩下的值就是0。再看下面的例子。unsigned int ui = UINT_MAX; // 4,294,967,295 ui++; printf("ui = %u\n", ui); // 0 ui--; printf("ui = %u\n", ui); // 4,294,967,295上面示例中,常量UINT_MAX是 unsigned int 类型的最大值。如果加1,对于该类型就会溢出,从而得到0;而0是该类型的最小值,再减1,又会得到UINT_MAX。溢出很容易被忽视,编译器又不会报错,所以必须非常小心。for (unsigned int i = n; i >= 0; --i) // 错误上面代码表面看似乎没有问题,但是循环变量i的类型是 unsigned int,这个类型的最小值是0,不可能得到小于0的结果。当i等于0,再减去1的时候,并不会返回-1,而是返回 unsigned int 的类型最大值,这个值总是大于等于0,导致无限循环。为了避免溢出,最好方法就是将运算结果与类型的极限值进行比较。unsigned int ui; unsigned int sum; // 错误 if (sum + ui > UINT_MAX) too_big(); else sum = sum + ui; // 正确 if (ui > UINT_MAX - sum) too_big(); else sum = sum + ui;上面示例中,变量sum和ui都是 unsigned int 类型,它们相加的和还是 unsigned int 类型,这就有可能发生溢出。但是,不能通过相加的和是否超出了最大值UINT_MAX,来判断是否发生了溢出,因为sum + ui总是返回溢出后的结果,不可能大于UINT_MAX。正确的比较方法是,判断UINT_MAX - sum与ui之间的大小关系。下面是另一种错误的写法。unsigned int i = 5; unsigned int j = 7; if (i - j < 0) // 错误 printf("negative\n"); else printf("positive\n");上面示例的运算结果,会输出positive。原因是变量i和j都是 unsigned int 类型,i - j的结果也是这个类型,最小值为0,不可能得到小于0的结果。正确的写法是写成下面这样。if (j > i) // ....sizeof 运算符sizeof是 C 语言提供的一个运算符,返回某种数据类型或某个值占用的字节数量。它的参数可以是数据类型的关键字,也可以是变量名或某个具体的值。// 参数为数据类型 int x = sizeof(int); // 参数为变量 int i; sizeof(i); // 参数为数值 sizeof(3.14);上面的第一个示例,返回得到int类型占用的字节数量(通常是4或8)。第二个示例返回整数变量占用字节数量,结果与前一个示例完全一样。第三个示例返回浮点数3.14占用的字节数量,由于浮点数的字面量一律存储为 double 类型,所以会返回8,因为 double 类型占用的8个字节。sizeof运算符的返回值,C 语言只规定是无符号整数,并没有规定具体的类型,而是留给系统自己去决定,sizeof到底返回什么类型。不同的系统中,返回值的类型有可能是unsigned int,也有可能是unsigned long,甚至是unsigned long long,对应的printf()占位符分别是%u、%lu和%llu。这样不利于程序的可移植性。C 语言提供了一个解决方法,创造了一个类型别名size_t,用来统一表示sizeof的返回值类型。该别名定义在stddef.h头文件(引入stdio.h时会自动引入)里面,对应当前系统的sizeof的返回值类型,可能是unsigned int,也可能是unsigned long。C 语言还提供了一个常量SIZE_MAX,表示size_t可以表示的最大整数。所以,size_t能够表示的整数范围为[0, SIZE_MAX]。printf()有专门的占位符%zd或%zu,用来处理size_t类型的值。printf("%zd\n", sizeof(int));上面代码中,不管sizeof返回值的类型是什么,%zd占位符(或%zu)都可以正确输出。如果当前系统不支持%zd或%zu,可使用%u(unsigned int)或%lu(unsigned long int)代替。类型的自动转换某些情况下,C 语言会自动转换某个值的类型。赋值运算赋值运算符会自动将右边的值,转成左边变量的类型。(1)浮点数赋值给整数变量浮点数赋予整数变量时,C 语言直接丢弃小数部分,而不是四舍五入。int x = 3.14;上面示例中,变量x是整数类型,赋给它的值是一个浮点数。编译器会自动把3.14先转为int类型,丢弃小数部分,再赋值给x,因此x的值是3。这种自动转换会导致部分数据的丢失(3.14丢失了小数部分),所以最好不要跨类型赋值,尽量保证变量与所要赋予的值是同一个类型。注意,舍弃小数部分时,不是四舍五入,而是整个舍弃。int x = 12.99;上面示例中,x等于12,而不是四舍五入的13。(2)整数赋值给浮点数变量整数赋值给浮点数变量时,会自动转为浮点数。float y = 12 * 2;上面示例中,变量y的值不是24,而是24.0,因为等号右边的整数自动转为了浮点数。(3)窄类型赋值给宽类型字节宽度较小的整数类型,赋值给字节宽度较大的整数变量时,会发生类型提升,即窄类型自动转为宽类型。比如,char或short类型赋值给int类型,会自动提升为int。char x = 10; int i = x + y;上面示例中,变量x的类型是char,由于赋值给int类型,所以会自动提升为int。(4)宽类型赋值给窄类型字节宽度较大的类型,赋值给字节宽度较小的变量时,会发生类型降级,自动转为后者的类型。这时可能会发生截值(truncation),系统会自动截去多余的二进制位,导致难以预料的结果。int i = 321; char ch = i; // ch 的值是 65 (321 - 256)上面例子中,变量ch是char类型,宽度是8个二进制位。变量i是int类型,将i赋值给ch,后者只能容纳i(二进制形式为101000001,共9位)的后八位,前面多出来的二进制位被丢弃,保留后八位就变成了01000001(十进制的65,相当于字符A)。浮点数赋值给整数类型的值,也会发生截值,浮点数的小数部分会被截去。double pi = 3.14159; int i = pi; // i 的值为 3上面示例中,i等于3,pi的小数部分被截去了。混合类型的运算不同类型的值进行混合计算时,必须先转成同一个类型,才能进行计算。转换规则如下:(1)整数与浮点数混合运算时,整数转为浮点数类型,与另一个运算数类型相同。3 + 1.2 // 4.2上面示例是int类型与float类型的混合计算,int类型的3会先转成float的3.0,再进行计算,得到4.2。(2)不同的浮点数类型混合运算时,宽度较小的类型转为宽度较大的类型,比如float转为double,double转为long double。(3)不同的整数类型混合运算时,宽度较小的类型会提升为宽度较大的类型。比如short转为int,int转为long等,有时还会将带符号的类型signed转为无符号unsigned。下面例子的执行结果,可能会出人意料。int a = -5; if (a < sizeof(int)) do_something();上面示例中,变量a是带符号整数,sizeof(int)是size_t类型,这是一个无符号整数。按照规则,signed int 自动转为 unsigned int,所以a会自动转成无符号整数4294967291(转换规则是-5加上无符号整数的最大值,再加1),导致比较失败,do_something()不会执行。所以,最好避免无符号整数与有符号整数的混合运算。因为这时 C 语言会自动将signed int转为unsigned int,可能不会得到预期的结果。整数类型的运算两个相同类型的整数运算时,或者单个整数的运算,一般来说,运算结果也属于同一类型。但是有一个例外,宽度小于int的类型,运算结果会自动提升为int。unsigned char a = 66; if ((-a) < 0) printf("negative\n"); else printf("positive\n");上面示例中,变量a是 unsigned char 类型,这个类型不可能小于0,但是-a不是 unsigned char 类型,会自动转为 int 类型,导致上面的代码输出 negative。再看下面的例子。unsigned char a = 1; unsigned char b = 255; unsigned char c = 255; if ((a - 5) < 0) do_something(); if ((b + c) > 300) do_something();上面示例中,表达式a - 5和b + c都会自动转为 int 类型,所以函数do_something()会执行两次。函数函数的参数和返回值,会自动转成函数定义里指定的类型。int dostuff(int, unsigned char); char m = 42; unsigned short n = 43; long long int c = dostuff(m, n);上面示例中,参数变量m和n不管原来的类型是什么,都会转成函数dostuff()定义的参数类型。下面是返回值自动转换类型的例子。char func(void) { int a = 42; return a; }上面示例中,函数内部的变量a是int类型,但是返回的值是char类型,因为函数定义中返回的是这个类型。类型的显式转换原则上,应该避免类型的自动转换,防止出现意料之外的结果。C 语言提供了类型的显式转换,允许手动转换类型。只要在一个值或变量的前面,使用圆括号指定类型(type),就可以将这个值或变量转为指定的类型,这叫做“类型指定”(casting)。(unsigned char) ch上面示例将变量ch转成无符号的字符类型。long int y = (long int) 10 + 12;上面示例中,(long int)将10显式转为long int类型。这里的显示转换其实是不必要的,因为赋值运算符会自动将右边的值,转为左边变量的类型。可移植类型C 语言的整数类型(short、int、long)在不同计算机上,占用的字节宽度可能是不一样的,无法提前知道它们到底占用多少个字节。程序员有时控制准确的字节宽度,这样的话,代码可以有更好的可移植性,头文件stdint.h创造了一些新的类型别名。(1)精确宽度类型(exact-width integer type),保证某个整数类型的宽度是确定的。int8_t:8位有符号整数。int16_t:16位有符号整数。int32_t:32位有符号整数。int64_t:64位有符号整数。uint8_t:8位无符号整数。uint16_t:16位无符号整数。uint32_t:32位无符号整数。uint64_t:64位无符号整数。上面这些都是类型别名,编译器会指定它们指向的底层类型。比如,某个系统中,如果int类型为32位,int32_t就会指向int;如果long类型为32位,int32_t则会指向long。下面是一个使用示例。#include <stdio.h> #include <stdint.h> int main(void) { int32_t x32 = 45933945; printf("x32 = %d\n", x32); return 0; }上面示例中,变量x32声明为int32_t类型,可以保证是32位的宽度。(2)最小宽度类型(minimum width type),保证某个整数类型的最小长度。int_least8_tint_least16_tint_least32_tint_least64_tuint_least8_tuint_least16_tuint_least32_tuint_least64_t上面这些类型,可以保证占据的字节不少于指定宽度。比如,int_least8_t表示可以容纳8位有符号整数的最小宽度的类型。(3)最快的最小宽度类型(fast minimum width type),可以使整数计算达到最快的类型。int_fast8_tint_fast16_tint_fast32_tint_fast64_tuint_fast8_tuint_fast16_tuint_fast32_tuint_fast64_t上面这些类型是保证字节宽度的同时,追求最快的运算速度,比如int_fast8_t表示对于8位有符号整数,运算速度最快的类型。这是因为某些机器对于特定宽度的数据,运算速度最快,举例来说,32位计算机对于32位数据的运算速度,会快于16位数据。(4)可以保存指针的整数类型。intptr_t:可以存储指针(内存地址)的有符号整数类型。uintptr_t:可以存储指针的无符号整数类型。(5)最大宽度整数类型,用于存放最大的整数。intmax_t:可以存储任何有效的有符号整数的类型。uintmax_t:可以存放任何有效的无符号整数的类型。上面的这两个类型的宽度比long long和unsigned long更大。
2023年09月20日
4 阅读
0 评论
0 点赞
2023-09-20
流程控制
流程控制C 语言的程序是顺序执行,即先执行前面的语句,再执行后面的语句。开发者如果想要控制程序执行的流程,就必须使用流程控制的语法结构,主要是条件执行和循环执行。if 语句if语句用于条件判断,满足条件时,就执行指定的语句。if (expression) statement上面式子中,表达式expression为真(值不为0)时,就执行statement语句。if后面的判断条件expression外面必须有圆括号,否则会报错。语句体部分statement可以是一个语句,也可以是放在大括号里面的复合语句。下面是一个例子。if (x == 10) printf("x is 10");上面示例中,当变量x为10时,就会输出一行文字。对于只有一个语句的语句体,语句部分通常另起一行。if (x == 10) printf("x is 10\n");如果有多条语句,就需要把它们放在大括号里面,组成一个复合语句。if (line_num == MAX_LINES) { line_num = 0; page_num++; }if语句可以带有else分支,指定条件不成立时(表达式expression的值为0),所要执行的代码。if (expression) statement else statement下面是一个例子。if (i > j) max = i; else max = j;如果else的语句部分多于一行,同样可以把它们放在大括号里面。else可以与另一个if语句连用,构成多重判断。if (expression) statement else if (expression) statement ... else if (expression) statement else statement如果有多个if和else,可以记住这样一条规则,else总是跟最接近的if匹配。if (number > 6) if (number < 12) printf("The number is more than 6, less than 12.\n"); else printf("It is wrong number.\n");上面示例中,else部分匹配最近的if(即number < 12),所以如果number等于6,就不会执行else的部分。这样很容易出错,为了提供代码的可读性,建议使用大括号,明确else匹配哪一个if。if (number > 6) { if (number < 12) { printf("The number is more than 6, less than 12.\n"); } } else { printf("It is wrong number.\n"); }上面示例中,使用了大括号,就可以清晰地看出else匹配外层的if。三元运算符 ?:C 语言有一个三元表达式?:,可以用作if...else的简写形式。<expression1> ? <expression2> : <expression3>这个操作符的含义是,表达式expression1如果为true(非0值),就执行expression2,否则执行expression3。下面是一个例子,返回两个值之中的较大值。(i > j) ? i : j;上面的代码等同于下面的if语句。if (i > j) return i; else return j;switch 语句switch 语句是一种特殊形式的 if...else 结构,用于判断条件有多个结果的情况。它把多重的else if改成更易用、可读性更好的形式。switch (expression) { case value1: statement case value2: statement default: statement }上面代码中,根据表达式expression不同的值,执行相应的case分支。如果找不到对应的值,就执行default分支。下面是一个例子。switch (grade) { case 0: printf("False"); break; case 1: printf("True"); break; default: printf("Illegal"); }上面示例中,根据变量grade不同的值,会执行不同的case分支。如果等于0,执行case 0的部分;如果等于1,执行case 1的部分;否则,执行default的部分。default表示处理以上所有case都不匹配的情况。每个case语句体的结尾,都应该有一个break语句,作用是跳出整个switch结构,不再往下执行。如果缺少break,就会导致继续执行下一个case或default分支。switch (grade) { case 0: printf("False"); case 1: printf("True"); break; default: printf("Illegal"); }上面示例中,case 0的部分没有break语句,导致这个分支执行完以后,不会跳出switch结构,继续执行case 1分支。利用这个特点,如果多个case分支对应同样的语句体,可以写成下面这样。switch (grade) { case 0: case 1: printf("True"); break; default: printf("Illegal"); }上面示例中,case 0分支没有任何语句,导致case 0和case 1都会执行同样的语句体。case后面的语句体,不用放在大括号里面,这也是为什么需要break的原因。default分支用来处理前面的 case 都不匹配的情况,最好放在所有 case 的后面,这样就不用写break语句。这个分支是可选的,如果没有该分支,遇到所有的 case 都不匹配的情况,就会直接跳出整个 switch 代码块。while 语句while语句用于循环结构,满足条件时,不断执行循环体。while (expression) statement上面代码中,如果表达式expression为非零值(表示真),就会执行statement语句,然后再次判断expression是否为零;如果expression为零(表示伪)就跳出循环,不再执行循环体。while (i < n) i = i + 2;上面示例中,只要i小于n,i就会不断增加2。如果循环体有多个语句,就需要使用大括号将这些语句组合在一起。while (expression) { statement; statement; }下面是一个例子。i = 0; while (i < 10) { printf("i is now %d!\n", i); i++; } printf("All done!\n");上面代码中,循环体会执行10次,每次将i增加1,直到等于10才退出循环。只要条件为真,while会产生无限循环。下面是一种常见的无限循环的写法。while (1) { // ... }上面的示例虽然是无限循环,但是循环体内部可以用break语句跳出循环。do...while 结构do...while结构是while的变体,它会先执行一次循环体,然后再判断是否满足条件。如果满足的话,就继续执行循环体,否则跳出循环。do statement while (expression);上面代码中,不管条件expression是否成立,循环体statement至少会执行一次。每次statement执行完毕,就会判断一次expression,决定是否结束循环。i = 10; do --i; while (i > 0);上面示例中,变量i先减去1,再判断是否大于0。如果大于0,就继续减去1,直到i等于0为止。如果循环部分有多条语句,就需要放在大括号里面。i = 10; do { printf("i is %d\n", i); i++; } while (i < 10); printf("All done!\n");上面例子中,变量i并不满足小于10的条件,但是循环体还是会执行一次。for 语句for语句是最常用的循环结构,通常用于精确控制循环次数。for (initialization; continuation; action) statement;上面代码中,for语句的条件部分(即圆括号里面的部分)有三个表达式。initialization:初始化表达式,用于初始化循环变量,只执行一次。continuation:判断表达式,只要为true,就会不断执行循环体。action:循环变量处理表达式,每轮循环结束后执行,使得循环变量发生变化。循环体部分的statement可以是一条语句,也可以是放在大括号里面的复合语句。下面是一个例子。for (int i = 10; i > 0; i--) printf("i is %d\n", i);上面示例中,循环变量i在for的第一个表达式里面声明,该变量只用于本次循环。离开循环体之后,就会失效。条件部分的三个表达式,每一个都可以有多个语句,语句与语句之间使用逗号分隔。int i, j; for (i = 0, j = 999; i < 10; i++, j--) { printf("%d, %d\n", i, j); }上面示例中,初始化部分有两个语句,分别对变量i和j进行赋值。for的三个表达式都不是必需的,甚至可以全部省略,这会形成无限循环。for (;;) { printf("本行会无限循环地打印。\n" ); }上面示例由于没有判断条件,就会形成无限循环。break 语句break语句有两种用法。一种是与switch语句配套使用,用来中断某个分支的执行,这种用法前面已经介绍过了。另一种用法是在循环体内部跳出循环,不再进行后面的循环了。for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { printf("%d, %d\n", i, j); break; } }上面示例中,break语句使得循环跳到下一个i。while ((ch = getchar()) != EOF) { if (ch == '\n') break; putchar(ch); }上面示例中,一旦读到换行符(\n),break命令就跳出整个while循环,不再继续读取了。注意,break命令只能跳出循环体和switch结构,不能跳出if结构。if (n > 1) { if (n > 2) break; // 无效 printf("hello\n"); }上面示例中,break语句是无效的,因为它不能跳出外层的if结构。continue 语句continue语句用于在循环体内部终止本轮循环,进入下一轮循环。只要遇到continue语句,循环体内部后面的语句就不执行了,回到循环体的头部,开始执行下一轮循环。for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { printf("%d, %d\n", i, j); continue; } }上面示例中,有没有continue语句,效果一样,都表示跳到下一个j。while ((ch = getchar()) != '\n') { if (ch == '\t') continue; putchar(ch); }上面示例中,只要读到的字符是制表符(\t),就用continue语句跳过该字符,读取下一个字符。goto 语句goto 语句用于跳到指定的标签名。这会破坏结构化编程,建议不要轻易使用,这里为了语法的完整,介绍一下它的用法。char ch; top: ch = getchar(); if (ch == 'q') goto top;上面示例中,top是一个标签名,可以放在正常语句的前面,相当于为这行语句做了一个标记。程序执行到goto语句,就会跳转到它指定的标签名。infinite_loop: print("Hello, world!\n"); goto infinite_loop;上面的代码会产生无限循环。goto 的一个主要用法是跳出多层循环。for(...) { for (...) { while (...) { do { if (some_error_condition) goto bail; } while(...); } } } bail: // ... ...上面代码有很复杂的嵌套循环,不使用 goto 的话,想要完全跳出所有循环,写起来很麻烦。goto 的另一个用途是提早结束多重判断。if (do_something() == ERR) goto error; if (do_something2() == ERR) goto error; if (do_something3() == ERR) goto error; if (do_something4() == ERR) goto error;上面示例有四个判断,只要有一个发现错误,就使用 goto 跳过后面的判断。注意,goto 只能在同一个函数之中跳转,并不能跳转到其他函数。
2023年09月20日
4 阅读
0 评论
0 点赞
2023-09-20
运算符
运算符C 语言的运算符非常多,一共有 50 多种,可以分成若干类。算术运算符算术运算符专门用于算术运算,主要有下面几种。+:正值运算符(一元运算符)-:负值运算符(一元运算符)+:加法运算符(二元运算符)-:减法运算符(二元运算符)*:乘法运算符/:除法运算符%:余值运算符(1)+,-+和-既可以作为一元运算符,也可以作为二元运算符。所谓“一元运算符”,指的是只需要一个运算数就可以执行。一元运算符-用来改变一个值的正负号。int x = -12;上面示例中,-将12这个值变成-12。一元运算符+对正负值没有影响,是一个完全可以省略的运算符,但是写了也不会报错。int x = -12; int y = +x;上面示例中,变量y的值还是-12,因为+不会改变正负值。二元运算符+和-用来完成加法和减法。int x = 4 + 22; int y = 61 - 23;(2)*运算符*用来完成乘法。int num = 5; printf("%i\n", num * num); // 输出 25(3)/运算符/用来完成除法。注意,两个整数相除,得到还是一个整数。float x = 6 / 4; printf("%f\n", x); // 输出 1.000000上面示例中,尽管变量x的类型是float(浮点数),但是6 / 4得到的结果是1.0,而不是1.5。原因就在于 C 语言里面的整数除法是整除,只会返回整数部分,丢弃小数部分。如果希望得到浮点数的结果,两个运算数必须至少有一个浮点数,这时 C 语言就会进行浮点数除法。float x = 6.0 / 4; // 或者写成 6 / 4.0 printf("%f\n", x); // 输出 1.500000上面示例中,6.0 / 4表示进行浮点数除法,得到的结果就是1.5。下面是另一个例子。int score = 5; score = (score / 20) * 100;上面的代码,你可能觉得经过运算,score会等于25,但是实际上score等于0。这是因为score / 20是整除,会得到一个整数值0,所以乘以100后得到的也是0。为了得到预想的结果,可以将除数20改成20.0,让整除变成浮点数除法。score = (score / 20.0) * 100;(4)%运算符%表示求模运算,即返回两个整数相除的余值。这个运算符只能用于整数,不能用于浮点数。int x = 6 % 4; // 2负数求模的规则是,结果的正负号由第一个运算数的正负号决定。11 % -5 // 1 -11 % -5 // -1 -11 % 5 // -1上面示例中,第一个运算数的正负号(11或-11)决定了结果的正负号。(5)赋值运算的简写形式如果变量对自身的值进行算术运算,C 语言提供了简写形式,允许将赋值运算符和算术运算符结合成一个运算符。+=-=*=/=%=下面是一些例子。i += 3; // 等同于 i = i + 3 i -= 8; // 等同于 i = i - 8 i *= 9; // 等同于 i = i * 9 i /= 2; // 等同于 i = i / 2 i %= 5; // 等同于 i = i % 5自增运算符,自减运算符C 语言提供两个运算符,对变量自身进行+ 1和- 1的操作。++:自增运算符--:自减运算符i++; // 等同于 i = i + 1 i--; // 等同于 i = i - 1这两个运算符放在变量的前面或后面,结果是不一样的。++var和--var是先执行自增或自减操作,再返回操作后var的值;var++和var--则是先返回操作前var的值,再执行自增或自减操作。int i = 42; int j; j = (i++ + 10); // i: 43 // j: 52 j = (++i + 10) // i: 44 // j: 54上面示例中,自增运算符的位置差异,会导致变量j得到不同的值。这样的写法很容易出现意料之外的结果,为了消除意外,可以改用下面的写法。/* 写法一 */ j = (i + 10); i++; /* 写法二 */ i++; j = (i + 10);上面示例中,变量i的自增运算与返回值是分离的两个步骤,这样就不太会出错,也提高了代码的可读性。关系运算符C 语言用于比较的表达式,称为“关系表达式”(relational expression),里面使用的运算符就称为“关系运算符”(relational operator),主要有下面6个。> 大于运算符< 小于运算符>= 大于等于运算符<= 小于等于运算符== 相等运算符!= 不相等运算符下面是一些例子。a == b; a != b; a < b; a > b; a <= b; a >= b;关系表达式通常返回0或1,表示真伪。C 语言中,0表示伪,所有非零值表示真。比如,20 > 12返回1,12 > 20返回0。关系表达式常用于if或while结构。if (x == 3) { printf("x is 3.\n"); }注意,相等运算符==与赋值运算符=是两个不一样的运算符,不要混淆。有时候,可能会不小心写出下面的代码,它可以运行,但很容易出现意料之外的结果。if (x = 3) ...上面示例中,原意是x == 3,但是不小心写成x = 3。这个式子表示对变量x赋值3,它的返回值为3,所以if判断总是为真。为了防止出现这种错误,有的程序员喜欢将变量写在等号的右边。if (3 == x) ...这样的话,如果把==误写成=,编译器就会报错。/* 报错 */ if (3 = x) ...另一个需要避免的错误是,多个关系运算符不宜连用。i < j < k上面示例中,连续使用两个小于运算符。这是合法表达式,不会报错,但是通常达不到想要的结果,即不是保证变量j的值在i和k之间。因为关系运算符是从左到右计算,所以实际执行的是下面的表达式。(i < j) < k上面式子中,i < j返回0或1,所以最终是0或1与变量k进行比较。如果想要判断变量j的值是否在i和k之间,应该使用下面的写法。i < j && j < k逻辑运算符逻辑运算符提供逻辑判断功能,用于构建更复杂的表达式,主要有下面三个运算符。!:否运算符(改变单个表达式的真伪)。&&:与运算符(两侧的表达式都为真,则为真,否则为伪)。||:或运算符(两侧至少有一个表达式为真,则为真,否则为伪)。下面是与运算符的例子。if (x < 10 && y > 20) printf("Doing something!\n");上面示例中,只有x < 10和y > 20同时为真,x < 10 && y > 20才会为真。下面是否运算符的例子。if (!(x < 12)) printf("x is not less than 12\n");上面示例中,由于否运算符!具有比<更高的优先级,所以必须使用括号,才能对表达式x < 12进行否运算。当然,合理的写法是if (x >= 12),这里只是为了举例。对于逻辑运算符来说,任何非零值都表示真,零值表示伪。比如,5 || 0会返回1,5 && 0会返回0。逻辑运算符还有一个特点,它总是先对左侧的表达式求值,再对右边的表达式求值,这个顺序是保证的。如果左边的表达式满足逻辑运算符的条件,就不再对右边的表达式求值。这种情况称为“短路”。if (number != 0 && 12/number == 2)上面示例中,如果&&左侧的表达式(number != 0)为伪,即number等于0时,右侧的表达式(12/number == 2)是不会执行的。因为这时左侧表达式返回0,整个&&表达式肯定为伪,就直接返回0,不再执行右侧的表达式了。由于逻辑运算符的执行顺序是先左后右,所以下面的代码是有问题的。while ((x++ < 10) && (x + y < 20))上面示例中,执行左侧表达式后,变量x的值就已经变了。等到执行右侧表达式的时候,是用新的值在计算,这通常不是原始意图。位运算符C 语言提供一些位运算符,用来操作二进制位(bit)。(1)取反运算符~取反运算符~是一个一元运算符,用来将每一个二进制位变成相反值,即0变成1,1变成0。// 返回 01101100 ~ 10010011上面示例中,~对每个二进制位取反,就得到了一个新的值。注意,~运算符不会改变变量的值,只是返回一个新的值。(2)与运算符&与运算符&将两个值的每一个二进制位进行比较,返回一个新的值。当两个二进制位都为1,就返回1,否则返回0。// 返回 00010001 10010011 & 00111101上面示例中,两个八位二进制数进行逐位比较,返回一个新的值。与运算符&可以与赋值运算符=结合,简写成&=。int val = 3; val = val & 0377; // 简写成 val &= 0377;(3)或运算符|或运算符|将两个值的每一个二进制位进行比较,返回一个新的值。两个二进制位只要有一个为1(包含两个都为1的情况),就返回1,否则返回0。// 返回 10111111 10010011 | 00111101或运算符|可以与赋值运算符=结合,简写成|=。int val = 3; val = val | 0377; // 简写为 val |= 0377;(4)异或运算符^异或运算符^将两个值的每一个二进制位进行比较,返回一个新的值。两个二进制位有且仅有一个为1,就返回1,否则返回0。// 返回 10101110 10010011 ^ 00111101异或运算符^可以与赋值运算符=结合,简写成^=。int val = 3; val = val ^ 0377; // 简写为 val ^= 0377;(5)左移运算符<<左移运算符<<将左侧运算数的每一位,向左移动指定的位数,尾部空出来的位置使用0填充。// 1000101000 10001010 << 2上面示例中,10001010的每一个二进制位,都向左侧移动了两位。左移运算符相当于将运算数乘以2的指定次方,比如左移2位相当于乘以4(2的2次方)。左移运算符<<可以与赋值运算符=结合,简写成<<=。int val = 1; val = val << 2; // 简写为 val <<= 2;(6)右移运算符>>右移运算符>>将左侧运算数的每一位,向右移动指定的位数,尾部无法容纳的值将丢弃,头部空出来的位置使用0填充。// 返回 00100010 10001010 >> 2上面示例中,10001010的每一个二进制位,都向右移动两位。最低的两位10被丢弃,头部多出来的两位补0,所以最后得到00100010。注意,右移运算符最好只用于无符号整数,不要用于负数。因为不同系统对于右移后如何处理负数的符号位,有不同的做法,可能会得到不一样的结果。右移运算符相当于将运算数除以2的指定次方,比如右移2位就相当于除以4(2的2次方)。右移运算符>>可以与赋值运算符=结合,简写成>>=。int val = 1; val = val >> 2; // 简写为 val >>= 2;逗号运算符逗号运算符用于将多个表达式写在一起,从左到右依次运行每个表达式。x = 10, y = 20;上面示例中,有两个表达式(x = 10和y = 20),逗号使得它们可以放在同一条语句里面。逗号运算符返回最后一个表达式的值,作为整个语句的值。int x; x = (1, 2, 3);上面示例中,括号里面的逗号运算符,返回最后一个表达式的值,所以变量x等于3。运算优先级优先级指的是,如果一个表达式包含多个运算符,哪个运算符应该优先执行。各种运算符的优先级是不一样的。3 + 4 * 5;上面示例中,表达式3 + 4 * 5里面既有加法运算符(+),又有乘法运算符(*)。由于乘法的优先级高于加法,所以会先计算4 * 5,而不是先计算3 + 4。如果两个运算符优先级相同,则根据运算符是左结合,还是右结合,决定执行顺序。大部分运算符是左结合(从左到右执行),少数运算符是右结合(从右到左执行),比如赋值运算符(=)。5 * 6 / 2;上面示例中,*和/的优先级相同,它们都是左结合运算符,所以从左到右执行,先计算5 * 6,再计算30 / 2。运算符的优先级顺序很复杂。下面是部分运算符的优先级顺序(按照优先级从高到低排列)。圆括号(())自增运算符(++),自减运算符(--)一元运算符(+和-)乘法(*),除法(/)加法(+),减法(-)关系运算符(<、>等)赋值运算符(=)由于圆括号的优先级最高,可以使用它改变其他运算符的优先级。int x = (3 + 4) * 5;上面示例中,由于添加了圆括号,加法会先于乘法进行运算。完全记住所有运算符的优先级没有必要,解决方法是多用圆括号,防止出现意料之外的情况,也有利于提高代码的可读性。
2023年09月20日
5 阅读
0 评论
0 点赞
1
...
12
13
14