程序员语法

ads

基础语法

数据类型

对于通常表示数值的类型,重点是关注它的范围大小,因为C语言数据类型的大小是不完全固定的,在不同的硬件平台,会有区别,尤其是一些嵌入式设备。下面给出一个通常情况下的表示范围

以上就是标准C中的基础数据类型,要记住,C中没有long long类型,很多人将C语言与C++语言搞混,切记!

以上表中,long类型有两种情况,使用32位编译器编译,则long为4字节,占32位,如果使用64位编译器,则long为8字节,占64位二进制。当然这个也不是绝对的,仍然与系统环境有关。

补充说明: 在C99新标准中,对C语言进行了扩展,其中提供了几种新的类型

  1. 新增复数类型(_Complex)和虚数类型(_Imaginary
  2. 新增布尔类型(_Bool,包含<stdbool.h>头文件时,可以使用bool来代替_Bool
  3. 新增整数类型long long int,该类型用于表示64位整数,共8字节,请注意与C++中的long long区分

「在C99 版本以前,C语言中是没有这些类型的,然而,在以前C99并不是一个被广泛支持的C语言版本,例如微软旗下最新的VC编译器就不完全兼容C99,这些阻碍导致C99没有被重视和完全普及,使得C语言新特性被割裂。」

在今天,C99标准已经被大部分编译器支持,例如gcc和clang,在不考虑极端兼容情况下,使用新特性语法并不会造成什么副作用,最后提一句,我们使用的GCC编译器是完全支持C99标准的。

修饰数值类型

除了直接使用这些类型,通常还会使用一个关键字unsigned来修饰,它表达的意思是无符号,例如:

unsigned int len = 10;

我们以前讲过,计算机中,最高位是符号位,例如32位,只有31位是有效位,因为最高位要用来表示符号,为0表示正数,1则表示负数。这样一来能用于表示的实际范围就变小了,有时候我们根本不需要使用负数,这时候就可以使用unsigned关键字来提升表示的范围,例如用unsigned修饰int后,就能将32位都有效的用于表示范围,则变量len能表示的范围变成了0 ~ (,如果不加关键字unsigned,则默认被signed修饰,即int len = 10; 等价于signed int len = 10;,通常我们当然是用简洁写法,什么都不写啦。

基本数据类型的打印

在第一个示例中,我们已经使用printf函数打印了hello world,这里print是打印的意思,那么f是什么意思呢?其实这里的f是缩写,是format的意思,代表格式化打印。既然是格式化打印,那一定会有格式占位符了,例如我要打印He is 18 years old"这句话,显然18是一个变量,他今年18,明年就是19,所以在这句话中需要把一个变量拼进去,这个时候就可以使用占位符,占位符的作用就是先把位置占着,到合适的时候在替换,这就像拿个水杯到图书馆占座一样。根据输入输出的变量的类型不同,占位符也不同,这里介绍最常用的几个

  • %d 有符号十进制整数
  • %f 浮点数
  • %s 字符串
  • %c 单个字符
  • %x 十六进制整数
#include<stdio.h>

int main(void){
    printf("He is %d years old",18);
    printf("He is %s years old","18");
}

获取数据类型的长度

前面已经说过,在不同的硬键平台上,不同的编译器下,这些类型的实际长度可能都有出入,那么如何才能确切的指定当前某个数据类型的实际长度到底是多少呢?答案就是使用sizeof运算符

#include<stdio.h>

int main(void){
    printf("char size = %dn",sizeof(char));
    printf("short size = %dn",sizeof(short));
    printf("int size = %dn",sizeof(int));
    printf("long size = %dn",sizeof(long));
    printf("float size = %dn",sizeof(float));
    printf("double size = %dn",sizeof(double));
}

输出:

char size = 1
short size = 2
int size = 4
long size = 4
float size = 4
double size = 8

可以看到,long类型仍然是4字节,然而在我的Mac OS电脑上测试,64位GCC编译出来的long输出的是8字节,在其他类Unix操作系统上一致,可见Windows系统上的C存在很多奇怪的特殊现象。所以再次建议尽量使用类Unix系统学习C语言,如Mac OS、Ubuntu系统等。

变量与常量

变量

在C语言中,「变量必须先声明后使用」,没有先声明的变量无法访问

#include<stdio.h>

int main(void){
    /* 声明变量 */
    int width;
    int height;
    float price;

    // 初始化变量
    width = 100;
    height = 200;

    // 对于浮点数,其字面量应带后缀f
    // 这不是必须的,但应具备这样的编码规范
    price = 15.6f;
}

注意,声明变量后没有初始化就使用,会造成一些不可预知的结果,因为未初始化的变量可能会具有一些随机值,而且这不是良好的C语言编程习惯,应当被批判。声明的变量没有预初始化为零值,这是C语言的一个缺陷!推荐的良好的编程范式,是在「声明的同时对变量进行零值初始化」

int main(void){
    //  声明的同时进行零值初始化
    //  不同类型的变量,其零值不同
    int width = 0;
    int height = 0;
    float price = 0.0f;
}

C语言还有一种在一行声明多个变量并初始化的方式,请警惕这其中的陷进

    int a, b, c=10;

以上代码中,只有变量c在声明的同时进行了初始化,而ab均未初始化,在后续中可能会导致未对其初始化就使用了。建议在声明时都进行零值初始化

int a = 0, b = 0, c = 10;

为什么在大量的C教材中,都存在先声明,后初始化的代码范例呢?这是因为在早期C语言版本中,不能在声明变量的同时初始化,换句话说,就是变量声明和初始化必须分开两行写,在大量陈旧落伍教材和书籍中存在这样的写法,这些不合时宜的写法对现在的C学习者造成极大误导。

常量

C语言中使用const关键字修饰的就是常量,常量是不能修改的。C语言中,约定使用变量名大写来表示常量,多个单词则使用下划线分隔,例如MD_MARK,这只是一种编码风格,不是必须的,但是建议遵守它,否则你可能会受到同行的鄙视。这样的好处是看的人一眼能识别出这是一个常量,而且能避免一些命名冲突。

const int PI = 3.14;

在C语言中,还有一种方式来定义常量

#include<stdio.h>

// 定义一个宏 PI
#define PI 3.14

int main(void){
    printf("%f",PI);
}

注意,以上实际上是定义了一个宏,它并不是我们所说的那种真正意义上的常量,但它的效果等同于常量,而且在某种时候这种方式比使用const关键字定义常量性能更好,这也是为什么许多C语言高手都喜欢使用宏的一个原因。定义宏时,是没有等号的,请留意。

拓展:C语言中int的正确使用姿势

上一节已经讲过,由于C语言中,整型的实际长度和范围不固定的问题,会导致C语言存跨平台移植的兼容问题,因此,C99标准中引入了stdint.h头文件,有效的解决了该问题。

#include<stdio.h>
#include<stdint.h>

int main(void){
    // 使用stdint.h中定义的类型表示整数
    int8_t a = 0;
    int16_t b = 0;
    int32_t c = 0;
    int64_t d = 0;

    // 前面加u,表示unsigned,无符号
    uint32_t e = 0;
    printf("int8 size is %dn",sizeof(int8_t));
    printf("int16 size is %dn",sizeof(int16_t));
    printf("int32 size is %dn",sizeof(int32_t));
    printf("int64 size is %dn",sizeof(int64_t));
    printf("uint32 size is %dn",sizeof(uint32_t));
}

打印结果:

int8 size is 1
int16 size is 2
int32 size is 4
int64 size is 8
uint32 size is 4

int8_t即表示8位整型,同理,int64_t就是64位整型,类型定义明确清晰,且能兼容多种平台。以上代码,使用32位编译器,编译成32位系统下的程序后,运行得到的结果依然不变。这里一定会有朋友质疑,为什么32位的系统下,还能表示并使用int64这种64位的整型?这当然就是stdint.h库给我们带来的便利了,简单说一下原理,如果当前平台的是32位的,那么经过组合,我们可以使用两个32位拼起来,不就能表示64位了吗?同理,即使是8位的CPU,经过这种拼合思路,照样能表示64位!当然,聪明人一眼就看出了弊端,使用这种拼合的方式,数据需要经过组合转换,处理也更加复杂,同时还会带来性能的损失,但是C99标准库已经为我们处理好了一切,虽然付出了一定的性能损失,但是成功的实现了C语言整型的跨平台兼容,这样的损失是完全值得的。

注意,微软早期版本的编译器完全不支持C99,到VS2010时才引入了stdint.h头文件,在VS2010及其以后的版本中,可以放心使用。

表达式

与其他编程语言不同,C语言强调表达式而不是语句。表达式就如同计算值的公式,通过运算符把变量和常量组合起来。

算术运算符

主要包括加减乘除+-*/

求余数,即取模运算 %

二元的算术运算还包括自增和自减++--

自增和自减运算符可以作为前缀或后缀使用,如下

int i = 0;
i++;
++i;

「那么i++++i的区别是什么呢?」关于这两者的区别,某些教材和网上一些资料是这样解释的,++做前缀,是先让i加1,做后缀则后加1,既在下一行代码前i被加1。类似这种说法其实是不准确的,甚至是错误的,理解太过于表面,只是对现象的概括而已。这里咱们就一次把这个问题彻底搞明白,永不犯迷糊。

前面已经说了,C语言强调的是表达式而不是语句,那么表达式和语句有什么区别呢?我个人认为其中一个区别就是表达式整体一定有一个值,而语句可以没有返回值。有其他编程基础的朋友一定清楚所谓返回值的概念,那么就是说表达式一定有一个返回值,或者应该说是表达式整体的值。

i++作为一个表达式,那么他的表达式的值是什么呢?其实我们可以用一个变量来保存表达式的值int r = i++;

    int i = 0;
    int r = i++;
    
    printf("r=%dn",r);

可以看到,表达式的r值为0。这个例子就很清楚了,所谓表达式的值,其实就是(i++)整体的一个值,它是一个独立的值。再运行下面的例子

    int i = 0;
    int r = ++i;
    
    printf("r=%dn",r);

可以看到,此时,表达式(++i)整体的值r变成了1。

来总结一下

  1. ++作为后缀时,自增表达式整体的值等于该变量初始值。如上例中int r = i++;,表达式整体的返回值r 等于i的初始值,而i未做自增运算前的初始值是0,所以r就是0。但是要注意,表达式一旦运行,i的值就会立刻发生变化,因此(i++)中,i的值是1

  2. ++作为前缀时,自增表达式整体的返回值等于该变量运算之后的值。如上例中int r = ++i;r的值等于(++i)表达式运算之后i的实际值。

因此,遇到复杂的自增运算符时,只需要问自己两个问题,自增变量的值是几?表达式整体的返回值又是几?下面我们看一个很常见的问题,问ij打印的值各是几?

    int i = 0;
    int j = i++ + ++i;
    printf("i=%d, j=%dn",i,j);

按照我们上面讲的知识来分解,先把式子拆分成(i++) + (++i);(i++)这个表达式整体的值是0,但此时i的值已经变成1了。而在(++i)这个表达式中,i的值则是1 + 1,所以执行(++i)后,i的值为2,那么j的值也就是0 + 2

大家千万要记住,不管是i++也好,++i也罢,变量i的值都会立刻增加,所以只看i的值,这两者是没有区别的,它的区别在我们说的另一个概念上,也就是所谓的表达式的返回值。

好了,「授人以鱼不如授人以渔」,如何证明我说的就是对的,别人的是错误的呢?C语言就是有一个好处,一切纷繁复杂的表象都能回归事物的本质。因为C语言与汇编语言是一一对应的,因此我们只需要查看C语言翻译成汇编语言后,在计算机内部到底发生了什么就能掌握真理,而无需人云亦云。

为了让生成的汇编语言更简单,我们去除头文件,编写最简单的代码test.c

int main(void){
    int i = 0;
    i++ + ++i;
    return 0;
}

打开cmd命令行,使用gcc命令生成汇编源码,这里学习一个新的gcc参数-S

gcc -S test.c

打开生成的test.s文件,这里截取关键部分如下:

 call __main
 movl $0, -4(%rbp)
 movl -4(%rbp), %eax
 addl $1, %eax
 movl %eax, -4(%rbp)
 addl $1, -4(%rbp)
 movl $0, %eax
 addq $48, %rsp
 popq %rbp
 ret

这里call __main相当于main函数入口,ret相当于return 0,这之间一段也就对应我们的两行C语言代码。特别说明一下,这里使用的gnu的工具链生成的是AT&T的x86-64汇编代码,而非大家熟悉的intel 80386汇编。高校教的汇编语言都是intel x86的32位汇编,因此学过汇编的人可能也会感觉非常陌生。实际上这段汇编非常简单,并不需要有什么汇编基础。

简单解释一下指令movl 对应80386汇编中的mov指令,是单词move的缩写,表示传递数据,addl则对应add指令,表示加法器。这里的-4(%rbp)表示的是一个内存地址,eax则是32位对应的8个通用寄存器中的第一个。movl $0, -4(%rbp)这句表示把一个常量0存到一个内存地址中,对应int i = 0;此后,-4(%rbp)这个地址就代指变量imovl -4(%rbp), %eax这句表示将变量i中的值取出来放到一个名叫eax的寄存器中。addl $1, %eax则对应i++,表示将常量1与寄存器eax的值相加,然后存到eax中,那么此时eax的值就是1。紧接着movl %eax, -4(%rbp),表示将寄存器eax的值刷新到变量i中,故而i++后,i的值立刻发生改变。 然后是addl $1, -4(%rbp),这句对应的C语言代码是++i,它表示将常量1直接与变量i的值相加,结果仍然保存到变量i中,那么此时就是1+1,故而变量i最后等于2。

到这里,其实汇编代码就结束了,并没有将(i++)的整体结果与(++i)的整体结果做最后的求和,这是因为我们没有用一个 变量来保存他们的和,所以编译器对C语言代码进行了优化,既然我们不需要结果,它干脆就不计算了。

现在修改代码,并再次生成汇编代码

int main(void){
    int i = 0;
    int j = i++ + ++i;
    return 0;
}

这次生成的汇编代码稍复杂,简单说明一下,edxeax都是32位通用寄存器,rax则是64位寄存器,在此处,可以把raxeax等同,可以看做是同一个寄存器。那么leal 1(%rax), %edx则表示,将寄存器rax(即eax)中的值加1,然后存到edx寄存器中。-4(%rbp)-8(%rbp)分别是变量i和变量j的内存地址,可以指代这两个变量。

通过上述汇编代码,我们可以清晰的发现,无论是i++还是++i,变量i的值都会立刻被改变。

最后,「关于i++++i的辟谣:」有一些陈旧的资料中指出,++i的性能要比i++更好,因为它是直接在内存中加1,在for循环中,推荐使用++i。让我们再次编写C代码,生成汇编代码来验证这个观点

int main(void){
    int i = 0;
    int j = 0;

    i++;
    ++j;
}

汇编代码

 call __main
 movl $0, -4(%rbp)
 movl $0, -8(%rbp)
 addl $1, -4(%rbp)
 addl $1, -8(%rbp)
 movl $0, %eax
 addq $48, %rsp
 popq %rbp
 ret

可以看到,i++;++j;生成的汇编代码一模一样,不存在谁性能更好的说法。现代编译器中,都已做了优化处理,因此你喜欢写那种风格都没问题。

关系运算符

用于比大小的一些运算,其中==表示两者相等<<=>>===

逻辑运算符

这是任何一种编程语言都具备的,如下,表示逻辑与或非&&||!

赋值运算符

=表示赋值运算符,在C语言中,存在「左值」「右值」的概念。简单说,=左边的叫左值,右边的叫右值。左值只能是计算机内存中的对象,而不能是常量或计算的结果。例如变量可以成为左值,而像5i + 2这样的不能做左值。

注意,重点来了,C语言中=运算符存在赋值陷阱!

首先看C语言的连环赋值语法

int i,j,k;

i = j = k = 0;

=遵循右结合,所有它等价于i = (j = (k = 0)),也就是说0先赋值给k,然后k的值再赋值给j,以此类推。Ok,这样是没问题的。

再看如下代码

    int i;
    float j;
    j = i = 6.1f;

j最终的值变成了6.0,这就是赋值陷阱。也就是说=存在类型自动转换的问题,值传递给i时,自动转化为int型,丢弃了小数部分。

除此外,赋值运算符还存在复合用法如下

    int8_t a = 0;
    int16_t b = 0;
    int32_t c = 0;
    int64_t d = 0;

    a += 1;  // 等价于 a = a + 1
    b -=1;   // 等价于 a = a - 1
    c *= 1;  // 等价于 a = a * 1
    d /=2;   // 等价于 a = a / 1
    d %=2;   // 等价于 a = a % 1

运算符优先级

算术运算符 > 关系运算符 > 逻辑运算符 > 赋值运算符。逻辑运算符中逻辑非 !除外

优先级

运算符

名称或含义

使用形式

结合方向

说明

1

[]

数组下标

数组名[常量表达式]

左到右


()

圆括号

(表达式)
函数名(形参表)


.

成员选择(对象)

对象.成员名


->

成员选择(指针)

对象指针->成员名


2

-

负号运算符

-表达式

右到左

单目运算符

(类型)

强制类型转换

(数据类型)表达式


++

自增运算符

++变量名
变量名++

单目运算符

--

自减运算符

--变量名
变量名--

单目运算符

*

取值运算符

*指针变量

单目运算符

&

取地址运算符

&变量名

单目运算符

!

逻辑非运算符

!表达式

单目运算符

~

按位取反运算符

~表达式

单目运算符

sizeof

长度运算符

sizeof(表达式)


3

/

表达式 / 表达式

左到右

双目运算符

*

表达式*表达式

双目运算符

%

余数(取模)

整型表达式%整型表达式

双目运算符

4

+

表达式+表达式

左到右

双目运算符

-

表达式-表达式

双目运算符

5

<<

左移

变量<<表达式

左到右

双目运算符

>>

右移

变量>>表达式

双目运算符

6

>

大于

表达式>表达式

左到右

双目运算符

>=

大于等于

表达式>=表达式

双目运算符

<

小于

表达式<表达式

双目运算符

<=

小于等于

表达式<=表达式

双目运算符

7

==

等于

表达式==表达式

左到右

双目运算符

!=

不等于

表达式!= 表达式

双目运算符

8

&

按位与

表达式&表达式

左到右

双目运算符

9

^

按位异或

表达式^表达式

左到右

双目运算符

10

|

按位或

表达式|表达式

左到右

双目运算符

11

&&

逻辑与

表达式&&表达式

左到右

双目运算符

12

||

逻辑或

表达式||表达式

左到右

双目运算符

13

?:

条件运算符

表达式1? 表达式2: 表达式3

右到左

三目运算符

14

=

赋值运算符

变量=表达式

右到左


/=

除后赋值

变量/=表达式


*=

乘后赋值

变量*=表达式


%=

取模后赋值

变量%=表达式


+=

加后赋值

变量+=表达式


-=

减后赋值

变量-=表达式


<<=

左移后赋值

变量<<=表达式


>>=

右移后赋值

变量>>=表达式


&=

按位与后赋值

变量&=表达式


^=

按位异或后赋值

变量^=表达式


|=

按位或后赋值

变量|=表达式


15

,

逗号运算符

表达式,表达式,…

左到右


「常见运算符优先级问题」

优先级问题 表达式 错误认识 实际结果
. 的优先级高于*-> 操作符用于消除这个问题) *p.f (*p).f *(p.f)
[] 高于 * int *ap[] int (*ap)[] int *(ap [])
函数() 高于 * int *fp() int (*fp)() int* ( fp() )
==!= 高于位操作 (val & mask != 0) (val & mask) != 0 val & (mask != 0)
==!= 高于赋值符 c = getchar() != EOF (c = getchar()) != EOF c = (getchar() != EOF)
算术运算符高于位移运算符 msb << 4 + lsb (msb << 4) + lsb msb << (4 + lsb)
逗号运算符在所有运算符中优先级最低 i = 1, 2 i = (1,2) (i = 1), 2

条件分支与循环

条件分支

C语言的条件分支与其他语言相似if-else分支,如下结构,这是Linux C语言推荐的代码范式,即将一个花括号紧跟小括号之后,写在同一行。

if (1 > 0){
// do something
}else{
// do something
}

if后面的条件表达式中存在陷阱,在C语言中没有布尔类型,使用0和非0来表示false和true。因此很多人会想当然的以为0是false,大于0就是true,实际上,-1也是true,要注意,是一切非0值,包括小数也是true。

if-else中只有一句时,语法上是可以省略花括号的,但是不建议这样,尤其包含嵌套的if语句时。C语言语法比较自由,正是如此,才更应该遵守规范。始终写上花括号,养成良好的编程规范,使代码易于阅读和维护。

if(a>b) max=a;
else max=b;

// 或者放两行
if(a>b) 
 max=a;
else 
 max=b;

多重条件的复合判断

if(/*条件1*/){
    //语句块1
else  if(/*条件2*/){
    //语句块2
else  if(/*条件3*/){
    //语句块3
}else{
     //语句块n
}

当复合的条件过多时,直接使用if - else if - else会显得代码冗长,因此C语言也提供了另一种语法编写选择分支,与Java、JavaScript等语言的switch相同

int a = 1;

switch(a){
        case 1
         printf("Mondayn"); 
         break;
        case 2
         printf("Tuesdayn"); 
         break;
        case 3
         printf("Wednesdayn"); 
         break;
        case 4
         printf("Thursdayn"); 
         break;
        case 5
         printf("Fridayn"); 
         break;
        case 6
         printf("Saturdayn"); 
         break;
        case 7
         printf("Sundayn"); 
         break;
        default:
         printf("errorn"); 
         break;
    }

需要注意,case 后面必须是一个整数,或者是结果为整数的表达式,但不能包含任何变量。在使用C语言的switch语句时,如果直接在case下声明了新的变量,会导致编译报错,此时需要在case下用一对花括号{}将所有代码包裹起来。

循环

while

最简单的循环当是while循环

while(/*表达式*/){
    //语句块
}

int i=1, sum=0;
while( i<=100 ){
    sum+=i;
    i++;
}

除此外,还存在while循环的变体,do - while循环

do{
    //语句块
}while(/*表达式*/);

//---------------------------------
int i=1, sum=0;

do{
    sum+=i;
    i++;
}while(i<=100);

do-while循环与while循环的不同在于,它会先执行“语句块”,然后再判断表达式是否为真,如果为真则继续循环;如果为假,则终止循环。因此,do-while 循环至少要执行一次“语句块”。再使用do-while循环时,要记住,while(i<=100);的小括号后面必须跟一个分号。

for

C语言中更常用的可能是for循环

「for 循环的一般形式」

for(表达式1; 表达式2; 表达式3){
    语句块
}
  1. 先执行“表达式1”。
  2. 再执行“表达式2”,如果它的值为真(非0),则执行循环体,否则结束循环。
  3. 执行完循环体后再执行“表达式3”。
  4. 重复执行步骤 2 和 3,直到“表达式2”的值为假,就结束循环。
// 使用for循环,进行等差数列求和
int sum=0;
for(int i=1; i<=100; i++){
    sum+=i;
}

printf("%dn",sum);

for 循环中的三个表达式都是可选项,都可以省略,但分号必须保留。

int i = 1, sum = 0;
for( ; i<=100; i++){
    sum+=i;
}

// 省略两个
for( ; i<=100 ; ){
    sum=sum+i;
    i++;
}

// 全部省略,表示死循环,等同于while(1){}
for( ; ; ){
// do something
}

实际上,for循环的灵活用法,完全可以替代while循环。另外,for循环中也能使用逗号表达式,当循环体只有一行时,亦可省略花括号

//表达式1 和 表达式3都是一个逗号表达式,即用逗号连接了两个表达式。
for( i=0,j=100; i<=100; i++,j-- )  k=i+j;

控制循环

在适当的时候,我们需要退出循环或跳过本次循环,这时候就需要控制循环。 控制循环通常使用breakcontinue关键字。

break 关键字用于 while、for 循环时,会终止循环而执行整个循环体后面的代码。break 关键字通常和 if 语句一起使用,即满足条件时便跳出循环

int i=1, sum=0;
while(1){  //死循环
    sum+=i;
    i++;
    if(i>100break;  //满足条件退出循环
}

continue 的作用是跳过本次循环中剩余的语句而强制进入下一次循环。它只用在 while、for 循环中,常与 if 条件语句一起使用

// 打印奇数
for(int i=1; i<=100; i++){
    if(i%2 == 0){   // 遇到偶数时跳过
     continue;
    }
    printf("%dn",i);
}

视频课程

了解视频课程,关注博主的视频网校

云课堂

「:编程之路从0到1」


最后编辑于:2024/1/10 拔丝英语网

admin-avatar

英语作文代写、国外视频下载

高质量学习资料分享

admin@buzzrecipe.com