目录
基础语法
从最基本的语法入手,主要包含关键字,语法,运算符优先级等方面。
关键字
随时间的发展,C语言主要的发布标准C89、C99、C11,未来或许会新增关键字,目前而言在使用或理解层面register、const、volatile相对要一点深刻的基础知识,const不易理解是因为即可修饰变量亦可修饰地址。
本章节中梳理内容的不包含完整C标准定义的关键字,如:未做C11标准中复数相关关键字相关说明。
数据类型
序号 | 关键字 | 字节长度 32位/64位 | 说明 |
1 | char | 1/1 | ASCII码,-128到+127,最小的负数总可以被表示。 |
2 | int | 4/4 | +/-21亿 |
3 | float | 4/4 | 长度32位,23 位有效位,8位浮点位。精确到7位小数。浮点数由于二进制存储方式,或者说受数学到计算机转化带来的失真影响,决不能通过”==”来判定相等。 |
4 | double | 8/8 | 长度64位,52 位有效位,11 位浮点位。精确到15位小数。浮点数由于二进制存储方式,或者说受数学到计算机转化带来的失真影响,决不能通过”==”来判定相等。 |
5 | short short int | 2/2 | short修饰int时,关键字int可省缺。 |
6 | long long int | 4/8 | 与机器位数保持一致,因此long总是用来保存指针或函数地址。long修饰int时,关键字int可省缺。 |
7 | long long long long int | 8/8 | long long修饰int时,关键字int可省缺。 |
8 | long double | 10/10 | 精确到20位小数。在我狭窄的眼光里是占10字节,据说也有12/16字节的情况。 |
数据修饰
事实上short和long也是数据修饰关键字,同时也是数据类型,故放到数据类型中。
序号 | 关键字 | 修饰范围 | 说明 |
1 | signed | 整型 | 编译器在解析数据类型时的省缺关键字(一般而言) |
2 | unsigned | 整型 | 省缺signed/unsigned的数据类型,编译器一般认定为有符号signed类型,总有例外,有幸在ARM上见识过编译器默认以无符号unsigned类型为省缺类型的。在系统内核或芯片底层应当显式定义,尽量避免隐式定义。 也有不少省缺数据类型的unsigned写法,小范围使用倒也简单,但并不是好习惯,省缺的默认数据类型一般是int。 |
复杂类型
序号 | 关键字 | 定义 | 说明 |
1 | void | 函数或指针 | 无类型 |
2 | struct | 结构体 | |
3 | union | 联合体 | |
4 | enum | 枚举 | 枚举变量占用的字节数一般是4字节,当然在编译ARM架构时也有根据枚举值动态调整枚举类型字节长度的情况,在gcc实际应用中,可通过编译参数-fshort-enums/-fno-short-enums控制编译策略,ARM编译一定要加上-fno-short-enums,因为精简指令集硬件不支持处理非对齐异常(X86无烦恼)。 |
5 | typedef | 数据类型定义 | |
6 | sizeof | 空间容量解析 | 获取类型或变量所占用的空间字节数 |
7 | typeof | 数据类型解析 | 获取变量的数据类型 |
8 | bool | 布尔类型 | 使用较少,一般会自定义bool类型。 |
存储类型
序号 | 关键字 | 修饰范围 | 说明 |
1 | auto | 变量 | 由编译器自动分配及释放,变量定义省缺值。通常在栈上存放。 |
2 | static | 变量/函数 | 静态变量,分配在静态变量区,修饰函数时,指定函数作用域为文件内部。 |
3 | register | 变量 | 寄存器变量,建议编译器将变量存储到寄存器中使用,也可以修饰函数形参,建议编译器通过寄存器而不是堆栈传递参数。 |
4 | extern | 变量/函数 | 指定对应变量为外部变量,也可修饰函数,表全局符号或外部引用。 extern 在编译处理中的用法,extern "C" { }的作用就是告诉C++编译器,将指定的函数或代码段用C规则编译。 |
5 | const | 变量 | 常量,指定变量不可被当前线程/进程改变(但有可能被系统或其他线程/进程改变)。 1、const int* p = &a; const和int可以互换位置。当const在最前面,修饰的是*p,*p不可变。*p表示指针变量p所指向的内存单元里面的内容,此时不可变。其他的都可变,如p中存放的是指向的内存单元的地址,即p的指向可变。 2、int *const p = &a; 此时 const 修饰的是p,所以p中存放的内存单元的地址不可变,而内存单元中的内容可变。即p的指向不可变,p所指向的内存单元的内容可变。 3、const int* const p = &a; 此时*p和p都被修饰,那么p中存放的内存单元的地址和内存单元中的内容都不可变。 |
6 | volatile | 变量 | 易被外部修改的变量,指定变量的值有可能会被系统或其他进程/线程改变,强制编译器每次从内存中取得该变量的值。常用于与硬件相关的寄存器,如:DMA和软件都会操作的寄存器或内存。一般而言少量数据存在易变性会使用volatile关键字修饰,大量数据或内存块访问会采用禁用cache或访问前刷新cache的方式,当然,一些高级硬件已经具备硬件保证cache一致性的能力。 |
7 | restrict | 变量 | 用于限定和约束指针,保证代码中restrict的指针独占其指向的内存,所有访问/修改其内存的操作都是基于该指针的,没有其他直接或间接的方式(其它变量或指针)。 |
8 | inline | 函数 | 内联函数,内联函数取消了函数调用的过程,将函数内嵌到调用函数内部,内联函数和函数宏最大的区别在于,内联函数具备和普通函数一样的作用域限制。 |
跳转结构
序号 | 关键字 | 应用范围 | 说明 |
1 | return | 函数 | 在函数体中,返回特定值(或者是void值,即不返回值)。 |
2 | continue | 循环结构 | 结束当前循环,开始下一轮循环,常用与for/while语句。 |
3 | break | 条件/循环结构 | 跳出当前循环或switch结构。 |
4 | goto | 函数 | 无条件跳转语句。 |
分支结构
序号 | 关键字 | 应用范围 | 说明 |
1 | if else if | 条件判断 | 条件语句 |
2 | else | 与if配合使用,使判定条件闭环 | |
3 | switch | 选择分支 | 多重分支语句 |
4 | case | 与switch配合使用 | |
5 | default | 与switch配合使用,使多重分支语句条件闭环 |
循环结构
序号 | 关键字 | 应用范围 | 说明 |
1 | for | 限定循环 | for循环结构,for(1;2;3)4;的执行顺序为1->2->4->3->2...循环,其中2为循环条件。一般也会配合continue/break使用。 |
2 | do while | 不限定循环 | do循环结构,do 1 while(2); 的执行顺序是 1->2->1...循环,2为循环条件。一般也会配合continue/break使用。 |
3 | while | 不限定循环 | while循环结构,while(1) 2; 的执行顺序是1->2->1...循环,1为循环条件。一般也会配合continue/break使用。 |
预处理命令
什么是预处理,一言以蔽之,在C程序编译前的准备工作,预处理操作都是给编译器的。或许已经写过C语言,甚至C教学已经完成,并取得了不错的成绩,似乎都没见到所谓的预处理,她的神秘面纱,但一定写过#include <stdio.h>,这正是一条预处理命令。
编写代码前,会将项目、功能拆分,预处理在C编程中必不可少,从此处入C语言的门。
宏处理
序号 | 关键字 | 定义 | 说明 |
1 | #define | 宏定义 | define关键字和指针并列为C的灵魂,可以让魔鬼数字变得通俗易懂、易于领会,可以简化重复的动作使可读写更高,甚至可以化繁为简到难以想象的地步,define几乎在C的世界里无所不能,但剑的双刃性也体现于此,无所不能的同时也造就了臭名昭著的“缝合”代码,不妨看看Linux或那些老项目中写得“一坨一坨”的代码,或多或少都和define相关(与架构无关,纯粹从可读性而言)。 |
2 | #undef | 解除宏定义 | 字面意思,被undef的宏定义将失效,一般配合define使用,用于更正已经定义的宏。 |
文件包含
序号 | 关键字 | 定义 | 说明 |
1 | #include | 文件引用 | 引用指定文件到当前文件,在引用文件时使用””或<>,一般双引号是本地头文件,尖括号是库里的头文件,但这不是严格执行的定义。当然一般是引用*.h文件,但谁说不能引用*.c文件,甚至是其它后缀名的文件。 |
条件编译
序号 | 关键字 | 定义 | 说明 |
1 | #if | 编译条件检测 | 条件成立编译,可用于判断宏值,处理多个条件时支持&&或||判断 |
2 | #elif | 条件成立编译,与if无本质区别 | |
3 | #else | 条件不成立编译,逻辑闭环 | |
4 | #ifdef | 宏定义检测 | 宏存在编译,不判断宏值,并只判定单个宏存在性 |
5 | #ifndef | 宏不存在编译,不判断宏值,并只判定单个宏不存在性,ifndef在头文件中保证只处理一次应用较多 | |
6 | #defined | 宏存在编译,不判断宏值,当多个宏的存在性需要判定时,defined是不错的选择,处理多个条件时支持&&或||判断 | |
7 | #endif | 结束条件编译 | 条件编译必要的配套语法 |
编译控制
序号 | 关键字 | 定义 | 说明 |
1 | #error | 编译报错 | #error message,message不需要用双引号包围。 正常代码不通过error来控制编译,因为编译到error命令控制的语句时将停止。 |
2 | #line | 重置行号和文件名 | #line number #line number[“filename”] 基本格式如上,修改后通过宏__LINE__与__FILE__获取的内容将产生变化。 |
3 | #pragma | 向编译器传送指令 | 功能极其强大,支持向编译器传送各种指令。#pragma once,保证头文件只被编译一次,此外未有使用经历。 |
预置宏
序号 | 关键字 | 定义 | 说明 |
1 | __DATE__ | 日期 | 当前源程序的创建日期 |
2 | __FILE__ | 文件名 | 当前源程序的文件名称(包括盘符和路径),即:全路径。 |
3 | __LINE__ | 行号 | 当前被编译代码的行号,使用较多。 |
4 | __STDC__ | 标准C判断 | 返回编译器是否为标准C,若其值为1表示符合标准C,否则不是标准C。 |
5 | __TIME__ | 时间 | 当前源程序的创建时间 |
6 | __FUNCTION__ __func__ | 函数名 | 当前被编译代码所属函数,使用较多。 |
7 | __cplusplus | C++标签 | __cplusplus是C++内置的宏 |
编译属性控制
__attribute__是GNU C 编译器用于声明函数、变量或类型属性的关键字。
主要用于告诉编译器在编译程序时需要进行哪些方面的优化或代码检查。
基础定义:__attribute__ ((ATTRIBUTE)),具体的编译属性ATTRIBUTE是被一对双括号“((ATTRIBUTE))”包围的。
事实上编译器支持的编译属性控制关键字具不完全统计有数十种,下文罗列常用的一些控制字。
序号 | 关键字 | 定义 |
1 | section | 自定义段 int unint_val __attribute__((section(".data"))); 自定义数据分段,在系统内核中使用较多,能做到一定隔离。 |
2 | aligned | 指定对齐 char c2 __attribute__((aligned(4))); 变量对齐,在系统内核中使用较多,CPU有些指令有地址对齐要求,很实用。 |
3 | packed | 紧缩对齐 int c __attribute__((packed)); |
4 | format | 检查函数变参格式 __attribute__((format (archetype, string-index, frist-to-check))) void LOG(const char *fmt, ...) __attribute__((format(printf,1,2))); LOG("hello world ,i am %d ages \n", age); /* 前者表示格式字符串,后者表示所有的参数*/ 对函数入参在编译阶段进行校验,语法格式较复杂。 |
5 | weak | 弱符号 void __attribute__((weak)) func(void); int num __attribute__((weak)); 处理符号冲突神技,类似于C++中的重写概念。一个强符号+一个弱符号的结构,在编译处理或源码阅读时有明确的逻辑结构,不建议对同一符号进行多次weak声明。 |
6 | alias | 给函数起别名,实际编码时基本都使用宏定义define从命名函数。 void __f(void) { printf("__f\n"); } void f(void) __attribute__((alias("__f"))); int main(void) { f(); return 0; } |
7 | noinline | 内联不展开 static inline __attribute__(( noinline)) void f(); 明确inline修饰的接口,编译时不将inline修饰的接口内联展开。 |
8 | always_inline | 内联函数总是展开 static inline __attribute__((always_inline)) void f(); 明确inline修饰的接口,编译时必须展开。已经使用inline定义的接口为什么要使用always_inline去定义,因为inline定义的函数是否内联,是由编译器进行综合评估决定的。 |
section
可执行文件主要由代码段,数据段、BSS 段构成,还可以包含一些其他自定义段。
代码段(.text):用来存放编译生成的可执行指令,比如函数定义、程序语句。
数据段(.data):用来存放初始化的全局变量、初始化的静态局部变量。
BSS段(.bss):用来存放未初始化的全局变量,未初始化的静态局部变量。
section 属性作用:编译时,将一个函数或者变量放到指定的段。
使用 __ attribute __ ((section(“xxx”))),修改段的属性。
aligned
aligned (x),参数x表示按几个字节对齐,该参数必须是 2 的幂次方,否则编译就会报错。
32位系统中CPU读写RAM时,硬件只支持4字节及其整数倍数对齐的地址访问,一个机器周期可以读写4字节数据,如果把一个int型数据就放在4字节对齐的地址上,那么CPU就可以在一个机器周期内完成数据的读写操作,否则可能需要两个机器周期才能完成。
在精简指令集架构,一般编译器会保证对齐,避免非对齐异常。
packed
aligned 对齐一般会增大变量的地址,变量或结构体成员变量之间地址对齐可能会造成一定的内存空洞(内存空间浪费),而packed对齐则正好相反,一般用来减少变量间的空洞,使变量间紧挨着,指定变量或类型使用最可能小的地址对齐方式。
format
format 属性用来指定变参函数的参数格式检查。
使用方法:
__attribute__((format (archetype, string-index, frist-to-check)))
示例:
void LOG(const char *fmt, ...) __attribute__((format(printf,1,2)));
LOG("hello world ,i am %d ages \n", age); /* 前者表示格式字符串,后者表示所有的参数*/
属性format(printf,1,2) 有3个参数,
第一个参数pritnf 是告诉编译器,按照printf的标准来检查;
第二个参数表示LOG()函数所有的参数列表中格式字符串的位置索引,即第一个参数"hello world ,i am %d ages \n";
第三个参数是告诉编译器要检查的参数的起始位置,即从第二个参数age开始检查。
weak
强符号:函数名,初始化的全局变量名。
弱符号:未初始化的全局变量名。
弱符号的这个特性在库函数开发设计中应用十分广泛,如果在开发一个库时,基础功能已经实现,有些高级功能还未实现,那么你就可以将这些函数通过weak 属性声明转换为一个弱符号。
alias
在Linux 内核中你会发现alias有时候会和weak属性一起使用。如有些接口随着内核版本升级,函数接口发生了变化,我们可以通过alias属性对旧的接口名字进行封装,重新起一个接口名字。
//f.c void __f(void) { printf("__f\n"); } void f() __attribute__((weak, alias("__f"))); //main.c void __attribute__((weak)) f(void); void f(void) { printf("f\n"); } int main() { f(); return 0; } |
如果main.c 中定义了f()函数,那么main 函数调用f()会调用新定义的函数(强符号),否则调用__f()函数(弱符号)。
noinline
编译器不一定会展开内联函数,编译器根据实际情况进行评估,权衡展开和不展开的利弊,并最终决定要不要展开。
满足以下几点的函数适合被声明为内联函数:
1. 函数体积小;
2. 函数体内无指针赋值、递归、循环语句等;
3. 调用频繁。
当一个函数满足以上三点,应做内联展开时,可以使用static inline 关键字修饰它,但是编译器不一定会内联展开。
如果想明确告诉编译器一定要展开,或者不展开就可以使用 noinline 和 always_inline 对函数的属性做一个声明。
always_inline
在Linux 内核中,存在大量的内联函数被定义在头文件中,而且常常使用static关键字修饰。
为什么定义在头文件中呢?
因为它是一个内联函数,可以像宏一样使用,在任何想使用内联函数的源文件中,都不必亲自在定义一遍,直接包含这个头文件即可。
为什么还要用static 修饰呢?
因为使用inline关键字定义的内联函数,编译器不一定会内联展开,那么当一个工程中多个头文件包含这个内联函数的定义时,编译时就可能报重复定义的错误。使用satic 关键字修饰,则可以限定这个函数的作用域在各自的源文件内,避免重复定义的发生。
控制字列表
梳理出编译属性控制中的控制字列表,不做详细介绍。
aligned | alloc_size | noreturn | returns_twice |
noinline | noclone | always_inline | hot |
flatten | pure | const | nothrow |
sentinel | format | format_arg | no_instrument_function |
no_split_stack | section | constructor | destructor |
used | unused | deprecated | weak |
malloc | alias | ifunc | warn_unused_result |
nonnull | gnu_inline | externally_visible | cold |
artificial | error | warning |
基础语法
常量符号
C语言提供的常量类型有整型常量、实型常量、字符常量、字符串常量和符号常量。
用一个标识符来代表一个常量,该标识符叫做符号常量,其一般形式为:
#define 标识符 常量
例如:
#define PI 3.1415926
变量
1、变量在内存中占用一定的储存单元,在该储存单元中存放变量值。
2、程序中用到的所有变量都必须有一个变量名
3、变量名和变量值是两个不同的概念
4、不同类型的变量在内存中占据储存单元的数量及储存的格式不相同,例如:char ch = ‘1';int i = 1;
5、变量必须“先定义后使用”,这样做的目的是:便于编译程序程序检测对该变量的运算是否合法。例如:整型变量可以进行求模(余数)运算,实型变量不可以进行求模运算。
数据类型
转义字符
转义符号 | 功能 | ASCII码值(十进制) |
\b | 退格(BS) | 008 |
\n | 换行(LF) | 010 |
\r | 回车(CR) | 013 |
\t | 水平制表(HT) | 009 |
\v | 垂直制表(VT) | 011 |
\\ | 反斜杠 | 092 |
\? | 问号字符 | 063 |
\' | 单引号字符 | 039 |
\" | 双引号字符 | 034 |
\0 | 空字符(NULL) 字符串结束符 | 000 |
\f | 换页符 | 012 |
注释
C语言的注释有两种方式:
// 单行注释
/* 多行注释 */
运算符
算术运算符:+、-、*、/、%等。
关系运算符:==、!=、>、<、>=、<=等。
逻辑运算符:&&、||、!等。
位运算符:&、|、^、~、<<、>>等。
赋值运算符:=、+=、-=、*=、/=、%=等。
数据类型
基本数据类型:int、float、double、char、void等。
复合数据类型:数组、结构体、共用体、枚举等。
控制语句
条件语句:if、else、switch等。
循环语句:while、do-while、for等。
跳转语句:break、continue、goto等。
函数
C语言中的函数定义格式为:
返回类型 函数名(参数列表) {
函数体
}
数组
C语言中的数组定义格式为:
数据类型 数组名[数组大小];
指针
C语言中的指针是一种特殊的变量类型,它存储的是内存地址。指针的定义格式为:
数据类型 *指针名;
结构体
C语言中的结构体是一种复合数据类型,它可以包含多个不同类型的变量。结构体的定义格式为:
struct 结构体名{
数据类型 变量名1;
数据类型 变量名2;
……
};
运算符优先级
代码编写时,最好的方式是显式表示运算优先级,但事实上在部分场景会让代码变得过度冗余繁重,因此必要的运算符优先级理解有很多实质帮助。
C优先级
优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 | 说明 |
1 | [] | 数组下标 | 数组名[常量表达式] | 左到右 | ----- |
() | 圆括号 | (表达式)/函数名(形参表) | ----- | ||
. | 成员选择(对象) | 对象.成员名 | ----- | ||
-> | 成员选择(指针) | 对象指针->成员名 | ----- | ||
2 | - | 负号运算符 | -表达式 | 右到左 | 单目运算符 |
(类型) | 强制类型转换 | (数据类型)表达式 | ----- | ||
++ | 前置自增运算符 | ++变量名 | 单目运算符 | ||
++ | 后置自增运算符 | 变量名++ | 单目运算符 | ||
-- | 前置自减运算符 | --变量名 | 单目运算符 | ||
-- | 后置自减运算符 | 变量名-- | |||
* | 取值运算符 | *指针变量 | 单目运算符 | ||
& | 取地址运算符 | &变量名 | 单目运算符 | ||
! | 逻辑非运算符 | !表达式 | 单目运算符 | ||
~ | 按位取反运算符 | ~表达式 | 单目运算符 | ||
sizeof | 长度运算符 | sizeof(表达式) | ----- | ||
3 | / | 除 | 表达式/表达式 | 左到右 | 双目运算符 |
* | 乘 | 表达式*表达式 | 双目运算符 | ||
% | 余数(取模) | 整型表达式/整型表达式 | 双目运算符 | ||
4 | + | 加 | 表达式+表达式 | 左到右 | 双目运算符 |
- | 减 | 表达式-表达式 | 双目运算符 | ||
5 | << | 左移 | 变量 | 左到右 | 双目运算符 |
>> | 右移 | 变量>>表达式 | 双目运算符 | ||
6 | > | 大于 | 表达式>表达式 | 左到右 | 双目运算符 |
>= | 大于等于 | 表达式>=表达式 | 双目运算符 | ||
< | 小于 | 表达式 | 双目运算符 | ||
<= | 小于等于 | 表达式 | 双目运算符 | ||
7 | == | 等于 | 表达式==表达式 | 左到右 | 双目运算符 |
!= | 不等于 | 表达式!= 表达式 | 双目运算符 | ||
8 | & | 按位与 | 表达式&表达式 | 左到右 | 双目运算符 |
9 | ^ | 按位异或 | 表达式^表达式 | 左到右 | 双目运算符 |
10 | | | 按位或 | 表达式|表达式 | 左到右 | 双目运算符 |
11 | && | 逻辑与 | 表达式&&表达式 | 左到右 | 双目运算符 |
12 | || | 逻辑或 | 表达式||表达式 | 左到右 | 双目运算符 |
13 | ?: | 条件运算符 | 表达式1? 表达式2: 表达式3 | 右到左 | 三目运算符 |
14 | = | 赋值运算符 | 变量=表达式 | 右到左 | ----- |
/= | 除后赋值 | 变量/=表达式 | ----- | ||
*= | 乘后赋值 | 变量*=表达式 | ----- | ||
%= | 取模后赋值 | 变量%=表达式 | ----- | ||
+= | 加后赋值 | 变量+=表达式 | ----- | ||
-= | 减后赋值 | 变量-=表达式 | ----- | ||
<<= | 左移后赋值 | 变量 | ----- | ||
>>= | 右移后赋值 | 变量>>=表达式 | ----- | ||
&= | 按位与后赋值 | 变量&=表达式 | ----- | ||
^= | 按位异或后赋值 | 变量^=表达式 | ----- | ||
|= | 按位或后赋值 | 变量|=表达式 | ----- | ||
15 | , | 逗号运算符 | 表达式,表达式,… | 左到右 | 从左向右顺序运算 |
C++优先级
在实际应用中,C与C++混用场景大量存在,此处对C++运算符也进行统计罗列。
运算符 | 描述 | 例子 | 可重载性 |
第一级别 | ----- | ----- | ----- |
:: | 作用域解析符 | Class::age = 2; | 不可重载 |
第二级别 | ----- | ----- | ----- |
() | 函数调用 | isdigit('1') | 可重载 |
() | 成员初始化 | c_tor(int x, int y) : _x(x), _y(y*10){}; | 可重载 |
[] | 数组数据获取 | array[4] = 2; | 可重载 |
-> | 指针型成员调用 | ptr->age = 34; | 可重载 |
. | 对象型成员调用 | obj.age = 34; | 不可重载 |
++ | 后自增运算符 | for( int i = 0; i < 10; i++ ) cout | 可重载 |
-- | 后自减运算符 | for( int i = 10; i > 0; i-- ) cout | 可重载 |
const_cast | 特殊属性转换 | const_cast(type_from); | 不可重载 |
dynamic_cast | 特殊属性转换 | dynamic_cast(type_from); | 不可重载 |
static_cast | 特殊属性转换 | static_cast(type_from); | 不可重载 |
reinterpret_cast | 特殊属性转换 | reinterpret_cast(type_from); | 不可重载 |
typeid | 对象类型符 | cout « typeid(var).name(); cout « typeid(type).name(); | 不可重载 |
第三级别(具有右结合性) | ----- | ----- | ----- |
! | 逻辑取反 | if( !done ) … | 可重载 |
not | ! 的另一种表达 | ----- | ----- |
~ | 按位取反 | flags = ~flags; | 可重载 |
compl | ~的另一种表达 | ----- | ----- |
++ | 预自增运算符 | for( i = 0; i < 10; ++i ) cout | 可重载 |
-- | 预自减运算符 | for( i = 10; i > 0; --i ) cout | 可重载 |
- | 负号 | int i = -1; | 可重载 |
+ | 正号 | int i = +1; | 可重载 |
* | 指针取值 | int data = *intPtr; | 可重载 |
& | 值取指针 | int *intPtr = &data; | 可重载 |
new | 动态元素内存分配 | long *pVar = new long; MyClass *ptr = new MyClass(args); | 可重载 |
new [] | 动态数组内存分配 | long *array = new long[n]; | 可重载 |
delete | 动态析构元素内存 | delete pVar; | 可重载 |
delete [] | 动态析构数组内存 | delete [] array; | 可重载 |
(type) | 强制类型转换 | int i = (int) floatNum; | 可重载 |
sizeof | 返回类型内存 | int size = sizeof floatNum; int size = sizeof(float); | 不可重载 |
第四级别 | ----- | ----- | ----- |
->* | 类指针成员引用 | ptr->*var = 24; | 可重载 |
.* | 类对象成员引用 | obj.*var = 24; | 不可重载 |
第五级别 | ----- | ----- | ----- |
* | 乘法 | int i = 2 * 4; | 可重载 |
/ | 除法 | float f = 10.0 / 3.0; | 可重载 |
% | 取余数(模运算) | int rem = 4 % 3; | 可重载 |
第六级别 | ----- | ----- | ----- |
+ | 加法 | int i = 2 + 3; | 可重载 |
- | 减法 | int i = 5 - 1; | 可重载 |
第七级别 | ----- | ----- | ----- |
<< | 位左移 | int flags = 33 | 可重载 |
>> | 位右移 | int flags = 33 >> 1; | 可重载 |
第八级别 | ----- | ----- | ----- |
< | 小于 | if( i < 42 ) … | 可重载 |
<= | 小于等于 | if( i | 可重载 |
> | 大于 | if( i > 42 ) … | 可重载 |
>= | 大于等于 | if( i >= 42 ) ... | 可重载 |
第九级别 | ----- | ----- | ----- |
== | 恒等于 | if( i == 42 ) ... | 可重载 |
eq | == 的另一种表达 | ----- | ----- |
!= | 不等于 | if( i != 42 ) … | 可重载 |
not_eq | !=的另一种表达 | ----- | ----- |
第十级别 | ----- | ----- | ----- |
& | 位且运算 | flags = flags & 42; | 可重载 |
bitand | &的另一种表达 | ----- | ----- |
第十一级别 | ----- | ----- | ----- |
^ | 位异或运算 | flags = flags ^ 42; | 可重载 |
xor | ^的另一种表达 | ----- | ----- |
第十二级别 | ----- | ----- | ----- |
| | 位或运算 | flags = flags | 42; | 可重载 |
bitor | |的另一种表达 | ----- | ----- |
第十三级别 | ----- | ----- | ----- |
&& | 逻辑且运算 | if( conditionA && conditionB ) … | 可重载 |
and | &&的另一种表达 | ----- | ----- |
第十四级别 | ----- | ----- | ----- |
|| | 逻辑或运算 | if( conditionA || conditionB ) ... | 可重载 |
or | ||的另一种表达 | ----- | ----- |
第十五级别(具有右结合性) | ----- | ----- | ----- |
? : | 条件运算符 | int i = (a > b) ? a : b; | 不可重载 |
第十六级别(具有右结合性) | ----- | ----- | ----- |
= | 赋值 | int a = b; | 可重载 |
+= | 加赋值运算 | a += 3; | 可重载 |
-= | 减赋值运算 | b -= 4; | 可重载 |
*= | 乘赋值运算 | a *= 5; | 可重载 |
/= | 除赋值运算 | a /= 2; | 可重载 |
%= | 模赋值运算 | a %= 3; | 可重载 |
&= | 位且赋值运算 | flags &= new_flags; | 可重载 |
and_eq | &= 的另一种表达 | ----- | ----- |
^= | 位异或赋值运算 | flags ^= new_flags; | 可重载 |
xor_eq | ^=的另一种表达 | ----- | ----- |
|= | 位或赋值运算 | flags |= new_flags; | 可重载 |
or_eq | |=的另一种表达 | ----- | ----- |
<<= | 位左移赋值运算 | flags <<=2; | 可重载 |
>>= | 位右移赋值运算 | flags >>= 2; | 可重载 |
第十七级别 | ----- | ----- | ----- |
throw | 异常抛出 | throw EClass(“Message”); | 不可重载 |
第十八级别 | ----- | ----- | ----- |
, | 逗号分隔符 | for( i = 0, j = 0; i < 10; i++, j++ ) … | 可重载 |
内嵌汇编
使用 GCC 编译器在 C 语言程序中嵌入汇编代码的方法与使用 GCC 编译器时类似。具体实现方法如下:
asm [volatile] (AssemblerTemplate : OutputOperands [ : InputOperands [ : Clobbers ]]); |
其中,各个参数的含义与使用 GCC 编译器时相同。需要注意的是,在 ARM 架构中,汇编代码的语法与 x86 架构有所不同,需要使用 ARM 汇编语言。
下面是一个简单的示例,演示如何在 C 语言程序中使用嵌入式汇编代码实现将两个整数相加的功能:
#include <stdio.h> int main(void) { int a = 1, b = 2, c; asm volatile ("add %[a], %[b], %[c]" : [c] "=r" (c) : [a] "r" (a), [b] "r" (b));
printf("result: %d\n", c); return 0; } |
在上述示例中,使用了 ARM 汇编代码模板 “add %[a], %[b], %[c]”,表示将操作数 a 和 b 相加,并将结果保存到 c 中。其中,占位符 %[a]、%[b] 和 %[c] 分别表示输入操作数 a、b 和输出操作数 c。
在输入操作数列表中,使用了修饰符 “r”,表示将变量 a 和 b 存储在通用寄存器中。在输出操作数列表中,使用了修饰符 “=r”,表示将变量 c 存储在通用寄存器中,并且需要在计算后返回给 C 语言程序。
需要注意的是,在使用嵌入式汇编代码时,需要确保代码的正确性和安全性,避免出现未定义的行为和安全漏洞。
关键字 asm
在 GCC 编译器中,关键字 asm 用于在 C 或 C++ 语言程序中嵌入汇编代码。通过嵌入汇编代码,可以实现一些高级的操作或者利用汇编代码的性能优势来提高程序的效率。
使用 asm 关键字时,需要按照一定的语法规则编写汇编代码,并指定输入操作数、输出操作数和使用的寄存器等信息。具体的使用方法可以参考前面提到的 Linux ARM GCC 在 C 语言中内嵌汇编的示例代码。
内嵌 MRS/MSR指令
以ARMv8 架构下读取CPU ID的操作作为例子:
static uint64_t get_cpu_id(void) { uint64_t mpidr_el1; asm volatile("mrs %0, mpidr_el1" : "=r" (mpidr_el1)); return mpidr_el1; } |
如果要是向 armv8 系统寄存器写值可以使用下面代码:
reg_val = 0x000018d9; asm volatile("msr TRCCONFIGR, %0" : : "r" (reg_val)); asm volatile("isb"); |
如果每一条指令都要使用一句 "asm volatile(“xxx”)显得过于麻烦,可以将多条汇编指令写到一块,如下:
asm volatile ( // Configure HCR_EL2 "orr w0, wzr, #(1 << 3)\n" "orr x0, x0, #(1 << 4)\n" "orr x0, x0, #(1 << 31)\n" "msr HCR_EL2, x0\n" ); |
应用实例
整理C语言中常见的编码或通用的模块,或者部分常用的算法。
基础实例
通用数据类型
typedef signed char __s8; typedef unsigned char __u8; typedef signed short __s16; typedef unsigned short __u16; typedef signed int __s32; typedef unsigned int __u32; typedef signed long long __s64; typedef unsigned long long __u64; typedef __s8 s8; typedef __u8 u8; typedef __s16 s16; typedef __u16 u16; typedef __s32 s32; typedef __u32 u32; typedef __s64 s64; typedef __u64 u64; typedef signed char int8_t; typedef unsigned char uint8_t; typedef signed short int16_t; typedef unsigned short uint16_t; typedef signed int int32_t; typedef unsigned int uint32_t; typedef signed long long int64_t; typedef unsigned long long uint64_t; |
用户态日志
/* Log level control, default: WARN */ enum log_level { LOG_LVL_ERR = 0, /* output: ERROR */ LOG_LVL_WARN, /* output: ERROR, WARNING */ LOG_LVL_DEBUG, /* output: ERROR, WARNING, DEBUG */ LOG_LVL_BUTT, }; #define LOG_LOG_LEVEL LOG_LVL_WARN #define LOG_ERROR(fmt, args...) \ do { \ if (LOG_LOG_LEVEL >= LOG_LVL_ERR) \ { \ printf("<ERROR>%s[%d]: "fmt"\n", __FUNCTION__, __LINE__, ##args); \ } \ } while (0); #define LOG_WARN(fmt, args...) \ do { \ if (LOG_LOG_LEVEL >= LOG_LVL_WARN) \ { \ printf("<WARN>%s[%d]: "fmt"\n", __FUNCTION__, __LINE__, ##args); \ } \ } while (0); #define LOG_INFO(fmt, args...) printf("<INFO>%s[%d]: "fmt"\n", __FUNCTION__, __LINE__, ##args); #define LOG_DEBUG(fmt, args...) \ do { \ if (LOG_LOG_LEVEL >= LOG_LVL_DEBUG) \ { \ printf("<DEBUG>%s[%d]: "fmt"\n", __FUNCTION__, __LINE__, ##args); \ } \ } while (0); |
日志输出到文件
/* Log level control, default: WARN */ enum log_level { LOG_LVL_ERR = 0, /* output: ERROR */ LOG_LVL_WARN, /* output: ERROR, WARNING */ LOG_LVL_DEBUG, /* output: ERROR, WARNING, DEBUG */ LOG_LVL_BUTT, }; #define LOG_LOG_LEVEL LOG_LVL_WARN #define LOG_OUTPUT_FILE_PATH ("dbg.log") #define LOG_ERROR(fmt, args...) \ do { \ FILE *out = NULL; \ if (LOG_LOG_LEVEL >= LOG_LVL_ERR) \ { \ out = fopen(LOG_OUTPUT_FILE_PATH, "a"); \ if(out != NULL) \ { \ fprintf(out, "<ERROR>%s[%d]: "fmt"\n", __FUNCTION__, __LINE__, ##args); \ fflush(out); \ fclose(out); \ } \ } \ } while (0);
#define LOG_WARN(fmt, args...) \ do { \ FILE *out = NULL; \ if (LOG_LOG_LEVEL >= LOG_LVL_WARN) \ { \ out = fopen(LOG_OUTPUT_FILE_PATH, "a"); \ if(out != NULL) \ { \ fprintf(out, "<WARN>%s[%d]: "fmt"\n", __FUNCTION__, __LINE__, ##args); \ fflush(out); \ fclose(out); \ } \ } \ } while (0); #define LOG_INFO(fmt, args...) \ do { \ FILE *out = NULL; \ out = fopen(LOG_OUTPUT_FILE_PATH, "a"); \ if(out != NULL) \ { \ fprintf(out, "<INFO>%s[%d]: "fmt"\n", __FUNCTION__, __LINE__, ##args); \ fflush(out); \ fclose(out); \ } \ } while (0); #define LOG_DEBUG(fmt, args...) \ do { \ FILE *out = NULL; \ if (LOG_LOG_LEVEL >= LOG_LVL_DEBUG) \ { \ out = fopen(LOG_OUTPUT_FILE_PATH, "a"); \ if(out != NULL) \ { \ fprintf(out, "<DEBUG>%s[%d]: "fmt"\n", __FUNCTION__, __LINE__, ##args); \ fflush(out); \ fclose(out); \ } \ } \ } while (0); |
内核态打印
printk(KERN_ERR "%s[%d]: \n", __FUNCTION__, __LINE__); |
用户态打印
printf("%s[%d]: \n", __FUNCTION__, __LINE__); |
标准C头文件
#include <assert.h> // 设定插入点 #include <ctype.h> // 字符处理 #include <errno.h> // 定义错误码 #include <float.h> // 浮点数处理 #include <iso646.h> // 对应各种运算符的宏 #include <limits.h> // 定义各种数据类型最值的常量 #include <locale.h> // 定义本地化C函数 #include <math.h> // 定义数学函数 #include <setjmp.h> // 异常处理支持 #include <signal.h> // 信号机制支持 #include <stdarg.h> // 不定参数列表支持 #include <stddef.h> // 常用常量 #include <stdio.h> // 定义输入/输出函数 #include <stdlib.h> // 定义杂项函数及内存分配函数 #include <string.h> // 字符串处理 #include <time.h> // 定义关于时间的函数 #include <wchar.h> // 宽字符处理及输入/输出 #include <wctype.h> // 宽字符分类 #include <complex.h> // 复数处理 #include <fenv.h> // 浮点环境 #include <inttypes.h> // 整数格式转换 #include <stdbool.h> // 布尔环境 #include <stdint.h> // 整型环境 #include <tgmath.h> // 通用类型数学宏 |
高级应用
前期工作中最明显的不足,有些算法、应用花了大量时间精力去研究,比如:KMP算法、josephus环、马踏棋盘等,但除了本地保存的源码,没有任何相关资料,这些知识,随时间流逝殆尽。
最大公约数
/* ex7_8.c */ #include "stdio.h" int gcd(int a,int b); void main(void) { int a=18,b=27; printf ("a=%d,b=%d,GCD=%d\n",a,b,gcd(a,b)); } /* 最大公约数 —— Greatest Common Divisor(GCD) */ int gcd(int a,int b) { if (a%b==0) return b; else return gcd(b,a%b); } |
数组循环左移
#include <stdio.h> // 将n个元素的数组循环左移p位 /* 算法思想: 将具有n个元素的数组A={a1,a2,...,an}从右至左划分为m-1个具 有p个元素的子数组A1,A2,...,Am-1,以及第m个不超过p个元素的子 数组Am,将A1与A2中p个元素按位置1到p相应交换,依次类推,再将 与A2交换后的A1与A3,..,Am-1,Am相应交换,交换结束得到前n-p个元 素的循环左移结果,即A2,...,Am-1,Am已经完成循环左移,此时还需 对A1进行循环左移,循环左移p-n%p位,截取A1进行递归,直到循环 左移位数p小于1,此时数组A循环左移结束 */ // 虽然是递归算法,但整个循环左移总交换次数不超过问题规模n // 时间复杂度O(n),空间复杂度O(1) #define N 50 // 数组长度 void print(int a[], int n) { int i = 0; for(i = 0; i < n; i++) printf("%3d", a[i]); puts(""); } void shift_left(int *a, int n, int p) { int i = 0, buf = 0; p %= n; if(n <= 1 || p < 1) return; for(i = n - 1; i >= p; i--) { buf = *(a + i - p); *(a + i - p) = *(a + n - 1 - (n - 1 - i) % p); *(a + n - 1 - (n - 1 - i) % p) = buf; } shift_left(a + n - p, p, p - n % p); } int main(void) { int Arr[N] = { 0 }; int i = 0; for(i = 0; i < N; i++) Arr[i] = i; print(Arr, N); shift_left(Arr, N, 13); print(Arr, N); return 0; } |
顺序表就地逆置
#include <stdio.h> #include <stdlib.h> // (1) 试以顺序表作存储结构,实现线性表就地逆置? // [分析] 线性表(0.1.2...n-1)的就地逆置即将对称元素交换。设线性表的长度为n,将线性表中第i个元素与第n-i-l个元素交换。 #define LIST_INIT_SIZE 100 #define LISTINCREMENT 10 typedef int ElemType; typedef struct{ ElemType *elem; int length; int listsize; } SqList; void InitList_Sq(SqList *L) { L->elem = (ElemType *)malloc(LIST_INIT_SIZE * sizeof(ElemType)); L->length = 0; L->listsize = LIST_INIT_SIZE; } void Dir_Turn(SqList *L) { // (1) int i = 0; for(i = 0; i < L->length / 2; i++) { *((*L).elem + i) = *((*L).elem + i) + *((*L).elem + L->length - i - 1); *((*L).elem + L->length - i - 1) = *((*L).elem + i) - *((*L).elem + L->length - i - 1); *((*L).elem + i) = *((*L).elem + i) - *((*L).elem + L->length - i - 1); } } int main() { int i = 0; SqList L; InitList_Sq(&L); for(i = 0; i < LIST_INIT_SIZE; i++) { *(L.elem + i) = i; L.length++; } printf("原始数据:\n"); for(i = 0; i < L.length; i++) printf("%3d", *(L.elem + i)); printf("\n就地逆置:\n"); Dir_Turn(&L); for(i = 0; i < L.length; i++) printf("%3d", *(L.elem + i)); printf("\n"); return 0; } |
Windows串口操作
#include <stdio.h> #include <Windows.h> void main(void) { char str[9] = { 0 }; HANDLE hCom; hCom = CreateFile(TEXT("COM3"), //COM1口 GENERIC_READ | GENERIC_WRITE, // 允许读和写 0, // 独占方式 NULL, OPEN_EXISTING, // 打开而不是创建 0, // 同步方式 NULL); if (hCom == (HANDLE)-1) printf("打开COM失败!\n"); else printf("COM打开成功!\n"); SetupComm(hCom, 1024, 1024); // 输入缓冲区和输出缓冲区的大小都是1024 COMMTIMEOUTS TimeOuts; // 设置超时 TimeOuts.ReadIntervalTimeout = 1000; TimeOuts.ReadTotalTimeoutMultiplier = 500; TimeOuts.ReadTotalTimeoutConstant = 5000; TimeOuts.WriteTotalTimeoutMultiplier = 500; TimeOuts.WriteTotalTimeoutConstant = 2000; SetCommTimeouts(hCom, &TimeOuts); DCB dcb; GetCommState(hCom, &dcb); dcb.BaudRate = 9600; // 波特率为9600 dcb.ByteSize = 8; // 每个字节有8位 dcb.Parity = NOPARITY; // 无奇偶校验位 dcb.StopBits = ONE5STOPBITS; // 两个停止位 SetCommState(hCom, &dcb); DWORD wCount; // 读取的字节数 BOOL bReadStat; while (1) { PurgeComm(hCom, PURGE_TXCLEAR | PURGE_RXCLEAR); // 清空缓冲区 printf("%s\n", str); bReadStat = ReadFile(hCom, str, 9, &wCount, NULL); if (!bReadStat) printf("读串口失败!\n"); else { str[8] = '\0'; printf("%s\n", str); } Sleep(100); } } |
X86停止CPU
hlt执行需要ring0特权
asm volatile("hlt": : :"memory"); |
cli、hlt执行需要ring0特权
asm volatile( "cli\n" // 禁用中断 "hlt\n" // 进入停机状态 ); |
X86跳转到rip + offset位置执行
static void jump_to(void) { unsigned long rip; unsigned long offset = 0x22b0f2; asm volatile("movq (%%rip), %0" : "=r" (rip)); rip += offset; printk(KERN_ERR "%s[%d]: rip = 0x%p\n", __FUNCTION__, __LINE__, rip); asm volatile("jmp *%0" :: "r" (rip)); } |
X86破坏GDT
破坏GDT、页表、TLB:
64位:
static const struct desc_ptr no_gdt = {}; load_gdt(&no_gdt); asm volatile("mov %cr0, %rax\n\t" "and $0x7fffffff, %rax\n\t" "mov %rax, %cr0\n\t" "xor %rax, %rax\n\t" "mov %rax, %cr3\n\t" "mov $0x1, %rax\n\t" "syscall"); |
命名规范
编码规范和命名规范作为编码质量一部分,一般而言维持整体代码风格一致更具意义。
驼峰风格
驼峰命名法又可以分成大驼峰命名法和小驼峰命名法。
驼峰风格,一般应用代码或高级语言使用较多,比如:常见python、java、shell、go代码编写多用驼峰风格。
大驼峰
大驼峰命名法,即为多个不同单词拼接作为变量(或函数)名称,每个单词的首字母要大写。
比如:
void QuickSort(......); int MyName; int MyAge; |
小驼峰
小驼峰命名法,即为多个不同单词拼接作为变量(或函数)名称,除去第一个单词首字母小写,其余单词首字母要大写。
比如:
void quickSort(......); int myName; int myAge; |
匈牙利风格
在变量名面前加上属性的标识。目前基本已经启用,可能在一些老项目还能看到,为什么弃用,因为现在的编辑工具都很高级。
比如:
char cMyName;//c即表示为char类型 float fScore;//f即表示score为float类型 int iAge;//i表示age为int类型 |
变量属性标识
前缀 | 含义 |
g_ | 全局变量 |
c_ | 常量 |
m_ | c++类成员变量 |
s_ | 静态变量 |
a | 数组 |
p | 指针 |
fn | 函数 |
v | 无效 |
h | 句柄 |
l | 长整型 |
b | 布尔 |
f | 浮点型(有时也指文件) |
dw | 双字 |
sz | 字符串 |
n | 短整型 |
d | 双精度浮点 |
c(通常用cnt) | 计数 |
ch(通常用c) | 字符 |
i(通常用n) | 整型 |
by | 字节 |
w | 字 |
r | 实型 |
u | 无符号 |
内核风格
一般也称之为下划线命名法,即为多个不同的单词拼接作为变量(或函数)名称,不同单词之间要用下划线连接(首字母不用大写)。
在驱动、内核或嵌入式领域该风格应用广泛,编程语言一般C、rust或C++应用较多。
比如:
void quick_sort(......); int my_name; int my_age; |
编码规范
编码规范其实可以有很多,不必刻意。
K&R风格、Allman风格是我狭窄眼光里最常见的。
K&R风格
Kernighan和Ritchie共同完成C语言”白皮书”C Programing Language一书的时候使用的风格。由于此书被当时的C程序员誉为C语言圣经,所以此风格也被争相模仿。左边的花括号出现在行的末尾。通过缩进保持代码的紧凑,缺点是不容易找到左边的花括号,尤其是写多重if……else if条件判断时相当晃眼睛。
if (line_num == MAX_LINES) { line_num = 0; page_num++; } |
Allman风格
Allman风格,Eric Allman(sendmail和其他一些UNIX工具的作者)的姓氏命名的,每个花括号单独占一行。容易检查括号的匹配。
if (line_num == MAX_LINES) { line_num = 0; page_num++; } |
Whitesmiths风格
Whitesmiths风格,它是因为Whitesmith C编译器而普及起来的,规定花括号采用缩进形式。
VxWorks中大部分代码是Whitesmiths风格,读起来一点点难受。
if (line_num == MAX_LINES) { line_num = 0; page_num++; } |
GUN风格
GUN风格,它用于GUN项目所开发的软件中,它对花括号采用缩进,然后再进一步缩进内层语句。少见多怪!
if (line_num == MAX_LINES) { line_num = 0; page_num++; } |