应试笔记与八股

#C-Language #应试笔记与八股

1 目录

2 八股简答题和思考题

2.1 C语言的优缺点

C语言是一个底层的、面向过程的语言。
其优点主要有:

  1. 高效性:C语言提供了对底层硬件的直接访问,允许编写高效的代码,并且通常生成高效的机器代码。
  2. 跨平台特性:C语言拥有良好的可移植性,支持几乎所有常用的CPU平台,通常进行简单移植后即可重新编译为二进制机器码。
  3. 支持操作底层:C语言可以通过定义和绑定寄存器地址、使用位操作、内嵌汇编代码等方式使用户可以高效的操作底层。
  4. 灵活性:C语言是图灵完备的,意味着该语言能够实现任何算法。
  5. 较为强大的库函数:C语言提供了功能相对丰富的标准库,包含字符串操作、文件操作、基础数学运算等。
  6. 适合底层开发:C语言性能高,支持底层操作,因此现代操作系统内核的实现都离不开C语言,例如Linux内核几乎全部由C语言完成。在嵌入式开发领域,几乎所有工作均由C语言完成。

其缺点主要有:

  1. 需要用户手动管理内存:C语言允许用户手动分配和释放内存,提高了程序运行效率,但也增加了程序开发复杂度,增加了内存泄漏带来的危险。
  2. 缺乏抽象:相对于一些高级语言,C语言缺乏一些高级抽象特性,如面向对象编程、异常处理等,这使得开发大型工程变得复杂和困难。
  3. 容易隐藏错误:例如 while(condition){ ... } 语句很容易因为多打一个分号从而导致无限循环 while(condition);{ ... }
  4. 不适合高级应用开发:C语言提供的函数库较为低级,且缺乏高级功能和抽象的应用,如网页开发、数据科学等领域,不适合对性能绝对敏感的高级开发场景。
    (观点主要出自《C语言程序设计现代方法》)

2.2 C语言执行效率高,简述一下C语言采用了哪些措施提高执行效率

  1. 允许程序直接访问内存,免去了各种抽象接口所造成的的额外开销。
  2. 使用指针特性,允许直接操作内存地址,可以直接访问内存中的数据,而无需进行额外的拷贝操作。
  3. 使用内联函数,允许编译器将部分函数代码直接插入到目标函数中,减少了函数调用的开销。
  4. 允许编译器对代码进行优化,使用重排、循环展开、函数内联等方式提高了生成的机器代码的执行效率。
  5. 提供低级语言特性,C语言提供了例如位操作、嵌入汇编等方式直接操作硬件,减少了代码的层次,提高了效率。
  6. 精简了标准库,减少了生成的机器代码的大小,提高了执行效率。
  7. 提供了宏功能(Macro),允许用户使用宏函数等方法减少了函数调用,将部分运算(例如sizeof)直接在预处理期完成。

2.3 [TODO]声明和定义的区别

2.4 讲一讲C语言编译的过程

C语言编译通常有以下过程:

  1. 预处理:最开始C语言源文件会被交给预处理器进行文本替换,将包括 #include#define 等宏操作或条件编译预处理为普通的代码文本,可以给程序增加或减少内容。
  2. 编译:编译器会将预处理处理后的源文件逐一翻译为对应的汇编代码。
  3. 汇编:汇编器将编译器生成的汇编代码翻译成机器指令,生成目标平台的二进制文件。通常的C语言编译器会将2、3两步合成为一步,即编译
  4. 链接:链接器把由编译器产生的目标代码和其他附加代码整合在一起,链接成一个可执行程序。

2.5 [TODO]讲一讲C语言编译和链接

Windows平台下:

Linux平台下:

以及静态链接、动态链接的优缺点(操作系统知识)

2.6 讲一讲ASCII

  1. ASCII表可以分为基本ASCII表和扩展ASCII表。
  2. 基本ASCII表中共使用了128个编码,均位于低6位上,其中主要编码如下:
    1. ASCII[0]ASCII[31] 是控制字符,重要控制字符如下:
      1. ASCII[10]\n ,换行符
      2. ASCII[13]\r ,回车符
    2. ASCII[48]ASCII[57] 是数字 0-9'0'在前面
    3. ASCII[65]ASCII[90] 是大写字符 A-Z大写排在小写前面
    4. ASCII[97]ASCII[122] 是小写字符 a-z大写排在小写前面
    5. 其他位置为符号位
  3. 扩展ASCII表附加了128个特殊符号字符、外来语字母和图形符号。

