《求职》第一部分 - 语言篇 - C语言常见面试题

1.变量

面试题1-1:变量的声明和定义有什么区别

为变量分配地址和存储空间的称为定义,不分配地址的称为声明。一个变量可以在多个地方声明,但只能在一个地方定义。加入extern修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。

说明:很多时候一个变量,只是声明,不分配内存空间,知道具体使用时才初始化,分配内存空间,如外部变量。

面试题1-2:a和 &a 有什么区别

主要目的是考察a和&a的区别。

#include<stdio.h>   

int main( void )   
{   
    int a[5]={1,2,3,4,5};  
    int *ptr=(int *)(&a+1);  

    printf("%d,%d",*(a+1),*(ptr-1));   

    return 0;  
 }  

输出结果:2,5。

注意:数组名a可以作数组的首地址,而&a是数组的指针。思考,将原式的int *ptr=(int *)(&a+1);改为int *ptr=(int *)(a+1);时输出结果将是什么呢?

面试题1-3:一个指针可以是volatile吗?

可以,因为指针和普通变量一样,有时也有变化程序的不可控性。常见例:子中断服务子程序修改一个指向一个buffer的指针时,必须用volatile来修饰这个指针。

面试题1-4:用变量a给出下面的定义

a) 一个整型数

b) 一个指向整型数的指针

c) 一个指向指针的的指针,它指向的指针是指向一个整型数

d) 一个有10个整型数的数组

e) 一个有10个指针的数组,该指针是指向一个整型数的

f) 一个指向有10个整型数数组的指针

g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数

h) 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数

答案是:

a) int a;

b) int *a;

c) int **a;

d) int a[10];

e) int *a[10];

f) int (*a)[10];

g) int (*a)(int);

h) int (*a[10])(int);

人们经常声称这里有几个问题是那种要翻一下书才能回答的问题,我同意这种说法。当我写这篇文章时,为了确定语法的正确性,我的确查了一下书。

但是当我被面试的时候,我期望被问到这个问题(或者相近的问题)。因为在被面试的这段时间里,我确定我知道这个问题的答案。应试者如果不知道所有的答案(或至少大部分答案),那么也就没有为这次面试做准备,如果该面试者没有为这次面试做准备,那么他又能为什么出准备呢?

面试题1-5:局部变量能否和全局变量重名?

答:能,局部会屏蔽全局。
局部变量可以与全局变量同名,在函数内引用这个变量时,会用到同名的局部变量,而不会用到全局变量。对于有些编译器而言,在同一个函数内可以定义多个同名的局部变量,比如在两个循环体内都定义一个同名的局部变量,而那个局部变量的作用域就在那个循环体内。

面试题1-6:如何引用一个已经定义过的全局变量?

答:extern
可以用引用头文件的方式,也可以用extern关键字,如果用引用头文件方式来引用某个在头文件中声明的全局变理,假定你将那个变量写错了,那么在编译期间会报错,如果你用extern方式引用时,假定你犯了同样的错误,那么在编译期间不会报错,而在连接期间报错。

面试题1-7:全局变量能否在多个.C文件包含的头文件中?

答:可以,在不同的C文件中以static形式来声明同名全局变量。只能在当前.c文件中使用。
可以在不同的C文件中声明同名的全局变量,前提是其中只能有一个C文件中对此变量赋初值,此时连接不会出错。

面试题1-8:写出float x 与“零值”比较的if语句。

if(x>0.000001&&x<-0.000001)

2.宏定义

面试题2-1:写一个“标准”宏MIN

#define min(a,b) ((a)<=(b)?(a):(b))

1)标识#define在宏中应用的基本知识。这是很重要的,因为直到嵌入(inline)操作符变为标准C的一部分,宏是方便产生嵌入代码的唯一方法,对于嵌入式系统来说,为了能达到要求的性能,嵌入代码经常是必须的方法。

2)三重条件操作符的知识。这个操作符存在C语言中的原因是它使得编译器能产生比if-then-else更优化的代码,了解这个用法是很重要的。

3)懂得在宏中小心地把参数用括号括起来

4)宏的副作用,例如:当你写下面的代码时会发生什么事?

least = min(*p++, b); 

面试题2-2:用宏定义表示1年中有多少秒(忽略闰年问题)

#define  SECONDS_PER_YEAR  (60 * 60 * 24 * 365)UL 
  1. #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)

  2. 预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的。

  3. 意识到这个表达式将使一个16位机的整型数溢出-因此要用到长整型符号L,告诉编译器这个常数是的长整型数。

  4. 如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点。

面试题2-3:typedef和define有什么区别

(1)用法不同:typedef用来定义一种数据类型的别名,增强程序的可读性。define主要用来定义常量,以及书写复杂使用频繁的宏。

(2)执行时间不同:typedef是编译过程的一部分,有类型检查的功能。define是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。

(3)作用域不同:typedef有作用域限定。define不受作用域约束,只要是在define声明后的引用都是正确的。

(4)对指针的操作不同:typedef和define定义的指针时有很大的区别。

注意:typedef定义是语句,因为句尾要加上分号。而define不是语句,千万不能在句尾加分号;

面试题2-4:宏定义的用法

看下面这个程序,求出结果

#include <stdio.h>  

#define S(a,b) a*b  

int main(void)  
{  
	int n = 3;  
	int m = 5;  
	printf("%d",S(n+m,m+n));  

	return 0;  
}  

这道题容易出现的错误结果是64,得到这个结果肯定是这样理解的 ( 3 + 5 ) ( 5 + 3 ) (3+5)*(5+3) 。其实并不是,大家要理解宏定义的概念,宏定义只是简单的符号替换,而不做其他处理,所以这里得到的结果是 3 + 5 5 + 3 = 31 3+5*5+3=31 .

要想得到正确结果,应该怎么样呢?应该这样改,define s(a,b) (a)*(b),这样才是正确结果;

大家记住这句话,宏定义只是简单的符号替换!

以下程序执行的结果是__

#include <stdio.h>]  

#define N  2  
#define M  N+1  
#define NUM  (M+1)*M/2  

int main(void)  
{  
	pritf("%d",NUM);  
	return 0;
}  

A、5 B、6 C、8 D、9

答案:C

注意宏定义的使用,此处 N U M = ( 3 + 1 ) 2 + 1 / 2 NUM = (3+1)*2+1/2 为8

