文章目录
1.Linux环境C/C++代码的编译和运行
1.1 C/C++代码编译到执行的过程

- 预处理:
gcc -o hello.i hello.cpp,获得预处理后的文件。 - 编译:
gcc -o hello.s hello.i,获得编译之后的文件。 - 汇编:
gcc -o hello.o hello.s,获得汇编后的文件。 - 链接:
gcc -o hello hello.o,链接生成可执行文件。 - 一步到位:
gcc -o hello hello.cpp ; ./hello;。 - 支持C++11的方法:
alias g++='g++ -std=c++11'
1.2 静态链接和动态链接的区别
- 链接时机:静态链接发生在程序编译时期;动态链接发生在程序运行时期。
- 内存消耗:静态链接每个可执行程序所有的目标文件都需要一个副本,浪费内存空间;动态链接多个可执行程序的所有目标文件共享同一个副本,节约内存空间。
- 稳定性:静态链接模块更新麻烦,不利于程序拓展;动态链接模块更新速度快,有利于程序拓展。
1.3 静态库和动态库
1.3.1 背景
- 公用函数库的程序文件
public.cpp程序文件是源代码,对任何程序员是可见的,没有安全性可言。但是在实际开发中,出于技术保密或其它方面考虑,开发者并不希望提供公用函数库的源代码。因此C/C++提供了一个可以保证代码安全性的方法,把公共的程序文件编译成库文件,库文件是一种可执行代码的二进制形式,可以与其它的源程序一起编译,也可以被操作系统载入内存执行,库文件分为静态库与动态库。
1.3.2 静态库
- 静态库在编译的时候,主程序与静态库一起编译,把主程序与主程序中用到的库函数一起整合进目标文件。这样做优点是在编译后的可执行程序可以独立运行,因为所使用的函数都已经被编译进去了。缺点是如果所使用的静态库发生更新改变,我们的程序必须重新编译。
- 静态库文件名的命名方式是
libxxx.a,库名前加lib,后缀用.a,xxx为静态库名。把程序文件public.cpp编译成静态库的指令:g++ -c -o libpublic.a public.cpp(-c不可省略)。使用方法如下:
使用静态库的方法一,直接把调用者源代码和静态库一起编译。
g++ -o main main.cpp libpublic.a
使用静态库的方法二,采用L参数指定静态库的目录,-l参数指定静态库名。
g++ -o main main.cpp -L/目录 -l库名
- 如果要指定多个静态库的目录,用法是
-L/目录1 -L目录2 -L目录3;链接库的文件名是libpublic.a,但链接库名是public;如果要指定多个静态库,用法是-l库名1 -l库名2 -l库名3。
1.3.3 动态库
- 动态库在编译时并不会被连接到目标代码中,而是在程序运行时才被载入,因此在程序运行时还需要指定动态库的目录。动态库的命名方式与静态库类似,前缀相同为
lib,后缀变为.so,xxx为动态库名。把程序文件public.cpp编译成动态库的指令:g++ -fPIC -shared -o libpublic.so public.cpp,使用动态库的方法与使用静态库的方法相同。 - 如果在动态库文件和静态库文件同时存在,优先使用动态库编译,
g++ -o main main.cpp -L/目录 -l库名。这是因为采用动态链接库的可执行程序在运行时需要指定动态库文件的目录。Linux系统中采用LD_LIBRARY_PATH环境变量指定动态库文件的目录。采用以下命令设置LD_LIBRARY_PATH环境变量:export LD_LIBRARY_PATH=/home/wucz/demo。注意:1)如果要指定多个动态库文件的目录,用法是export LD_LIBRARY_PATH=目录1:目录2:目录3:.,目录之间用半角的冒号分隔,最后的圆点指当前目录。
1.3.4 静态库的优缺点
- 优点:
静态链接相当于复制一份库文件到可执行程序中,不需要像动态库那样有动态加载和识别函数地址的开销,也就是说采用静态链接编译的可执行程序运行更快。
- 缺点:
1)静态链接生成的可执行程序比动态链接生成的大很多,运行时占用的内存也更多。
2)库文件的更新不会反映到可执行程序中,可执行程序需要重新编译。
1.3.5 动态库的优缺点
- 优点:
1)相对于静态库,动态库在时候更新(修复bug,增加新的功能),不需要重新编译。
2)全部的可执行程序共享动态库的代码,运行时占用的内存空间更少。
- 缺点:
1)使可执行程序在不同平台上移植变得更复杂,因为它需要为每每个不同的平台提供相应平台的共享库。
2)增加可执行程序运行时的时间和空间开销,因为应用程序需要在运行过程中查找依赖的库函数,并加载到内存中。
1.4 在main执行之前和之后
1.4.1 main函数执行之前,主要就是初始化系统相关资源:
- 设置栈指针。
- 将未初始化部分的全局变量赋初值,数值型short,int,long等为0,bool为false,指针为NULL等等,即.bss段的内容。
- 初始化静态变量和全局变量,即.data段的内容。
- 全局对象初始化。
- 将main函数的参数argc,argv等传递给main函数。
_attribute_((constructor))修饰的函数。
1.4.2 main函数执行之后:
- 全局对象的析构函数。
_attribute_((destructor))修饰的函数。- 用
atexit注册的函数。
2.宏定义
2.1 宏定义和函数的区别
- 宏定义属于在结构中插入代码,没有返回值;函数调用有返回值。
- 宏在预处理阶段完成替换,被替换的文本参与编译,不存在调用,执行起来更快;函数运行时存在函数调用。
- 宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要进行类型检查。
- 宏定义不能在最后加分号。
2.2 宏定义和内联函数的区别
- 内联函数和普通函数相比可以加快程序运行的速度。因为不需要中断调用,在编译的时候内联函数可以直接嵌入到目标代码中。
- 内联函数适用场景:
a.使用宏定义的地方都可以使用inline函数。
b.作为类成员接口函数来读写类的私有成员或者保护成员,会提高效率。
- 为什么不能把所有的函数写成内联函数:内联函数以代码复杂为代价,它以省去函数调用的开销来提高执行效率。所以一方面如果内联函数体内代码执行时间相比函数调用开销大,则没有太大的意义;另一方面每一处内联函数的调用都要复制代码,消耗更多的内存空间,因此以下情况不宜使用内联函数:
a.函数体内有循环,函数执行时间要比函数调用开销大。
b.函数体内的代码比较长,将导致内存消耗代价。
- 主要区别:
a.宏在预处理时展开,内联函数在编译时展开。
b.宏是简单的做文本替换,没有类型检查,内联函数直接嵌入到目标代码中,有类型检测、语法判断等功能。
c.内联函数是函数,有返回值;而宏不是函数,没有返回值。
e.宏定义时要注意书写(参数要括起来)否则容易出现歧义,内联函数不会产生歧义。
f.内联函数代码是被放到符号表中,使用时像宏一样展开,没有调用的开销,效率很高。
g.内联函数可以作为某个类的成员函数,这样可以使用类的保护成员和私有成员,进而提升效率。而当一个表达式涉及到类保护成员或私有成员时,宏就不能实现了。
2.3 宏定义和typedef的区别
- 宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。
- 宏替换发生在预处理阶段,属于文本插入替换,不进行类型检查;typedef是编译的一部分,需要进行类型检查。
- 宏不是语句,不能在最后加分号;typedef是语句,要加分号标识结束。
2.4 宏定义和const的区别
- 编译阶段:define 在编译的预处理阶段起作用,而const是在编译、运行的时候起作用。
- 安全性:define 只做替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住全部的内容,要不然很容易出错;const常量有数据类型,编译器可以对其进行类型安全检查。
- 内存占用:宏定义的数据只是插入替换掉,没有分配内存空间;const定义的变量只是值不能改变,但要分配内存空间,能将复杂的的表达式计算出结果放入常量表。
2.5 说一下你理解的ifdef和endif代表什么
- 一般情况下,源程序中所有行都参加编译。但是有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是条件编译。有时,希望当某条件满足时对一组语句进行编译,而不满足时则编译另一组语句。
- 条件编译命令最常见的形式为:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2。
1) #ifdef 标识符
程序段1
#else
程序段2
#endif
2)其中#else部分也可以没有,即:
#ifdef
程序段1
#endif
- 在一个大的软件工程里面,可能会有多个文件同时包含一个头文件,当这些文件编译链接成一个可执行文件上时,就会出现大量重定义错误。在头文件中使用#define、#ifndef、#ifdef、#endif能避免头文件重定义。
3.头文件其它题目
3.1 extern"C"的用法
- 为了能够正确的在C++代码中调用C语言的代码。在程序中加上
extern "C"后,相当于告诉编译器这部分代码是C语言写的,因此要按照C语言进行编译而不是C++。哪些情况下使用extern “C”:
a.C++代码中调用C语言代码;
b.在C++中的头文件中使用;
c.在多
个人协同开发时,可能有人擅长C语言,而有人擅长C++;
3.2 C++中#include中<>和" "的区别
- #include“”优先从自己定义的源文件中查找,找不到之后才去标准库文件中查找。
- #include<>优先从引入的标准库文件中查找。<>里面一般都放标准库.h。
4.C++抛出异常的三种方法
- try、throw和catch关键字。
- 函数的异常声明列表:程序员在定义函数的时候知道函数可能发生的异常,可以在函数声明和定义时,指出所能抛出异常的列表,写法如下:
int fun() throw(int,double,A,B,C){...};。这种写法表名函数可能会抛出int,double型或者A、B、C三种类型的异常,如果throw为空,表明不会抛出任何异常;如果throw不为空,则可能抛出任何异常。 - C++标准异常类exception:C++ 标准库中有一些类代表异常,这些类都是从exception类派生而来的。

