1. 从编码到执行的过程
预处理
删除注释
宏和inline
宏(Macro):
-
预编译替换: 宏是在预处理阶段进行文本替换的,即在代码被编译之前。
-
简单的字符串替换: 宏通常是简单的字符串替换,它将代码中的宏名称替换为宏定义的文本。
-
类型检查: 宏的替换是简单的文本替换,不进行类型检查。这可能导致一些问题,因为宏不关心数据类型,可能引入潜在的错误。
-
代码大小: 由于是文本替换,宏可能会生成较大的代码。每次使用宏时,都会复制宏的内容,可能导致代码膨胀。
-
栈的使用: 宏不涉及函数调用,因此不需要调用栈。宏只是简单地进行文本替换。
inline
关键字:
-
编译时替换:
inline
是在编译时进行的,而不是在预处理阶段。编译器有权决定是否真正进行内联。 -
类型检查: 内联函数会进行类型检查,因为内联函数在编译时被视为真正的函数调用。这有助于避免一些宏可能引入的类型问题。
-
代码大小: 内联函数的代码通常比宏更小,因为它是在编译器控制下插入的。不像宏,不会导致代码膨胀。
-
栈的使用: 内联函数与普通函数一样,可能涉及栈的使用。但是,由于内联函数通常较短,编译器可能会选择在调用点内联它,从而减少调用的开销。
处理预编译指令:include递归/条件编译
#pragma 编译器指令
#pragma
是一种用于向编译器发出特定指令的预处理器指令。它的作用是为了提供一种在不同编译器之间进行特定于平台或实现的设置和控制的标准化机制。#pragma
的具体行为因编译器而异,因为它们通常是编译器特定的。
编译:通过语法/词法分析生成汇编代码
汇编:将汇编代码转变成机器可以执行的指令
链接:
why?
目标文件主要分为两个区域:数据区域和指令区域。 每一个指令和数据都被安排了地址。
(1)地址重定位: 目标文件被整合的时候,每个目标文件的数据区被整合到一起,每个目标文件的指令区被整合到一起。假如目标文件1被整合前指令的地址是00000001,目标文件n整合前指令的地址也是00000001,整合到一起后,他们的地址是要重新编排的,这个叫地址重定位。数据区域的地址相应的也要重新编排。
重定位地址的作用:CPU会通过这个重定位的地址进行寻址,找到在内存中要执行的指令和数据,然后取出指令执行,并按照指令要求处理数据。重定位以后,会给执行文件中的计算机指令数据,重新安排地址,CPU会通过这些地址取指令执行,并处理这些数据。最终需要通过这些地址找到内存中的指令和数据。
(2) 符号统一 : 直接举个C语言中的例子,假设程序有两个.c文件,分别是a.c和b.c,这两个文件中都有名叫var的变量,a.c被编译得到a.o,b.c被编译得到b.o,将a.o和b.o链接到同一个文件时,var命名重复了,需要根据规则对着两个符号进行统一,与此相似的还有函数名的“符号统一”问题。
连接器重复符号处理(一)动态库重复符号处理规则_程序和动态链接库定义了相同的符号-优快云博客
静态
各自一个副本
快,更新难,占用空间多
函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。
空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个 目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;
更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西, 在执行的时候运行速度快。
动态
执行时共享,更新方便,不知道动态库地址
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副 本,而是这多个程序在执行时共享同一份副本;
更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。
性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。
--> .so 文件放到 lib 下,每次在生产环境进行编译,
引申:不同语言的编译过程有什么区别?
c语言,java,Python跨平台运行原理_不同编程语言怎么联动-优快云博客
c:编译后在不同操作系统是不同的机器码,所以不同操作系统编译c程序的编译器不一样
java:一次编译,不同平台运行,JVM把编译后的文件转换为JVM可以识别的字节码,再转变为不同平台可以识别的机器码
python:逐行解释器解释为机器码或者字节码。
机器码执行快,所以可以优化解释器。
JIT(Just-In-Time)编译器在运行时将热点代码编译为本地机器代码,以提高执行速度。
2.内存泄露
该释放的没释放
避免内存泄漏的补充建议:
-
智能指针: 使用智能指针(
std::shared_ptr
和std::unique_ptr
)可以大大简化内存管理。它们使用 RAII(资源获取即初始化)原则,确保在离开作用域时自动释放内存。 -
RAII(资源获取即初始化): 不仅仅是智能指针,任何资源(包括文件句柄、网络连接等)都可以使用 RAII 进行管理。通过对象的生命周期管理资源的分配和释放,减少手动管理的错误。
-
计数法
-
基类的析构函数为虚函数
-
编码规范和代码审查: 强调良好的编码规范,进行代码审查时特别注意内存管理的正确性。遵循一致的规范和最佳实践可以降低出现内存泄漏的可能性。
-
内存分析工具: 除了检测工具外,一些集成开发环境(IDE)也提供了内存分析工具,例如 Visual Studio 的 Memory Debugger。
3.智能指针 unique_ptr
是的,你对 unique_ptr
的特性描述是正确的。unique_ptr
是一种智能指针,它确保在任意时刻只有一个 unique_ptr
拥有对其指向的对象的所有权。当 unique_ptr
被销毁时,它所管理的对象也会被销毁。
这种独占所有权的特性使得 unique_ptr
适合在需要管理动态分配的资源(如堆上的对象)时使用,因为它确保了资源的所有权不会被多个指针共享,从而避免了潜在的资源管理问题。
不同方式调用构造函数
SomeType* rawPtr = new SomeType();
unique_ptr<SomeType> ptr = unique_ptr<SomeType>(rawPtr); // 显式调用构造函数
SomeType* rawPtr = new SomeType();
unique_ptr<SomeType> ptr = rawPtr; // 错误,禁止隐式转换
包含头文件:
#include <memory>
template <typename T, typename D = default_delete<T>>
class unique_ptr
{
public:
explicit unique_ptr(pointer p) noexcept; // 不可用于转换函数。
~unique_ptr() noexcept;
T& operator*() const; // 重载*操作符。
T* operator->() const noexcept; // 重载->操作符。
unique_ptr(const unique_ptr &) = delete; // 禁用拷贝构造函数。
unique_ptr& operator=(const unique_ptr &) = delete; // 禁用赋值函数。
unique_ptr(unique_ptr &&) noexcept; // 右值引用。
unique_ptr& operator=(unique_ptr &&) noexcept; // 右值引用。
// ...
private:
pointer ptr; // 内置的指针。
};
第一个模板参数T:指针指向的数据类型。
第二个模板参数D:指定删除器,缺省用delete释放资源。
测试类AA的定义:
class AA
{
public:
string m_name;
AA() { cout << m_name << "调用构造函数AA()。\n"; }
AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
~AA() { cout << m_name << "调用了析构函数~AA(" << m_name << ")。\n"; }
};
一、基本用法
1)初始化
方法一:
unique_ptr<AA> p0(new AA("西施")); // 分配内存并初始化。