2.7 讲一讲负整数在计算机中是如何存储的

  1. 负整数在计算机中的存储使用补码存储,目的是为了方便计算机对正负数之间的运算,见例1.1。
  2. 可以简单理解为 "找一个无符号正数,使得这个无符号整数的运算性质和这个负数一样",见例1.2。
  3. 因此有负0这个特殊的数。

补码计算规则:

  1. 最高位为符号位,正数为 0 ,负数为 1不参与下述运算
  2. 将负数的绝对值用二进制表示,以 int32_t a = -13 为例,为
int32_t a = -13; // (1). 13的二进制为: 1000 0000 0000 1101
  1. 除符号位的其他位取反,及:
int32_t a = -13; // (2). 除符号位取反: 1111 1111 1111 0010
  1. 对该二进制进行二级制加 1除符号位不动,其他位正常溢出:
int32_t a = -13; // (3). 符号位不动,二进制加一: 1111 1111 1111 0011

例1:

  1. 如果直接在最高位加一个符号位 1 直接存储负数,并按照二进制加法处理 -12 ,则会有:
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. 如果使用上述补码原则,并按照二进制加法处理 -12 ,则会有:
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

2.8 讲一讲运算符的运算顺序

几个比较重要(常考)的运算优先级

  1. . 运算符取成员的优先级大于 * 解引用的优先级,例如:
    • *p.member 错误
    • (*p).member 正确
  2. int (*p)[3]p 为一个指向有3个 int 型变量的数组的指针
  3. int *p[3]p 是一个有3个 int* 变量的数组
  4. a = *p++ ,先对 p 解引用,随后 p 自增
  5. a = *(p++) ,同上
  6. 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 逗号 , 左结合性

此外,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;

式中有运算:

  • 二元加减法,优先级4
  • 二元乘除法,优先级3
    则应当先计算乘法,有:
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;

2.9 讲自自增自减运算符

  1. 该这两种运算符主要有两种使用方式:
    1. ++i--i ,表示立即自增或自减,再参与运算
    2. i++i-- ,表示先用 i 值运算,稍后再自增自减。但是 稍后 所推迟的时间在C标准中并未规定

2.10 讲一讲逗号运算符

  1. 逗号表达式即顺序求值表达式,可以在带括号的表达式或在条件运算符的第二表达式中使用,其左边的操作数会被当成无返回值的语句执行,最右边的表达式当做该逗号运算表达式的值。
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");
}
  1. 但是逗号运算符不能出现在使用逗号分隔列表中的项的上下文中(例如函数的参数或初始值设定项列表)。如果想用请加括号。
  2. 计算顺序为从左向右依次执行。

2.11 讲一讲三目运算符

三目运算符即:

<result> = <expression 1> ? <expression 2> : <expression 3>

等价于:

if(<expression 1>)
	<result> = <expression 2>;
else
	<result> = <expression 3>;

2.12 整数除法是什么计算原则?

即"向零截断"原则,"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
}

2.13 负数取余规则

  1. 运算结果正负号与被除数符号一致
  2. 负数间取余的结果绝对值等于各数绝对值之间取余
#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
}

2.14 讲一讲转义符

  1. 一般 \ 字符后可以接控制字符,使得控制字符被编译器识别为普通字符,如:
    • \\ 识别为 \
    • \& 识别为 &
  2. 转义符后还可以接普通字符表示控制字符,如:
    • \a
    • \b
    • \f
    • \n
    • \r
    • \t
    • \v
  3. 转义符后还可以接八进制、十六进制序列转义为字符,如:
    • \101 即字符 A八进制转义前不需要加额外的 0
    • \x42 即字符 B十六进制转义前需要加额外的 x
  4. 转义符无法转义输出 % ,若需表示 % ,需要使用 %%