bad_typeid:使用typeid运算符,如果其操作数是一个多态类的指针,而该指针的值为NULL,则会拋出此异常。
bad_cast:在用 dynamic_cast 进行从多态基类对象(或引用)到派生类的引用的强制类型转换时,如果转换是不安全的,则会拋出此异常。
bad_alloc:在用new运算符进行动态内存分配时,如果没有足够的内存,则会引发此异常。
out_of_range:用 vector或string的at成员函数根据下标访问元素时,如果下标越界,则会拋出此异常。
5.将字符串“hello world”从开始到打印到屏幕上的全过程
- 用户告诉操作系统执行HelloWorld程序(通过键盘输入等)。
- 操作系统:找到helloworld程序的相关信息,检查其类型是否是可执行文件;并通过程序首部信息,确定代码和数据在可执行文件中的位置并计算出对应的磁盘块地址。
- 操作系统:创建一个新进程,将HelloWorld可执行文件映射到该进程结构,表示由该进程执行helloworld程序。
- 操作系统:为helloworld程序设置cpu上下文环境,并跳到程序开始处。
- 执行helloworld程序的第一条指令,发生缺页异常。
- 操作系统:分配一页物理内存,并将代码从磁盘读入内存,然后继续执行helloworld程序。
- helloword程序执行puts函数(系统调用),在显示器上写一字符串。
- 操作系统:找到要将字符串送往的显示设备,通常设备是由一个进程控制的,所以,操作系统将要写的字符串送给该进程。
- 操作系统:控制设备的进程告诉设备的窗口系统,它要显示该字符串,窗口系统确定这是一个合法的操作,然后将字符串转换成像素,将像素写入设备的存储映像区。
- 视频硬件将像素转换成显示器可接收和一组控制数据信号。
- 显示器解释信号,激发液晶屏。
6.比较++i和i++
- 前置++i:返回引用,不会产生临时对象。++i实现代码为:
int &operator++(){
*this += 1;
return *this;
}
- 后置i++:返回对象,必须产生临时对象,临时对象会导致效率降低。i++实现代码为:
int operator++(int){
int temp = *this;
++*this;
return temp;
}
7.解决两个库中相同函数冲突
- 可以采用
dlopen方式。打开指定库,然后引用其中的函数指针,也就是动态指定函数指针来实现。以ffmpeg库为例子,第三方引用并修改了老版本的ffmpeg库,由于升级起来麻烦(很多文件是由第三方维护);而自己加的功能又只能用新的官方的ffmpeg库,所以可以把新的ffmpeg库编译成动态库,而代码中采用dlopen方式打开动态库,引用新的接口。
8.printf函数
8.1 printf实现原理
- 缓冲区,例如行缓冲。
- 解析%d格式或者其它格式。
- 调用write系统调用完成实际输出。
8.2 执行while(true);和while(true) printf(“xxxx”); 区别
- 前者CPU利用率一直保持100%,后者需要调用write系统调用,CPU的利用率降低。
9.字节对齐为什么可以提高访问速度?
- 如果地址没有使用字节对齐,那么访问时候,就有可能需要使用两条 Cache Line,增加对Cache的使用量;而且使用的数据没有在Cache里面的比例会增加,降低程序的速度。