一、C语言学习心得记录

函数递归

编写顺序

  1. 终结条件
  2. 输入下一级递归参数,调用下一级递归函数.
  3. 当前递归函数的操作代码,在下一级递归函数执行完成后执行的操作代码.
#include <stdio.h>
#include <string.h>void revert(char *s, int len)
{// 终结条件if(len <= 1)return;// 1,将中间部分翻转revert(s+1, len-2);// 2,调换首尾两个字符char tmp;tmp = s[0];s[0] = s[len-1];s[len-1] = tmp;
}int main(void)
{char s[] = "abcdefg";// 翻转revert(s, strlen(s));printf("%s\n", s);
}

函数参数之数组指针传入

知识点:

任何数组名,都可能有两个含义:例如 int a[3]; 1,代表整块数组:
a. 在定义语句中的时候: int a[3];
b. 在取址的时候:&a;
c. 在使用 sizeof 计算内存大小的时候:printf(“%d\n”, sizeof(a)); 2,除了上述情况之外,其他任何情况都代表首元素的地址

注意: 当数组作为函数参数传递的时候,表示是指针,不能用sizeof(名称),求出来是计算机指针字长。

知识点:

sumup(a, b, c, 4); // 等价于sumup(&a[0], &b[0], &c[0]);

知识点:三者等价

void sumup(int a[ ], int b[ ], int c[ ], int len)
void sumup(int *a, int *b, int *c, int len) // 本质形式
void sumup(int a[4], int b[4], int c[4], int len)

二维指针传入:代码

//函数外定义
int m, n;
int a[m][n];
int b[m][n];
copy(m, n, a, b);
copy(m, n, &a[0], b);
//函数定义
void copy(int m, int n, int (*a)[n], int b[m][n])

理解int (*a)[n]:

  1. (*a) *声明a是指针变量, (*a)括起来提前声明为指针.
  2. int [n] 声明a指针变量指向的类型,是int型的数组
    删除线格式
  3. 定义指针变量的时候需要思考是否有指定的变量内存空间可以指向,没有则需要自己初始化

C语言变量空间分布


枚举 enum

枚举类型 枚举常量列表
重要作用:
1,用有意义的单词,代替无意义的数字
2,限定整型的数值范围
3,如果没有对任何枚举常量赋值,默认从0开始递增
如果有赋值x,那么后续的枚举常量x开始递增
注意:
C语言只实现了枚举的部分特性,不包含以上第2条
因此在C语言中,枚举类型跟int型语法上完全一样
C++才真正实现了枚举

  1. 枚举列表中的 Mon、Tues、Wed 这些标识符的作用范围是全局的(严格来说是 main() 函数内部),不能再定义与它们名字相同的变量。
  2. Mon、Tues、Wed 等都是常量,不能对它们赋值,只能将它们的值赋给其他的变量。
    枚举和宏其实非常类似:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可以将枚举理解为编译阶段的宏。
    Mon、Tues、Wed 这些名字都被替换成了对应的数字。这意味着,Mon、Tues、Wed 等都不是变量,它们不占用数据区(常量区、全局数据区、栈区和堆区)的内存,而是直接被编译到命令里面,放到代码区,所以不能用&取得它们的地址。
    http://c.biancheng.net/view/2034.html
enum colors {red=0, blue=1, green=2};
colors =100;//可以这样无意义赋值
enum week{ a = 1, b = 5, c = 3} d;d = 10;printf("a=%d d=%d\r\n",sizeof(a),sizeof(d));

结果

a=4 d=4

// C语言中,经常使用宏来定义常量,代替枚举
// 约定俗成的规矩: 宏用大写字母
// C++中,宏没落了,枚举有用正规的定义

#define RED 0
#define BLUE 1
#define GREEN 2
typedef enum
{
GetCarVelocity_STATE_Door1_START, // 车辆进入第一个光电门
GetCarVelocity_STATE_Door1_FINISH, // 车辆出去第一个光电门
GetCarVelocity_STATE_Door2_START, // 车辆进入第二个光电门
GetCarVelocity_STATE_Door2_FINISH // 车辆出去第个光电门
}GetCarVelocityStateTypedef

联合体 union

4.1 定义

联合”与“结构”有一些相似之处。但两者有本质上的不同。在结构中各成员有各自的内存空间,一个结构体变量的总长度大于等于各成员长度之和。而在“联合”中,各成员共享一段内存空间,一个联合变量的长度等于各成员中最长的长度。应该说明的是,这里所谓的共享不是指把多个成员同时装入一个联合变量内,而是指该联合变量可被赋予任一成员值,但每次只能赋一种值,赋入新值则冲去旧值。如下面介绍的“单位”变量,如定义为一个可装入“班级”或“教研室”的联合后,就允许赋予整型值(班级)或字符型(教研室)。要么赋予整型值,要么赋予字符型,不能把两者同时赋予它。联合类型的定义和联合变量的说明:一个联合类型必须经过定义之后,才能把变量说明为该联合类型。

联合体,也叫共用体
本质特征: 所有的成员,共用同一块内存
1,有什么用: 存储一些互斥的量
2,只有最后一个被赋值的成员是有效的
3,最常用的场合:
作为结构体的一员,表达一些互斥的信息
联合体很少单独使用

标签:
1,用来区分不同的联合体
2,可以省略,但省略之后只能在模板中定义变量
一般情况下,作为单独的联合体,不省略标签
当作为结构体的成员时,一般会省略标签

联合体的尺寸、m值:
1,联合的尺寸取决于最大的成员
2,联合的m值取决于成员中最大的m值

4.2联合体的应用示例

1、检测当前处理器是大端模式还是小端模式?

  1. 联合体union的存放顺序是所有成员都从低地址开始存放
  2. 16位操作系统中,int 占16位;在32位操作系统中,int 占32位。但是现在人们已经习惯了 int 占32位,因此在64位操作系统中,int 仍为32位。64位整型用 long long 或者 __int64
  3. char范围-128-127 0xFFFFFFFFFFFFFF80-0x7d 64位

之前分享的《什么是大小端模式?》中已经有介绍怎么判断当前处理器的大小端问题:

现在,可以使用联合体来做判断:

2、分离高低字节
单片机中经常会遇见分离高低字节的操作,比如进行计时中断复位操作时往往会进行

(65535-200)/256,

(65535-200)%256

这样的操作,而一个除法消耗四个机器周期,取余也需要进行一系列复杂的运算,如果在短时间内需要进行很多次这样的运算无疑会给程序带来巨大的负担。其实进行这些操作的时候我们需要的仅仅是高低字节的数据分离而已,这样利用联合体我们很容易降低这部分开销。

代码:

union div
{
int n; // n中存放要进行分离高低字节的数据
char a[2]; // 在keil c中一个整形占两个字节,char占一个字节,所以n与数组a占的字节数相同
}test;
test.n = 65535-200; // 进行完这句后就一切ok了,下面通过访问test中数组a的数据来取出高低字节的数据
TH1 = test.a[0]; // test.a[0]中存储的是高位数据
TL1 = test.a[1]; // test.a[1]中储存了test.n的低位数据

联合体内数据是按地址对齐的。具体是高位数据还是低位数据要看平台的大小端模式,51是大端,stm32默认是小端,如果其他编译器还请自测。仅仅用了一条减法指令就达到了除法、取余的操作,在进行高频率定时时尤为有用。

3、寄存器封装
看看TI固件库中寄存器是怎么封装的:
所有的寄存器被封装成联合体类型的,联合体里边的成员是一个32bit的整数及一个结构体,该结构体以位域的形式体现。这样就可以达到直接操控寄存器的某些位了。比如,我们要设置PA0引脚的GPAQSEL1寄存器的[1:0]两位都为1,则我们只操控两个bit就可以很方便的这么设置:

GpioCtrlRegs.GPAQSEL1.bit.GPIO0 = 3

或者直接操控整个寄存器:

GpioCtrlRegs.GPAQSEL1.all |=0x03

union–一道经典的C++笔试题

#i nclude <stdio.h>
union
{
int i;
char x[2];
}a;  void main()
{
a.x[0] = 10;
a.x[1] = 1;
printf("%d",a.i);
}
答案:266 (低位低地址,高位高地址,内存占用情况是Ox010A)
b)
main()
{
union{ /*定义一个联合*/
int i;
struct{ /*在联合中定义一个结构*/
char first;
char second;
}half;
}number;
number.i=0x4241; /*联合成员赋值*/
printf("%c%c/n", number.half.first, mumber.half.second);
number.half.first='a'; /*联合中结构成员赋值*/
number.half.second='b';
printf("%x/n", number.i);
getch();
}

答案: AB (0x41对应’A’,是低位;Ox42对应’B’,是高位)
6261 (number.i和number.half共用一块地址空间)

union中的各个成员共用一块内存,而且这块内存的大小是和union中占空间最大的元素所占空间一样大(例如上边T1,sizeof(a)=4)

而且对union中不同成员的写操作,会覆盖其他成员的值

T1:

main函数中对union变量a中的数组X进行赋值,由于会分配4个字节的空间,但是x只占用了两个字节的空间,而且在赋值时从低地址开始,

所以a在内存中的分布是这样的:

从高位到低位读也就是0x00 00 01 0A 注意:低位两个字节是01 0A 不是10 A0(我就是当时把这个搞错了。。)

当输出a.i时,由于占用同样一块内存,所以会读出四个字节,转换为10进制也就是266

还有一个误区就是:10在16进制中是A,但是A只有4位,但是一个字节有八位,在高位添0,所以10在内存中是0A,而不是A(当时我也搞错了。。。)

T2:

union中有两个变量,一个int i 占四个字节,一个结构体,占两个字节,所以这个union占有四个字节的内存

当给i赋值后内存中是这样的:

当输出结构体中的成员时:

printf(“%c%c/n”, number.half.first, mumber.half.second);

第一个字节(也就是0x41)被赋给 number.half.first,第二个字节(0X42)被赋给 number.half.second

于是分别输出了AB

当给结构体中的元素赋值后:

number.half.first=‘a’; /联合中结构成员赋值/
number.half.second=‘b’;

‘a‘=0x61,’b‘=0x62

内存中是这样的:

当输出i的时候,把四个字节都读出来

用十六进制输出就是0X00 00 62 61也就是6261
https://www.cnblogs.com/paulbai/articles/2711809.html

结构体 struct

知识点

64位系统: 字长64位
即意味着:
1,long型数据是64位的
2,任何指针尺寸也都是64位的

任何一个变量都有所谓的m值,对此,约定:
1,m值代表该变量的地址,必须是m的整数倍
2,m值代表该变量的尺寸,必须是m的整数倍

另外,m值越大,代表该变量对位置的要求越高,越挑剔
另外,m值是可以调整的,当然只能调大不能调小
另外,数组的m值等于数组元素类型的m值

// 以下结构体的大小是4+1+1+2 = 8字节
// 其中,第二个1是系统为了地址对齐自动填补上去的
struct node1
{int a;  // 4,m1=4char c; // 1, m2=1short d;// 2, m3=2
}; // M=max{m1, m2, m3}=4
//以下结构体的大小是32+32 = 64字节
struct node2
{char c; // 大小为1字节, m2=1int a;  // 大小为4字节,m1=4// 大小为2字节, m3=32short d __attribute__((aligned(32)));}; // M=max{m1, m2, m3}=32

解释:
第二个 char c 的地址作为结构体的首地址,所以取3者最大的m值32,后续int a可以放大char c的后面,所以int a和char c的大小加起来一起为32字节.

任何类型的指针变量一般都占4个字节,结构体由于存在边界对齐问题,实际占用内存比真实所需内存大,根据边界对齐要求降序排列结构成员可以最大限度地减少浪费。sizeof返回的值包含了结构中浪费的内存空间

位域

在结构体定义时,我们可以指定某个成员变量所占用的二进制位数(Bit),这就是位域。
有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可。例如开关只有通电和断电两种状态,用 0 和 1 表示足以,也就是用一个二进位。正是基于这种考虑,C语言又提供了一种叫做位域的数据结构。

在结构体定义时,我们可以指定某个成员变量所占用的二进制位数(Bit),这就是位域。请看下面的例子:

struct bs{unsigned m;unsigned n: 4;unsigned char ch: 6;
};

:后面的数字用来限定成员变量占用的位数。成员 m 没有限制,根据数据类型即可推算出它占用 4 个字节(Byte)的内存。成员 n、ch 被:后面的数字限制,不能再根据数据类型计算长度,它们分别占用 4、6 位(Bit)的内存。

n、ch 的取值范围非常有限,数据稍微大些就会发生溢出,请看下面的例子:

#include <stdio.h>
int main(){struct bs{unsigned m;unsigned n: 4;unsigned char ch: 6;} a = { 0xad, 0xE, '$'};//第一次输出printf("%#x, %#x, %c\n", a.m, a.n, a.ch);//更改值后再次输出a.m = 0xb8901c;a.n = 0x2d;a.ch = 'z';printf("%#x, %#x, %c\n", a.m, a.n, a.ch);return 0;
}
运行结果:
0xad, 0xe, $
0xb8901c, 0xd, :

对于 n 和 ch,第一次输出的数据是完整的,第二次输出的数据是残缺的。

第一次输出时,n、ch 的值分别是 0xE、0x24(‘$’ 对应的 ASCII 码为 0x24),换算成二进制是 1110、10 0100,都没有超出限定的位数,能够正常输出。

第二次输出时,n、ch 的值变为 0x2d、0x7a(‘z’ 对应的 ASCII 码为 0x7a),换算成二进制分别是 10 1101、111 1010,都超出了限定的位数。超出部分被直接截去,剩下 1101、11 1010,换算成十六进制为 0xd、0x3a(0x3a 对应的字符是 :)。

C语言标准规定,位域的宽度不能超过它所依附的数据类型的长度。通俗地讲,成员变量都是有类型的,这个类型限制了成员变量的最大长度,:后面的数字不能超过这个长度。

例如上面的 bs,n 的类型是 unsigned int,长度为 4 个字节,共计 32 位,那么 n 后面的数字就不能超过 32;ch 的类型是 unsigned char,长度为 1 个字节,共计 8 位,那么 ch 后面的数字就不能超过 8。

我们可以这样认为,位域技术就是在成员变量所占用的内存中选出一部分位宽来存储数据。

C语言标准还规定,只有有限的几种数据类型可以用于位域。在 ANSI C 中,这几种数据类型是 int、signed int 和
unsigned int(int 默认就是 signed int);到了 C99,_Bool 也被支持了。

关于C语言标准以及 ANSI C 和 C99 的区别,我们已在付费教程《C语言的三套标准:C89、C99和C11》中进行了讲解。
但编译器在具体实现时都进行了扩展,额外支持了 char、signed char、unsigned char 以及 enum 类型,所以上面的代码虽然不符合C语言标准,但它依然能够被编译器支持。
位域的存储
C语言标准并没有规定位域的具体存储方式,不同的编译器有不同的实现,但它们都尽量压缩存储空间。

位域的具体存储规则如下:

  1. 当相邻成员的类型相同时,如果它们的位宽之和小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。

以下面的位域 bs 为例:

#include <stdio.h>
int main(){struct bs{unsigned m: 6;unsigned n: 12;unsigned p: 4;};printf("%d\n", sizeof(struct bs));return 0;
}
运行结果:
4

m、n、p 的类型都是 unsigned int,sizeof 的结果为 4 个字节(Byte),也即 32 个位(Bit)。m、n、p 的位宽之和为 6+12+4 = 22,小于 32,所以它们会挨着存储,中间没有缝隙。
sizeof(struct bs) 的大小之所以为 4,而不是 3,是因为要将内存对齐到 4 个字节,以便提高存取效率,这将在《C语言内存精讲》专题的《C语言内存对齐,提高寻址效率》一节中详细讲解。
如果将成员 m 的位宽改为 22,那么输出结果将会是 8,因为 22+12 = 34,大于 32,n 会从新的位置开始存储,相对 m 的偏移量是 sizeof(unsigned int),也即 4 个字节。

如果再将成员 p 的位宽也改为 22,那么输出结果将会是 12,三个成员都不会挨着存储。

  1. 当相邻成员的类型不同时,不同的编译器有不同的实现方案,GCC 会压缩存储,而 VC/VS 不会。

请看下面的位域 bs:

#include <stdio.h>
int main(){struct bs{unsigned m: 12;unsigned char ch: 4;unsigned p: 4;};printf("%d\n", sizeof(struct bs));return 0;
}

在 GCC 下的运行结果为 4,三个成员挨着存储;在 VC/VS 下的运行结果为 12,三个成员按照各自的类型存储(与不指定位宽时的存储方式相同)。
m 、ch、p 的长度分别是 4、1、4 个字节,共计占用 9 个字节内存,为什么在 VC/VS 下的输出结果却是 12 呢?这个疑问将在《C语言和内存》专题的《C语言内存对齐,提高寻址效率》一节中为您解开。
3) 如果成员之间穿插着非位域成员,那么不会进行压缩。例如对于下面的 bs:

struct bs{unsigned m: 12;unsigned ch;unsigned p: 4;
};

在各个编译器下 sizeof 的结果都是 12。

通过上面的分析,我们发现位域成员往往不占用完整的字节,有时候也不处于字节的开头位置,因此使用&获取位域成员的地址是没有意义的,C语言也禁止这样做。地址是字节(Byte)的编号,而不是位(Bit)的编号。

无名位域
位域成员可以没有名称,只给出数据类型和位宽,如下所示:

struct bs{int m: 12;int  : 20;  //该位域成员不能使用int n: 4;
};

无名位域一般用来作填充或者调整成员位置。因为没有名称,无名位域不能使用。

上面的例子中,如果没有位宽为 20 的无名成员,m、n 将会挨着存储,sizeof(struct bs) 的结果为 4;有了这 20 位作为填充,m、n 将分开存储,sizeof(struct bs) 的结果为 8。
http://c.biancheng.net/view/2037.html

指针

void型指针可以强转为任意指针

宏定义 define

// 复合赋值语句: ({…})
// 常用语宏中,将多语句合并成一条语句
// 以下的斜杠,用来将多个物理行,变成一个逻辑行
// (void)(&_x==&_y); 的作用:
// 1,当用户使用两个不同的类型进行比对的时候,给出警告
// 2,void的作用是骗过编译器,不对后续比对本身给出警告

#define MAX(x, y)\({\typeof(x) _x = x;\typeof(y) _y = y;\(void)(&_x==&_y);\_x>_y ? _x : _y;\})
#define paster( n ) printf("token " #n" = %d\n ", token## n )
那么可以调用:
paster( 1 )paster( 2 )paster( 1234 )paster( ABCD );
意为:
printf( "token1 " = %d", token1 )printf( "token2 " = %d", token2 )printf( "token1234 " = %d", token1234 )printf( "tokenABCD " = %d", tokenABCD )

#define语句中的#是把参数字符串化,##是连接两个参数成为一个整体。

注意:#n 两边的引号必须要

// ## 两边的空格可有可无,不影响

多次宏定义只取最后一个宏定义,可能和编译器版本有关

头文件

// 仔细分析,头文件可以包含如下内容:
// 0,其他头文件
// 1,普通函数的声明
// 2,宏定义
// 3,全局变量的声明
// 4,静态函数的定义

头文件定义

// 以下两句话,能防止头文件被重复包含
#ifndef __HEADER_H // 如果没有定义该宏,则编译以下内容
#define __HEADER_H // 立刻定义该宏,使得下一次的判断不成立

静态函数

// 静态函数的定义
// 普通函数:在所有的.c文件中都可见
// 静态函数:只在.c文件内部可见

static void static_func(void)
{printf("我是一个静态函数\n");
}

因为只在.c文件内部可见,所以每个.c文件都可以有static_func(void)函数。
猜测效果:不同c文件有相同的函数名,但是不同的函数操作,是否可以实现类似于c++的继承

const只读变量

const int a =1 / int const a=1 变量a的值不能改变
const int *a=&b/ int const *a=&b (常目标指针)指针变量a指向的值不能改变,b可以改变内容(常用)
int * const a=&b (常指针)指针的指向不能改变
int const *const a=&b 指针指向不能改变,指向的值也不能改变
理解:代码从右往左看,const靠近的那个变量就修饰为只读。
int const *a=&b *a等于是引用变量,const把引用变量的方式修饰为只读 int声明为整数型,放前放后都可以
int * const a=&b a为地址值,指向地址只读=指针变量指向不可改变

inline

编译期,内联函数会在它被调用的位置上将自身的代码展开
Linux 内核常常使用 static 修饰内联函数,因为可以避免函数的重复定义。

static

static
a) static在面向过程编程中的使用场景包括三种:

  1. 修饰函数体内的变量(局部)
  2. 修饰函数体外的变量(全局)
  3. 修饰函数
    第一种情况,static延长了局部变量的生命周期,static的局部变量,并不会随着函数的执行结束而被销毁,当它所在 的函数被第再次执行时,该静态局部变量会保留上次执行结束时的值。
    对于后面的两种情况,static是对它修饰的对象进行了作用域限定,static修饰的函数以及函数外的变量,都是只能在当前的源文件中被访问,其它的文件不能直接访问。当多个模块中有重名的对象出现时,我们不妨尝试用static进行修饰。
    b)在面向对象编程中,static可以被用来修饰类内数据成员和成员函数。
  4. 修饰数据成员
    *)被static修饰的数据成员实际上相当于类域中的全局变量。因此,对于类的每个对象来说,它是共有的。它在整个程序中只有一份拷贝,只在定义时分配一次内存,供该类所有的对象使用,其值可以通过每个对象来更新。由于静态数据成员存储在全局数据区,因此,在定义时就要分配内存,这也就导致静态数据成员不能在类声明中定义。

判断系统是小端序还是大端序

大端模式:高字节保存在内存的低地址
小端模式:低字节保存在内存的低地址
int–char的强制转换,是将低地址的数值截断赋给char
强制将char型量p指向i则p指向的一定是i的最低地址

int a = 0x12345678;
int b= 0x87654321;
char *p_a = (char*)&a;
char *p_b = (char*)&b;
char *p_a_tail = ((char*)&a)+3;
printf("address of a:%p\r\n",&a);
printf("address of b:%p\r\n",&b);
printf("p_a:%x\r\n",*p_a);
printf("p_b:%x\r\n",*p_b);
printf("address of p_a:%p\r\n",p_a);
printf("address of p_a_tail:%p\r\n",p_a_tail);
printf("p_a = %x\r\n",*p_a);
printf("p_a_tail = %x\r\n",*p_a_tail);
printf("address of p_b:%p\r\n",p_b);;
if (*p_a == 0x78)
{printf("小端序");
}
else if (*p_a == 0x12)
{printf("大端序");
}

变量在栈中地址是如何分布的

linux

address of a:0x7fffad867070
address of b:0x7fffad867074
p_a:78
p_b:21
address of p_a:0x7fffad867070
address of p_b:0x7fffad867074
little endian

address of a:0x7ffd11df2728
address of b:0x7ffd11df272c
p_a:78
p_b:21
address of p_a:0x7ffd11df2728
address of p_a_tail:0x7ffd11df272b
p_a = 78
p_a_tail = 12
address of p_b:0x7ffd11df272c
little endian

windows

address of a:0060FEF4
address of b:0060FEF0
p_a:78
p_b:21
address of p_a:0060FEF4
address of p_b:0060FEF0
little endian

address of a:0060FEE0
address of b:0060FEDC
p_a:78
p_b:21
address of p_a:0060FEE0
address of p_a_tail:0060FEE3
p_a = 78
p_a_tail = 12
address of p_b:0060FEDC
little endian

#include <stdio.h>
#include <stdlib.h>

int main()
{
int a = 0x12345678;
int b = 0x87654321;
int c[10] = {1,2,3};
char p_a = (char)&a;
char p_b = (char)&b;
char p_a_tail = ((char)&a) + 3;
printf(“address of a:%p\r\n”, &a);
printf(“address of b:%p\r\n”, &b);
printf(“p_a:%x\r\n”, *p_a);
printf(“p_b:%x\r\n”, *p_b);
printf(“address of p_a:%p\r\n”, p_a);
printf(“address of p_a_tail:%p\r\n”, p_a_tail);
printf(“p_a = %x\r\n”, *p_a);
printf(“p_a_tail = %x\r\n”, *p_a_tail);
printf(“address of p_b:%p\r\n”, p_b);

int i = 0;
for (i = 0; i < 10; i++)
{printf("value of c[%d]:%d\r\n", i, c[i]);printf("address of c[%d]:%p\r\n", i, &c[i]);
}if (*p_a == 0x78)
{printf("小端序");
}
else if (*p_a == 0x12)
{printf("大端序");
}
return 0;

}

address of a:0x7ffdd9c8478c
address of b:0x7ffdd9c84790
p_a:78
p_b:21
address of p_a:0x7ffdd9c8478c
address of p_a_tail:0x7ffdd9c8478f
p_a = 78
p_a_tail = 12
address of p_b:0x7ffdd9c84790
value of c[0]:1
address of c[0]:0x7ffdd9c847b0
value of c[1]:2
address of c[1]:0x7ffdd9c847b4
value of c[2]:3
address of c[2]:0x7ffdd9c847b8
value of c[3]:0
address of c[3]:0x7ffdd9c847bc
value of c[4]:0
address of c[4]:0x7ffdd9c847c0
value of c[5]:0
address of c[5]:0x7ffdd9c847c4
value of c[6]:0
address of c[6]:0x7ffdd9c847c8
value of c[7]:0
address of c[7]:0x7ffdd9c847cc
value of c[8]:0
address of c[8]:0x7ffdd9c847d0
value of c[9]:0
address of c[9]:0x7ffdd9c847d4
小端序

堆的分布也同样如此

int *a = ma
lloc(sizeof(int));
a = 0x12345678;

大小端序是单个数据存储方式
栈从高向低地址分配是数据访问方式
两者没关系

C语言、内存管理、堆、栈、动态分配

栈是由高地址向低地址扩展的数据结构,有先进后出的特点,即依次定义两个局部变量,首先定义的变量的地址是高地址,其次变量的地址是低地址。函数参数进栈的顺序是从右向左(主要是为了支持可变长参数形式)。

理解 类似于函数嵌套进行运行,第一个函数运行,里面的第二个函数调用在函数基础上再运行;退出也是先退出第二个函数,再退出第一个函数

https://blog.csdn.net/weixin_39371711/article/details/81783780

字符指针数组

