当前正在阅读
C语言程序设计
预计阅读时间: 2 小时
  知识库提供的所有文档均为本站版权所有,禁止任何未经授权的个人或企业发布、传播、售卖本站提供的文档,如经发现,本站有权起诉侵权方并追究法律责任。

C语言基础

前面我们已经搭建好了基本的学习环境,现在就让我们开始C语言的学习吧!

C语言的语法层面内容相比其他语言来说,其实算少的了,但是它的难点在于很多概念上的理解,这也是为什么上一章一直在说一些计算机基础相关内容(包括这一章还会继续补一点),这样会有助于各位对于语言的理解,C语言可以说是步入编程领域的分水岭,跨过了这道坎,后续其他编程语言的学习都会无比轻松。

学习编程的过程可能会很枯燥,但是请各位一定不要心急,一步一个脚印,相信大家一定能通关。

C程序基本格式

前面我们在创建项目之后自动生成了一个.c文件,这个就是我们编写的程序代码文件:

c 复制代码
#include <stdio.h>

int main() {
    printf("Hello World!");
  	return 0;
}

操作系统需要执行我们的程序,但是我们的程序中可能写了很多很多的代码,那么肯定需要知道从哪里开始执行才可以,也就是程序的入口,所以我们需要提供一个入口点,我们的C语言程序入口点就是main函数(不过现在还没有讲到函数,所以各位就理解为固定模式即可)它的写法是:

c 复制代码
int main() {  //所有的符号一律采用英文的,别用中文
    程序代码...
}

注意是int后面空格跟上main(),我们的程序代码使用花括号{}进行囊括(有的人为了方便查阅,会把前半个花括号写在下面)

然后我们看到,如果我们需要打印一段话到控制台,那么就需要使用printf(内容)来完成,这其实就是一种函数调用,但是现在我们还没有接触到,我们注意到括号里面的内容就是我们要打印到控制台的内容:

c 复制代码
printf("Hello World!");   //注意最后需要添加;来结束这一行,注意是英文的分号,不是中文的!

我们要打印的内容需要采用双引号进行囊括,被双引号囊括的这一端话,我们称为字符串,当然我们现在还没有学到,所以各位也是记固定模式就好,当我们需要向控制台打印一段话时,就要用双引号囊括这段话,然后放入printf即可。我们会在后续的学习中逐渐认识printf函数。

最顶上还有一句:

c 复制代码
#include <stdio.h>

这个是引入系统库为我们提供的函数,包括printf在内,所以我们以后编写一个C语言程序,就按照固定模式:

c 复制代码
#include <stdio.h>

int main() {
    程序代码
}

除了程序代码部分我们会进行编写之外,其他的地方采用固定模式就好。

我们在写代码的过程中可以添加一些注释文本,这些文本内容在编译时自动忽略,所以比如我们想边写边记点笔记,就可以添加注释,注释的格式为:

java 复制代码
#include <stdio.h>   //引入标准库头文件

int main() {   //主函数,程序的入口点
    printf("Hello World!");  //向控制台打印字符串
}

当然我们也可以添加多行注释:

java 复制代码
#include <stdio.h>

/*
 * 这是由IDE自动生成的测试代码
 * 还是可以的
 */
int main() {
    printf("Hello World!");
  	//最后还有一句 return 0; 但是我们可以不用写,编译器会自动添加,所以后面讲到之后我们再来说说这玩意。
}

OK,基本的一些内容就讲解完毕了。

基本数据类型

我们的程序离不开数据,比如我们需要保存一个数字或是字母,这时候这些东西就是作为数据进行保存,不过不同的数据他们的类型可能不同,比如1就是一个整数,0.5就是一个小数,A就是一个字符,C语言提供了多种数据类型供我们使用,我们就可以很轻松的使用这些数据了。

不同的数据类型占据的空间也会不同,这里我们需要先提一个概念,就是字、字节是什么?

我们知道,计算机底层实际上只有0和1能够表示,这时如果我们要存储一个数据,比如十进制的3,那么就需要使用2个二进制位来保存,二进制格式为11,占用两个位置,再比如我们要表示十进制的15,这时转换为二进制就是1111占用四个位置(4个bit位)来保存。一般占用8个bit位表示一个字节(B),2个字节等于1个字,所以一个字表示16个bit位,它们是计量单位。

