1. 代码执行过程
1.1 简述编译运行一段代码的过程
1.1.1 预处理
为了接下来能够解释的更加清楚,使用linux
平台下的
gcc
编译器解释。先书写一个非常简单的程序来介绍:
第一步发生的是预编译,使用-E
指令会使程序只进行到预编译指令。经过预编译指令后的会生成一 个.i
文件
在预编译的过程中,主要处理源代码中的预处理指令,引入头文件,去除注释,处理所有的条件编
译指令(
#ifdef,#ifndef,#else,#elif,#endif
),宏的替换,添加行号,保留所有的编译器指令
。
当进行预编译以后的文件中将不再存在宏,所有的宏都已经被替代。当想要判断宏是否正确或者头
文件包含是否正确时,也可以通过预编译来查看
。
1.1.2 编译
1.1.3 汇编
1.1.4 链接
1.2 静态链接和动态链接有什么区别
静态链接是指把要调用的函数或者过程直接链接到可执行文件中
,成为可执行文件的一部分。换句 话说,函数和过程的代码就在程序的
exe
文件中
,
该文件包含了运行时所需的全部代码
。静态链接的缺 点是:当多个程序都调用相同函数时,内存中就会存在这个函数的多个复制,这样就浪费了内存资源;
动态链接是相对于静态链接而言的。动态链接调用的函数代码并没有被复制到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)
。仅当应用程序被
装
入内存开始运行时
,在操作系统的管理下,才在应用程序与相应的动态链接库(
dynamic link library,d)之间
建立链接关系
。
静态链接的执行程序能够在其他同类操作系统
的机器上直接运行。例如,一个
exe
文件是在
Windows2000
系统上静态链接的,那么将该文件直接复制到另一台
Windows2000
的机器上,是可以运 行的。而动态链接的执行程序则不可以,除非把该exe
文件所需的
dll
文件都一并复制过去,或者对方机 器上也有所需的相同版本的dll
文件,否则是不能保证正常运行的。
1.3 静态链接库和动态链接库有什么区别
静态链接库就是使用的lib
文件,库中的代码最后需要链接到可执行文件中去,所以静态链接的可执 行文件一般比较大一些。
动态链接库是一个包含可由多个程序同时使用的代码和数据的库,它包含函数和数据的模块的集合。程序文件(如exe文件或
d
文件)在运行时加载这些模块(即所需的模块映射到调用进程的地址空间)。
静态链接库和动态链接库的相同点是它们都实现了代码的共享,不同点是静态链接库lib文件中的代码被包含在调用的exe
文件中,该
lib
文件中不能再包含其他动态链接库或者静态链接库了。而动态链接 库dll
文件可以被调用的
exe
动态地
“
引用
”
和
“
卸载
”
,该
dll
文件中可以包含其他动态链接库或者静态链接库。
1.4 字节对齐
1.4.1 字节对齐的概念
在现代计算机中,内存空间都是按照字节(byte)
划分的。访问特定类型的变量的时候经常在特定的内存地址访问,这就需要各种类型的数据按照一定规则在空间上排列,而不是顺序地一个接一个地排放。
字节对齐可以提升存取效率,也就是用空间换时间。
1.4.2 为什么需要字节对齐?
因为各个硬件平台对存储空间的处理上有很大的不同,一些平台对某些特定类型的数据只能从某些 特定地址开始存取。比如有些架构的CPU
在访问一个没有进行对齐的变量的时候会发生错误
,
那么在这种 架构下编程必须保证字节对齐.
其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。
比如有些平台每次读都是从偶地址开始,如果一个int 型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这
32bit
,而如果存放在奇地址开始的地方,就需要2
个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该
32bit
数据。显然在读取效率上下降很多。
1.4.3 几个基本概念
1.4.4 字节对齐的几个例子浅析
1.5 #define和const的区别,const比#define的优点
2. 函数
2.1 malloc、free和new、delete的区别与联系
2.2 Assert
assert宏的原型定义在
<assert.h>
中,其作用是如果它的条件返回错误,则终止程序执行,原型定 义:
2.3 void 函数的返回值问题
(1)函数结束后返回单数据类型的返回值
(2)利用指针在函数执行过程中返回值
2.4 C语言函数嵌套定义与嵌套调用
C语言中函数的定义都是相互平行、相互独立的,也就是说在函数定义时,
函数体内不能包含另一
个函数的定义,即函数不能嵌套定义,但可以嵌套调用
。嵌套调用就是某个函数调用另外一个函数(即 函数嵌套允许在一个函数中调用另外一个函数)。
2.5 静态存储区和动态存储区
2.6 数组指针和指针数组
数组指针:指向数组的指针;二维的需要两次取值
指针数组:
2.7 函数指针和指针函数
函数指针:
最后需要注意的是,指向函数的指针变量没有 ++ 和 -- 运算。
指针函数:
3. 语句
3.1 定义常量谁更好?# define还是 const
#define:单纯的文本替换,预处理进行,编译期结束,不分配内存空间,不存在代码段,没有数据类型,不作安全检查。
const:const常量存在于程序的数据段,再堆栈中分配空间,确实存在可以被调用,有数据类型。可以支持安全检查和调试。使用const定义变量类型可以增加程序的健壮性。
3.2 *(a[1]+1)、*(&a[1][1])、(*(a+1))[1] 表示什么意思
-
*(a[1] + 1)
:
- 这里
a
是一个数组,a[1]
表示数组 a
的第二个元素(数组索引通常从0开始)。 a[1] + 1
将 a[1]
的地址与1相加,得到一个新的地址,这个地址位于 a[1]
的下一个存储单元。*
是解引用操作符,用来获取该地址处的值。- 因此,
*(a[1] + 1)
表示获取 a[1]
后面那个存储单元中存储的值。
-
*(&a[1][1])
:
a
应该是一个二维数组(或多维数组)。a[1]
获取数组的第一行(跳过第0行)。a[1][1]
获取第一行的第二个元素。&
是取地址操作符,&a[1][1]
获取该元素的内存地址。*
再次解引用这个地址,得到该地址处的值。- 因此,
*(&a[1][1])
表示获取 a
数组第一行第二个元素的值。
-
(*(a + 1))[1]
:
- 这个表达式稍微复杂一些。首先,如果
a
是一个数组,a + 1
将数组的首地址向前移动一个单位,指向下一个元素的地址。 *(a + 1)
解引用这个新的地址,如果 a
是一个一维数组,它将获取 a[1]
的值。- 然而,表达式
(*(a + 1))[1]
似乎意味着 a + 1
解引用后仍然是一个数组,这可能意味着 a
是一个指针到数组的指针,或者 a
是一个多维数组的行指针。 - 如果
a
是一个多维数组的指针,*(a + 1)
将获取下一行的数组,然后 [1]
获取这一行的第二个元素。
3.3 不使用流程控制语句,如何打印出1~1000的整数
3.4 a是数组,(int*)(&a+1)表示什么意思
数组指针:指针加1,数据类型加1,指向一维数组的下一维起始地址;
对于数组而言,一个数组名代表的含义是数组中第一个元素的位置,即地址,通过数组名可以访问 该数组。程序示例如下:
&a是一个指向
int(*)[5]
的指针。由于
&a
是一个指针,那么在
32
位机器上,
sizeof
(
&a
)
=4
,但是
&a+1
的值取决于
&a
指向的类型
,由于
&a
指向
int(*)[5]
,所以
&a+1=&a+ sizeof
(
int(*)
[5])=&a+20
。
&a
是数组的首地址,由此
&a+1
表示
a
向后移动了
5
个
int
从而指向
a[5]
,所以
ptr
指向的是 a[5],由于
ptr
是
int *
类型,那么
ptr-1
就指向
a[4]
,所以
*(ptr
-
1)=a[4]=5
。
3.5 数组下标可以为负数吗
可以,因为下标只是给出了一个与当前地址的偏移量
而已,只要根据这个偏移量能定位得到目标地址即可。下面给出一个下标为负数的示例:
数组下标取负值的情况:
3.6 C语言宏中"#"和"##"的用法
(#x,将x转换成字符串的作用),具体可用途径:
2. 填充结构
3.记录文件名
4.得到一个数值类型所对应的字符串缓冲大小
3.7 typedef和 define有什么区别
typedef与
define
都是
替一个对象取一个别名
,以此来增强程序的可读性,但是它们在使用和作用 上也存在着以下4
个方面的不同。
1
)原理不同
#define是
C
语言中定义的语法,它是预处理指令,在预处理时进行简单而机械的字符串替换,
不
做正确性检査
,不管含义是否正确照样代入,只有在编译已被展开的源程序时,才会发现可能的错误并 报错。
typedef是关键字,它在编译时处理,所以 typedef
具有类型检查的功能。它在自己的作用域内给 一个已经存在的类型一个别名,但是不能在一个函数定义里面使用标识符 typedef
。例如,
typedef int INTEGER ,这以后就可用
INTEGER
来代替
int
作整型变量的类型说明了,例如:
INTEGER a,b
;
2
)功能不同
typedef用来定义类型的别名,这些类型不仅包含内部类型(
int
、
char
等),还包括自定义类型 (如 struct),可以起到使类型易于记忆的功能。
#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。
3
)作用域不同
#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而
typedef
有 自己的作用域。
4
)对指针的操作不同
两者修饰指针类型时,作用不同。
3.8 数组名和指针的区别与联系
3.9 strlen("\0") =? sizeof("\0")=? p64
strlen("\0") =0
,
sizeof("\0")=2
strlen用来计算字符串的长度(在
C/C++
中,字符串是
以
"\0"
作为结束符的
),它从内存的某个位置 (可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描直到碰到第一个字符串 结束符\0为止,然后返回计数器值;
sizeof
是
C
语言的关键字,它以
字节的形式
给出了其操作数的
存储大
小
,操作数可以是一个表达式或括在括号内的类型名,操作数的存储大小由操作数的类型决定。
sizeof既是关键字又是运算符,strlen是函数
3.10 不使用 sizeof,如何求int占用的字节数
利用字节差计算
3.
3.11 不能使用任何变量,如何实现计算字符串长度函数
3.12 逗号表达式
在使用逗号表达式时,首先需要弄清楚其表示形式,其表示形式如下:
表达式1
,表达式
2
,表达式
3……
表达式
n
使用逗号表达式,需要注意以下3
个方面的事项
1)逗号表达式的运算过程为从左往右逐个计算表达式的值
2)逗号表达式作为一个整体,它的值为最后一个表达式的值。
3)逗号运算符的优先级别在所有运算符中最低。
3.13 C语言中 struct与 union的区别是什么
1
)结构体与联合体虽然都是由多个不同的数据类型成员组成的,但不同之处在于联合体中所有成员
共用一块地址空间
,即联合体只存放了一个被选中的成员,而结构体中所有成员占用空间是累加的,其 所有成员都存在,不同成员会存放在不同的地址。在计算一个结构型变量的总长度时,其内存空间大小 等于所有成员长度之和(需要考虑字节对齐),而在联合体中,所有成员不能同时占用内存空间,它们 不能同时存在,所以一个联合型变量的长度等于其最长的成员的长度
。
2
)对于联合体的不同成员赋值,
将会对它的其他成员重写
,原来成员的值就不存在了,而对结构体 的不同成员赋值是互不影响的。
3.14 全局变量和静态变量的区别
1
)全局变量的作用域为程序块,而局部变量的作用域为当前函数。
2
)内存存储方式不同,全局变量(静态全局变量,静态局部变量)分配在全局数据区(静态存储空 间),后者分配在栈区
3
)生命周期不同。全局变量随主程序创建而创建,随主程序销毁而销毁,局部变量在局部函数内
部,甚至局部循环体等内部存在,退出就不存在了。
4
)使用方式不同。通过声明为全局变量,程序的各个部分都可以用到,而局部变量只能在局部使
用。
3.15 理解非常复杂的声明
3.16 变量定义和变量声明
定义也是声明”
,这说明定义包括声明,对于
int a
来说,它既是定义,又是声明,对于
extem int a 来说,它是声明,不是定义。一般为了叙述方便,把申请存储空间的声明称为定义
,而把
不申请存储空
间的声明称为声明
。
4. 存储与运算
4.1 一个十六进制的数占用多少字节
一个16进制位=0.5个字节=4位2进制
4.2 嵌人式编程中,什么是大端?什么是小端
4.3 不用除法操作符如何实现两个正整数的除法
用减法算除法:
移位操作实现翻倍
如何只用逻辑运算实现加法运算:异或和移位
如何只用逻辑运算实现乘法运算 :乘法移位和加法
4.4 不使用if/?:switch及其他判断语句如何找出两个int型变量中的最大值和最小值
利用库函数,max,abs和比较
4.5 一些结构声明中的冒号和数字是什么意思
C语言的结构体可以实现位段(也称位域),它的定义形式是在一个定义的结构体成员后加上冒 号,然后是该成员所占的位数。位段的结构体成员必须是int
或者
unsigned int
类型,不能是其他类型。 位段在内存中的存储方式是由具体的编译器决定的。
首先,定义位段的长度不能大于存储单元的长度。存储单元是指该位段的类型大小。其次,个位段 如果不能放在一个存储单元里,那么它会把这个存储单元中剩余的空间闲置,而从下一个存储单元开始 存储下一个位段,即一个位段不能跨越两个存储单元,位段在一个存储单元中的存储是紧凑的。再次, 位段名缺省时称作无名位段,无名位段的存储空间通常不用,而位段长度为0位表示下一个位段存储在一 个新的存储单元中,位段长度为0
的时候位段名必须缺省(不能定义位段名)。最后,一个结构体中既可 以定义位段成员,也可以同时定义一般的结构体成员。这个时候,一般成员不和位段存储在同一个存储 单元中。
4.6 不能用 sizeof()函数,如何判断操作系统是16位,还是32位
利用int变量·类型的最大值进行判断
4.7 Makefile包含几部分
4.8 ++a和a++那个运算效率高,为什么?
4.9 左值和右值
左值是指可以出现在等号左边的变量或表达式,它最重要的特点就是可写
(可寻址)。也就是说, 它的值可以被修改
,如果一个
变量或表达式的值不能被修改
,那么它就不能作为左值。
右值是指只可以出现在等号右边的变量或表达式
。它最重要的特点是
可读
。一般的使用场景都是把 一个右值赋值给一个左值。
通常,左值可以作为右值,但是右值不一定是左值;(可写大于可读)
4.10 # include< filename. h>和# include" filename. h"有什么区别
对于 include< filename. h>
,编译器先从标准库路径开始搜索
filename.h
,使得系统文件调用较快。而对于# include“ filename.h"”
,编译器先从用户的工作路径开始搜索
filename.h
,然后去寻找系统路径,使得自定义文件较快。
4.11 const在不同位置的区别
就近原则
4.12 Heap与stack的差别
Heap是堆,stack是栈
4.13 请简述栈在C语言中的作用
栈在多线程编程中的作用,栈在操作系统中的作用。
C语言中栈用来存储临时变量,临时变量包括
函数参数和函数内部定义的临时变量
。函数调用中和 函数调用相关的函数返回地址
,函数中的临时变量,寄存器等均保存在栈中,函数调动返回后从栈中恢 复寄存器和临时变量等函数运行场景。
多线程编程的基础是栈,栈是多线程编程的基石
,每一个线程都最少有一个自己专属的栈,用来存 储本线程运行时各个函数的临时变量和维系函数调用和函数返回时的函数调用关系和函数运行场景。操作系统最基本的功能是支持多线程编程,支持中断和异常处理,每个线程都有专属的栈,中断和异常 处理也具有专属的栈,栈是操作系统多线程管理的基石。
4.14 要对绝对地址0x100000赋值,我们可以用
4.15 有符号数和无符号数
4.16 求一个正整数的开方,要求不能使用库函数sqrt,结果精度在0.01即可。
二分查找法:有两个边界,判断所得中间值平方是否大于目标值,大于或小于是的边界等于中间值,推出循环条件为插值满足0.01
根据语法,sizeof如用于数组,只能测出静态数组的大小,无法检测动态分配的或外部数组大小。
getmemory中的malloc 不能返回动态内存, free()对str
操作很危险。
4.17 短路求值
或条件并不一定会执行完,如果前一个条件已经满足的情况下。
4.18 关键字volatile有什么含意?并举出三个不同的例子?
1
)
并行设备的硬件寄存器。
存储器映射的硬件寄存器通常加
volatile
。例如状态寄存器。以为设
备寄存器会在你的程序不知道或者不介入的时候发生改变,那是因为设备寄存器可以被外设硬件修改。 相反,变量中的不会变。设备寄存器的内容是易失的,或者在不注意的时候被修改。当声明指向设备寄
存器的指针时一定要用
volatile
它会告诉编译器不要对存储在这个地址的数据进行假设
,编译器在优化 这个变量时应该把它看作编译时未知的。
2
)
一个中断服务程序中修改的供其他程序检测的变量
volatile
提醒编
译器它后面所定义的变量
随时都有可能改变,因此编
译后的程
序每次 需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile
关键字,则 编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话, 将出现不一致的现象。
3
)
多线程应用中被几个任务共享的变量。单地说就是防止编译器对代码进行优化
5.3 常量指针
5.4 const的作用
5.5 局部变量能否和全局变量重名?
5.6 用两个栈实现一个队列的功能?要求给出算法和思路!
6. 进程和线程
6.1 创建进程有哪几种方式
6.2 进程和线程有什么区别
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,它是系统进行资源分配和调
度的一个独立单位
。例如,用户运行自己的程序,系统就创建一个进程,并为它
分配资源
,包括各种表格、内存空间、磁盘空间、IO
设备等,然后该进程被放入到进程的就绪队列,进程调度程序选中它,为 它分配CPU
及其他相关资源,该进程就被运行起来。
线程是进程的一个实体,是CPU
调度和分配的基本单位
,线程自己基本上不拥有系统资源,只拥有 一些在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但是,它可以与同属一个进程的其他的线程共享进程所拥有的全部资源。
在没有实现线程的操作系统中,进程既是资源分配的基本单位,又是调度的基本单位,它是系统中
并发执行的单元。而在实现了线程的操作系统中,进程是资源分配的基本单位而线程是调度的基本单
位,是系统中并发执行的单元
。
引入线程主要有以下
4
个方面的
优点
:
1
)易于调度。
2
)提高并发性。通过线程可以方便有效地实现并发
3
)开销小。创建线程比创建进程要快,所需要
的开销也更少。
4
)有利于发挥多处理器的功能。通过创建多线程,每个线程都在一个处理器上运行,从而实现应用 程序的并行,使每个处理器都得到充分运行。
6.3 进程间通信方式
进程通信(Interprocess Communication
,
IPC)
是一个进程与另一个进程间共享消息的一种通信方 式。消息(message)
是发送进程形成的一个消息块,将消息内容传送给接收进程。
IPC
机制是消息从一个 进程的地址空间拷贝到另一个进程的地址空间。
6.3.1 管道
半双工通信,只能在亲缘关系的进程间使用,通常关系指的是父子进程
流管道s_pipe:
去除了第一种限制
,
可以
双向传输
(全双工)
.
管道可用于具有亲缘关系进程间的通信。命名管道
:name_pipe
克服了管道没有名字的限制,因此, 除具有管道所具有的功能外,它还允许无亲缘关系
进程间的通信;
6.3.2 信号量
信号量是一个计数器
,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,
防止某进
程正在访问共享资源时,其他进程也访问该资源
。因此,主要作为进程间以及同一进程内不同线程之间 的同步手段。
6.3.3 消息队列
消息队列是由消息组成的链表,存放在内核中并由消息队列标识符标识。
消息队列是消息的链接表,包括Posix消息队列
system V
消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
6.3.4 信号
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生
。主要作为进程间以及同一 进程不同线程之间的同步手段。
6.3.5 共享内存
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都
可以访问
。
共享内存是最快的
IPC
方式
,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
6.3.6 套接字
套解字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
6.4 线程间同步方式
6.5 内核线程和用户线程的区别
内核线程的建立和销毁都是由操作系统负责、通过系统调用完成的,操作系统在调度时,参考各进程内的线程运行情况做出调度决定。如果一个进程中没有就绪态的线程,那么这个进程也不会被调度占用CPU。
和内核线程对应的是用户线程。用户线程指不需要内核支持而在用户程序中实现的线程,其不依赖
于操作系统核心
,用户进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。
6.6 锁
6.6.1 读写锁
6.6.2 死锁
6.7 用户栈和内核栈有什么区别
内核在创建进程的时候,在创建 task_struct
的同时,会为进程创建相应的堆栈。每个进程会有两个 栈:一个用户栈,存在于用户空间。一个内核栈,存在于内核空间。当进程在用户空间运行时,
CPU
堆
栈指针寄存器里面的内容是用户堆栈地址,使用用户栈:当进程在内核空间时,
CPU
堆栈指针寄存器里
面的内容是内核栈空间地址,使用内核栈。
当进程因为中断或者系统调用而从用户态转为内核态时,进程所使用的堆栈也要从用户栈转到内核 栈。进程陷入内核态后,先把用户态堆栈的地址保存在内核栈之中,然后设置堆栈指针寄存器的内容为 内核栈的地址,这样就完成了用户栈向内核栈的转换
;当进程从内核态恢复到用户态时,把内核栈中保 存的用户态的堆栈的地址恢复到堆栈指针寄存器即可。这样就实现了内核栈和用户栈的互转
但是在陷入内核的时候,如何知道内核栈的地址?关键在进程从用户态 转到内核态的时候,进程的内核栈总是空的。这是因为当进程在用户态运行时,使用的是用户栈,当进
程陷入到内核态时,内核栈保存进程在内核态运行的相关信息,但是,一旦进程返回到用户态后,
内核
栈中保存的信息无效,会全部恢复,因此每次进程从用户态陷入内核时得到的内核栈都是空的
,所以在 进程陷入内核的时候,直接把内核栈的栈顶地址给堆栈指针寄存器就可以了。
6.8 内存泄漏
堆上分配的内存
,如果不再使用了,就应该及时释放,以便后面其他地方可以重 用。而在 C
语言中,
内存管理器不会自动回收不再使用的内存。
如果忘了释放不再使用的内存,这些内
存就不能被重用了,这就造成了内存泄漏。
6.9 内存管理的方式
6.10 虚拟内存