面试题2-5:调试打印

用法1:

#define MYDEBUG

#ifdef MYDEBUG
#define DEBUG(arg...) {\
printf("[debug]:%s:%s:%d ---->",__FILE__,__FUNCTION__,__LINE__);\
printf(arg);\
fflush(stdout);\
}

#else
#define DEBUG(arg...) {}
#endif

用法2:

//#define dbg_printf(f, a...) \
// do { \
// fprintf(stdout, "%s(%d): " f, __func__, __LINE__, ## a); \
// }while (0)

 
#define dbg_printf(f, a...) \
fprintf(stdout, "%s(%d): " f, __func__, __LINE__, ## a)

面试题2-6:可变参数宏

#define debug(fmt, ...) printf(fmt, __VA_ARGS__)  // GCC
__VA_ARGS__替换可变参数

#define debug2(fmt, args...) printf(fmt, args)   // GNU

#define debug3(fmt, ...) printf(fmt, ##__VA_ARGS__)  //可以接受无参数(变参为空), ## 屏蔽逗号

#define C(x)  #x   // 一个#将参数字符串化

面试题2-7:do { code; } while(0); 宏

实现复杂的宏定义:函数式宏、符合语句;使用局部变量;在条件语句中使用复杂的宏定义。

struct student{
    char name[20];
    int age;
};

// #define PRINT(a) printf(“%s\n”, a.name);printf(“%d\n”, a.age);

#define PRINT(a)  \
do{printf(“%s\n”, a.name);printf(“%d\n”, a.age);}while(0);

int main()
{
    struct student stu = {“wit”, 20};
    if(0)
        PRINT(stu); // 至少运行一次
    return 0;
}

3.关键字

面试题3-1:关键字const是什么

const用来定义一个只读的变量或对象。主要优点:便于类型检查、同宏定义一样可以方便地进行参数的修改和调整、节省空间,避免不必要的内存分配、可为函数重载提供参考。

const只读变量,在定义时必须初始化,否则后面不能赋值;编译器不为const常量分配存储空间,将其保存在符号表中,效率高。

说明:const修饰函数参数,是一种编程规范的要求,便于阅读,一看即知这个参数不能被改变,实现时不易出错。

前面是官方的说法,我们经常会听到被面试者说:“const意味着常数”,我就知道我正在和一个业余者打交道。 去年Dan Saks已经在他的文章里完全概括了const的所有用法,因此ESP(译者:Embedded Systems Programming)的每一位读者应该非常熟悉const能做什么和不能做什么.

如果你从没有读到那篇文章,只要能说出const意味着 “只读”就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。(如果你想知道更详细的答案,仔细读一下Saks的文章吧。)如果应试者 能正确回答这个问题,我将问他一个附加的问题:下面的声明都是什么意思?

const int a; a是一个常整型数

int const a; a是一个常整型数

const int *a; a是一个指向常整型数的指针

int * const a; a是一个指向整型数的常指针

int const * a const; a是一个指向常整型数的常指针

前两个的作用是一样,a是一个常整型数。

第三个意味着a是一个指向常整型数的指针(整型数是不可修改的,但指针可以)

第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。

最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数 是不可修改的,同时指针也是不可修改的)。

如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字 const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const呢?我也如下的几下理由:

1)关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理 其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。)

2)通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。

3)合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。

// 常量指针
int b = 10;
int c = 100;
const int* ptr1 = &b;
//*ptr = 20;//错误
b = 20;
printf("%d\n",*ptr1);

ptr1 = &c;
printf("%d\n",*ptr1);

// 指针常量
int* const ptr2 = &b;
//ptr2 = &c;//错误
printf("%d\n",*ptr2);

面试题3-2:static有什么作用

static在C中主要用于定义全局静态变量、定义局部静态变量、定义静态函数。在C++中新增了两种作用:定义静态数据成员、静态函数成员。

注意:因为static定义的变量分配在静态区,所以其定义的变量的默认值为0,普通变量的默认值为随机数,在定义指针变量时要特别注意。

在C语言中,关键字static有三个明显的作用:

1)在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。

2)在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。

3)在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。大多数应试者能正确回答第一部分,一部分能正确回答第二部分,同是很少的人能懂得第三部分。这是一个应试者的严重的缺点,因为他显然不懂得本地化数据和代码范围的好处和重要性。

面试题3-3:C语言中static函数与普通函数的区别是什么?

静态函数
在函数的返回类型前加上关键字static,函数就被定义成为静态函数。
函数的定义和声明默认情况下是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
定义静态函数的好处:
<1> 其他文件中可以定义相同名字的函数,不会发生冲突
<2> 静态函数不能被其他文件所用。

局部静态变量
在局部变量之前加上关键字static,局部变量就被定义成为一个局部静态变量。
1)内存中的位置:静态存储区
2)初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)
3)作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域随之结束。

全局静态变量
在全局变量之前加上关键字static,全局变量就被定义成为一个全局静态变量。
1)内存中的位置:静态存储区(静态存储区在整个程序运行期间都存在)
2)初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)
3)作用域:全局静态变量在声明他的文件之外是不可见的。准确地讲从定义之处开始到文件结尾。

好处:
<1>不会被其他文件所访问,修改。
<2>其他文件中可以使用相同名字的变量,不会发生冲突。

面试题3-4:extern有什么作用

extern标识的变量或者函数声明其定义在别的文件中,提示编译器遇到此变量和函数时在其它模块中寻找其定义。

面试题3-5:修饰符volatile含义是什么?其应用场合有哪些

volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。

面试题3-6:关键字volatile有什么含意?并给出三个不同的例子。

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:

  1. 并行设备的硬件寄存器(如:状态寄存器)

  2. 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

  3. 多线程应用中被几个任务共享的变量

回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用到volatile变量。不懂得volatile的内容将会带来灾难。

假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。

1)一个参数既可以是const还可以是volatile吗?解释为什么。
2)一个指针可以是volatile 吗?解释为什么。

  1. 下面的函数有什么错误:
int square(volatile int *ptr)
{
    return *ptr * *ptr;
}

下面是答案:
1)是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2)是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。
3) 这段代码有点变态。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:

int square(volatile int *ptr) 
 {
     int a,b;
     a = *ptr;
     b = *ptr;
     return a * b;
 }

