嵌入式C语言基础学习笔记(李慧芹老师)
一个非常非常注重细节的嵌入式c语言教学视频,本人做了详细的笔记,水平有限,如有错误请在评论区指出,感谢~
b站视频链接:【史上最强最细腻的linux嵌入式C语言学习教程【李慧芹老师】】https://www.bilibili.com/video/BV18p4y167Md?p=69&vd_source=a36d0c31e1b9411aed0d55d6d2e02437
目录
嵌入式C语言
C语言学习笔记
一、gcc和vim的使用
1、c源文件编译顺序
c源文件->预处理->编译->汇编->链接->可执行
(1)直接用gcc hello.c
gcc自动进行编译汇编链接操作,在输入./a.out即可运行文件
(2)用make hello(即解释hello.c 文件,如果输入make hello.c,则就解释的是hello.c.c文件)
再输入./hello即可运行文件
2、编程注意的问题
(1)头文件要正确包含
malloc函数是包含在#include <stdlib.h>中(输入man malloc可查看malloc()函数源码):
所以使用任何一个函数时,要注意其在哪个函数库里,否则c语言会识别不出malloc函数,将其当成一个整形来看待,则会发生数据类型错误
(2)以函数为单位进行程序编写
(3)声明部分+实现部分
(4)return 0 是给其父进程看的,main函数的父进程就是shell
例如printf()也是有返回值的
Hello world10个字符加回车共11个字符,作为main函数的返回值返回给shell接收
(5)多用空格空行以及注释
(6)算法:解决问题的方法(流程图、NS图、有限状态机FSM)
二、数据类型、运算符、表达式
1、数据类型
2、常量与变量
(1)常量
*define的使用
在编译处理时并不进行检错,直接用后面数字替换define。
例如#define ADD 2+3
#define ADD(2+3)这样才可以计算正确
define更深层次用法
#define处理在程序的预处理阶段,占编译时间,不占运行时间,一改全改。缺点:不检查语法,只是单纯的宏体与宏名之间的替换!!!
(2)变量
如果没有指定存储类型,默认为auto:自动分配空间,自动回收空间(不进行初始化的话默认为随机值,但现在GCC一般会优化为0)
register:(建议型)寄存器类型。(如果某个变量需要反复使用,建议将该变量放在寄存器中,但GCC不一定会听取建议)只能定义局部变量,大小有限制, 只能定义同机器字长大小的数据类型,如32位机不能定义double类型(64位),寄存器无地址,所以寄存器类型的变量无法打印出地址查看或使用。
static:静态型。自动初始化为0或者空值,并且其变量的值有继承性,常用于修饰变量或函数。static修饰变量时表示该变量只能在该文件中使用,其他文件无法访问,防止变量在多个文件中重复定义发生冲突。修饰函数时表示该函数只能在当前文件中使用。(防止当前函数对外扩展)
extern:说明型。意味着不能改变被说明的变量的值或类型。(使用extern修饰的关键字不管是类型还是值,都无法被修改)
使用方式:假设A.c有一个全局int型变量a, 在B.c中使用extern int a可以引用A.c的a变量。注意extern修饰时不能进行修改。
在main.c中定义全局变量i,后在proj.c中用extern声明i,表示i变量取值从其他函数中寻找,即为main.c中的i=10。
注意:用extern声明后的变量,不需要再进行赋值,并且与相同变量要保持类型一致
3、运算符和表达式
%取余运算必须是整型
5.0 % 2 运算错误!
前自增(++i)和后自增(i++)的区别:
逻辑运算符
&& ||的短路的特性:若为逻辑或,则m为0会继续判断n,m为1则不会继续判断n
位运算符(重要!)
按位与(&)、按位或(|)、异或(^)(相同为0,不同为1) B表示二进制
原则:若为0,则0可通过或运算使操作数保持不变,若为1,则1可通过与运算使操作数保持不变
三、输入、输出操作
1、printf 用法
(1) (float浮点数,精度为6位,需要进行补齐或删除)
(2)printf中修饰字符讲解
修饰符m
如果输出%6d,则i= 123;
如果输出%2d,则小于i赋值长度,原样输出i= 123;
修饰符.n
输出则为hello,但并不代表str变为hello,str仍为helloworld
修饰符-
在平时的编程中,应当注意整型数太大时而导致的溢出问题,这时候我们应该有意识的在数字后面加上LL,如下面的一个面试题,问如何表示一年有多少秒,可以使用一个宏定义,如下:
#define SEC_YEAR (60LL60LL24LL*365LL)
(3)关于\n的一个知识点
\n除了用来表示输出换行以外,还有刷新缓冲区的作用
a、本应输出死循环之前的printf()内容,但由于没有使用\n来刷新缓冲区,所以导致没有任何输出。
没有加\n,before这条语句就放到了输出缓冲区当中,此时只有三种情况会刷新缓冲区:
1、等程序结束,自动刷新缓冲区
2、遇到强制刷新的函数,如flash()
3、等缓冲区满了,一次性刷新
而上面的printf什么情况也不满足,所以before在缓冲区中刷新不出来
b、在第一个printf函数中使用了\n,所以在死循环前先将要输出的内容打印到了屏幕上。
c、中间不是while(1)而是sleep的情况
2、scanf用法
在使用scanf函数进行输入时,其输入的内容格式要与scanf中的格式保持完全一致,否则输入会出错,假设:
scanf(“%d,%f”, &i, &f);
那么在输入的时候,输完了第一个数i之后,要再输入一个”,”再才能输入第二个数,所以建议不要再格式化的输入之间添加任何其他字符,比如可以像这样:
scanf(“%d%f”, &i, &f);
这样的话在输入完第一个数之后,可以使用 空格符,tab键,或者回车来间隔i和f。
注意,在对字符串进行输入时,不需要加“&“符号,因为字符串名称就是该字符串的首地址。
同时,使用%s获得输入时,不能有空格,即若输入了 “hello world”到字符串str中,则str只保留了hello,而没有后面的空格和world。(%s比较危险,容易内存越界,不建议使用)
scanf()函数放在循环中很危险,如果确有需要,记得做判断(scanf函数有返回值,即scanf中有几个值得到了正确的输入,就返回几,可以依据此来做if判断,在适当的时机退出循环。)
抑制符*的使用
在连续使用scanf时,输入i的值后,需要点击回车/空格/tab键才可以继续输入c的值,若中间不使用抑制符,则回车/空格/tab键将会被判定为c的值,此时需要使用“*”去吃掉回车/空格/tab操作,才能继续输入c的值。
或者使用getchar()去吃掉回车/空格/tab操作。
用scanf("%s")没有办法去输入带有空格的一串字符,原因与上放类似。
3、字符串输入输出
可以使用getline去解决越界问题,getline实现动态存储
4、练习题
(1)一个水分子的质量大约为3.0e-23g,一夸脱水大约950克,编写一程序,要求从终端输入水的夸脱数,然后显示这么多夸脱水中包含有大概多少个水分子。
如果将num定义为int型,那么需要使用显示类型转换:int sum = (int) num*kq/waterpiece;
浮点型与整形做除法,会自动隐式转换为浮点型
(2)从终端输入三角形三边长,求面积
s = 1/2 * (a+b+c);
area = sqrt(s(s-a)(s-b)*(s-c));
(注:当源文件中包含math.h时,在使用gcc进行编译时,要在编译命令后面加上-lm参数)
因为s=1/2*(a+b+c)出错!1/2为整形与(a+b+c)相乘确实直接隐式转换为float型。但是1/2是整形除法,结果为0!
可以这样修改,注意细节!
四、流程控制
1、if...else...
注意:else只与离他最近的if想匹配,除非用{}隔开
2、switch-case
case后面只能使用常量表达式,即执行过程中不会发生变化的表达式,不能用>/</=这种关系型表达式
尤其要记住每个case后面很有可能都要有一个break跳出switch语句,如果不加就会顺着下一个case继续顺序执行语句,直到遇到break或者switch语句末尾才会跳出switch语句(当然也有特殊情况,牢记这个关键字,根据自己的需求判断是否需要使用break关键字),最后记得要有一个default控制,表示默认情况,即所有的case都不执行的时候,就去执行default修饰的语句。
同时跟上面的if的注意事项差不多,不要相信自己的判断,假设以为只有两种情况,也要在default中做出错相关的处理,如下:
3、循环
(1)while和do...while
(2)for循环
(3)if...goto
(慎用:goto实现的是无条件的跳转,且不能跨函数调用,不能跨函数调用的原因是goto不会保存现场,将相关变量等压栈,而一般的函数调用时,会将现场保存在栈中,待函数调用完之后,回到原处,再出栈恢复现场。所以goto不能跨函数调用。)
(4)典型的死循环
while(1);
for(; ;);
杀掉死循环:Ctrl+C(终止当前进程)
(5)辅助控制
break: 跳出当前循环
continue: 跳出本次循环(即跳过这一次的循环内容,继续执行下一轮循环)
4、练习题
五、数组
1、一维数组
(1)定义
数组越界,系统不会进行检查,因为数组是通过指针偏移来构成的。例如:
定义arr的长度的3,此时我再初始化arr[3]=10,不会报错。对于arr[3]=10操作的解释:将10赋值给从arr数组第一个地址偏移3个单位的地址上。
(2)练习题
2、二维数组
(1)定义(二维数组在定义时,只有行号可以省略,列号不可以省略)
通过这张图,我们可以把a2理解成一个一维数组,然后a中的每个元素又都是一个一个数组,所以a表示的是a[0], a+1表示的是a[1]。如果这时我们定义一个int类型的执政去指向a,即 int *p; p = a; 可以以p的方式来访问a中的第一行(虽然会有类型不匹配的warning)即 p[0]表示的a[0] [0]。如果令 p = a+1;这时p将指向a的第二行,此时p[0]表示a[1] [0],最后,如果想要消除类型不匹配的warning,可以将p=a改写成p=a[0] p = a+1 改写成p = a[1];(具体原因同学们可以自己调试查看提示信息)
(2)练习题
3、字符数组
(1)定义
特殊:区别于一维二维数组,字符数组在存储时需要在末尾多占用一个地址,添加尾零。
例如 str[N]={'a','b','c'} 对应存储--------> 'a','b','c','/0'
输入输出gets()很危险,不要用。因为gets()是将数据直接读取到缓冲区中,并没有直接存储到数组所造物理地址。puts()就相当于从缓冲区中取出数据,并在末尾加"/0"。这就导致gets()并不会去检查越界情况,且对数据类型也不会检测
(2)字符数组中常用的函数
strlen(str1):以尾零“/0"为单位计算数组的长度。
strcpy(str1,str2)/strcpy(str1,"abcde"):将后面的字符串内容复制到前面的字符串中。弥补了不能使用str = "abcde"的特性(str是字符串常量,不是变量)。
strncpy(str1,str2,n):将str2中前n个字节复制到str1中,防止越界问题。(一般n设置为str1的长度)。注意:实际从str2取出的是n-1个字符,还要包含一个尾零。
strcat(str1,str2):将str2追加到str1后面。
strncat(str1,str2,n):将str2最多n个字节追加到str1后面。
strcmp(str1,str2):str1的ASI码-str2的ASI码值,返回ASI码之间的差值
strncmp(str1,str2,n):比较前n个ASI码的差值。
4、多维数组
如上图,分到第三级即为在内存中连续存储的数组下标。
5、练习题
(1)字符数组中的单词计数问题
六、指针
1、指针相关定义
(1)变量与地址
指针等价于地址,变量名就是抽象出来某块空间的别名。
(2)指针与指针变量
用于存放指针的地址的变量叫指针变量
(3)指针运算
& * 关系运算 ++ --
2、直接或间接访问
(1)*和&的含义
*的含义: *p表示的是p对应的地址中存储的值,即为i。
&的含义: &p表示存储p的地址,即为ox3000。
*与&二者类似互逆操作
(2)解释int *p=&i;
注意:c语言定义变量格式如下: TYPE NAME = VALUE;
int *p=&i 中TYPE=int *,NAME=p, VALUE=&i。
int *表示的TYPE含义是int类型的指针。
(3)指针的大小
不管指针是几级的指针或者何种类型定义,在某个平台下,指针定义的大小始终一致。
例如在64位OS下,指针大小始终为8个字节。
(定义指针类型需要和指向地址中所在的值类型相同,如i为int型,那么*p=&i也必须为int型,否则在做指针运算时很容易发生错误)
3、空指针与野指针、空类型
(1)指针一旦确定就必须有指向,否则就变成了野指针,为了避免这种情况,需要把没有明确指向的指针定义为空指针:int *p = NULL;
注意:像下图这样的野指针一定不要用,指针一旦定义出来就要有明确的变量指向。
(2)空类型指针:void *q=NULL;
在不知道需要什么数据类型时,可以使用空类型指针,可以表示任意数据类型
4、指针与一维数组
从上例中,a与p似乎是等价关系,唯一不同点在于a是数组常量,p是指针变量。
(1)区分p++和p+1
p++ 即 p=p+1 --> p指针变动了,指向下一个元素的地址。
p+1 --> p指针没有变动,仍指向这个元素,但不获得它的地址,而是获得它下一个元素的地址。
*a= *(a+0)即为a[0];
在上图中p指针是一直向下移动的,所以在scanf输入后,p指针指向a[2]下一个位置,此时在进行printf输出,则输出的是a[2]后面三个地址的值和对应元素。
printf("%p->%d\n",p, * p++)存在运算级的问题,printf在装载函数时,先存 *p++这个参数,导致p向前移动了一位(同级别运算符,从左往右一次运算,则先运算了p++再取*,导致p又多移动了一个单元,故将p++移到for循环中)
例如:
上图中输出y的值为5,a[0]为6
(2)指针可以直接引用数组
创建了一个匿名临时对象,直接用指针引用数组,数组并没有名字。
5、指针与二维数组
a表示行指针,a+1表示第二行地址指针
(1)数组指针
int (* p)[3]对应type为数组int [3],对应name为*p;
此时p+1不再是移动一个地址单元,而是一个int [3]大小的地址单元
(2)指针数组
归根结底是一个数组
int * arr[3]; -->TYPE NAME; -->int *[3] arr;
指针数组的选择排序
6、指针与字符数组
(1)指针长度
7、const与指针
const:将变量给常量化。
例如:
用const将pi常量化,此时在给pi直接赋值就会发生错误。而通过定义指针变量p指向pi的地址,从而间接给pi赋值,不会出错,但非常危险,不建议使用。
(1)常量指针和指针常量的区别
常量指针:指针的指向可以发生变化,但是指针指向当前地址处的值不可以发生变化。
指针常量:指针的指向永远不能变化,但是目标变量的值可以发生变化。
(谁在前谁就不能变!)
区分:
常量指针:const int *p / int const *p
指针常量:int *const p
const在前就是常量指针,*在前就是指针常量
常量指针
指针常量
去看一些函数比如memcpy
都会将*src给const修饰,通过定义常量指针意味着传过来的参数stc是不可改变的,从而复制给dest变量。在平时定义函数时,也可以借鉴这些定义原则。
8、多级指针
还是这个图
七、函数
1、函数的定义
(1)函数返回
一个进程返回状态是给其父进程看的。main函数的父进程就是shell
main函数第一个参数为函数名。
注意shell有自动解析通配符的功能
char *agrv[]:字符指针数组
2、函数的传参
(1)值传递
(2)地址传递*
值传递,形参会对实参拷贝一份进行操作,所以实参不会交换。传地址可以实现交换,C++引用就是传地址
3、函数的调用
(1)嵌套调用
(2)递归(常考)
递归:一个函数直接或间接的调用自己
递归解决阶乘问题
//用递归解决阶乘问题
#include <stdio.h>
#include <stdlib.h>
int func(int i) {
if (i > 1) {
return func(i - 1) * i;
}
else
return 1;
}
int main() {
int i;
int ret;
printf("请输入您要计算的阶乘数为:");
scanf_s("%d", &i);
ret = func(i);
printf("输出结果为%d\n", ret);
}
递归解决斐波那契数列问题
//递归解决斐波那契数列问题
int fib(int n) {
if (n < 1)
return -1;
if (n == 1)
return 1;
if (n == 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
int main() {
int n;
int res;
scanf_s("%d", &n);
res = fib(n);
printf("fib[%d] = %d\n", n, res);
}
4、函数和数组
(1)函数和一维数组
故要传入数组长度的参数。
void printf_arr(int *p,int n) {
for (int i = 0; i < n; i++) {
printf("%d ", *(p + i));
}
}
int main() {
int a[] = { 1,3,6,7,8 };
/* 定义打印数组的函数
for (int i = 0; i < sizeof(a) / sizeof(*a); i++) {
printf("%d ", a[i]);
}*/
printf_arr(a,sizeof(a)/sizeof(*a));
}
注意,如果参数指针改为数组int p[],在形参的角度来说,它的本质依然是一个指针,一个[]对应一个*
传参类型对应
函数实现逆序功能
//输出
void printf_arr(int *p,int n) {
for (int i = 0; i < n; i++) {
printf("%d ", *(p + i));
}
}
//交换函数
void rechange (int* a, int* b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
//逆序函数
void func(int *p, int n) {
int tmp;
for (int i = 0; i < n / 2; i++) {
/*交换函数
tmp = *(p + i);
*(p + i) = *(p + n - i-1);
*(p + n - i - 1) = tmp;
*/
rechange((p + i), (p + n - i - 1));
}
}
int main() {
int a[] = { 1,3,6,7,8 };
/* 定义打印数组的函数
for (int i = 0; i < sizeof(a) / sizeof(*a); i++) {
printf("%d ", a[i]);
}*/
func(a, sizeof(a) / sizeof(*a));
printf_arr(a, sizeof(a) / sizeof(*a));
}
(2)函数与二维数组
二维数组打印输出
法一:当成一维数组
法二:当成二维数组
注意:sizeof(a)=48,sizeof(p)=8
法三
形参就是一个指针大小。
(3)字符数组
将一个字符数组复制给另一个字符数组,即定义strcpy函数。(面试会问)
将一个字符数组的n个字符复制给另一个字符数组,即定义strncpy函数。
正确输出:
整个代码:
char *mystrcpy(char *dest, const char *sor) {
char *ret = dest;
if (dest != NULL && sor != NULL) {
while ((*dest++ = *sor++) != '\0'); //!!!考虑优先级问题!先进行复制运算,在与尾零进行对比
}
return ret;
}
char *mystrncpy(char *dest, const char *src,int n) {
char *ret = dest;
int min = strlen(src) - n <= 0 ? strlen(src) : n;
while (min > 0) {
*dest++ = *src++;
min--;
}
*dest = '\0';//!!!!注意此时dest指针位于第n个位置,但并不是尾零,所以如果不将dest置尾零,则后续还会输出乱码!
return ret;
}
int main() {
char str1[] = "helloworld";
char str2[128];
char str3[128];
mystrcpy(str2, str1);
printf("str2=");
puts(str2);
mystrncpy(str3, str1, 2);
printf("str3=");
puts(str3);
exit(0);
}
八、构造类型
1、结构体
(1)类型描述
简单定义
嵌套定义
(2)定义变量、初始化
#include <stdio.h>
#include <stdlib.h>
struct birthday_st {
int year;
int month;
int day;
};
struct stu {
int id;
char name[128];
struct birthday_st birth;
int math;
int chinese;
};
int main() {
struct stu stu_1 = { 18,"jack",{2002,05,06},91,88 };
stu_1.birth.day = 2024;
printf("%d\n%d\n", stu_1.birth.day, stu_1.math);
exit(0);
}
部分初始化
用指针来引用成员变量
数组存放结构体
int main() {
struct stu arr[2] = { { 10011,"alan",{2002,11,1},38,29 }, {10012,"snick",{2021,11,1},24,99} };
struct stu *p = &arr[0];
for (int i = 0; i < 2; i++, p++) { //p++ -> p=p+1;这里的“1”为一个结构体单位
printf("%d %s %d-%d-%d %d %d\n", p->id, p->name, p->birth.year, p->birth.month, p->birth.day, p->math, p->chinese);
}
exit(0);
}
(3)占用内存空间大小
结构体默认对齐:sizeof(struct)=12bite
(4)函数传参
使用结构体传参的参数占用空间大小非常大!
故应采用指针传参,指针固定8bite大小
输出为8
(5)练习(微型学生管理系统)
微型学生管理系统:
struct student_st {
int id;
char name[NAMESIZE];
int math;
int chinese;
};
void stu_set(struct student_st *p) //通过指针传参,减少开销!
{
p->id = 10011;
//注意字符常量不能通过赋值定义!不能通过p->name="Alan"错误!
strncpy_s(p->name, "Alan", NAMESIZE);
p->math = 90;
p->chinese = 88;
};
void stu_show(struct student_st* p) {
printf("%d %s %d %d\n", p->id, p->name, p->math, p->chinese);
}
int main() {
struct student_st stu;
stu_set(&stu);
stu_show(&stu);
exit(0);
}
改善:通过取输入学生信息
struct student_st {
int id;
char name[NAMESIZE];
int math;
int chinese;
};
void stu_set(struct student_st *p,struct student_st *q) //通过指针传参,减少开销!
{
/*
p->id = q->id;
//注意字符常量不能通过赋值定义!不能通过p->name="Alan"错误!
strcpy_s(p->name, q->name);
p->math = q->math;
p->chinese = q->chinese;
*/
*p = *q;
};
void stu_show(struct student_st* p) {
printf("%d %s %d %d\n", p->id, p->name, p->math, p->chinese);
}
int main() {
struct student_st stu,tmp;
printf("please enter for the stu[id name math chinese]:");
//复习scanf用法!!!
scanf_s("%d", &tmp.id);
scanf_s("%s", tmp.name,NAMESIZE);
scanf_s("%d%d", &tmp.math, &tmp.chinese);
stu_set(&stu,&tmp);
stu_show(&stu);
exit(0);
}
完整微型学生管理系统菜单:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define NAMESIZE 32
struct student_st {
int id;
char name[NAMESIZE];
int math;
int chinese;
};
void stu_set(struct student_st *p) //通过指针传参,减少开销!
{
/*
p->id = q->id;
//注意字符常量不能通过赋值定义!不能通过p->name="Alan"错误!
strcpy_s(p->name, q->name);
p->math = q->math;
p->chinese = q->chinese;
*/
printf("please enter for the stu[id name math chinese]:");
//复习scanf用法!!!
scanf_s("%d", &p->id);
scanf_s("%s", p->name, NAMESIZE);
scanf_s("%d%d", &p->math, &p->chinese);
};
void stu_show(struct student_st* p) {
printf("%d %s %d %d\n", p->id, p->name, p->math, p->chinese);
}
void stu_changename(struct student_st* p, char *q) {
printf("please enter for newname:");
scanf_s("%s", q, NAMESIZE);
strcpy_s(p->name, q);
}
void menu(void) {
printf("\n1 set\n2 changename\n3 show\n\n");
printf("please enter the num:(q for quit)");
}
int main() {
struct student_st stu;
char newname[NAMESIZE];
int choice;
int ret;//用于退出循环
do {//必定循环一次
menu();
ret = scanf_s("%d", &choice);
if (ret != 1)
break;//输入的choice不为整型就退出while循环
switch (choice)
{
case 1:
stu_set(&stu);
break;
case 2:
stu_changename(&stu, newname);
break;
case 3:
stu_show(&stu);
break;
default:
exit(1);//直接退出while循环
}
} while (1);
exit(0);
}
2、共用体
多个变量共用同一块空间。
(1)类型描述
(2)定义变量
(3)将32位数的高16位与低16位相加
法一:通过位运算
法二:通过共用体的概念
(4)位域
w.x.a输出结果为-1
硬件中的得扣掉符号位再取反加一
补码符号位与原码的符号位相同
3、枚举类型
输出结果为5
常见用法:
将enum枚举当做多个define宏来使用,在预处理时并不会被数字取代,从而让代码更加清晰,方便debug。
若用define宏定义,那么switch语句中就会case 1,case 2,case 3
注意:多个无头枚举里面有相同的成员会报错
九、动态内存管理
原则:谁申请谁释放
1、malloc
(1)定义
即p指针指向申请的int型大小的空间
使用动态内存分配的方式做的动态数组
(2)易错点***
两种修改方法:
(1)通过地址传参(回顾前面所学!)
(2)修改func函数返回值,让p指针接收
2、free
此时p指针就变成了野指针!
所以警惕:在free(p)后就立马将p=null置空!
free类似于去租房子,房子就是那套空间,p就是你在使用。free之后,即你退租了。这时候在进去就不合适了。
free代表着通过p对原来那块空间再也没有引用的权限了。p指针依然存在,那块空间也依然存在,p可能仍指向那块空间,也可能指向别的地方。
3、重构学生管理系统
4、typedefine
更多推荐
所有评论(0)