我们常说的内存大小1G、2G等,实际上就是按照下面的进制进行计算的:

8 bit = 1 B ,1024 B = 1KB,1024 KB = 1 MB,1024 MB = 1GB,1024 GB = 1TB,1024TB = 1PB(基本上是1024一个大进位,但是有些硬盘生产厂商是按照1000来计算的,所以我们买电脑的硬盘容量可能是512G的但是实际容量可能会缩水)

在不同位数的系统下基本数据类型的大小可能会不同,因为现在主流已经是64位系统,本教程统一按照64位系统进行讲解。

原码、反码和补码

原码

上面我们说了实际上所有的数字都是使用0和1这样的二进制数来进行表示的,但是这样仅仅只能保存正数,那么负数怎么办呢?

比如现在一共有4个bit位来保存我们的数据,为了表示正负,我们可以让第一个bit位专门来保存符号,这样,我们这4个bit位能够表示的数据范围就是:

  • 最小:1111 => - (22+21+2^0) => -7
  • 最大:0111 => + (22+21+2^0) => +7 => 7

虽然原码表示简单,但是原码在做加减法的时候,很麻烦!以4bit位为例:

1+(-1) = 0001 + 1001 = 怎么让计算机去计算?(虽然我们知道该去怎么算,但是计算机不知道,计算机顶多知道1+1需要进位!)

我们得创造一种更好的表示方式!于是我们引入了反码:

反码

正数的反码是其本身
负数的反码是在其原码的基础上, 符号位不变,其余各个位取反
经过上面的定义,我们再来进行加减法:

1+(-1) = 0001 + 1110 = 1111 => -0 (直接相加,这样就简单多了!)

思考:1111代表-0,0000代表+0,在我们实数的范围内,0有正负之分吗?

0既不是正数也不是负数,那么显然这样的表示依然不够合理!

补码

根据上面的问题,我们引入了最终的解决方案,那就是补码,定义如下:

正数的补码就是其本身 (不变!)
负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)
其实现在就已经能够想通了,-0其实已经被消除了!我们再来看上面的运算:

1+(-1) = 0001 + 1111 = (1)0000 => +0 (现在无论你怎么算,也不会有-0了!)

所以现在,4bit位能够表示的范围是:-8~+7(C使用的就是补码!)

整数类型

我们首先来看看整数类型,整数就是不包含小数点的数据,比如199666等数字,整数包含以下几种类型:

  • int - 占用 4 个字节,32个bit位,能够表示 -2,147,483,648 到 2,147,483,647 之间的数字,默认一般都是使用这种类型
  • long - 占用 8 个字节,64个bit位。
  • short - 占用2个字节,16个bit位。

浮点类型

浮点类一般用于保存小数,不过为啥不叫小数类型而是浮点类型呢?因为我们的一个小数分为整数部分和小数部分,我们需要用一部分的bit位去表示整数部分,而另一部分去表示小数部分,至于整数部分和小数部分各自占多少并不是固定的,而是浮动决定的(在计算机组成原理中会深入学习,这里就不多介绍了)

  • float - 单精度浮点,占用4个字节,32个bit位。
  • double - 双精度浮点,占用8个字节,64个bit位。

字符类型

除了保存数字之外,C语言还支持字符类型,我们的每一个字符都可以使用字符类型来保存:

  • char - 占用1个字节(-128~127),可以表示所有的ASCII码字符,每一个数字对应的是编码表中的一个字符:
image-20220603114358826

编码表中包含了所有我们常见的字符,包括运算符号、数字、大小写字母等(注意只有英文相关的,没有中文和其他语言字符,包括中文的标点符号也没有)

某些无法直接显示的字符(比如换行,换行也算一个字符)需要使用转义字符来进行表示:

img

有关基本类型的具体使用我们放到下一节进行讲解。


变量

前面我们了解了C语言中的基本类型,那么我们如何使用呢?这时我们就可以创建不同类型的变量了。

