C语言笔记

C语言没有.c文件的笔记!以后也别做。
待把PPT内容复制到这个博客里,以后只留这个博客

杂七杂八的知识点

牛客网练习输入输出:https://www.nowcoder.com/exam/test/77638941/detail?pid=27976983#question

一个C语言的执行是从本程序的main函数开始,到main函数结束,但需要注意的是,也是有特殊情况的,若是代码中出现了exit函数,则直接结束程序。

.h文件叫头文件,.c文件叫源文件。

在本地用"Visual C++"或网页"lightly"编译和运行 含有分文件(.h, .c)的文件时,总是运行报错,原因大概率是找不到.h文件对应的那个.c文件,所以编译不了该.c文件。

在某些C语言编辑器(如我本科的那个软件)里,变量的定义必须全部写前面,for 循环里的 int i = 0;  也必须写前面。在内核中,也必须把变量的定义都写在函数的前面,for 循环的 { } 里也可以重新定义变量,但这些变量也必须全部写在for循环的前面,否则实测过编译就会失败。

ASCII 字符:只有127个字符,对应整数0~127。
48是 '0' ,57是 '9',65是 'A',90是 'Z',97是 'a',122是 'z'。
大写转小写是加 32。
char范围:-128~127,如果 printf("%c", 负数) 则乱码。

C/Cpp:不可以返回局部变量的地址,但可以返回局部变量的值。

一行写不下:
1、预处理一行写不下:把一个预处理指示写成多行需要加换行符 "\" 续行,因为根据定义一条预处理指令只能由一行逻辑代码构成。
2、正常程序一行写不下:把C代码写成多行则不必使用续行符,因为换行在C代码中只不过是一种空白字符,在做语法解析时所有空白字符都被丢弃了。

C的编译不能发现注释中的拼写错误。    C本身没有输入输出语句,标准库提供之。

字节

字节:一个字节存储 8 位无符号数,储存的数值范围为 0-255 。一个字节为 8 位。一个字节是八个比特,即:1byte = 8bit。

头文件:a.c调用b.c中的函数

a.c中 include <b.h>        (a.h 不用 include <b.h>)
b.h 中要声明被调用的函数,b.c 中那些不被其他.c文件调用的函数可以不用在 b.h 中声明。

.h文件:写声明:变量声明、宏定义、枚举声明、结构体声明、函数声明。
.h文件是对其对应.c文件的接口的声明,接口包括提供给其他模块调用的外部函数和外部全局变量。其他模块访问这些外部定义的变量和函数都要在.h文件中用extern关键字声明,而.c文件内部的函数和全局变量要在.c文件开头用static关键字声明。
.c文件:写变量的定义、函数的实现。

头文件只能放变量/函数(统称为符号)的声明,不能放它们的实现/定义。因为头文件不参加编译,如果一个头文件被多个.c包含,如果这时头文件里有变量/函数的定义,编译器就报错,无论包含该.h的.c里是否用该.h里的变量/函数。(重定义?因为重定义才报错的,不是因为格式才报错的)。
但是下面3种情况可以在头文件在定义符号(以C++为例):
1.头文件里可以写const对象和static对象的定义。因为全局的const对象默认没有extern声明,所以只在当前文件有效,即使头文件被其他.cpp文件包含进去,那这个对象也只在包含它的那个文件中有效,对其他文件来说是不可见的,所以不会被重定义。
2.头文件可写内联函数inline的定义。因为inline函数需要编译器在遇到它的地方把它内联展开的,编译器需要在编译时看到内联函数完整的定义,所以C++规定内联函数可以直接在头文件里定义且建议这样做。
3.头文件写类的定义。类的定义包含数据成员和函数成员,数据成员要等具体对象被创建才会被定义(分配空间),但函数成员要一开始就被定义。一般,把类的定义放在头文件,把函数成员的实现代码放在.cpp,但也可以把函数成员的实现代码写进类的定义,一起放入头文件。

分文件编写时,.h 和 .c 都怎么 include

成功案例1:
 // tool1.h  ,这里被 include 了2次,但不知为什么不报错。
void mytool1(void);
 // tool1.c
#include <stdio.h>
#include "tool1.h"

void mytool1(void) {
 printf("jxw\n");
}
 //main.c
#include <stdio.h>
#include "tool1.h"

int main() {
 mytool1();
 return 0;
}

成功案例2:
 // tool1.h
#include <stdio.h>
void mytool1(void);
 // tool1.c
#include "tool1.h"
void mytool1(void) {
 printf("jxw\n");
}
 //main.c
#include <stdio.h>
#include "tool1.h"

int main() {
 mytool1();
 return 0;
}

#include路径问题:

#include在编译前,即预编译时就起作用,作用是原封不动地把后面那个 "***.h" 替换为 .h 文件中的内容,就是简单的一字不拉的文本替换,它本身是没有其他作用的。这就是出现重定义的根本原因。

eg:
#include <stdio.h>           :从系统库中获取 stdio.h,在系统文件目录下查找。
#include "myheader.h"    :从本地目录中获取 myheader.h。系统优先从当前文件夹寻找该头文件,如果没有,就从默认存放头文件的文件夹里找,最后在系统文件中查找。(20230131:不看我都快忘记了。20230203:才几天我又忘记了!还有其他一大堆地方都是)

 ~ 当我们使用尖括号<>包含一个头文件时,头文件的搜索顺序为:
1.通过GCC参数gcc -I指定的目录(注:大写的i)
2.通过环境变量CINCLUDEPATH指定的目录
3.GCC的内定目录
4.搜索规则:当不同目录下存在同名的头文件时,先搜到那个使用哪个,搜索停止

 ~ 当我们使用双引号""来包含头文件路径时,编译器会首先到项目当前目录去搜索需要的头文件,在当前项目目录下面搜不到,再到其他指定的路径下面去搜索:
0.项目当前目录(找到就停下不找了)
1.通过GCC参数 gcc -I 指定的目录
2.通过环境变量CINCLUDEPATH指定的目录
3.GCC的内定目录
4.搜索规则:当不同目录下存在同名的头文件时,先搜到那个使用哪个(因此,已验证:在当前目录也可以新建一个文件:stdio.h,然后 #include "stdio.h")

~ 在服务器上 ls /usr/include/       大概是系统库,可以查看哪些适合 #include <> 

~ 如果是自己写的**.h文件必须写"**.h",若换成<**.h>则编译报错

头文件路径一般分为绝对路径和相对路径:绝对路径以根目录/或者Windows下的每个盘符为路径起点;相对路径则是以程序文件当前的目录为起点:
#include "/home/wit/code/xx.h"  //Linux下的绝对路径(实测如果 #include "~/jxwDraft0208/stdio.h" 则编译报错)
#include "F:/litao/code/xx.h"   //Windows下的绝对路径
#include "../lcd/lcd.h"         //相对路径,..表示当前目录的上一层目录(实测可以成功)
#include "./lcd.h"             //相对路径,.表示当前目录(实测可以成功)
#include "lcd.h"               //相对路径,当前文件所在的目录(实测可以成功)

eg:
edk2/QcomModulePkg/Include/Library/Board.h:  UINT32 BoardTargetId (VOID);     (定义)
edk2/QcomModulePkg/Library/BootLib/Board.c:  UINT32 BoardTargetId (VOID)      (实现,里面是 #include <Board.h> )

eg:在  boot_images/boot/QcomPkg/***/***TransInfo.c  中:
#include <Uefi.h
//位置在: boot_images/MdePkg/Include/Uefi.h
#include <Library/UefiApplicationEntryPoint.h
//位置在: boot_images/MdePkg/Include/Library/UefiApplicationEntryPoint.h
#include <Library/UefiBootServicesTableLib.h
//位置在: boot_images/MdePkg/Include/Library/UefiBootServicesTableLib.h
#include <Protocol/EFI***.h>    
//位置在: boot_images/QcomPkg/Include/Protocol/EFI***.h
#include <Library/LockLib.h>    
//位置在: boot_images/QcomPkg/Include/Library/LockLib.h

结论:#include < > 的,必然在boot_images目录下,在boot_images下面的哪个目录里都有可能,但必然在Include目录下。jxw估计是在哪个文件里手动设置了"系统目录"。

eg: 经实测,QcomPkg下的.c文件(如QcomPkg/Library/MDPLib/下的.c文件、QcomPkg/SocPkg/Library/***Lib/下的.c文件):
如果要include QcomPkg/Include/Library/下的.h文件,直接 #include "***.h"即可;
但是#include "***.h" 的这个***.h不一定就在QcomPkg/Include/Library/下;
如果要include 与QcomPkg平级的 Pkg 如 MdePkg/Include/Library/下的.h文件,则需要写成 #include <Library/**.h>,注意到:凡是包含Library/**.h 的必然是用< >。

eg: 头文件 QcomPkg/Library/PmicLib/app/pon/src/pm_app_pon.h 被下面.c文件包含:(这里可能是和.inf等配置有关,照着模仿就行)
QcomPkg/Library/PmicLib/app/pon/src/pm_app_key_press.c QcomPkg/Library/PmicLib/target/nicobar_pm6125_pmi632/system/src/pm_sbl_boot_oem.c QcomPkg/Drivers/PmicDxe/PmicTestProtocol.c
QcomPkg/Drivers/PmicDxe/Pmic.c

eg:头文件:
kernel/msm-4.14/include/linux/max9675x.h
kernel/msm-4.14/drivers/gpu/drm/bridge/max9675x.c 里有 #include <linux/max9675x.h> ,该文件中include的文件在 kernel/msm-4.14/include 目录下都有。
kernel/msm-4.14/drivers/i2c/busses/i2c-qcom-geni.c 里也有 #include <linux/max9675x.h> 。

mtk:
\preloader\platform\mt6983\src\drivers\inc\gpio.h
\preloader/platform/mt6983/src/drivers/gpio.c
\preloader\platform\mt6983\src\drivers\platform.c
\preloader\platform\mt6983\src\core\main.c
gpio.c 、platform.c 都在 platform/mt6983/src/drivers/ 裡面:
gpio.c 是 #include <gpio.h>,platform.c 是寫 #include "gpio.h" ,可见这两个都可以。
main.c 在  platform/mt6983/src/core/ , 可以写相对路径: include "../drivers/inc/gpio.h"(实测成功,实测写 include "gpio.h" 也可以)

#include <stdlib.h>

使用 system("pause");  要#include <stdlib.h>

作用域,局部,全局

局部变量和全局变量的名称可以相同,但是在函数内,如果两个名字相同,会使用局部变量值,全局变量不会被使用。eg:
#include <stdio.h> 
int g = 10;
int main() { 
 int g = 20; 
 printf("%d\n", g);    //输出20 
 return 0;
}

全局变量保存在内存的全局存储区中,占用静态的存储单元; 局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。

当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。定义全局变量时,系统会自动对其初始化,不同数据类型的初始化的值为:
int : 0
char : '\0'
float : 0
double : 0
pointer : NULL

static, extern, 静态变量和静态函数

若全局变量仅在单个C文件中访问,则可以将这个变量修改为静态全局变量,以降低模块间的耦合度。
若全局变量仅由单个函数访问,则可以将这个变量改为该函数内的静态局部变量,以降低模块间的耦合度。

static修饰局部变量:

见网站:https://www.jb51.net/article/257290.htm

void func(int * p) {
 static int num = 9; // static 修饰局部变量
 printf("%d\n", *p); // 5
 p = &num;
 printf("%d\n", *p); // 9
 (*p)--;
 printf("%d\n", *p); // 8
}
int main() {
 int i = 5;
 int *p = &i;
 printf("main : %d\n", *p); // 5
 func(p);
 printf("main : %d\n", *p); // 5
 return 0;
}

static修饰全局变量:

static修饰全局变量的时,这个全局变量只能在该全局变量所在的.c文件中访问,不能在include该文件的其他文件中访问。(全局变量的作用域是整个工程)一个全局变量具有外部链接属性的 (就是可以被其他源文件引用)但是被 static 修饰后外部链接属性就变成了内部链接属性 只能在自己所在的源文件内部使用不能在其他文件内部使用。
(待检验:这个性质很重要,因为安卓代码里某.c文件可能被其他文件include,而里面那个全局变量如果被static修饰,则不可能出现在其他文件里,这样找这个变量时就不用在整个安卓代码里grep了)

eg1: 同一工程的同一目录下有:jxw1.c, jxw2.h, jxw2.c
// jxw2.h只有2行代码:
#include <stdio.h>
int a;  //全局变量
//jxw2.c的代码如下:
#include  "jxw2.h"
int a = 666;   //前面的int也不能省略。
// jxw1.c的代码如下:
#include <stdio.h>
#include "jxw2.h"
extern int a;     //此处#include "jxw2.h"和extern int a至少写一个,也可以都写
int main() {
  printf("%d\t%d\n", a, a);
  return 0;
}

eg2: 同一工程的同一目录下有:jxw1.c, jxw2.h, jxw2.c
// jxw2.h只有2行代码:
#include <stdio.h>
static int a;  //全局变量
//jxw2.c的代码如下:
#include  "jxw2.h"
static int a = 666;
// jxw1.c的代码如下:
#include <stdio.h>
#include "jxw2.h"
extern int a;
int main() {
  printf("%d\t%d\n", a, a);
  return 0;
}
// 能正常运行,但加了static后输出0,删了static才输出正确的666

static修饰函数 

static修饰函数:(叫做静态函数)
1.一个函数本身具有外部链接属性 被static修饰后外部链接属性变成了内部链接属性。static修饰一个函数,则这个函数的只能在该函数所在的.c文件中调用,不能被其他文件调用,所以以后遇到static修饰函数,不用去服务器上grep了,因为肯定都在本.c文件里。
2.不同C语言文件可以使用相同名字的静态函数(2023.1.29:这里C语言文件是不是指.c文件?不给别人看要让未来的我看懂啊。应该指.c文件)(very important, grep时有很多同名函数在不同文件里。)

eg: 同一工程的同一目录下有文件:jxw1.c, jxw2.h, jxw2.c
   //jxw2.h的代码如下:
#include <stdio.h>
static int add(int x, int y);
   //jxw2.c的代码如下:(要#include <stdio.h>或"jxw2.h")
#include "jxw2.h"
static int add(int x, int y) {
  return  (x+y);
}
   //jxw1.c的代码如下:
#include <stdio.h>
#include "jxw2.h"     //必须写"",若换成<>则编译报错
//extern int add(int x, int y);
int main() {
  int a = 2;
  int b = 3;
  printf("%d\n", add(a, b));
  return 0;
}
//此时编译报错,删掉上面2个static则编译通过。

extern

修饰符extern用在变量或者函数的声明前,用来说明"此变量/函数是在别处定义的,要在此处引用"。extern后面的声明要写完整。

include了头文件后就不用加extern了;而加extern后就不用include头文件了;至少二选一,也可以都写。 extern可以在编译大型代码时加快编译速度。

eg: kernel/msm-4.14/drivers/gpu/drm/msm/dsi-staging/dsi_panel.c 可以用 extern 来调用 kernel/msm-4.14/drivers/gpu/drm/bridge/lt9211.c 里的变量。

static inline

作用:减小CPU开销。通常,程序执行时,处理器从内存中读取代码执行。当程序中调用一个函数时,程序跳到存储器中保存函数的位置,开始读取代码执行,执行完后再返回。为了提高速度,C语言定义了inline函数,告诉编译器把函数代码在编译时直接拷贝到程序中,这样就不用执行时另外读取函数代码。
static函数告诉编译器其他文件看不到这个函数,因此该函数只能在当前文件中被调用。static inline函数只能在当前文件中被调用,同时执行速度快,几个文件中都可以使用同样的函数名。eg:

void myprint() {
  printf ( "ok" );
}
void  main() {
  int  i;
  for (i=0; i<100; i++) {
    myprint();
  }
}
// 这里主函数中调用100次myprint函数,意味着这个函数要进栈100次,出栈100次,浪费时间。言外之意 static inline 不用进栈出栈。将代码改为:

static inline void myprint() {
  printf ( "ok" );
}
void  main() {
  int  i;
  for (i=0; i<100; i++) {
    myprint();
  }
}

此时相当于:
void  main() {
  int  i;
  for (i=0; i<100; i++) {
    printf ( "ok" );
  }
}

预处理

以#开头的都是预处理,预处理在编译前处理C,包括:1.#define宏定义,2.#include文件包含,3.条件编译 #if, #elif, #else, #endif, #ifdef, #ifndef
#开头的预处理一般顶格写,且可以用 // 注释掉。
#if 和 #endif 是一组同时使用的,叫"条件编译指令",如果满足#if后面的条件,就编译#if和#endif之间的程序段,否则不编译。 #elif 和 if else 差不多。

#ifdef DEBUG
   /* Your debugging statements here */
#endif
// 如果定义了 DEBUG,则执行处理语句。

#ifndef MESSAGE
   #define MESSAGE "You wish!"
#endif
// 只有当 MESSAGE 未定义时,才定义 MESSAGE。

#undef  FILE_SIZE
#define FILE_SIZE 42
这个指令告诉 CPP 取消已定义的 FILE_SIZE,并定义它为 42。

defined 运算符:用在常量表达式中的,用来确定一个标识符是否已经使用 #define 定义过。如果指定的标识符已定义,则值为真(非零)。如果指定的标识符未定义,则值为假(零)。
#define JXW 22
#if defined(JXW)
    #define AAA 1
#endif

预处理:宏定义

宏定义分为:对象宏、函数宏

对象宏:#define 标识符 常量
一般符号常量的标识符用大写字母,变量用小写字母。用define来定义常量可以增加可读性。
符号常量的值在其作用域内不能改变,不能被赋值。eg:

#define PRICE 30
int main(){
    int n = 10;
    printf ("%d\n", n+PRICE);  // 40
return 0;}

函数宏:#define  PLUS(x, y)  x + y          // 参数列表的左括号和define的函数之间不能有空格!

#define  PLUS(x, y)  x + y
int main(){
    printf("%f\n", PLUS(9.1, 9.1));    //输出18.200000
    printf("%d\n", 5*PLUS(3, 4));      // 输出19
    return 0;
}

三目运算符的宏定义:https://www.runoob.com/w3cnote/macro-definition.html
小白写法:#define MIN(A,B) A < B ? A : B
码农写法:#define MIN(A,B) (A < B ? A : B)
工程师写法:#define MIN(A,B) ((A) < (B) ? (A) : (B))
大牛写法:#define MIN(A,B)    ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; })

宏延续运算符( \ )
一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符(\)。例如:
#define  message_for(a, b)  \
    printf(#a " and " #b ": We love you!\n")

字符串常量化运算符(#):把一个宏的参数转换为字符串常量。eg:

#define  message_for(a, b)    printf(#a " and " #b "\n")
int main(void) {
   message_for(jxw, jxd);
   return 0;
}

##    :用于连接2个令牌。eg:

#define concat(a, b) a ## b
int main() {
    int xy = 100;
	printf("%d\n", concat(x, y));  // 预处理器把concat(x, y)转化成了:concat(xy)
    return 0;
}

预定义的宏定义:

printf("File :%s\n", __FILE__ );    // 当前文件名,一个字符串常量
printf("Date :%s\n", __DATE__ );    // 当前日期
printf("Time :%s\n", __TIME__ );    // 当前时间
printf("Line :%d\n", __LINE__ );    // 该printf所在的行号
printf("ANSI :%d\n", __STDC__ );    // 当编译器以 ANSI 标准编译时,则定义为 1


 

const

// 1.const 修饰局部变量
int const a = 1;
const int a = 1;
// 上面两种写法是一样的。注意加了const后a就不能改了,所以在声明时就要初始化。

// 2. const 修饰“常量字符串”
const char *p = "jxw";
printf("%s\n", p);    //注意%s对应指针,所以不能写&p
/* 此处不能把const去掉,因为字符串不能改;如果去掉const,又改了字符串里的字符如p[0]="a",编译不出错,但是run后出错,这非常危险!加上const的话编译时就报错了 */

// 3. 
int b = 1;
int c = 9;
const int * a = &b;    /*常量指针:const后面的*a不能改,但是指针a可以改。不能通过这个指针a改变b的值,但是可以通过其他的引用来改变变量的值:不能 *a=2; ,但是可以 b=2; 。可以 a=&c*/
int * const a = &b;    /*指针常量:指针a不能改即指针指向的地址不能改,*a可以改即指针指向对象的内容可以改 */

// 4. const修饰函数的参数:
在函数中那个被const修饰的参数不能改,如果是指针则和常量指针与指针常量一样。

补码(也叫二进制码)

原码:最高位是符号位,0 是正数,1 是负数。非符号位为该数字的绝对值的二进制。

正数:原码 = 反码 = 补码。

负数的反码:符号位不变,其他取反。负数的补码:该数字的反码+1(注意这个+1的意思)。计算机中负数用补码表示,这可以将加减法转换为位运算。

负数:补码的补码就是原码。

十进制原码反码补码
30000 0011同原码同原码
-31000 00111111 11001111 1101

比如:char:8位,最高位也是符号位,0是正数,1是负数。

char jxw = 0b10000011 ;  // -3(其实这个-3是补码,原码是-125)
printf("%d\n\n", jxw);   // 输出的是 -125,即-3转补码
char jxw = 0b10000000 ;  // -128
printf("%d\n\n", jxw);   // 输出的是 -128

进制

在程序中是根据前缀来区分各种进制数的
1. 十进制常整数:没有前缀;
2. 八进制常整数:必须以 0 开头,数值取 0~7 。
printf( "%d" , 010 );     // 输出8
3. 十六进制:前缀为 0x / 0X,数值取 0~9、A~F 或 a~f。
4. 二进制:前缀为 0b 。

整型变量

1. 基本型:int
2. 短整型:short  /  short int
3. 长整型:long  /  long int
4. 无符号整型:unsigned
5. char

数据类型的取值范围:

// 各种整型的 sizeof()   。占多少字节与系统和编译器规定有关,只能自己试。在putty上:
printf("%ld\n", sizeof(char)); //1
printf("%ld\n", sizeof(int)); //4
printf("%ld\n", sizeof(short)); //2
printf("%ld\n", sizeof(long)); // 8
printf("%ld\n", sizeof(long int)); // 8
printf("%ld\n", sizeof(unsigned)); //4
printf("%ld\n", sizeof(unsigned int)); //4
printf("%lu\n", sizeof(size_t) ); //8

u8   占1个字节(8bit),0~+127[0~2^8-1]
u16 占2个字节,0~+65535[0~2^16-1]
u32 占4个字节,0~+2147483647  [0 ~ 2^31 - 1]

int 在 32/64 位机器上都是占4字节。

说法2,int :8个字节,64个二进制位,-2147483648~+2147483647[-2^31~2^31-1]。
当表示正数时,最高位为符号位(符号位为0),最大的正数是 0111 1111 1111 1111 1111 1111 1111 1111 即 2^31 - 1 = 2147483647 。
当表示负数时,最高位为符号位(符号位为1),最小的负数是 1000 0000 0000 0000 0000 0000 0000 0000 而在计算机中是以补码的形式存储的,C语言规定 1000 0000 0000 0000 0000 0000 0000 0000 的补码为 -2147483648 。

unsigned int : 4294967295

C语言标准库中的 <limits.h> 头文件定义了 unsinged int 的最大值宏 —— UINT_MAX,可以直接使用 printf 函数将其打印出来。
printf("unsigned int最大值:%u\n", UINT_MAX ); // 4294967295
limits.h里也有 INT_MAX , INT_MIN

char范围:-128~127

字符型,转义字符

字符常量:用单引号括起来的一个字符,如  'a' , '=' 
1. 字符常量只能用单引号括起来,不能用双引号或其它括号。
2. 字符常量只能是单个字符,不能是字符串。

转义字符:也属于字符常量,以 ' \ ' 开头,后跟一个或几个字符。

转义字符意思ASCII码
\n回车换行10
\t横向跳到下一个制表位置9
\b退格8
\r回车13
\\ \ 92
\''39
\""34
\a鸣铃7
\ddd1~3位八进制数代表的字符
\xhh1~2位十六进制数代表的字符

char是整数的一种,只是printf输出 %c, %d 的区别。

char a,b,c;
a = 120;
printf("%d , %c\n", a, a);
b = a + 1;
printf("%d , %c\n", b, b);
c = 'x'; 
printf("%d , %c\n", c, c);

bool

C语言中有没有bool类型: C99之前没有。C99标准中新增的头文件中引入了bool类型,与C++中的bool兼容。该头文件为stdbool.h,include <stdbool.h> 后即可使用bool。or:

typedef char bool;
#define TRUE 1
#define FALSE 0

浮点型/实型

double比float : 允许的小数点后位数多,占空间多

分十进制小数和指数形式。

十进制小数:必须有小数点。

指数形式:格式为:a e/E n    (e/E都可以,n是十进制整数)
如:2.1e5  (等于2.1*10^5)
3.7e-2 等于  3.7*10^(-2)
-2.8e-2  等于  -2.8*10^(-2)

标准C允许浮点数使用后缀,后缀为 f / F ,即表示该数为浮点数,如 356f 和 356. 是等价的。

printf("%lu\n",sizeof(float));  //4
printf("%lu\n",sizeof(double)); //8
printf("%lu\n",sizeof(long double));

常量

1.整数常量:如1,-5.
2.实型常量:小数,如1.5、-3.1
3.字符常量:用单引号括起来的一个字符,如 '1' , '!' , '\n'(转移字符也是一个字符,在存储中占一个字节)。
4.字符串常量:用双引号括起来的字符组合,如"hello, xx" , "a"。每个字符串结尾都有隐藏的’\0’表示字符串的结束,占一个字节。

(1) 字符常量由单引号括起来,字符串常量由双引号括起来。
(2) 字符常量只能是单个字符,字符串常量则可以含一个或多个字符。
(3) 可以把一个字符常量赋予一个字符变量,但不能把一个字符串常量赋予一个字符变量。例如: 可比char a = 'a' 不能 char a ="a"。
(4) 字符常量占一个字节的内存空间。字符串常量占的内存字节数等于字符串中字节数加1,增加的一个字节中存放字符 ' \0 ' (ASCII码为0),这是字符串常量结束的标志。'a'只占一个字节,而"a"占2个字节

赋值运算符两边的数据类型不同:自动类型转换

(1)实型赋值给整型,舍去小数部分。

(2)整型赋值给实型,数值不变,但以浮点型存放,增加小数部分为0 。

(3)字符型赋值给整型,字符型为1个字节,整型为4个字节,将字符的ASCII码放到整型量的低八位,高位为0;

(4)整型赋值给字符型,只把低八位赋值给字符型。

int a, b = 448;
float x, y = 8.88;
char c1 = 'k', c2;
a = y;   // 8
x = b;   // 448.000000
a = c1;  // 107
c2 = b;  // -64
printf("%f\n%d\n\n", x, a );
printf("%d  %c\n", c2, c2);
int a = 1 / 2;     // 0
float b = 1 / 2;   // 0.00000
printf("%d   %f   \n", a, b);

数据类型的自动转换和强制转换

整数提升: 把小于 int 或 unsigned int 的整数类型(如char)转换为 int 或 unsigned int 的过程。eg: 在 int 中加一个 char :

   int  i = 17;
   char c = 'c'; /* ascii 值是 99 */
   int sum;
   sum = i + c;
   printf("Value of sum : %d\n", sum );   //输出是116

不同数据类型的混合运算:

long x, y;
int a,b,c,d;
x=5;  y=6;
a=7;  b=8;
c=x+a;    	         //长整形和整形之间允许进行运算,结果为长整形。但c为基本整形,因此最后结果是基本整形。
d=y+b;
printf("c = %d, d = %d \n", c, d);    // 输出:c = 12, d = 14
printf("%d\n", sizeof (c) );	    //输出:4

变量的数据类型是可以转换的。数据类型转换 有2种方法,一种是自动转换,一种是强制转换。
自动转换发生在不同数据类型混合运算时,由编译系统自动完成。自动转换遵循以下规则:
(1)若参与运算的数据类型不同,则先转换为同一类型,再进行运算。
(2)转换按数据长度增加的方向进行,以保证精度不降低。如 int 和 long 运算时,先把 int 转为 long 后再进行运算。
(3)所有浮点运算都是以双精度 double 进行的,即使仅含 float 单精度运算的表达式,也要先转为 double,再进行运算。
(4)char 和 short 参与运算时,必须先转换为 int。
(5)在赋值运算中,赋值号两边量的数据类型不同时,赋值号右边量的类型将转换为左边的类型,如果右边的数据类型比左边的长,将丢失一部分数据、降低精度,丢失的部分按四舍五入向前舍入。

float PI = 3.1415926;
int s, r = 5;
s = r * r * PI;
printf(" %f\n", r * r * PI);  // 78.539810
printf(" %d\n", s);  // 78

上例中:在执行 s = r * r * PI 时,r 和 PI 都转换为 double 型,结果也是 double 型。是之后赋值给 s 后舍去了小数部分。

自动转换规则:char、short < int < unsigned int < long < unsigned long < long long < unsigned long long < float < double < long double 。
隐式地把值强制转换为相同的类型,编译器先执行整数提升,如果操作数类型不同,则它们会按上面的规则往数据长度增加的方向进行,以保证精度不降低。但这不适用于赋值运算符、逻辑运算符 && 和 || 。

   int  i = 17;
   char c = 'c';   /* ascii 值是 99 */
   float sum;
   sum = i + c;
   printf("Value of sum : %f\n", sum );	//输出116.000000

上例中:c 首先被转换为整数,但是由于最后的值是 float 型的,所以会应用常用的算术转换,编译器会把 i 和 c 转换为浮点型,并把它们相加得到一个浮点数。

强制类型转换

强制类型转换通过类型转换运算来实现,可以把表达式的运算结果强制转换为某个数据类型。格式:
(数据类型)  表达式               // 前面的类型说明符两边必须加括号。

eg:    (float)  a          // 只是这一次变成float,以后还是原来的数据类型
(int) (x+y)

(1)数据类型和表达式两边都必须加括号,只有表达式为单个变量才可以不加括号。如果把 (int) (x+y),写成 (int) x + y 就变成先把 x 转为 int 再与 y 相加了。
(2)无论是强制类型转换还是自动转换,都只是为了本次运算的需要而对变量的数据长度进行的临时转换,而不改变数据说明时对该变量定义的类型。

float f = 5.75;
printf("%d \n", (int)(f) );
printf("%f , %f \n", f , (double)(f));
// 5 , 5.750000 , 5.750000
int sum = 17, count = 5;
double mean;
mean = (double) sum / count;        //强制类型转换运算符的优先级大于除法
printf("Value of mean : %f\n", mean );    //输出3.400000

手写整数转字符串:
#include<stdio.h>
#include<string.h>
static void itoa(int num, char str[100]) {
        int i = 0;
        do {
                str[i] = num % 10 + '0';    // 对10取余再加'0'就得到了个位上的数。
                num /= 10;
                i++;
        } while (num);
        str[i] = '\0';
}
int main() {
        int num =12345678;
        char str[100];
        itoa(num, str);
        for (int i = strlen(str) -1; i >= 0; i--)
                printf("%c", str[i]);
        printf("\n");
}

数据赋值时的数据类型转换:

~ 有参函数的参数进行传递时,不能函数定义时是 *u8,而函数实际调用时是 *int 。

~ 在 i2c_msg 中,若把一个 int* 指针赋值给 buf (__u8*),则报错。

C语言逻辑判断

假:0
真:1 或 非零

int a = 0;
printf( "%d\n", 1 < 2 );   // 1
printf( "%d\n", 1 == 2 );   // 0
printf( "%d\n", a = 20 );   // 20
printf( "%d\n", -3 && 2 );  // 1

表达式和语句

eg:赋值表达式如(x = y + 5)是一种表达式,它可以出现在任何允许表达式出现的地方,而赋值语句如(x = y + 5;)则不能。

这是合法的:if ( (x = y + 5) > 0 )   z = x;

这是非法的:if ( (x = y + 5;) > 0 )   z = x;                      此处(x = y + 5;)是语句

运算符

逗号运算符:,

格式:表达式1, 表达式2, 表达式3 .......

从左往右依次运算,最右边的值作为整个逗号运算符的值。

eg: int a = 1, b = 2;  则 (a+3, b=a+b, b+5, a+b) 的值为:4 。
a+3 == 1+3 == 4
b = a+b == 3
b+5 == 8
a+b == 4

int a = 2, b = 4, c = 6, x , y ;
y = ( (x = a + b) , (b + c) );
printf("%d   %d   \n", x, y);    //  x = 6, y = 10
算术运算符

即:+-*/

两边都是 int 时,结果也是 int ,舍去小数。

printf("%d, %d\n", 20/7, -20/7);      // 2, -2
printf("%f, %f\n", 20.0/7, -20/7.0);  // 2.857143, -2.857143

% : 取余。

N & 0xFF : 相当于正整数N对256取余运算。

除法默认舍去小数、也就是向下取整,怎么向上取整:

1. 检查余数:
if(temp%k == 0) result = temp/k;
else result = (temp/k) + 1;

2. 使用 “cmath” 头文件中的ceil()函数 
int result = (int) ceil(temp*1.0/k); //ceil()函数返回的是double型,所以用强制转换
// 此方法在极端条件下容易出现精度丢失
举个例子:
假设某两个数的ceil计算结果原本是2.0,但由于精度问题,ceil结果其实是1.9999999999999999999999999
在结果转为int型数据时,发生了精度丢失,计算结果由2.0转换为1
总结:在任何可能与浮点数进行比较或者整形与浮点型之间进行类型转换的地方都必须特别注意!

比较运算符

>      大于
>=    大于等于
<      小于
<=    小于等于
==     等于
!=      不等于

逻辑运算符

&&是逻辑运算符,表示逻辑与;x && y :若x和y均为true,则取值为true。

注意与&的区别:&是位运算符,同时也是取地址符。

||  :逻辑或。

位运算符

对整数和字符数据的位(bit)进行逻辑与位移的运算,通常区分为"位逻辑运算符"与"位位移运算符"。

位操作都是按照补码来计算的。先全部转补码,进行计算,然后将结果再转补码。

位逻辑运算符功能运算过程
&AND(与)转为位二进制后,逐位与,再转为十进制
|OR(或)逐位或
^XOR(异或)逐位异或
~NOR(非)逐位非:~n = -n-1(正负数都是这样)
    int x = 7;     // 0111
    int y = 6;     // 0110
    int a = x & y; // 0110 = 6
    int b = x | y; // 0111 = 7
    int c = x ^ y; // 0001 = 1
    int d = ~x;    //  -8
    printf("%d\n%d\n%d\n%d\n", a, b, c, d);

-5 & 3 结果为 0000 0011,即 3,符号位也进行了与运算。
-5 | 3 结果为 -5。 -5 补码是 1111 1011,3 补码是 0000 0011,二者进行或运算,结果是 1111 1011,再转补码:1000 0101,即-5。

移位运算符 :先转二进制,转补码,移位,转补码,二进制转十进制。

左移运算符 << : 左边丢弃,符号位也要左移,移位后符号位是什么就是什么,如果变成1 就变成负数,右边空出来的位补0。
右移运算符 >> : 1.算术移位(C语言是这种):左边用原该值的符号位填充,右边丢弃。(2.逻辑移位: 左边补0,右边丢弃。)

以 char 为例:
正数:
char jxw = 3 << 1;                         // 6
jxw = 1 << 6;                                 // 64  ,不越界的情况下,左移几位就乘几个 2 。
char jxw = 1 ; jxw = jxw << 7;          // -128
char jxw = 3 ; jxw = jxw << 6;          // -64
char jxw = 3 ; jxw = jxw >> 1;        // 1
jxw = 1 >> 9;                                 // 0

负数左移:先把十进制负数转二进制 -> 原码转补码 -> 左移 -> 再转补码 -> 再二进制转十进制。
左移时符号位1也左移,移位后符号位是什么就是什么,如果变成0就变成正数;左边舍弃,右边补0。
char jxw = -1; jxw = jxw << 6;           // -64 。
char jxw = -96 ; jxw = jxw << 1;      // 64 。-96转补码 1010 0000 ,左移后:0100 0000,转补码后不变,转十进制:64

负数右移:先把十进制负数转二进制 -> 原码转补码 -> 右移 -> 再转补码 -> 再二进制转十进制。
移位时符号位1也右移;左边补1,右边舍弃;再转补码。所以负数右移后必然是负数。
jxw = -4 >> 1;          // -2
jxw = -4 >> 9;          // -1,因为右移很多位后补码必然是 1111 1111
char jxw = -68; jxw = jxw >> 1;    // -34。-68 原码 1100 0100,转补码:1011 1100,移位后:1101 1110,再转补码:1010 0010,二进制转十进制:-34

赋值运算符

在变量的声明中不能连续赋值,如这样是错误的:int a = b = 5;  ,必须写为: int a = 5, b = 5; 
而赋值语句允许连续赋值。

复合赋值:对人不习惯,但有利于编译,能提高编译效率。
a += b  相当于  a = a + b
a >>= b  相当于 a = a>>b (如果只执行 a<<3;  则相当于没有给a赋值,a 还是原来的值)  
a <<= b  相当于 a = a<<b
a &= b   相当于 a = a&b
a ^= b    相当于 a = a^b
a |= b     相当于 a = a | b
a *= y + 7    相当于   a = a * (y + 7)

三目运算符 / 条件运算符

xxx ? yyy : zzz

xxx 是条件,如果满足条件 xxx ,则执行yyy,否则执行zzz。

三目运算符的优先级低于关系运算符和算术运算符,但高于赋值运算符。因此:
max = (a>b)?a:b
可以去掉括号变成:
max = a>b?a:b

三目运算符的结合方向是自右至左,例如:
a>b?a:c>d?c:d
应该理解为:
a>b?a:(c>d?c:d)

运算符优先级

详细见:C++ Primer Plus-异步社区-运算符优先级

 结合性            运算符                                        优先级
自左向右       ()  []  ->  .   后缀++/--                 优先级最高
自右向左       !  ~  +  -   前缀++/--   &  *  sizeof
自左向右       *  /  %
自左向右       +  -
自左向右       <<   >>
自左向右       <  <=  >  >=
自左向右       ==   !=
自左向右        &
自左向右       ^
自左向右       |
自左向右       &&
自左向右       ||
自右向左       ?:
自右向左       =  +=  -+  *=  /=  %=  &=   ^=   |=   <<=   >>=
自左向右       ,                                                   优先级最低

记忆方法2:算术>关系>逻辑
1. 括号运算符: ()
2. 算术运算符: +  -  *  /  %   ++   --
3. 关系运算符:<  >  <=  >=  ==  !=
4. 逻辑运算符:&&   ||  !
5. 复合运算符:+=  -=  *=  /=  %= 

C语言的运算符优先级一共15级,在一个运算量两侧的运算符优先级相同时,按结合性的结合方向处理。
自左向右(左结合性):如 x-y+z ,y 应该先与 - 结合执行 x-y 。
自右向左(右结合性):如 x=y=z=5 ,相当于 x = ( y = (z = 5) ) 。

// eg:
char c = 'k';
int i=1, j=2, k=3;
float x=3e3, y=0.85;
printf("%d, %d \n", 'a'+5 < c, -i-2*j >= k+1);   // 1, 0
printf("%d, %d \n", -1<j>0, x-5.25 <= x+1);   // 1, 1
printf("%f \n", x);    // 3000.000000
printf("%d \n", k==j==i-1);    // 1

指针解引用 和++、--的优先级:
优先级排序:p1++ > *p2 = ++p3 ( 后置 > 指针解引 = 前置

结合性:相邻的运算优先级相同该如何去结合计算。

左结合性:后置++是左结合性
a*2/1:这里*和/优先级相同,且是左结合性,所以先计算a*2,然后计算2/1

右结合性:前置++、指针解引用是右结合性。

前置++和指针解引用
根据优先级前置和指针解引用是同级的,然后根据结合性都是右结合性。
*++p等价于 *(++p),等价于:p+=1, *p = 0

int arr[] = {1, 3};
int *p = &arr[0];
printf("p is %d\n", p);             // 1703720
*++p = 5;
printf("now p is %d\n", p);         // 1703724
printf("*p is %d\n", *p);           // 5
printf("arr[0] is %d\n", arr[0]);   // 1
printf("arr[1] is %d\n", arr[1]);   // 5

++*p等价于 ++(*p)

int arr[] = {1, 3};
int *p = &arr[0];
printf("p is %d\n", p);                  // 1703720
++*p;
printf("now p is %d\n", p);              // 1703720
printf("*p is %d\n", *p);                // 2
printf("arr[0] is %d\n", arr[0]);        // 2
printf("arr[1] is %d\n", arr[1]);        // 3

*p-- :单目运算符从右至左。先 --。*p--等价于*(p--)。注意这里如果*p--还有赋值则*p的p是p--之前的值。

int arr[2] = {1, 2};
int *p = &arr[1];
printf("p is %d\n", p);            // 1703724
*p-- = 18;      //
printf("now p is %d\n", p);        // 1703720
printf("*p is %d\n", *p);          // 1
printf("arr[0] is %d\n", arr[0]);  // 1
printf("arr[1] is %d\n", arr[1]);  // 18

数学运算:绝对值、指数、底数等(可以背诵)

要#include <math.h>

int x;  abs(x);    ——求整数x的绝对值

double x;  fabs(x);     ——求浮点数x的绝对值

double x;  sin(x);     ——求sinx
double x;  cos(x);     ——求cosx
double x;  tan(x);      ——求tanx

double x;  exp(x);      ——求e的x次方。( C语言就用exp(1)来求e )

double x, y;  pow(x, y);       ——求x的y次方。(pow后面两个参数必须是常量数字,不能是变量)

double x;   sqrt(x);     ——求 x 的 0.5 次方

double x;   log(x);     ——求 lnx

double x;   log10(x);     ——求 lgx

求两数的最大值,返回一个浮点数:fmax(x, y),要 #include <math.h>
double fmax (double x   ,   double y);
float  fmaxf ( float x  ,  float y );
long double fmaxl ( long double x, long double y );

goto

语法:
goto label;
label: xxxxxxx;

label: 会直接被忽略,和没有一样,就算没有goto label, 就加个label: 也不会出错。
label: 可以和语句写在同一行。

C语言读写文件

#include <stdio.h>
#include <stdlib.h>
int main () {
    FILE *fp;
    char ch;
    fp = fopen("/home/jixiaowei1/jxwDraft/jxwCmain/spiid.txt", "r");
    while((ch = fgetc(fp))!=EOF) {
        putchar(ch);
    }
    fclose(fp);
    return 0;
}
//某次增加几百个spiid:读文件,五个五个输出
#include <stdio.h>
#include <stdlib.h>
int main () {
    FILE *fp;   char ch;   int len = 0;   char arr[20000];   int line = 1;   int jxwEnter = 1;
    fp = fopen("/home/jixiaowei1/jxwDraft/jxwCmain/spiid.txt", "r");
    while((ch = fgetc(fp))!=EOF) {
        arr[len] = ch;
        len++;
    }       printf("\n%d\n",len);
    for(int i = 0; i<len ; i++) {
        if (arr[i]=='0' && arr[i+1]=='x') {
            //printf("%d_",line);
            line++;
            for(int j = 0 ; j<10  ; j++) 
                printf("%c", arr[i+j]);
            if(jxwEnter%5==0)
                printf(", \\\n");
            else
                printf(",");
            jxwEnter++;
        }
    }
    fclose(fp);     return 0;   }

基本语句

printf

printf("abc");

printf :输出列表中的求值顺序,不同的编译系统不一定相同,可以从左到右,也可以从右到左,如:
int i = 8;
printf("%d\n%d\n%d\n%d\n%d\n%d\n%d\n",i, ++i, --i, i++, i--, -i++, -i--);   
// VC++6和VS输出结果不一样

%后面加个字母叫“格式化字符串”

%d以十进制输出带符号整数,正数不输出符号
%ld以十进制长整型输出
%x, %X以十六进制输出无符号整数,不输出前缀0x。x表示输出的16进制字母是小写;X表示大写
%o以八进制输出无符号整数,不输出前缀0
C语言没有二进制形式输出,要自己实现。
%uunsigned 以十进制输出无符号整数
%lulong unsigned int
%f以小数形式输出单、双精度实数
%e, %E以指数形式输出单、双精度实数
%g, %G以%f或%e中较短的输出宽度 输出单、双精度实数
%c输出单个字符,char
%s输出字符串, string

printf("%e\n", 123.456);     // 1.234560e+02

输出字符数组

char c[10] = { 'a' , 'b' , 'c' , 'd' , 'e' };            //   !这里如果写成 char c[10] = "abcde";  则下面3种方法也是完全一样的。

方法1:
int i = 0;
for ( ; i < 10; i++) printf("%c", c[i]);     // abcde

方法2:
printf("%s\n" , c);     // abcde

方法3:(不推荐,虽然方便,但又要记一个)
puts(c);

~ 字符数组里的最后最好多加一个 '\0'。字符符数组只输出第一个 '\0' 前面的字符。eg:
char c[10] = { 'a' , 'b' , '\0' , 'c' , '\0' };
printf("%s\n", c);         // 只输出 ab

printf("%m.nf")       :极少用到

一共占m个格子,其中小数点后n个格子,m 前面加 - 表示补空格补在小数点后面。

printf("%5.2f\n",  1.23 );  // 1.23
printf("%-5.2f\n", 1.23 );  //1.23
printf("%7.4f\n",  1.23 );  // 1.2300

%04d  ,  %4d

// %04d:其他情况不变,当输出不满 4 位时,前面补 0,直到输出满了 4 位。

int a = 123;
printf("%02d\n", a); // 123,全部输出的
printf("%04d\n", a); // 0123
printf("%06d\n", a); // 000123

// %4d:其他和 %04d 相同,除了:%04d 是在前面补 0,而 %4d 是在前面补空格。

%5.3s

// %5.3s :输出一共占 5 格,只取该字符串的左端 3 个字符,不足 5 格的在左边补空格。

printf("%s\n",    "computer" );  //computer 
printf("%5.3s\n", "computer" );  //  com

~ 标志字符为 - 、 + 、 # 、 空格  4种,其意义如下:

-结果左对齐,右边填空格
+输出符号(正号或负号)
空格输出值为正时为空格,为负时为负号
#对c, s, d, u 类无影响;对o类,在输出时加前缀o;对x类,在输出时加前缀ox;对e, g, f 类,当结果有小数时才给出小数点。

~ 精度:以 " . " 开头,后跟十进制整数,本项的意义是:如果输出数字,则表示小数的位数;如果输出的是字符,则表示输出字符的个数;
~ 长度格式符:分 h, l 两种,h表示按短整型量输出,l 表示按长整型量输出。

int a = 15;
float b = 123.1234567;
double c = 12345678.1234567;
char d = 'p';
printf("%d, %5d, %o, %x\n", a, a, a, a); // 15,    15, 17, f
printf("%f, %lf, %5.4lf, %e\n", b, b, b, b);  
// 123.123459, 123.123459, 123.1235, 1.231235e+02
printf("%lf, %f, %8.4lf\n", c, c, c);    
// 12345678.123457, 12345678.123457, 12345678.1235
printf("d=%c, %8c\n", d, d);             // d=p,        p

scanf

格式:scanf("格式控制字符串", 地址列表);          // scanf后面接地址,printf后面只接变量名

如果“格式控制字符串”中有“非格式字符”(就是 ' , ' 、空格 等等)则输入时也要输入该格式字符比如: 
int a, b ;
scanf("%d, %d", &a, &b);      // 其中用非格式字符 " , " 作间隔符,此时输入必须用 " , " 隔开。

又如:
scanf("a=%d, b=%d", &a, &b );    // 此时输入应为:a=1, b=2

又如:
scanf("%d%d%d", &a, &b, &c);     // 此处输入的东西之间 不能用" , "

~ 在输入多个数值数据时,如果“格式控制字符串”中没有非格式字符作为输入数据之间的间隔则可用 空格、TAB、回车 作为间隔。C编译在碰到 空格、TAB、回车、非法数据(例如对 “%d” 输入12A,这里A即为非法数据)时,即认为该数据结束。例如下面输入5个数之间实测可以用 空格、TAB、回车 隔开:

int a[5], i = 0;
for (i = 0; i < 5; i++)
  scanf("%d", &a[i]);
for (i = 0; i < 5; i++)
  printf("%d  ", a[i]);

注:
~ scanf 中没有精度控制,如:scanf("%5.2f", &a);    是非法的。不能企图用此语句输入小数为2位的实数。
~ scanf 中要求给出变量地址,如只有变量名则出错。
~ * :*用于表示该输入项读入后不赋予相应的变量,即跳过该输入值。如:scanf("%d %*d %d", &a, &b);   ,当输入为 1 2 3 时,把1赋予a,2被跳过,3赋予b。

~ 宽度:用十进制整数指定输入的宽度(即字符数),如:scanf("%5d", &a);    ,当输入12345678,只把12345赋值给变量a,其余部分被舍去。又如:scanf("%4d%4d", &a, &b);    ,当输入12345678,则把1234赋予a,把5678赋予b。

~ 可以输入浮点数、字符型。

~ 在输入字符数据时,如果格式控制字符串中无非格式字符,则认为所有输入的字符均为有效字符。如:scanf("%c%c%c", &a, &b, &c);    ,输入 d e f ,则把 'd' 赋予a,' ' 赋予b,'e' 赋予c。

EOF:文件结束符。在while循环中以EOF作为文件结束的标准。

牛客eg:输入未知多行,每行两个数,计算他们的和:
int a, b;
while ( scanf("%d %d", &a, &b) != EOF ) { // 注意while处理多个case
  printf("%d\n", a + b); }

输入字符数组

1. 挨个单词输入 scanf("%s%s...", )             2. 整句输入 gets()

eg: 输入 "aa bb jxw",存入数组:(字符串里有空格,而scanf只能提取除了 '空格' 之外的部分)

方法1:
char c1[5], c2[5], c3[5];
scanf("%s%s%s", c1, c2, c3); // 没有&
printf("%s %s %s \n", c1, c2, c3);
// 此处 c1 实际上是:'a' , 'a' , '\0' , '\0' , '\0' 。c2 、c3 同理。

方法2:
char c[18];
gets(c);
printf("%s \n", c);

getchar()   ,   putchar()

char a;
a = getchar();
putchar(a);
putchar('\n');

选择语句

if(a > 1)
   ***;
else
   ***;

~ if 和 else 后面不用加;    ,   如果if/else后面只有一个语句可以不加{}

~ 如果 if 语句嵌套了还没有大括号 {},为了避免二义性,C语言规定 else 总是与它前面最近的且不带else的 if 配对。

~ if 关键字后的表达式一般是逻辑表达式/关系表达式,但也可以是其他表达式,如赋值表达式等,也可以是一个变量。例如:
if (a=5)
if (b)
都是允许的,只要表达式的值非0,即为真。

~ if (-1) 也会执行, if (0) 不会执行

~ 技巧:为避免"a==5"写成"a=5",可以改成"5==a",如果"5=a",会编译报错。

if (表达式1)
    语句1;
else if (表达式2)
    语句2;
...
else
    语句n;

// 如果 if 后面为真,则后面的else if就都不执行了。如果某一个else if后面为真,则它后面的else if也不执行了。如果某else if前面的else if和if都是假,则它还是要执行。

eg:
    printf("%d\n", printf("jxw123\n"));    // 7
    if (1==printf("jxw11\n")) {}
    else if (1==printf("jxw22\n")) {}
    else if (printf("jxw33\n")) {}
    else {}

switch

格式:

switch (整型变量或字符型变量)
{
  case  常量1:  语句1;   break;           // 字符型变量的话要加单引号
  case  常量2:  语句2;   break;
......
  default:  语句n;   break;
}

// case 关键字后面必须是一个整数,或者是结果为整数的表达式,但不能包含任何变量。

// case 后的各常量表达式的值不能相同,否则会出错。

// case 后允许有多个语句,可以不用 { } 括起来。

// 各 case 和 default 子句的先后顺序可以变动,而不会影响程序执行结果。

// default 子句可以省略不用。

// switch 不一定要有break。

// 前后两个case要执行的内容都一样的话,上面的可以不写,eg :
case 'A':
case 'B':  printf("jxw\n");  break;

循环语句

while语句

~ while 后面没有 " ; "
~ while 之前得把各个参数都取好值
~ 满足while后面括号内的条件则执行一次while后面大括号里的东西:

int i = 0, sum = 0;
while (i<=100) {
 sum = sum + i;
 i++;
 printf("%d\n", sum); }

while ( z-- > 0 && ++x < 5 ) { }
上例中:若 z-- > 0 为假,则不执行 ++x < 5,但 z 还是要 -1 。

if() break  :跳出整个循环

if() continue  :进入下一次循环,eg:输出100~200之间不能被4整除的整数:

int i = 99;
while (i<=200) {
 i++;
 if( i % 4 == 0 || i > 200) continue;
 printf("%d\n", i); }

do...while 和 while 是一样的。除了:do...while是不管while先执行一次,再判断是否符合while。eg:

int i = 1, sum = 0;
do {
 sum = sum + i;
 i++;
}
while(i<=3);
printf("sum = %d\n\n", sum);

for循环

把for和while看成两种完全无关的东西,不要类比她们之间的联系了。

格式:for (语句1; 语句2; 语句3) {}

for中的语句3:一次循环结束后就执行语句3,即使有continue也执行语句3

语句1~3可以都不写。

死循环:for(;;) {}   相当于  while(1) {}。或者 :for(**; 1; **) {}

int i;
for (i = 0; i < 3; i++) {
    printf("%d  ", i);     // 0  1  2
}
printf("\n%d\n", i);       //这里最后输出3,可见最后一次循环依然执行了 i++
int i = 0; 
for( ; i < 3; i++) {
	if(i == 1) break;	
}
printf("%d\n", i); // 输出1,可见 break 之后,语句3就不执行了!
int val=0, i;
for (i = 0; i<3; i++) 
 val++;
printf("val = %d \n" , val); // val = 3,可见val的改变的保留的

数组

一维数组

定义 / 数组初始化:int a[5];    // 包括 a[0], a[1] ~ a[4]
int a[5] = {2,4,6,8,10};    // ==>  int a[5];  a[0]=2;  a[1]=4;  a[2]=6;  a[3]=8;  a[4]=10;
int a[5] = {2,4,6};            // ==>  int a[5];  a[0]=2;  a[1]=4;  a[2]=6;  a[3]=0;  a[4]=0;
int jxw[3] = {};           // 相当于 int jxw[3] = {0} ,相当于 int jxw[3] = {0, 0} 。数组元素都初始化为0。此时若输出 jxw[4] 则是随机数。
int jxw[3];                 // 则数组元素为随机一个数。

int jxw[3] = {1};    //输出:1,0,0
for (int i = 0; i<3; i++) {
    printf("__%d__\n", jxw[i]);
}
printf("%lu\n", sizeof(jxw));    //12
int a[4];   //不报错,但输出是随机数
for(int i=0;i<=3;i++)
    printf("%d\t",a[i]);
printf("\n\n输入4个面积\n");
for(int i=0;i<=3;i++)
    scanf("%d", &a[i]);
for(int i=0;i<=3;i++)
    printf("%d\t",a[i]);
printf("\n");
int a[] = {1,2,3,4,5};    // 实际上只有a[4]
for(int i=0; i<=6; i++)
    printf("%d\t",a[i]);    //没赋值的输出随机数
printf("\n %lu \n", sizeof(a));   // 20,不包括未赋值的

数组长度能不能是一个变量:C99可以。c99标准中,新增了可变长度数组:Variable-length array (VLA)。

  1. C99之前,数组的大小要为常量或常量表达式( sizeof()被视为整形常量 ),这就导致在源码中就明确了数组长度。
  2. C99的VLA,用变量指定数组长度,而变量的数值在程序运行过程中才能确定,这就导致可变长度数组的大小只有在运行到定义数组长度的变量后,才能确定变长数组的长度。
  3. 其实,数组的内存分配,C99之前或C99都是在程序运行的时候,才分配实际的内存。所以,只要在给数组分配实际内存之前,确定了数组长度,就OK了。而数组长度是在编译时候确定,还是在程序运行中给数组分配内存之前确定,都不影响数组。
  4. 结合3,知道可变长度数组(VLA)其实也是静态数组,即在数组分配内存之前确定长度,分配内存后就不再改变该数组长度。
  5. 由VLA的特点,当在源码中无法确定数组长度,而需要通过程序运行后才能确定数组长度,且之后数组不再需要改变大小(不会造成空间浪费也不会空间不足)的情况下,使用VLA比使用动态数组更方便。

可变长度数组的限制:

  1. 必须是自动变量,即:不能使用static、extern关键字修饰
  2. 数组长度可以是一个变量;但数组长度是变量时数组不能在定义时就初始化,如下面不能 int arr[len] = {1};
int len = 3;
int arr[len];
arr[0] = 9;
printf("%d\n", arr[0]);

C语言字符数组:

C语言没有字符串变量string,只有字符数组。
char c[5] = {'a', 'b', ' ', '\t', 'e'};
printf("%s\n", c); // 等价于 printf("%s\n", &c[0]);
printf("%s\n", &c[1]); // 从字符数组的c[1]开始打印字符串
printf("%c\n", c[1]);

char str[3] = { 'j' };          //只定义1个字符
printf("__%c__\n", str[0]);  
printf("__%c__\n", str[1] + 49);  // 1 ,可见没手动初始化的字符也是初始化为 '\0' 的
printf("__%c__\n", str[2] + 49);  // 1  
printf("%lu\n", sizeof(str));  // 3
printf("%lu\n", strlen(str));  //1
char str[10] = "jxw";
str[5] = 'j';                                //这个依然有输出
for(int i=0; i<=9; i++)
    printf("__%c__\n", str[i]);       //没有定义的不输出。
printf("%lu\n", sizeof(str));    //10
printf("%lu\n", strlen(str));    //3 (不包括后来的赋值!)
printf("%lu\n", sizeof("jxw"));   //4(包括了最后的\0)
printf("%lu\n", strlen("jxw"));   //3
// 声明字符数组时不指定元素个数。
char str[] = "jxw";    // 这一下会使str变成char str[4] !
str[1] = 'j';
str[5] = 'j';    // 编译不报错,也可以输出这个值。
for(int i=0; i<=8; i++)
    printf("__%c__\n", str[i]);       //没定义的没用输出
printf("%lu\n", sizeof(str));    //4
printf("%lu\n", strlen(str));     //3

动态数组

char *p;    // 动态数组
p = "abcdef";
for(int i = 0; i < strlen(p); i++){
    printf("%c   ", *(p+i));    //a   b   c   d   e   f 
}
printf("\n%lu\n", sizeof(p));    // 8
printf("\n%lu\n", strlen(p));    // 6
printf("\n%lu\n", sizeof(*p));  // 1

""内的字符串也可以用[]解析,如:
char* pstr = "abcde";
printf("%c\n", pstr[2]);

int i = 0;
char Filename[13] = "UefiLog0.txt";
for (; i < 15; i++) {
    printf("%c", Filename[i]);
}
printf("\n%c\n", Filename[12]+49);  //输出1,可见:最后一个确实是'\0'

输入一个长度未知的动态数组:数组满了就自己动态扩容

#include<stdio.h>
#include<stdlib.h>
int main() {
 int capacity = 10;
 int *arr = malloc(sizeof(int) * capacity);
 int temp;
 int size = 0;   // 当前的数组长度
 while(1) {
  scanf("%d", &temp);
  if (temp == -999)
    break;    // 如果输入-999就表示结束
  if (size >= capacity) {
    capacity *= 2;        // 如果数组满了,动态扩容
    arr = realloc(arr, sizeof(int) * capacity);  // realloc扩容后原数组内容不变
  }
  arr[size] = temp;
  size++;
 }
 for(int i = 0; i < size; i++)
  printf("%d  ", arr[i]);
 printf("\n");
 free(arr);
 return 0;
}

sizeof() , strlen() 

sizeof():返回数组所占内存的大小(占多少个字节 Byte)int a[3]则为12。
如果是字符串常量也包含了最后的\0,如:printf("%d\n", sizeof("jxw")); // 4
返回一个long unsigned int,printf 要%lu,%d会报warning。

strlen():计算字符串在第一个 \0 之前有几个是单个字符。
需要 #include <string.h> 。
定义:extern unsigned long strlen(const char* __s)。
返回一个long unsigned int,printf要%lu。
只有 const char* 才可以strlen(),不适用于整数数组,普通字符数组如 char jxw[10] 有时候也可以。
字符串"abcd"的最后有一个隐藏的'\0'(整数数组和字符数组没有\0),
strlen("abcd")=4,sizeof("abcd")=5
strlen("abc\0def")=3

char a1[] = {'a', 'b', 'c', 'd' };
printf("%lu\n",sizeof(a1));	// 4
printf("%lu\n",strlen(a1));	// ?   strlen是在a1[]之后遇到'\0'才停止计数
char a2[] = {'a', 'b', 'c', 'd', '\0' };
printf("%lu\n",sizeof(a2));	// 5
printf("%lu\n",strlen(a2));	// 4
char A[6] = {'a','b','\0','d','e'};
printf("%lu\n", sizeof(A));    // 6, 没赋值的也算进去了
printf("%lu\n", strlen(A));    // 2
printf("%s\n", A); // ab
char B[]  = {'a','b','\0','d','e'};
printf("%lu\n", sizeof(B));    // 5
printf("%lu\n", strlen(B));    // 2
printf("%s\n", B); // ab

char* C = {'a','b','\0','d','e'};  //报warn或报错
printf("%lu\n", sizeof(C));   // 8
printf("%lu\n", strlen(C));   // 报错

char str[] = "abcde";
printf("%lu\n", sizeof(str));    //6  
printf("%lu\n", strlen(str));    //5
char *p1 = str;          
printf("%lu\n", sizeof(p1));    // 8 !可见p1 不完全= str
printf("%lu\n", strlen(p1));    // 5
printf("%s\n", p1); // abcde

char *pstr = "abcde";
printf("%lu\n", sizeof(pstr));   // 8
printf("%lu\n", strlen(pstr));   // 5

strcat, strncat, strcpy, strncpy:

strcat:
函数原型:char *strcat(char *dest, const char *src)
参数:dest:指向目标数组。该目标包含一个C字符串,且要足够容纳追加之后的字符串,如果数组长度不够,则编译不报错、可能运行报错,C语言没有给出办法,无论空间够不够都会进行强制拼接,因此会产生越界溢出,影响其他区域的数据。(dest是destination)
参数:src: 指向要追加的字符串。(src是source)
返回值:一个指向最终目标字符串dest的指针。
要include头文件:<string.h>
~ dest和src: 不是字符串,是字符数组也可以。
~ 原来dest最后的\0被删了,src接在这个位置。

char dest[10] = "abcde";
char src[10] = "1234";
char *ret = strcat(dest, src);
printf("%s\n", ret);    // abcde1234
printf("%s\n", dest);   // abcde1234
printf("%s\n", src);    // 1234

char dest2[10] = {'a', 'b'};
char src2[10] = {'1', '2'};
char *ret = strcat(dest2, src2);
printf("%s\n", ret);    // ab12
printf("%s\n", dest2);  // ab12
printf("%s\n", src2);   // 12

strncat:令参数列表为“str1, str2, n”,在str1的第一个'\0'的位置,把这个'\0'删了,再加上str2的前n个字符,然后在最后加个'\0'。如果n大于str2里元素的个数:与n等于str2元素个数时一样,不会在最后补'\0'。
这个执行完毕后,str2不变,把str1改变了,然后返回的是str1的首地址。eg:
char str1[] = "abcd\0efg";
char str2[] = "123";
char *jxw;
printf("%s\n", str1);         // abcd
jxw = strncat(str1, str2, 5);
printf("%s\n", jxw);         // abcd123
printf("%s\n", str1);        // abcd123
printf("%s\n", str2);        // 123

strcpy: 
函数原型:char* strcpy(char* dest, const char* src);
参数:dest:目标字符串;src:源字符串。
返回值:dest
功能:strcpy() 会把src指向的字符串复制到dest,dest原本的内容没了。
必须保证dest足够大,能容下src,否则会导致溢出、越界。

char dest[10] = {0, '1', '2', '3', '4'};
char src[10] = {"abc"};
strcpy(dest, src);
printf("%s", dest);    // abc

strncpy:令参数列表为 “str1, str2, n” ,str2元素个数为l2。如果n小于等于l2,则仅仅替换n个字符;如果n大于l2,则在str2后面补(n-l2)个 '\0',然后把它替换str1的前n个字符(str1剩下的字符不变,但要注意str2里可能有 '\0' )。如果str2里有'\0',则strncpy也视为到了str2的结束,后面的内容不会拷贝,而是补'\0'。
这个执行完毕后,str2不变,把str1改变了,然后返回的是str1的首地址。 

char str1[] = "abcdefg";
char str2[] = "123";
char *jxw;
printf("%s\n" , str1);    // abcdefg
jxw = strncpy(str1, str2, 2);
printf("%s\n" , jxw);     // 12cdefg
printf("%s\n" , str1);    // 12cdefg
printf("%s\n" , str2);    // 123

手写实现strncpy:

char *jxw_strncpy(char * str1, const char *str2, int n) {
    char *ret = str1;  //存放目标空间首地址
	while (n--) {
	    if (str2 == '\0')
			*str1++ = '\0';
		else
			*str1++ = *str2++;
	}
	return ret;
}

strcmp() , strncmp :

strcmp函数: 是string compare 的缩写,用于比较两个字符串并根据比较结果返回整数。
基本形式为strcmp(str1,str2),若str1==str2,则返回零;若str1<str2,则返回负数;若str1>str2,则返回正数。
语法:#include <string.h>
int strcmp(const char *s1, const char *s2);
int strncmp(const char *s1, const char *s2, size_t n);
The strcmp() function compares the two strings s1 and s2. It returns an integer less than, egual to, or greater than zero if s1 is found, respectively, to be less than, to match, or be greater than s2.
The strncmp() function is similar, except it only compares the first (at most) n characters of s1 and s2.

在字符串前面加一个L的作用:unicode字符集是两个字节组成的,L告诉编译器使用两个字节的unicode字符集,如 L"abc" 表示将ANSI字符串转换成unicode的字符串,就是每个字符占用2个字节。不加 L 就是每个字符8位,L表示宽字符,每个字符16位。
strlen("asd")   =   3;   
strlen(L"asd")   =   6;    // 这样写编译不过。

C语言数组在定义之后,不允许再次进行赋值

C语言数组在定义之后,不允许再次进行赋值,仅允许修改和读取操作,而且修改也只能逐个进行。

char str[4] = "abc"; // 这是可以的,定义时就赋值是可以的。

char *str;
str = "abc"; // 这也是可以的,把 "abc" 的首地址给str
char str[4];
str = "abc"; 
// 不可以!报错:array type 'char [4]' is not assignable 。声明了 char str[4],那么 str 就代表了这个 char 数组的首地址。数组名只是代表数组第一个元素的地址的值,比如数组 int a[10],a实际上就是 &a[0],它只是一个值,就像 5 这类整型常量一样,是不能作为左值的,不能给它赋值。

如果想对字符数组重新赋值,可以使用strcpy函数。如果想对整数数组进行赋值,可以使用memcpy函数。

二维数组

~ 二维数组实际上就是元素为一维数组的数组,本质上也是一维数组。二维数组名可以看做指向其第一个元素(一维数组)的指针。a[2][3]的第一个元素是a[0],其数组名a是a[0]的地址。

定义:
int a[2][3] = { {1,2,3} , {4,5,6} };
==> int a[2][3] = { 1,2,3,4,5,6 };
==> int a[2][3];  a[0][0]=1;  a[0][1]=2;  a[0][2]=3;  a[1][0]=4;  a[1][1]=5;  a[1][2]=6;
~ 此时如果 printf("%d\n", a[0][4]); // 5

int a[2][3] = { {1,2,3} , {} };
==> int a[2][3] = { 1,2,3 };
==> int a[2][3];  a[0][0]=1;  a[0][1]=2;  a[0][2]=3;  a[1][0]=0;  a[1][1]=0;  a[1][2]=0;

for(int i = 0; i < 2; i++)
  for(int j = 0 ; j < 3; j++)
    printf("%d\n", a[i][j]);

int **

int ** 是一个指向整型指针的指针;也可以用来表示一个指向指针数组的指针变量;也可以理解为是一个二维数组。

int a = 1;
int * b = &a;
int * c = &b;
printf("%d \n", b);    // -1651491900
printf("%d \n", *c);   // -1651491900
// 指针指向二维数组
int a[2][3] = { {1,2,3} , {4,5,6} };
int* ptr[3] = { a[0] , a[1] , a[2] };
int** ptr_ptr = ptr;
printf("%d %d %d \n", a[0][1] ,ptr[0][1], ptr_ptr[0][1]); // 2 2 2
char a[10][10] = { "jxw1" , "jxw2" };
char* ptr[10] = { a[0] , a[1] };
char** ptr_ptr = ptr;
printf("%s \n%s \n%s \n", a[1], ptr[1], ptr_ptr[1]); // jxw2 jxw2 jxw2

数组指针

int (*p)[4]  是一个数组指针,也叫行指针,是指向数组的指针(不是指向数组首元素地址),本质上是一个指针。后面的[4]不代表他有4个元素。
() 的优先级更高,说明p是一个指针,指向一个整型的一维数组,如果这个一维数组的长度是n,则p+1 时,p要跨过n个整型数据的长度。数组首元素地址的指针。
int (*p)[4] 中:p 即为指向数组的指针,又称数组指针 。

eg: 一维数组指针:

int arr[4] = { 1, 10, 20, 30} ;
// 将arr看做一个只有一个元素的二维数组,该二维数组的元素是 int[4] 类型的
int * p = arr;
int (*p_arr)[4] = &arr; 
printf("%d\n" , p[0]);       // 1
printf("%d\n" , *arr);       // 1
printf("%d\n" , *p_arr[0]);  // 1
printf("%d\n" , *p_arr );   // 1731406400,p_arr[0] 相当于 *p_arr 相当于 arr
printf("%d\n" , p_arr[0] ); // 1731406400
printf("%d\n" , arr );      // 1731406400
printf("%d\n" , arr[1]);       // 10
printf("%d\n" , *(p+1) );      // 10
printf("%d\n" , p[1] );        // 10
printf("%d\n" , (*p_arr)[1] ); // 10
printf("%d\n" , p_arr[0][1] ); // 10

eg : 二维数组指针:
二维数组指针:int (*p)[5] :p是一个指针,它指向包含5个int元素的一维数组,此时p的增量以它所指向的一维数组长度为单位。*(p+i) 是 a[i][0] 的地址,*(p+2)+3 是 a[2][3] 的地址, *( *(p+2)+3 ) 表示 a[2][3] 的值。

int arr[3][4] = { {10,20,30,40} , {50,60,70,80} , {90,100,110,120} } ;
int (*p_arr)[4] = arr; // 等价于 int* p_array = &a[0],此时p_arr和arr等价。
// p_arr是指向一维数组的数组指针,此时p_arr是arr数组的第一个元素(该元素是一维数组)的地址
printf("%d %d\n" , p_arr, arr);       // 1770543424  1770543424
printf("%d %d\n" , &p_arr[0], &arr[0]);     // 1770543424  1770543424
printf("%d %d\n" , *p_arr, *arr);     // 1770543424  1770543424
printf("%d %d\n" , p_arr[0], arr[0]); // 1770543424  1770543424
printf("%d %d\n" , &p_arr[0][0], &arr[0][0]); // 1770543424  1770543424
// 上面 10 个完全等价,*arr相当于arr[0],arr 相当于 &arr[0] 相当于 &arr[0][0]

printf("\n%d %d\n" , **p_arr, **arr); // 10  10
printf("%d\n" , p_arr[1][1]);    // 60
printf("%d\n" , arr[1][1]);      // 60
printf("%d\n" , *p_arr[0]);        // 10
printf("%d\n" , *( (*p_arr) + 1 ) ); // 20

printf("\n%d %d %d\n" , p_arr[0][9] , (*p_arr)[9] , *(*p_arr + 9) );
// (*p) + 1 == &a[0][1], *((*p) + 1) == a[0][1]
printf("\n%d %d %d\n" , (*(p_arr+2))[1] , (p_arr+2)[0][1] , ((p_arr+1)[1])[1] ); // (p_arr+1)[1] 等价于 p_arr[2]
printf("\n%d %d \n" , *(*(p_arr+2)+1) , (*(p_arr+2))[1] );
// 上面这些都是100,全部等价


 

指针数组

int *p[3]  是一个指针数组,(看名称后缀是个数组)数组中每个元素都是指针。eg:

int *p[3];
int *a0, *a1, *a2;
int b0 = 10, b1 = 11, b2 = 12;
a0 = &b0; a1 = &b1; a2 = &b2;
p[0] = a0; p[1] = a1; p[2] = a2; // 注意 p 是怎么写的。
printf("%d %d %d \n", *p[0], *p[1], *p[2]); // 10 11 12

int *k[10][30]; // 也是指针数组
printf("%d\n", sizeof(k)); // 2400

函数

调用有参函数:eg:

int main() {
 float max(float x, float y);
 float a[4] = { 1.1, 2.2, 4.4, 3.3 };
 printf("%f\n", max( max(a[0],a[1]) , max(a[2], a[3]) ) );
 return 0;
}
float max(float x, float y) {
 return x>y?x:y;
}

调用无参函数:不用 return :eg:

void a() {
 printf("aaa\n");
}
int main() {
 a();
}

没返回值的函数是否需要return:没有返回值的return语句只能用在返回类型是void的函数中。返回void的函数不要求非得有return语句,因为在这类函数的最后一句后面会隐式执行return。
通常情况下,void函数如果想在它的中间位置提前退出,可以使用return语句。return的这种用法有点类似于我们用的break语句退出循环。

函数的嵌套,函数里调用另一个函数

int max2 (int a, int b) {
    return (a>b?a:b);
}

int max4 (int a, int b, int c, int d) {
    return max2(max2(a,b), max2(c,d));
}

//找4个数里的最大值:
int main () {
    int a, b, c, d, zuida;
    scanf("%d, %d, %d, %d", &a, &b, &c, &d);
    zuida = max4(a,b,c,d);
    printf("%d\n", zuida);
    return 0;
}

函数的递归:必须有初值:

int jiecheng(int n) {
 int jieguo;
 if(n < 0)
  printf("n<0, input is wrong");
 else if(n==0 || n==1)
  jieguo = 1;
 else
  jieguo = jiecheng(n-1) * n;
 return jieguo;
}
int main() {
 printf("%d\n", jiecheng(4));
}

主函数和调用的函数中定义了同名变量:

// 在主函数中用函数定义的变量,在被调用的函数中用该函数中的变量

void max(int x) {
 int jxw = 0;   //如果把这句删了,则报错。即从函数调用不到主函数中定义的变量,如果不传参数的话
 printf("%d\n",jxw);
}
int main() {
 int jxw=10;
 max(1);   //输出0
 return 0;
}

传递一个数组:

// eg: 给10个数从小到大排序。
int main() {
    void paixu(int a[], int n);
    int a[10] = {5,6,7,8,9,0,1,2,3,4};
    paixu(a, 10);
    for(int i = 0; i<=9; i++)
        printf("%d",a[i]);
    printf("\n");
    return 0;
}
void paixu(int a[], int n) {
    int t;
    for(int i = 0; i < n-1; i++)
        for(int j = i+1; j<n; j++)
            if(a[i] > a[j]) {
                t=a[i]; a[i]=a[j]; a[j]=t;
            }
}
//也可以:
static void itoa(int num, char str[100]) {
    // 上面参数列表里的数组:把100删了也可以
    ***  
}
int main() {
        int num =12345678;
        char str[100];
        itoa(num, str);
}

指针    // 2021.4.29 开始学的

指针 = &变量。
*指针 = 指针指向变量的值。
指针类型与指定的变量一样。
交换两个指针:需要再定义一个新指针。

int a = 5;
int * p = &a; // 注意声明时就赋值的格式
printf("%d\n", *p);

int a = 5, * p;
p = &a; //  * p 相当于 a,p 相当于 &a 。
*p = 99;
printf( "%d %d \n" , *p, a ); // 99 99 。改指针后,指针指向的变量也改变了

eg:
int a = 5, b = 10, * p, * q, * r;
p = &a; q = &b;
r = p; p = q; q = r; // 现在p指向b,q指向a。a和b的值没变。
printf("%d %d \n", *p, *q); // 10 5
printf("%d %d \n", a , b ); // 5 10
*p = 99; *q =100;
printf ( "%d %d \n", *p, *q); // 99 100
printf ( "%d %d \n", a , b ); // 100 99

// 不能直接 int *p; *p = 1;    ,会报错。

空指针,野指针

空指针:

int *a = NULL;

NULL在C/C++中定义为:
#ifdef _cplusplus        // 定义这个符号就表示当前是C++环境
#define NULL 0    // 在C++中NULL就是0
#else
#define NULL (void *)0 // 在C中NULL是强制类型转换为void *的0
#endif

空指针就是指向地址为0的地方。

野指针:

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。

野指针的错误来源就是指针定义了以后没有初始化,也没有赋值(总之就是指针没有明确的指向一个可用的内存空间),然后去解引用。因此个人推荐大家一般常用的方法:

第一点:定义指针时,同时初始化为NULL

第二点:在指针解引用之前,先去判断这个指针是不是NULL

第三点:指针使用完之后,将其赋值为NULL

第四点:在指针使用之前,将其赋值绑定给一个可用地址空间

linux中   ERR_PTR、PTR_ERR、IS_ERR、IS_ERR_OR_NULL

这几个是 linux内核中判断返回指针是否错误的内联函数。

        理解IS_ERR(),首先理解要内核空间。所有的驱动程序都是运行在内核空间,内核空间虽然很大,但总是有限的,而在这有限的空间中,其最后一个page是专门保留的,也就是说一般人不可能用到内核空间最后一个page的指针。换句话说,你在写设备驱动程序的过程中,涉及到的任何一个指针,必然有三种情况:
1,有效指针;
2,NULL,空指针;
3,错误指针,或者说无效指针。
  所谓的错误指针就是指其已经到达了最后一个page,即内核用最后一页捕捉错误。比如对于32bit 的系统来说,内核空间最高地址0xffffffff,那么最后一个page就是指的0xfffff000~0xffffffff(假设4k一个page),这段地址是被保留的。
  内核空间为什么留出最后一个page?我们知道一个page可能是4k,也可能是更多,比如8k,但至少它也是4k,所以留出一个page出来就可以让我们把内核空间的指针来记录错误号了。内核返回的指针一般是指向页面的边界(4k边界),即 ptr & 0xfff == 0 。如果你发现你的一个指针指向这个范围中的某个地址,那么你的代码肯定出错了。
        IS_ERR() :判断指针是否有错,是的话返回 true,不是的话返回0。如果指针并不是指向最后一个page,那么没有问题;如果指针指向了最后一个page,那么说明实际上这不是一个有效的指针,这个指针里保存的实际上是一种错误代码。
        IS_ERR_OR_NULL() :判断指针是空指针或是错误指针,是的话返回 true,不是的话返回0。

常用的方法就是先用 IS_ERR(指针p) 或 IS_ERR_OR_NULL(指针p) 来判断是否是错误,然后如果是,那么就调用 PTR_ERR(指针p) 来返回。例如:
if (IS_ERR_OR_NULL(ext_bridge)) {
    rc = PTR_ERR(ext_bridge);
    pr_err("failed to find ext bridge\n");
    goto error;
}

include/asm-generic/errno-base.h文件里有很多错误号,如:
#define EINVAL 22         /* Invalid argument */ //无效参数
最常见的几个是-EBUSY,-EINVAL,-ENODEV,-EPIPE,-EAGAIN,-ENOMEM.这些是每个体系结构里都有的,另外各个体系结构也都定义了自己的一些错误代码.这些东西当然也都是宏,实际上对应的是一些数字,这个数字就叫做错误号. 对于Linux内核来说,不管任何体系结构,最大的错误号不会超过4095.而4095又正好是比4k小1,即4096减1.而我们知道一个page可能是4k,也可能是更多,比如8k,但至少它也是4k,所以留出一个page出来就可以让我们把内核空间的指针来记录错误了。

malloc,calloc,realloc,free

C 库函数 malloc() :是一个函数。
分配一块连续的内存,返回值是一个指向已分配大小的内存的指针,指向一段可用内存的起始地址。
声明: void *malloc(size_t size)  
size 为 内存块的大小,以字节为单位; 
malloc 分配的内存大小至少为size参数所指定的字节数。
eg: 
int * ret = malloc( sizeof(int) * 2 );         // 构建一个整数数组,只有2个元素

free() :释放之前调用 calloc、malloc 或 realloc 所分配的内存空间。
声明:void free(void *ptr)
ptr :指针指向一个要释放内存的内存块,该内存块之前是通过调用 malloc、calloc 或 realloc 进行分配内存的。如果传递的参数是一个空指针,则不会执行任何动作。该函数没有返回值。

char *str;
str = (char *) malloc(15);
strcpy(str, "runoob");
printf("%s,\t,%u\n", str, str);
free(str);

calloc:
void *calloc( size_t num, size_t size );
参数:
num: Number of elements    元素有多少个
size: Length in bytes of each element   每个元素有多长
calloc函数会自动把数据都存为0 。

// int* p = (int*)malloc( sizeof(int) * 10 );
int* p = (int*)calloc(10, sizeof(int));  // 这样10个整数就都初始化为0
free p;

realloc:
原型:extern void *realloc(void *mem_address, unsigned int newsize);
mem_address 指向之前已分配内存块的指针。
newsize 是新大小(字节)。
返回值:失败返回空指针NULL,成功返回 新分配内存空间的首地址。
头文件:#include <stdlib.h> 有些编译器需要#include <malloc.h> 
新的大小可大可小(如果新的大小大于原内存大小,则新分配部分不会被初始化;如果新的大小小于原内存大小,可能会导致数据丢失。

kmalloc

kmalloc和kfree需包含:#include <linux/slab.h>

kmalloc 语法,void *kmalloc(size_t size, int flags);

void kfree(void *ptr);

参数:size : 要分配内存的大小. 以字节为单位。flags : 要分配内存的类型。

kfree在内核源码中的位置: ap\kernel\msm-5.4\mm\slab.c  :  void kfree(const void *objp)

在设备驱动程序或者内核模块中动态开辟内存,不是用malloc,而是kmalloc, vmalloc, 或者用get_free_pages直接申请页。释放内存用的是kfree, vfree, 或free_pages。 kmalloc函数返回的是虚拟地址(线性地址)。 kmalloc特殊之处在于它分配的内存是物理上连续的。而用vmalloc分配的内存只是线性地址连续,物理地址不一定连续,不能直接用于DMA。

kmalloc最大只能开辟128k-16,16个字节是被页描述符结构占用了。内存映射的I/O口,寄存器或者是硬件设备的RAM(如显存)一般占用F0000000以上的地址空间。在驱动程序中不能直接访问,要通过kernel函数vremap获得重新映射以后的地址。另外,很多硬件需要一块比较大的连续内存用作DMA传送。这块内存需要一直驻留在内存,不能被交换到文件中去。但是kmalloc最多只能开辟大小为 32*PAGE_SIZE 的内存,一般的PAGE_SIZE=4kB,也就是128kB的大小的内存。

指针与一维数组

int *p, *q, a[3] = {5, 12, 15};
p = &a[0];
q = &a[1];
*p = *p*2; // *p=10=a[0]
*(q+1) = *p*2; // a[2]=20
printf("%d\t%d\n", a[0], a[2]); // 10 20
printf("%d\n", *a); // 10
printf("%d\n", *(a+1) ); // 12
printf("%d\t%d\n", p[0], q[0]); // 10 12

p 相当于 p+0 相当于 &a[x]  相当于 a+x
p + i 相当于 &a[x+i] 。
a[i] 相当于 *(a+i)
p 和 a 是完全等价的。

*p 相当于 a[0]

   int * p, a[3]={2,10,15};
   p = a;  // 数组名 a 就是数组首元素地址&a[0], 数据类型是 int*
   *p = *p*2;  // 指针解引用的优先级高于乘法
   printf("%d %d %d %d\n", *p, *a, p[0], a[0]); // 都是4
   printf("%d %d\n", a, &a[0]);
   printf("%d %d\n", p, &p[0]); // 上面4个输出都一样,都是数组首元素地址
// a, &a[0], p, &p[0] 的数据类型都是 int*,用%d输出会报warning

   int *q = &a[1];
   printf("%d\n", *q); // 10
   printf("%d\n", *(q-1)); // 4
   printf("%d\n", q[1]); // 15

void *

void * 表示未确定类型的指针,可以强制转换为其他任意类型的指针。eg:

void *p1;
int *p2;
p1 = p2;        // 就是说其他任意类型的指针都可以直接赋值给它,但反过来不行。

有各种问题,比如不能 printf(" %d\n ", *p1); ,不要轻易使用。

函数的参数传递:值传递、指针传递

假设要传递的是 int a = 1。值传递:只是传递了一个变量的名字 a ,函数只拿到了 1 这个值,但不知道它的地址。

 指针函数

是个函数,这个函数返回一个指针。eg:

int a = 10;
int* func() {        // 要注意不能写成 (*func)() ,否则就变成函数指针了。这里其实是 int* func(),有时候会写成:int *func()。
	return &a;
}
int main() {
	int *p = func();    // func的返回值是一个指针,要用相同类型的指针接收返回值
	printf("%d\n", *p);   // 10
	return 0;
}

函数指针

函数是有其地址的,调用函数就是跳转到函数的位置执行。因此可以用指针来保存函数的内存地址。

函数指针 是 指针变量,是指向函数的指针变量。通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数。函数指针可以像一般函数一样,用于调用函数、传递参数。C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。

eg: int (*p) (int, int);    首先它是一个指针变量,所以要有一个"*",即 (*p);前面的int表示这个指针变量可以指向返回值类型为int的函数;后面的2个int表示这个指针变量可以指向有两个参数且都是int型的函数,参数列表只写参数类型也可以

注意这里函数指针两边的括号不能省略,否则改变了运算符的优先级。函数指针和指针函数格式上的区别就在这里:函数指针要把 * 和函数名用括号括起来

函数指针变量声明:返回值类型 (*指针变量名) (参数列表)。

eg:
int Func(int x);           // 声明一个函数 
int (*p) (int x);            // 定义一个函数指针,这里x可以省略(一般不省略)
p = Func;                  // 将Func函数的首地址赋给指针变量p。赋值时函数 Func 不带括号,也不带参数列表(遇到这种情况就是函数指针)。由于函数名 Func 代表函数的首地址,因此经过赋值以后,指针变量 p 就指向函数 Func() 代码的首地址了。p的返回值和Func相同。调用 Func(x) 就可以写为 p(x) :eg: 

int func(int x, int y) {
  return x > y ? x : y;
}
int main() {
  int a=1, b=3;
  int (*p) (int , int);
  p = func;
  printf(" %d\n", func(a,b) );   // 3
  printf(" %d\n", p(a,b)    );   // 3
  return 0;
}

指针变量与一维数组相关函数

(主要看参数怎么传递)

float aver(float a[], int n) {
 float sum = a[0], pingjunshu;
 for ( int i = 1; i<n ; i++ )
  sum = sum + a[i];
 pingjunshu = sum / n;
 return pingjunshu;
}
int main() {
 float zu[5];
 float * p; p = zu;
 zu[0]=0; zu[1]=1; zu[2]=2; zu[3]=3; zu[4]=4.5;
 printf("aver is %f \n" , aver(zu , 5)); // 注意格式
 printf("aver is %f \n" , aver(p , 5));
 return 0;
}

结构体

去看该网站:https://www.runoob.com/cprogramming/c-structures.html 上面的入门写得很详细,比本笔记写得好。

C语言的结构体没有存取控制权限,相当于C++存取控制权限中的public:

互换结构体:要再定义一个结构体。

定义结构体:

结构体定义后面必须加 ;

struct student {
 int xuehao;
 char name[30];
 char xingbie;
}; // 注意这里最后有个分号
struct student a = { 004 , "HouGe" , 'M' }; // 此处前面的struct不能省略!
printf("%s\n", a.name);
struct student {
 int xuehao;
 char name[30];
 char xingbie;
}a = { 004 , "HouGe" , 'M' }; 
// 和其它类型变量一样,对结构体变量可以在定义时指定初始值。
printf("%c\n", a.name[1]); // o
struct student {
 int xuehao;
 char name[30];
 char xingbie;
};
struct student b = { 2 };       
// 赋初值可以只给第一个元素赋值,但不能只给其他元素赋值,否则编译error
printf(" xuehao:%d\n name:%s\n xingbie:%c\n", b.xuehao, b.name, b.xingbie);
printf("%ld\n", sizeof(struct student)); // 36
// 实际使用时的常见格式:
struct jxw {
  int aaa;
  double bbb;
};
struct jxw jxw1 = {
  .aaa = 10,     // 这里可以写成 " 10 " ,也可以写成 " 10, "
  .bbb = 2.2,
};
printf("%d\n%f\n", jxw1.aaa, jxw1.bbb);

//此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c;同时又声明了结构体变量s1;
//这个结构体没有标签
struct  {
    int a;
    char b;
    double c;
} s1;

struct SIMPLE {
    int a;
    char b;
    double c;
};
struct SIMPLE  t1, t2[20], *t3;         // 声明了变量t1、t2、t3。
//在上面的2个结构体的声明中,第一个和第二声明被编译器当作两个完全不同的类型,即使他们的成员列表是一样的,如果令 t3=&s1 或 t1=s1,则是非法的。

//也可以用typedef创建新类型(实际最常用)
typedef struct {
    int a;
    char b;
    double c; 
} Simple2;
// 现在可以用Simple2作为类型声明新的结构体变量,且可以不用加struct。
Simple2 u1, u2[20], *u3;

也可以写成:
typedef struct DtInfo {
    int a;
    char b;
    double c; 
} DtInfo;

//此结构体的声明包含了其他的结构体
struct COMPLEX {
    char string[100];
    struct SIMPLE a;
};

//此结构体的声明包含了指向自己类型的指针
struct NODE {
    char string[100];
    struct NODE *next_node;
};

//如果两个结构体互相包含,则需要对其中一个结构体进行不完整声明:
struct B;    //对结构体B进行不完整声明
struct A {
    struct B *partner; //结构体A中包含指向结构体B的指针
};
struct B {
    struct A *partner; //结构体B中包含指向结构体A的指针,在A声明完后,B也随之进行声明
};

结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙):
struct student {
 int xuehao;
 char xingbie;
}A;
printf("%ld\n", sizeof(A) ); // 8

结构体成员后缀 : num   , 位域

typedef unsigned char u_char;
struct frame_control {
 u_char fc_subtype : 4;        //低位
 u_char fc_type : 2;
 u_char fc_protocol_version : 2;    //高位

 u_char fc_order : 1;
 u_char fc_wep : 1;
 u_char fc_more_data : 1;
 u_char fc_pwr_mgt : 1;
 u_char fc_retry : 1;
 u_char fc_more_frag : 1;
 u_char fc_from_ds : 1;
 u_char fc_to_ds : 1;
};

1表示这个成员的大小占所定义类型的 1bit,: 2 占 2bit。
一个bytes(字节)是8 bit(bit),unsigned char 是一个字节,共8bit。
fc_subtype占了4bit,fc_type占2bit,fc_protocol_version占2bit,共8bit,正好是一个字节。其他八个成员,各占1bit,共8bit,正好也是一个字节。因此你的结构的大小如果用sizeof(struct frame_control)计算,就是 2bytes

结构体数组(一维数组)

struct student {
 int xuehao;
 char name[30];
 char xingbie;
};
struct student xuesheng[3] = { { 0 , "HouGe" , 'M' } , { 11 , "jxw" , 'M' } , { 22, "jxd", 'M'} };
printf("%s\n", xuesheng[1].name); // jxw

结构体 作为函数参数

eg: 结构体数组作为函数参数:

struct student {
	float fen1;
	float fen2;
}sheng[2];
int main(){
 struct student add(struct student a[]);
 struct student zonghe;
 sheng[0].fen1 = 1; sheng[0].fen2 = 2;
 sheng[1].fen1 = 3; sheng[1].fen2 = 4;
 zonghe = add(sheng);
 printf("fen1 = %f \n fen2 = %f \n" , zonghe.fen1, zonghe.fen2);
 return 0;
}

struct student add(struct student a[]) { // 注意格式
 struct student ret;
 ret.fen1 = a[0].fen1 + a[1].fen1;
 ret.fen2 = a[0].fen2 + a[1].fen2;
 return ret;
}

结构体指针

struct student {
    int xuehao;
    char name[30];
} a = {001, "jxw"};
int main() {
    struct student *p;
    p = &a;
    printf("学号:%d\t 姓名:%s\n", a.xuehao, a.name);
    printf("学号:%d\t 姓名:%s\n", p->xuehao, p->name); return 0;
}

结构体指针不能定义后就直接赋值,如上面例子中,struct student *p 后,不能直接
p->xuehao = 9; 
也不能直接
printf("%d\n", p->xuehao); 
否则:编译能通过,执行后报错:Segmentation fault (core dumped)。

enum 枚举

用途:有时define会过多:#define one 1 ......  #define ninety 90

格式:eg:
enum week{ Mon, Tues, Wed, Thurs, Fri, Sat, Sun };    
    // 上面最后一个值Sun后面可以加个逗号,没有给出名字对应的值,默认从0开始,往后依次加1,即Mon=0, Tues=1....
enum week{ Mon = 1, Tues = 2, Wed = 3, Thurs = 4, Fri = 5, Sat = 6, Sun = 7 };
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };
    // 只给第一个名字的值:枚举从1开始递增;可以在定义枚举类型时改变枚举元素的值;没有指定值的枚举元素,其值为前一元素加1。

枚举是一种类型,通过它可以定义枚举变量,例如上面的week:
enum week a, b, c;

可以在定义枚举类型的同时定义变量:
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a, b, c;

省略枚举名称,直接定义变量:
enum { Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a;
不定义变量a、光一个 enum { ... }; 也可以,可以直接用 {} 里面的东西。

定义枚举后,不能在包括定义枚举的函数内再定义与枚举内部各个名字相同的变量,即Mon等。
上面枚举Mon, Tues等都是常量,不能赋值;且因为不是变量,所以不占用数据区(常量区、全局数据区、栈、堆)的内存,而是直接被编译到命令里面,放到代码区,因此也不能用&获得它们的地址。
宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。

//eg:
enum DAY {
  A = 1, B = 5, C, D,
};
printf("%d", D);   // 7

共用体 union

共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。

结构体的各个成员占用不同的内存,互相没有影响。而共用体的所有成员占用同一片内存,修改一个成员会影响其余所有成员。

共用体占用的内存等于最长的成员占用的内存。 

union data {
 int n;
 char ch;
};
union data a, b, c;
union data {
 int n;
 char ch;
} a;
// data 和 a 可以并存或者只存在一个,如果没有data,则表示这是一个匿名共用体,那么接下来程序中就无法使用该共用体来进行变量声明。

printf("%d\n", &a); // 2121365956
printf("%d\n", &a.n); // 2121365956
printf("%d\n", &a.ch); // 2121365956
// 变量a与其各成员的地址都相同
// 如果不再定义新的变量,也可以将共用体的名字省略
union {
 int n;
 char ch;
} a, b, c;

// 要解决匿名共用体只能在定义的时候声明变量,无法在后续的程序中再次声明变量的问题,可以通过 typedef 关键字来自定义数据类型的名称。
typedef union {
 int n;
 char ch;
} A;
// 这段代码不但验证了共用体的长度,还说明共用体成员之间会相互影响,修改一个成员的值会影响其他成员。
union data {
 int n;
 char ch;
 short m;
};
union data a;
printf("%ld, %ld\n", sizeof(a), sizeof(union data) ); // 4, 4
a.n = 0x40;
printf("%X, %c, %hX\n", a.n, a.ch, a.m); // 40, @, 40
a.ch = '9';
printf("%X, %c, %hX\n", a.n, a.ch, a.m); // 39, 9, 39
a.m = 0x2059;
printf("%X, %c, %hX\n", a.n, a.ch, a.m); // 2059, Y, 2059
a.n = 0x3E25AD54;
printf("%X, %c, %hX\n", a.n, a.ch, a.m); // 3E25AD54, T, AD54
// 不能直接 printf("%d\n" , a);

成员 n、ch、m 在内存中“对齐”到一头,对 ch 赋值修改的是前一个字节,对 m 赋值修改的是前两个字节,对 n 赋值修改的是全部字节。也就是说,ch、m 会影响到 n 的一部分数据,而 n 会影响到 ch、m 的全部数据。

eg: float 和 int 在内存中存储的二进制是不同的,因此放入同一个共用体后会导致值不一样:

union data {
 int d1;
 float d2;
} a;
a.d1 = 10;
printf("%f\n", a.d2); // 0.000000
a.d2 = 10;
printf("%d\n", a.d1); // 1092616192

~ 共用体不能在定义时对变量中的成员进行初始化赋值,如:

union A{
 int i;
 char c;
} a = {3, 'c'};
printf("%c", a.c); // 输出不是c,而是空字符\0

~ union 指针

union A{
 int ccc;
};
union A jxw;
union A * jxwp;
jxw.ccc = 9;
jxwp = &jxw;
printf("%d\n", jxwp->ccc);

安卓LINUX 内核 相关代码的知识点(包括一些API)

备注、MISC知识点

kernel\msm-5.4\lib\string.c 中的 strnstr 函数:好像是高通自带的,不是C语言原生的,该文件里有实现过程。

声明数组时要立即赋初值 0 ,全部都要初始化。

内核代码不能进行浮点算术。
内核驱动不允许定义了变量但未使用

#define IN
#define OUT
上面的 IN  OUT : 都只是简单 #define了一下,没有值,它们的作用就是纯粹说明性的,如IN就描述某参数的作用是传入,它们对编译没有什么影响。

EFI_STATUS EFIAPI  UefiMain (IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable) 上面 EFIAPI 我记得也是说明性的。

struct device

struct device {
 ***
 void        *platform_data;    /* Platform specific data, device core doesn't touch it */
 void        *driver_data;    /* Driver data, set and get with dev_set/get_drvdata */
 struct device_node    *of_node;    //Associated device tree node. 关联的设备树节点。
}

struct mutex

互斥体。是内核定义的一种结构体。

互斥体是一种睡眠锁,他是一种简单的睡眠锁,其行为和 count 为 1 的信号量类似。(关于信号量参考:Linux 内核同步(四):信号量 semaphore)。

互斥体简洁高效,但是相比信号量,有更多的限制,因此对于互斥体的使用条件更加严格。

1. 初始化互斥体 -- mutex_init(); // mutex使用之前都需要先init

2. 获得互斥体 -- mutex_lock();

3. 释放互斥体 -- mutex_unlock();

mutex_lock 是一种多线程编程中的同步机制,用于确保同一时间只有一个线程可以访问共享资源,避免多个线程同时修改数据造成数据不一致问题。mutex_lock 的主要功能是在一个线程访问共享资源时,它会锁住这个资源,直到这个线程完成对资源的操作并释放锁,其他线程才能访问该资源。mutex_lock 在多线程编程中用于保护临界区,确保线程安全。
mutex_lock 是一种线程同步机制,用于确保在多个线程访问共享资源时,只有一个线程能够同时访问该资源。当某个线程调用 mutex_lock 函数时,它会尝试获得一个互斥锁(mutex),如果这个锁当前没有被其他线程持有,它将成功获取该锁并且可以访问共享资源;如果这个锁已经被其他线程持有,该线程将被阻塞,直到锁被释放为止。

printk

C语言的标准函数库不能使用:驱动属于内核的一部分,我们此时还无法使用类似像printf这样的C库,但是内核会提供自己的C库,在内核中我们用printk代替printf函数。printk()是内核空间的,printf()是用户空间的。printk靠自己运行,没有C库的帮助,模块能调用printk是因为,在insmod加载了它之后,模块被连接到内核且可存取内核的公用符号(函数和变量)。有8种记录字串,在头文件<linux/kernel.h>中定义,按照严重性递减的顺序列出它们:

KERN_EMERG    :用于紧急消息,常常是那些崩溃前的消息
KERN_ALERT    :需要立即动作的情形
KERN_CRIT        :严重情况,常常与严重的硬件或软件失效有关
KERN_ERR        :用来报告错误情况,报告硬件故障;
KERN_WARNING    :有问题情况的警告,这些情况自己不会引起系统的严重问题;
KERN_NOTICE    :正常情况,但是仍然值得注意,这个级别一些安全相关的情况会报告。
KERN_INFO        :信息型消息。在这个级别,很多驱动在启动时打印它们发现的硬件的信息。
KERN_DEBUG    :用于调试消息。

以上每个字串(在宏定义扩展里)代表一个在角括号中的整数,整数范围从0到7,越小的数表示越大的优先级。没有指定优先级的printk语句缺省的是DEFAULT_MESSAGE_LOGLEVEL,在2.6.10内核中是KERN_WARNING。

EFI_D_ERROR可以输出,并不是对应数字越大越可以输出,EFI_D_INFO可以输出,EFI_D_INIT、***BM不能输出。

__func__  打印出当前printk语句所在的函数名称;
__LINE__  打印出当前printk语句所在的行号。
__FILE__  打印源代码文件名。

eg:  printk(KERN_EMERG "%s __jxw__enter.....\n", __func__); // 优先级后面不加 ","

~ pr_err("***%d\n", aaa);    更好,可以把这句log所在的函数也给打印出来。

怎么在用户空间修改printk函数的记录级别:

先说printk,printk和用户空间的printf函数格式完全相同,printk打印的字符串头部可以加入“ <n> ”样式的字符,表示这条信息的记录级别。n为0~7。0~7 分别代表什么等级在 kernel\msm-4.19\include\linux\kern_levels.h 里。也可以写成:printk(KERN_EMERG " jxw \n",);

挂载proc文件系统后,cat /proc/sys/kernel/printk 后默认是:6       6       1       7

这四个数据分别对应:
~ console_loglevel   :控制台日志级别。如果该值是7,则意味着只有优先级的数字小于 7(KERN_DEBUG)的打印消息才能输出到终端。对于 printk("<n>"...);  ,只有n小于 console_loglevel ,这个信息才会被打印。
~ default_message_loglevel    :未指定日志级别的printk() 采用的默认级别:用该优先级来打印没有优先级的消息,printk的默认级别。如果printk没有指定级别,这个值是4,则printk在处理时会自动加上<4>.
~ minimum_console_loglevel   :最低的控制台日志级别。是个预设值,平时不起作用,通过其他工具来设置 console_loglevel  的值时,这个值不能小于 minimum_console_loglevel   。
~ default_console_loglevel    :默认的控制台日志级别。也是个预设值,平时不起作用,表示设置console_loglevel 时的默认值。

上面这四个值是在 kernel/msm-4.19/include/linux/printk.h 和  kernel/msm-4.19/kernel/printk/printk.c 里定义的:
int console_printk[4] = {
    CONSOLE_LOGLEVEL_DEFAULT,    /* console_loglevel */
    MESSAGE_LOGLEVEL_DEFAULT,    /* default_message_loglevel */
    CONSOLE_LOGLEVEL_MIN,             /* minimum_console_loglevel */
    CONSOLE_LOGLEVEL_DEFAULT,    /* default_console_loglevel */
};

extern int console_printk[];
#define console_loglevel (console_printk[0])
#define default_message_loglevel (console_printk[1])
#define minimum_console_loglevel (console_printk[2])
#define default_console_loglevel (console_printk[3])

想屏蔽掉所有的内核printk打印,需要把第一个数值调到最小值1或者0,比如:
# echo 1 4 1 7 > /proc/sys/kernel/printk             // 这使将 console_loglevel 被改为1,于是所有的printk信息都不会被打印。
# echo 0 4 0 7 > /proc/sys/kernel/printk
这操作不需要remount就可以。

Linux内核使用的字符串转整形函数kstrtouint

还有 kstrtos32

在uefi打印串口log的API

ap下显示串口log:DEBUG

bp下显示串口log:(有好几种)
1.boot_log_message("jxwXBLRamDumpMain");       (这个自带句末换行。但有时不能正常使用,include一些东西和修改.inf文件也没用)(定义在 bp\bp\boot_images\QcomPkg\XBLLoader\boot_logger.c)
2.MDP_Log_Message(MDP_LOGLEVEL_ERROR, "_____jxw_____\n");
3.性能最强的:#include <Library/DebugLib.h> 之后:DEBUG((EFI_D_WARN,"__%d__", XXX));
4. bp\boot_images\QcomPkg\Library\PmicLib\app\pon\src\pm_app_key_press.c 里 pm_log_message("KeyPressd for %d msec was invalid, release_time %d, press_time %d \n\r", debounce_time, release_time, press_time);
5. ufs_error_log()    :boot_images\QcomPkg\Library\UfsCommonLib

memset

void *memset(void *s, int ch, size_t n);
(typedef unsigned int size_t ,size_t 一般就是 int)

使用这个需要 #include<string.h>

功能:将指针 / 数组 s 中当前位置后面的 n 个 字节 用 ch 替换并返回 s 。在一段内存块中填充 ch,它是对较大的结构体 / 数组清零的一种最快方法,ch 一般是 0 / -1(补码是32个1)。

ch 的实际范围应该在 0-255,因为 memset 只能取 ch 的后八位,只有后八位是有效的。

eg :下面为什么赋值 1 后 a 变成了 16843009:因为 memset 是按照 1 个字节(8位)来赋值的,1 就是 0000 0001,则 a = 00000001 00000001 00000001 00000001 = 16843009.

int a = 10;
memset(&a , 0 , sizeof(a));
printf("%d\n", a); // 0
memset(&a , 1 , sizeof(a));
printf("%d\n", a); // 16843009

eg :对于 int a[4];  memset( a , -1 , sizeof(a) ) 和 memset( a , 511 , sizeof(a) ) 所赋值的结果都是一样的,因为 -1 和 511 的二进制补码的后八位都是 1111 1111,所以数组 a 中的每个字节都被赋值为 1111 1111。若 int 占4个字节,则 a[0] 占4个字节,a[0] 的4个字节都被赋值为 1111 1111,则 a[0] = 11111111 11111111 11111111 11111111,即 a[0] = -1 。

eg :int a[4];  memset( a , 1 , 4 ) ,为数组 a 的前4个字节赋值(若int是4个字节),实际输出:16843009, 0, 0, 0  。如果要全部初始化,需要 memset( a , 1 , 16 ) 。不同机器 int 的大小不同,最好用 sizeof 。

memcpy

void *memcpy(void *str1, const void *str2, size_t n)

功能:从存储区 str2 复制 n 个字节到存储区 str1

参数:

  • str1 -- 指向用于存储复制内容的目标数组,类型强制转换为 void* 指针。
  • str2 -- 指向要复制的数据源,类型强制转换为 void* 指针。
  • n -- 要被复制的字节数。

返回值: 该函数返回一个指向目标存储区 str1 的指针。

 整数转字符串:sprintf(这个只了解了基本的用法,还得百度查更完整的知识)

char str[10] ;
sprintf(str, "%d", 123456);
printf("%s\n", str); // 123456
sprintf(str, "%x", 100); // 后面的 100 是10进制,要先把它转为16进制,再转为字符串
printf("%s\n", str); // 64
sprintf(str, "%8d", 4567); // 转为字符串后宽度占8个位置,右对齐
printf("%s\n", str); //     4567(前面有4个空格)

sprintf 函数可以将格式化的数据输出到一个字符串中。sprintf 函数将 int 类型的变量 num 转换为字符串类型,并将结果存储在 char 类型的数组 string 中。在格式化字符串中,%d 表示将整数格式化为十进制整数。在使用 sprintf 函数时需要注意 char 数组的大小,确保其足够存储转换后的字符串。sprintf 只需要 #include <stdio.h> 。

itoa 函数仅在 windows 中可以使用,在linux中没有相应的函数。用于代替itoa的备选方案是sprintf / snprintf。这些是 stdio.h 的一部分。使用itoa要include <stdlib.h>。itoa并不是一个标准的C函数,它是Windows特有的,如果要写跨平台的程序,请用sprintf。是Windows平台下扩展的,标准库中有sprintf,功能比这个更强。

sprintf 的返回值:成功写入的字符串的长度。

sprintf 跟printf 在用法上几乎一样,只是打印的目的地不同而已, 前者打印到字符串中, 后者则直接在命令行上输出。

snprintf()

函数原型:int snprintf(char *str , size_t size , const char *format , ...)

功能:先把参数 “ ... ” 转为 format 表示的格式化字符串,再转为字符串弄到 str 中。
若格式化后的字符串长度 < size,则将此字符串全部复制到 str 中,并在其后添加一个字符串结束符 '\0' 。
若格式化后的字符串长度 >= size,则只将其中的(size -1)个字符复制到 str 中,并在其后添加一个字符串结束符 '\0' ,返回值是 预计写入的字符串长度。

需要头文件:#include <stdio.h>

函数返回值:若成功则返回“预计写入”的字符串长度,若出错则返回负值(-1),与 sprintf 的返回值不同。

char str[10] = {0};
int nLen = snprintf( str , sizeof(str) , "%ld" , 1234567890123456 );
// int nLen = snprintf( str , sizeof(str) , "%d" , 123456 );
printf("%s\n", str); // 123456789
printf("%d\n", nLen); //16

sprintf(str, "jxw__%d", 123456); // 这样则会在 str 中加入 jxw___ 这个字符串。

~ sprintf() , strcpy() , strcat() 都是存在安全隐患的,其对应的安全版本为:snprintf() , strncpy() , strncat() 。(众所周知,sprintf不能检查目标字符串的长度,可能造成众多安全问题,所以都会推荐使用snprintf。自从snprintf代替了sprintf,相信大家对snprintf的使用都不会少。)

sscanf

从字符串中提取数据。

int sscanf(const char* str, const char* format, ...);

参数:str :要解析的字符串。format:格式化字符串,指定了要匹配的格式规则。 ... :可变参数列表,用于接收解析后的数据。

返回值:返回成功解析的数据项的个数,如果解析失败或没有匹配的数据项,则返回0.

eg: 解析整数:将字符串"42"解析为整数,并将结果存储在变量num中:
int num;
sscanf("42" , "%d" , &num);

eg: 解析浮点数:将字符串"3.14"解析为浮点数,并将结果存储在变量f中:
float f;
sscanf("3.14" , "%f" , &f);

eg: 解析字符串:解析为字符串,并将结果存储在字符数组str中:
char str[20];
sscanf("HelloABC" , "%s" , str);

eg: 解析多个数据:将字符串"10 20"解析为两个整数,并分别存储在变量ab中:
int a , b;
sscanf("10 20" , "%d %d" , &a , &b);

eg: 使用格式限定符:将字符串"10:30"解析为小时和分钟,并将结果存储在变量hoursminutes中:
int hours , minutes;
sscanf("10:30" , "%d:%d" , &hours , &minutes);

~ sscanf函数只会从字符串中解析数据,不会对字符串进行修改。

~ 要确保格式字符串与要解析的数据格式匹配,否则可能会导致解析错误或未定义的行为。

延时

一、mdelay和msleep的本质区别
1、从模块本身分析:
mdelay,属于忙等待函数(相当于for循环),在延迟过程中无法运行其他任务,这个时间是准确的,需要等待多少实际就会等待多少时间;
msleep,属于休眠函数,不会忙等待,时间是不准确的,比如msleep(200),大概率是会等待多于200ms的。

2、从系统角度分析:
mdelay,会占用系统资源,导致其他功能此时也无法使用cpu;
msleep,不会占用cpu资源,当该模块msleep时,其他模块可以占用cpu。sleep函数会让调用的进程进行休眠。

二、udelay,mdelay,ndelay 区别
时间单位不同
udelay:us
mdelay:ms
ndelay:ns
1s = 10^3ms = 10^6us = 10^9ns

EXPORT_SYNBOL, extern, Makefile 用法的一个案例

EXPORT_SYMBOL(符号名);

EXPORT_SYMBOL_GPL(符号名); // 符合名可以是函数,也可以是结构体

EXPORT_SYMBOL 和 EXPORT_SYMBOL_GPL 用于导出符号到内核符号表中(内核符号表可通过 proc/kallsyms 查看,最左边是符号对应的地址),导出的符号可以被其它模块调用,对全部内核代码公开,调用前需要先用extern声明。

通过 EXPORT_SYMBOL 导出的符号可以被包含GPL许可权的模块和不包含GPL许可权的模块调用;

通过 EXPORT_SYMBOL_GPL 导出的符号只能被包含GPL许可权的模块调用,否则会报错。

格式:

1.在 模块函数 定义后使用“ EXPORT_SYMBOL(函数名) ”来声明。

2.在调用该函数的另一个模块中使用extern对之声明。

3.先加载定义该函数的模块,再加载调用该函数的模块。

(参考:https://blog.csdn.net/qq_37858386/article/details/78444168)说明:在模块export_symbol_one中定义函数function_one(void); 在另一个模块export_symbol_two中定义函数function_two(void), function_two(void)里面会调用function_one(void)。代码如下:

/* export_symbol_one.c */
#include<linux/init.h>
#include<linux/module.h>
#include<linux/kernel.h>
static int function_one(void) {
    printk("EXPORT_SYMBOL In Func: %s...\n", __func__);
    return 0;
}
EXPORT_SYMBOL(function_one); // !
static int __init export_symbol_init(void) {
    printk("EXPORT_SYMBOL Module one, Init!\n");
    return 0;
}
static void __exit export_symbol_exit(void) {
    printk("EXPORT_SYMBOL Module one, Exit!\n");
}
module_init(export_symbol_init);
module_exit(export_symbol_exit);
/* Makefile */
obj-m += export_symbol_one.o
KDIR := /home/weifanghai/Android_4.4_git/xunwei/kernel/iTop4412_Kernel_3.0
PWD ?= $(shell pwd)
all:
    make -C $(KDIR) M=$(PWD) modules
clean:
    rm -rf *.o
/* export_symbol_two.c */
#include<linux/init.h>
#include<linux/kernel.h>
#include<linux/module.h>
static int fuction_two(void) {
    extern int function_one(void); // !
    function_one(); // !
    printk("EXPORT_SYMBOL In Func: %s...\n", __func__);
    return 0;
}
static int __init export_symbol_init(void) {
    printk("EXPORT_SYMBOL  Module two, Init!\n");
    function_two();
    return 0;
}
static void __exit export_symbol_exit(void) {
    printk("EXPORT_SYMBOL  Module two, Exit!\n");
}
module_init(export_symbol_init);
module_exit(export_symbol_exit);
/* Makefile */
obj-m += export_symbol_two.o
KDIR := /home/weifanghai/Android_4.4_git/xunwei/kernel/iTop4412_Kernel_3.0
PWD ?= $(shell pwd)
all:
    make -C $(KDIR) M=$(PWD) modules
clean:
    rm -rf *.o

在Linux依次加载两个模块:

[**@**]# insmod export_symbol_one.ko

[**@**]# insmod export_symbol_two.ko

运行结果效果图:

入口函数

入口函数:应用层编写我们的入口就是main函数,但在驱动编写时不是这样的,有两种情况:
static int __init lt9211_init(void) 加载模块时的初始化函数,也就是驱动模块的入口函数;
static void __exit lt9211_exit(void) 卸载模块时的函数,也就是卸载某个驱动时要执行的函数;  
以双下划线__开头的函数名:通常是一个底层的接口组件。

platform_driver_register() , platform_device_register()

驱动与设备的两种绑定方式:
1.在设备进行注册时进行绑定
2.在驱动进行注册时进行绑定

新的是platform driver机制,传统的是device driver机制。

platform是一个虚拟的地址总线,它主要用于描述SOC的片上资源,比如下图的LCD控制、UART控制器、Camera控制器、GPIO控制器和I2C控制器都属于SOC的片上资源,这些设备都要通过platform_driver_register()与platform_device_register()来进行注册,而其他的外围设备如LCD、Keys、加速传感器、Touch Screen都使用device_register和driver_register来进行注册。

dump_stack()

可以在一个函数里加 dump_stack();  ,就能看到他的调用栈。

最后调用的先打出来,dump_stack 最先打出来。

使用 dump_stack 函数时不需要添加头文件,直接调用即可。

函数调用依赖栈来完成。栈被用于函数调用中参数的传递、创建局部变量、返回信息的保存。

machine_restart(NULL)

machine_restart(NULL)函数用于将系统重新启动。在Linux内核中,这个函数会执行一系列的操作,包括关闭所有的设备、卸载所有的模块、重新初始化硬件等,最终系统会重新启动。通过调用这个函数,可以实现对系统的软重启操作。

sys_sync()

要 include:
#include <linux/fs.h>
#include <linux/syscalls.h>

Linux系统中,ksys_sync 和 sys_sync 都是用于同步文件系统中未写入的缓冲数据到磁盘的系统调用函数。

区别在于:

  1. ksys_sync 是 Linux 内核中的一个函数,用于将缓冲区的数据写入磁盘。它直接在内核中执行,不需要与用户空间的程序进行通信。
    而 sys_sync 是一个系统调用,会导致用户空间的进程等待同步完成。调用该函数会将缓冲数据写入磁盘,并阻塞调用进程直到写入完成,是一个阻塞操作。

  2. 由于 ksys_sync 直接在内核中执行,不需要与用户进程通信,因此在使用上更加高效。而 sys_sync 则需要进程从用户空间切换到内核空间,并在内核空间执行同步操作,会带来一定的开销。

综上所述,ksys_sync 更加高效,而 sys_sync 是一个系统调用函数,使用上相对不那么高效。

atomic_set、atomic_read、原子操作

原子操作是最小的执行单位,该操作绝不会在执行完毕前被任何其他任务或事件打断,也就说,它的最小的执行单位,不可能有比它更小的执行单位。

原子类型 atomic_t 在内核文件linux-3.19.3/include/linux/types.h中定义,该参数一般传递一个指针:

typedef struct {
    int counter; // counter为一个int变量的计数器。
} atomic_t;

atomic_set
定义:static inline void atomic_set(atomic_t * v, int i)
atomic_set() 的功能是将原子类型的变量 v 的值设置为 i
atomic_set() 没有返回值。

atomic_read(atomic_t * v)
该函数对原子类型的变量进行原子读操作, 返回原子类型的变量v的值。

eg:
atomic_t my_atomic ;
printk("before atomic_set, my_atomic.counter = %d\n", atomic_read(&my_atomic) ); // 0
atomic_set( &my_atomic, 5 );
printk("after atomic_set, my_atomic.counter = %d\n", atomic_read(&my_atomic) ); // 5

container_of

作用:通过一个结构体变量中的一个成员的地址来获得该结构体变量的首地址,返回这个地址。

第一个参数:指向某个结构体成员变量的指针。
第二个参数:结构体名
第三个参数:结构体成员名。

如下,pdata 是指向 panel_data 的指针,panel_data 是 mdss_dsi_ctrl_pdata 的成员,通过成员 panel_data 的地址来获得结构体 mdss_dsi_ctrl_pdata 的地址,ctrl_pdata 是一个指向该结构体的指针。

ctrl_pdata = container_of(pdata, struct mdss_dsi_ctrl_pdata, panel_data);

module_param_call

eg:
max96745.c里有:
  module_param_call(max96745_statu, param_set_int, check_max96745_status,
                        &max96745_statu, 0644);
  MODULE_PARM_DESC(max96745_statu, "Max96745 statu");
Makefile中是:obj-y += max96745.o
然后会有节点:/sys/module/max96745/parameters,该节点下有:max96745_statu

驱动程序框架

MODULE_LICENSE("GPL"); :模块的许可证声明/ LICENSE声明:Linux是开源的系统,那就要我们遵守一定的规范,我们一般用GPL规范。从2.4.10版本内核开始,模块必须通过MODULE_LICENSE宏声明此模块的许可证,否则在加载此模块时,会收到内核被污染“kernel tainted”的警告。从linux/module.h文件中可以看到,被内核接受的有意义的许可证有 “GPL”(适合GNU通用公共许可的任何版本),“GPL v2”(只适用GPL版本2),“GPL and additional rights”,“Dual BSD/GPL”,“Dual MPL/GPL”,“Proprietary”。

MODULE_AUTHOR("jixiaowei"); :声明谁编写了模块。

MODULE_DESCRIPTION("max96745"); :关于模块做什么的声明。

MODULE_VERSION :代码修订版本号。

参考有些驱动文件如 **.c ,上面这些都可以不写。但安卓13 MODULE_LICENSE 不写会全编报错。

eg1: 新建文件 kernel\msm-4.19\drivers\odmm\jxw.c:

#include <linux/module.h> // 指定你的初始化和清理函数;
#include <linux/init.h> // 包含大量加载模块需要的函数和符号的定义
static char *book_name = "jxwabc";
module_param(book_name, charp, S_IRUGO);
static int book_num = 4000;
module_param(book_num, int, S_IRUGO);
static int __init jxw_init(void) {
	printk(KERN_EMERG "__jxw__enter.....\n");
	printk(KERN_ERR "jxw book_name: %s\n", book_name);
	printk(KERN_ERR "jxw book_num: %d\n", book_num);
	return 0;
}
module_init(jxw_init);
// 不用改drivers\odmm\Kconfig,在drivers\odmm\Makefile里加一句:obj-y += jxw.o,即可成功编译通过和生效。
// module_init(**); 可以用 module_platform_driver(**); 来代替,详见:
https://blog.csdn.net/m0_37765662/article/details/106490792?ydreferer=aHR0cHM6Ly9jbi5iaW5nLmNvbS8%3D
// 如果不加参数(module_param),则不会在sys/module下生成jxw这个目录,加了参数才会生成。

eg2:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("Dual BSD/GPL");
static int __init demo_init(void)    // 入口函数/初始化函数。应该声明为静态的(但是不强制),因为它们不会在其他文件可见;__init是一个给内核的暗示,给定的函数只是在初始化使用。
{
 printk("Hello World enter\n"); 
 return 0;
}
static void __exit demo_exit(void) 				// 出口函数/清理函数,它注销接口,在模块被去除前返回所有资源给系统。__exit修饰符标识这个代码是只用于模块卸载
{
 printk("Hello World exit\n"); 
}
module_init(demo_init);			// 模块入口。该语句不能省。加载xx模块
module_exit(demo_exit);		// 模块出口。这句可以没有。卸载xx模块。这句和上面那句使用了特别的内核宏来指出这两个函数的角色。

编译它会产生hello.ko目标文件,通过"insmod ./hello.ko"可以加载它,"rmmod hello"可以卸载它,加载时输出Hello World enter,卸载时输出Hello World exit。模块加载函数必须以“module_init(函数名)”的形式来指定,不写参数列表,它返回整形值,若初始化成功,应返回0,若初始化失败,应返回错误编码,在Linux内核里,错误编码是一个负值,在<linux/errno.h>中定义,包括-ENODEV等。模块卸载函数必须以“module_exit(函数名)”来指定,不返回任何值。通常来说,模块卸载函数要完成与模块加载函数相反的功能,如:1.若模块加载函数注册了 XXX,则模块卸载函数应该注销 XXX;2..若模块加载函数动态申请了内存,则模块卸载函数应释放该内存;3.若模块加载函数申请了硬件资源(中断、DMA 通道、I/O 端口和 I/O 内存等)的占用,则模块卸载函数应释放这些硬件资源;4.若模块加载函数开启了硬件,则卸载函数中一般要关闭硬件;

obj-y表示把test.o文件编译进内核。
obj-m表示把编译生成的文件hello.o编译成内核模块,不会编译到内核,但是会生成一个独立的"hello.ko"文件;

之后在终端输入:

$ make (即可在目录下看到生成的驱动模块hello.ko文件,即编译产生hello.ko文件)

$ sudo insmod hello.ko (将模块加载入内核,加载时输出)

$ sudo rmmod hello (卸载内核模块,卸载时输出)

几个驱动中常用的命令 :

  • insmod xxx.ko

加载驱动模块insmod, 将生成驱动模块.ko文件加载,加载后就会执行xxxx_init函数。

  • rmmod xxxx

卸载驱动,注意不用加.ko

  • lsmod

查看内核中的模块信息,相当于在根目录cat proc/modules

insmd : 载入模块。Linux操作系统的核心具有模块化的特性,因此在编译核心时,需把全部的功能都放入核心。你可以将这些功能编译成一个个单独的模块,待有需要时再分别载入它们。

语法:insmod [-fkmpsvxX][-o <模块名称>][模块文件][符号名称 = 符号值]

-o <模块名称> :指定模块的名称,可使用模块文件的文件名

其他参数详见https://www.runoob.com/linux/linux-comm-insmod.html

eg:   # insmod led.o //向内核加载模块

rmmod : 删除模块。

语法:rmmod [-as][模块名称...]

-a : 删除所有目前不需要的模块

-s : 把信息输出至syslog常驻服务,而非终端机界面。

eg: # rmmod -v pppoe (此处pppoe是一个模块)

  • modinfo

查看模块的描述信息,我们可以在驱动程序添加一些辅助信息:如:作者、驱动描述等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值