由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:

long square(volatile int *ptr) 
 {
     int a;
     a = *ptr;
     return a * a;
 }

面试题3-7:union 和 struct

union中的各个域的空间是重叠的,struct中的各个域的空间是不重叠的。

面试题3-8:register

单片机中register有没有地址,C语言中register有没有地址?

面试题3-9:typeof关键字

它的作用是自动推导表达式的数据类型。typeof构造的主要应用是用在宏定义中。可以使用typeof关键字来引用宏参数的类型。

C语言中 typeof 关键字是用来定义变量数据类型的。在linux内核源代码中广泛使用。下面是Linux内核源代码中一个关于typeof实例:

#define min(x, y) ({ \

typeof(x) _min1 = (x); \

typeof(y) _min2 = (y); \

(void) (&_min1 == &_min2); \

_min1 < _min2 ? _min1 : _min2; })

1.当x的类型为是 int 时 _min1变量的数据类型则为 int。

2.当x为一个表达式时(例: x = 3-4), _min1变量的数据类型为这个表达式结果的数据类型。

3.typeof括号中也可以是函数

例:

int function(int, int);

typeof(function(1. 2)) val;

此时val的数据类型为 函数function(int, int)返回值的数据类型 ,即int类型。(注意: typeof并不会执行函数function)。

typeof关键字有点类似与c++中的decltype关键字。

https://blog.csdn.net/zhanshen2015/article/details/51495273

4.数组和指针

面试题4-1:简述指针常量与常量指针区别

指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。其实指针常量是唯一的,即NULL;常量指针是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。

指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。

注意:无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变特性。

面试题4-2:如何避免“野指针”

“野指针”产生原因及解决办法如下:

(1)指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向NULL。

(2)指针 p 被 free 或者 delete 之后,没有置为 NULL。解决办法:指针指向的内存空间被释放后指针应该指向NULL。

(3)指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向NULL。

注意:“野指针”的解决方法也是编程规范的基本原则,平时使用指针时一定要避免产生“野指针”,在使用指针前一定要检验指针的合法性。

面试题4-3:指针数组和数组指针的区别

对指针数组和数组指针的概念,相信很多C程序员都会混淆。下面通过两个简单的语句来分析一下二者之间的区别,示例代码如下所示:

int *p1[5];
int (*p2)[5];
  • 指针数组

首先,对于语句“int *p1[5]”,因为“[]”的优先级要比“*”要高,所以 p1 先与“[]”结合,构成一个数组的定义,数组名为 p1,而“int*”修饰的是数组的内容,即数组的每个元素。也就是说,该数组包含 5 个指向 int 类型数据的指针,因此,它是一个指针数组。

如要将二维数组赋给一指针数组:

int *p[3];
int a[3][4];
for(i=0;i<3;i++)
	p[i]=a[i];

这里int *p[3] 表示一个一维数组内存放着三个指针变量,分别是p[0]、p[1]、p[2]
所以要分别赋值。

  • 数组指针(也称行指针)

其次,对于语句“int(*p2)[5]”,“()”的优先级比“[]”高,“*”号和 p2 构成一个指针的定义,指针变量名为 p2,而 int 修饰的是数组的内容,即数组的每个元素。也就是说,p2 是一个指针,它指向一个包含 5 个 int 类型数据的数组。很显然,它是一个数组指针,数组在这里并没有名字,是个匿名数组。

如要将二维数组赋给一指针,应这样赋值:

int a[3][4];
int (*p)[4]; //该语句是定义一个数组指针,指向含4个元素的一维数组。
p=a;    //将该二维数组的首地址赋给p,也就是a[0]或&a[0][0]
p++;    //该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][]

所以数组指针也称指向一维数组的指针,亦称行指针。

面试题4-4:0长度数组

GNU C 的0长度数组, 也叫变长数组, 柔性数组就是这样一个扩展. 对于0长数组的这个特点,很容易构造出变成结构体,如缓冲区,数据包等等:

// 0长度数组
struct zero_buffer
{
  int   len;
  char  data[0];
} __attribute((packed)); 

让编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,这样子两边都需要使用 _attribute_ ((packed))取消优化对齐,就不会出现对齐的错位现象。

这样的变长数组常用于网络通信中构造不定长数据包, 不会浪费空间浪费网络流量, 因为char data[0]; 只是个数组名, 是不占用存储空间的,

即 sizeof(struct zero_buffer) = sizeof(int) = 4 (32位系统下)

长度为0的数组并不占有内存空间, 而指针方式需要占用内存空间;

对于长度为0数组, 在申请内存空间时, 采用一次性分配的原则进行; 对于包含指针的结构体, 才申请空间时需分别进行, 释放时也需分别释放;

对于长度为0的数组的访问可采用数组方式进行。

https://blog.csdn.net/gatieme/article/details/64131322?tdsourcetag=s_pctim_aiomsg

5.函数

面试题5-1:描述函数调用的整个过程。

https://blog.csdn.net/qq_38646470/article/details/79213082

面试题5-2:sizeof和strlen的区别

sizeof是一个操作符,strlen是库函数。

sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为‘\0‘的字符串作参数。

编译器在编译时就计算出了sizeof的结果。而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度。

数组做sizeof的参数不退化,传递给strlen就退化为指针了。

注意:有些是操作符看起来像是函数,而有些函数名看起来又像操作符,这类容易混淆的名称一定要加以区分,否则遇到数组名这类特殊数据类型作参数时就很容易出错。最容易混淆为函数的操作符就是sizeof。

说明:指针是一种普通的变量,从访问上没有什么不同于其他变量的特性。其保存的数值是个整型数据,和整型变量不同的是,这个整型数据指向的是一段内存地址。

面试题5-3:简述strcpy sprintf与mencpy的区别

三者主要有以下不同之处:

(1)操作对象不同,strcpy的两个操作对象均为字符串,sprintf的操作源对象可以是多种数据类型,目的操作对象是字符串,memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。

(2)执行效率不同,memcpy最高,strcpy次之,sprintf的效率最低。

(3)实现功能不同,strcpy主要实现字符串变量间的拷贝,sprintf主要实现其他数据类型格式到字符串的转化,memcpy主要是内存块间的拷贝。

说明:strcpy、sprintf与memcpy都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来选择合适的函数实现拷贝功能。

面试题5-4:数值交换