变量的使用

变量就像我们在数学中学习的xy一样,我们可以直接声明一个变量,并利用这些变量进行基本的运算,声明变量的格式为:

c 复制代码
数据类型 变量名称 = 初始值;    //其中初始值可以不用在定义变量时设定
// = 是赋值操作,可以将等号后面的值赋值给前面的变量,等号后面可以直接写一个数字(常量)、变量名称、算式

比如我们现在想要声明一个整数类型的变量:

java 复制代码
int a = 10;   //变量类型为int(常用),变量名称为a,变量的初始值为10
c 复制代码
int a = 10, b = 20;   //多个变量可以另起一行编写,也可以像这样用逗号隔开,注意类型必须是一样的

其中,变量的名称并不是随便什么都可以的,它有以下规则:

  • 不能重复使用其他变量使用过的名字。
  • 只能包含英文字母或是下划线、数字,并且严格区分大小写,比如aA不算同一个变量。
  • 虽然可以包含数字,但是不能以数字开头。
  • 不能是关键字(比如我们上面提到的所有基本数据类型,当然还有一些关键字我们会在后面认识)
  • (建议)使用英文单词,不要使用拼音,多个词可以使用驼峰命名法或是通过下划线连接。

初始值可以是一个常量数据(比如直接写10、0.5这样的数字)也可以是其他变量,或是运算表达式的结果,这样会将其他变量的值作为初始值。

我们可以使用变量来做一些基本的运算:

java 复制代码
#include <stdio.h>

int main() {
    int a = 10;  //将10作为a的值
    int b = 20;
    int c = a + b;   //注意变量一定要先声明再使用,这里是计算a + b的结果(算式),并作为c的初始值
}

这里使用到了+运算符(之后我们还会介绍其他类型的运算符)这个运算符其实就是我们数学中学习的加法运算,会将左右两边的变量值加起来,得到结果,我们可以将运算结果作为其他变量的初始值,还是很好理解的。

但是现在虽然做了运算,我们还不知道运算的具体结果是什么,所以这里我们通过前面认识的printf函数来将结果打印到控制台:

c 复制代码
#include <stdio.h>

int main() {
    int a = 10;
    int b = 20;
    int c = a + b;

    printf(c);   //直接打印变量c
}

但是我们发现这样似乎运行不出来结果,不对啊,前面你不是说把要打印到控制台的内容写到printf中吗,怎么这里不行呢?实际上printf是用于格式化打印的,我们来看看如何进行格式化打印,输出我们的变量值:

c 复制代码
printf("c的结果是:%d", );   //使用%d来代表一个整数类型的数据(占位符),在打印时会自动将c的值替换上去

我们来看看效果:

image-20220603131740600

这样,我们就知道该如何打印我们变量的值了,当然,除了使用%d打印有符号整数之外,还有其他的:

格式控制符 说明
%c 输出一个单一的字符
%hd、%d、%ld 以十进制、有符号的形式输出 short、int、long 类型的整数
%hu、%u、%lu 以十进制、无符号的形式输出 short、int、long 类型的整数
%ho、%o、%lo 以八进制、不带前缀、无符号的形式输出 short、int、long 类型的整数
%#ho、%#o、%#lo 以八进制、带前缀、无符号的形式输出 short、int、long 类型的整数
%hx、%x、%lx %hX、%X、%lX 以十六进制、不带前缀、无符号的形式输出 short、int、long 类型的整数。如果 x 小写,那么输出的十六进制数字也小写;如果 X 大写,那么输出的十六进制数字也大写。
%#hx、%#x、%#lx %#hX、%#X、%#lX 以十六进制、带前缀、无符号的形式输出 short、int、long 类型的整数。如果 x 小写,那么输出的十六进制数字和前缀都小写;如果 X 大写,那么输出的十六进制数字和前缀都大写。
%f、%lf 以十进制的形式输出 float、double 类型的小数
%e、%le %E、%lE 以指数的形式输出 float、double 类型的小数。如果 e 小写,那么输出结果中的 e 也小写;如果 E 大写,那么输出结果中的 E 也大写。
%g、%lg %G、%lG 以十进制和指数中较短的形式输出 float、double 类型的小数,并且小数部分的最后不会添加多余的 0。如果 g 小写,那么当以指数形式输出时 e 也小写;如果 G 大写,那么当以指数形式输出时 E 也大写。
%s 输出一个字符串

