C
C programming
大学学过c语言的应该非常多,但是能够领悟c的精髓的太少了,现在应该是适合再次看c的时候了,也正好 为看内核做更好的补充,因为我需要写例子来佐证我对内核的理解程度,不能停留纸上功夫。
这里推荐大家实践的一个好办法是去leecode里面找些题来做,只有痛过,才会印象更深。
介绍
c语言是静态语言,需要编译才能运行
c语言的程序可以很方便的移植到其他体系结构的服务器,比如arm,darwin等
c的语法相对比较简单,没有复杂的语法,没有类
c语言没有gc,内存需要自己管理
基本语法
hello, world!
注意emacs里面begin_src C,org-babel-load-languages里面要有C . t
|
|
Hello world!
一个c程序需要包含一个main函数,作为入口
printf
printf和golang里面的fmt.Printf基本一样
|
|
Hello | liuliancao. | ||
Say | 1 | 2 | 3 |
|
|
变量
命名
- 字母、数字和下划线组成
- 不能以数字开头
- 长度不建议超过63个字符
|
|
liuliancao is 0
使用
|
|
liuliancao | is | in | 1 | room. |
benben | is | in | 2 | room. |
运算符
算数运算符
+,-,*,/,%
关系运算符
>,<,==,!=,>=,<=
逻辑运算符
!,&&,||
位运算符
|
|
~0 | is | -1 |
~1 | is | -2 |
~2 | is | -3 |
~3 | is | -4 |
~4 | is | -5 |
~5 | is | -6 |
~6 | is | -7 |
~7 | is | -8 |
~8 | is | -9 |
~9 | is | -10 |
~10 | is | -11 |
假设为8位,你可能会觉得比较奇怪,上面的计算
这个和计算机存储二进制方式有关,计算机是存储二进制的补码
正数的补码和源码一致,比如1,
原码:0000 0001 补码:0000 0001
负数的补码符号位不变,原码取反再1, 比如-1
原码:原码1000 0001 补码:符号位不变,取反1111 1110 +1 1111 1111
所以0在计算机里面对应0000 0000,取反是1111 1111,这个补码对应的值是-1
2在计算机对应的0000 0010,它的补码是0000 0010,取反1111 1101获得计算机存储的补码
补码转原码,和原码到补码相反,符号位不变 -1取反 -1 1111 1100 取反1000 0011,所以2的取反在计算机中是-3
-2也算下 二进制:1000 0010 符号不变取反:1111 1101 +1:1111 1110 谁的补码是这个呢 -2取反是0000 0001,这个是1的补码,所以~-2=1
你可能发现了~n=-(n+1)
所以记得,计算机里面取反和二进制取反不同在于不是以反码形式存储的,这个我觉得是为了 表示不同的1000 0000和0000 0000,以补码这种形式可以利用每一个进制且不会有歧义
|
|
1 | & | 2 | is | 0 |
1 | & | -3 | is | 1 |
仍然以8位为例 eg1: 0000 0001 补码0000 0001 0000 0010 补码0000 0010 0000 0000 补码0000 0000
eg2: 0000 0001 补码0000 0001 1000 0011 补码1111 1101 0000 0001 补码0000 0001
和取反一样,我觉得&也应该是取补码进行相与,随便弄了两个例子结果是一样的
1 | 1 = 1, 1 | 0 = 1, 0 | 0 = 0, 0 | 1 = 1
1 | 0 = 1, 1 | 1 = 0, 0 | 1 = 1, 0 | 0 = 0
运算数每一位左移2位,替换成0
|
|
8 | << | 2 | is | 32 |
8 | >> | 2 | is | 2 |
按8位 8二进制0000 1000,左移2位0010 0000,为32 右移2位0000 0010,为2
逗号运算符
|
|
a | is | 1, | b | is | 2 |
c | is | 1 |
,连接每个表达式 从左到右依次执行
优先级
优先级顺序
|
|
一般来说,就我个人来说不建议写出混淆的代码,写出的代码需要可读性,而不是产生意想不到的结果哈。
流程控制
一般来说,代码是继续执行还是调到哪里,还是循环,这里相当于workflow如何设计。
if
if (条件) statement
if大家都会的哈,只是建议不要多层套用(想想全是if的代码),另外建议优先写likely的 少写if的方法有很多种,比如:
通过其他方法简化写法
- 比如switch或者三元表达式
- 放进数组里面,通过下标方式访问
通过重构
- 看是不是完全不可能发生的事情,是不是没必要的if,而尾端,通常可以少写一个else
- 使用面向对象的方法,这里不细说了哈
|
|
a is not ready at scores: 59
switch(开关)
switch-case这种很多语言都有,存在即合理,可以增加条件的可读性
|
|
liuliancao needs to be harder at score: 59
break一定要记得加哈
|
|
liuliancao | needs | to | be | harder | at | score: | 59 |
liuliancao | is | a | mystery. |
三目运算符(ternary operator)
expression ? express真的时候 : expression为假的时候
|
|
max is 60
while(只要)
while(只要条件满足){我就执行;}
|
|
a | is | now: | 10 |
a | is | now: | 9 |
a | is | now: | 8 |
a | is | now: | 7 |
a | is | now: | 6 |
a | is | now: | 5 |
a | is | now: | 4 |
a | is | now: | 3 |
a | is | now: | 2 |
a | is | now: | 1 |
a | is | now: | 0 |
do while(先做一下,再说条件)
do while和while不一样,do while是先做do里面的语句,然后再判断是否再do
|
|
a | is | now: | 10 |
a | is | now: | 9 |
a | is | now: | 8 |
a | is | now: | 7 |
a | is | now: | 6 |
a | is | now: | 5 |
a | is | now: | 4 |
a | is | now: | 3 |
a | is | now: | 2 |
a | is | now: | 1 |
a | is | now: | 0 |
可以发现,其实和while效果一样,只是这个至少会做一次,比如我们执行到这里了,我们希望至少有日志 输出,那么我们可以这样写, 这样机器状态好的时候我们也能看到一行日志 do { log recording… } while (state is bad)
for循环
永远的classical for loop
|
|
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
|
|
i: 0 | j: 10 |
i: 1 | j: 9 |
i: 2 | j: 8 |
i: 3 | j: 7 |
i: 4 | j: 6 |
i: 5 | j: 5 |
i: 6 | j: 4 |
i: 7 | j: 3 |
i: 8 | j: 2 |
i: 9 | j: 1 |
i从0到9,十次循环,同样作用于j,10到1
数据类型
C语言是静态语言,和python不一样,能通过变量具体去猜 每一个变量都需要定义明确的数据类型,这样编译器才能通过,为之分配最合适的编译参数
基本数据类型分为字符(char), 整数(int), 浮点数(float)
字符(char)
字符类型char指的是单个字符,由一个字节(位)存储,字符变量放在单引号里面,每个字符和ascii码是有 对应关系的,所以也可以赋值对应的ascii码, 除了ab…AB…还有,啥的都是字符
|
|
a is 97 | |
b is 98 | |
A is 65 | |
B is 66 | |
is 44 |
大写字母位于65-96 小写字母97-128
整数(int)
整数类型是int,无符号和有符号在前面分别加unsigned和signed, int默认是有符号的
整数int也可以分开为short int, long int, long long int, 那么如何获取这些的最大最小值呢
以int为例, 其实我们可以反推下 我们只是位数不知道,对于无符号的值,最大是什么呢 这个最大值对应的是全1, 全1是原码,反求补码也是全1,这个是无符号0的取反, 直接对0取反就是unsigned int的最大值
如果是有符号位,最大值是011,对应的补码是011我们无从下手 最小值是111,计算机里面对应的补码是(001) 而这个值是1的补码,也就是~1 = 这个值 max real -> max buma 01..1 -> 01..1
补码取反10..0 是-0,这个无法拿到对应的y y real -> y buma
min real -> min buma 11..1 -> 10..1 补码取反找到那个数的补码值01..0 不好找,哈哈
这里只需要注意取反是对补码(计算机的存储方式)的取反就好了,无符号0取反就是无符号最大值,记住 这点
|
|
u: max: 4294967295 | min: 0 |
s: max: 2147483647 | min: -2147483648 |
在标准库limits.h里面已经定义了int max和int min等临界,需要的时候建议调用这个
|
|
s: max: 2147483647 | min: -2147483648 |
浮点数(float)
使用float关键字
占用4个字节,8位存放指数和符号,24位存放小数和符号
|
|
a is 1000.000000
布尔(bool)
c中没有bool,一般用0,1表示,如果需要,需要引入stdbool.h
占了多少字节sizeof
|
|
int | is | 4 | bytes. |
float | is | 4 | bytes. |
指针pointer
指针,大家大学应该都学过,存的是内存地址。
其中int \*intPtr表示一个指向整数的指针,来看个例子
|
|
variable a 3 | is in 0x7ffdcc6ee404 which is 3 |
所以,
- 指针也是一个变量
- 变量里面存的是内存地址
- 取变量地址用&操作符
- 取指针指向用*操作符
指针取值与指针例子,取最大值
|
|
c 5 | d 6 max is 6 |
函数function
函数是一组可以重复执行的代码,
|
|
返回类型如果不返回可以写void,参数如果是空参数,也可以写void
main()函数每个程序都必须有,程序从这里开始。
函数使用我就不介绍了,大家应该都会,简单看下使用递归方法来计算斐波那契数列
|
|
fb(1) | is | 1 |
fb(2) | is | 1 |
fb(3) | is | 2 |
fb(4) | is | 3 |
fb(5) | is | 5 |
fb(6) | is | 8 |
fb(7) | is | 13 |
fb(8) | is | 21 |
fb(9) | is | 34 |
函数参数
看一下交换两个数的操作哈,这个是菜鸟上面的例子, 大家尽量把例子自己默写下来
|
|
i: 1 | j: 2 |
i: 1 | j: 2 |
|
|
i: 1 | j: 2 |
i: 2 | j: 1 |
为什么会改变呢?
前面我们说到变量其实是个符号,对应的是一个值,这个值放在内存里面,我们通过指针,修改到了对应内存的值
当我们再次访问这个变量的时候,访问的就是这个值了
需要注意下这里的例子,我们的函数主要目的是为了交换i和j的值,那比较好奇如何交换两个的地址呢?
|
|
now addresses: a: 0x7fff4528177c | b:0x7fff45281778 | ||
swapped addresses: a: 0x7fff45281778 | b:0x7fff4528177c | ||
p1: 0x7fff4528177c | *p1: 1 | p2: 0x7fff45281778 | *p2: 2 |
为什么没改变呢, 因为我们虽然交换了地址,但是这个是在另一个函数空间的,有点类似回到值传递的过程, 指针地址被当成值进行传递,所以回过来还是不会执行,那怎么样才能修改才能让两个指针指向变化呢? 下面的例子
|
|
p1: 0x7ffd060f2b4c | *p1: 2 | p2: 0x7ffd060f2b48 | *p2: 1 |
说白了,如果我们要改一个值,那么我们要找到这个值得地址,对地址进行修改,所以要改整数,那就要改 对应的整数指针,要改整数指针,那就要改二级指针,依次类推才可以。
extern
extern用于表示通过其他文件去找定义和变量
static
static可以定义全局静态变量,局部静态变量,定义静态函数
全局静态变量有以下特点,
- 在全局分配内存
- 如果没有初始化,默认值为0
- 该变量,在文件内定义开始到文件结束可见
局部静态变量有如下特点,
- 在全局分配内存
- 始终驻留在全局数据区,直到程序运行结束
- 作用于为局部作用域,当定义它的函数或者语句块结束,就结束
看下下面这个经典的例子
|
|
add: | n | is | 1 |
add: | n | is | 1 |
add_static: | n | is | 1 |
add_static: | n | is | 2 |
n | is | 1 |
会发现,加static的时候让变量有了暂存功能
静态函数有如下特点,
- 静态函数只能在本源文件使用
- 在文件作用域中声明的inline函数默认为static类型
- 静态函数的好处在于其他文件无法引用,对于容易命名冲突的,比较方便,这一点内核代码里面会发现,
大量的inline函数
- 由于静态函数的特点,所以可以用来当计数器
inline
内联函数,inline关键字,原来是函数可能在某个地方,现在相当于要执行的函数直接在附近,所以解决了效率问题 内联函数有如下特点,
- 一般不要太多行,不能递归,不能使用循环
- 函数声明处添加关键字会导致inline无效,inline需要放到函数定义处
|
|
b is 2
const
const表示常量,和static一样,通常表示常量,并且不可修改,所以 const可以防止环境变量被修改
|
|
hello, | 5 | |
b | is | 3 |
可变参数
可变函数通常用…表示,需要引入stdarg.h,
我个人理解还是少使用比较好,偏向单一原则
数组array
C里面的数组,是存一组相同类型的值,按照内存地址顺序,依次存放每一个值,从0开始编号,依次index的基础上+1个单位 来索引所有的数据
初始化
数组初始化的方式,通过{},请看下面一组例子
|
|
a1[0] | is | 129835584 |
a1[1] | is | 3 |
a1[2] | is | 129835088 |
a1[3] | is | 22025 |
a1[4] | is | 390302608 |
a2[0] | is | 0 |
a2[1] | is | 3 |
a2[2] | is | 0 |
a2[3] | is | 0 |
a2[4] | is | 0 |
a3[0] | is | 0 |
a3[1] | is | 1 |
a3[2] | is | 2 |
a3[3] | is | 2 |
a3[4] | is | 4 |
我们可以发现,
|
|
多维数组
数组可以是多维的,比如[ [1,2 ], [3,4]], 类似这种就是一个二维数组,通过两层下标来标记
分别是
a[0][0] = 1, a[0][1] = 2,
a[1][0] = 3, a[1][1] = 4
看下小例子
|
|
a[0][0]: | 1 |
a[0][1]: | 2 |
a[1][0]: | 3 |
a[1][1]: | 4 |
数组作为参数
|
|
array[0]: | 1 |
array[1]: | 2 |
array[2]: | 3 |
array[3]: | 4 |
array[4]: | 5 |
array[0]: | 1 |
array[1]: | 2 |
array[2]: | 3 |
array[3]: | 4 |
array[4]: | 5 |
可以通过传头指针的方式
或者可以通过索引的方式
拷贝数组
由于数组的名字本质上是头指针,所以不能简单=,这会导致指向同一个数组,
所以真正的拷贝可以新建一个数组,逐个拷贝或者使用memcopy库函数拷贝
|
|
a_copy[0]: 1 | b_copy[0]: 1 |
a_copy[1]: 2 | b_copy[1]: 2 |
a_copy[2]: 3 | b_copy[2]: 3 |
a_copy[3]: 4 | b_copy[3]: 4 |
a_copy[4]: 5 | b_copy[4]: 5 |
字符串string
C里面没有字符串这个类型,而是通过char数组来表示字符串,通过尾巴放acii \0来表示字符串到了结尾
Hello表示为{'H','e','l','l','o','\0'}
字符串的定义
|
|
Hello | Liuliancao! |
字符串的函数
注意str后面都是三个字符串
示例程序
|
|
strcopy(s3,s1) | s3 | is | hello | ||||||||
s1 | before | is | hello | ,after | strcat(s1,s2) | is | hello | liuliancao, | s2 | is | iancao |
strlen(s1) | is | 6 | |||||||||
strcmp(hello | ,hello | ) | is | 0 | |||||||
strcmp(hello | ,iancao) | is | -1 |
|
|
strcopy(s3,s1) | s3 | is | hello | ||||||||
s1 | before | is | hello | ,after | strcat(s1,s2) | is | hello | liuliancao, | s2 | is | liuliancao |
strlen(s1) | is | 6 | |||||||||
strcmp(hello | ,hello | ) | is | 0 | |||||||
strcmp(hello | ,liuliancao) | is | -4 |
注意几个点:
- 字符串的声明时候要注意留一个给'\0',否则会存在越界行为,
- strcopy(目的,源) 需要注意方向,copy不会影响源,但是后续请尽量使用copy的值
- strcat(s3,s2) 这个会让s3和s2拼接在一起,和cat命令一样, 这里一定要注意s3的大小要够(如果计划要cat)
|
|
after | u(include) | is | uliancao |
after | iu | is | iuliancao |
注意这里需要注意一点是使用字符的时候,一定要单引号,否则是字符串
枚举enum
枚举,比如性别分为男和女,星期分为1到7
一个简单的例子,来自菜鸟c
|
|
THR | is | 4 |
You | said | 1 |
结构体struct
结构体,用户可以自定义的一种数据类型,一个结构体通常包含各种各样的字段,这些组成了这个结构, 是组成的关系
以Employee为例,包含ID, 英文姓名,显示姓名,性别,邮件等, 用结构体定义如下
|
|
Employee(xiaoA) | |
id: | 1 |
name: | XiaoA |
displayName: | 小A |
gender: | 1 |
mail: | xiaoa@liuliancao.com |
总结:
- 结构体类似类,可以包我们需要的东西来实现抽象
- 结构体对象的访问内容方式是NAME.ATTRIBUTE
- 结构体指针的访问内容方式P->ATTRIBUTE, 注意(&xiaoA)->mail这个,需要括号
- 结构体一般建议传指针(地址)
- 注意string的赋值,使用string库里面的strcpy完成赋值
类型定义typedef
给类型起一个名字,这样可以直接使用这个名字表示这个类型
typedef 原有类型 我定义的名字;
|
|
a is 5
clangd
liuliancao@lqx:~$ sudo apt-cache search clangd
clang-tools-6.0 - clang-based tools for C/C++ developments
clang-tools-7 - clang-based tools for C/C++ developments
liuliancao@lqx:~$ sudo apt-get install clang-tools-7 -y
liuliancao@lqx:~$ sudo update-alternatives --install /usr/bin/clangd clangd /usr/bin/clangd-7 100
update-alternatives: using /usr/bin/clangd-7 to provide /usr/bin/clangd (clangd) in auto mode