例如:

#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;
}

2.15 数字的格式化输出

主要在于整数的 %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" 输出,左对齐

注意

  1. 浮点数输出格式为: %${控制位}${最小字符长度}.${小数位数}f
  2. 浮点数输出时,小数点占1位
  3. 整数输出格式为: %${控制位}${补全位}${字符最短长度}d
  4. 原则上保留小数位数时按四舍五入原则。但是由于 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

2.16 [TODO]讲一下scanf的使用方法

  1. scanf 的使用方法和 printfformat 不完全相同,
#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;
}

2.17 [TODO]讲一下scanf的工作方法

2.18 讲一下printf是如何传参的

  1. 在原理上,printf 等价于第一个参数为 stdoutfprintf 函数,故其传参规则也等同于 fprintf 函数。
  2. 其中,在C语言中,函数传参时会将参数放入栈中。栈的结构为先入后出,而在 fprintf 函数要先打印左侧的参数,因此左边的参数只能后入,即各参数从右向左依次入栈
  3. 但是C语言并未规定 printf 函数参数的计算顺序,但当不得不做题时应当从右向左计算。例如下列代码:
int main()
{
    int a = 32;
    printf("%d %d %d", a = a >> 2, a = a >> 2, a);
    return 0;
}

2.19 讲一讲strcat

  1. strcat 函数的参数的声明为:
char *strcat(char * restrict s1, const char * restrict s2);
  1. 其作用是将 s2 所指字符串拼接到 s1 的后部。
  2. s1s1 所指字符串重合时,该行为未定义。
  3. 由于 strcat 函数不做边界检查,也不知道 s1 的合法内存空间范围,因此需要用户自行做边界检查防止溢出或越界。

2.20 [TODO]讲一讲Makefile

2.21 [TODO]讲一讲什么是do语句

2.22 [TODO]讲一讲常用的三种退出循环的方式

break、continue、goto的区别

2.23 C语言标识符(Identifiers)规则

  1. C语言标识符必须以字母 a-z 或字母 A-Z 或下划线 _ 开头,后面可以接任意个字母、数字、下划线。不可以数字开头
  2. 标识符区分大小写字母。
  3. C89规定标识符的长度在31字符以内,C99规定在63个字符以内,C11对标识符长度无限制。
  4. C语言中的关键字不可做为标识符(见C11标准附录A.1.2)

2.24 对数组使用sizeof会得到什么结果

  1. 在同一函数内定义的数组的 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;
}
  1. 但是在不同的函数间定义的数组的 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 )

2.25 Const & Pointer

const 优先修饰其左边的描述符,如果无法修饰左边描述符时则去修饰右边描述符。而从效果而言可以分为修饰指针和修饰指向的内存两种。因此有以下几种情况:

区别
const int* p int const * p int * const p
含义 const 修饰 int即p指针指向的内容不可改变 const 修饰 int,同左 const 修饰 int *即p指针所指向的地址不可改变
不可进行的操作 *p = 100; *p = 100; p ++;

2.26 数组是如何传参的

数组在实际传参时是按照指针传参的,并不关心数组是否匹配,如下:

#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;
}

2.27 Array VS Pointer

区别
int a[size]
int *a
内存分配 定义时即分配 需要手动分配
内存回收 自动回收 需要手动回收
内存置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] 访问

注:

  1. 数组出现在函数原型的声明中时,会被替换为指针。见C语言规范C标准学习笔记 > 6 7 6 2 数组声明Pasted image 20240307142115.png
  2. int (*p)[3]p 为一个指向有3个 int 型变量的数组的指针
  3. int *p[3]p 是一个有3个 int* 变量的数组

2.28 数组越界会产生什么后果

数组越界是指访问数组时,访问到了该数组以外的内存空间,因此可以分为如下几类:

  1. 读取、写入到了该程序允许读写的内存空间:一般会导致未定义的后果,例如错误写入到别的变量的内存区域,会导致该变量值异常,并在后续触发对应后果,一般该后果难以预测。
  2. 写入到了该程序不允许写入的内存空间:一般将此类地址空间称为非法内存地址,会触发操作系统的异常中断,可能会导致操作系统弹出错误消息或者直接终止程序的执行。
  3. 在无虚拟内存的操作系统(如嵌入式常用的FreeRTOS)中,可能会操作到其他进程甚至系统进程的内存,会导致其他进程或系统异常并崩溃。
  4. 数组越界或指针经常会导致一些安全漏洞,攻击者可利用该特性绕过安全检查。

