笔者前段时间负责了部门应届生的面试工作,在面试过程中,发现很多学生C/C++应用较多,甚至参与过一些大的软件项目的开发,但是却缺乏理论,希望本系列能够为即将毕业的同学们提供帮助。
——本文会持续更新,今天先写这么多。
目录
C++的编译和链接
1 简单的hello word程序
2 Hello word的ELF文件
3 多个C++文件链接
内存分布问题
从内存的角度看链接问题
从编译的角度看多态
从内存角度看多态
从内存角度看虚函数
从内存角度看父类与子类的指针
在编程阶段,经常报错的一个最简单的程序,分为编译和链接两部分,本节解释从简单的例子解释这几个概念。
老生常谈,继续从hello word说起
#include
int main(void)
{
printf("hello word\n");
}
将上述文件保存为一个C++文件,命名为HelloWord.cpp,然后使用g++编译
gcc heloword.cpp
a.out
输出为:
hello word
上述命令较为简单,从helloword.cpp文件直接生成了可执行文件a.out,没有展示编译的具体过程,一个编译过程分为预处理、编译、汇编、可执行文件生成几个步骤,对过程感兴趣的,可以分别使用如下命令:
gcc -E helloword.cpp >helloword_Extend.cpp //预处理后的文件
gcc -c helloword.cpp //只执行编译命令,生成.o文件
gcc -S helloword.cpp //生成汇编文件
也可以按喜好修改生成的可执行文件
gcc -o helloword.bin helloword.cpp
如果需要分析一个可执行文件,我们一般使用readelf命令,如使用readelf -h a.out读取文件头:
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: DYN (共享目标文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x530
程序头起点: 64 (bytes into file)
Start of section headers: 6448 (bytes into file)
标志: 0x0
本头的大小: 64 (字节)
程序头大小: 56 (字节)
Number of program headers: 9
节头大小: 64 (字节)
节头数量: 29
字符串表索引节头: 28
ELF文件的头部Magic存储了一组数字,称为魔数,有兴趣的可以搜索魔数相关的内容
很多学生在回答这个问题的时候,可以轻易把概念说出来,对该问题的来源、原理、结果却没有相应的认识,多文件链接,首先解决的是,模块间的协作问题,这个协作,一是让别的模块知道本模块对外提供的数据和接口(变量、函数、宏定义等),二是在最终生成的文件中,能找到相应元素,找到相应符号,这两个过程分别对应到编译和链接阶段。
我们把helloword问题路略微作一个扩展,准备两个文件,1.cpp和linkTest.cpp
1.cpp:
#include
void func1(void)
{
printf("This is 1.c func1\n");
}
linkTest.cpp:
#include
extern void func1(void);
int main(void)
{
printf("Call func1\n");
func1();
}
gcc 1.cpp linkTest.cpp -o linkTest
linkTest输出
Call func1
This is 1.c func1
上述过程,是一个最简单的跨文件函数调用,因为我们有func1函数的源码,所以可以随心所欲的使用,如果因为某些原因,我们不想提供func1的源代码,引申出下面的几个问题:
1、1.cpp文件编译成静态库,如何调用
2、1.cpp文件编译成动态库,如何调用
3、静态库和动态库对于生成的linkTest目标文件到底有什么区别
4、程序执行阶段,main函数是如何找到func1函数的
执行如下命令
gcc -c 1.cpp
ar -rc 1.a 1.o(ar直接使用cpp生成.a文件可能导致符号表错误)
会生成静态库1.a,这里遇到一个问题:
使用.a文件链接失败,而使用.o链接成功,从符号表看,二者并无差异
shell@ubuntu:~/test$ gcc 1.a linkTest.cpp
/tmp/cc8hB75y.o:在函数‘main’中:
linkTest.cpp:(.text+0x11):对‘func1()’未定义的引用
collect2: error: ld returned 1 exit status
shell@ubuntu:~/test$ gcc 1.o linkTest.cpp
而二者的符号表并无差异:
shell@ubuntu:~/test$ readelf -s 1.a
文件:1.a(1.o)
Symbol table '.symtab' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS 1.cpp
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 6
9: 0000000000000000 19 FUNC GLOBAL DEFAULT 1 _Z5func1v
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
shell@ubuntu:~/test$ readelf -s 1.o
Symbol table '.symtab' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS 1.cpp
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 6
9: 0000000000000000 19 FUNC GLOBAL DEFAULT 1 _Z5func1v
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
解决过程1:
上遇到编译过程,首先考虑1.cpp使用了cpp后缀,所以自动按C++的标准进行了编译,修改1.cpp为1.c,重复上述过程,得到新的1.a
shell@ubuntu:~/test$ readelf -s 1.a
文件:1.a(1.o)
Symbol table '.symtab' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS 1.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 6
9: 0000000000000000 19 FUNC GLOBAL DEFAULT 1 func1//注意这个符号的变化
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
func1的名称,由_Z5func1v变成了我们期待的func1,但是并为解决问题。
解决过程2:
考虑符号表查找问题,把1.a和linkTest.c更换顺序,使用如下命令重新比编译:
gcc linkTest.cpp 1.a
运行成功,分析原因,是因为linkTest.cpp中作了
extern void func1(void);
编译器认为完成linkTest.cpp的编译后,会出现func1的具体定义,所以需要1.a出现在linkTest.cpp之后,而使用1.o时,编译器会把.o文件作为编译过程的一部分,将其符号表列入其维护的编译过程的符号表内,这个问题我们后续讲到makefile的时候解决。
内存分布问题