a=3,b=5,不用第三变量temp,对a和b的值进行交换

如果有第三者temp,a和b交换非常方便:

temp = a;  
a = b;  
b =temp;  

若无temp,可以这样做:

a = a + b;
b = a - b;
a = a - b;

当然,我们可以利用C语言的位运算符:

a = 3;b = 5;  
a ^= b;  
b ^= a;  
a ^= b;  

原理是a ^ b ^ b == a; a ^ b == b ^ a;

面试题5-5:编码实现某位清0或置1

给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清a的bit 3,在以上两个操作中,要保持其他位不变。

笔者认为,在对ARM寄存器操作时会经常用到这一块,所以要注意这块:

#define BIT3 (0x1 << 3 ) Satic int a;  

//设置a的bit 3:   
void set_bit3( void )   
{
	a |= BIT3; //将a第3位置1   
}  

//清a的bit 3   
void set_bit3( void )   
{
	a &= ~BIT3; //将a第3位清零   
}  

说明:在置或清变量或寄存器的某一位时,一定要注意不要影响其他位。所以用加减法是很难实现的。

还有一个就是保留某位:

//保留第k位 
void set_bit3(void) 
{ 
	a &= BIT3;  
} 

一些人喜欢为设置和清除值而定义一个掩码同时定义一些说明常数,这也是可以接受的。我希望看到几个要点:说明常数、|=和&=~操作。

面试题5-6:malloc和calloc

malloc是系统调用函数还是库函数,而系统调用是怎么进入内核的?

malloc用于用户空间堆扩展的函数接口。该函数是C库,属于封装了相关系统调用(brk())的glibc库函数。而不是系统调用(系统可没有sys_malloc()。

http://blog.csdn.net/ordeder/article/details/41654509

malloc和calloc的区别?

1.传递参数不同,malloc函数有1个参数,及申请空间大小;calloc有2个参数,分别为元素的个数和每个元素的大小,这两个参数的乘积就是申请空间的大小。

2.malloc不能初始化申请空间,而calloc可以。

面试题5-7:container of()函数

函数是内置的,执行效率高,速度快;宏可以自己定制,比较灵活,但执行速度相对较慢。

container_of(ptr, type,member)函数的实现包括两部分:

1.判断ptr 与 member 是否为同意类型

2.计算size大小,结构体的起始地址 = (type *)((char *)ptr - size) (注:强转为该结构体指针)

现在我们知道container_of()的作用就是通过一个结构变量中一个成员的地址找到这个结构体变量的首地址。

container_of(ptr,type,member),这里面有ptr,type,member分别代表指针、类型、成员。

6.内存

面试题6-1:简述C、C++程序编译的内存分配情况

C/C++中内存分配方式可以分为三种:

(1)从静态存储区域分配:

内存在程序编译时就已经分配好,这块内存在程序的整个运行期间都存在。速度快、不容易出错,因为有系统会善后。例如全局变量,static变量等。

(2)在栈上分配:

在执行函数时,函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

(3)从堆上分配:

即动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活。如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,另外频繁地分配和释放不同大小的堆空间将会产生堆内碎块。

一个C/C++程序编译时内存分为5大存储区:堆区、栈区、全局区和静态存储区、文字常量区、程序代码区。

1.栈区(stack):存放函数的参数值、局部变量的值。由编译器自动分配释放。除此以外,在函数被调用时,栈用来传递参数和返回值。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。作用域:当前函数,生命周期:函数调用结束

2.堆区(heap):有程序员手动分配和释放。用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc/free等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张)/释放的内存从堆中被剔除(堆被缩减)。作用域:全局(指针传递),生命周期:直道手动释放或程序结束

3.全局区(satic):存放全局变量和静态变量,初始化的全局变量和静态变量在.data区域,未初始化的全局变量和静态变量.bss区域。程序结束由OS释放。