2.29 讲一讲函数指针

对于返回值为 return_type ,参数列表为 args 的函数,其指针类型为:

return_type (*p)(args);

例如对于:

int main(int argc, char argv[]);

则其函数指针为:

int *p(int argc, char argv[]);

2.30 数组指针与指针数组

  1. 数组指针:指向指针的数组,例如
int *p[3];  // p为成员类型int*的数组
  1. 指针数组:指向数组的指针,例如
int (*p)[3]; // p为指向int[3]的指针,若sizeof(int) = 4;则 p+1 向后移动12位

2.31 讲一讲一维数组的初始化

  1. 一维数组在指定元素数值后,可以不指定大小
int a[] = { 1, 2, 3, 4, 5 };
  1. 一位数组指定元素值时,若指定的元素数量比数组大小小,则剩下元素均会被初始化为0
int a[32] = { 0 }; //a中所有元素均为0
int b[32] = { 1 }; //b中第一个元素为1,后续均为0
  1. 可以从中部指定元素位置初始化,如下:
int a[32] = { 0, 1, [10] = 10, 11 }; //a[1] = 1, a[10] = 10, a[11] = 11, 其他为0
  1. 使用字符串初始化char数组时,字符串会存储到常量区(代码段),因此不可进行修改,只可进行读取。
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"
  1. 若初始化数组的数量大于数组大小,则该行为我不知道(反正标准里面我没找到,gcc 抛警告, msvc 报错)
int a[2] = { 1, 2, 3 };

2.32 对于一个n维数组a[1][2]...[n],则a[1][2]...[n-1]是什么

假设有一个数组 int a[1][2] ,则 int a[1] 的类型是 int[2] ,即含有两个元素的数组。
则对于n维数组 int a[1][2]...[n] ,则 int a[1][2]...[n-1] 的类型为 int[n]

2.33 多维数组怎么初始化

对于二维数组:

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]

则其初始化方式有:

  1. 完全括号定义初始化(要求嵌套定义,见注1),则会按照初始化参数进行填空,空缺位为0:
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
  1. 顺序初始化,下方的代码有和上方代码一致的效果:
int array[4][3] = {
	1, 3, 5, 2, 4, 6, 3, 5, 7
};
  1. 特定位置初始化,例如想要把 array[2][2]1array[3][0]2 ,其他位置置 0 ,则可以有代码:
int array[4][3] = {
	[2][2] = 1, 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
};

注意:

  1. 不是嵌套定义的完全定义将会被视为一行内的定义
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
};
  1. 省略部分大小的初始化不可省略后面的大小约束
int a[2][] = {  // error: array type has incomplete element type ‘int[]’
    {1, 2, 3},
    {4, 5, 6}
}

2.34 定义常量有哪些合法的方式(十六进制、科学计数法等)

数值常量:

  1. 整数型支持:
    • 八进制:0 开头,例如 077 、八进制,值为十进制的 63可以为负
    • 十进制:略
    • 十六进制:0x 开头,例如 0xFee0xfeelllong 后缀
    • 除此之外不支持其他进制(如二进制)
  2. 浮点型支持:
    • 直接表示法:如 3.1415926
    • 科学计数法:格式为 ${整数部分k}E${10的幂n}${整数部分k}e${10的幂n} ,即:
      例如:
float pi = 31415926e-7;     //3.1415926
double Na = 6.02214076e23;  //6.02214076x10^23

类型修饰:

  1. 整数型修饰:
    • uU ,表示 unsigned
    • lL ,表示 long
  2. 浮点型修饰:
    • f 表示浮点型。

不要忘了字符串常量也是常量!!!"abc" 也是常量,做题时要注意。

典型示例:

常量示例
说明
123e+2.3 错误,指数部分不可为小数
e-310 错误,没有整数部分
-0. 正确,表示一个值非常接近于零(即负零,negative zero),实际上是负数的零

2.35 变量名优先级问题

由于在项目中,各个程序员仅负责整个项目一小部分的工作。因此难免出现 局部变量全局变量 重名的问题。因此当遇到变量重名时,作用域小的优先级高。当作用域相同时,则不允许变量重名(如局部变量和函数内静态变量)。

全局变量&局部变量:

#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;
}

2.36 C语言数据类型分类

Pasted image 20240302173838.png
主要注意几个类型概念:

  • 实型(real type):即浮点型
  • 构造类型:需要构造的数组、结构体、共用体、枚举

不包括

  • 逻辑型
详见下方画布:
C数据类型

2.37 形参和实参的区别

无聊的概念。

  1. 形参即形式参数,是函数定义中的参数变量。
  2. 实参是实际参数,是函数调用中传入参数的实际变量。
    例如:
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);
}

考试可能会问形参和实参的传递方式

  1. 按值传递,在传递时拷贝一份副本给对应函数
  2. 按引用传递(C++),C语言中没有。
  3. 按地址传递,传递时是传递了该参数的地址,是该参数地址的值传递。在C语言中通常通过传递数据的指针来达到直接修改参数减少大变量内存拷贝的消耗
    例如 "形参和实参都是数组元素、实参是数组地址,形参是指针、形参和实参都是数组地址时,传递方式都是什么?"
  4. 形参和实参都是数组元素时,传递方式为按值传递
  5. 实参是数组地址,形参是指针时,传递方式为按指针传递
  6. 形参和实参都是数组地址时,传递方式为按指针传递

2.38 [TODO]print函数和puts函数的区别

2.39 [TODO]讲一讲malloc、realloc、calloc的区别和用法

2.40 讲一讲字符串操作(strcmp、strcat、strcpy等)

C语言在 <string.h> 中主要实现了如下函数:

2.40.1 memcpy 内存复制函数

函数原型:

#include <string.h>  
void *memcpy(void * restrict s1,  
	const void * restrict s2,  
	size_t n);

描述:
memcpy 函数将 s2 指向的内存的 n 个字符复制到 s1 指向的内存中。如果复制发生在重叠的对象之间,则行为是未定义的

返回值:
memcpy 函数将返回 s1 指针。

注意:

  1. 内存块间不可有重叠,否则是未定义行为。
  2. 返回值为目标地址指针。

2.40.2 memmove 内存安全拷贝函数

函数:

#include <string.h>  
void *memmove(void *s1, const void *s2, size_t n);

描述:
memmove 函数将 s2 指向的内存的 n 个字符复制到 s1 指向的内存中。复制的过程就像是将 s2 指向的对象中的 n 个字符首先复制到一个 n 个字符的临时数组中,这个临时数组不会与 s1s2 指向的内存重叠,然后将临时数组中的 n 个字符复制到 s1 指向的内存中。

返回值:
memmove 函数将返回 s1 指针。

注意:

  1. 内存块间可以有内存重叠,但是实现效率略有降低(多了个判断分支)。
  2. 原内存块在不考虑重叠的情况下保持原数据

2.40.3 strcpy 字符串拷贝函数

概要:

#include <string.h>  
char *strcpy(char * restrict s1,  
	const char * restrict s2);

描述:
strcpy 函数将 s2 指向的字符串(包括终止空字符)复制到 s1 指向的数组中。如果复制发生在重叠的对象之间,则行为是未定义的

返回值:
strcpy 函数将返回 s1 指针。

注:

  1. strcpy 函数操作的字符串要以 \0 结尾。

2.40.4 strncpy 函数

概要:

#include <string.h>  
char *strncpy(char * restrict s1, 
	const char * restrict s2, 
	size_t n);

描述:

  1. strncpy 函数将不超过 n 个字符(空字符后面的字符不复制)从 s2 指向的数组复制到 s1 指向的数组。如果复制发生在重叠的对象之间,则行为是未定义的
  2. 如果 s2 指向的数组是一个比 n 个字符短的字符串,则空字符将附加到 s1 指向的数组中的副本,直到所有字符中的 n 个字符都写入(即后尾全为 \0 )。