char *a[ ]=(Morning", "Afternoon", "Evening","Night");

进去函数前确认进入的值和变量类型

题目一:

void GetMemory( char *p )
{p = (char *) malloc( 100 );
}void Test( void )
{char *str = NULL;GetMemory( str ); strcpy( str, "hello world" );printf( str );
}

【运行错误】传入GetMemory(char* p)函数的形参为字符串指针,在函数内部修改形参并不能真正的改变传入形参的值。执行完

char *str = NULL;
GetMemory( str );

后的str仍然为NULL。编译器总是要为每个参数制作临时副本,指针参数p的副本是_p,编译器使_p=p。如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改,这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变。所以GetMemory并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存。
题目二:

char *GetMemory( void )
{ char p[] = "hello world"; return p;
}void Test( void )
{ char *str = NULL; str = GetMemory(); printf( str );
}

【运行错误】GetMemory中的p[]为函数内的局部自动变量,在函数返回后,内存已经被释放。这是很多程序员常犯的错误,其根源在于不理解变量的生存期。用调试器逐步跟踪Test,发现执行str=GetMemory语句后str不再是NULL指针,但是str的内容不是“hello world”,而是垃圾。
题目三:

void GetMemory( char **p, int num )
{*p = (char *) malloc( num );
}void Test( void )
{char *str = NULL;GetMemory( &str, 100 );strcpy( str, "hello" ); printf( str );
}

【运行正确,但有内存泄露】题目三避免了题目一的问题,传入GetMemory的参数为字符串指针的指针,但是在GetMemory中执行申请及赋值语句

*p = (char *) malloc( num );
后未判断内存是否申请成功,应加上
if ( *p == NULL )
{...//进行申请内存失败处理
}

也可以将指针str的引用传给指针p,这样GetMemory函数内部对指针p的操作就等价于对指针str的操作:

void GetMemory( char *&p)     //对指针的引用,函数内部对指针p的修改就等价于对指针str的修改
{p = (char *) malloc( 100 );
}void Test(void)
{char *str=NULL;GetMemory(str);strcpy( str, "hello world" );puts(str);}

https://blog.csdn.net/zhuxiaoyang2000/article/details/8084629

字符数组引用

c i[s]等价于*(i+s)等价于*(s+i)等价于s[i]

printf("%c","xyz"[1]);//'y'
printf("%c",*("xyz"+1));//'y'
printf("%c",*(1+"xyz"));//'y'
printf("%c",1["xyz"]);//'y'

习惯把"xyz"[1]看成*("xyz"+1)来思考问题,本质就是字符指针+1,再加*取内容运算符进行取内容操作。

switch区间匹配

switch(tmp)
{
case 0 … 9 :
tmp += tmp + ‘0’;
break;
case 10 … 15 :
tmp += tmp + ‘A’;
break;
}

switch参数宏定义(方便查看)

#define LEFT 1
#define RIGHT 2
switch (touchpanel(tp)){case LEFT://...break;case RIGHT://...break;}

linux C中的__ASSEMBLY__的作用

转载地址:http://my.oschina.net/u/930588/blog/134751

某些常量宏会同时出现被c和asm引用,而c与asm在对立即数符号的处理上是不同的。asm中通过指令来区分其操作数是有符号还是无符号,而不是通过操作数。而c中是通过变量的属性,而不是通过操作符。c中如要指名常量有无符号,必须为常量添加后缀,而asm则通过使用不同的指令来指明。如此,当一个常量被c和asm同时包含时,必须作不同的处理。故AFLAGS中将添加一项D__ASSEMBLY__,来告知预处理器此时是asm。

下面的代码取自kernel 2.6.10,include/asm-i386/page.h,L123-127

#ifdef __ASSEMBLY__
#define __PAGE_OFFSET        (0xC0000000)
#else
#define __PAGE_OFFSET        (0xC0000000UL)
#endif
 类似的大家也可以去分析一下__KERNEL__的作用.

逻辑左移和算术右移

C语言中移位操作符实现的是逻辑左移和算术右移,但是算术左移和逻辑左移的效果相同,算术右移和逻辑右移的效果不同,要实现逻辑右移可将操作数强制类型转化为无符号数。

算术右移需要考虑符号位,右移一位,若符号位为1,就在左边补1,;否则,就补0。

数组方式访问地址偏移

C51 宏定义:
#define XBYTE ((unsigned char volatile xdata *) 0)
调用方法:
#define PA XBYTE[0x7cff]
PA = 0X80;

通常是这么用的: *(unsigned char volatile xdata )(0x3000)=0xFF这类的方式来进行对外部绝对地址的字节访问。
其实XBYTE就相当于一个指向外部数据区的无符号字符型变量的指针(的名称,且当前的指针指向外部RAM的0地址),而在C里面指针一般和数组是可以混用的。可以直接用XBYTE[0xnnnn]或
(XBYTE+0xnnnn)访问外部RAM了。

等于使用[]的寻址方法,调用从0偏移0x7cff个8位char型地址进行写值操作

字符串复制

strcpy不安全,没有指定边界
使用strncpy(this->s,s,len);

extern (转载)

编辑本段编译、链接
1、 声明外部变量
  现代编译器一般采用按文件编译的方式,因此在编译时,各个文件中定义的全局变量是

互相透明的,也就是说,在编译时,全局变量的可见域限制在文件内部。下面举一个简单的例子。创建一个工程,里面含有A.cpp和B.cpp两个简单的C++源文件:

//A.cpp

int i;

void main()

{

}

//B.cpp

int i;

这两个文件极为简单,在A.cpp中我们定义了一个全局变量i,在B中我们也定义了一个全局变量i。

我们对A和B分别编译,都可以正常通过编译,但是进行链接的时候,却出现了错误,错误提示如下:

Linking…

B.obj : error LNK2005: “int i” (?i@@3HA) already defined in A.obj

Debug/A.exe : fatal error LNK1169: one or more multiply defined symbols found

Error executing link.exe.

A.exe - 2 error(s), 0 warning(s)

这就是说,在编译阶段,各个文件中定义的全局变量相互是透明的,编译A时觉察不到B中也定义了i,同样,编译B时觉察不到A中也定义了i。

但是到了链接阶段,要将各个文件的内容“合为一体”,因此,如果某些文件中定义的全局变量名相同的话,在这个时候就会出现错误,也就是上面提示的重复定义的错误。

因此,各个文件中定义的全局变量名不可相同。

在链接阶段,各个文件的内容(实际是编译产生的obj文件)是被合并到一起的,因而,定义于某文件内的全局变量,在链接完成后,它的可见范围被扩大到了整个程序。

这样一来,按道理说,一个文件中定义的全局变量,可以在整个程序的任何地方被使用,举例说,如果A文件中定义了某全局变量,那么B文件中应可以使用该变量。修改我们的程序,加以验证:

//A.cpp

void main()

{

i = 100; //试图使用B中定义的全局变量

}

//B.cpp

int i;

编译结果如下:

Compiling…

A.cpp

C:\Documents and Settings\wangjian\桌面\try extern\A.cpp(5) : error C2065: ‘i’ : undeclared identifier

Error executing cl.exe.

A.obj - 1 error(s), 0 warning(s)

编译错误。

其实出现这个错误是意料之中的,因为:文件中定义的全局变量的可见性扩展到整个程序是在链接完成之后,而在编译阶段,他们的可见性仍局限于各自的文件。

编译器的目光不够长远,编译器没有能够意识到,某个变量符号虽然不是本文件定义的,但是它可能是在其它的文件中定义的。

虽然编译器不够远见,但是我们可以给它提示,帮助它来解决上面出现的问题。这就是extern的作用了。

extern的原理很简单,就是告诉编译器:“你现在编译的文件中,有一个标识符虽然没有在本文件中定义,但是它是在别的文件中定义的全局变量,你要放行!”

我们为上面的错误程序加上extern关键字:

//A.cpp

extern int i;

void main()

{

i = 100; //试图使用B中定义的全局变量

}

//B.cpp

int i;

顺利通过编译,链接。

编辑本段函数
  extern 函数1

常见extern放在函数的前面成为函数声明的一部分,那么,C语言的关键字extern在函数的声明中起什么作用?

答案与分析:

如果函数的声明中带有关键字extern,仅仅是暗示这个函数可能在别的源文件里定义,没有其它作用。即下述两个函数声明没有明显的区别:

extern int f(); 和int f();

当然,这样的用处还是有的,就是在程序中取代include “*.h”来声明函数,在一些复杂的项目中,我比较习惯在所有的函数声明前添加extern修饰。

extern 函数2

当函数提供方单方面修改函数原型时,如果使用方不知情继续沿用原来的extern申明,这样编译时编译器不会报错。但是在运行过程中,因为少了或者多了输入参数,往往会造成系统错误,这种情况应该如何解决?

答案与分析:

目前业界针对这种情况的处理没有一个很完美的方案,通常的做法是提供方在自己的xxx_pub.h中提供对外部接口的声明,然后调用包涵该文件的头文件,从而省去extern这一步。以避免这种错误。

宝剑有双锋,对extern的应用,不同的场合应该选择不同的做法。
转载链接

三元运算符

三元运算符 性能更好,但是会带来类型转化的问题
例子

int x=4;
cout << ((x>4)?99.0:9);

输出的结果为9.0

printf()后不打印东西

printf会把东西送到缓冲区,而如果缓冲区不刷新到话,你便不会在屏幕上看到东西,而能导致缓冲区刷新到情况有这些:

1 强制刷新 标准输出缓存fflush(stdout);
2,放到缓冲区到内容中包含/n /r ;
3,缓冲区已满;
4,需要从缓冲区拿东西到时候,如执行scanf;

volatile

Volatile意思是“易变的”,应该解释为“直接存取原始内存地址”比较合适。
.一般用处:

一般说来,volatile用在如下的几个地方:
  1) 中断服务程序中修改的供其它程序检测的变量,需要加volatile;

当变量在触发某中断程序中修改,而编译器判断主函数里面没有修改该变量,因此可能只执行一次从内存到某寄存器的读操作,而后每次只会从该寄存器中读取变量副本,使得中断程序的操作被短路。
  2) 多任务环境下各任务间共享的标志,应该加volatile;

在本次线程内, 当读取一个变量时,编译器优化时有时会先把变量读取到一个寄存器中;以后,再取变量值时,就直接从寄存器中取值;当内存变量或寄存器变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致 。
  3) 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;

假设要对一个设备进行初始化,此设备的某一个寄存器为0xff800000。for(i=0;i< 10;i++) *output = i;前面循环半天都是废话,对最后的结果毫无影响,因为最终只是将output这个指针赋值为9,省略了对该硬件IO端口反复读的操作。
  https://www.cnblogs.com/hjh-666/p/11148119.html

寄存器操作

*pGPFDAT &= ~(7<<4);//清零第4位的4个bit
*pGPFDAT |= (tmp<<4);//写入第4位的4个bit

转移字符 \t \177 \7f

转义字符的初衷是用于ASCII编码,所以它的取值范围有限:

八进制形式的转义字符最多后跟三个数字,也即\ddd,最大取值是’\177’;//注意是单引号不是双引号
十六进制形式的转义字符最多后跟两个数字,也即\xdd,最大取值是’\7f’。

定义一个无返回值,无参数的函数类型fun

typedef void(*fun)();

逗号表达式

int a= 0, b = 0, c = 0;
c = (a-=a-5), (a=b,b+3);
printf(“%d,%d,%d\n”,a,b,c);

这是个逗号表bai达式,逗号表达式du有三点要领:
(1) 逗号表达式的运算过程为:从zhi左dao往右逐个计算表达式。
(2) 逗号表达式作为一个整体,它的值为最后一个表达式(也即表达式n)的值。
(3) 逗号运算符的优先级别在所有运算符中最低
由此可知:先计算: (a-=a-5),a=5,由于逗号表达式的优先级别低于’=',此时5赋值给c
然后计算(a=b,b+3),这个也是逗号表达式,此时赋值a=b,所以a=0,整个表达式的值为3
最后输出a=0,b=0,c=5

二、Linux系统命令

1.linux终端的复制和粘贴

在终端选中文字就是复制,右键文字就是粘贴

2.linux终端的复制和粘贴

关于环境变量
查看所有的环境变量
ubuntu:~$ env

查看单个指定的环境变量
ubuntu:~$ echo $PATH

知识点:环境变量 PATH 的作用是:规定系统中的可执行文件的位置。只要是处于这些位置中的可执行文件,执行的时候就不需要指定路径,直接执行即可。

设置 PATH(在其原有的路径的基础上,增添一个路径/home/gec)
// 临时设定 PATH :(所谓临时,指的是关闭终端之后就失效)
gec@ubuntu:~$ PATH=$PATH:/home/gec

// 永久设定 PATH :
将语句 PATH=$PATH:/home/gec 添加到文件 ~/.bashrc 的末尾

知识点:终端tty也是可以添加变量的。
所以可以临时设定PATH

ubuntu:~$ a = apple
ubuntu:~$ echo $a
apple

3.linux网络设置

设置网络参数

配置文件:/etc/network/interfaces,有两种配置方法:

固定IP:

# interfaces(5) file used by ifup(8) and ifdown(8)
auto lo
iface lo inet loopbackauto ens33
iface ens33 inet static
address 169.254.54.200 # IP地址,根据具体的网络环境来写
netmask 255.255.0.0    # 子网掩码
gateway 169.254.54.1  #网关地址
  1. 动态IP(自动获取IP)
# interfaces(5) file used by ifup(8) and ifdown(8)
auto lo
iface lo inet loopbackauto ens33
iface ens33 inet dhcp

将static改成dhcp,就由原来的静态固定IP改成动态自动获取IP。

重新加载网络配置和重启网络服务

gec@ubuntu:~$ sudo service networking force-reload
gec@ubuntu:~$ sudo service networking restart
    注意:老版本的Ubuntu可能不支持以上命令,可以试试下面这个