比如现在我们要进行小数的运算,还记得我们前面介绍的小数类型有哪些吗?

c 复制代码
#include <stdio.h>

int main() {
    double a = 0.5;
    float b = 2.5f;   //注意直接写2.5默认表示的是一个double类型的值,我们需要再后面加一个f或是F表示是flaot类型值

    printf("a + b的结果是:%f", a + b);   //根据上表得到,小数类型需要使用%f表示,这里我们可以直接将a + b放入其中
}

可以看到,结果也是正确的:

image-20220603132459810

当然,我们也可以一次性打印多个,只需要填写多个占位符表示即可:

c 复制代码
#include <stdio.h>

int main() {
    double a = 0.5;
    float b = 2.5f;   //整数类型默认是int,如果要表示为long类型的值,也是需要在最后添加一个l或L

    printf("a = %f, b = %f", a, b);   //后面可以一直添加(逗号隔开),但是注意要和前面的占位符对应
}

结果也是正常的:

image-20220603132713970

我们再来看看字符类型:

c 复制代码
char c = 'A';   //字符需要使用单引号囊括,且只能有一个字符,不能写成'AA',这就不是单个字符了
//注意这里的A代表的是A这个字符,对应的ASCII码是65,实际上c存储的是65这个数字

我们也可以通过格式化打印来查看它的值:

c 复制代码
#include <stdio.h>

int main() {
    char c = 'A';
    printf("变量c的值为:%c 对应的ASCII码为:%d", c, c);   //这里我们使用%c来以字符形式输出,%d输出的是变量数据的整数形式,其实就是对应的ASCII码
}
image-20220603133727498

当然,我们也可以直接让char存储一个数字(ASCII码),同样也可以打印出对应的字符:

c 复制代码
#include <stdio.h>

int main() {
    char c = 66;
    printf("变量c的值为:%c 对应的ASCII码为:%d", c, c);
}
image-20220603133858133

那么现在请各位小伙伴看看下面这段代码会输出什么:

c 复制代码
#include <stdio.h>

int main() {
    int a = 10;
    char c = 'a';
    printf("变量c的ASCII码为:%d", c);
}

没错,这里得到的结果就是字符a的ASCII码值,注意千万不要认为c得到的是变量a的值,这里使用的是字符a,跟上面的变量a半毛钱关系都没有:

image-20220603134234040

但是如果我们去掉引号,就相当于把变量a的值给了c,c现在的ASCII码就是10了,所以这里一定要分清楚。

对于某些无法表示的字符,比如换行这类字符,我们没办法直接敲出来,只能使用转义字符进行表示:

c 复制代码
char c = '\n';

详细的转义字符表参见前面的基本数据类型章节。

变量除了有初始值之外,也可以在后续的过程中得到新的值:

c 复制代码
#include <stdio.h>

int main() {
    short s = 10;
    s = 20;    //重新赋值为20,注意这里就不要再指定类型了,指定类型只有在声明变量时才需要
    printf("%d", s);   //打印结果
}

可以看到,得到的是我们最后一次对变量修改的结果:

image-20220603135152184

那要是我们不对变量设定初始值呢?那么变量会不会有默认值:

c 复制代码
#include <stdio.h>

int main() {
    int a, b, c, d;
    printf("%d,%d,%d,%d", a, b, c, d);
}

可以看到,虽然定义变量但是我们没有为其设定初始值,那么它的值就是不确定的了(千万注意并不是不设定值默认就是0):

image-20230814161423039

所以各位小伙伴以后在使用时一定要注意这个问题,至于为什么不是0,这是因为内存分配机制,我们在下一章高级篇再进行讲解。

我们再来看一个例子:

c 复制代码
#include <stdio.h>

