C语言-数组、函数及位运算
一、数组
什么是数组:
数组就是变量的组合,是一种批量定义变量的方式
如何定义数组:
类型名 数组名[数量];
int arr[8];// 相当于定义了8个int类型的变量
int a1, a2, a3,...;
访问数组中的变量:
数组名[下标];
下标从0开始,范围0~数量-1
遍历数组:
与for循环配合,使用循环变量作为数组遍历的下标
int arr[10];
for (int i = 0; i < 10; ++i) {
printf("%d%c",arr[i], " \n"[i == 9]);
}
数组的初始化:
数组的元素的默认值也是随机的,如果想要对数组初始化可以使用以下语法:
类型名 数组名[数量] = {v1, v2, v3, ...};
- 如果初始化数据过多,编译器会产生警告并丢弃
- 如果初始化数据不足,编译器会自动补0
- 对数组初始化时,数组的初始值可以省略,大括号不能省略,编译器会自动全部补0
- 当使用初始化语法时,数组的数量可以省略,编译器会计算出大括号中数据的个数,然后确定数组的数量
- 初始化语法只能在定义数组时使用,这也是唯一一次能对数组批量访问的机会,后序只能单个元素访问
数组与sizeof
int arr[10] = {};
sizeof(数组名) // 计算整个数组的总字节数
sizeof(数组名[下标]) // 计算某个元素的字节数
sizeof(arr) / sizeof(arr[0]) // 计算数组的元素个数
数组越界:
使用非法的下标访问该下标的数组元素,该动作称为数组越界。C语言中当数组越界访问时,编译器不会报错,因为C编译器是不检查数组下标是否合法的,因为这样可以提高代码的编译速度、运行速度
数据越界的后果:
- 运气好,一切正常
- 段错误,操作系统发现程序非法访问内存,主动杀死程序
- 脏数据,数据越界访问到了其它程序、数组、变量的数据,破坏了别人的数据
二、二维数组
什么是二维数组:
普通一维数组,可以看做若干个变量排成一排
二维数组,可以把若干个类型相同的变量排成一个方阵
定义二维数组:
类型名 数组名[行数][列数];
int arr[3][5];
访问二维数组元素:
数组名[行下标][列下标];
行下标:0 ~ 行数-1
列下标:0 ~ 列数-1
int arr[3][5];
[0][0] [0][1] [0][2] [0][3] [0][4]
[1][0] [1][1] [1][2] [1][3] [1][4]
[2][0] [2][1] [2][2] [2][3] [2][4]
二维数组的遍历:
一般与双层for循环配合,一般外层循环遍历行,内层循环遍历列
int arr[3][5];
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 5; ++j) {
printf("%d%c", arr[i][j], " \n"[j == 4]);
}
}
二维数组的初始化:
二维数组在初始化时,其它特点与一维数组基本相同
注意:二维数组初始化时,可以省略行数,但是一定不能省略列数,并且要提供初始化数据才可以省略行数,编译器会自动计算元素个数
类型名 数组名[行数][列数] = {{第一行}, {第二行}, ...};
int arr[][5] = {1,2,3,4}; true
int arr[1][] = {1,2,3,4}; false
三、变长数组
定长数组:
使用常量作为定义数组的长度参数,或者使用初始化数据定义数组时省略长度参数,这些数组都称为定长数组,由编译器最终确定数组的长度
变长数组:
使用变量作为定义数组的长度参数时,称为变长数组,当程序运行时,执行定义数组的语句前,可以改变数组的长度变量,所以数组的长度可以每一次运行都不同,但是一旦数组的长度确定后,都无法改变
变长数组的优缺点:
优点:可以根据运行的实际需求定义数组的长度,从而节约内存空间
缺点:变长数组不能初始化,对数组的初始化是发生在编译器编译期间,而变长数组的长度是运行到定义语句后才能确定,因此编译器在编译期间无法得知变长数组的长度,因此无法初始化
四、函数
什么是函数(function):
函数就是一段具有某一项功能的代码集合,它是C语言中管理代码的最小单位,把具有某项功能的若干行代码封装在函数中方便管理代码且方便重复调用。
函数的分类:
- 标准库函数:C语言标准委员会为C语言以函数形式提供了一些基础功能,这些函数被封装在
libc.so
库文件中,使用时需要导入对应的头文件,它们的详细介绍在man手册的第3章节。 - 系统函数:操作系统为程序员提供了一些系统
API
,可以以函数形式调用,但它们不是真正的函数,它们的详细介绍在man手册的第2章节。 - 第三方库函数:一些公司或开源组织实现的一些常用工具供程序员使用。MD5、JSON 序列化反序列化、XML配置文件。
- 自定义函数:为了更方便的管理、调用代码,降低开发难度,程序员自己封装的一些函数。
常用标准库函数介绍:
标准库中除上封装了函数,还提供一些头文件,里面是对函数的说明。
stdio.h 输入输出相关功能的函数:
int printf(const char *format, ...);
功能:输出数据到终端
format:提示信息+占位符+转义字符组成
...:若干个变量名或数据
返回值:成功输出的字符个数
int scanf(const char *format, ...);
功能:从终端读取数据
format:一般情况下,只需要占位符即可,除了占位符以外的信息,在输入数据时要原样补出。
...:若干个变量的地址
返回值:成功读取的变量个数
int getchar(void);
int putchar(int c);
int puts(const char *s);
stdlib.h 实用的库函数:
int system(const char *command);
功能:调用系统命令,命令执行完成后,该函数才返回
返回值:成功返回0,失败返回-1。
system("clear"); // 清屏命令
int rand(void);
功能:从系统中获取随机数 0~RAND_MAX
返回值:都是正整数,如果需要负数或浮点数,需要程序员自已处理。
void srand(unsigned int seed);
功能:所谓的随机数就是把所有整数打乱顺序,从某个位置获取,默认从1位置获取,程序运行时如果"位置"不改变,获取随机数与上次一样,为了保证每次运行时,提供的位置都发生变化,一般把time函数的返回值提供给srand作为随机数的种子。
srand(time(NULL));
int abs(int j);
功能:计算并返回j的绝对值
ctype.h 字符类型的判断函数:
int isdigit(int ch );
功能:判断是否是数字字符
int islower(int ch );
功能:判断是否是小写字母
int isupper(int ch );
功能:判断是否是大写字母
time.h 时间日期相关的函数:
time_t time(time_t *tloc);
功能:获取当前系统的时间,返回自 1970年1月1日 00:00:00 到现在一共过了多少秒,格林时间+8小时就是北京时间。
time(NULL)
struct tm *localtime(const time_t *timep);
功能:把秒数据时间转换成年月日、时分秒
math.h 数学相关的函数 :
double pow(double x, double y);
功能:计算出x的y次数
double sqrt(double x);
功能:计算x的平方根
double ceil(double x);
功能:向上取整,返回大于x的最小整数
double floor(double x);
功能:向下取整,返回小于x的最大整数
注意:使用这些函数在编译时必须有-lm参数 libm.so
自定义函数:
有两情况适合把代码封装成自定义函数:
- 代码量过多,一般代码量超过50行就要考虑封装成函数,方便管理代码,提高代码的安全性(程序员平均每50行会出现一个BUG)。
- 如果一个代码需要在不同位置多次执行,为了防止出现代码冗余,就要把它封装成函数,方便重复使用,也能降低可执行文件的大小。
函数声明:
返回值类型 函数名(参数列表);
- 根据函数的功能为函数取名字,Linux系统下函数名全部小写,多个单词用下划线分隔。
- 参数列表,指的是函数执行时,调用者需要传递它的数据,此时重点关注的是参数的类型,在函数声明时可以忽略参数的名,如果函数执行时不需要调用者传递数据则写void。
返回值类型 函数名(类型名 形参名1,类型名 形参名2,...);
返回值类型 函数名(int n1,int n2,...);
返回值类型 函数名(void);
- 返回值类型,指的是函数的执行结果是什么类型的数据,如果函数没有返回值,则写
void
。 - 函数声明就是告诉编译器该函数的格式,方便编译器检查调用者的使用是否正确。
- 一般函数声明放在
main
函数之前
函数定义:
返回值类型 函数名(类型 参数名) {
// 函数体
} // 如果函数的定义出现在调用之前,函数声明可以省略
函数调用:
函数名(实参);
- 调用者会把实参赋值给形参变量。
- 函数的返回值会放置在调用位置,可立即使用,也可用变量保存。
自定义函数要注意的问题:
- 函数的命名空间互相独立,函数之间传参是单向值传递(实参给形参赋值),所以两个函数之间不能通过传参共享变量。
- C语言中如果函数的参数列表是空的,则意味着该函数提供任意类型、多个参数都可以调用,容易给调用者造成误会,影响代码的可读性,如果函数执行时不需要调用者传递数据则参数列表要写
void
,不要空着。 - 如果函数有返回值但没有写
return
语句,调用该函数时依然有返回值。当调用一个有返回值的函数时,系统会为调用者和被调用者约定一个空间用于存储返回值,而return
语句的作用就是把一个数据存储到这个空间,如果没有写return
语句,调用者依然会从共用空间读取返回值,只是读取到的数据是随机的。gcc -Wall -Werror xxx.c
可以防止漏写return
语句。
xxx.c:x:x: error: control reaches end of non-void function [-Werror=return-type]
- 当使用数组作为函数的参数传递时,它的长度信息就丢失了,(数组会蜕变成指针),无法使用
sizeof
计算数组的长度,需要调用者额外提供一个参数作为数组的长度。 - 当函数中使用数组作为参数传递是,是"址传递" ,是可以被函数所共享数组
- 当使用二维数组作为函数的参数时,C语言规则定义二维数组时必须有列数,所以要行、列数在前,数组在后,并且把列数设置给数组。
五、进制转换:
现在的CPU只能识别高低两种电流,只能对二进制数据进行计算。
二进制数据虽然可以直接CPU计算识别,但不方便书写、记录,把二进制数据转换成八进制是为了方便记录在文档中。
随着CPU的不断发展位数不断增加,由早期的8位逐渐发展成现在的64位,因此八进制就不能满足需求了,所以发展出了十六进制,但由于历史原因八进制还不能退出历史舞台。为了理解编程时的一些奇怪现象,我们需要掌握二进制数据、八进制数据、十六进制数据。
二进制数据:
由0~1两个数字组成,逢2进1,由于计算机的运算器只能识别高低两种电流,所以它在运算时只能识别二进制数据。
十进制转换成二进制:假如把x转换成二进制,x/2记录下余数,然后对商继续除以2,重复这个过程,直到商为0结束,然后把记录的余数倒序汇总,就得到了x的二进制。
八进制数据:
由0~7八个数字组成,逢8进1,早期使用它记录二进制数据,现在基本不再使用,文件的权限还依然使用8进制数据表示,所以还无法退出历史。0644
二进制数据转换八进制:从二进制的低位开始划分,每三位二进制对应一位八进制。
注意:在C代码中,以0开头的是八进制数据,以%o
输出的也八进制数据。
十六进制数据:
由0~9
和a~f
十六个字符组成,随着计算机的发展CPU的位数越来越多,输出的二进制也越来越长,随后科学家又发明出十六进制用于记用二进制数据。
二进制转换成十六进制:从二进制的低位开始划分,每四位二进制对应一位十六进制,超过9的用字母ABCDEF表示(不区分大小写)。
注意:在C代码中,以0x开头的是十六进制数据,以%x
,%p
输出的是十六进制数据。%#x
、%#o
以对应的进制显示数据
六、原码、反码、补码:
原码:
正数二进制就是它的原码。
负数符号位为1,数值部分取绝对值的二进制就是它原码。
反码:
正数的原码就是它的反码
负数的反码是他的原码除符号位外,按位取反。
补码:
正数的原码就是补码
负数的反码+1是补码。
注意:十进制的数据是以补码形式存储在计算机中的,因为计算机的CPU中只有加法器,也就是只能运算加法,其它运算都是使用加法模拟的。
注意:同一个数据,以不同类型显示时,可能结果不一样,所以要统一类型去讨论
%hhd 0x81 -127
%d 0x81 129
补码的两种解析方式:
无符号解析:
由于无符号数据全部是正数,所以补码就是原码,直接转换成十进制即可。
有符号解析:
根据补码的最高位判断它是正数的补码还是负数的补码。
最高位是1:它必然是负数的补码。
- 补码-1得到反码
- 反码符号位不变,按位求反得到原码
- 原码转换成十进制数据。
最高位是0:它必然是正数的补码,直接转换成十进制即可。
注意1:当遇到补码转换成十进制数据时,必须有是否是有符号信息。
注意2:一个整型变量的取值范围是环型状的,当它是最大值时再加1就会变成最小值,当它是最小值时减1就是变成最大值。
七、位运算符:
位运算符是针对数据的补码进行运算。
A & B 按位与运算,它是针对数据的补码进行按位与运算
0 & 0 结果是0
0 & 1 结果是0
1 & 0 结果是0
1 & 1 结果是1
1001 1101 0x9d
0011 1110 0x3e
0001 1100 0x1c
A | B 按位与运算
0 | 0 结果是0
0 | 1 结果是1
1 | 0 结果是1
1 | 1 结果是1
A ^ B 按位异或运算 相同为假、相异为真
0 ^ 0 结果是0
0 ^ 1 结果是1
1 ^ 0 结果是1
1 ^ 1 结果是0
~A 按位求反,是单目运算符
~0 结果是1
~1 结果是0
x << n 把x的补码前n位丢掉,末尾补上n个0,按位左移。相当于乘2
10101100 << 3 01100000
01100000
0000 0001 << 1
0000 1000
x >> n 把x的补码后n位丢掉,前面n位,如果x是正数则补0,如果是负数则补1。
char num = -3;
printf("%hhd\n",num >> 2 + 1);
1000 0011
1111 1100
1111 1101
1111 1111
num & -1 == num
num | -1 == -1
num | 0 == num
num ^ 0 == num
num ^ -1 == ~num == -(num + 1)
num 如果是正数 -num = ~num + 1
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)