BSS段:(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。

数据段:数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。

作用域:全局(extern引用),生命周期:程序结束

作用域:当前函数,生命周期:程序结束

作用域:全局(当前文件),生命周期:程序结束

4.文字常量区:存放常量字符串.rodata。程序结束由OS释放。

5.程序代码区:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

我们通过代码段来看看静态存储区、栈区与堆区三部分内存需要怎样的操作和不同,以及应该注意怎样的地方。

例一:静态存储区与栈区

char* p = "Hello World1";
char a[] = "Hello World2";
p[2] = ‘A’;
a[2] = ‘A’;
char* p1 = "Hello World1";

这个程序是有错误的,错误发生在p[2] = ‘A’这行代码处,为什么呢,是变量p和变量数组a都存在于栈区的(任何临时变量都是处于栈区的,包括在main()函数中定义的变量)。但是,数据“Hello World1”和数据“Hello World2”是存储于不同的区域的。

因为数据“Hello World2”存在于数组中,所以,此数据存储于栈区,对它修改是没有任何问题的。因为指针变量p仅仅能够存储某个存储空间的地址,数据“Hello World1”为字符串常量,所以存储在静态存储区。虽然通过p[2]可以访问到静态存储区中的第三个数据单元,即字符‘l’所在的存储的单元。但是因为数据“Hello World1”为字符串常量,不可以改变,所以在程序运行时,会报告内存错误。并且,如果此时对p和p1输出的时候会发现p和p1里面保存的地址是完全相同的。换句话说,在数据区只保留一份相同的数据。

例二:栈区与堆区

char* f1()
{
   char* p = NULL;
   char a;
   p = &a;
   return p;
}

char* f2()
{
   char* p = NULL:
   p =(char*) new char[4];
   return p;

}

这两个函数都是将某个存储空间的地址返回,二者有何区别呢?f1()函数虽然返回的是一个存储空间,但是此空间为临时空间。也就是说,此空间只有短暂的生命周期,它的生命周期在函数f1()调用结束时,也就失去了它的生命价值,即:此空间被释放掉。所以,当调用f1()函数时,如果程序中有下面的语句:

char* p ;
p = f1();
*p = ‘a’;

此时,编译并不会报告错误,但是在程序运行时,会发生异常错误。因为,你对不应该操作的内存(即,已经释放掉的存储空间)进行了操作。但是,相比之下,f2()函数不会有任何问题。因为,new这个命令是在堆中申请存储空间,一旦申请成功,除非你将其delete或者程序终结,这块内存将一直存在。也可以这样理解,堆内存是共享单元,能够被多个函数共同访问。如果你需要有多个数据返回却苦无办法,堆内存将是一个很好的选择。但是一定要避免下面的事情发生:

void f()
{char * p;
   p = (char*)new char[100];}

这个程序做了一件很无意义并且会带来很大危害的事情。因为,虽然申请了堆内存,p保存了堆内存的首地址。但是,此变量是临时变量,当函数调用结束时p变量消失。也就是说,再也没有变量存储这块堆内存的首地址,我们将永远无法再使用那块堆内存了。但是,这块堆内存却一直标识被你所使用(因为没有到程序结束,你也没有将其delete,所以这块堆内存一直被标识拥有者是当前您的程序),进而其他进程或程序无法使用。我们将这种不道德的“流氓行为”(我们不用,却也不让别人使用)称为内存泄漏。这是我们C++程序员的大忌!!请大家一定要避免这件事情的发生。

总之,对于堆区、栈区和静态存储区它们之间最大的不同在于,栈的生命周期很短暂。但是堆区和静态存储区的生命周期相当于与程序的生命同时存在(如果您不在程序运行中间将堆内存delete的话),我们将这种变量或数据成为全局变量或数据。但是,对于堆区的内存空间使用更加灵活,因为它允许你在不需要它的时候,随时将它释放掉,而静态存储区将一直存在于程序的整个生命周期中。

面试题6-2:“栈 stack”和“堆 heap”的区别?

1.申请方式
stack: 由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap: 需要程序员自己申请,并指明大小,在c中malloc函数

p1 = (char *)malloc(10); 

2.申请后系统的响应
栈: 只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆: 首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时, 会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序.

3.申请大小的限制
栈: 栈顶的地址和栈的最大容量是系统预先规定好的,是一块连续的内存的区域,在WINDOWS下,栈的大小是2M ,如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。 (有限内存)
堆: 堆是向高地址扩展的数据结构,是不连续的内存区域。(自己分配)

void fun(void)
{
    int a[10];
    int *p = (int *)malloc(10*sizeof(int));
    
    if(p == NULL)
    {
        return;
    }
}

4.申请效率的比较
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

小结:

堆和栈的区别可以用如下的比喻来看出:使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

面试题6-3:嵌入式内存操作

嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务。

这一问题测试你是否知道为了访问一绝对地址把一个整型数强制转换(typecast)为一指针是合法的。这一问题的实现方式随着个人风格不同而不同。典型的类似代码如下:

int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa55;

A more obscure approach is:
一个较晦涩的方法是:

*(int * const)(0x67a9) = 0xaa55;

即使你的品味更接近第二种方案,但我建议你在面试时使用第一种方案。

面试题6-4:什么是MMU,MMU的作用?

MMU是Memory Management Unit(内存管理单元)
1)虚拟内存。有了虚拟内存,可以在处理器上运行比实际物理内存大的应用程序。为了使用虚拟内存,操作系统通常要设置一个交换分区(通常是硬盘),通过将不活跃的内存中的数据放入交换分区,操作系统可以腾出其空间来为其它的程序服务。虚拟内存是通过虚拟地址来实现的。
2)内存保护。根据需要对特定的内存区块的访问进行保护,通过这一功能,我们可以将特定的内存块设置成只读、只写或是可同时读写。

面试题6-5:堆栈溢出和内存溢出

1.堆栈溢出:
由于过多的函数调用,导致调用堆栈无法容纳这些调用的返回地址,一般在递归中产生。堆栈溢出很可能是由无限递归产生,也有可能是过多的堆栈层级。

2.内存溢出:
程序申请的内存大于系统当前内存的可用空间。

3.内存泄漏:
程序申请内存后,没有释放已申请的内存空间。

4.指针访问越界:
指针访问了申请内存空间以外的数据。

面试题6-6:共享内存和全局变量的区别

共享内存用于跨进程,全局变量没法跨进程。

MMU把各个进程的虚拟内存映射到不同的物理内存上,这样就保证了进程的虚拟内存是独立的。然而,物理内存往往远远少于各个进程的虚拟内存的总和。怎么办呢,通常的办法是把暂时不用的内存写到磁盘上去,要用的时候再加载回内存中来。一般会搞一个专门的分区保存内存数据,这就是所谓的交换分区。

共享内存设置为4KB。

7.其他

面试题7-1:运算符

C语言中,运算对象必须是整型数的运算符的有()

A、% B、/ C、%和/ D、*

答案:A

取余对象必须是整数型。

运算符优先级

前述运算符 > 单目运算符 > 算术运算符 > 移位运算符 > 关系运算符 > 逻辑运算符 > 条件运算符 > 赋值运算符 > 逗号运算符

1.前述运算符

(),[],->,.

2.单目运算符

!,~,++,–,+,-,*,&,(类型转换),sizeof()

3.算术运算符
*,/,%
+,-

4.移位运算
<<,>>

5.关系运算
<,<=,>,>=
==,!=

6.逻辑运算
&
^
|
&&
||

7.条件运算
?:

8.赋值运算
=,+=,-=,*=,/=,&=,^=,|=,<<=.>>=

9.逗号运算
,

面试题7-2. 请问TCP/IP协议分为哪几层?FTP协议在哪一层?

ISO/OSI的参考模型共有7层,由低层至高层分别为:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。

物理层:在物理媒体上传输原始的数据比特流。

数据链路层:将数据分成一个个数据帧,以数据帧为单位传输。有应有答,遇错重发。

网络层:将数据分成一定长度的分组,将分组穿过通信子网。

传输层:提供不具体网络的高效、经济、透明的端到端数据传输服务。

会话层:进程间的对话也称为会话,会话层管理不同主机上各进程间的对话。

表示层: 为应用层进程提供格式化的表示和转换数据服务。

应用层:提供应用程序访问OSI环境的手段。

应用层:TELNET、FTP、TFTP、SMTP、SNMP、HTTP、BOOTP、DHCP、DNS
表示层:
文本:ASCII,EBCDIC
图形:TIFF,JPEG,GIF,PICT
声音:MIDI,MPEG,QUICKTIME

会话层:NFS、SQL、RPC 、X-WINDOWS、ASP(APPTALK会话协议)、SCP

传输层:TCP、UDP、SPX