int main() {
    char c = 127;    //已经到达c的最大值了
    c = c + 1;   //我不管,我就要再加
    printf("%d", c);    //这时会得到什么结果?
}
image-20220603143909688

怎么127加上1还变成-128了呢?这是由于位数不够,导致运算结果值溢出:

  • 127 + 1= 01111111 + 1
  • 由于现在是二进制,满2进1,所以最后变成
  • 10000000 = 补码形式的 -128

所以,了解上面这些计算机底层原理是很重要的,我们能够很轻松地知道为什么会这样。

在我们的运算中,可能也会存在一些一成不变的值,比如π的值永远都是3.1415....,在我们的程序中,也可以使用这样不可变的变量,我们成为常量。

定义常量和变量比较类似,但是需要在前面添加一个const关键字,表示这是一个常量:

image-20230814161342923

可以看到,常量在一开始设定初始值后,后续是不允许进行修改的。

无符号数

我们知道,所有的数据底层都是采用二进制来进行保存的,而第一位则是用于保存符号位,但是如果我们不考虑这个符号位,那么所有的数都是按照正数来表示,比如考虑了符号位的char类型:

  • 考虑符号表示范围:-128~127
  • 不考虑符号:0~255

我们也可以直接使用这些不带符号位的数据类型:

c 复制代码
int main() {
    unsigned char c = -65;   //数据类型前面添加unsigned关键字表示采用无符号形式
    printf("%u", c);    //%u以无符号形式输出十进制数据
}

可以看到这里给了无符号char类型c一个-65的值,但是现在很明显符号位也是作为数值的表示部分,所以结果肯定不是-65:

image-20220603142210120

结合我们前面学习的基础知识,我们来看看为什么得到的是191这个数字。首先char类型占据一个字节,8个bit位:

  • 00000000 -> 现在赋值-65 -> -65的补码形式 -> 10111111
  • 由于现在没有符号位,一律都是正数,所以,10111111 = 128 + 32 + 16 + 8 + 4 + 2 + 1 = 191

我们也可以直接以无符号数形式打印:

c 复制代码
#include <stdio.h>

int main() {
    int i = -1;
    printf("%u", i);    //%u以无符号形式输出十进制数据
}
image-20220603143441616

得到无符号int的最大值。

类型转换

一种类型的数据可以转换为其他类型的数据,这种操作我们称为类型转换,类型转换分为自动类型转换强制类型转换,比如我们现在希望将一个short类型的数据转换为int类型的数据:

java 复制代码
#include <stdio.h>

int main() {
    short s = 10;
    int i = s;   //直接将s的值传递给i即可,但是注意此时s和i的类型不同
}

这里其实就是一种自动类型转换,自动类型转换就是编译器隐式地进行的数据类型转换,这种转换不需要我们做什么,我们直接写就行,会自动进行转换操作。

c 复制代码
float a = 3;    //包括这里我们给的明明是一个int整数3但是却可以赋值给float类型,说明也是进行了自动类型转换

如果我们使用一个比转换的类型最大值都还要大的值进行类型转换,比如:

c 复制代码
#include <stdio.h>

int main() {
    int a = 511;
    char b = a;   //最大127
    printf("%d", b);
}
image-20220606180919318

很明显char类型是无法容纳大于127的数据的,因为只占一个字节,而int占4个字节,如果需要进行转换,那么就只能丢掉前面的就只保留char所需要的那几位了,所以这里得到的就是-1:

  • 511 = int -> 00000000 00000000 00000001 11111111
  • char -> 11111111 -> -1

我们也可以将整数和小数类型的数据进行互相转换:

c 复制代码
#include <stdio.h>

int main() {
    int a = 99;
    double d = a;
    printf("%f", d);
}
image-20230814161659373

不过这里需要注意的是,小数类型在转换回整数类型时,会丢失小数部分(注意,不是四舍五入,是直接丢失小数!):

c 复制代码
#include <stdio.h>

int main() {
    double a = 3.14;
    int b = a;    //这里编译器还提示了黄标,我们可以通过之后讲到的强制类型转换来处理
    printf("%d", b);
}
image-20230814161647388

除了赋值操作可以进行自动类型转换之外,在运算中也会进行自动类型转换,比如:

c 复制代码
#include <stdio.h>

int main() {
    float a = 2;
    int b = 3;
    double c = b / a;   //  "/" 是除以的意思,也就是我们数学中的除法运算,这里表示a除以b
    printf("%f", c);
}
image-20220606191838425

可以看到,这里得到的结果是小数1.5,但是参与运算的既有整数类型,又有浮点类型,结果为什么就确定为浮点类型了呢?这显然是由于类型转换导致的。那么规则是什么呢?

image-20220606191412418
  • 不同的类型优先级不同(根据长度而定)
  • char和short类型在参与运算时一律转换为int再进行运算。
  • 浮点类型默认按双精度进行计算,所以就算有float类型,也会转换为double类型参与计算。
  • 当有一个更高优先级的类型和一个低优先级的类型同时参与运算时,统一转换为高优先级运算,比如int和long参与运算,那么int转换为long再算,所以结果也是long类型,int和double参与运算,那么先把int转换为double再算。

我们接着来看看强制类型转换,我们可以为手动去指定类型,强制类型转换格式如下:

c 复制代码
(强制转换类型) 变量、常量或表达式;

比如:

c 复制代码
#include <stdio.h>

int main() {
    int a = (int) 2.5;   //2.5是一个double类型的值,但是我们可以强制转换为int类型赋值给a,强制转换之后小数部分丢失
    printf("%d", a);
}

我们也可以对一个算式的结果进行类型转换:

c 复制代码
#include <stdio.h>

int main() {
    double a = 3.14;
    int b = (int) (a + 2.8);   //注意得括起来表示对整个算式的结果进行类型转换(括号跟数学中的挺像,也是提升优先级使用的,我们会在运算符部分详细讲解),不然强制类型转换只对其之后紧跟着的变量生效
    printf("%d", b);
}

在我们需要得到两个int相除之后带小数的结果时,强制类型转换就显得很有用:

java 复制代码
#include <stdio.h>

int main() {
    int a = 10, b = 4;
    double c = a / b;    //不进行任何的类型转换,int除以int结果仍然是int,导致小数丢失
    double d = (double) a / b;   //对a进行强制类型转换,现在是double和int计算,根据上面自动类型转换规则,后面的int自动转换为double,结果也是double了,这样就是正确的结果了
    printf("不进行类型转换: %f, 进行类型转换: %f", c, d);
}

合理地使用强制类型转换,能够解决我们很多情况下的计算问题。


运算符

前面我们了解了如何声明变量以及变量的类型转换,那么我们如何去使用这些变量来参与计算呢?这是我们本小节的重点。

基本运算符

基本运算符包含我们在数学中常用的一些操作,比如加减乘除,分别对应:

  • 加法运算符:+
  • 减法运算符:-
  • 乘法运算符:*
  • 除法运算符:/(注意不是“\”,看清楚一点)

当然,还有我们之前使用的赋值运算符=,我们先来看看赋值运算符的使用,其实在之前我们已经学习过了:

c 复制代码
变量 = 值   //其中,值可以直接是一个数字、一个变量、表达式的结果等

实际上等号左边的内容准确的说应该是一个左值,不过大部分情况下都是变量,这里就不展开左值和右值的话题了(感兴趣的小伙伴可以去详细了解,有助于后面学习C++理解右值引用)

最简单的用法就是我们前面所说的,对一个变量进行赋值操作:

c 复制代码
int a = 10;

也可以连续地使用赋值操作,让一连串的变量都等于后面的值:

c 复制代码
int a, b;
a = b = 20;   //从右往左依次给b和a赋值20

可以看出,实际上=运算除了赋值之外,和加减乘除运算一样也是有结果的,比如上面的 a = 就是b = 20 运算的结果(可以看着一个整体),只不过运算的结果就是b被赋值的值,也就是20。

我们接着来看加减法,这个就和我们数学中的是一样的了:

c 复制代码
#include <stdio.h>

int main() {
    int a = 10, b = 5;
    printf("%d", a + b);   //打印 a + b 的结果
}