返回值:
strncpy 函数将返回 s1 指针。

2.40.5 strcat 函数

概要:

#include <string.h>  
char *strcat(char * restrict s1, 
	const char * restrict s2);

描述:

  1. strcat 函数将 s2 指向的字符串的副本(包括终止空字符)附加到 s1 指向的字符串的末尾。 s2 的初始字符覆盖 s1 末尾的 null 字符。如果复制发生在重叠的对象之间,则行为是未定义的

返回值:
strcat 函数将返回 s1 指针。

2.41 [TODO]讲一讲存储类型(auto、static、extern、register)

2.42 讲一讲大小端

大端(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 } ...

其中,常见的大小端平台有:

  • 小端(LSB):
    • x86
    • ARM
    • Tensilica Xtensa
  • 大端(MSB):
    • 8051
    • MIPS
    • 部分RISC-V
    • Power

2.43 讲一讲什么是异或

异或是位运算的一种,运算规则是 同0异1 ,计做 XOR^
在C语言中直接使用 ^ 运算符即可进行异或运算。

int a = 7;     //二进制:  0111
int b = 3;     //二进制:  0011

int c = a ^ b; //运算结果:0100 = 4

异或的性质:

  1. 对同一个数进行偶数次异或会被抵消
int a = 7;
a ^= 3; //a = 4
a ^= 3; //a = 7
a ^= 3; //a = 4
a ^= 3; //a = 7
...

异或的应用主要有:

  1. 利用性质1,找出一个数列中唯一出现次数为计数的值(LeetCode 260)。

2.44 讲一讲C语言移位操作(>> and <<)

顾名思义,C语言的移位操作就是对C语言对应的二进制位进行移位操作。

注意点:

  1. 使用对象仅为整数类类型
  2. 位移运算结果的类型取决于符号左侧的类型。
  3. 如果右侧操作数的值是负值或超过了左侧操作数的宽度,则该行为是未定义行为
  4. 位移之后空位由0填充
  5. 对于无符号整形,E1 << E2 的结果的值为:,而E1 >> E2 的结果的值为:
  6. 对于 E1 << E2 ,如果 E1有符号的非负值,并且在对应的类型中是可表示的,则结果就是该表达式的值。
  7. 对于 E1 >> E2 ,如果 E1 是一个有符号类型的负值,则结果是由实现来定义的
  8. 因此移位运算不受大小端影响

2.45 C语言中隐式转换规则

考试背这个

  1. 整数提升规则:较小的整数(如charshort)参与运算时,会被自动提升为 intunsigned int ,尽可能避免表达式中精度丢失问题。
  2. 算数类型转换规则:在有不同类型的操作数的表达式时,低精度的操作数会被隐式转换为高精度的操作数。
  3. 赋值转换规则:在进行赋值操作时,会隐式的提高或降低变量的类型,可能会丢失精度。
  4. 函数参数转换规则:当调用函数时,如果函数的参数类型与传递给它的实际参数的类型不匹配,则会进行函数参数转换。

理解用这个:

C语言在运算时要将各运算参数转换为同一种运算参数后才可以进行运算,该过程需要进行隐式转换,其转换规则如下:

  1. 当运算的操作数类型不同时,会进行隐式转换
  2. 隐式转换会向精度增加、长度增加的方向转换
  3. 有符号、无符号间的类型转换通常不会修改内存的值,因此可能会导致错误(具体原理为负数补码原理),如下:
int32_t a = -1;
uint32_t b = a; // 此时b的二进制为0xffffffff
printf("%d", b); // printf中%d以有符号十进制整数输出,仍然为-1
printf("%u", b); // %u以无符号十进制整数输出,结果为4294967295
  1. floatint默认规则为截断取整(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);    // 向上取整

2.46 C语言内存布局问题

2.47 内存对齐问题

字节对齐主要是为了方便CPU访问数据更加方便,例如32位机一次读取4个字节、而64位机一次读取8个字节。若不考虑内存对齐问题,在32位机下则会出现访问一个小于4字节的元素需要读取两次4字节的内存进行拼凑再移位的情况,降低了CPU效率。