网络层:IP、IPX、ICMP、RIP、OSPF(Open Shortest Path First开放式最短路径优先)

数据链路层:SDLC、HDLC、PPP、STP(Spanning Tree Protocol)、帧中继

物理层:EIA/TIA RS-232、EIA/TIA RS-449、V.35、RJ-45

面试题7-3:在网络应用中,函数htons,htonl,ntohs,ntohl的作用是什么?

uint32_t htonl(uint32_t hostlong);//32位的主机字节序转换到网络字节序
uint16_t htons(uint16_t hostshort);//16位的主机字节序转换到网络字节序
uint32_t ntohl(uint32_t netlong);//32位的网络字节序转换到主机字节序
uint16_t ntohs(uint16_t netshort);//16位的网络字节序转换到主机字节序
(皆为大小端的改变)

面试题7-4:IPv4的IP地址分类。请写出B和C类地址的范围和掩码,D类地址的用途是什么?

A类IP地址范围:0.0.0.0到127.255.255.255
B类IP地址范围:128.0.0.0到191.255.255.255
C类IP地址范围:192.0.0.0到223.255.255.255
D类IP地址范围:224.0.0.0到239.255.255.255
E类IP地址范围:224.0.0.0到254.255.255.255

D类地址用于多点播送
E类地址保留,仅作实验和开发用

全零(“0.0.0.0”)地址指任意网络。
全“1”的IP地址(“255.255.255.255”)是当前子网的广播地址。

面试题7-5:中断嵌入式

中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准C支持中断。具代表事实是,产生了一个新的关键字 __interrupt。下面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。

__interrupt double compute_area (double radius) 
 {
     double area = PI * radius * radius;
     printf("\nArea = %f", area);
     return area;
 }

这个函数有太多的错误了,以至让人不知从何说起了:
1)ISR 不能返回一个值。如果你不懂这个,那么你不会被雇用的。

  1. ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。

  2. 在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。

  3. 与第三点一脉相承,printf()经常有重入和性能上的问题。如果你丢掉了第三和第四点,我不会太为难你的。不用说,如果你能得到后两点,那么你的被雇用前景越来越光明了。

面试题7-6:嵌入式位操作

嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在以上两个操作中,要保持其它位不变。

对这个问题有三种基本的反应

1)不知道如何下手。该被面者从没做过任何嵌入式系统的工作。

  1. 用bit fields。Bit fields是被扔到C语言死角的东西,它保证你的代码在不同编译器之间是不可移植的,同时也保证了的你的代码是不可重用的。我最近不幸看到 Infineon为其较复杂的通信芯片写的驱动程序,它用到了bit fields因此完全对我无用,因为我的编译器用其它的方式来实现bit fields的。从道德讲:永远不要让一个非嵌入式的家伙粘实际硬件的边。

  2. 用 #defines 和 bit masks 操作。这是一个有极高可移植性的方法,是应该被用到的方法。最佳的解决方案如下:

#define BIT3 (0x1 << 3)

static int a;

void set_bit3(void) 
{
     a |= BIT3;
}
void clear_bit3(void) 
{
    a &= ~BIT3;
}

一些人喜欢为设置和清除值而定义一个掩码同时定义一些说明常数,这也是可以接受的。我希望看到几个要点:说明常数、|=和&=~操作。

访问固定的内存位置(Accessing fixed memory locations)

面试题7-7:程序编译过程

源代码-->预处理-->编译-->优化-->汇编-->链接–>可执行文件

  1. 预处理
    

读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。包括宏定义替换、条件编译指令、头文件包含指令、特殊符号。 预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。.i预处理后的c文件,.ii预处理后的C++文件。

预处理过程(头文件的包涵,去掉注释,宏展开)—#include 预处理过程不做语法检查
命令:gcc -E helloworld.c -o helloworld.i

  1. 编译阶段
    

编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。.s文件

编译:编译过程做语法检查 生成汇编语言
命令:gcc -S helloworld.i -o helloworld.s

  1. 汇编过程
    

汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。.o目标文件

汇编:将汇编语言生成对应的二进制数据
命令:gcc -c helloworld.s -o helloworld.o

  1. 链接阶段
    

链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。

链接:添加对应操作系统可以执行的链接,否则无法在系统下运行
命令:gcc helloworld.o -o helloworld

参考:https://blog.csdn.net/sunzz93/article/details/77507980?utm_source=copy

面试题7-8:头文件

头文件的作用是什么?哪些东西不能放进头文件中?

在C语言家族程序中,头文件被大量使用。一般而言,每个C++/C程序通常由头文件(header files)和定义文件(definition files)组成。头文件作为一种包含功能函数、数据接口声明的载体文件,用于保存程序的声明(declaration),而定义文件用于保存程序的实现 (implementation)。而且 .c就是你写的程序文件。

http://blog.csdn.net/wzjemb/article/details/7451978

面试题7-9:gcc和g++区别

1.后缀为.c的,gcc把它当作是C程序,而g++当作是c++程序;后缀为.cpp的,两者都会认为是c++程序。

2.编译阶段,g++会调用gcc,对于c++代码,两者是等价的,但是因为gcc命令不能自动和C++程序使用的库联接,所以通常用g++来完成链接,为了统一起见,干脆编译/链接统统用g++了,这就给人一种错觉,好像cpp程序只能用g++似的。

编译可以用gcc/g++,而链接可以用g++或者gcc -lstdc++。因为gcc命令不能自动和C++程序使用的库联接,所以通常使用g++来完成联接。但在编译阶段,g++会自动调用gcc,二者等价。

3、无论是gcc还是g++,用extern "c"时,都是以C的命名方式来为symbol命名,否则,都以c++方式命名。

8.链接装载库

8.1编译链接

各平台文件格式

平台 可执行文件 目标文件 动态库/共享对象 静态库
Windows exe obj dll lib
Unix/Linux ELF、out o so a
Mac Mach-O o dylib、tbd、framework a、framework

