首页
前端面试题
前端报错总结
电子书
更多
插件下载
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语言入门
标准库
嵌入式
页面
前端面试题
前端报错总结
电子书
插件下载
搜索到
104
篇与
的结果
2023-09-20
指针
指针指针是 C 语言最重要的概念之一,也是最难理解的概念之一。简介指针是什么?首先,它是一个值,这个值代表一个内存地址,因此指针相当于指向某个内存地址的路标。字符*表示指针,通常跟在类型关键字的后面,表示指针指向的是什么类型的值。比如,char*表示一个指向字符的指针,float*表示一个指向float类型的值的指针。int* intPtr;上面示例声明了一个变量intPtr,它是一个指针,指向的内存地址存放的是一个整数。星号*可以放在变量名与类型关键字之间的任何地方,下面的写法都是有效的。int *intPtr; int * intPtr; int* intPtr;本书使用星号紧跟在类型关键字后面的写法(即int* intPtr;),因为这样可以体现,指针变量就是一个普通变量,只不过它的值是内存地址而已。这种写法有一个地方需要注意,如果同一行声明两个指针变量,那么需要写成下面这样。// 正确 int * foo, * bar; // 错误 int* foo, bar;上面示例中,第二行的执行结果是,foo是整数指针变量,而bar是整数变量,即*只对第一个变量生效。一个指针指向的可能还是指针,这时就要用两个星号**表示。int** foo;上面示例表示变量foo是一个指针,指向的还是一个指针,第二个指针指向的则是一个整数。* 运算符*这个符号除了表示指针以外,还可以作为运算符,用来取出指针变量所指向的内存地址里面的值。void increment(int* p) { *p = *p + 1; }上面示例中,函数increment()的参数是一个整数指针p。函数体里面,*p就表示指针p所指向的那个值。对*p赋值,就表示改变指针所指向的那个地址里面的值。上面函数的作用是将参数值加1。该函数没有返回值,因为传入的是地址,函数体内部对该地址包含的值的操作,会影响到函数外部,所以不需要返回值。事实上,函数内部通过指针,将值传到外部,是 C 语言的常用方法。变量地址而不是变量值传入函数,还有一个好处。对于需要大量存储空间的大型变量,复制变量值传入函数,非常浪费时间和空间,不如传入指针来得高效。& 运算符&运算符用来取出一个变量所在的内存地址。int x = 1; printf("x's address is %p\n", &x);上面示例中,x是一个整数变量,&x就是x的值所在的内存地址。printf()的%p是内存地址的占位符,可以打印出内存地址。上一小节中,参数变量加1的函数,可以像下面这样使用。void increment(int* p) { *p = *p + 1; } int x = 1; increment(&x); printf("%d\n", x); // 2上面示例中,调用increment()函数以后,变量x的值就增加了1,原因就在于传入函数的是变量x的地址&x。&运算符与*运算符互为逆运算,下面的表达式总是成立。int i = 5; if (i == *(&i)) // 正确指针变量的初始化声明指针变量之后,编译器会为指针变量本身分配一个内存空间,但是这个内存空间里面的值是随机的,也就是说,指针变量指向的值是随机的。这时一定不能去读写指针变量指向的地址,因为那个地址是随机地址,很可能会导致严重后果。int* p; *p = 1; // 错误上面的代码是错的,因为p指向的那个地址是随机的,向这个随机地址里面写入1,会导致意想不到的结果。正确做法是指针变量声明后,必须先让它指向一个分配好的地址,然后再进行读写,这叫做指针变量的初始化。int* p; int i; p = &i; *p = 13;上面示例中,p是指针变量,声明这个变量后,p会指向一个随机的内存地址。这时要将它指向一个已经分配好的内存地址,上例就是再声明一个整数变量i,编译器会为i分配内存地址,然后让p指向i的内存地址(p = &i;)。完成初始化之后,就可以对p指向的内存地址进行赋值了(*p = 13;)。为了防止读写未初始化的指针变量,可以养成习惯,将未初始化的指针变量设为NULL。int* p = NULL;NULL在 C 语言中是一个常量,表示地址为0的内存空间,这个地址是无法使用的,读写该地址会报错。指针的运算指针本质上就是一个无符号整数,代表了内存地址。它可以进行运算,但是规则并不是整数运算的规则。(1)指针与整数值的加减运算指针与整数值的运算,表示指针的移动。short* j; j = (short*)0x1234; j = j + 1; // 0x1236上面示例中,j是一个指针,指向内存地址0x1234。由于0x1234本身是整数类型(int),跟j的类型(short*)并不兼容,所以强制使用类型投射,将0x1234转成short*。你可能以为j + 1等于0x1235,但正确答案是0x1236。原因是j + 1表示指针向内存地址的高位移动一个单位,而一个单位的short类型占据两个字节的宽度,所以相当于向高位移动两个字节。同样的,j - 1得到的结果是0x1232。指针移动的单位,与指针指向的数据类型有关。数据类型占据多少个字节,每单位就移动多少个字节。(2)指针与指针的加法运算指针只能与整数值进行加减运算,两个指针进行加法是非法的。unsigned short* j; unsigned short* k; x = j + k; // 非法上面示例是两个指针相加,这是非法的。(3)指针与指针的减法相同类型的指针允许进行减法运算,返回它们之间的距离,即相隔多少个数据单位。高位地址减去低位地址,返回的是正值;低位地址减去高位地址,返回的是负值。这时,减法返回的值属于ptrdiff_t类型,这是一个带符号的整数类型别名,具体类型根据系统不同而不同。这个类型的原型定义在头文件stddef.h里面。short* j1; short* j2; j1 = (short*)0x1234; j2 = (short*)0x1236; ptrdiff_t dist = j2 - j1; printf("%td\n", dist); // 1上面示例中,j1和j2是两个指向 short 类型的指针,变量dist是它们之间的距离,类型为ptrdiff_t,值为1,因为相差2个字节正好存放一个 short 类型的值。(4)指针与指针的比较运算指针之间的比较运算,比较的是各自的内存地址哪一个更大,返回值是整数1(true)或0(false)。
2023年09月20日
5 阅读
0 评论
0 点赞
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 点赞
1
...
16
17
18
...
35