当然也可以像数学中那样写在一个数或是变量的最前面,表示是正数:

c 复制代码
int a = +10, b = +5;

不过默认情况下就是正数,所以没必要去写一个+号。减法运算符其实也是一样的:

c 复制代码
#include <stdio.h>

int main() {
    int a = 10, b = 5;
    printf("%d", a - b);   //打印 a - b 的结果
}
c 复制代码
#include <stdio.h>

int main() {
    int a = -10;   //等于 -10
    printf("%d", -a);   //输出 -a 的值,就反着来嘛
}

接着我们来看看乘法和除法运算:

c 复制代码
#include <stdio.h>

int main() {
    int a = 20, b = 10;
    printf("%d, %d", a * b, a / b);   //使用方式和上面的加减法是差不多的
}

还有一个比较有意思的取模运算:

c 复制代码
#include <stdio.h>

int main() {
    int a = 20, b = 8;
    printf("%d", a % b);   //取模运算实际上就是计算a除以b的余数
}

不过很遗憾,在C中没有指数相关的运算符(比如要计算5的10次方),在后面学习了循环语句之后,我们可以尝试来自己实现一个指数运算。

运算符优先级

和数学中一样,运算符是有优先级的:

java 复制代码
#include <stdio.h>

int main() {
    int a = 20, b = 10;
    printf("%d", a + a * b);   //如果没有优先级,那么结果应该是400
}

很明显这里的结果是考虑了优先级的:

image-20230814161757064

在数学中,加减运算的优先级是没有乘除运算优先级高的,所以我们需要先计算那些乘除法,最后再来进行加减法的计算,而C语言中也是这样,运算符之间存在优先级概念。我们在数学中,如果需要优先计算加减法再计算乘除法,那么就需要使用括号来提升加减法的优先级,C语言也可以:

c 复制代码
#include <stdio.h>

int main() {
    int a = 20, b = 10;
    printf("%d", (a + a) * b);   //优先计算 a + a 的结果,再乘以 b
}

那要是遇到多重的呢?类似于下面的这种:

复制代码
数学上的写法:[1 - (3 + 4)] x (-2 ÷ 1) = ?

那么我们在C中就可以这样编写:

java 复制代码
#include <stdio.h>

int main() {
    printf("%d", (1 - (3 + 4)) * (-2 / 1));   //其实写法基本差不多,只需要一律使用小括号即可
}

这样,我们就可以通过()运算符,来提升运算优先级了。

我们来总结一下,上面运算符优先级如下,从左往右依次递减:

  • () > + - (做符号表示,比如-9) > * / % > + - (做加减运算) > =

根据上面的优先级,我们来看看下面a的结果是什么:

c 复制代码
int c;
int a = (3 + (c = 2)) * 6;
c 复制代码
int b, c;
int a = (b = 5, c = b + 8);  //逗号运算符从前往后依次执行,赋值结果是最后边的结果

自增自减运算符

我们可以快速使用自增运算符来将变量的值+1,正常情况下我们想要让一个变量值自增需要:

c 复制代码
int a = 10;
a = a + 1;

现在我们只需要替换为:

c 复制代码
int a = 10;
++a;   //使用自增运算符,效果等价于 a = a + 1

并且它也是有结果的,除了做自增运算之外,它的结果是自增之后的值:

c 复制代码
#include <stdio.h>

int main() {
    int a = 10;
    //int b = a = a + 1;  下面效果完全一致
    int b = ++a;
    printf("%d", b);
}

当然我们也可以将自增运算符写到后面,和写在前面的区别是,它是先返回当前变量的结果,再进行自增的,顺序是完全相反的:

c 复制代码
#include <stdio.h>

int main() {
    int a = 10;
    int b = a++;   //写在后面和写在前面是有区别的
    printf("a = %d, b = %d", a, b);
}

重点内容:自增运算符++在前,那么先自增再出结果;自增运算符++在后,那么先出结果再自增。各位小伙伴可以直接记运算符的位置,来方便记忆。

那要是现在我们不想自增1而是自增2或是其他的数字呢?我们可以使用复合赋值运算符,正常情况下依然是使用普通的赋值运算符:

c 复制代码
int a = 10;
a = a + 5;

但是现在我们可以简写:

c 复制代码
int a = 10;
a += 5;

效果和上面是完全一样的,并且得到的结果也是在自增之后的:

c 复制代码
#include <stdio.h>

int main() {
    int a = 10;
    int b = a += 5;
    printf("a = %d", b);
}

复合赋值运算符不仅仅支持加法,还支持各种各样的运算:

c 复制代码
#include <stdio.h>

int main() {
    int a = 10;
    a %= 3;   //可以复合各种运算,比如加减乘除、模运算、包括我们我们还要讲到的位运算等
    printf("a = %d", a);
}

当然,除了自增操作之外,还有自减操作:

c 复制代码
#include <stdio.h>

int main() {
    int a = 10;
    a--;   //--是自减操作,相当于a = a - 1,也可以在前后写,规则和上面的自增是一样的
    printf("a = %d", a);
}

注意自增自减运算符和+-做符号是的优先级一样,仅次于()运算符,所以在编写时一定要注意:

c 复制代码
#include <stdio.h>

int main() {
    int a = 10;
    int b = 5 * --a;
    printf("b = %d", b);
}

位运算符

前面我们学习了乘法运算符*,当我们想要让一个变量的值变成2倍,只需要做一次乘法运算即可:

c 复制代码
int a = 10;
a *= 2;  //很明显算完之后a就是20了

但是我们现在可以利用位运算来快速进行计算:

c 复制代码
int a = 10;
a = a << 1;   //也可以写成复合形式 a <<= 1

我们会发现这样运算之后得到的结果居然也是20,这是咋算出来的呢?实际上<<是让所有的bit位进行左移操作,上面就是左移1位,我们可以来看看:

  • 10 = 00001010 现在所以bit位上的数据左移一位 00010100 = 20

是不是感觉特别神奇?就像我们在十进制中,做乘以10的操作一样:22乘以10那么就直接左移了一位变成220,而二进制也是一样的,如果让这些二进制数据左移的话,那么相当于在进行乘2的操作。

比如:

c 复制代码
#include <stdio.h>

int main() {
    int a = 6;
    a = a << 2;   //让a左移2位,实际上就是 a * 2 * 2,a * 2的平方(类比十进制,其实还是很好理解的)
    printf("a = %d", a);
}

当然能左移那肯定也可以右移:

c 复制代码
#include <stdio.h>

int main() {
    int a = 6;
    a = a >> 1;   //右移其实就是除以2的操作
    printf("a = %d", a);
}

当然除了移动操作之外,我们也可以进行按位比较操作,先来看看按位与操作:

c 复制代码
#include <stdio.h>

int main() {
    int a = 6, b = 4;
    int c = a & b;   //按位与操作
    printf("c = %d", c);
}

按位与实际上也是根据每个bit位来进行计算的:

  • 4 = 00000100
  • 6 = 00000110
  • 按位与实际上就是让两个数的每一位都进行比较,如果两个数对应的bit位都是1,那么结果的对应bit位上就是1,其他情况一律为0
  • 所以计算结果为:00000100 = 4

除了按位与之外,还有按位或运算:

c 复制代码
int a = 6, b = 4;
int c = a | b;
  • 4 = 00000100
  • 6 = 00000110
  • 按位与实际上也是让两个数的每一位都进行比较,如果两个数对应bit位上其中一个是1,那么结果的对应bit位上就是1,其他情况为0。
  • 所以计算结果为:00000110 = 6

还有异或和按位非(按位否定):

c 复制代码
int a = 6, b = 4;
int c = a ^ b;    //注意^不是指数运算,表示按位异或运算,让两个数的每一位都进行比较,如果两个数对应bit位上不同时为1或是同时为0,那么结果就是1,否则结果就是0,所以这里的结果就是2
a = ~a;   //按位否定针对某个数进行操作,它会将这个数的每一个bit位都置反,0变成1,1变成0,猜猜会变成几

按位运算都是操作数据底层的二进制位来进行的。

大纲 (于 2025年1月1日 更新)
正在加载页面,请稍后...