写在前面:
- 本系列专栏主要介绍C语言的相关知识,思路以下面的参考链接教程为主,大部分笔记也出自该教程。
- 除了参考下面的链接教程以外,笔者还参考了其它的一些C语言教材,笔者认为重要的部分大多都会用粗体标注(未被标注出的部分可能全是重点,可根据相关部分的示例代码量和注释量判断,或者根据实际经验判断)。
- 如有错漏欢迎指出。
参考教程:C语言程序设计从入门到进阶【比特鹏哥c语言2024完整版视频教程】(c语言基础入门c语言软件安装C语言指针c语言考研C语言专升本C语言期末计算机二级C语言c语言_哔哩哔哩_bilibili
一、操作符和表达式
1、算术操作符和算术表达式
(1)最常用的算术运算符:
运算符 | 含义 | 举例 | 结果 |
+ | 正号运算符(单目运算符) | +a | a的值 |
- | 负号运算符(单目运算符) | -a | a的算数负值(相反数) |
* | 乘法运算符(双目运算符) | a*b | a和b的乘积 |
/ | 除法运算符(双目运算符) | a/b | a除以b的商 |
% | 取余运算符(双目运算符) | a%b | a除以b的余数 |
+ | 加法运算符(双目运算符) | a+b | a与b的和 |
- | 减法运算符(双目运算符) | a-b | a与b的差 |
①两个实数相除的结果是双精度实数,两个整数相除的结果为整数(只舍不入),不过如果除数或被除数中有一个为负值,则相除的结果的舍入方向是不固定的。
②取余运算符要求参加运算的运算对象(即操作数)为整数,结果也为整数,除了取余运算符以外的运算符的操作数都可以是任何算数类型。
(2)自增(++)、自减(--)运算符:
①自增、自减运算符的作用是使变量的值加1或减1,分为前置和后置两种,如下所示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
//前置++和--
int a1 = 10;
int x1 = ++a1;
//先对a1进行自增,然后使用a1给x1赋值,x1为11。
int y1 = --a1;
//先对a1进行自减,然后使用a1给y1赋值,y1为10。
//后置++和--
int a2 = 10;
int x2 = a2++;
//先使用a2给x2赋值,然后a2自增,这样x2的值是10,之后a变成11。
int y2 = a2--;
//先使用a2给y2赋值,然后a2自减,这样y2的值是11,之后a变成10。
return 0;
}
②自增和自减运算符通常用在循环语句中,使循环变量自动加1;也用于指针变量,使指针指向下一个地址。
③不要在一个复杂的表达式中使用自增或自减运算符,否则结果可能会无法预测,在不同编译器中的测试结果会不一样。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int i = 10;
i = i-- - --i * (i = -3) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
(3)由算术运算符(包括单目和双目)连接运算分量而组成的式子称为算术表达式,每个算术表达式的值为一个数值,其类型按照以下规则确定:
①当参加运算的两个运算分量均为整型时(具体类型可以不同,比如一个为int型,另一个为char型),其运算结果为int型。
②当参加运算的两个运算分量中至少有一个是单精度型,并且另一个不是双精度型时,则运算结果为float型。
③当参加运算的两个运算分量中至少有一个是双精度型时,则运算结果为双精度型。
(4)算术转换:如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换,如果某个操作数的类型在下面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
1 | long double |
2 | double |
3 | float |
4 | unsigned long int |
5 | long int |
6 | unsigned int |
7 | int |
8 | unsigned short int |
9 | short int |
10 | unsigned char |
11 | char |
2、移位操作符
(1)移位操作符有左移操作符(<<)和右移操作符(>>)两种,它们都是双目运算符(即有两个操作符),且两个操作数都只能是整数。
(2)一个整数有它对应的二进制形式,移位操作符的作用则是将左操作数的二进制形式移动若干位,返回移位后的结果,具体移动多少位由右操作数决定(不要移动负数位,这个是标准未定义的),具体移动方向取决于是左移操作符还是右移操作符。
(3)左移操作符的移位规则:左边溢出的位抛弃、右边的空位补0。
①例1:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 7; //00000000000000000000000000000111(7的二进制)
int b = a << 1; //00000000000000000000000000001110(14的二进制)
printf("b=%d\n", b);
return 0;
}
②例2:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = -7; //11111111111111111111111111111001(-7的二进制补码)
int b = a << 1; //11111111111111111111111111110010(-14的二进制补码)
printf("b=%d\n", b);
return 0;
}
(4)右移操作符的移位规则:右移运算分为逻辑移位和算术移位,采取逻辑移位时左边的空位用0填充,采取算数移位时左边的空位用原值的符号位填充。(有符号数采取算术移位,无符号数采取逻辑移位)
①例1:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
unsigned int a = 2147483655; //10000000000000000000000000000111(2147483655的二进制补码)
unsigned int b = a >> 1; //01000000000000000000000000000011(1073741827的二进制补码,这里采用了逻辑移位)
printf("b=%u\n", b);
return 0;
}
②例2:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = -7; //11111111111111111111111111111001(-7的二进制补码)
int b = a >> 1; //11111111111111111111111111111100(-4的二进制补码,这里采用了算数移位)
printf("b=%d\n", b);
return 0;
}
3、位操作符
(1)位操作符有按位与(&)、按位或(|)、按位异或(^)和按位取反(~)四种,按位与、按位或和按位异或是双目运算符(即有两个操作数),按位取反是单目运算符(即只有一个操作数),它们的操作数都只能是整数。
(2)一个整数有它对应的二进制形式,位操作符的作用就是将它的两个操作数按位执行与/或/异或操作,或者将它唯一的操作数按位执行取反操作。
操作数1的一个位 | 1 | 1 | 0 | 0 |
操作数2的一个位 | 1 | 0 | 1 | 0 |
操作数1的一个位和操作数2的一个位按位与的结果 | 1 | 0 | 0 | 0 |
操作数1的一个位和操作数2的一个位按位或的结果 | 1 | 1 | 1 | 0 |
操作数1的一个位和操作数2的一个位按位异或的结果 | 0 | 1 | 1 | 0 |
操作数1的一个位按位取反的结果 | 1 | 0 |
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int num1 = 1; //00000000000000000000000000000001
int num2 = 2; //00000000000000000000000000000010
num1 & num2; //00000000000000000000000000000000(与:有假即假)
num1 | num2; //00000000000000000000000000000011(或:有真即真)
num1 ^ num2; //00000000000000000000000000000011(异或:不同则为真)
~num1 ; //11111111111111111111111111111110(取反:真假翻转)
return 0;
}
(3)举例:
①例1:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
a = a ^ b; //x^y^y=x x^y^x=y
b = a ^ b;
a = a ^ b;
printf("a = %d b = %d\n", a, b); //两数互换
return 0;
}
②例2:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 100;
int num = 0;
int count = 0;
while (num <= 32)
{
num++;
if (a & 1 == 1)
count++;
a = a >> 1;
}
//printf("%d\n", num);
printf("%d\n", count); //整数a存储在内存中的二进制中1的个数
return 0;
}
4、赋值操作符和赋值表达式
(1)赋值符号“=”就是赋值运算符,它的作用就是将一个数据(右操作数)赋给一个变量(左操作数),可以把一个常量赋给一个变量,也可以将一个变量或表达式赋给一个变量。赋值号也可以使用在常量和变量的声明语句中,用于给符号常量和变量赋初值,但这里的赋值号只起到赋初值的作用,并不构成赋值表达式。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int weight = 120; //初始化
weight = 89; //使用赋值操作符赋值
double salary = 10000.0;
salary = 20000.0; //使用赋值操作符赋值
int a = 10;
int x = 0;
int y = 20;
a = x = y + 1; //连续赋值,赋值号的运算顺序是从右向左(在实际应用中不建议连着写)
return 0;
}
(2)赋值运算符的左运算分量应该与右运算分量的类型相同,当两边的数据类型不同时,则在赋值前会自动把右边表达式的值转换为与左边变量类型相同的值,然后再把这个值赋给左边变量。(当把一个实数值赋给一个整型量时,将丢失小数部分,获得的只是整数部分)
(3)由赋值符号连接左端变量和右端表达式而构成的式子称为赋值表达式,每个赋值表达式都有一个值,这个值就是通过赋值得到的左端变量的值。(对于任一种赋值运算,其赋值号或复合赋值号左边必须是一个左值,左值是指具有对于的可由用户访问的存储单元,并且能够由用户改变其值的量,一个赋值表达式的结果实际上是一个左值)
(4)在赋值符之前加上其它(双目)运算符,可以构成复合的运算符,如+=、-=、*=、/=、%=、>>=、<<=、&=、|=、^=。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int x = 10;
x = x + 10;
x += 10; //与上一条语句等效
//其它运算符一样的道理,这样写更加简洁
return 0;
}
5、关系操作符和关系表达式
(1)C语言提供了六种关系运算符(如下图所示),它们都是双目运算符(也就是有两个运算分量),运算结果为逻辑型值true(对应整数1)或false(对应整数0)。
(2)由一个关系运算符连接前后两个数值表达式而构成的式子称为关系表达式,简称关系式,当关系式成立时,其计算结果为逻辑值真(true),否则为逻辑值假(false)。
(3)举例:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
//比较运算符
// ==
int a = 10;
int b = 20;
printf("%d\n", (a == b)); //括号的作用是为了保证先让表达式“a == b”进行运算,下同理
// !=
printf("%d\n", (a != b));
// >
printf("%d\n", (a > b));
// <
printf("%d\n", (a < b));
// >=
printf("%d\n", (a >= b));
// <=
printf("%d\n", (a <= b));
return 0;
}
6、逻辑操作符和逻辑表达式
(1)C语言提供了三种逻辑运算符,其中“!”是单目运算符(也就是只有一个运算分量),“&&”和“||”是双目运算符。
(2)逻辑运算的运算分量是逻辑型数据(逻辑常量、逻辑变量、关系表达式等都是逻辑型数据),由逻辑型数据和逻辑运算符连接而成的式子称为逻辑表达式,简称逻辑式。一个数值表达式也可以作为逻辑型数据使用,当值为0时则默认是逻辑值false,当值为非0时则默认为是逻辑值true。
(3)举例:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
//逻辑运算符
int a = 10;
int b = 20;
printf("%d\n", (a && b)); //1
printf("%d\n", (a || b)); //1
printf("%d\n", (!a)); //0
a = 0;
b = 20;
printf("%d\n", (a && b)); //0
printf("%d\n", (a || b)); //1
printf("%d\n", (!a)); //1
a = 0;
b = 0;
printf("%d\n", (a && b)); //0
printf("%d\n", (a || b)); //0
printf("%d\n", (!a)); //1
return 0;
}
(4)逻辑非的运算优先级最高,其次是逻辑与,最后是逻辑或。在同优先级的情况下,逻辑运算是从左向右进行的,如果在运算过程中逻辑表达式已经产生一个确定的结果,那么逻辑表达式的后面部分将不会执行,直接跳过。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
//从左向右运算,a为假,那结果就是假,后面的内容不再运行(但是a的后置递增会运行),直接到下一行;如果是或运算,第一个为真,结果就为真,后面的内容不再运行
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
return 0;
}
7、逗号操作符和逗号表达式
(1)逗号表达式就是用逗号隔开的多个表达式。
(2)逗号表达式从左向右依次执行,整个表达式的结果是最后一个表达式的结果。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 1;
int b = 2;
int c, d = 0;
if (a = b + 1, c = a / 2, d > 0)
;
/*上面的if语句等效为:
a = b + 1;
c = a / 2;
if (d > 0)
;
*/
return 0;
}
8、强制类型转换运算符
(1)可以利用强制类型转换运算将一个表达式转换成所需类型,其一般形式为:
(<类型名>)(<表达式>) //注意表达式应该用括号括起来,因为强制类型转换运算符的优先级很高
(2)在强制类型转换时,会得到一个所需类型的中间数据,而原来变量的类型不会发生变化。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 1;
float b = (float)a;
printf("%d\n", a);
printf("%f\n", b);
return 0;
}
二、隐式类型转换
1、整型提升概述
(1)C的整型算术运算总是至少以缺省整型类型的精度来进行的,为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
(2)整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度,因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算的(虽然机器指令中可能有这种字节相加指令),所以表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
2、不同数据的整型提升
(1)负数产生整型提升时,高位补充符号位1。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
char c1 = -1;
//变量c1的二进制位(补码)中只有8个比特位:11111111(有符号类型的char型,值为负时符号位为1)
//c1整型提升的结果:11111111 11111111 11111111 11111111(高位补充符号位)
printf("%d\n", c1); //-1
return 0;
}
(2)正数产生整型提升时,高位补充符号位0。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
char c2 = 1;
//变量c2的二进制位(补码)中只有8个比特位:00000001(有符号类型的char型,值为正时符号位为0)
//c2整型提升的结果:00000000 00000000 00000000 00000001(高位补充符号位)
printf("%d\n", c2); //1
return 0;
}
(3)上面两点都是针对有符号类型的变量的,当无符号类型变量发生整型提升时,高位全部补0。在上一章的整型数据的输出中有提及整型提升,和这里介绍的整型提升是一样的,只要赋给变量的数据不超出其数据类型允许的存储范围,整型提升基本不会影响程序的运行结果。
#include<stdio.h>
int main()
{
//32768的二进制补码:00000000 00000000 10000000 00000000
//-32768的二进制补码:10000000 00000000
int a = 32768; //a中的存储情况:00000000 00000000 10000000 00000000
short int b = 32768; //b中的存储情况:10000000 00000000(超出短整型的存储范围,取其补码的前16位存入变量b中,最高位被误当成符号位)
unsigned short int c = -32768; //c中的存储情况:10000000 00000000(无符号整型的二进制代码最高位不是符号位,也认不出10000000 00000000是一个负数的补码)
signed int d = -32768; //d中的存储情况:11111111 11111111 10000000 00000000(补码位数过少,用符号位的数字往后扩展至32位为止)
//以有符号的十进制整数形式输出变量
printf("%d\n", a); //输出正常
printf("%d\n", b); //由于变量b不足32位,发生整型提升,高16位全部补1,接着按补码形式转换为十进制数就是-32768
printf("%d\n", c); //由于变量c不足32位,发生整型提升,高16位全部补0,接着按补码形式转换为十进制数就是32768
printf("%d\n", d); //补码位数扩展不会影响补码本身对应的十进制数,所以输出正常
//以无符号的十进制整数形式输出变量
printf("%u\n", a); //输出正常
printf("%u\n", b); //由于变量b不足32位,发生整型提升,高16位全部补1,接着按普通二进制数的形式转换为十进制数就是4294934528
printf("%u\n", c); //由于变量c不足32位,发生整型提升,高16位全部补0,接着按普通二进制数的形式转换为十进制数就是32768
printf("%u\n", d); //变量d的32位二进制代码按普通二进制数的形式转换为十进制数就是4294934528
return 0;
}
3、举例
(1)例1:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
char a = 5;
char b = 126;
char c = a + b; //先把a和b提升为整型,再运算
//a:00000000 00000000 00000000 00000101
//b:00000000 00000000 00000000 01111110
//32位运算时a+b=00000000 00000000 00000000 10000011
//加法运算完成之后,结果将被截断(整型占32bit,字符型占8bit),然后再存储于c中,此时c的存储情况为10000011
printf("%d\n", c); //-125
return 0;
}
(2)例2:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
char a = 0xb6;
short b = 0xb600;
int c = 0xb6000000;
//整型常量前加“0x”,代表这是个十六进制数据
//a,b要进行整型提升,但是c不需要整型提升
if (a == 0xb6) //10110110提升为11111111 11111111 11111111 10110110
printf("a");
if (b == 0xb600) //10110110 00000000提升为11111111 11111111 10110110 00000000
printf("b");
if (c == 0xb6000000)
printf("c");
//a,b整型提升之后,变成了负数,所以表达式 a==0xb6 , b==0xb600 的结果是假,但是c不发生整型提升,则表达式 c == 0xb6000000 的结果是真
printf("%d\n", a); //-74
printf("%d\n", b); //-18944
printf("%d\n", c); //-1241513984
return 0;
}
(3)例3:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
//c只要参与表达式运算,就会发生整型提升
//表达式 +c 就会发生整型提升,所以 sizeof(+c) 的返回值是4个字节
//表达式 -c 也会发生整型提升,所以 sizeof(-c) 的返回值是4个字节
//c没有任何算术运算,所以 sizeof(c) 的返回值就是1个字节
return 0;
}
三、运算符的优先级和结合性
1、概述
(1)复杂表达式的求值有三个影响的因素:操作符的优先级、操作符的结合性、是否控制求值顺序。
(2)两个相邻的操作符先执行哪个,取决于它们的优先级,如果两者的优先级相同,取决于它们的结合性。
2、C语言中所有运算符的优先级和结合性
优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 | 说明 |
1 | [] | 数组下标 | 数组名[常量表达式] | 左到右 | ----- |
() | 圆括号 | (表达式)/函数名(形参表) | ----- | ||
. | 成员选择(对象) | 对象.成员名 | ----- | ||
-> | 成员选择(指针) | 对象指针->成员名 | ----- | ||
2 | - | 负号运算符 | -表达式 | 右到左 | 单目运算符 |
(类型) | 强制类型转换 | (数据类型)表达式 | ----- | ||
++ | 前置自增运算符 | ++变量名 | 单目运算符 | ||
++ | 后置自增运算符 | 变量名++ | 单目运算符 | ||
-- | 前置自减运算符 | --变量名 | 单目运算符 | ||
-- | 后置自减运算符 | 变量名-- | 单目运算符 | ||
* | 取值运算符 | *指针变量 | 单目运算符 | ||
& | 取地址运算符 | &变量名 | 单目运算符 | ||
! | 逻辑非运算符 | !表达式 | 单目运算符 | ||
~ | 按位取反运算符 | ~表达式 | 单目运算符 | ||
sizeof | 长度运算符 | sizeof(表达式) | ----- | ||
3 | / | 除 | 表达式/表达式 | 左到右 | 双目运算符 |
* | 乘 | 表达式*表达式 | 双目运算符 | ||
% | 余数(取模) | 整型表达式/整型表达式 | 双目运算符 | ||
4 | + | 加 | 表达式+表达式 | 左到右 | 双目运算符 |
- | 减 | 表达式-表达式 | 双目运算符 | ||
5 | << | 左移 | 变量<<表达式 | 左到右 | 双目运算符 |
>> | 右移 | 变量>>表达式 | 双目运算符 | ||
6 | > | 大于 | 表达式>表达式 | 左到右 | 双目运算符 |
>= | 大于等于 | 表达式>=表达式 | 双目运算符 | ||
< | 小于 | 表达式<表达式 | 双目运算符 | ||
<= | 小于等于 | 表达式<=表达式 | 双目运算符 | ||
7 | == | 等于 | 表达式==表达式 | 左到右 | 双目运算符 |
!= | 不等于 | 表达式!=表达式 | 双目运算符 | ||
8 | & | 按位与 | 表达式&表达式 | 左到右 | 双目运算符 |
9 | ^ | 按位异或 | 表达式^表达式 | 左到右 | 双目运算符 |
10 | | | 按位或 | 表达式|表达式 | 左到右 | 双目运算符 |
11 | && | 逻辑与 | 表达式&&表达式 | 左到右 | 双目运算符 |
12 | || | 逻辑或 | 表达式||表达式 | 左到右 | 双目运算符 |
13 | ?: | 条件运算符 | 表达式1? 表达式2: 表达式3 | 右到左 | 三目运算符 |
14 | = | 赋值运算符 | 变量=表达式 | 右到左 | ----- |
/= | 除后赋值 | 变量/=表达式 | ----- | ||
*= | 乘后赋值 | 变量*=表达式 | ----- | ||
%= | 取模后赋值 | 变量%=表达式 | ----- | ||
+= | 加后赋值 | 变量+=表达式 | ----- | ||
-= | 减后赋值 | 变量-=表达式 | ----- | ||
<<= | 左移后赋值 | 变量<<=表达式 | ----- | ||
>>= | 右移后赋值 | 变量>>=表达式 | ----- | ||
&= | 按位与后赋值 | 变量&=表达式 | ----- | ||
^= | 按位异或后赋值 | 变量^=表达式 | ----- | ||
|= | 按位或后赋值 | 变量|=表达式 | ----- | ||
15 | , | 逗号运算符 | 表达式,表达式,… | 左到右 | 从左向右顺序运算 |
3、需要注意的问题
(1)在不确认或难以分辨运算符的运算顺序时,可以使用括号进行区分,因为括号运算符的优先级非常高。
(2)一些问题表达式:
①如下所示,在计算的时候,由于*比+的优先级高,只能保证*的计算是比+早,但是优先级并不能决定第三个*比第一个+早执行。
a*b + c*d + e*f
②如下所示,虽然在大多数的编译器上求得结果都是相同的,但是从下述代码“answer = fun() - fun() * fun();”中只能通过操作符的优先级得知先算乘法、再算减法,函数的调用先后顺序无法通过操作符的优先级确定。
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf( "%d\n", answer);
return 0;
}