一.《第1课 - 基本数据类型》
1. 数据类型
1.1 什么是数据类型?
在汇编时期,没有数据类型的概念。汇编程序中经常需要申请1byte、2byte、4byte大小的内存空间,程序员需要记住内存的起始地址,非常麻烦。。。。。
(1)数据类型可以理解为固定内存大小的别名
(2)数据类型是创建变量的模子
1.2 数据类型的本质
2. 变量的本质
(1)变量是一段实际连续存储空间的别名
(2)程序中通过变量来申请并命名存储空间
(3)通过变量的名字可以使用存储空间
3. 实例分析
【类型与变量的关系】
#include<stdio.h>
int main()
{
char c;
short s;
int i;
// 模子 和 模子所创建的变量 的大小应该相同
printf("%zu, %zu\n", sizeof(char), sizeof(c)); // 1,1
printf("%zu, %zu\n", sizeof(short), sizeof(s)); // 2,2
printf("%zu, %zu\n", sizeof(int), sizeof(i)); // 4,4
return 0;
}
【自定义类型与创建的变量】
#include<stdio.h>
typedef int INT32;
typedef unsigned char BYTE;
typedef struct _tag_TS
{
BYTE b1;
BYTE b2;
short s;
INT32 i;
}TS;
int main()
{
INT32 i32;
BYTE b;
TS ts;
// 自定义的模子大小 == 由该模子创建出来的变量的大小
printf("%zu, %zu\n", sizeof(INT32), sizeof(i32)); // 4, 4
printf("%zu, %zu\n", sizeof(BYTE), sizeof(b)); // 1, 1
printf("%zu, %zu\n", sizeof(TS), sizeof(ts)); // 8, 8
return 0;
}
二.《第2课 - 有符号数与无符号数》
1. 有符号整型的符号位
有符号整型有正数和负数,那在内存中是如何区分它们的呢?最高位用来标识数据的正负。
- 最高位为1,表明这个数为负数
- 最高位为0,表明这个数为正数
【有符号数的符号位】
#include <stdio.h>
int main()
{
char c = -5;
short s = 6;
int i = -7;
// 与最高位进行&运算,结果为0表示正数,结果为1表示负数
printf("%d\n", ((c & 0x80) != 0 )); // 1 ==> 负数
printf("%d\n", ((s & 0x8000) != 0)); // 0 ==> 正数
printf("%d\n", ((c & 0x80000000) != 0)); // 1 ==> 负数
return 0;
}
2. 有符号整型的表示法
在计算机内部用补码表示有符号整型
- 正数的补码为正数本身
- 负数的补码为负值的绝对值各位取反后加1
举例:
char型整数 5 的补码:0000 0101
char型整数 -7 的补码:0000 0111 ==> 1111 1000 ==> 1111 1001
short型整数 20 的补码:0000 0000 0001 0100
short型整数 -13 的补码:0000 0000 0000 1101 ==> 1111 1111 1111 0010 ==> 1111 1111 1111 0011
3. 无符号整型的表示法
(1)在计算机内部用原码表示无符号数
-
- 无符号数默认为正数
- 无符号数没有符号位
(2)对于固定长度的无符号数
-
- MAX_VALUE + 1 ==> MIN_VALUE
- MIN_VALUE - 1 ==> MAX_VALUE
(3)signed和unsigned
-
- C语言中整型变量默认为有符号的类型
- 用unsigned关键字声明整型变量为无符号类型
1 int i; // 默认为有符号整数
2
3 signed int j; // 显式声明变量为有符号整数
4
5 unsigned int k ; // 声明为无符号整数
下面看两个笔试中容易考察到的题目:
(1)当有符号数遇到无符号数时,有符号数在内存中的值会被看作无符号数
#include<stdio.h>
int main()
{
unsigned int i = 5;
int j = -10;
// 有符号数遇到无符号数,会被看作无符号数
// -10 在内存中表示为 0000 0000 0000 0000 0000 0000 0000 1010 ==>
// 1111 1111 1111 1111 1111 1111 1111 0101 ==>
// 1111 1111 1111 1111 1111 1111 1111 0110 ==> 这个看作无符号数是一个非常大的数
if ((i + j) > 0)
{
printf("i+j >= 0\n"); // 程序走到这个分支
}
else
{
printf("i+j <= 0\n");
}
return 0;
}
(2)错误的使用了unsigned,unsigned修饰的无符号数不会是一个负数
#include <stdio.h>
int main()
{
unsigned int i = 0;
// 当i = 0时,i--变为最大值
// 前面讲过 MIN_VALUE - 1 ==> MAX_VALUE
for (i = 9; i >= 0; i--) // i >= 0一直成立
{
printf("i = %u\n", i);
}
return 0;
}
三.《第3课 - 浮点数的秘密》
1. 浮点数在内存中的存储方式
float与double类型的数据在计算机内部的表示法是相同的,但由于所占存储空间的不同,其分别能够表示的数值范围和精度不同。
2. 浮点数的转换
2.1 浮点数的转换方法
如何将十进制的浮点数转换为内存中二进制表示的浮点数呢?按照下面三个步骤:
① 将浮点数转换为二进制
② 用科学计数法表示二进制浮点数
③ 计算指数偏移后的值(需要加上偏移量,float型:指数 + 127,double型:指数 + 1023)
比如对于指数6,float型偏移量就为 127 + 6 = 133,double型偏移量为1023 + 6 = 1029
2.2 浮点数的转换示例
下面以十进制的 8.25 演示一下上述的转换方法。
① 将8.25转换为二进制(注意小数的二进制表示方法) ==> 1000.01
② 用科学计数法表示1000.01(注意这里是二进制,2^3) ==> 1.00001(2^3)
③ 指数3偏移后为:127 + 3 = 130
所以浮点数8.25对应的符号位、指数、尾数分别为:
符号位:0
指数:130 ==> 10000010
小数:00001 // 要将小数转为尾数,需要在后面补0
8.25在内存中的二进制表示为:0 1000 0010 000 01000000 0000 0000 0000 = 0x41040000
下面我们写代码验证一下:
#include <stdio.h>
int main()
{
float f = 8.25;
unsigned int *p = (int *)&f;
printf("f = 0x%08x\n", *p);
return 0;
}
结果和我们前面分析的相同。
3. 浮点数的秘密
我们知道int型数据占用4个字节,表示的范围为:【-231 , 231-1】;float型也占用4个字节,表示的范围为:【-3.4*1038,3.4*1038】。
看到这里不少人很奇怪,4个字节按照排列组合能表示的数据范围应该是固定的,为什么float类型表示的范围却比int型的大?这里我们就要揭露一下float类型的秘密了。
(1)float能表示的具体数值的个数与int型相同,因为都是4字节,排列组合都是232-1个
(2)之所以float表示出来的范围比int型大,是因为float可表示的数字之间是不连续的,数据之间存在跳跃
(3)float只是一种近似的表示法,不能作为精确数使用
(4)由于float类型的内存表示方法相对复杂,float的运算速度比int慢很多
※ 因为double与float具有相同的内存表示法,所以double也是不精确的。由于double占用的内存较多,所能表示的精度比float高。
【float类型的不精确示例】
#include<stdio.h>
int main()
{
float f1 = 3.1415f;
float f2 = 123456789;
// 精确到小数点后面的10位
printf("%0.10f\n", f1); // 3.1414999962 ==> 不等于3.1415,表示的结果是不精确的
printf("%0.10f\n", f2); // 123456792.0000000000 ==> 与123456789不相等,表示的数据是不连续的、跳跃的
return 0;
}
再添加0.25和1.25的转换方法,加深理解:
0.25
① 0.25 转换为二进制 0.01
② 二进制0.01用科学计数法表示 1.0*(2^-2),指数为 -2 + 127 = 125
③ 小数为0
所以0.25在内存中表示为:0 01111101 00000000000000000000000 = 0x3e800000
1.25
① 1.25转换为二进制 1.01
② 二进制1.01用科学计数法表示1.01*(2^0) ,指数为 0 + 127 = 127
③ 小数为0.25
所以1.25在内存中表示为:0 01111111 01000000000000000000000 = 0x3fa00000
四.《第4课 - 类型转换》
1. 类型之间的转换
C语言的数据类型之间可以进行转换,包括以下两种:
- 强制类型转换 ==> 程序员人为的将数据类型进行转换
- 隐式类型转换 ==> 编译器主动进行的数据类型转换
【类型之间的转换】
#include<stdio.h>
int main()
{
long l = 800;
int i = (int)l; // 强制类型转换
short s = 800;
int k = s; // 隐式类型转换
return 0;
}
2. 强制类型转换
(1)强制类型转换的语法
(Type)var_name
(Type)value
(2)强制类型转换的结果
-
- 如果目标类型能够容纳目标值,那么结果不变
- 如果目标类型不能容纳目标值,那么结果将产生截断
(3)※注意:不是所有的强制类型转换都能成功,当不能进行强制类型转换时,编译器将产生错误信息
【强制类型转换分析】
#include <stdio.h>
struct TS {
int i;
int j;
};
struct TS ts;
int main()
{
short s = 0x1122;
char c = (short)s; // 目标类型不能容纳目标值,产生截断,取内存中的最后一个字节,0x22
int i = (int)s; // 目标类型能够容纳目标值,结果不变,0x1122
int j = (int)3.1415; // 浮点数和整型在内存中的表示方法不同,截断的方法就是舍弃小数部分,取整数部分
// 3
unsigned int p = (unsigned int)&ts; // 在32位系统上指针为4字节,是可以的;在64位系统上指针为8字节,会产生截断
//long l = (long)ts; // 两种类型之间不能进行强制类型转换,编译报错 error: aggregate value used where an integer was expected
// ts = (struct TS)l; // 与上面相同,编译报错
printf("s = 0x%x\n", s); // 0x1122
printf("c = 0x%x\n", c); // 0x22
printf("i = 0x%x\n", i); // 0x1122
printf("j = 0x%x\n", j); // 0x3
printf("p = 0x%x\n", p); // 0x601044
printf("&ts = %p\n", &ts); // 0x601044
return 0;
}
3. 隐式类型转换
(1)隐式类型转换
隐式类型转换是编译器主动进行的数据类型转换。
1 char c = 0; // 变量c占用1个字节
2
3 short s = c; // c到s -> 隐式类型转换
4 int i = s; // s到i -> 隐式类型转换
5 long l = i; // i到l -> 隐式类型转换
※注意:
① 低类型到高类型的隐式转换是安全的,不会产生截断 (高类型与低类型是相对的,比较的是占用内存的大小)
② 高类型到低类型的隐式转换是不安全的,导致不正确的结果
(2)隐式类型转换的发生点 ★★★
-
- 算术运算中,低类型转换为高类型
- 赋值表达式中,表达式的值转换为左边变量的类型
- 函数调用时,实参转换为形参的类型
- 函数返回值,return表达式转换为返回值类型
※ 标准C编译器的类型检查是比较宽松的,因此隐式类型转换可能带来意外的错误,写程序时需要时刻注意隐式类型转换!!!
【隐式类型转换分析】
#include <stdio.h>
int main()
{
char c = 'a'; // 97
int i = c; // 低类型==>高类型,安全
unsigned int j = 0x11223344;
short s = j; // 高类型==>低类型,不安全
printf("c = %c\n", c); // ‘a’
printf("i = %d\n", i); // 97
printf("j = 0x%x\n", j); // 0x11223344
printf("s = %x\n", s); // 0x3344
printf("sizeof(c+s) = %d\n", sizeof(c+s)); // char和short都会先转化为int,然后进行运算,结果为4
return 0;
}
五.《第5课 - 变量属性》
1. C语言变量的属性
C语言中的变量可以拥有自己的属性。在定义变量时可以加上属性关键字,用来指明变量的特有意义。
语法:
property type var_name;
示例:
1 auto char i; 2 register int j; 3 static long k; 4 extern double m;
2. auto关键字
(1)auto属性关键字将被修饰的变量存储在栈上
(2)C编译器默认所有的局部变量都是auto属性的(auto是局部变量的默认属性),即局部变量存储在栈上
示例:
1 void f(){ 2 int i; // 局部变量默认属性为auto 3 auto int j; // 显式声明局部变量为auto属性 4 }
3. register关键字
(1)register关键字请求编译器将局部变量存储于寄存器中,而不是内存中,以加快其存取速度,多用于修饰需要频繁使用的局部变量
※※ register不能修饰全局变量
(2)不能使用 & 运算符获取register变量的地址,因为寄存器是没有地址的,只有内存才有地址
#include<stdio.h>
// register int g_v; // 全局变量的生命周期从程序运行到程序结束,那么在整个过程中都要占用寄存器,但CPU寄存器的数量是有限的,长时间占用会影响CPU工作
// 因此不允许register修饰全局变量,register修饰全局变量编译器会直接报错,error!
int main()
{
register char var;
// printf("0x%08X\n", &var); // 寄存器变量没有地址,编译报错,error!
return 0;
}
(3)register只是请求寄存器变量,但不一定请求成功
(4)由于register修饰的变量存储在寄存器中,因此该变量必须是CPU寄存器可以接受的值
下面是libevent中一个拷贝字符串的函数,使用了register关键字
size_t event_strlcpy_(char *dst, const char *src, size_t size)
{
register char *d = dst;
register const char *s = src;
register size_t n = size;
/* Copy as many bytes as will fit */
if (n != 0 && --n != 0) {
do {
if ((*d++ = *s++) == 0)
break;
} while (--n != 0);
}
/* Not enough room in dst, add NUL and traverse rest of src */
if (n == 0) {
if (size != 0)
*d = '\0'; /* NUL-terminate dst */
while (*s++)
;
}
return (s - src - 1); /* count does not include NUL */
}
前面说了register可以加快局部变量的存取速度,可能不太直观,我们通过下面的例子直观的感受一下! 0.68s VS 0.195s
4. static关键字
(1)static 关键字指明变量的"静态"属性,局部变量存储在程序静态区(普通的局部变量存储在栈上)
(2)sttaic关键字同时具有"作用域限定符"
- static修饰的全局变量,作用域是声明该变量的文件中,其它文件不能使用
- static修饰的函数,作用域是声明该函数的文件中,其它文件不能使用
#include <stdio.h>
int g_v; // 全局变量,程序的任意地方均能访问
static int g_vs; // 静态全局变量,只有当前文件中可访问
int main()
{
int var; // 局部变量,在栈上分配空间
static int svar; // 静态局部变量,在静态数据区分配空间
return 0;
}
【auto、register、static对比分析】
#include<stdio.h>
int f1()
{
int r = 0;
r++;
return r;
}
int f2()
{
static int r = 0; // 静态局部变量,只初始化一次
r++;
return r;
}
int main()
{
auto int i = 0; // 显式声明auto属性,i为栈变量
static int k = 0; // 局部变量k的存储区位于静态区,作用域位于main中
register int j = 0; // 向编译器申请将j存储于寄存器中
// 两个变量在前面是相邻定义的,地址差别却非常之大,就是前者存储在栈上,后者存储在静态区
printf("%p\n", &i); // 0x7ffd7784424c
printf("%p\n", &k); // 0x601048
// printf("%p\n", &j); // compile error,寄存器变量不能取地址
for (i = 0; i < 5; i++)
{
printf("%d\n", f1()); // 1 1 1 1 1
}
for (i = 0; i < 5; i++)
{
printf("%d\n", f2()); // 1 2 3 4 5
}
return 0;
}
5. extern关键字
(1)extern 用于声明"外部"定义的变量和函数
- extern 变量在其它地方分配空间
- extern 函数在文其它地方定义
#include<stdio.h>
extern int g_i; // 告诉编译器g_i在其它的地方定义
int main()
{
printf("%d\n", g_i); // 先使用,后面链接时在其它地方再寻找
return 0;
}
int g_i = 0;
(2)extern用于告诉编译器用C方式编译代码
【static和extern的使用】
// g.c
static int g_i; // g_i只能在本文件中使用
int getI()
{
return g_i;
}
// main.c
#include<stdio.h>
extern int getI(); // extern声明getI()是在其它地方定义的
int main()
{
printf("%d\n", getI()); // 0
return 0;
}
六.《第9课 - const 和 volatile分析》
1. const只读变量
(1)const修饰的变量是只读的,本质上还是变量,并不是真正意义上的常量 ※※ const只是告诉编译器该变量不能出现在赋值符号的左边
(2)const修饰的局部变量在栈上分配空间;const修饰的全局变量在全局数据区分配空间
(3)const只在编译期间有用(检查const修饰的变量有没有出现在赋值符号左边,如果有就会编译报错),在运行期间无用
#include <stdio.h>
int main()
{
const int cc = 1;
int *p = (int*)&cc;
// cc = 10; // compile error: assignment of read-only variable ‘cc’
printf("cc = %d\n", cc); // cc = 1
*p = 10;
printf("cc = %d\n", cc); // cc = 10
return 0;
}
2. const全局变量的分歧
(1)在标准C语言编译器中,const修饰的全局变量仍然存储于全局数据区,并没有改变存储方式,通过指针可以隐式的修改全局变量的值。
(2)在现代C语言编译器中,将const 修饰的全局变量分配在只读存储区,改变了存储方式,通过指针隐式修改会导致程序段错误。
#include <stdio.h>
const int g_ci = 100;
int main()
{
int *p = (int *)&g_ci;
// g_ci = 10; // compile error: assignment of read-only variable ‘g_ci’
printf("g_ci = %d\n", g_ci);
*p = 10; // 通过指针隐式修改
printf("g_ci = %d\n", g_ci);
return 0;
}
使用gcc编译执行(现代C编译器) ==> 段错误
使用bcc32编译执行(标准C编译器) ==> 修改成功
3. const的本质
(1)C 语言中的const 使得变量具有只读属性
(2)现代C编译器中的const将具有全局生命周期的变量(全局变量 + static修饰的局部变量)存储于只读存储区
【static修饰局部变量】
#include <stdio.h>
int main()
{
const static int si = 100; // const修饰static修饰的局部变量
int *p = (int *)&si;
*p = 1; // 使用gcc、VS2010编译执行会产生段错误
// 使用bcc32编译执行,可以修改si的值为1
printf("si = %d\n", si);
return 0;
}
【const的本质分析】
#include <stdio.h>
const int g_array[5] = {0};
void modify(int* p, int v)
{
*p = v;
}
int main()
{
int const i = 0; // const放在int前后都可以
const static int j = 0;
int const array[5] = {0};
modify((int*)&i, 1); // ok
modify((int*)&j, 2); // error,j存储在只读存储区
modify((int*)&array[0], 3); // ok
modify((int*)&g_array[0], 4); // error,g_array[5]数组存储在只读存储区
printf("i = %d\n", i);
printf("j = %d\n", j);
printf("array[0] = %d\n", array[0]);
printf("g_array[0] = %d\n", g_array[0]);
return 0;
}
4. const修饰函数参数和返回值
(1)const 修饰函数参数表示在函数体内不希望改变参数的值
(2)const 修饰函数返回值表示返回值不可改变,多用于返回指针的情形
TIP:C 语言的字符串字面量存储于只读存储区中,在程序中需要使用 const char* 指针
【const修饰函数参数与返回值】
#include <stdio.h>
const char* f(const int i)
{
// i = 5; // error, i不能作为左值
return "swj striving! 2019-12-23 22:23:57";
}
int main()
{
char* pc = f(0); // 编译会报warning,函数f的返回值为const char*
printf("%s\n", pc);
pc[1] = '_'; // error,试图修改只读存储区中的字符串
printf("%s\n", pc);
return 0;
}
5. 深藏不露的volatile
(1)volatile 可理解为 "编译器警告指示字"
(2)volatile 告诉编译器必须每次去内存中取变量值
(3)volatile 主要修饰可能被多个线程访问的变量
(4)volatile 也可以修饰可能被未知因数更改的变量
6. 有趣的问题
const volatile int i = 0;
ⅰ:变量 i 具有什么样的特性? i为int型变量,每次都必须到内存取值,且i是只读变量
ⅱ:编译器如何处理这个变量? i不能作为左值
七.《第13课 - 接续符和转义符》
1. 接续符
(1)接续符的意义:C语言中的接续符(\)是指示编译器行为的利器
/**********
额外的思考发现:
① 接续符由预处理器在预处理阶段处理
② 接续符在读取后面的数据时,应该是根据"贪心法则"读取的,可以参考gcc -E的结果,接续符后面一行并没有全部读入
③ #define 宏代码块只能定义在同一行,不能跨行(语法规定);可以使用接续符多行显示宏代码块,代码可读性更高。
**********/
(2)接续符的使用
- 编译器会将接续符剔除,跟在接续符后面的字符自动接续到前一行
- 在接续单词时,接续符之后不能有空格,接续符的下一行之前也不能有空格
- 接续符适合在定义宏代码块时使用
【接续符在宏中的应用】
#include <stdio.h>
// 该宏实现两个变量值的交换
// 使用接续符,宏代码块更美观,可读性更强
#define SWAP(a,b) \
{ \
int temp = a; \
a = b; \
b = temp; \
}
int main()
{
int a = 1;
int b = 2;
int c = 3;
SWAP(a,b);
printf("a = %d, b = %d\n", a, b); // a = 2, b = 1
SWAP(b,c);
printf("b = %d, c = %d\n", b, c); // b = 3, c = 1
return 0;
}
2. 转义符
(1)C语言的转义符(\)主要用于表示无回显字符,也可用于表示常规字符
(2)当反斜杠作为转义字符使用时必须出现在单引号或者双引号之间
#include <stdio.h>
int main()
{
char enter = '\n';
char* p = "\141\t\x62"; // 八进制的141对应十进制的97,即a的ASCII码
// 十六进制的62对应十进制的98,即b的ASCII码
// \t 表示tab制表符
printf("%s", p); // a b
printf("%c", enter); // 换行
return 0;
}
3. 小结
C语言中的反斜杠(\)同时具有接续符和转义符的作用
- 作为接续符使用时可以直接出现在程序中
- 作为转义符使用时需要出现在单引号或双引号之间