目录
12. 全局变量、编译预处理和宏、大程序的结构
12.1 全局变量
定义在函数内部的变量为本地变量,
定义在函数外面的变量为全局变量
全局变量具有全局的生存期和作用域
他们与任何函数无关
在任何函数内部都可以使用它们
#include <stdio.h>
int f(void);
int gAll = 12;
int main(int argc, char const *argv[ ])
{
printf("in %s gAll=%d\n",_func_, gAll); //__func_:是输出当前函数名称
f( );
printf( "agn in %s gAll=%d\n",_func_, gAll);
return 0;
}
int f(void)
{
printf("in %s gAll=%d\n", _func_, gAll);
gAll +=2;
printf( "agn in %s gAll=%d\n", _func_, gAll);
return gAll;
}
_func_ 是输出当前函数名字,但是我试的时候错误,uncleared
f函数改变了全局变量gAll的值, 可见我们可以在任何函数中直接访问、改变全局变量。
全局变量的初始化
—— 没有做初始化的全局变量会得到0值
— 指针会得到NULL值
—— 只能用编译时刻已知的值来初始化全局变量(不能是函数处理后的结果)
—— 它们的初始化发生在main函数之前
int gAll = f(); //不行,编译时无法知道f()是啥
int gAll = 12;
int g2 = gAll; // 不行
const int gAll=12;
int g2=gAll; // 可以 但是不建议这样做。初始化顺序相对比较明确
被隐藏的全局变量
如果函数内部存在与全局变量同名的变量,则全局变量被隐藏。
#include <stdio.h>
int f(void);
int gAll = 12;
int main(int argc, char const *argv[ ])
{
printf("in %s gAll=%d\n",_func_, gAll); //__func_:是输出当前函数名称
f( );
printf( "agn in %s gAll=%d\n",_func_, gAll);
return 0;
}
int f(void)
{
int gAll = 1; // 新的gAll
printf("in %s gAll=%d\n", _func_, gAll);
gAll +=2;
printf( "agn in %s gAll=%d\n", _func_, gAll);
return gAll;
}
进入f()时候,gAll是12,f()中有个gAll 初始化为1,则输出都是1和1+2 = 3,返回了main后,又是12,函数f()中的gAll的生存期只有f()。
静态本地变量
- 在本地变量定义时加上static修饰符就成为静态本地变量
- 当函数离开的时候,静态本地变量会继续存在并保持其值
- 静态本地变量的初始化只会在第一次进入这个函数时做,以后进入函数时会保持上次离开时的值
- static 在这里的意思是,局部作用域(本地可访问)
#include <stdio.h>
int f(void);
int gAll = 12;
int main(int argc, char const *argv[ ])
{
f();
printf("\n");
f();
printf("\n");
f();
return 0;
}
int f(void)
{
int All = 1;
static int all = 1;
printf("in f All=%d\n", All);
All +=2;
printf( "agn in f All=%d\n", All);
printf("in f all=%d\n", all);
all +=2;
printf( "agn in f all=%d\n", all);
return All, all;
}
第二次进入f()时,all为第一次输出的值,没有再进行初始化。
静态本地变量实际是全局变量。
gAll和all放在一起,k离得很远。
静态本地变量,它有全局的生存期和本地的作用域。
返回指针的函数
- 返回本地变量的地址是危险的
- 返回全局变量或静态本地变量的地址是安全的
- 返回在函数内malloc的内存是安全的,但是容易造成问题
- 最好的做法是返回传入的指针
#include<stdio.h>
int *f(void);
void g(void);
int main(int argc, char const *argv[])
{
int *p = f();
printf("*p=%d\n", *p);
g();
printf("*p=%d\n", *p);
return 0;
}
int *f(void)
{
int i=12;
return &i;
}
void g(void)
{
int k=24;
printf("k=%d\n",k);
}
f()结束后,变量的地址可能再给别的程序使用,留着地址,访问的内容也不对了。
tips
- 不要使用全局变量来在函数间传递参数和结果。
- 尽量避免使用全局变量
- 丰田汽车的案子
- *使用全局变量和静态本地变量的函数是线程不安全
12.2 编译预处理和宏
12.2.1宏定义
编译预处理指令
- #开头的是编译预处理指令
- 它们不是C语言的成分,但是C语言程序离不开它们
- #define用来定义一个宏
#include <stdio.h>
const double PI = 3.14159; //C99的做法
#define PI 3.14159 //更老的版本没有const时的做法
int main(int argc, char const *argv [])
{
printf("%f\n",2*PI*3.0);
//printf ( "%f\ n",2*3.14159*3.0 );
return 0;
}
这样C语言在编译预处理时会把所有PI替换成3.14159。
我们可以通过下面的方法看在编译过程中留下来的零始文件
gcc:
.c的文件是源代码——>经过编译预处理,执行所有的编译预处理指令,产生中间文件.i ——>由C的编译器编译,产生汇编代码文件.s——>做汇编产生目标代码文件.o——>做链接和其他东西链接一起,产生可执行的东西.out
发现.i比.c大很多
可以用tail来输出c和i的结尾部分
可以看到.i中把已经所有的PI替换为3.14159。
同样的,我们也可以
#define FORMAT "%f\n"
FORMAT,按格式输出。
在main函数中写:
printf(FORMAT,2*PI);
编译出来也是没有问题的。不过注意如果FORMAT是写在双引号里面,printf输出的就是FORMAT这六个字母了。
printf("FORMAT",2*PI);
- #define <名字> <值>
- 注意没有结尾的分号,因为不是C的语句
- 名字必须是一个单词,值可以是各种东西
- 在C语言的编译器开始编译之前,编译预处理程序(cpp)会把程序中的名字换成值
- 完全的文本替换
- gcc --save-temps
- 如果一个宏的值中有其他的宏的名字,也是会被替换的
- 如果一个宏的值超过一行,最后一行之前的行末需要加\
- 宏的值后面出现的注释不会被当作宏的值的一部分
#define PI2 2*PI // pi * 2
空格、其他的标点符号都会被当做是宏定义的值的一部分。
#define prt printf("%f ",PI);\
printf("%f\n",PI2);
没有值的宏
#define _DEBUG
这类宏是用于条件编译的,后面有其他的编译预处理指令来检查这个宏是否已经被定义过。存在编译这部分代码,不存在编译另部分代码
预定义的宏
__LINE__ //当前行的行号
__FILE__ //源代码文件的文件名
__DATE__ //编译时的日期
__TIME__ //编译时的时间
__STDC__
#include <stdio.h>
int main(int argc, char const *argv [])
{
printf("%s:%d\n", _FILE_,_LINE_);
printf("%s:%s\n", _DATE_,_TIME_);
return 0;
}
12.2.2 带参数的宏
#define cube(x) ((x)*(x)*(x)*)
宏可以带参数
#include <stdio.h>
#define cube(x) ((x)*(x)*(x))
int main(int argc, char const *argv [])
{
int i = 0;
printf("%d\n", cube(5) );
printf("%d\n", cube(i) );
printf("%d\n", cube(i+2) );
return 0;
}
错误定义的宏
#define RADTODEG(x) (x*57.29578)
#define RADTODEG(x) (x)*57.29578
带参数的宏的原则
- 一切都要括号
- 整个值要括号
- 参数出现的每个地方都要括号
- #define RADTODEG(x) ( (x) *57.29578)
- 可以带多个参数:#define MIN(a,b) ( (a) > (b) ? (b) : (a) )。
- 也可以组合(嵌套)使用其他宏
不要加分号;
#define PRETTY_PRINT(msg) printf(msg);
if (n <10)
PRETTY_PRINT("n is less than 10");
else
PRETTY_PRINT("n is at least 10");
预处理后,将变成有两个分号,第二个分号相当于有个空行,导致else和if分开了。
- 在大型程序的代码中使用非常普遍
- 可以非常复杂,如“产生"函数
- 在#和##这两个运算符的帮助下
- 存在中西方文化差异
- 部分宏会被inline函数替代
代替函数时,运行效率比函数高,代码大小比函数大,牺牲空间换效率。
宏有一个缺点:没有类型检查。
C语言inline机制,有类型检查,可能可以取代宏。
其他编译预处理指令:条件编译、error、。。。
12.3 大程序结构
12.3.1 多个源代码文件
多个.c文件
- main()里的代码太长了适合分成几个函数
- 一个源代码文件太长了适合分成几个文件
- 两个独立的源代码文件不能编译形成可执行的程序
如果直接把函数放在另一个源代码文件里,编译不会通过。需要新建一个项目。
项目
- 在Dev C++中新建一个项目,然后把几个源代码文件加入进去
- 对于项目,Dev C++的编译会把一个项目中所有的源代码文件都编译后,链接起来
- 有的IDE有分开的编译和构建两个按钮,前者是对单个源代码文件编译,后者是对整个项目做链接(Dev c++ 比较特殊的IDE,允许单个源代码文件。)
- 有的IDE是有编译和构建两个按钮,前者对单个源代码文件进行编译,后者对所有源代码文件链接。
编译单元
- 一个.c文件是一个编译单元
- 编译器每次编译只处理一个编译单元(形成.o目标代码文件)
12.3.2 头文件
在刚才的多个源代码文件中,main文件里有加入一条声明:
int max(int a,int b);
如果不加这条声明,C语言也会默认a和b是Int类型的。
若把max文件中的int改为double,返回类型也改为double,编译通过,运行结果是一串奇怪的数字
main中默认a,b是int→传入double 的max→以int形式传回来,链接没有问题,但传入传出都是错的。
如何让他们一致?需要中间媒介——头文件。
新建max.h文件,内容为函数原型。
如此函数类型不同,编译会报错。
- 把函数原型放到一个头文件(以.h结尾)中,在需要调用这个函数的源代码文件(.c文件)中#include这个头文件,就能让编译器在编译的时候知道函数的原型。
#include
- #include是一个编译预处理指令,和宏一样,在编译之前就处理了
- 它把那个文件的全部文本内容原封不动地插入到它所在的地方
- 所以也不是一定要在.c文件的最前面 #include
命令行下,多个源代码文件的链接编译,放所有的.c文件,不放.h文件
-c 只编译,不连接
" "还是< >
- #include有两种形式来指出要插入的文件
- " "要求编译器首先在当前目录(.c文件所在的目录)寻找这个文件,如果没有,到编译器指定的目录去找
- <>让编译器只在指定的目录去找
- 编译器自己知道自己的标准库的头文件在哪里
- 环境变量和编译器命令行参数也可以指定寻找头文件的目录
树莓派linux的位置
#include的误区
- #include不是用来引入库的
- stdio.h里只有printf的原型,printf的代码在另外的地方,某个.lib(Windows)或.a(Unix)中
- 现在的C语言编译器默认会引入所有的标准库
- #include <stdio.h>只是为了让编译器知道printf函数的原型,保证你调用时给出的参数值是正确的类型
头文件
- 在使用和定义这个函数的地方都应该#include这个头文件
- 一般的做法就是任何.c都有对应的同名的.h,把所有对外公开的函数的原型和全局变量的声明都放进去。(全局变量也可以在多个源代码文件中共享;不过也像函数一样要有恰当的方法。)
- 对于不对外公开的函数
- 在函数前面加上static就使得它成为只能在所在的编译单元中被使用的函数
- 在全局变量前面加上static就使得它成为只能在所在的编译单元中被使用的全局变量
12.3.3声明
在某个单元里有个全局变量,想在其他地方也能访问到它:在.h文件 中进行声明
变量 的声明
- int i; 是变量的定义
- extern int i; 是变量的声明
- 声明是不产生代码的东西:函数原型、变量声明、结构声明、宏声明、枚举声明、类型声明、 inline函数。
- 定义是产生代码的东西(函数、全局变量)
头文件
- 只有声明可以被放在头文件中
- 是规则不是法律
- 否则(头文件中放了定义)会造成一个项目中多个编译单元里有重名的实体
- *某些编译器允许几个编译单元中存在同名的函数,或者用weak修饰符来强调这种存在
重复声明
- 同一个编译单元里,同名的结构不能被重复声明
- 如果你的头文件里有结构的声明,很难这个头文件不会在一个编译单元里被#include多次
- 所以需要“标准头文件结构”
如果在max.h里定义了一个struct,然后在min.h里需要用到这个struct,就#include"max.h";而在主函数中要调用函数max、min就要#include"max.h" #include"min.h",就会导致重复声明①②,main这个文件中,#include"max.h"处替换了,有struct的声明①,后面,#include"min.h"处,替换为了:#include"max.h"和min的内容,又一次将#include"max.h"替换为了含有struct定义的内容②。尽管再怎么避免这件事的发生,在.h中加入了结构,还是很有可能会发生的。
标准头文件结构
#ifndef __LIST_HEAD__
#define __LIST_HEAD__//如果没有定义过max.h则定义max.h
#include "node.h"
typedef struct _list{
Node* head;
Node* tail;
}List;
#endif
- 运用条件编译和宏,保证这个头文件在一个编译单元中只会被#include一次
- #pragma once也能起到相同的作用,但是不是所有的编译器都支持
13.文件与位运算
13.1 文件
13.1.1 格式化输入输出
- printf
- %[flags][width][.prec][hlL]type
- scanf
- %[flag]type
[flags]
flag | 含义 |
---|---|
- | 左对齐(和width一起用) |
+ | 在前面方 + 或 - 号(强制输出+号,-号是如果值是负的一定有的) |
(space) | 正数留空 |
0 | 空格用0填充(不能和-同时使用) |
#include <stdio.h>
int main(int argc, char const *argv[])
{
printf("%9d\n",123);
printf ("%-9d\n",123);
printf("%+9d\n",123);
printf ("%+-9d\n",-123);
printf("%09d\n",123);
printf ("%-9d\n",123);
printf("%9.2f\n",123.0);
printf("%*d\n",6,123);
printf("%hhd\n",(char)12345);
int num;
printf("%d %n\n",12345,&num);
printf("%d\n",num);
printf("%dty %n\n",12345,&num);
printf("%d\n",num);
return 0;
}
[width][.prec]
width或prec | 含义 |
number | 最小字符数(包括小数点) |
* | 下一个参数是字符数 |
.number | 小数点后面的位数 |
.* | 下一个参数是小数点后的位数 |
[hlL]
类型修饰 | 含义 |
hh | 单个字节 |
h | short |
l | long |
ll | long long |
L | long double |
type
type | 用于 |
i或d | int |
u | unsigned int |
o | 八进制 |
x | 十六进制 |
X | 大写字母的十六进制 |
f或F | float, 6 |
e或E | 指数 |
g | float |
G | float |
a或A | 十六进制浮点 |
c | char |
s | 字符串 |
p | 指针 |
n | 读入/写出的个数(%n是当操作做到这里时,已经输出了多少个字符,并且填到指针所指的变量里。) |
scanf —— %[flag]type
[flag]
flag | 含义 |
* | 跳过 |
数字 | 最大读多少字符数 |
hh | char |
h | short |
l | long, double |
ll | long long |
L | long double |
type
type | 用于 |
d | int |
i | 整数,可能是十六进制或八进制:根据输入(如0x12、012)来判断是十六进制还是八进制还是十进制。 |
u | unsigned int |
o | 八进制 |
x | 十六进制 |
a, e, f, g | float |
c | char |
s | 字符串(单词) |
[...] | 所允许的字符 |
p | 指针 |
#include <stdio.h>
int main(int argc, char const *argv[])
{
printf("%9d\n",123);
printf ("%-9d\n",123);
printf("%+9d\n",123);
printf ("%+-9d\n",-123);
printf("%09d\n",123);
printf ("%-9d\n",123);
printf("%9.2f\n",123.0);
printf("%*d\n",6,123);
printf("%hhd\n",(char)12345);
int num;
printf("%d %n\n",12345,&num);
printf("%d\n",num);
printf("%dty %n\n",12345,&num);
printf("%d\n",num);
int number;
scanf("%*d%d",&number);
printf("%d\n",number);
return 0;
}
[...] | 所允许的字符 |
[^.]
GPS的输出报文:
%*[^,] ——从开始读到 , 为止,前面的东西都跳过。
, —— 用于读掉输入的 ,
%[^,] —— 将 , 前面的任何东西,作为字符串读入,交给sTime这个字符串变量。
printf、scanf的返回值
- 读入的项目数
- 输出的字符数
- 在要求严格的程序中,应该判断每次调用scanf或printf的返回值,从而了解程序运行中是否存在问题
scanf读到了1个1234;print输出了5个字符(第五个是回车换行符号\n)
13.1.2 文件的输入输出
程序的重定向方式:< >
FILE
- FILE* fopen(const char * restrict path,const char *restrict mode);
- int fclose(FlILE *stream);
- fscanf(FILE*,...)
- fprintf(FILE*,...)
打开文件的标准代码:
FILE* fp = fopen("file","r");//file是文件名,r表示读
if(fp){//如果没打开,会返回NULL
fscanf(fp,...);//读文件。...内容和scanf的内容一样
fclose(fp);
}else{
...//无法打开的反馈。如:输出 无法打开文件
}
#include <stdio.h>
int main(int argc, char const *argv [])
{
FILE*fp =fopen("12.in","r");
if ( fp ) {
int num;
fscanf(fp,"%d",&num) ;
printf( "%d\n", num) ;
fclose(fp);
} else {
printf("无法打开文件\n");
}
return 0;
}
fopen第一个参数是文件名的字符串,第二个参数字符串用途如下:
r | 打开只读 |
r+ | 打开读写,从文件头开始读/写 |
w | 打开只写,如果不存在则新建,如果存在则清空 |
w+ | 打开读写,如果不存在则新建,如果存在则清空 |
a | 打开追加,如果不存在则新建,如果存在,从文件尾开始 |
..x | 只新建,如果文件已存在则不能打开 |
13.1.3 二进制文件
二进制文件
- 其实所有的文件最终都是二进制的
- 文本文件无非是用最简单的方式可以读写的文件
- Unix:more、tail 可以打开文件;cat 打开文件或定向到其他文件;vi 做完整的编辑
- 而二进制文件是需要专门的程序来读写的文件
- 文本文件的输入输出是格式化,可能经过转码
文本 vs二进制
- Unix喜欢用文本文件来做数据存储和程序配置
- 交互式终端的出现使得人们喜欢用文本和计算机“talk"
- Unix的shell提供了一些读写文本的小程序
- Windows喜欢用二进制文件
- DOS是草根文化,并不继承和熟悉Unix文化
- PC刚开始的时候能力有限,DOS的能力更有限,二进制更接近底层
- 文本的优势是方便人类读写,而且跨平台
- 文本的缺点是程序输入输出要经过格式化,开销大
- 二进制的缺点是人类读写困难,而且不跨平台 int 的大小不一致,大小端的问题...
- 二进制的优点是程序读写快
程序为什么要文件
- 配置
- Unix用文本,Windows用注册表
- 数据
- 稍微有点量的数据都放数据库了
- 媒体
- 这个只能是二进制的
- 现实是,程序通过第三方库来读写文件,很少直接读写二进制文件了
二进制读写
- size_t fread(void *restrict ptr, size_t size, size_t nitems, FILE*restrict stream);
- size_t fwrite(const void *restrict ptr, size_t size,size_t nitems,FILE*restrict stream);
- 参数表:1 你要读或者写的那块内存(指针);2 这块内存多大;3 有几个这样的内存;4 文件指针。
- 注意FILE指针是最后一个参数
- 返回的是成功读写的字节数
为什么nitem?
- 因为二进制文件的读写一般都是通过对一个结构变量的操作来进行的
- 于是nitem就是用来说明这次读写几个结构变量!
头文件 student.h
#ifndef _STUDENT_H_
#define _STUDENT_H_
//const int STR_LEN = 20;
//这边用const来不行,还是改用宏定义
#define STR_LEN 20
typedef struct _student {
char name[STR_LEN];
int gender;
int age;
}Student;
#endif
main.c
#include <stdio.h>
#include "student.h"
void getList(Student aStu[],int number);
int save( Student astu[], int number);
int main( int argc, char const *argv [ ])
{
int number = 0;
printf("输入学生数量:");
scanf ("%d",&number);
Student aStu [number];
getList(aStu, number) ;
if ( save(aStu,number) ) {
printf("保存成功\n");
}else {
printf("保存失败\n");
}
return 0;
}
void getList(Student aStu[], int number)
{
char format[STR_LEN] ;
sprintf(format,"%%%ds",STR_LEN-1); //向字符串输出
// "%19s"
int i;
for ( i=0; i<number; i++ ) {
printf("第%d个学生:\n",i);
printf("\t姓名:");
scanf(format, aStu[i].name);
printf("\t性别(0-男,1-女,2-其他):" );
scanf ("%d", &aStu[i].gender) ;
printf("\t年龄:");
scanf ( "%d" , &aStu[i].age);
}
}
int save( Student aStu[], int number){
int ret = -1;
FILE*fp =fopen ( "student.data" , "w" );
if ( fp ) {
ret = fwrite(aStu, sizeof(Student), number,fp);
fclose(fp);
}
return ret == number;
}
在文件中定位
- long ftell(FILE *stream);
- int fseek(FILE*stream, long offset,int whence);
- SEEK_SET:从头开始
- SEEK_CUR:从当前位置开始
- SEEK_END:从尾开始(倒过来)
read.c
ude <stdio.h>
#include "student.h"
void read(FILE *fp, int index);
int main(int argc, char const *argv[])
{
FILE*fp = fopen( "student.data","r");
if ( fp ) {
fseek(fp,0L,SEEK_END); // SEEK_END从尾巴上开始,倒过来往前算,0,当前位置在尾巴上
long size = ftell(fp); // 得到当前位置,即文件大小
int number = size / sizeof(Student) ; // 文件大小除以结构大小,即为几个这样的结构
int index=0;
printf("有%d个数据,你要看第几个:", number) ;
scanf( "%d", &index) ;
read(fp, index-1); //一般人是从1开始,C语言从0开始
fclose(fp);
}
return 0;
}
void read(FILE*fp, int index)
{
fseek ( fp, index*sizeof( Student), SEEK_SET); // 从文件的头开始,定位到索引位置
Student stu;
if (fread(&stu, sizeof( Student), 1, fp) == 1 ) {
printf("第%d个学生: \n", index+1);
printf("\t姓名: %s\n" , stu.name);
printf("\t性别: ");
switch ( stu.gender ) {
case 0: printf("男\n"); break;
case 1: printf("女\n" ); break;
case 2: printf("其他\n" ); break;
}
printf("\t年龄: %d\n", stu.age);
}
}
可移植性
- 这样的二进制文件不具有可移植性
- 在int为32位的机器上写成的数据文件无法直接在int为64位的机器上正确读出
- 解决方案之一是放弃使用int,而是typedef具有明确大小的类型
- 更好的方案是用文本
时至今日,人们很少直接用C语言最底层的文件读写数据的方式来操作数据了,要么数据库,要么通过第三方库。
13.2 位运算
13.2.1 按位运算
C有这些按位运算的运算符:
- & 按位的与
- l 按位的或
- ~ 按位取反
- ^ 按位的异或
- << 左移
- >> 右移
按位与 &
- 如果 (x)i==1 并且 (y)i== 1,那么(x & y)i=1,否则的话(x& y)i= 0
- 按位与常用于两种应用:
- 让某一位或某些位为0∶ x & 0xFE
- 取一个数中的一段: x & 0xFF
按位或 l
- 如果 (x)i == 1或 (y)i == 1,那么 (x | y)i = 1,否则的话,(x l y)i == 0
- 按位或常用于两种应用:
- 使得一位或几个位为1 :x | 0x01(使得最右边那位为1)
- 把两个数拼起来∶ 0x00FF | 0xFF00
按位取反~
- (~x)i = 1 - (x) i
- 把1位变0,0位变1
- 想得到全部位为1的数: ~0
- 7的二进制是0111,x | 7 使得低3位为1,而x&~7,就使得低3位为0
#include <stdio.h>
int main(int argc, char const *argv [])
{
unsigned char c = 0xAA;
printf(" c=%hhx\n", c); //直接输出16进制的AA
printf("~c=%hhx\n", (char)-c) ; // 按位取反 结果为int 希望作为字节大小的整数输出
printf( "-c=%hhx\n", (char)-c); // 补码 结果为int 希望作为字节大小的整数输出
return 0;
}
逻辑运算vs按位运算
- 对于逻辑运算,它只看到两个值:0和1
- 可以认为逻辑运算相当于把所有非0值都变成1,然后做按位运算
- 5 & 4—>4,而 5 && 4—>1 & 1—> 1
- 5 | 4—>5,而 5 || 4—>1 | 1—> 1
- ~4 —>3,而 !4—> !1—>0
按位异或 ^
- 如果 (x)i == (y)i ,那么 (x ^ y)i = 0
- 否则的话,(x ^ y)i == 1
- 如果两个位相等,那么结果为0;不相等,结果为1
- 如果x和y相等,那么x^ y的结果为0
- 对一个变量用同一个值异或两次,等于什么也没做:x ^y ^y—>x
C语言没有表示幂次的运算符
13.2.2 移位运算
左移<<
- i << j
- i 中所有的位向左移动 j 个位置,而右边填入0,左移不管符号位
- 所有小于int的类型,移位以int的方式来做,结果是int
- x <<= 1 等价于 x *= 2
- x <<= n 等价于 x * = 2n
右移>>
- i >>j
- i中所有的位向右移j位
- 所有小于int的类型,移位以int的方式来做,结果是int
- 对于unsigned的类型,左边填入0
- 对于signed的类型,左边填入原来的最高位(保持符号不变)
- x >>= 1等价于 x /=2
- x >>= n等价于 x /=2n
#include <stdio.h>
int main(int argc, char const *argv [])
{
unsigned char c = 0xA5;
printf(" c = %d\n", c);
printf("c<<2 = %d\n", c<<2) ;
int a = 0x80000000; // 10000000...0000
unsigned int b = 0x80000000; // 10000000...0000
printf( " a = %d\n", a);
printf( " b = %u\n", b);
printf( "a>>1 = %d\n", a>>1); // 11000000...0000 有符号,保持符号不变
printf( "b>>1 = %u\n", b>>1); // 01000000...0000 无符号 左边填入0
int d = 0xC0000000; // 11000000...0000
printf("d = %d\n", d); // 结果和有符号0x80000000右移一位的结果一样
printf( "a<<1 = %d\n", a<<1); // 00000000...0000 左移不管符号位
printf( "b<<1 = %u\n", b<<1); // 00000000...0000
return 0;
}
移位的时候位数不要用负数,这是没有定义的行为:x<<-2 // NO!
13.2.3 位运算的例子
输出一个数的二进制
#include <stdio.h>
// 输出一个数的二进制数
int main(int argc, char const *argv [])
{
int number;
//scanf("%d",&number);
number = 0x55555555;
unsigned mask = 1u<<31; // 省略了int 编译器可理解。
// 1 就是最低位为1的数;u表无符号;左移31个bit;结果即为1000...0000
// 1从左向右一位一位移动
for( ; mask; mask>>=1){
printf("%d", number & mask?1:0);// 取余 结果大于0 输出1 否则输出0
}
printf("\n");
return 0;
}
单片机程序是常常遇到MCU的SFR特殊功能寄存器
怎么对这个寄存器做操作?
定义符号SBS PE;
用或使某些比特为1,用和使某些比特为0,由于SBS PE只有1个bit为1,可以或 | 起来 使得两个bit同时操作。
13.2.4 位段
在SFR表中,有的也不止一个bit。而前面的技巧只能控制一个bit,如何控制多个bit?
位段
把一个int的若干位组合成一个结构,冒号后面的数字表示 该成员占几个比特。
struct {
unsigned int leading : 3;
unsigned int FLAG1 : 1 ;
unsigned int FLAG2 : 1;
int trailing: 11;
};
#include <stdio.h>
void prtBin(unsigned int number) ;
struct U0 {
unsigned int leading: 3;
unsigned int FLAG1: 1;
unsigned int FLAG2: 1;
int trailing: 27;
};
int main(int argc, char const *argv[]){
struct U0 uu;
uu.leading = 2;
uu.FLAG1 = 0;
uu.FLAG2 = 1;
uu.trailing = 0;
printf("sizeof(uu)=%lu\n", sizeof(uu));
prtBin(*(int*)&uu);
//取得了uu的地址,指向struct U0的指针,强制类型转为指向int的指针,再取这个指针所指的int,交给prtBin函数。
return 0;
}
// 输出二进制位
void prtBin(unsigned int number)
{
unsigned mask = 1u<<31;
for( ; mask ;mask>>=1)
{
printf("%d",number&mask?1:0);
}
printf("\n");
}
- 可以直接用位段的成员名称来访问
- 比一位、与、或还方便
- 编译器会安排其中的位的排列,不具有可移植性
- 当所需的位超过一个int时,会采用多个int