编译链接过程

  1. 预编译(预编译器处理如 #include#define 等预编译指令,生成 .i.ii 文件)
  2. 编译(编译器进行词法分析、语法分析、语义分析、中间代码生成、目标代码生成、优化,生成 .s 文件)
  3. 汇编(汇编器把汇编码翻译成机器码,生成 .o 文件)
  4. 链接(连接器进行地址和空间分配、符号决议、重定位,生成 .out 文件)

现在版本 GCC 把预编译和编译合成一步,预编译编译程序 cc1、汇编器 as、连接器 ld

MSVC 编译环境,编译器 cl、连接器 link、可执行文件查看器 dumpbin

目标文件

编译器编译源代码后生成的文件叫做目标文件。目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。

可执行文件(Windows 的 .exe 和 Linux 的 ELF)、动态链接库(Windows 的 .dll 和 Linux 的 .so)、静态链接库(Windows 的 .lib 和 Linux 的 .a)都是按照可执行文件格式存储(Windows 按照 PE-COFF,Linux 按照 ELF)

目标文件格式
  • Windows 的 PE(Portable Executable),或称为 PE-COFF,.obj 格式
  • Linux 的 ELF(Executable Linkable Format),.o 格式
  • Intel/Microsoft 的 OMF(Object Module Format)
  • Unix 的 a.out 格式
  • MS-DOS 的 .COM 格式

PE 和 ELF 都是 COFF(Common File Format)的变种

目标文件存储结构
功能
File Header 文件头,描述整个文件的文件属性(包括文件是否可执行、是静态链接或动态连接及入口地址、目标硬件、目标操作系统等)
.text section 代码段,执行语句编译成的机器代码
.data section 数据段,已初始化的全局变量和局部静态变量
.bss section BSS 段(Block Started by Symbol),未初始化的全局变量和局部静态变量(因为默认值为 0,所以只是在此预留位置,不占空间)
.rodata section 只读数据段,存放只读数据,一般是程序里面的只读变量(如 const 修饰的变量)和字符串常量
.comment section 注释信息段,存放编译器版本信息
.note.GNU-stack section 堆栈提示段

其他段略

链接的接口————符号

在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。

如下符号表(Symbol Table):

Symbol(符号名) Symbol Value (地址)
main 0x100
Add 0x123

8.2Linux 的共享库(Shared Library)

Linux 下的共享库就是普通的 ELF 共享对象。

共享库版本更新应该保证二进制接口 ABI(Application Binary Interface)的兼容

命名

libname.so.x.y.z

  • x:主版本号,不同主版本号的库之间不兼容,需要重新编译
  • y:次版本号,高版本号向后兼容低版本号
  • z:发布版本号,不对接口进行更改,完全兼容

路径

大部分包括 Linux 在内的开源系统遵循 FHS(File Hierarchy Standard)的标准,这标准规定了系统文件如何存放,包括各个目录结构、组织和作用。

  • /lib:存放系统最关键和最基础的共享库,如动态链接器、C 语言运行库、数学库等
  • /usr/lib:存放非系统运行时所需要的关键性的库,主要是开发库
  • /usr/local/lib:存放跟操作系统本身并不十分相关的库,主要是一些第三方应用程序的库

动态链接器会在 /lib/usr/lib 和由 /etc/ld.so.conf 配置文件指定的,目录中查找共享库

环境变量

  • LD_LIBRARY_PATH:临时改变某个应用程序的共享库查找路径,而不会影响其他应用程序
  • LD_PRELOAD:指定预先装载的一些共享库甚至是目标文件
  • LD_DEBUG:打开动态链接器的调试功能

so 共享库的编写

使用 CLion 编写共享库

创建一个名为 MySharedLib 的共享库

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MySharedLib)

set(CMAKE_CXX_STANDARD 11)

add_library(MySharedLib SHARED library.cpp library.h)

library.h

#ifndef MYSHAREDLIB_LIBRARY_H
#define MYSHAREDLIB_LIBRARY_H

// 打印 Hello World!
void hello();

// 使用可变模版参数求和
template <typename T>
T sum(T t)
{
    return t;
}
template <typename T, typename ...Types>
T sum(T first, Types ... rest)
{
    return first + sum<T>(rest...);
}

#endif

library.cpp

#include <iostream>
#include "library.h"

void hello() {
    std::cout << "Hello, World!" << std::endl;
}

so 共享库的使用(被可执行项目调用)

使用 CLion 调用共享库

创建一个名为 TestSharedLib 的可执行项目

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(TestSharedLib)

# C++11 编译
set(CMAKE_CXX_STANDARD 11)

# 头文件路径
set(INC_DIR /home/xx/code/clion/MySharedLib)
# 库文件路径
set(LIB_DIR /home/xx/code/clion/MySharedLib/cmake-build-debug)

include_directories(${INC_DIR})
link_directories(${LIB_DIR})
link_libraries(MySharedLib)

add_executable(TestSharedLib main.cpp)

# 链接 MySharedLib 库
target_link_libraries(TestSharedLib MySharedLib)

main.cpp

#include <iostream>
#include "library.h"
using std::cout;
using std::endl;

int main() {

    hello();
    cout << "1 + 2 = " << sum(1,2) << endl;
    cout << "1 + 2 + 3 = " << sum(1,2,3) << endl;

    return 0;
}

执行结果

Hello, World!
1 + 2 = 3
1 + 2 + 3 = 6

8.3 Windows 应用程序入口函数

  • GUI(Graphical User Interface)应用,链接器选项:/SUBSYSTEM:WINDOWS
  • CUI(Console User Interface)应用,链接器选项:/SUBSYSTEM:CONSOLE
_tWinMain 与 _tmain 函数声明
Int WINAPI _tWinMain(
    HINSTANCE hInstanceExe,
    HINSTANCE,
    PTSTR pszCmdLine,
    int nCmdShow);

int _tmain(
    int argc,
    TCHAR *argv[],
    TCHAR *envp[]);
应用程序类型 入口点函数 嵌入可执行文件的启动函数
处理ANSI字符(串)的GUI应用程序 _tWinMain(WinMain) WinMainCRTSartup
处理Unicode字符(串)的GUI应用程序 _tWinMain(wWinMain) wWinMainCRTSartup
处理ANSI字符(串)的CUI应用程序 _tmain(Main) mainCRTSartup
处理Unicode字符(串)的CUI应用程序 _tmain(wMain) wmainCRTSartup
动态链接库(Dynamic-Link Library) DllMain _DllMainCRTStartup

8.3 Windows 的动态链接库(Dynamic-Link Library)

知识点来自《Windows核心编程(第五版)》

