学习材料是”C语言深度剖析”
补码
按我自己的理解, 补码其实很简单, 就是一个用无符号数(状态)表示有符号数的过程,因为计算机底层0101本身就没符号的概念,所以将有符号数建立在无符号数基础上也很自然:
128 |
129 | 127
130 | 126
131 | 125
… | …
254 | 2
255 | 1
| 0
注: ‘‘代表没有对应值, 同一行的两个数相加恒等于256(0除外);
如上图所示,我们以一个字节为例,也就是char类型, 竖线右边的值对应二进制最高位为0, 左边对应二进制最高位为1, 更宽的数据以此类推:
- 当为unsigned char时: 数据范围从0逆时针到255,一共256种状态.
- 当为signed char/char时,数据范围从(-128) - 127, 竖线左边为负值对应的补码(其原码就是对应的竖线右边的那个值取负, 如254对应的原码就是-2), 右边为正值补码(正值的补码=原码)
- 对于8bits, 0-255就是所有的256中状态,计算机只存储0-255对应的二进制, 如果要用有符号, 我把左边解析成负数,右边解析成正数就行了.
- 有了上面的图解析过程就很简单了:
- 从内存取出二进制值(补码) val;
- 当V的最高位为0时, 就把V的值作为解析结果;
- 当V的最高位为1时, 就把V-256的值作为解析结果;
- 问题是第三步, 总不能为了解析一个数,再让CPU去做以此减法吧, 正好(~val)+1 <=> 256-val => val-256 <=> -((~val)+1) (~val是val的二进制按位取反), 按位取反对计算机来说那就so easy了.
C语言32个关键字:
数据存储类别 : auto static register extern
基本数据类型 : void char int float double
类型修饰符 : short long signed unsigned
类型限定符 : const volatile
组合数据类型 : struct union enum
控制语句 : if else do while for switch case default break continue goto return
其它 : typedef sizeof
数据存储类别指示符:
C语言声明变量有存储类型指示符:extern, static, auto, register,不指定时编译器根据约定自动取缺省值auto。
存储类型指示符的位置是任意的,仅对于存储类型指示符而言:
C语言规范中,进行语法分析的时候,typedef和数据存储类别指示符是等价的, 仅对于typedef而言:
typedef不能和static等存储类型指示符同时使用,因为每种变量只能有一种存储类型.
1. static(静态变量):
- 修饰局部变量,扩展其生存期到整个程序生存期,但变量作用域不变,作用域外无法访问,位于静态存储区;
- 修饰全局变量或函数,限制其作用域,由整个程序或者项目到仅限于所在cpp文件(对应于内部链接);
- [C++]修饰类的的数据成员和成员函数,表示数据唯一性,即该数据成员或者成员函数属于类而不是某个对象.
注: linkage(链接性): 描述某个标识符(或名称)在整个程序或者某个源文件中能否绑定到同一实体(某块内存,如具体的变量以及函数体等)
无链接性:
局部变量(包括类的非静态成员), 函数形参.
内部链接(internal linkage): 标识符只能绑定到某个编译单元(预处理后的某个源码文件)内部的实体,不能跨编译单元:
a. 静态全局变量, 全局const常量(C++,C中还是外部链接);
b. inline函数(自由函数和类成员函数), 静态自由函数;
外部链接(external linkage):标识符可以跨编译单元绑定到某一实体:
a. 非静态全局变量, 类的静态数据成员;
b. 非静态自由函数,类成员函数和类静态成员函数;
2. extern(声明其具有外部链接属性,同static相反):
- 声明本编译单元中的全局变量(一般用于全局变量定义之前使用该全局变量)
- 声明其它源文件中的全局变量(注意这里的源文件是指cpp文件,不是头文件,头文件直接include就行了,不需要用extern);
- extern与const同时使用,表示具有外部链接属性的常量.
- extern “C” {}:用于C++代码,告诉编译器{}中的代码编译时不做名称改写,防止由于C++的名称改写而导致的连接错误;一般用于C语言写的库(要在头文件的声明中使用),并且允许它用于C++语言时.
类型限定符:
1. const(只读变量):
- 实质还是变量,只是不能修改(C中可通过指针间接修改);
- C中不能用const变量来指定数组元素个数,但C++中可以;
分析const的作用对象时,忽略基本类型说明符即可,所以
1const int i; <=> int const i;可以用来说明只读数组;
修饰指针:
1234const int * p; // => const (*p) <= 指针指向的内存不可修改int const * p; // => const (*p) <= 指针指向的内存不可修改int * const p; // => * (const p) <= 指针变量本身的值不可修改const int * const p; // => const (* (const p)) <= 指针变量本身和指针指向的内存都不可修改修饰函数参数: 防止实参被函数修改
- 修饰函数返回值: 返回值不可被改变,一般用于返回引用的函数,防止外部修改被引用的变量.
- const与#define
编译器不会为const只读变量分配存储空间,而是放在符号表中,效率高, const定义的只读变量从汇编角度来看,只是给出了对应的内存地址, 而不是像define一样给出的是立即数,const只读变量在程序运行期间只有一份拷贝, 因为它是全局只读变量, 存放在静态区:1234567const int N = 5; // 此时未将N放入内存中// ... main ...int I = M; // 预编译期间进行宏替换,分配空间int J = M; // 再进行宏替换,又一次分配内存int i = N; // 此时为N分配内存,以后不再分配!int j = N; // 没有内存分配
2. volatile(关闭优化,让编译器老老实实按语句的意思来编译):
|
|
|
|
foo5和foo6中的最长元素长度是int的长度(换句话说看的是类型长度,不是变量长度),所以foo5和foo6的长度一定是4的整数倍。
对于位域,比如foo6中a和b的类型是int,占4个字节,而a、b只占去了1个字节,占去的是这四个字节中的低位字节,也就是说,位域是通过偏移来实现的,而这个偏移是从低位向高位偏移,即a占据低位的0-4bits,b占据低位的5-7bits;
对于位域,我觉得只要把它以bytes为基本单位来分析,同时时刻铭记它是在一个内存模型(数据类型)中即可.
由于内存对齐的存在,我们在设计结构体或者C++的类时(C++中的结构体和类也有内存对齐), 就需要细心了. 比如下面的foo8占用24bytes, 但是通过调整成员顺序得到foo9, 它的内存占用就变为了16bytes, 这是多么客观!
详细的结构体内存对齐知识,可以研读失传的C结构体打包技艺,这是我从ludx那fork过来的,不是我翻译的,里面有原文链接, 另外clang编译器有个-Wpadded参数,加上它就会在编译输出信息中以警告的形式输出有关内存对齐的信息.
3. 结构体中的匿名成员
结构体中的匿名联合体或者结构体的成员,可以当做该结构体成员来访问,但是非匿名的就不能直接当成成员访问了.
4. union
- 所有成员共享一块内存空间,同一时间只能存储其中的一个数据成员,所有的数据成员具有相同的起始地址(等于union的起始地址);
- union所占字节数等于其最大成员所占字节数;
- 用union检测大小端(big_endian,little_endian):1234567891011121314int IsLittleEndian(){union un_ {short sh;char ch;} check;check.sh = 0x01;if (check.ch == 0x01) {return 1;} else {return 0;}}
注: 大端模式和小端模式:
几乎所有的机器上,多字节对象都被存储为连续的字节序列,例如C语言中,int型变量x的地址为0x100,则&x=0x100,这个0x100是指int所占的四个字节中地址最低的那个字节的内存地址(基址),与内存增长顺序无关。也就是说,x的四个字节将被存储在存储器的0x100,0x101,0x102,0x103的位置
而存储地址内的排列则有两种通用规则:大端模式和小端模式
大端:低地址存高字节
小端:低地址存低字节
3. enum(枚举,是个好东西)
- 枚举的成员称为枚举常量,枚举常量的值只能取整型值,不能是浮点值;
- 枚举里面的成员是可以取到的值,跟int变量可以取… -2 -1 0 1 2 …一样, 所以sizeof一个enum的结果是4,因为默认成员是int类型;
- 注意: struct,union成员都是用分号”;”分割,而enum是用逗号”,”分割的; struct,class,union,enum的{}后面要加”;”。
- 用enum标示插件或者选项:123456789101112131415161718192021enum flag_ {ATE = 0x00000001;APP = 0x00000002;IN = 0x00000004;OUT = 0x00000008;BIN = 0x00000016;TRUNC = 0x00000032;} flag;...flag option = OUT | ATE | BIN;bool open(const char* filename, const flag option){if (option & ATE == ATE) { // 设置了此选项...}if (option & APP == APP) { // 设置了此选项...}...}// 同样的,插件系统中也可以用enum标示哪些插件启用了,哪些没启用,启用一个就按位或(|)上对应标识,关闭一个就按位与(&)上对应标识的按位取反(~)
控制语句
- default语句不应省略,而且要用来处理真正的默认情况(或者未定义的行为);
- case 后面只能是整型或字符型的常量或常量表达式,const变量不可以;
- 多重循环中,尽可能将长循环放在外层,最短的放最内层;
- 函数如果无参数,应当显示用void指明;
- 所有函数都应该显示给出返回值,不应以来编译器特性(构造,析构除外).
typedef(给已经存在的类型起别名)
1. typedef与类型限定符const
|
|
2. typedef与#define
|
|
|
|
sizeof
sizeof()求值是在编译期, 所以类似如下代码是没问题的:
但是如果通过函数而不是sizeof()来使用a[15]就会出问题,因为函数是运行期执行的,运行期a[15]明越界了.
符号
注释(/**/ //)
1. /* */ 预处理时,编译器会使用空格替换,所以
|
|
2. y = x / *p 不等于 y = x/*p,
因为编译器解析语法的时候,是从左到右,找最长的有意义的单元,对于第二段代码,编译器找到x后继续往后找,判断’x/‘是否是合法的标识符或者关键字,发现不是,于是记录下’x’,
然后从’/‘开始找,继续向下找,判断’/‘是否是有意义的符号,发现是,他是注释的开始,但不会停止,编译器继续往下找,判断’/\p’是不是有意义的符号,发现不是,于是记下’/*‘,
再从’p’开始, … ,
所以第二段语句是错误的,因为解析出来了’/*‘但没解析出*/,无法配对,所以报错.
逻辑运算符(|| &&)
1. 存在短路现象:
|
|
或运算: 从左到右,发现值大于0的条件后,后面的条件不会再计算;
与运算: 从左到右,发现值等于0的条件后,后面的条件不会再计算。
位运算符(| & ~ ^ << >>)
- << 左操作数的各个二进制为向左边移动右操作数个位值,高位丢弃,低位补零;
- >> 左操作数的各个二进制为向右边移动右操作数个位值,无符号数,高位补零,有符号数,高位补符号位的数值(正数为0,负数为1);
- << >> 的位数不能大于数据长度,不能小于0。
自增自减运算符(++ –)
1. C中的计算单位:
- 逗号表达式中,每一个”逗号(,)”,都表示一个计算单位的结束;
- for循环中,中间的条件表达式计算完成之后,表示一个计算单位结束;
- “分号(;)”表示一个计算单位结束。1234567891011int i = 1;int j = (i\++, i\++, i\++); // 逗号表达式,i每遇到一个逗号就认为本计算单元结束,自加int y;//每次y与10比较完,不管比较结果,Y就认为本计算单元结束,自加,不是{}里的算完就马上加。for ( y = 0; y < 10; y\++){NULL;}i = 1;int k = (i\++) + (i\++) + (i\++); // i遇到;认为本次计算单元结束,把累积的三次自加全部执行一次,为6
2. var++,var–计算单位结束后才会进行自增或者自减操作.
运算符优先级
1. 运算符优先级表

2. 易错点:
(copy自C语言中文网)

连接符(\)
反斜杠作为连接符时,后面不能有任何字符,即使空格也不行.
预处理
预处理指令:
1. 预处理指令表
(copy自C语言中文网)


2. ASCII C 定义的宏:
_LINE_: 正在编译的文件的行号
_FILE_: 正在编译的文件名
_DATE_: 编译时刻的日期字符串 “25 Dec 2017”
_TIME_: 编译时刻的时间字符串 “12:30:58”
宏定义(#define #undef)
|
#pragma预处理
用于设定编译器状态或者只是编译器完成一些特定动作
1. #pragma message(“文本”)
在编译信息输出窗口输出相应的”文本”信息.
2. #pragma once
保证头文件只被变编译一次,防止头文件的重复包含.
3. #pragma warning(…)
|
|
4. #pragma pack(n)
|
|
注: 这里用
指针与数组
指针
指针就是一个变量, 只不过这个变量的值,是一个内存地址而已.
- 指针变量 加/减 整数,所表达的含义是:相对当前指针值的偏移,偏移量为: 整数*指针指向的数据类型的大小
- void 类型的指针,偏移时是以字节为单位的,同char (gcc)
- 相同类型的指针 相减,所表达的含义是: 这两个指针之间的偏移量(元素的个数) <=>
/ sizeof(data_type)``` 123456789* 相同类型的指针不能进行 相加 运算* 不同类型的指针不能进行算数运算### 函数指针#### 1. 函数指针函数与变量/常量一样,也要存储与内存中,只不过变量/常量存的是数据,而函数存的是逻辑或者叫算法,之前说过,凡是内存中的东西,都可以通过指针访问(当然这只是理论上),那函数也不例外,也可以通过指针来访问,指向函数的指针成为函数指针,函数名称就代表函数的地址,当然也可以对函数名取地址,二者效果相同, 即```func() => func <=> &func```.```Cchar *func(const char* ch1, const char* ch2); // 声明一个函数char (*pfunc)(const char* ch1, const char* ch2); // 定义一个函数指针
上面的代码中,func是一个返回字符指针的函数, 而pfunc是一个返回字符的函数指针,二者的形参列表是相同的.
2. 函数指针数组
|
|
上面的代码定义了一个函数指针数组,跟指针数组没什么区别,只不过指针数组里的元素指向的是数据,而函数指针数组里面的元素指向的是函数而已,但就数组本身而言,二者是完全一样的,N一定,二者大小都一样.
既然是数组,当然也可以定义函数指针数组的指针, 与普通数组指针没啥区别:
数组
1. 数组名的含义
数组名不能作为左值(放在赋值运算符左边), 作为右值时代表的是数组首元素地址, 但sizeof(数组名)的结果是整个数组的大小,因为此处数组名不是右值;
如下例中, a是数组名(代表数组首元素的地址), 而&a的含义是数组的地址, 它们在值上是相等的,因为数组首元素相对于整个数组来说,它的偏移是0,所以在内存中数组的地址等于数组首元素的地址; 但是它们两个的含义完全不同,通过取值就能看出, (&a) 是地址,等同与数组名a, 而a是一个值,是首元素的值.
2. 数组与指针
它们没有任何关系, 只是有相似的使用方式而已.
虽然前面我们说数组名代表首元素的地址, 但只是代表而已, 实际上并没有a这么一个变量,里面存着数组有元素的地址.
由上例可以看出, a显然不是指针,因为指针变量的大小是8, 而&a才是实实在在的指向数组的指针;
另外(&a)与p也是完全不同的, (&a)表示的是整个数组, 而p只是代表一个字符而已;
只要是存在与内存中的数据,都能通过指针来访问,数组也是存在于内存中的数据,所以它也可以通过指针(利用基址+偏移地址方式)来访问, 就跟用指针访问基本数据一样, 只是恰好C语言支持指针通过下表来操作, 同时数组支持通过偏移的形式来操作, 所以看起来指针与数组比较像而已.
|
|
编译器总是把基于下标的操作解析为基于’基址+偏移’的操作方式, 汇编就是这么干的.
3. 基址+偏移方式访问数组元素问题
直接看下面的例子即可, ptr3这种情况需要特别注意:
4. 数组指针与指针数组
|
|
5. 数组作为函数参数
一维数组作为函数参数, 编译器会把它处理成指针:
6. 多维数组
一维数组作为函数参数, 编译器会把它处理成指针, 但这个过程不是递归的, 也就是说只有一维数组才会这样, 当数组超过一维时, 将第一维改写为指向数组的指针后,后面的维再也不可改写:
二维数组做参数第一维的维度可以省略,但第二维的不行, 因为它标示了第一维的指针指向数据的类型.
有个小陷阱要注意:
内存管理
linux程序内存模型:
每一个进程都有一个大小与物理内存相同的虚拟内存空间, 然后具体用时映射到物理内存, 因为有虚拟内存空间的存在, 所以编译器和连接器可以在编译或链接时直接分配内存地址, 它们分配的是虚拟内存地址.

- 用户栈: 局部自动变量,函数栈等,向低地址生长
- 运行时堆(动态内存分配区): malloc/new, 向高地址生长
- 读/写段(静态数据区):
- .data: 已经初始化的全局自动变量和静态变量(全局的和局部的)
- .bss: 未初始化的全局自动变量和静态变量(全局的和局部的)
- 只读段(代码段):
- .text: 存放程序代码
- .rodata: 常量区,存储字符串常量,const常量(全局的或者静态的,局部的不在这)
所谓的内存管理,其实就是管理”运行时堆”.
函数
递归
不使用库函数编写strlen, 但是当str很长时间,这个函数并不实用: