C语言是一个底层的、面向过程的语言。
其优点主要有:
其缺点主要有:
while(condition){ ... }
语句很容易因为多打一个分号从而导致无限循环 while(condition);{ ... }
考察企业:
sizeof
)直接在预处理期完成。考查企业:
C语言编译通常有以下过程:
#include
、 #define
等宏操作或条件编译预处理为普通的代码文本,可以给程序增加或减少内容。Windows平台下:
Linux平台下:
以及静态链接、动态链接的优缺点(操作系统知识)
ASCII[0]
到 ASCII[31]
是控制字符,重要控制字符如下:
ASCII[10]
: \n
,换行符ASCII[13]
: \r
,回车符ASCII[48]
到 ASCII[57]
是数字 0-9
,'0'在前面ASCII[65]
到 ASCII[90]
是大写字符 A-Z
,大写排在小写前面ASCII[97]
到 ASCII[122]
是小写字符 a-z
,大写排在小写前面补码计算规则:
0
,负数为 1
,不参与下述运算。int32_t a = -13
为例,为int32_t a = -13; // (1). 13的二进制为: 1000 0000 0000 1101
int32_t a = -13; // (2). 除符号位取反: 1111 1111 1111 0010
1
,除符号位不动,其他位正常溢出:int32_t a = -13; // (3). 符号位不动,二进制加一: 1111 1111 1111 0011
例1:
1
直接存储负数,并按照二进制加法处理 -1
和 2
,则会有:int32_t a = -1; // 按照本假设原则,二进制为:1000 0000 0000 0001
int32_t b = 2; // 按照本假设原则,二进制为:0000 0000 0000 0010
int32_t c = a + b; // 直接进行二进制加法,结果:1000 0000 0000 0011 => -3
-1
和 2
,则会有:int32_t a = -1; // 按照补码原则,二进制为:1111 1111 1111 1111
int32_t b = 2; // 按照补码原则,二进制为:0000 0000 0000 0010
int32_t c = a + b; // 直接进行二进制加法,结果:0000 0000 0000 0001 => 1
几个比较重要(常考)的运算优先级:
.
运算符取成员的优先级大于 *
解引用的优先级,例如:
*p.member
错误(*p).member
正确int (*p)[3]
, p
为一个指向有3个 int
型变量的数组的指针int *p[3]
, p
是一个有3个 int*
变量的数组a = *p++
,先对 p
解引用,随后 p
自增a = *(p++)
,同上a = (*p)++
,对 p
解引用,并对 p
指向的对象自增在 C Programming: A Modern Approch, Second Edition (K.N.King) 的附录A中给出了如下优先级表:
优先级 | |||
---|---|---|---|
1 | 数组取下标 | [] |
左结合性 |
1 | 函数调用 | () |
左结合性 |
1 | 取struct和union成员 | . 、 -> |
左结合性 |
1 | 自增(后缀) | i++ |
左结合性 |
1 | 自减(后缀) | i-- |
左结合性 |
2 | 自增(前缀) | ++i |
右结合性 |
2 | 自减(前缀) | --i |
右结合性 |
2 | 取地址 | & |
右结合性 |
2 | 间接寻址 | * |
右结合性 |
2 | 一元正号 | + |
右结合性 |
2 | 一元负号 | - |
右结合性 |
2 | 按位求反 | ~ |
右结合性 |
2 | 逻辑非 | ! |
右结合性 |
2 | 计算变量空间 | sizeof |
右结合性 |
3 | 强制类型转换 | (type) |
右结合性 |
4 | 乘法类运算符 | * 、 / 、 % |
左结合性 |
5 | 加法类运算符 | + 、 - |
左结合性 |
6 | 移位 | >> 、 << |
左结合性 |
7 | 数学关系符(除判等) | > 、 < 、 >= 、 <= |
左结合性 |
8 | 判等关系符 | == 、 != |
左结合性 |
9 | 按位与运算 | & |
左结合性 |
10 | 按位异或 | ^ |
左结合性 |
11 | 按位或 | | |
左结合性 |
12 | 逻辑与 | && |
左结合性 |
13 | 逻辑或 | || |
左结合性 |
14 | 三目条件运算符 | ? : |
右结合性 |
15 | 赋值运算符 | = 、 *= 、 /= ... |
右结合性 |
16 | 逗号 | , |
左结合性 |
Dataview (inline field '='): Error: -- PARSING FAILED -------------------------------------------------- > 1 | = | ^ Expected one of the following: '(', 'null', boolean, date, duration, file link, list ('[1, 2, 3]'), negated field, number, object ('{ a: 1, b: 2 }'), string, variable
此外,CPP Reference也给出了C语言运算符优先级表,算是比较权威的资料,与上述资料基本印证(区别在于上述资料把部分同级但结合性不同的运算符拆分为两个优先级了)
同样的,CPP Reference也给出了求值顺序中的部分未定义行为
例如经典的未定义行为:
int i = 3;
int k = (++i)+(++i)+(++i); //15? 16? UB?
在 gcc 9.2.0
下结果是 16
而在 clang++ 17.0.6
下是 15
在上述规则表中,+
优先级低于 ++i
,但依旧无法处理。
对于普通表达式,可以按照上述方式进行运算:
int a = 7 + 8 * 5 - 6;
式中有运算:
int a = 7 + 8 * 5 - 6;
= 7 + 40 - 6;
随后看同级运算中的结合顺序,加减法的结合顺序为从左至右,即:
int a = 7 + 8 * 5 - 6;
= 7 + 40 - 6;
尽管是未定义行为,但是应试不得不计算时可按照如下方式尝试计算:
int i = 3;
int k = (i++)+(i++)+(i++);
= (3)+(4)+(5);
= 12;
++i
或 --i
,表示立即自增或自减,再参与运算i++
或 i--
,表示先用 i
值运算,稍后再自增自减。但是 稍后
所推迟的时间在C标准中并未规定。int a = 5;
func(a, (t=3, t+2), c); //等价于 fuc(5, 5, c);
if(a, a+1 > 5)
{
printf("value of `a, a+1` is 6\r\n");
}
C标准笔记见:
C标准学习笔记 > 6 5 17 逗号运算符
三目运算符即:
<result> = <expression 1> ? <expression 2> : <expression 3>
等价于:
if(<expression 1>)
<result> = <expression 2>;
else
<result> = <expression 3>;
即"向零截断"原则,"truncation toward zero"。
例如:
#include <stdio.h>
int main() {
printf("%d\r\n", 5/3); // 5/3=1.6667 => 1
printf("%d\r\n", -5/3); // -5/3=-1.667 => -1
printf("%d\r\n", -1/3); // -1/3=-0.333 => 0
}
#include <stdio.h>
int main() {
printf("%d\r\n", 8%3); //2;被除数为正,绝对值取余为2
printf("%d\r\n", 8%-3); //2;被除数为正,绝对值取余为2
printf("%d\r\n", -8%3); //-2;被除数为负,绝对值取余为2
printf("%d\r\n", -8%-3);//-2;被除数为负,绝对值取余为2
}
\
字符后可以接控制字符,使得控制字符被编译器识别为普通字符,如:
\\
识别为 \
\&
识别为 &
\a
\b
\f
\n
\r
\t
\v
\101
即字符 A
。八进制转义前不需要加额外的 0
。\x42
即字符 B
。十六进制转义前需要加额外的 x
。%
符,若需表示 %
,需要使用 %%
。例如:
#include <string.h>
#include <stdio.h>
int main()
{
char str[] = "\101\x42\x43"; // "ABC"
printf("strlen: %d\r\n", strlen(str)); // 3
printf("str: %s\r\n", str); // "ABC"
return 0;
}
C标准见:
C标准学习笔记 > 6 4 4 4 字符常量
主要在于整数的 %xd
和浮点数的 %x.xf
。
对于整数:
#include <stdio.h>
int main() {
int a = 123, b = 123456;
printf("%5d#\n", a); // 默认右对齐,且最少取5位整数,多余5位全取,不足5位使用空格左补全
printf("%05d#\n", a); // 使用0代替空格,在左边补齐位数
printf("%-5d#\n", a); // 左对齐,不足位数,使用空格补全
printf("%-05d#\n", a);// 左对齐,不足位数,还是用空格补全('0'在此处不生效)
printf("%5d#\n", b); // 超过5位全取
}
输出结果:
123#为 123 的%5d输出结果,补齐5位、右对齐(默认)、空格补全(默认)
00123#为 123 的%05d输出结果,补齐5位、右对齐(默认)、'0'补全
123 #为 123 的%-5d输出结果,补齐5位、左对齐、空格补全(默认)
123 #为 123 的%-05d输出结果,补齐5位、左对齐,依旧是空格补全('0'在此处不生效)
123456#当数据大于5位时全取。
对于浮点数:
#include <stdio.h>
int main() {
double a = 123.326, b = 90.12;
printf("%.2f\n", a); // 保留2位小数,第三位四舍五入(原则上如此,见注4)
printf("%.3f\n", b); // 保留3位小数,不足的用0补全
printf("%3.2f\n", b); // 输出至少3位字符和2位小数,总输出位数可以大于3,小数点占1位
printf("%7.2f\n", b); // 输出至少7位字符和2位小数,总输出位数可以大于7,右对齐
printf("%-7.2f\n", b); // 输出至少7位字符和2位小数,总输出位数可以大于7,左对齐
}
输出:
123.33#123.326 的 "%.2f" 输出,保留2位小数
90.120#90.12 的 "%.3f" 输出,保留3位小数,不足的用0补全
90.12# 90.12 的 "%3.2f" 输出,输出至少3位字符和2位小数,总输出位数可以大于3,小数点占1位
90.12#90.12 的 "%7.2f" 输出
90.12 #90.12 的 "%-7.2f" 输出,左对齐
注意:
%${控制位}${最小字符长度}.${小数位数}f
%${控制位}${补全位}${字符最短长度}d
IEEE 754
表示浮点数存在精度误差,导致输出的结果不一定为四舍五入,例如给定 100.565
,其单精度和双精度的实际值为:
100.56500244140625
100.56499999999999772626324556767940521240234375
printf
的保留两位结果分别为:float fvalue = 100.565; // 精确值为100.56500244140625
double dvalue = 100.565; // 精确值为100.56499999999999772...
printf("%.2f", fvalue); // 输出结果为100.57
printf("%.2lf", dvalue); // 输出结果为100.56
scanf
的使用方法和 printf
的 format
不完全相同,#include<stdio.h>
int main()
{
char str[100] = { 0x00 };
// scanf("%[^!]", str);//以!结束输入 注:记得清理缓冲区中剩余字符
// scanf("%[^\n]",str);//以回车结束输入 注:。。。
//
// scanf("%[123]",str); //只能输入123,遇到其他字符后停止匹配
// scanf("%[^123]",str); //只能输入非123,遇到其123后停止匹配
//
// scanf("%[a-z,A-Z]", str); //只能输入英文字符,遇到其他字符后停止匹配
// scanf("%[^a-z,A-Z]",str); //只能输入非英文字符,遇到其他字符后停止匹配
//
// scanf("%*c",str);//清理缓冲区中第一个字符,比如:上次遗留下的\n
// scanf("%*[^!] %*c",str); //跳过一行
printf("%s", str);
return 0;
}
printf
等价于第一个参数为 stdout
的 fprintf
函数,故其传参规则也等同于 fprintf
函数。fprintf
函数要先打印左侧的参数,因此左边的参数只能后入,即各参数从右向左依次入栈。printf
函数参数的计算顺序,但当不得不做题时应当从右向左计算。例如下列代码:int main()
{
int a = 32;
printf("%d %d %d", a = a >> 2, a = a >> 2, a);
return 0;
}
gcc 9.2.0
上输出结果为 2 2 2
2 8 32
strcat
函数的参数的声明为:char *strcat(char * restrict s1, const char * restrict s2);
s2
所指字符串拼接到 s1
的后部。s1
和 s1
所指字符串重合时,该行为未定义。strcat
函数不做边界检查,也不知道 s1
的合法内存空间范围,因此需要用户自行做边界检查防止溢出或越界。break、continue、goto的区别
a-z
或字母 A-Z
或下划线 _
开头,后面可以接任意个字母、数字、下划线。不可以数字开头。C11标注参考:
C标准学习笔记 > 6 4 2 标识符 Identifiers
sizeof
为该数组的实际内存大小,即使是不定长数组(VLA)也是如此:(因此 sizeof
不一定是预处理时求值(但是开优化后一般直接生成表达式))#include <stdlib.h>
size_t vla_test(int size)
{
int array[size];
return sizeof(array);
}
int main()
{
printf("%lld", vla_test(89)); //356,4Byte per `int`
return 0;
}
sizeof
运算结果仅为指针大小。#include <stdio.h>
void array_test(int array[])
{
printf("sizeof array: %lld\r\n", sizeof(array)); //8
}
int main()
{
int array[16] = { 0 };
array_test(array);
return 0;
}
#include <stdio.h>
int array[16] = { 0 };
void array_test()
{
printf("sizeof array: %lld\r\n", sizeof(array)); //64
}
int main()
{
array_test();
return 0;
}
因此在处理数组的函数传参时,一定要传两个参数,一个是数组(或指针),一个是大小。(可以考虑使用宏函数自动处理 sizeof
)
const
优先修饰其左边的描述符,如果无法修饰左边描述符时则去修饰右边描述符。而从效果而言可以分为修饰指针和修饰指向的内存两种。因此有以下几种情况:
const int* p |
int const * p |
int * const p |
|
---|---|---|---|
含义 | const 修饰 int ,即p指针指向的内容不可改变。 |
const 修饰 int ,同左 |
const 修饰 int * ,即p指针所指向的地址不可改变。 |
不可进行的操作 | *p = 100; |
*p = 100; |
p ++; |
数组在实际传参时是按照指针传参的,并不关心数组是否匹配,如下:
#include <stdio.h>
void array_test(int array[32])
{
printf("value of array: %lld\r\n", array); //140722025083408(Address of `array`)
printf("sizeof array: %lld\r\n", sizeof(array)); //sizeof(int*)
}
int main()
{
int array[16] = { 0 };
array_test(array); //哪怕int [16]也可以传进int [32]中
return 0;
}
内存分配 | 定义时即分配 | 需要手动分配 |
内存回收 | 自动回收 | 需要手动回收 |
内存置0 | 定义时可直接置0,例如:int array[32] = { 0 }; 但是不可用于置其他数值,见: CPP/应试笔记与八股 > 1 15 讲一讲一维数组的初始化 |
申请时使用 calloc 、或申请后使用 memset |
sizeof 运算 |
不同函数内定义的数组(经过传参的)的sizeof运算结果为指针, 直接访问的是实际内存大小,见: CPP/应试笔记与八股 > 1 3 对数组使用sizeof会得到什么结果 |
一定是指针大小 |
a 是否可变 |
不可变,为常量 | 可变,为变量,仅指向内存地址 |
指向内容 | 必定为数组对应的数据类型 | 可以指向任意类型的内存区域 |
函数传参 | 数组在传递时只传递了指针,和指针传参无本质区别。见注1。 | |
隐式变换 | 允许 int *p = a; //a为int a[size] |
但是不允许 int a[] = p; //p为int *p |
访问方式 | E1[E2] 的本质为 (*((E1) + (E2))) ,允许 *(a + offset) 访问。 |
允许指针 a[offset] 访问 |
注:
int (*p)[3]
, p
为一个指向有3个 int
型变量的数组的指针int *p[3]
, p
是一个有3个 int*
变量的数组数组越界是指访问数组时,访问到了该数组以外的内存空间,因此可以分为如下几类:
对于返回值为 return_type
,参数列表为 args
的函数,其指针类型为:
return_type (*p)(args);
例如对于:
int main(int argc, char argv[]);
则其函数指针为:
int *p(int argc, char argv[]);
int *p[3]; // p为成员类型int*的数组
int (*p)[3]; // p为指向int[3]的指针,若sizeof(int) = 4;则 p+1 向后移动12位
int a[] = { 1, 2, 3, 4, 5 };
int a[32] = { 0 }; //a中所有元素均为0
int b[32] = { 1 }; //b中第一个元素为1,后续均为0
int a[32] = { 0, 1, [10] = 10, 11 }; //a[1] = 1, a[10] = 10, a[11] = 11, 其他为0
char p[] = "abc"; // char p[] = { 'a', 'b', 'c', '\0' }
char q[3] = "abc"; // char q[3] = { 'a', 'b', 'c' }
char str[] = { "abc" }; // It's OK, str = "abc"
char s[] = { "abc" "def" }; // It's OK, s = "abcdef"
gcc
抛警告, msvc
报错)int a[2] = { 1, 2, 3 };
假设有一个数组 int a[1][2]
,则 int a[1]
的类型是 int[2]
,即含有两个元素的数组。
则对于n维数组 int a[1][2]...[n]
,则 int a[1][2]...[n-1]
的类型为 int[n]
。
对于二维数组:
int array[4][3];
则其定义的是 4行 、 3列 的数组,如下方表格所示:
array[0][0] | array[0][1] | array[0][2] |
---|---|---|
array[1][0] | array[1][1] | array[1][2] |
array[2][0] | array[2][1] | array[2][2] |
array[3][0] | array[3][1] | array[3][2] |
则其初始化方式有:
int array[4][3] = {
{ 1, 3, 5 }, // 必须要带 `{ }` 进行分行嵌套
{ 2, 4, 6 },
{ 3, 5, 7 },
};
//等价于
int array[4][3] = { { 1, 3, 5 }, { 2, 4, 6 }, { 3, 5, 7 } };
1 | 3 | 5 |
---|---|---|
2 | 4 | 6 |
3 | 5 | 7 |
0 | 0 | 0 |
int array[4][3] = {
1, 3, 5, 2, 4, 6, 3, 5, 7
};
array[2][2]
置 1
, array[3][0]
置 2
,其他位置置 0
,则可以有代码:int array[4][3] = {
[2][2] = 1, 2 //从中部指定位置后,其后续值从指定元素后开始排列。
};
int array[][3] = { //则此数组为array[1][3],且仅array[0][1]不为0
{ 0, 1 }
};
int array[][3] = { 0, 1 }; //则此数组为array[1][3],且仅array[0][1]不为0
int array[][3] = { //则此数组为array[2][3],且仅array[0][1]不为0
0, 1, 0, 0
};
注意:
int array[4][3] = { //4行3列数组
1, 2,
4, 5, 6,
7, 8
};
// 等价于
/**
* | 1 | 2 | 4 |
* | 5 | 6 | 7 |
* | 8 | 0 | 0 |
* | 0 | 0 | 0 |
*/
int array[4][3] = { //4行3列数组
1, 2, 4, 5, 6, 7, 8
};
int a[2][] = { // error: array type has incomplete element type ‘int[]’
{1, 2, 3},
{4, 5, 6}
}
数值常量:
0
开头,例如 077
、八进制,值为十进制的 63
。可以为负。0x
开头,例如 0xFee
或 0xfeel
,l
为 long
后缀3.1415926
${整数部分k}E${10的幂n}
或 ${整数部分k}e${10的幂n}
,即:float pi = 31415926e-7; //3.1415926
double Na = 6.02214076e23; //6.02214076x10^23
类型修饰:
u
或 U
,表示 unsigned
l
或 L
,表示 long
f
表示浮点型。不要忘了字符串常量也是常量!!!如 "abc"
也是常量,做题时要注意。
典型示例:
123e+2.3 |
错误,指数部分不可为小数 |
e-310 |
错误,没有整数部分 |
-0. |
正确,表示一个值非常接近于零(即负零,negative zero),实际上是负数的零 |
由于在项目中,各个程序员仅负责整个项目一小部分的工作。因此难免出现 局部变量
和 全局变量
重名的问题。因此当遇到变量重名时,作用域小的优先级高。当作用域相同时,则不允许变量重名(如局部变量和函数内静态变量)。
全局变量&局部变量:
#include <stdio.h>
int a = 1;
int main(){
int a = 2;
printf("%d", a); // a = 2;
return 0;
}
全局变量&函数内静态变量:
#include <stdio.h>
int a = 1;
int main(){
static int a = 3;
printf("%d", a); // a = 3;
return 0;
}
静态变量&块(block)内变量:
#include <stdio.h>
int main(){
int a = 1;
{
int a = 2;
printf("%d", a); // a = 2;
}
return 0;
}
不允许的情况:
#include <stdio.h>
int main(){
int a = 2;
static int a = 3;
printf("%d", a);
return 0;
}
主要注意几个类型概念:
不包括:
无聊的概念。
int add(int a, int b)
{
// 在函数体内,a和b就是形参
return a + b;
}
int main()
{
int var1 = 1;
int var2 = 2;
// 则下列函数调用中,var1和var2就是实参,值分别为1和2。在函数传递时,var1会被复制给a,var2会被复制给b
return add(var1, var2);
}
考试可能会问形参和实参的传递方式:
C语言在 <string.h>
中主要实现了如下函数:
memcpy
内存复制函数考察企业:
函数原型:
#include <string.h>
void *memcpy(void * restrict s1,
const void * restrict s2,
size_t n);
描述:
memcpy
函数将 s2
指向的内存的 n
个字符复制到 s1
指向的内存中。如果复制发生在重叠的对象之间,则行为是未定义的。
返回值:
memcpy
函数将返回 s1
指针。
注意:
memmove
内存安全拷贝函数函数:
#include <string.h>
void *memmove(void *s1, const void *s2, size_t n);
描述:
memmove
函数将 s2
指向的内存的 n
个字符复制到 s1
指向的内存中。复制的过程就像是将 s2
指向的对象中的 n
个字符首先复制到一个 n
个字符的临时数组中,这个临时数组不会与 s1
和 s2
指向的内存重叠,然后将临时数组中的 n
个字符复制到 s1
指向的内存中。
返回值:
memmove
函数将返回 s1
指针。
注意:
strcpy
字符串拷贝函数概要:
#include <string.h>
char *strcpy(char * restrict s1,
const char * restrict s2);
描述:
strcpy
函数将 s2
指向的字符串(包括终止空字符)复制到 s1
指向的数组中。如果复制发生在重叠的对象之间,则行为是未定义的。
返回值:
strcpy
函数将返回 s1
指针。
注:
strcpy
函数操作的字符串要以 \0
结尾。strncpy
函数概要:
#include <string.h>
char *strncpy(char * restrict s1,
const char * restrict s2,
size_t n);
描述:
strncpy
函数将不超过 n
个字符(空字符后面的字符不复制)从 s2
指向的数组复制到 s1
指向的数组。如果复制发生在重叠的对象之间,则行为是未定义的。s2
指向的数组是一个比 n
个字符短的字符串,则空字符将附加到 s1
指向的数组中的副本,直到所有字符中的 n
个字符都写入(即后尾全为 \0
)。返回值:
strncpy
函数将返回 s1
指针。
strcat
函数概要:
#include <string.h>
char *strcat(char * restrict s1,
const char * restrict s2);
描述:
strcat
函数将 s2
指向的字符串的副本(包括终止空字符)附加到 s1
指向的字符串的末尾。 s2
的初始字符覆盖 s1
末尾的 null
字符。如果复制发生在重叠的对象之间,则行为是未定义的。返回值:
strcat
函数将返回 s1
指针。
C标准笔记参见:
C标准学习笔记 > 7 24 字符串库 string h
考查企业:
static
关键字的作用)static
在C语言中是用于修饰存储类型的关键字,
static
可以修饰变量的可见性。使用了 static
修饰了的变量或函数仅能在其定义域内访问,可以避免大型项目中的命名冲突问题。static
修饰的变量会存储到数据段,若指定了初值则会被存储到数据段中的已初始化数据段(.DATA);若未指定初值则会被存储到数据段中的未初始化数据段(.BSS),此时其二进制的值一定为0。static
变量在整个程序内存中只会存放一份且不会被销毁,并且无论是第几次调用,其值均会保持为上一次的值。static
修饰的变量也会遵守变量名优先级原则,当变量名重复时,作用域越小的变量会被优先使用。大端(MSB,Most Significant Bit)和小端(LSB,Least Significant Bit)是单个变量超过1Byte的变量在计算机内存中的排列顺序,但是不影响数组间元素的排列顺序。
以 int32_t a = 32;
为例,其二进制编码如下:
0000 0000
0000 0000
0000 0000
0100 0000
或十六进制:
0x00
0x00
0x00
0x20
则按照如下二进制排列的为小端(LSB):
0100 0000
0000 0000
0000 0000
0000 0000
即十六进制的:
0x20
0x00
0x00
0x00
按照如下二进制排列的为大端(MSB):
0000 0000
0000 0000
0000 0000
0100 0000
即十六进制:
0x00
0x00
0x00
0x20
但是对于数组间排列,例如:
char str = "abcd";
int16_t array[] = { 1, 2, 3, 4 };
则对应的大端存储为:
char str = "abcd"; // str => { 'a' } { 'b' } { 'c' } { 'd' } { '\0' }
int16_t array[] = { 1, 2, 3, 4 }; // array => { 0x00 0x01 } { 0x00 0x02 } ...
对应的小端为:
char str = "abcd"; // str => { 'a' } { 'b' } { 'c' } { 'd' } { '\0' }
int16_t array[] = { 1, 2, 3, 4 }; // array => { 0x01 0x00 } { 0x02 0x00 } ...
其中,常见的大小端平台有:
异或是位运算的一种,运算规则是 同0异1
,计做 XOR
或 ^
。
在C语言中直接使用 ^
运算符即可进行异或运算。
int a = 7; //二进制: 0111
int b = 3; //二进制: 0011
int c = a ^ b; //运算结果:0100 = 4
异或的性质:
int a = 7;
a ^= 3; //a = 4
a ^= 3; //a = 7
a ^= 3; //a = 4
a ^= 3; //a = 7
...
异或的应用主要有:
>>
and <<
)顾名思义,C语言的移位操作就是对C语言对应的二进制位进行移位操作。
注意点:
E1 << E2
的结果的值为:E1 >> E2
的结果的值为:E1 << E2
,如果 E1
是有符号的非负值,并且E1 >> E2
,如果 E1
是一个有符号类型的负值,则结果是由实现来定义的。C标准笔记见:
C标准学习笔记 > 6 5 5 位移运算符
考试背这个:
char
、short
)参与运算时,会被自动提升为 int
或 unsigned int
,尽可能避免表达式中精度丢失问题。理解用这个:
C语言在运算时要将各运算参数转换为同一种运算参数后才可以进行运算,该过程需要进行隐式转换,其转换规则如下:
- 当运算的操作数类型不同时,会进行隐式转换
- 隐式转换会向精度增加、长度增加的方向转换
- 有符号、无符号间的类型转换通常不会修改内存的值,因此可能会导致错误(具体原理为负数补码原理),如下:
int32_t a = -1;
uint32_t b = a; // 此时b的二进制为0xffffffff
printf("%d", b); // printf中%d以有符号十进制整数输出,仍然为-1
printf("%u", b); // %u以无符号十进制整数输出,结果为4294967295
float
转int
时默认规则为截断取整(CPP/应试笔记与八股 > 2 11 整数除法是什么计算原则?也是此原则),此外,C语言还提供了四舍五入(round
)、向上取整(ceil
)、向下取整(floor
)的方法。此外,四舍五入可以使用一些技巧手动完成(可以用此原理做五舍六入等)。
float floatValue = 3.14;
int intValue_truncate = (int)floatValue; // 截断取整 => 3
int intValue_round = (int)(floatValue + 0.5); // 手动四舍五入,可以用此原理做五舍六入等
int intValue_round = (int)round(floatValue); // 库函数四舍五入,部分编译器不支持
int intValue_floor = (int)floor(floatValue); // 向下取整
int intValue_ceil = (int)ceil(floatValue); // 向上取整
考察企业:
一个C语言的基本内存布局如下图所示:
其中:
malloc
等函数以及CPP的 new
等操作符分配的内存空间而下表为各种类型的变量/常量/立即数所存放的地方:
数据类型 | 所在内存中的存储区域 | |
---|---|---|
立即数 | 代码段(TEXT/CODE) | 并非所有体系架构的代码段都会读入内存。 |
常量(通常为 const 修饰) |
只读数据段(RODATA) | 部分嵌入式系统的 .rodata 不会存入内存运行时直接读取ROM |
const char* s = "A" |
只读数据段 | |
static int a = 3; 中的 3 |
已初始化数据段(DATA) | |
static int a; 中的 a |
未初始化数据段(BSS) | |
初始化的全局变量 | 已初始化数据段 | |
未初始化的全局变量 | 未初始化数据段 | |
局部变量 | 栈 | |
new 、 malloc 、 realloc 等 |
堆 |
注:
注意与可执行文件的布局进行区分。
字节对齐主要是为了方便CPU访问数据更加方便,例如32位机一次读取4个字节、而64位机一次读取8个字节。若不考虑内存对齐问题,在32位机下则会出现访问一个小于4字节的元素需要读取两次4字节的内存进行拼凑再移位的情况,降低了CPU效率。
结构体内存对齐的基本规则:
具体对齐流程:
编程补充:
offsetof()
函数。#pragma pack (value)
来控制该文件下所有结构体的内存对齐单位(而非内存格)。#pragma pack (1) // 以8位为对齐单位
typedef struct {
uint16_t a;
uint8_t b;
uint16_t c;
} type_a; // sizeof(type_a) = 2+1+1 = 5Byte
gcc
编译器,可以使用 __attribute__((packed))
来禁用一个结构体的内存对齐。typedef struct {
uint16_t a;
uint8_t b;
uint16_t c;
} __attribute__((packed)) type_a; // sizeof(type_a) = 2+1+1 = 5Byte
gcc
编译器,可以使用 __attribute__((aligned(n)))
来扩大一个结构体的内存对齐单位。具体可见实验1实验1, __attribute__((aligned(n)))
只可用于扩大单个结构体的内存对齐单位,无法缩小:
#pragma pack (1) // 以8位为对齐单位
typedef struct {
uint16_t a;
uint8_t b;
uint16_t c;
} __attribute__((aligned(2))) type_a; // sizeof(type_a) = 6Byte
n
和 基本规则2 中的较大值作为默认对齐数#pragma pack (4) // 以32位为对齐单位
typedef struct {
uint16_t a;
uint8_t b;
uint16_t c;
} __attribute__((aligned(1))) type_a; // sizeof(type_a) = 6Byte
( __attribute__((aligned(n)))
的规则可见C语言标准)
联合体内存对齐的基本规则:
实验2,
union bin
{
char a[99];
int b[3];
double c;
}; //sizeof(union bin) = 104,为8的整数倍
在C语言中并没有提供 byte
类型,因此C语言使用 unsigned char
来表示 byte
。选择采用 unsigned char
而不采用 signed char
的考虑主要有以下几点:
byte
被 signed char
解释为负数并不影响 byte
原本的二进制值,但是 byte
本身也没有负数特性,没有使用符号的必要。signed char
解释值为 0xff
的 byte
时,其 signed char
对应的值为 -1
。而当需要将 signed char
提升为 int32_t
类型时,对应的 int32_t
的值也应当为 -1
。但是此时的二进制从 0xff
提升为了 0xffffffff
,擅自多了24位 1
。signed char
和 char
问题,C语言并未规定 char
一定是 signed char
,也可以是 unsigned char
,具体使用哪个取决于编译器实现。规范见C标准学习笔记 > 6 3 1 1 布尔、字符和整数。而在C语言所提供的 string.h
库中,许多字符串操作(例如 strchr
等 str*
函数)和byte二进制操作(例如 memchr
等 mem*
函数)通常使用 int
来接收参数,是因为:
int
的整数部分可以接收 unsigned char
所表示的 byte
而不触发负数补码机制,再次转换为 unsigned char
不会改变二进制值。char
的操作(如 fgetc
)需要返回 EOF
来表示错误或其他情况,而 EOF
为 int
类型的 -1
(至少为16位的 1
),不会影响 char
类型的 -1
( 0xff
,扩展ASCII表中为字符 ÿ
)的表示。printf("%x")
正常打印,而signed不行。*.o
、 *.lib
等)的时间戳做对比,检测文件是否被修改,随后再进行增量编译。考察企业:
C语言关键字被定义在C11附录的A.1.2,主要有如下几类关键字:
描述变量类型或者类型相关的关键字:
程序流程控制关键字:
const
extern
float
inline
register
restrict
sizeof
static
volatile
_Alignas
_Alignof
_Atomic
_Bool
_Complex
_Generic
_Imaginary
_Noreturn
_Static_assert
_Thread_local
考察企业:
Note:持续更新
FUNC(i++)
中的 i++
就是一个有副作用的表达式。因为该表达式可能会在宏函数中多次被求值,例如 #define SQUARE(x) ((x) * (x))
。*alloc
所分配空间的函数。*alloc
系列函数的所有返回值必须可以被 free
函数接收,而 *alloc
有可能返回空指针,因此 free
函数可以接收 NULL
。考察企业:
extern
等效,即加和不加 extern
是一致的extern
来方便开发者阅读。init_wait_entry
被 extern
在了 include/linux/wait.h
中,但是其定义在 kernel/sched/wait.c
中。考察企业:
virtual ReturnType func() { //Do sth.. }
即可。virtual ReturnType func()=0;
即可。考察企业:
考查企业:
考查企业:
考查企业:
考查企业:
考察企业:
考察企业:
考查企业:
考查企业:
考察企业:
考察企业:
考查企业:
考查企业:
考查企业:
考察企业:
考察企业:
考察企业:
考察企业:
考察企业:
考察企业:
考察企业:
考察企业:
考查企业:
考察企业:
考查企业:
考察企业:
考察企业: