volatile
Volatile意思是“易变的”,应该解释为“直接存取原始内存地址”比较合适。“易变”是因为外在因素引起的,像多线程,中断等。
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改。volatile 提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如 果没有 volatile 关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。所以遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
static
-
隐藏
静态全局变量 可以被该文件内的所有函数访问,但不能被其它文件内的函数(通过extern int a;的方式)访问。
静态函数 static 的作用仅限于隐藏。 -
保持变量内容持久
存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和 static 变量(在函数内部),只不过和全局变量比起来,static 可以控制变量的可见范围,说到底 static 还是用来隐藏的。- 重复调用同一个函数时,它的值不会因为函数调用的结束而被清除,当函数再次被调用时,它的值是上一次调用结束后的值。
-
默认初始化为0
其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是 0x00。
inline
内联函数,如果一些函数被频繁调用,不断地有函数入栈,即函数栈,会造成栈空间或栈内存的大量消耗。
- inline函数和带参数的宏的区别
inline()函数 | 带参数的宏 | |
---|---|---|
展开的时机 | 在编译的时候展开,因此inline关键字是一个编译关键字 | 在预处理时展开,因此#define关键字是一个预处理关键字 |
参数类型检查 | inline()函数是一种函数,会进行严格的参数类型检查 | 不会检查参数类型,只是做简单的字符串替换,因此在使用带参数的宏时会有一些副作用,编写程序是要人为预防 |
是否允许有复杂语句 | 不允许出现复杂语句,如果出现复杂语句,该函数将不会展开,例如递归,大型循环等 | 对此不做要求。宏只是做字符串替换操作,而不了解语句的含义 |
是否一定被展开 | 不一定,是否展开由编译器决定 | 一定,只要使用了宏就可以保证被展开 |
- inline和#define的区别:
inline由编译器一起处理,直接将比那以后的函数体插入调用的地方。
#define由预处理器处理,进行简单的文本替换,没有任何编译过程。
关于include的问题
some others:一个全局变量的问题,当两个文件A和B都定义了全局变量a的时候,这个两个文件中的a是没有关系的,要想在另一个文件中使用a只有用extern声明。宏的定义也是同样的道理。所以我们在.h文件中不定义变量,否则两个不同的文件如果都包含了这个头文件就会出现变量在两个文件中都定义了。同理,头文件中定义的宏,在编译器编译完后实际上是在A和B中都定义了这个宏。所以在头文件中的ifndef ### 语句不是防止A和B重复引用头文件,因为宏不会影响这两个文件,而是防止在A文件中有多次引用头文件的情况,比如头文件中包含头文件
1. 双引号 和 尖括号
#include <file2>
- 直接到系统指定的某些目录中去找某些头文件。gcc按照以下顺序查找file2:
-Idir1 -Idir2 ...
/usr/local/include
libdir/gcc/<target>/<version>/include
/usr/<target>/include
/usr/include
编译参数 -I 最优先
#include "file2"
- 先在当前目录内查找,查找不到再按尖括号顺序查找。
2. 链接阶段
在链接阶段,gcc编译器如何寻找库文件呢
通常查找路径如下:
任何由-rpath-link或-rpath选项指定的目录
LD_RUN_PATH环境变量(如果没有找到-rpath或-rpath-link选项)
-Ldir1 -Ldir2 ...
/usr/lib/gcc/<target>/<version>/
/usr/lib/
宏定义相关
1. 宏定义中的反斜杠""
https://www.jb51.net/article/255138.htm
#define _CPU_ISR_Disable( _isr_cookie ) \
do { \
_isr_cookie = AArch64_interrupt_disable(); \
} while (0)
在宏定义中 要换行必须使用 \ 结尾。
2. do{…}while(0)的妙用
https://www.jianshu.com/p/99efda8dfec9
#define DOSOMETHING() foo1(); foo2();
if(a>0)
DOSOMETHING();
/* 会导致: */
if(a>0)
foo1();
foo2();
被do{…}while(0)包裹住之后,不管在调用代码中怎么使用分号和大括号,而该宏总能确保其行为是一致的。
c语言面向对象思想
1. 封装
将结构体模拟成类
将函数指针或由函数指针组成的结构体 作为结构体成员 是类的方法。
其他结构体成员 是类的属性。
2. 继承
通过在子类中内嵌另一个结构体或结构体指针(父类),来模拟类的继承。
子类通过这个结构体(指针)成员去使用父类的属性和方法。
typedef struct {
rtems_termios_device_context context; /* 父类 */
const uintptr_t regs_base;
const uint32_t clock;
const uint32_t initial_baud;
...
} pl011_context;
typedef struct rtems_termios_device_context {
union {
/* Used for TERMIOS_POLLED and TERMIOS_IRQ_DRIVEN */
rtems_interrupt_lock interrupt;
/* Used for TERMIOS_IRQ_SERVER_DRIVEN and TERMIOS_TASK_DRIVEN */
rtems_mutex mutex;
} lock;
void ( *lock_acquire )(
struct rtems_termios_device_context *,
rtems_interrupt_lock_context *
);
void ( *lock_release )(
struct rtems_termios_device_context *,
rtems_interrupt_lock_context *
);
} rtems_termios_device_context;
pl011_context
是子类,rtems_termios_device_context
是父类
子类可以使用父类的属性和方法。
私有指针
接口
3. 多态
基类中的方法,是函数指针。函数指针 的不同实现就叫多态。
4. 通过基类地址获取子类中的信息
https://zhuanlan.zhihu.com/p/589703748
rtems_termios_device_context *base; /* 父类 */
pl011_context *context = (void *)base; /* 子类 */
当我调用函数只需要访问父类的信息(更加抽象的信息),而不需要访问子类的数据结构的时候,我们只需要将&stu->base这个值传下去。
function2(struct people * p){
//只需要访问people父类数据结构
}
function2(&stu->base);
这样一来在function2函数中只能看到父类people类型的信息 p->age,p->name。
话说回来,如果在函数的调用栈中,又突然需要访问子类信息怎么办呢?
使用上述的指针方法论,可以很方便的把父类扩展到子类,具体看下述代码实现。
function3(struct student * stu){
//需要访问子类信息
}
function2(struct people * p){
struct student * stu = (struct student *)p;
function3(stu);
}
function2(&stu->base);
看到,只需要将 &stu->base 强转为 (struct student *)类型即可,因为 stu指针和&stu->base是完全取值相同的。这个是根本的原因,也是C能做到这一点的保证,但是局限性在于这个性质只能用在结构体第一个成员指针上,因此一个子类只能继承一个父类。
在大型工程中,例如mesa,大量用到了这个写法,将子类的第一个结构体指针指向父类实体,然后再函数调用的过程中只传递父类实体的首地址,再有需要访问子类数据结构的时候将其还原。
这样的好处在于:
安全性,在C语言中,安全性是十分奢侈的,因为C的struct没有public private之类的概念,在多人协作中,我不希望其他协作者修改子类数据结构,因此我只把父类的地址传给你,让你无法访问我的私有成员。当然,当我真正需要的时候,也可以将其还原,从而访问到子类的数据类型。传递的指向小实体的指针,随时可以强转为指向大实体的指针。
结构体
结构体成员前加 . 点
这种方式称为指定初始化(designated initializer)
https://blog.csdn.net/cherylchenyajun/article/details/107920435
加 “.”的话可以不考虑赋值顺序,表示在这个结构体中选择这个变量来赋值,所以可以不考虑结构体中变量的顺序。
一般结构体赋值:
struct sgo sgo_up = {1, 2, "up", "down", 0, 1};
加 . 可以这样赋值:
struct sgo sgo_up =
{
.a = 1,
.p2 = "down",
.p1 = "up",
.d = 1,
};
结构体数组内部出现方括号
这种方式称为指定初始化(designated initializer)
https://blog.csdn.net/Mr_liu_666/article/details/105340972
https://www.cnblogs.com/clover-toeic/p/3737189.html
int arr[6] = { [0]=5, [1]=6, [3] =10, [4]=11 }; 或
int arr[6] = { [0]=5, 6, [3] =10, 11 }; 或
int arr[6] = { [3] =10, 11, [0]=5, 6 }; (指定顺序可变)
均等效于:int arr[6] = {5, 6, 0, 10, 11, 0};
int arr[]={ [0 ... 127]=-1 };
等效于:memset(arr, 0xFF, sizeof(arr));
struct是数据“尺子”
https://www.zhihu.com/question/20877352/answer/16478107
struct是layout信息,是数据“尺子”,不是数据本身;struct不会生成实际代码,不存在跨编译单元重复定义问题(但是在link层次有一致性要求),#include进来用不到的struct也无所谓,不会有副作用。
__attribute__((aligned(16)));
https://blog.csdn.net/iceboy314159/article/details/121915572
https://blog.csdn.net/fengbingchun/article/details/81321419
关键字__attribute__
允许你在定义struct、union、变量等类型时指定特殊属性。此关键字后面是跟着双括号括起来的属性说明。__attribute__
不属于标准C语言,它是GCC对C语言的一个扩展用法。
__attribute__((aligned(n)))
:此属性指定了指定类型的变量的最小对齐(以字节为单位)。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。
结构体对齐
https://zhuanlan.zhihu.com/p/614269719
常量指针和指针常量 const
https://zhuanlan.zhihu.com/p/373141333
常量指针
常量指针是指针指向的内容是常量,可以有以下两种定义方式。
const int * num;
int const * num;
- 常量指针说的是不能通过这个指针改变变量的值,但可以通过其他的引用来改变变量的值。
- 常量指针指向的值不能改变,但这并不意味着指针本身不能改变,常量指针可以指向其他的地址。
指针常量
指针常量是指指针本身是个常量,不能再指向其他的地址,写法如下:
int *const num;
结构体const成员
https://juejin.cn/s/%E7%BB%93%E6%9E%84%E4%BD%93%20const%E6%88%90%E5%91%98
结构体中的const成员是指该成员的值在结构体被创建后就不能被修改了,是一种常量。