结构体内存对齐的基本规则

  1. 在C语言中,struct的地址和结构体中第一个元素的地址相同(但C++不一定)
  2. 字节对齐的时候总会以结构体中单块最大的元素作为单位进行字节对齐,可以称该值为内存对齐单位
  3. 同理,对于结构体内定义其他结构体成员的,则该结构体成员的offset为该结构体成员的若干成员中最大成员的整数倍进行存储。

具体对齐流程:

  1. 将内存划分为与CPU单次存取内存的大小相同的内存块(在这里称作"内存格",并非规范说法),该"内存格"在默认情况下仅受CPU位数影响
  2. 找到结构体中单块最大的元素,求出内存对齐单位
  3. 按顺序尽可能密实地堆积内存,但不可将一个小于等于内存块大小的元素放到两个内存块上。如出现该情况则单开一格内存放置。
  4. 将所有元素放置完毕后,以内存对齐单位的整数倍划分该结构体的总内存。

编程补充:

  1. 查询结构体中某一成员的offset可以使用 offsetof() 函数。
  2. 可以使用宏 #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
  1. 对于 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
  1. 对于 gcc 编译器,可以使用 __attribute__((aligned(n)))扩大一个结构体的内存对齐单位。具体可见实验1

实验1__attribute__((aligned(n))) 只可用于扩大单个结构体的内存对齐单位无法缩小

  • 先用宏定义将该文件的所有结构体的"内存格"缩小为1
    • 再用本条所述指令将该结构体的内存对齐单位重新扩大为2(不可用于缩小)
    • 故总大小为6
#pragma pack (1)  // 以8位为对齐单位
typedef struct {
    uint16_t a;
    uint8_t b;
    uint16_t c;
} __attribute__((aligned(2))) type_a;  // sizeof(type_a) = 6Byte
  • 先用宏定义将该文件的所有结构体的"内存格"设定为8
    • 再用本条所述指令将该结构体的内存对齐单位尝试缩小为1
    • 失败,因为C语言在对齐时,会取 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语言标准)

联合体内存对齐的基本规则

  1. 联合体内最大单体元素的大小为内存对齐单位,最终轨道联合体大小为该内存对齐单位的整数倍。

实验2

union bin
{
	char a[99];
	int b[3];
	double c;
};  //sizeof(union bin) = 104,为8的整数倍

2.48 为什么在C语言中,一定要用unsigned char表示byte而非signed char或char

在C语言中并没有提供 byte 类型,因此C语言使用 unsigned char 来表示 byte 。选择采用 unsigned char 而不采用 signed char 的考虑主要有以下几点:

  1. 必要性问题,虽然由于C语言负数补码规则的存在, bytesigned char 解释为负数并不影响 byte 原本的二进制值,但是 byte 本身也没有负数特性,没有使用符号的必要。
  2. 类型提升问题,若用 signed char 解释值为 0xffbyte 时,其 signed char 对应的值为 -1 。而当需要将 signed char 提升为 int32_t 类型时,对应的 int32_t 的值也应当为 -1但是此时的二进制从 0xff 提升为了 0xffffffff擅自多了24位 1
    而关于 signed charchar 问题,C语言并未规定 char 一定是 signed char ,也可以是 unsigned char ,具体使用哪个取决于编译器实现。规范见C标准学习笔记 > 6 3 1 1 布尔、字符和整数

而在C语言所提供的 string.h 库中,许多字符串操作(例如 strchrstr* 函数)和byte二进制操作(例如 memchrmem* 函数)通常使用 int 来接收参数,是因为:

  1. int 的整数部分可以接收 unsigned char 所表示的 byte 而不触发负数补码机制,再次转换为 unsigned char 不会改变二进制值。
  2. 部分返回值为 char 的操作(如 fgetc )需要返回 EOF 来表示错误或其他情况,而 EOFint 类型的 -1 (至少为16位的 1 ),不会影响 char 类型的 -1 ( 0xff ,扩展ASCII表中为字符 ÿ )的表示。
    此外,unsigned类型可以使用 printf("%x") 正常打印,而signed不行。