gec@ubuntu:~$ sudo /etc/init.d/networking force-reload
gec@ubuntu:~$ sudo /etc/init.d/networking restart

测试网络是否连通(ping)

gec@ubuntu:~$ ping www.qq.com
PING public-v6.sparta.mig.tencent-cloud.net (14.18.175.154) 56(84) bytes of data.
64 bytes from 14.18.175.154 (14.18.175.154): icmp_seq=1 ttl=52 time=12.0 ms
64 bytes from 14.18.175.154 (14.18.175.154): icmp_seq=2 ttl=52 time=11.7 ms
64 bytes from 14.18.175.154 (14.18.175.154): icmp_seq=3 ttl=52 time=10.8 ms
64 bytes from 14.18.175.154 (14.18.175.154): icmp_seq=4 ttl=52 time=11.8 ms
注:只要有返回延时时间,就代表网络是通的;如果卡主不动,代表网络不通或者网络拥塞

查看指定的网址的IP地址:

gec@ubuntu:~$ host www.qq.com
www.qq.com is an alias for public-v6.sparta.mig.tencent-cloud.net.
public-v6.sparta.mig.tencent-cloud.net has address 14.18.175.154
public-v6.sparta.mig.tencent-cloud.net has address 113.96.232.215
public-v6.sparta.mig.tencent-cloud.net has IPv6 address 240e:ff:f101:10::15f
注:
host成功返回域名的IP地址,代表当前网络是通的。
host成功返回域名的IP地址,代表当前系统的DNS解析是正常的。
DNS解析,就是通过域名,查询其对应的IP
如果ping成功,但host不成功,代表当前电脑的DNS配置有问题,解决办法:
gec@ubuntu:~$ sudo vi /etc/resolv.conf

在以上文件中,添加如下信息:

nameserver x.x.x.x(可以上百度搜索,选择离你家最近的)
gec@ubuntu:~$ sudo service systemd-resolved restart
以上命令用来重启 DNS 服务

查看或修改网络接口配置信息:(ifconfig)

查看当前活跃的网络接口

gec@ubuntu:~$ ifconfig
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500inet 192.168.1.103  netmask 255.255.255.0  broadcast 192.168.1.255inet6 fe80::20c:29ff:fe80:949c  prefixlen 64  scopeid 0x20<link>ether 00:0c:29:80:94:9c  txqueuelen 1000  (Ethernet)RX packets 2020  bytes 266623 (266.6 KB)RX errors 0  dropped 0  overruns 0  frame 0TX packets 8299  bytes 548748 (548.7 KB)TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536inet 127.0.0.1  netmask 255.0.0.0inet6 ::1  prefixlen 128  scopeid 0x10<host>loop  txqueuelen 1000  (Local Loopback)RX packets 37191  bytes 2722682 (2.7 MB)RX errors 0  dropped 0  overruns 0  frame 0TX packets 37191  bytes 2722682 (2.7 MB)TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
    注:其中,ens33是当前虚拟机的虚拟网卡lo是Linux系统的本地回环设备,一般不用管它

启停指定的网络接口(网卡)

gec@ubuntu:~$ sudo ifconfig ens33 up   ==> 启用ens33
gec@ubuntu:~$ sudo ifconfig ens33 down ==> 停用ens33以上命令也可以用如下命令替代,注意:有些系统不支持
gec@ubuntu:~$ sudo ifup ens33          ==> 启用ens33
gec@ubuntu:~$ sudo ifdown ens33        ==> 停用ens33

临时修改指定的网络接口的IP地址(即重启后失效)

gec@ubuntu:~$ sudo ifconfig ens33 192.168.1.103

查看当前网络状态:查看各种协议的网络信息

gec@ubuntu:~$ netstat

重新启动Ubuntu:

  sudo shutdown -r now

5.kill

向指定的一堆进程发送信号:(killall)

gec@ubuntu:~$ killall while       ==> 杀死当前系统中所有名为while的进程

6.find xargs grep(当前目录下所有.cpp文件中查找efg函数)

在当前目录下所有.cpp文件中查找efg函数
find . -name “*.cpp” | xargs grep ‘efg’
xargs展开find获得的结果,使其作为grep的参数

另外 rm mv等命令对大量文件操作时报错 -bash: /bin/rm: Argument list too long也可用xargs 解决
删除当前目录下所有.cpp文件
find . -name “*.cpp” | xargs rm

下面用find和grep来进一步说明:
// 将find找到的*.h文件列表的名称直接作为grep要查找的数据(输入)

gec@ubuntu:~$ find . -name "*.h" | grep 'abc'

// 将find找到的*.h文件列表的名称直接作为grep要查找的文件(参数)

gec@ubuntu:~$ find . -name "*.h" |xargs grep 'abc'
注:
1.find命令是把找到的结果输出到标准终端输出流中,管道改变输出流指向为指向grep的输入流。sort排序指令,把文件的文本内容提取出来输出缓冲区,清空文件;在缓冲区排序后,输出到文件,并且输出到标准终端输出流(方便配合管道指令)。
2. 第一种就是.h就是文本输入,第二种.h的文字段识别为文件,然后打开文件进行操作(或者说每行的内容识别成一个文件,把他打开再进行搜索)

7.查看文本内容

gec@ubuntu:~$ cat a.txt  ==> 查看 a.txt 的内容
gec@ubuntu:~$ head -n 20  a.txt  ==> 查看 a.txt 的前20行的内容
gec@ubuntu:~$ tail -n 20  a.txt  ==> 查看 a.txt 的末20行的内容
gec@ubuntu:~$ less a.txt  ==> 分屏查看 a.txt 的内容
gec@ubuntu:~$ more a.txt  ==> 分屏查看 a.txt 的内容(推荐?)
gec@ubuntu:~$ od a    ==> 查看二进制 a 的内容(默认八进制)
gec@ubuntu:~$ od -d a ==> 查看二进制 a 的内容(以十进制)
gec@ubuntu:~$ od -x a ==> 查看二进制 a 的内容(以十六进制)

8.od查看二进制文件

查看二进制文件,用od或hexdump命令。

$ od -tx1 -tc -Ax binFile
000000  61  62  63  64  65  31  32  33  34  35  61  62  63  64  65  31a   b   c   d   e   1   2   3   4   5   a   b   c   d   e   1
000010  32  33  34  35  61  62  63  64  65  31  32  33  34  35  61  622   3   4   5   a   b   c   d   e   1   2   3   4   5   a   b
000020  63  64  65  31  32  33  34  35  0ac   d   e   1   2   3   4   5  \n
000029
-tx1选项表示将文件中的字节以十六进制的形式列出来,每组一个字节(类似hexdump的-c选项)-tc选项表示将文件中的ASCII码以字符形式列出来(和hexdump类似,输出结果最左边的一列是文件中的地址,默认以八进制显示)-Ax选项要求以十六进制显示文件中的地址

https://zhidao.baidu.com/question/1860941808766537387.html

9.将文件内容排序、去除文件中的重复的相邻行

sort / uniq
作用:将文件内容排序、去除文件中的重复的相邻行
用法:

gec@ubuntu:~$ sort file ==> 将文件file的内容排序输出
gec@ubuntu:~$ uniq file ==> 将文件file的重复相邻行删除后输出
gec@ubuntu:~$ sort file | uniq ==> 先排序,再去除重复行

10.管道命令

管道: |
作用:将不同的命令连接起来,将前面命令的输出,作为后面命令的输入或者参数
用法:

gec@ubuntu:~$ cmd1 |      cmd2 ==> 将cmd1的输出,作为cmd2的输入
gec@ubuntu:~$ cmd1 |xargs cmd2 ==> 将cmd1的输出,作为cmd2的参数

11. 显示ELF格式目标文件的信息(查看代码段)

readlf -S

11. 查看外网ip

curl icanhazip.com

12. 设置终端代理

export http_proxy="http://127.0.0.1:10808"
export https_proxy="http://127.0.0.1:10808"
export http_proxy="socks5://127.0.0.1:7890"
export https_proxy="socks5://127.0.0.1:7890"

13. 查看历史命令

history

三、文件IO

1.把一个文件的内容复制到另一个文件

注: read函数特性,无法确定的把100个字节全部读取进入字节流,所以需要while进行处理
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>int main(int argc, char *argv[]) // ./copy    a       b     ...// argv[0] argv[1] argv[2] ...// argc = 外部参数总数
{// 判断外部参数(包括程序本身)个数的合法性if(argc != 3){printf("参数错误!用法: <源文件> <目标文件>\n");exit(0);}// 1,打开你要操作的文件// 打开第一个参数指定的文件,并将模式指定为只读: O_RDONLY// 此种打开模式代表: 文件必须存在,否则报错int fd1 = open(argv[1], O_RDONLY); // aif(fd1 == -1){// 万一不幸打不开,输出失败的原因perror("打开源文件失败");exit(0);}// 打开第二个参数指定的文件,并将模式指定为只写: O_WRONLY// 另外://    O_CREAT代表如果文件不存在则创建文件,并将权限设置为0644//    O_TRUNC代表如果文件已存在则清空文件int fd2 = open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, 0644); // bif(fd2 == -1){// 万一不幸打不开,输出失败的原因perror("打开目标文件失败");exit(0);}// 2,不断地读取源文件内容,并放入目标文件中char *buf = calloc(1, 100);while(1){// 从文件fd1中读取最多100个字节,放入buf中// read的返回值n,代表实际读取的字节数(n<=100)int n = read(fd1, buf, 100);// 读完了if(n == 0){printf("复制完毕!收工!\n");break;}// 出错了!if(n == -1){perror("读取源文件内容失败");break;}// 将buf中最多n个字节,写入fd2中// write的返回值m,代表实际写入的字节数(即m<=n)char *tmp = buf;while(n > 0){int m = write(fd2, tmp, n);n   -= m;tmp += m;}}// 3,关闭文件,释放相关资源close(fd1);close(fd2);free(buf);return 0;
}

2. 静态库、动态库的制作

静态库、动态库基本概念
静态库(相当于书店,东西只卖不借)
原理:编译时,库中的代码将会被直接复制一份到程序中
优点:程序不依赖于库、执行效率稍高
缺点:浪费存储空间、无法对用户升级迭代

动态库(相当于图书馆,东西只借不卖)
原理:编译时,程序仅确认库中功能模块的匹配关系,并未复制
缺点:程序依赖于库、执行效率稍低
优点:节省存储空间、方便对用户升级迭代

库文件的命名
都以 lib 作为前缀,比如 libx.a、liby.so
静态库的后缀为 .a ,动态库的后缀为 .so

静态库、动态库的制作和使用
不管是静态库还是动态库,其原料都是 *.o 文件
不管是静态库还是动态库,天生都是用来被其他程序链接的功能模块,一定不能包含main
编译生成 *.o 文件的方法如下:

gec@ubuntu:~$ gcc a.c -o a.o -c -fPIC

静态库的制作与使用:

// 将a.o b.o c.o ... 制作成一个静态库文件libx.a
gec@ubuntu:~$ ar rcs libx.a   a.o b.o c.o ...// 链接静态库libx.a
gec@ubuntu:~$ gcc main.c -o main -L . -lx
动态库的制作与使用:
// 将a.o b.o c.o ... 制作成一个动态库文件liby.so
gec@ubuntu:~$ gcc -shared -fPIC -o liby.so   a.o b.o c.o ...// 链接动态库liby.so
gec@ubuntu:~$ gcc main.c -o main -L . -ly

链接了动态库的程序怎么运行?
由于程序和动态库在编译之后,都可能会随着系统的变迁而发生位置改变,因此链接了动态库的程序在每次运行时都会动态地去寻找其所依赖的库文件,这是他们为什么被称为动态库的原因。
三种办法可以使得程序在运行时成功找到其所依赖的动态库:
设置环境变量:

gec@ubuntu:~$ export LD_LIBRARY_PATH=...

编译时预告:

gec@ubuntu:~$ gcc a.c -o a -L . -ly -Wl,-rpath=xxx/
注意:此处xxx/代表程序在运行时,动态库所在的路径

修改系统默认库路径(!!危险!!):

gec@ubuntu:~$ sudo vi /etc/ld.so.conf.d/libc.conf

在以上文件中,添加动态库所在路径即可。
此处要小心编辑,一旦写错可能会导致系统无法启动,这是一种污染系统的做法,不推荐

数据结构

内核链表

为什么内核链表是大结构体里面有小结构体链表+数据的形式?

理解:内核链表中的链表元素不与特定类型相关,具有通用性。如果是结构链表+结构数据,还需要指定结构体数据的地址,那就需要确认结构体数据的形式。而且在项目封装的时候,如果第二次定义结构链表+结构数据的时候,需要全部重新修改程序封装 。但是内核链表的小结构体则是直接脱离了本身,内核链表可以小结构体独立走,然后任意形式的大结构体包住小结构体即可实现携带传输。但是必须得用malloc进行创建? 堆的地址分配是低到高 ,zh

利用小结构体获取大结构体的地址

/**
* list_entry – get the struct for this entry
* @ptr:    the &struct list_head pointer.
* @type:    the type of the struct this is embedded in.
* @member:    the name of the list_struct within the struct.
*/
#define list_entry(ptr, type, member)\
((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))

理解 :
(char *)(ptr):使得指针的加减操作步长为一字节(理解:不然减偏移量的时候就是移动结构体指针)
(unsigned long)(&((type *)0)->member):获取结构体中指向的member地址与该member所在结构体的基地址的偏移量。


> 测试代码
typedef struct node
{int data; // 数据域 4 4+8+8struct list_head list; // 小结构体 16
}node;typedef struct node2
{struct list_head list; // 小结构体 16int data; // 数据域 4
}node2;
int main(void)
{node* a = (node*)malloc( sizeof(node) );if(a != NULL){// 数据域a->data = 123;// 小结构体要自己形成循环链表INIT_LIST_HEAD(&a->list);}node* p_a = ((node *)((char *)(&a->list)-(unsigned long)(&((node *)0)->list)));printf("address of a:%p\r\n",a);printf("address of data:%p\r\n",&a->data);printf("address of list:%p\r\n",&a->list);printf("address of (char *)&a->list:%p\r\n",(char *)&a->list);printf("address of ((node *)0)->list:%p\r\n",&((node *)0)->list);printf("(unsigned long)(&((node *)0)->list)=%ld\r\n",(unsigned long)(&((node *)0)->list));printf("address of (char *)(&a->list)-(unsigned long)(&((node *)0)->list:%p\r\n",\(char *)(&a->list)-(unsigned long)(&((node *)0)->list));printf("address of p_a:%p\r\n",p_a);printf("sizeof(a->data)=%ld\r\n",sizeof(a->data));printf("sizeof(a->list)=%ld\r\n",sizeof(a->list));node2* b = (node2*)malloc( sizeof(node2) );if(b != NULL){// 数据域b->data = 123;// 小结构体要自己形成循环链表INIT_LIST_HEAD(&b->list);}node2* p_b = ((node2 *)((char *)(&b->list)-(unsigned long)(&((node2 *)0)->list)));printf("address of b:%p\r\n",b);printf("address of data:%p\r\n",&b->data);printf("address of list:%p\r\n",&b->list);printf("address of (char *)b->list:%p\r\n",(char *)&b->list);printf("address of ((node2 *)0)->list:%p\r\n",&((node2 *)0)->list);printf("(unsigned long)(&((node2 *)0)->list)=%ld\r\n",(unsigned long)(&((node2 *)0)->list));printf("address of (char *)(&b->list)-(unsigned long)(&((node2 *)0)->list:%p\r\n",\(char *)(&b->list)-(unsigned long)(&((node2 *)0)->list));printf("address of p_b:%p\r\n",p_b);printf("sizeof(b->data)=%ld\r\n",sizeof(b->data));printf("sizeof(b->list)=%ld\r\n",sizeof(b->list));return 0;
}

结果
address of a:0x55ce33cc9260
address of data:0x55ce33cc9260
address of list:0x55ce33cc9268
address of (char *)&a->list:0x55ce33cc9268
address of ((node *)0)->list:0x8
(unsigned long)(&((node *)0)->list)=8
address of (char *)(&a->list)-(unsigned long)(&((node *)0)->list:0x55ce33cc9260
address of p_a:0x55ce33cc9260
sizeof(a->data)=4
sizeof(a->list)=16
address of b:0x55ce33cc9690
address of data:0x55ce33cc96a0
address of list:0x55ce33cc9690
address of (char *)b->list:0x55ce33cc9690
address of ((node2 *)0)->list:(nil)
(unsigned long)(&((node2 *)0)->list)=0
address of (char *)(&b->list)-(unsigned long)(&((node2 *)0)->list:0x55ce33cc9690
address of p_b:0x55ce33cc9690
sizeof(b->data)=4
sizeof(b->list)=16

四、linux系统文件操作(建立工程模板)

包含头文件

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mman.h>#include <linux/fb.h>
#include <linux/input.h>#include <string.h>
#include <errno.h>
#include <time.h>
#include <fcntl.h>

1.参数初始化(设计功能后,列一个特殊情况检查表格进行检查)

if(argc != 2){printf("参数错误,用法: <BMP图片>\n");exit(0);}// 判断命令行参数// 最终要显示的目标target,可能是当前文件夹(默认)// 也可能是用户指定的一个文件夹,或者图片char *target = ((argc==1) ? "." : argv[1]);struct stat info;bzero(&info, sizeof(info));stat(target, &info);if(S_ISREG(info.st_mode)){if(!isbmp(target)){printf("我只能显示BMP图片,再见!\n");exit(0);}struct node *picnode = new_node(target);display(picnode);}// 遍历文件夹else if(S_ISDIR(info.st_mode)){// 搞一个空链表struct node * head = init_list();DIR *dp = opendir(target);chdir(target);// 读取目录,将所有的BMP图片信息存入链表中(不读取RGB)struct dirent *ep;while(1){ep = readdir(dp);// 读完目录了if(ep == NULL)break;// 只挑BMP图片放入链表if(isbmp(ep->d_name)){printf("图片:[%s]入链表\n", ep->d_name);// 搞个新节点,并置入链表struct node * picnode = new_node(ep->d_name);head = list_add_tail(head, picnode);}}if(empty(head)){printf("你指定的目录中没有BMP图片,再见!\n");exit(0);}}elseprintf("我是相册程序,你指定的文件不是我的菜!\n");return 0;
}

1.1理解 argc argv参数之间的关系

2.打开文件

 int pic = open(argv[1], O_RDONLY);

3.读取图片的格式头,获取其各种信息

3.1 bmp图片的读取

    struct bitmap_header head;struct bitmap_info   info;bzero(&head, sizeof(head));bzero(&info, sizeof(info));read(pic, &head, sizeof(head));read(pic, &info, sizeof(info));

3.2 读取触摸屏的数据

struct input_event buf;int x, y;int xnew, ynew;bool xready = false;bool yready = false;while(1){bzero(&buf, sizeof(buf));read(tp, &buf, sizeof(buf));if(buf.type == EV_ABS && buf.code == ABS_X){xnew = buf.value;xready = true;}if(buf.type == EV_ABS && buf.code == ABS_Y){ynew = buf.value;yready = true;}if(xready && yready && (xnew!=x && ynew!=y)){printf("(%d, %d)\n", xnew, ynew);xready = false;yready = false;x = xnew;y = ynew;}}

4.映射LCD的显存(对应3.1图片读取)

int lcd = open("/dev/ubuntu_lcd", O_RDWR);
if(lcd == -1)
{perror("打开LCD失败");exit(0);
}
char *p = mmap(NULL, 800*480*4, PROT_READ|PROT_WRITE,MAP_SHARED, lcd, 0);
if(p == MAP_FAILED)
{perror("映射内存失败");exit(0);
}
bzero(p, 800*480*4); // 清屏(纯黑色)

5. 将图片的RGB读出,并置入显存中

int rgb_size = head.size-sizeof(head)-sizeof(info);
char *rgb = calloc(1, rgb_size);// 将图片中的RGB放入rgb中
int total = 0;
while(1)
{int n = read(pic, rgb+total, rgb_size);if(n == 0)break;total += n;rgb_size -= n;
}// 计算每一行的无效字节数
int pad = (4 - ((info.width*3) % 4))%4; // 0-3// 将rgb指向最末一行的起始位置
rgb += (info.width*3 + pad) * (info.height-1);// 将太大的图片按比例缩小
int wt = info.width/801+1;
int ht = info.height/481+1;
int scale = wt>ht ? wt : ht; // >=1// 让图片居中显示
int x = (800-info.width/scale)/2;
int y = (480-info.height/scale)/2;
char *tmp = p + (y*800 + x)*4;// 将rgb中的数据,妥善地放入LCD显存
for(int j=0, m=0; j<480-y && m<info.height; j++, m+=scale)
{for(int i=0, k=0; i<800-x && k<info.width; i++, k+=scale){int lcd_offset = 4*i + 800*4*j;int rgb_offset = 3*k - (info.width*3+pad)*m;memcpy(tmp+lcd_offset, rgb+rgb_offset, 3);}
}

6.释放资源

munmap(p, 800*480*4);
close(lcd);
close(pic);

7.串口数据发送

// 通过串口向读卡器发送'A'指令(探测指令)tcflush (fd, TCIFLUSH);write(fd, PiccRequest_IDLE, PiccRequest_IDLE[0]);

tcflush() 丢弃要写入引用的对象,但是尚未传输的数据,或者收到但是尚未读取的数据,取决于 queue_selector 的值:

TCIFLUSH   刷新收到的数据但是不读

TCOFLUSH  刷新写入的数据但是不传送

TCIOFLUSH  同时刷新收到的数据但是不读,并且刷新写入的数据但是不传送

将输出缓冲器清空,把输入缓冲区清空。缓冲区里的数据都废弃。

五、LINUX系统编程

1. 创建进程

#include "common.h"int main()
{pid_t pid = fork();if(pid > 0){printf("PID:%d, PPID:%d\n", getpid(), getppid());// 判断子进程的退出值int status;wait(&status);switch (WEXITSTATUS(status)){case 0:printf("子进程成功执行了任务!\n");break;case 1:printf("子进程任务失败:xxx\n");break;case 2:printf("子进程任务失败:yyy\n");break;default:printf("子进程任务失败:不明原因\n");break;}}if(pid == 0){// exec函数族// 功能: 让进程加载一段新的代码(旧瓶装新酒),覆盖原有的代码// 其中://   "./childProcess" 是要加载的程序//   "./childProcess", "ab", "12", NULL 是程序的参数列表
#if 0printf("%d\n", __LINE__);execl("./childProcess", "./childProcess", "ab", "12", NULL);printf("%d\n", __LINE__);
#endif#if 1// 没有获取环境变量PATH的值execl("/bin/ls", "/bin/ls", NULL);
#else// 从终端获取了环境变量PATH的值execlp("ls", "ls", NULL);
#endif}return 0;
}

理解:C程序调用shell脚本共有三种法子:system()、popen()、exec系列函数 。
system()不用你自己去产生进程,它已经封装了,直接加入自己的命令;exec 需要你自己 fork 进程,然后exec
自己的命令;popen()也可以实现执行你的命令,比system 开销小。

execl函数特点:: 当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。

用另一个新程序替换了当前进程的正文、数据、堆和栈段。

当前进程的正文都被替换了,那么execl后的语句,即便execl退出了,都不会被执行。

1.1fork()函数(两个进程)

// 将本进程复刻一个子进程
// 1,fork函数将本进程复制一份,并使之成为独立的子进程
// 2,子进程拥有父进程拷贝过来的一切代码,但只从fork函数往下开始执行
// 3,父子进程同时并发运行,此刻无法确定他们的执行次序.
// 4,fork函数在父子进程两处,分别返回不同的值(大于0是父进程,等于0是子进程)

pid_t pid = fork();
理解:子进程内没有子进程,所以返回值为0

1.2 缓冲区中的数据

进程间的数据是独立的

printf("缓冲区中的数据");pid_t pid = fork();if(pid > 0){// 查看内存中各个区域的数据是否受到影响printf("global:%d\n", global);printf("x:%d\n", x);printf("*p:%d\n", *p);}

输出结果:

理解:因为`printf("缓冲区中的数据");`没有加换行,所以没有打印大屏幕,而且存储在内存中。
fork()就复制了一份子进程,然后下一个换行符一并打印出来。

1.3 kill(0,SIGKILL)杀死该进程所在进程组的所有进程(即父进程和所有子进程)。

C程序中,kill(0,SIGKILL)将会杀死该进程所在进程组的所有进程(即父进程和所有子进程)。

2.创建daemon进程(建立工程模板)

1.可以被kill结束
2.目的:脱离终端,类似tcp连接这些,伪系统进程
#include "daemon.h"int main(void)
{pid_t a;int max_fd, i;/*********************************************1. ignore the signal SIGHUP, prevent theprocess from being killed by the shutdownof the present controlling termination**********************************************/signal(SIGHUP, SIG_IGN);/***************************************2. generate a child process, to ensuresuccessfully calling setsid()****************************************/a = fork();if(a > 0)exit(0);/******************************************************3. call setsid(), let the first child process runningin a new session without a controlling termination*******************************************************/setsid();/*************************************************4. generate the second child process, to ensurethat the daemon cannot open a terminal fileto become its controlling termination**************************************************/a = fork();if(a > 0)exit(0);/*********************************************************5. detach the daemon from its original process group, toprevent any signal sent to it from being delivered**********************************************************/setpgrp();/*************************************************6. close any file descriptor to release resource**************************************************/max_fd = sysconf(_SC_OPEN_MAX);for(i=0; i<max_fd; i++)close(i);/******************************************7. clear the file permission mask to zero*******************************************/umask(0);/****************************************8. change the process's work directory,to ensure it won't be uninstalled*****************************************/chdir("/");// Congratulations! Now, this process is a DAEMON!//pause();openlog("daemon_test", LOG_CONS | LOG_PID, LOG_DAEMON);return 0;
}

3.dup2()文件描述符重定向

//    int fd = open("a.txt", O_RDWR);//    // 功能: 复制0号描述符为fd
//    // 即从此之后,fd对应的文件与0一样
//    dup2(0, fd);//    char buf[50];
//    bzero(buf, 50);
//    read(fd, buf, 50);//    printf("%s", buf);
int fd = open("b.txt", O_RDWR|O_CREAT|O_TRUNC, 0644);// 功能: 将1覆盖fd,fd丧失了原来的文件关联,fd就变成跟1一样dup2(1, fd);write(fd, "abc", 3); // 向屏幕输出abc

3.1重定向到管道文件进行通信,理解ls | wc

 // 1,创建无名管道int fd[2];pipe(fd);// 2,创建子进程pid_t pid = fork();// 父进程if(pid > 0){// 将标准输出(1号描述符)重新定向到管道的写端dup2(fd[1], STDOUT_FILENO); // cp file1 file2// 此时,执行ls,他将会把数据写入1号描述符,即管道的写端execlp("ls", "ls", NULL);}// 子进程if(pid == 0){// 将标准输入(0号描述符)重新定向到管道的读端dup2(fd[0], STDIN_FILENO); // cp file1 file2close(fd[1]);// 此时,执行wc,他将会从0号描述符读取数据,即管道的读端execlp("wc", "wc", "-w", NULL);}

3.管道

3.1 无名管道

无名管道不存在管道文件,其借助于父子进程共享fork之前打开的文件描述符。(文件打开机制)其数据存储在内存中。

无名管道限制:只能使用于父子进程之间(无法跨越父子关系)

int main()
{// 创建无名管道int fd[2];pipe(fd);// 创建子进程pid_t pid = fork();// parentif(pid > 0){// 父进程只负责读取管道数据,所以最好关闭写端close(fd[1]);char buf[20];// 静静地等待子进程的消息...bzero(buf, 20);read(fd[0], buf, 20);printf("第一次收到子进程的消息:%s\n", buf);// 静静地等待子进程的消息...bzero(buf, 20);read(fd[0], buf, 20);printf("第二次收到子进程的消息:%s\n", buf);}// childif(pid == 0){// 子进程只负责将数据写入管道,所以最好关闭读端close(fd[0]);sleep(2);write(fd[1], "你好", 20);// 暂停
//        pause();}return 0;
}

3.2 有名管道

写者

int main(void)
{// 1,创建有名管道mkfifo("/home/gec/fifo", 0666);/*    // 设置为非阻塞int state = fcntl(fd, F_GETFL);state |= O_NONBLOCK;fcntl(fd, F_SETFL, state);*/// 2,打开管道int fd = open("/home/gec/fifo", O_RDWR);if(fd == -1){perror("打开管道失败");exit(0);}// 3,向对方说一句话write(fd, "abcd", 4);close(fd);return 0;
}

读者

  // 1,创建有名管道mkfifo("/home/gec/fifo", 0666);// 2,打开管道int fd = open("/home/gec/fifo", O_RDONLY);if(fd == -1){perror("打开管道失败");exit(0);}// 3,等待对方的消息char buf[10];bzero(buf, 10);read(fd, buf, 10);printf("对方的消息: %s", buf);close(fd);return 0;

3.3管道的读写特性

打开文件操作时,不允许管道以单边形式(只有一边,或者只读,或者只写)存在,这样会堵塞然后打不开。
堵塞等待写者:open(“xxx.fifo”,O_RDONLY);

4.信号

4.1sleep()延时函数会被信号的到来打断.

4.2 信号创建和处理(程序模板)

#include "common.h"// f是信号响应函数,注意接口是固定的
void f(int sig)
{// 回收了一个僵尸子进程int status;wait(&status);printf("%d号子进程已被回收\n", WEXITSTATUS(status));
}int main()
{// 关联信号与函数// 即: 将来如果我收到 SIGCHLD,就去执行fsignal(SIGCHLD, f);int n = 0;while(1){n++;pid_t pid = fork();// 父进程,1秒生一个孩子if(pid > 0){pause();sleep(1);continue;}// 子进程,生出来就死掉if(pid == 0){exit(n);}}return 0;
}
  1. Unix系统提供了两种方式来改变信号处置:signal() 和 sigaction()。

    signal() 的行为在不同Unix实现间存在差异,这也意味着对可移植性有所追求的程序绝不能使用此调用 来建立信号处理函数。故此,sigaction() 是建立信号处理器的首选API(强力推荐)。

  2. 同时SIGSTOP/SIGKILL这俩信号无法捕获和忽略。注意,经过实验发现,signal函数也会堵塞当前正在处理的signal,但是没有办法阻塞其它signal,比如正在处理SIG_INT,再来一个SIG_INT则会堵塞,但是来SIG_QUIT则会被其中断,如果SIG_QUIT有处理,则需要等待SIG_QUIT处理完了,SIG_INT才会接着刚才处理。

4.2.1细节:进程的信号挂起队列中,没有相同的信号(即相同的信号会被丢弃)。

理解:信号阻塞接触后,首先清零信号集中对应的信号位,然后执行信号处理函数

4.2.2进程在响应信号时,不同的信号会相互嵌套。

4.2.3挂起信号不会被子进程继承,但信号阻塞掩码会被继承。

4.2.4 getchar会被信号打断

判断代码

ch = getchar();
if (ch = -1 && errno == EINTR)
{printf("读操作被信号中断了");
}

4.2 sigaction

  1. signal和sigaction的区别 下面所指的signal都是指以前的older
    signal函数,现在大多系统都用sigaction重新实现了signal函数
    1、signal在调用handler之前先把信号的handler指针恢复;sigaction调用之后不会恢复handler指针,直到再次调用sigaction修改handler指针。
    :这样,(1)signal就会丢失信号,而且不能处理重复的信号,而sigaction就可以。因为signal在得到信号和调用handler之间有个时间把handler恢复了,这样再次接收到此信号就会执行默认的handler。(虽然有些调用,在handler的以开头再次置handler,这样只能保证丢信号的概率降低,但是不能保证所有的信号都能正确处理)
    2、signal在调用过程不支持信号block;sigaction调用后在handler调用之前会把屏蔽信号(屏蔽信号中自动默认包含传送的该信号)加入信号中,handler调用后会自动恢复信号到原先的值。
    (2)signal处理过程中就不能提供阻塞某些信号的功能,sigaction就可以阻指定的信号和本身处理的信号,直到handler处理结束。这样就可以阻塞本身处理的信号,到handler结束就可以再次接受重复的信号。
    3、sigaction提供了比signal多的多的功能,可以参考man

分割
2. sigaction,这个相对麻烦一些,函数原型如下:

int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);

函数到关键就在于struct sigaction

stuct sigaction
{void (*)(int) sa_handle;sigset_t sa_mask;int sa_flags;
}
1 #include <signal.h>2 #include <stdio.h>3 #include <unistd.h>4 5 6 void ouch(int sig)7 {8     printf("oh, got a signal %d\n", sig);9 10     int i = 0;11     for (i = 0; i < 5; i++)12     {13         printf("signal func %d\n", i);14         sleep(1);15     }16 }17 18 19 int main()20 {21     struct sigaction act;22     act.sa_handler = ouch;23     sigemptyset(&act.sa_mask);24     sigaddset(&act.sa_mask, SIGQUIT);25     // act.sa_flags = SA_RESETHAND;26     // act.sa_flags = SA_NODEFER;27     act.sa_flags = 0;28 29     sigaction(SIGINT, &act, 0);30 31 32     struct sigaction act_2;33     act_2.sa_handler = ouch;34     sigemptyset(&act_2.sa_mask);35     act.sa_flags = 0;36     sigaction(SIGQUIT, &act_2, 0);37 while(1){sleep(1);}38     return;}
  1. 阻塞,sigaction函数有阻塞的功能,比如SIGINT信号来了,进入信号处理函数,默认情况下,在信号处理函数未完成之前,如果又来了一个SIGINT信号,其将被阻塞,只有信号处理函数处理完毕,才会对后来的SIGINT再进行处理,同时后续无论来多少个SIGINT,仅处理一个SIGINT,sigaction会对后续SIGINT进行排队合并处理。

  2. sa_mask,信号屏蔽集,可以通过函数sigemptyset/sigaddset等来清空和增加需要屏蔽的信号,上面代码中,对信号SIGINT处理时,如果来信号SIGQUIT,其将被屏蔽,但是如果在处理SIGQUIT,来了SIGINT,则首先处理SIGINT,然后接着处理SIGQUIT。

  3. sa_flags如果取值为0,则表示默认行为。还可以取如下俩值,但是我没觉得这俩值有啥用。

SA_NODEFER,如果设置来该标志,则不进行当前处理信号到阻塞

SA_RESETHAND,如果设置来该标志,则处理完当前信号后,将信号处理函数设置为SIG_DFL行为

4.3 信号阻塞

    // 2,阻塞信号XXX// 2.1 将要阻塞的信号们放入信号集中sigset_t sigs;sigemptyset(&sigs);sigaddset(&sigs, SIGINT);// 2.2 将信号集交给sigprocmaks去集中处理sigset_t oldsigs;sigprocmask(SIG_BLOCK, &sigs, &oldsigs);// 3,休眠15秒钟for(int i=15; i>0; i--){printf("%d\n", i);sleep(1);}// 4,解除信号XXX的阻塞sigprocmask(SIG_UNBLOCK, &sigs, &oldsigs);

5.flag操作

5.1 置位和清零

对应flag置位(使能)

flag |= O_NONBLOCK

对应flag清零

flag &= ~O_NONBLOCK
 int fd[2];pipe(fd);char buf[10];bzero(buf, 10);// 将管道设置为非阻塞状态// a.获取当前文件的flagsint flags = fcntl(fd[0], F_GETFL);// b.在flags的基础上,设置非阻塞属性flags |= O_NONBLOCK;// c.将新的flags设置为文件的FLfcntl(fd[0], F_SETFL, flags);// 此处,管道是非阻塞的int n = read(fd[0], buf, 10);if(n > 0)printf("读取到数据: %s", buf);if(n == 0)printf("管道无写者,且无数据\n");if(n < 0)perror("读取管道失败");// 重新将管道设置为阻塞状态flags = fcntl(fd[0], F_GETFL);flags &= ~O_NONBLOCK;fcntl(fd[0], F_SETFL, flags);printf("试图读取管道内容...\n");n = read(fd[0], buf, 10);if(n > 0)printf("读取到数据: %s", buf);if(n == 0)printf("管道无写者,且无数据\n");if(n < 0)perror("读取管道失败");return 0;
}

5.共享内存

int main()
{// 0,创建一个专属的keykey_t key = ftok(PROJ_PATH, PROJ_ID);// 1,创建/打开共享内存int id = shmget(key, 1024, IPC_CREAT|0666);// 2,将共享内存映射到本进程内存空间// id: 你要映射的共享内存// NULL: 代表让系统帮你决定映射之后的入口地址// 0: 代表映射后对共享内存可读可写char *addr = shmat(id, NULL, 0);// 3,将数据写入共享内存printf("按回车给共享内存写入数据\n");getchar();snprintf(addr, 1024, "%s", "You jump, I jump!\n");// 4,解除共享内存的映射shmdt(addr);return 0;
}

读者

#include "common.h"int main()
{// 0,创建一个专属的keykey_t key = ftok(PROJ_PATH, PROJ_ID);// 1,创建/打开共享内存int id = shmget(key, 1024, IPC_CREAT|0666);// 2,将共享内存映射到本进程内存空间// id: 你要映射的共享内存// NULL: 代表让系统帮你决定映射之后的入口地址// 0: 代表映射后对共享内存可读可写char *addr = shmat(id, NULL, 0);// 3,将数据从共享内存中读出printf("按回车将共享内存中的数据打印出来\n");getchar();printf("%s", addr);// 4,解除共享内存的映射shmdt(addr);// 5,删除共享内存对象shmctl(id, IPC_RMID, NULL);return 0;
}

5.systemV

几个跟 system-V IPC 对象相关的命令:
ipcs -a:查看当前系统中存在的所有的 IPC 对象。
ipcs -q:查看当前系统中存在的 消息队列。
ipcs -m:查看当前系统中存在的 共享内存。
ipcs -s:查看当前系统中存在的 信号量。
删除 IPC 对象
ipcrm -Q key : 删除指定的消息队列
ipcrm -q id : 删除指定的消息队列

ipcrm -M key : 删除指定的共享内存
ipcrm -m id: 删除指定的共享内存

ipcrm -S key : 删除指定的信号量
ipcrm -s id: 删除指定的信号量

5.信号量


当同一资源的数量为N时,意味能够允许N个进程同时使用该资源,此时,可以设置相应信号量的初值为N。
如果资源只有一个,且互斥使用时,信号量的初值必须为1。
原子操作的概念与信号量的初值概念无关,说的是一小段程序操作不能被打断。

信号量用来进行资源的分配,当外设有被多个进程/线程进行访问的时候,为了防止进程/线程访问数据紊乱,需要进行类似于互斥锁,信号量的控制。因此每个这样的外设都应该有一个相近的命令的信号量进行控制,使用前进行p操作,使用后进行v操作

sem_init(&wholePic,  0, 0);
sem_init(&threadDone,0, 0);

5.1 信号量(组)

需要先定义

union semun
{int val;struct semid_ds *buf;unsigned short *array;struct seminfo *__buf;
};
#include "common.h"// 对信号量semid中的第n个元素,进行P操作
void sem_p(int semid, int n)
{struct sembuf buf;bzero(&buf, sizeof(buf));buf.sem_num = n; // 第n个元素buf.sem_op  = -1;// 减操作buf.sem_flg = 0; // 默认的选项// 此处就是P操作,申请资源,可能会发生阻塞semop(semid, &buf, 1);
}// 对信号量semid中的第n个元素,进行V操作
void sem_v(int semid, int n)
{struct sembuf buf;bzero(&buf, sizeof(buf));buf.sem_num = n; // 第n个元素buf.sem_op  = +1;// 加操作buf.sem_flg = 0; // 默认的选项// 此处就是V操作,增加资源,永不阻塞semop(semid, &buf, 1);
}int main()
{// 0,创建两个keykey_t key1 = ftok(PROJ_PATH, PROJ_SHM);key_t key2 = ftok(PROJ_PATH, PROJ_SEM);// 1,创建共享内存、信号量int shmid = shmget(key1, 256, IPC_CREAT|0666);int semid = semget(key2, 2, IPC_CREAT|0666);// 2,映射共享内存char *addr = shmat(shmid, NULL, 0);// 3,初始化信号量union semun a;// 将第0个信号量元素的值,设置为a.vala.val = 1;semctl(semid, 0, SETVAL, a);// 将第1个信号量元素的值,设置为a.vala.val = 0;semctl(semid, 1, SETVAL, a);// 4,不断往共享内存中写入数据while(1){// 对代表内存的第0个信号量元素进行P操作sem_p(semid, 0);// 直接往共享内存输入数据fgets(addr, 256, stdin);// 对代表数据的第1个信号量元素进程V操作sem_v(semid, 1);}return 0;
}

5.2 有名信号量(进程间)

#include "common.h"int main()
{// 1,创建POSIX有名信号量sem_t *s = sem_open("/abc", O_CREAT, 0666, 1/*初始值*/);if(s == SEM_FAILED)perror("创建有名信号量失败");elseprintf("创建有名信号量成功");// 2,创建两个进程,使之使用以上信号量来进行协同pid_t p1, p2;p1 = fork();if(p1 > 0){p2 = fork();if(p2 > 0)exit(0);// 进程p2:else{while(1){// 进行P操作sem_wait(s);// 疯狂输出字母for(int i=0; i<26; i++){fprintf(stderr, "%c", 'a'+i);usleep(20*1000);}// 进程V操作sem_post(s);}}}// 进程p1:else{while(1){// 进行P操作sem_wait(s);// 疯狂输出数字for(int i=0; i<10; i++){fprintf(stderr, "%d", i);usleep(20*1000);}// 进程V操作sem_post(s);}}return 0;
}

5.3无名信号量(线程间)

#include "common.h"// 定义POSIX无名信号量
sem_t s;void *routine(void *arg)
{char *t = (char *)arg;if(strcmp(t, "t1")==0){while(1){// 进行P操作sem_wait(&s);// 疯狂输出字母for(int i=0; i<26; i++){fprintf(stderr, "%c", 'a'+i);usleep(20*1000);}// 进程V操作sem_post(&s);}}if(strcmp(t, "t2")==0){while(1){// 进行P操作sem_wait(&s);// 疯狂输出数字for(int i=0; i<10; i++){fprintf(stderr, "%d", i);usleep(20*1000);}// 进程V操作sem_post(&s);}}
}int main()
{// 1,初始化sem_init(&s, 0/*作用范围是线程间*/, 1/*初始值*/);// 2,搞两条线程去进程P/V操作pthread_t t1, t2;pthread_create(&t1, NULL, routine, "t1");pthread_create(&t1, NULL, routine, "t2");pthread_exit(NULL);
}

6.消息队列(messageQueue)

// 消息结构体
struct msgbuf
{// 第一个成员是规定long型,是每个消息的标签long type;// 后续成员没有规定,想发什么数据就写什么char data[30];};// 自定义消息标签
#define J2R 1
#define R2J 2

写者

#include "common.h"int main()
{// 0,搞一个keykey_t key = ftok(PROJ_PATH, PROJ_ID);// 1,创建/打开一个消息队列// IPC_CREAT: 如果不存在就创建,如果存在就打开int id = msgget(key, IPC_CREAT | 0666);if(id == -1){perror("创建/打开消息队列失败");exit(0);}// 2,准备消息结构体struct msgbuf buf;bzero(&buf, sizeof(buf));buf.type = J2R;// 3,输入数据并发送fgets(buf.data, 30, stdin);msgsnd(id, &buf, strlen(buf.data)+1, 0/*阻塞型发送*/);return 0;
}

读者

#include "common.h"int main()
{// 0,搞一个keykey_t key = ftok(PROJ_PATH, PROJ_ID);// 1,创建/打开一个消息队列// IPC_CREAT: 如果不存在就创建,如果存在就打开int id = msgget(key, IPC_CREAT | 0666);if(id == -1){perror("创建/打开消息队列失败");exit(0);}// 2,准备消息结构体来接收Jack的消息struct msgbuf buf;bzero(&buf, sizeof(buf));// 3,接收对方的消息并打印int n = msgrcv(id, &buf, 30, J2R, 0/*阻塞型接收*/);if(n == -1){perror("接收消息失败");exit(0);}elseprintf("Jack的消息: %s", buf.data);// 4,收完消息后,删除消息队列msgctl(id, IPC_RMID, NULL);return 0;
}

6共享内存(建立工程模板)

 // 0,创建一个专属的keykey_t key = ftok(PROJ_PATH, PROJ_ID);// 1,创建/打开共享内存int id = shmget(key, 1024, IPC_CREAT|0666);// 2,将共享内存映射到本进程内存空间// id: 你要映射的共享内存// NULL: 代表让系统帮你决定映射之后的入口地址// 0: 代表映射后对共享内存可读可写char *addr = shmat(id, NULL, 0);// 3,将数据从共享内存中读出printf("按回车将共享内存中的数据打印出来\n");getchar();printf("%s", addr);// 4,解除共享内存的映射shmdt(addr);// 5,删除共享内存对象shmctl(id, IPC_RMID, NULL);

6.1消息队列和管道的区别

管道一般用于父子进程间通信(有名管道除外,有名管道不限于父子进程通信)。而消息队列可用于你机器上的任何进程间通信(只要进程有权操作消息队列)。

7. 创建线程

常用命令
ps -uxH 查看当前用户的系统的线程数

7.0 线程的本质:

在Linux中,新建的线程并不是在原先的进程中,而是系统通过 一个系统调用clone()。该系统copy了一个和原先进程完全一样的进程,并在这个进程中执行线程函数。不过这个copy过程和fork不一样。 copy后的进程和原先的进程共享了所有的变量,运行环境。所以线程是共享全局变量和环境的。

7.1创建线程工程模板

void *routine(void *arg)
{int a = *(int *)arg;printf("a: %d\n", a);// 打印一些数字for(int i=0; i<10; i++){a++;fprintf(stderr, "%d", a);usleep(200*1000);}// 退出线程pthread_exit("abcdefg");// 线程一旦退出,立即就会变成僵尸线程
}int main()
{// 创建一条线程// 如果创建成功,系统会为这条线程分配一个ID号称为线程ID// tid: 存放线程ID// NULL: 不指定特定的线程属性,创建一条标准线程// routine: 线程的启动函数// &a: 传给线程的参数pthread_t tid;int a = 100;pthread_create(&tid, NULL, routine, (void *)&a);// 静静地等待回收子线程的资源... ...void *val;pthread_join(tid, &val); // 相当于wait()/waitpid()printf("返回值: %s\n", (char *)val);return 0;
}

7.2 子线程分离(系统回收线程资源)

#include "common.h"
#include <errno.h>void *routine(void *arg)
{//第二种// 将自己分离出去pthread_detach(pthread_self());pthread_exit("abcd");
}int main()
{// 两种方法.第一种// 1,设置线程的属性变量pthread_attr_t attr;pthread_attr_init(&attr);pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);// 2,根据属性变量来创建线程pthread_t tid;pthread_create(&tid, &attr, routine, NULL);void *val;int err = pthread_join(tid, &val);if(err == 0){printf("子线程返回值: %s\n", (char *)val);}else{printf("获取子线程失败: %s\n", strerror(err));}return 0;
}

7.1 线程等待和唤醒 pthread_cond()

#include "common.h"// 余额
int balance = 0;// 互斥锁与条件量
pthread_mutex_t m;
pthread_cond_t  v;// 所有的子线程的启动函数
void *routine(void *args)
{// 加锁pthread_mutex_lock(&m);// 判断余额while(balance <= 0){// 进入条件量v的等待房间// 进入以下函数,将自动解锁m// 退出以下函数,将自动加锁mpthread_cond_wait(&v, &m);}// 取钱balance -= 80;printf("我是第%d条线程,取了80元之后的余额是:%d\n", (int)args, balance);// 解锁pthread_mutex_unlock(&m);
}int main(int argc, char **argv) // ./main 10
{// 初始化pthread_mutex_init(&m, NULL);pthread_cond_init(&v, NULL);// 创建一些子女线程pthread_t tid;for(int i=0; i<atoi(argv[1]); i++){pthread_create(&tid, NULL, routine, (void *)i);}// 主线程充当了父母,去充钱pthread_mutex_lock(&m);balance += 500;pthread_mutex_unlock(&m);// 唤醒在条件量房间中睡觉的线程(们)
//    pthread_cond_signal(&v); // 唤醒一个pthread_cond_broadcast(&v); // 唤醒全部// 退出主线程pthread_exit(NULL);
}

注意 : 无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait(),下同)的竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

7.2互斥锁

互斥锁是s=1的二值信号量
同步是s=0的信号量 ,先执行的进行v操作,后执行的进行p操作(s=0进入睡眠状态)

7.2.1程序模板

#include "common.h"// 定义互斥锁
pthread_mutex_t m;void output(const char *s)
{while(*s != '\0'){putc(*s, stderr);usleep(1000);s += 1;}return;
}void *routine(void *arg)
{pthread_mutex_lock(&m);output("info output bu sub-thread.\n");pthread_mutex_unlock(&m);
}int main()
{// 初始化互斥锁pthread_mutex_init(&m, NULL);pthread_t tid;pthread_create(&tid, NULL, routine, NULL);pthread_mutex_lock(&m);output("message delivered by main thread.\n");pthread_mutex_unlock(&m);// 在main函数中return,相当于退出了进程,即所有的线程都被迫退出了// return 0;// 退出当前线程pthread_exit(NULL);
}

7.2读写锁

#include "common.h"// 定义读写锁
pthread_rwlock_t rwlock;// 全局共享的数据
// 大家都可以访问的数据,通常被称为临界资源
int global = 100;void *routine1(void *arg)
{// 对临界资源发生操作的代码,称为临界代码// 由于以下代码对共享数据发生了读、写操作// 所以必须加写锁pthread_rwlock_wrlock(&rwlock);global += 1;printf("我是%s, global=%d\n", (char *)arg, global);sleep(1);// 离开了临界代码,必须解锁pthread_rwlock_unlock(&rwlock);pthread_exit(NULL);
}void *routine2(void *arg)
{pthread_rwlock_wrlock(&rwlock);global = 666;sleep(1);printf("我是%s, global=%d\n", (char *)arg, global);pthread_rwlock_unlock(&rwlock);pthread_exit(NULL);
}void *routine3(void *arg)
{pthread_rwlock_rdlock(&rwlock);if(global > 0)printf("我是%s,目前global>0\n");pthread_rwlock_unlock(&rwlock);pthread_exit(NULL);
}int main()
{// 初始化读写锁pthread_rwlock_init(&rwlock, NULL);// 创建一些线程pthread_t t1, t2, t3;pthread_create(&t1, NULL, routine1, "thread 1");pthread_create(&t2, NULL, routine2, "thread 2");pthread_create(&t3, NULL, routine3, "thread 3");// 主线程阻塞等待回收子线程,并销毁读写锁pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_rwlock_destroy(&rwlock);return 0;
}
7.2.1互斥锁与读写锁的概念

删除线格式

一 点睛

先看看互斥锁,它只有两个状态,要么是加锁状态,要么是不加锁状态。假如现在一个线程a只是想读一个共享变量 i,因为不确定是否会有线程去写它,所以我们还是要对它进行加锁。但是这时又有一个线程b试图去读共享变量 i,发现被锁定了,那么b不得不等到a释放了锁后才能获得锁并读取 i 的值,但是两个读取操作即使是同时发生的,也并不会像写操作那样造成竞争,因为它们不修改变量的值。所以我们期望在多个线程试图读取共享变量的时候,它们可以立刻获取因为读而加的锁,而不是需要等待前一个线程释放。

读写锁可以解决上面的问题。它提供了比互斥锁更好的并行性。因为以读模式加锁后,当有多个线程试图再以读模式加锁时,并不会造成这些线程阻塞在等待锁的释放上。

读写锁是多线程同步的另外一个机制。在一些程序中存在读操作和写操作问题,对某些资源的访问会存在两种可能情况,一种情况是访问必须是排他的,就是独占的意思,这种操作称作写操作,另外一种情况是访问方式是可以共享的,就是可以有多个线程同时去访问某个资源,这种操作称为读操作。这个问题模型是从对文件的读写操作中引申出来的。把对资源的访问细分为读和写两种操作模式,这样可以大大增加并发效率。读写锁比互斥锁适用性更高,并行性也更高。

需要注意的是,这里只是说并行效率比互斥高,并不是速度一定比互斥锁快,读写锁更复杂,系统开销更大。并发性好对于用户体验非常重要,假设互斥锁需要0.5秒,使用读写锁需要0.8秒,在类似学生管理系统的软件中,可能90%的操作都是查询操作。如果突然有20个查询请求,使用的是互斥锁,则最后的查询请求被满足需要10秒,估计没人接收。使用读写锁时,因为读锁能多次获得,所以20个请求中,每个请求都能在1秒左右被满足,用户体验好的多。

二 读写锁特点

1 如果一个线程用读锁锁定了临界区,那么其他线程也可以用读锁来进入临界区,这样可以有多个线程并行操作。这个时候如果再用写锁加锁就会发生阻塞。写锁请求阻塞后,后面继续有读锁来请求时,这些后来的读锁都将会被阻塞。这样避免读锁长期占有资源,防止写锁饥饿。

2 如果一个线程用写锁锁住了临界区,那么其他线程无论是读锁还是写锁都会发生阻塞。

三 读写锁使用的函数

操作

相关函数说明

初始化读写锁

pthread_rwlock_init 语法

读取读写锁中的锁

pthread_rwlock_rdlock 语法

读取非阻塞读写锁中的锁

pthread_rwlock_tryrdlock 语法

写入读写锁中的锁

pthread_rwlock_wrlock 语法

写入非阻塞读写锁中的锁

pthread_rwlock_trywrlock 语法

解除锁定读写锁

pthread_rwlock_unlock 语法

销毁读写锁

pthread_rwlock_destroy 语法

读写锁是用来解决读者写者问题的,读操作可以共享,写操作是排他的,读可以有多个在读,写只有唯一个在写,同时写的时候不允许读。

具有强读者同步和强写者同步两种形式

强读者同步:当写者没有进行写操作,读者就可以访问;

强写者同步:当所有写者都写完之后,才能进行读操作,读者需要最新的信息,一些事实性较高的系统可能会用到该所,比如定票之类的。

读写锁的操作:

读写锁的初始化:

    定义读写锁:          pthread_rwlock_t  m_rw_lock;函数原型:              pthread_rwlock_init(pthread_rwlock_t * ,pthread_rwattr_t *);返回值:0,表示成功,非0为一错误码

读写锁的销毁:

    函数原型:             pthread_rwlock_destroy(pthread_rwlock_t* );返回值:0,表示成功,非0表示错误码

获取读写锁的读锁操作:分为阻塞式获取和非阻塞式获取,如果读写锁由一个写者持有,则读线程会阻塞直至写入者释放读写锁。

    阻塞式:函数原型:pthread_rwlock_rdlock(pthread_rwlock_t*);非阻塞式:函数原型:pthread_rwlock_tryrdlock(pthread_rwlock_t*);返回值: 0,表示成功,非0表示错误码,非阻塞会返回ebusy而不会让线程等待

获取读写锁的写锁操作:分为阻塞和非阻塞,如果对应的读写锁被其它写者持有,或者读写锁被读者持有,该线程都会阻塞等待。

  阻塞式:函数原型:pthread_rwlock_wrlock(pthread_rwlock_t*);非阻塞式:函数原型:pthread_rwlock_trywrlock(pthread_rwlock_t*);返回值: 0,表示成功

释放读写锁:

                     函数原型:pthread_rwlock_unlock(pthread_rwlock_t*);

总结(转):

互斥锁与读写锁的区别:

当访问临界区资源时(访问的含义包括所有的操作:读和写),需要上互斥锁;

当对数据(互斥锁中的临界区资源)进行读取时,需要上读取锁,当对数据进行写入时,需要上写入锁。

读写锁的优点:

对于读数据比修改数据频繁的应用,用读写锁代替互斥锁可以提高效率。因为使用互斥锁时,即使是读出数据(相当于操作临界区资源)都要上互斥锁,而采用读写锁,则可以在任一时刻允许多个读出者存在,提高了更高的并发度,同时在某个写入者修改数据期间保护该数据,以免任何其它读出者或写入者的干扰。

读写锁描述:

获取一个读写锁用于读称为共享锁,获取一个读写锁用于写称为独占锁,因此这种对于某个给定资源的共享访问也称为共享-独占上锁。

有关这种类型问题(多个读出者和一个写入者)的其它说法有读出者与写入者问题以及多读出者-单写入者锁。

————————————————
版权声明:本文为CSDN博主「不材之木」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wonderisland/article/details/16940925

c++读写锁实现

https://blog.csdn.net/zxc024000/article/details/88814461
C++17,提供了shared_mutex。配合C++14,提供的shared_lock。及C++11,提供的 unique_lock, 可以方便实现读写锁。
但上述的前提是,允许你使用C++17。在国内的开发环境下,别说C++17,连C++11用的也不多。
所以,大多数时候,我们需要自己实现一套C++读写锁(C++11环境下)。

RWLock.h
#ifndef RWLOCK__H
#define RWLOCK__H#ifndef __cplusplus
#    error ERROR: This file requires C++ compilation(use a .cpp suffix)
#endif#include <mutex>
#include <condition_variable>namespace linduo {class RWLock {public:RWLock();virtual ~RWLock() = default;void lockWrite();void unlockWrite();void lockRead();void unlockRead();private:volatile int m_readCount;volatile int m_writeCount;volatile bool m_isWriting;std::mutex m_Lock;std::condition_variable m_readCond;std::condition_variable m_writeCond;
};class ReadGuard {public:explicit ReadGuard(RWLock& lock);virtual ~ReadGuard();private:ReadGuard(const ReadGuard&);ReadGuard& operator=(const ReadGuard&);private:RWLock &m_lock;
};class WriteGuard {public:explicit WriteGuard(RWLock& lock);virtual ~WriteGuard();private:WriteGuard(const WriteGuard&);WriteGuard& operator=(const WriteGuard&);private:RWLock& m_lock;
};} /* namespace linduo */
#endif  // RWLOCK__HRWLock.cpp
#include "RWLock.h"namespace linduo {RWLock::RWLock(): m_readCount(0), m_writeCount(0), m_isWriting(false) {}void RWLock::lockRead() {std::unique_lock<std::mutex> gurad(m_Lock);m_readCond.wait(gurad, [=] { return 0 == m_writeCount; });++m_readCount;
}void RWLock::unlockRead() {std::unique_lock<std::mutex> gurad(m_Lock);if (0 == (--m_readCount)&& m_writeCount > 0) {// One write can go onm_writeCond.notify_one();}
}void RWLock::lockWrite() {std::unique_lock<std::mutex> gurad(m_Lock);++m_writeCount;m_writeCond.wait(gurad, [=] { return (0 == m_readCount) && !m_isWriting; });m_isWriting = true;
}void RWLock::unlockWrite() {std::unique_lock<std::mutex> gurad(m_Lock);m_isWriting = false;if (0 == (--m_writeCount)) {// All read can go onm_readCond.notify_all();} else {// One write can go onm_writeCond.notify_one();}
}ReadGuard::ReadGuard(RWLock &lock): m_lock(lock) {m_lock.lockRead();
}ReadGuard::~ReadGuard() {m_lock.unlockRead();
}WriteGuard::WriteGuard(RWLock &lock): m_lock(lock) {m_lock.lockWrite();
}WriteGuard::~WriteGuard() {m_lock.unlockWrite();
}} /* namespace linduo */使用
RWLock m_Lock;void func() {// 写锁WriteGuard autoSync(m_Lock);
}void func() {// 读锁ReadGuard autoSync(m_Lock);
}

7.2 线程理解

7.2.1主线程main()函数pthread_exit(NULL)退出,不影响其他线程继续执行,进程没有结束,保留进程资源,供其他由main创建的线程使用,直至所有线程都结束。

7.2.1 在任何一个线程中调用exit函数都会导致进程结束。进程一旦结束,那么进程中的所有线程都将结束。

7.2.2 一个线程默认的状态是joinable,如果线程是joinable状态,当线程函数自己返回退出时或pthread_exit时都不会释放线程所占用堆栈和线程描述符

7.2.3 不能对一个已经处于detach状态的线程调用pthread_join

7.3线程调度策略

7.3.1 工程模板


void *routine(void *arg)
{if(*(char *)arg == 'A')nice(0);else if(*(char *)arg == 'B')nice(0);while(1){fprintf(stderr, "%c", *(char *)arg);}
}int main(int argc, char **argv)
{pthread_attr_t attr1;pthread_attr_t attr2;pthread_attr_init(&attr1);pthread_attr_init(&attr2);pthread_attr_setinheritsched(&attr1, PTHREAD_EXPLICIT_SCHED);//inherit 继承pthread_attr_setinheritsched(&attr2, PTHREAD_EXPLICIT_SCHED);//sched 时间表pthread_attr_setschedpolicy(&attr1, SCHED_RR);pthread_attr_setschedpolicy(&attr2, SCHED_RR);struct sched_param param1;struct sched_param param2;param1.sched_priority = 91;param2.sched_priority = 92;pthread_attr_setschedparam(&attr1, &param1);pthread_attr_setschedparam(&attr2, &param2);pthread_t tid1, tid2;pthread_create(&tid1, &attr1, routine, "A");pthread_create(&tid2, &attr2, routine, "B");pause();return 0;
}

7.3.2 sudo才能操作pthread_attr_setschedparam(&attr1 ,SCHED_RR);

属于修改系统层面的优先级

7.3.2SCHED_RR 和SCHED_FIFO的param.sched_priority需要大于0才可以成功

7.3.2 函数nice(10)修改动态优先级

同样需要sudo执行程序

7.4线程取消函数pthread_cancel()(最好自身pthread_exit()

嵌入式代码学习心得记录相关推荐

  1. Vue学习心得记录之模板语法

    下面是我这半年以来总结的Vue学习笔记,帮助自己复习学习Vue的基本用法.有需要的同志可以参考下. Vue的模板语法 Vue有很简单的模板语法,这些Vue指令用来响应式改变渲染DOM可以快速入门上手这 ...

  2. [Javascript 高级程序设计]学习心得记录 函数参数传递与引用

    最近开始啃js的红宝书:<Javascript 高级程序设计>,偶有心得,记录一下. 先上代码 function howManyArgs() {alert(arguments.length ...

  3. [Javascript 高级程序设计]学习心得记录9 js面向对象

    感觉最难的部分就是面向对象了,大学期间学习的是面向过程的c/c++,工作之后也没有深入了解过面向对象,通过这次的学习和回顾,也算是对面向对象有了新的认识.不过,就我在书上学到了结合个人理解随便说说,很 ...

  4. LNA设计学习心得记录----MOS管的选取

    本文参考相关书籍,仅供学习LNA的过程记录. 在设计LNA之前,要选取合适的MOS管,要对MOS管进行分析 在确定工艺之后,主要仿真两个方面,一个是Vgs对于NFmin,Gmax,二是栅宽对NFmin ...

  5. [Javascript 高级程序设计]学习心得记录6 变量和作用域

    js的变量和其他语言的变量区别还是挺大的,它只是在特定时间用于保存特定值的一个名字而已,js的变量高度灵活,同时又很容易出问题,需要专门学习. 一,基本类型和引用类型的值 基本类型值指数据的五种基本类 ...

  6. 嵌入式Linux学习问题解决记录

    问题: make menuconfig提示'make menuconfig' requires the ncurses libraries解决方法 解决: Google了一下,原来只需要安装libnc ...

  7. [Javascript 高级程序设计]学习心得记录10 js函数表达式

    在前面说对象的时候已经提到了函数对象,对函数的定义参数的传递包括通过argumentd.callee实现递归.这篇博客我会继续深入讲解js中的函数表达式. 一,闭包 关于闭包的概念,可以先看看http ...

  8. [Javascript 高级程序设计]学习心得记录11 js的BOM

    BOM(浏览器对象模型)是web中js的核心,而BOM的核心对象就是window对象.在浏览器中,window对象有双重角色,它既是通过js访问浏览器的一个接口,又是规定的Global对象.此外,还有 ...

  9. [Javascript 高级程序设计]学习心得记录2 Javascript的垃圾回收机制

    Javascript 是自动垃圾收集机制,不需要像c/c++的开发人员一样担心内存泄漏问题.这种垃圾收集机制通过找出那些不再使用的变量,释放其占用的内存从而达到垃圾回收的效果.而如何如何找出那些不再使 ...

最新文章

  1. Python入门100题 | 第048题
  2. Python中集合的介绍以及常见操作
  3. Linux系统学习----前言
  4. 可添加至收藏夹并在浏览器地址栏运行的JS代码
  5. 第八十一期:Java性能优化:35个小细节,提升你的Java代码运行效率
  6. html盒子有哪些属性,盒子模型有哪些属性 在html5中哪些元素具有盒子模型
  7. MediaPlayer控件的初探
  8. 我对架构的理解-概念篇
  9. day034 锁,信号量,事件,队列,子进程与子进程通信,生产者消费者模型,joinableQueue...
  10. artTemplate -- 性能卓越的 js 模板引擎
  11. 45.Linux/Unix 系统编程手册(下) -- System V IPC 介绍
  12. golang 微信小程序获取二维码scene参数报错 invalid scene rid: f05f96ab-5382f139-14b13d2f
  13. vba调用excel内置函数
  14. opencv图像灰化_Opencv——彩色图像灰度化的三种算法
  15. Rplidar A2M8屏蔽角度
  16. [C++]求模与求余运算
  17. 如何使用微信编辑器排版微信公众号内容?
  18. idea2018 2020_2019~2020上海沪牌价格一览表
  19. 51单片机按键控制数码管0~9_LED数码管精选电路方案合辑
  20. VirtualBox导入虚拟电脑

热门文章

  1. c语言:删除字符串中的子串
  2. STM32之VCP1/VCAP2引脚的处理
  3. Flutter前端编译frontend_server
  4. 2015最新百度搜索引擎(seo优化)排名算法
  5. k8s教程05(Kubernetes Ingress)
  6. Mac下的ananconda死活找不到包的解决方法
  7. 配置mod_proxy_fcgi加php fpm
  8. 可调电阻封装图_嵌入式硬件电路杂谈(二)贴片电阻选用
  9. 使用Visual Studio Code开发Java程序
  10. 越看对方不顺眼,越要跟对方好好相处