用处

  • 扩展了应用程序的特性
  • 简化了项目管理
  • 有助于节省内存
  • 促进了资源的共享
  • 促进了本地化
  • 有助于解决平台间的差异
  • 可以用于特殊目的

注意

  • 创建 DLL,事实上是在创建可供一个可执行模块调用的函数
  • 当一个模块提供一个内存分配函数(malloc、new)的时候,它必须同时提供另一个内存释放函数(free、delete)
  • 在使用 C 和 C++ 混编的时候,要使用 extern “C” 修饰符
  • 一个 DLL 可以导出函数、变量(避免导出)、C++ 类(导出导入需要同编译器,否则避免导出)
  • DLL 模块:cpp 文件中的 __declspec(dllexport) 写在 include 头文件之前
  • 调用 DLL 的可执行模块:cpp 文件的 __declspec(dllimport) 之前不应该定义 MYLIBAPI

加载 Windows 程序的搜索顺序

  1. 包含可执行文件的目录
  2. Windows 的系统目录,可以通过 GetSystemDirectory 得到
  3. 16 位的系统目录,即 Windows 目录中的 System 子目录
  4. Windows 目录,可以通过 GetWindowsDirectory 得到
  5. 进程的当前目录
  6. PATH 环境变量中所列出的目录

DLL 入口函数

DllMain 函数
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch(fdwReason)
    {
    case DLL_PROCESS_ATTACH:
        // 第一次将一个DLL映射到进程地址空间时调用
        // The DLL is being mapped into the process' address space.
        break;
    case DLL_THREAD_ATTACH:
        // 当进程创建一个线程的时候,用于告诉DLL执行与线程相关的初始化(非主线程执行)
        // A thread is bing created.
        break;
    case DLL_THREAD_DETACH:
        // 系统调用 ExitThread 线程退出前,即将终止的线程通过告诉DLL执行与线程相关的清理
        // A thread is exiting cleanly.
        break;
    case DLL_PROCESS_DETACH:
        // 将一个DLL从进程的地址空间时调用
        // The DLL is being unmapped from the process' address space.
        break;
    }
    return (TRUE); // Used only for DLL_PROCESS_ATTACH
}

载入卸载库

LoadLibrary、LoadLibraryExA、LoadPackagedLibrary、FreeLibrary、FreeLibraryAndExitThread 函数声明
// 载入库
HMODULE WINAPI LoadLibrary(
  _In_ LPCTSTR lpFileName
);
HMODULE LoadLibraryExA(
  LPCSTR lpLibFileName,
  HANDLE hFile,
  DWORD  dwFlags
);
// 若要在通用 Windows 平台(UWP)应用中加载 Win32 DLL,需要调用 LoadPackagedLibrary,而不是 LoadLibrary 或 LoadLibraryEx
HMODULE LoadPackagedLibrary(
  LPCWSTR lpwLibFileName,
  DWORD   Reserved
);

// 卸载库
BOOL WINAPI FreeLibrary(
  _In_ HMODULE hModule
);
// 卸载库和退出线程
VOID WINAPI FreeLibraryAndExitThread(
  _In_ HMODULE hModule,
  _In_ DWORD   dwExitCode
);

显示地链接到导出符号

GetProcAddress 函数声明
FARPROC GetProcAddress(
  HMODULE hInstDll,
  PCSTR pszSymbolName  // 只能接受 ANSI 字符串,不能是 Unicode
);

DumpBin.exe 查看 DLL 信息

VS 的开发人员命令提示符 使用 DumpBin.exe 可查看 DLL 库的导出段(导出的变量、函数、类名的符号)、相对虚拟地址(RVA,relative virtual address)。如:

DUMPBIN -exports D:\mydll.dll

DLL 库的编写(导出一个 DLL 模块)

DLL 库的编写(导出一个 DLL 模块)

DLL 头文件

// MyLib.h

#ifdef MYLIBAPI

// MYLIBAPI 应该在全部 DLL 源文件的 include "Mylib.h" 之前被定义
// 全部函数/变量正在被导出

#else

// 这个头文件被一个exe源代码模块包含,意味着全部函数/变量被导入
#define MYLIBAPI extern "C" __declspec(dllimport)

#endif

// 这里定义任何的数据结构和符号

// 定义导出的变量(避免导出变量)
MYLIBAPI int g_nResult;

// 定义导出函数原型
MYLIBAPI int Add(int nLeft, int nRight);

DLL 源文件

// MyLibFile1.cpp

// 包含标准Windows和C运行时头文件
#include <windows.h>

// DLL源码文件导出的函数和变量
#define MYLIBAPI extern "C" __declspec(dllexport)

// 包含导出的数据结构、符号、函数、变量
#include "MyLib.h"

// 将此DLL源代码文件的代码放在此处
int g_nResult;

int Add(int nLeft, int nRight)
{
    g_nResult = nLeft + nRight;
    return g_nResult;
}

DLL 库的使用(运行时动态链接 DLL)

DLL 库的使用(运行时动态链接 DLL)
// A simple program that uses LoadLibrary and 
// GetProcAddress to access myPuts from Myputs.dll. 
 
#include <windows.h> 
#include <stdio.h> 
 
typedef int (__cdecl *MYPROC)(LPWSTR); 
 
int main( void ) 
{ 
    HINSTANCE hinstLib; 
    MYPROC ProcAdd; 
    BOOL fFreeResult, fRunTimeLinkSuccess = FALSE; 
 
    // Get a handle to the DLL module.
 
    hinstLib = LoadLibrary(TEXT("MyPuts.dll")); 
 
    // If the handle is valid, try to get the function address.
 
    if (hinstLib != NULL) 
    { 
        ProcAdd = (MYPROC) GetProcAddress(hinstLib, "myPuts"); 
 
        // If the function address is valid, call the function.
 
        if (NULL != ProcAdd) 
        {
            fRunTimeLinkSuccess = TRUE;
            (ProcAdd) (L"Message sent to the DLL function\n"); 
        }
        // Free the DLL module.
 
        fFreeResult = FreeLibrary(hinstLib); 
    } 

    // If unable to call the DLL function, use an alternative.
    if (! fRunTimeLinkSuccess) 
        printf("Message printed from executable\n"); 

    return 0;
}

欢迎访问我的网站:

BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
CSDN博客

接收更多精彩文章及资源推送,请订阅我的微信公众号:

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/u013162035/article/details/105358428