自己的网站首页背景怎么做,萝岗定制型网站建设,推荐几个做网页设计的网站,网站后台用什么语言合适C面试常考题——编译内存相关 转自#xff1a;https://leetcode-cn.com/leetbook/read/cpp-interview-highlights/e4ns5g/ C程序编译过程
编译过程分为四个过程#xff1a;编译#xff08;编译预处理、编译、优化#xff09;#xff0c;汇编#xff0c;链接。
编译预处…C面试常考题——编译内存相关 转自https://leetcode-cn.com/leetbook/read/cpp-interview-highlights/e4ns5g/ C程序编译过程
编译过程分为四个过程编译编译预处理、编译、优化汇编链接。
编译预处理处理以 # 开头的指令
编译、优化将源码 .cpp 文件翻译成 .s 汇编代码
汇编将汇编代码 .s 翻译成机器指令 .o 文件
链接汇编程序生成的目标文件即 .o 文件并不会立即执行因为可能会出现.cpp 文件中的函数引用了另一个 .cpp 文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些文件对应的目标文件连接成一个整体从而生成可执行的程序 .exe 文件。 链接分为两种
静态链接代码从其所在的静态链接库中拷贝到最终的可执行程序中在该程序被执行时这些代码会被装入到该进程的虚拟地址空间中。动态链接代码被放到动态链接库或共享对象的某个目标文件中链接程序只是在最终的可执行程序中记录了共享对象的名字等一些信息。在程序执行时动态链接库的全部内容会被映射到运行时相应进行的虚拟地址的空间。
二者的优缺点
静态链接浪费空间每个可执行程序都会有目标文件的一个副本这样如果目标文件进行了更新操作就需要重新进行编译链接生成可执行程序更新困难优点就是执行的时候运行速度快因为可执行程序具备了程序运行的所有内容。动态链接节省内存、更新方便但是动态链接是在程序运行时每次执行都需要链接相比静态链接会有一定的性能损失。
C内存管理
C 内存分区栈、堆、全局/静态存储区、常量存储区、代码区。
栈存放函数的局部变量、函数参数、返回地址等由编译器自动分配和释放。堆动态申请的内存空间就是由 malloc 分配的内存块由程序员控制它的分配和释放如果程序执行结束还没有释放操作系统会自动回收。全局区/静态存储区.bss 段和 .data 段存放全局变量和静态变量程序运行结束操作系统自动释放在 C 语言中未初始化的放在 .bss 段中初始化的放在 .data 段中C 中不再区分了。常量存储区.data 段存放的是常量不允许修改程序运行结束自动释放。 代码区.text 段存放代码不允许修改但可以执行。编译后的二进制文件存放在这里。 说明
从操作系统的本身来讲以上存储区在内存中的分布是如下形式(从低地址到高地址).text 段 -- .data 段 -- .bss 段 -- 堆 -- unused -- 栈 -- env
程序实例
#include iostream
using namespace std;/*
说明C 中不再区分初始化和未初始化的全局变量、静态变量的存储区如果非要区分下述程序标注在了括号中
*/int g_var 0; // g_var 在全局区.data 段
char *gp_var; // gp_var 在全局区.bss 段int main()
{int var; // var 在栈区char *p_var; // p_var 在栈区char arr[] abc; // arr 为数组变量存储在栈区abc为字符串常量存储在常量区char *p_var1 123456; // p_var1 在栈区123456为字符串常量存储在常量区static int s_var 0; // s_var 为静态变量存在静态存储区.data 段p_var (char *)malloc(10); // 分配得来的 10 个字节的区域在堆区free(p_var);return 0;
}堆和栈的区别
申请方式栈是系统自动分配堆是程序员主动申请。申请后系统响应分配栈空间如果剩余空间大于申请空间则分配成功否则分配失败栈溢出申请堆空间堆在内存中呈现的方式类似于链表记录空闲地址空间的链表在链表上寻找第一个大于申请空间的节点分配给程序将该节点从链表中删除大多数系统中该块空间的首地址存放的是本次分配空间的大小便于释放将该块空间上的剩余空间再次连接在空闲链表上。栈在内存中是连续的一块空间向低地址扩展最大容量是系统预定好的堆在内存中的空间向高地址扩展是不连续的。申请效率栈是有系统自动分配申请效率高但程序员无法控制堆是由程序员主动申请效率低使用起来方便但是容易产生碎片。存放的内容栈中存放的是局部变量函数的参数堆中存放的内容由程序员控制。
变量的区别
全局变量、局部变量、静态全局变量、静态局部变量的区别
C 变量根据定义的位置的不同的生命周期具有不同的作用域作用域可分为 6 种全局作用域局部作用域语句作用域类作用域命名空间作用域和文件作用域。
从作用域看
全局变量具有全局作用域。全局变量只需在一个源文件中定义就可以作用于所有的源文件。当然其他不包含全局变量定义的源文件需要用 extern 关键字再次声明这个全局变量。静态全局变量具有文件作用域。它与全局变量的区别在于如果程序包含多个文件的话它作用于定义它的文件里不能作用到其它文件里即被 static 关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量它们也是不同的变量。局部变量具有局部作用域。它是自动对象auto在程序运行期间不是一直存在而是只在函数执行期间存在函数的一次调用执行结束后变量被撤销其所占用的内存也被收回。静态局部变量具有局部作用域。它只被初始化一次自从第一次被初始化直到程序运行结束都一直存在它和全局变量的区别在于全局变量对所有的函数都是可见的而静态局部变量只对定义自己的函数体始终可见。
从分配内存空间看
静态存储区全局变量静态局部变量静态全局变量。栈局部变量。
说明
静态变量和栈变量存储在栈中的变量、堆变量存储在堆中的变量的区别静态变量会被放在程序的静态数据存储区.data 段中静态变量会自动初始化这样可以在下一次调用的时候还可以保持原来的赋值。而栈变量或堆变量不能保证在下一次调用的时候依然保持原来的值。静态变量和全局变量的区别静态变量用 static 告知编译器自己仅仅在变量的作用范围内可见。 在《CPP》这本书中这部分的内容被描述为存储持续性、作用域与链接性。 全局变量在书中为外部链接性、静态持续变量 静态全局变量在书中为内部链接性、静态持续变量 静态局部变量在书中为无链接性、静态持续变量 局部变量在书中为自动存储持续性的变量 全局变量定义在头文件中会有什么问题
如果在头文件中定义全局变量当该头文件被多个文件 include 时该头文件中的全局变量就会被定义多次导致重复定义因此不能再头文件中定义全局变量。
对象创建限制在堆或栈
说明C 中的类的对象的建立分为两种静态建立、动态建立。
静态建立由编译器为对象在栈空间上分配内存直接调用类的构造函数创建对象。例如A a;动态建立使用 new 关键字在堆空间上创建对象底层首先调用 operator new() 函数在堆空间上寻找合适的内存并分配然后调用类的构造函数创建对象。例如A *p new A();
限制对象只能建立在堆上 最直观的思想避免直接调用类的构造函数因为对象静态建立时会调用类的构造函数创建对象。但是直接将类的构造函数设为私有并不可行因为当构造函数设置为私有后不能在类的外部调用构造函数来构造对象只能用 new 来建立对象。但是由于 new 创建对象时底层也会调用类的构造函数将构造函数设置为私有后那就无法在类的外部使用 new 创建对象了。因此这种方法不可行。 解决方法一 将析构函数设置为私有。 原因静态对象建立在栈上是由编译器分配和释放内存空间编译器为对象分配内存空间时会对类非静态函数进行检查即编译器会检查析构函数的访问性。当析构函数设为私有时编译器创建的对象就无法通过访问析构函数来释放对象的内存空间因此编译器不会在栈上为对象分配内存。 代码 class A
{
public:A() {}void destory(){delete this;}private:~A(){}
};该方法存在的问题。 用 new 创建的对象通常会使用 delete 释放该对象的内存空间但此时类的外部无法调用析构函数因此类内必须定义一个 destory() 函数用来释放 new 创建的对象。无法解决继承问题因为如果这个类作为基类析构函数要设置成 virtual然后在派生类中重写该函数来实现多态。但此时析构函数是私有的派生类中无法访问。 解决方法二 构造函数设置为 protected并提供一个 public 的静态函数来完成构造而不是在类的外部使用 new 构造将析构函数设置为 protected。 原因类似于单例模式也保证了在派生类中能够访问析构函数。通过调用 create() 函数在堆上创建对象。 代码 class A
{
protected:A() {}~A() {}public:static A *create(){return new A();}void destory(){delete this;}
};限制对象只能建立在栈上
解决方法将 operator new() 设置为私有。原因当对象建立在堆上时是采用 new 的方式进行建立其底层会调用 operator new() 函数因此只要对该函数加以限制就能够防止对象建立在堆上。
class A
{
private:void *operator new(size_t t) {} // 注意函数的第一个参数和返回值都是固定的void operator delete(void *ptr) {} // 重载了 new 就需要重载 delete
public:A() {}~A() {}
};内存对齐
什么是内存对齐内存对齐的原则为什么要进行内存对齐有什么优点
内存对齐编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中。
内存对齐的原则
结构体变量的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除结构体每个成员相对于结构体首地址的偏移量 offset 都是该成员大小与对齐基数中的较小者的整数倍如有需要编译器会在成员之间加上填充字节 internal padding结构体的总大小为结构体最宽基本类型成员大小与对齐基数中的较小者的整数倍如有需要编译器会在最末一个成员之后加上填充字节 trailing padding。
实例
/*
说明程序是在 64 位编译器下测试的
*/
#include iostreamusing namespace std;struct A
{short var; // 2 字节int var1; // 8 字节 内存对齐原则填充 2 个字节 2 (short) 2 (填充) 4 (int) 8long var2; // 12 字节 8 4 (long) 12char var3; // 16 字节 内存对齐原则填充 3 个字节12 1 (char) 3 (填充) 16string s; // 48 字节 16 32 (string) 48
};int main()
{short var;int var1;long var2;char var3;string s;A ex1;cout sizeof(var) endl; // 2 shortcout sizeof(var1) endl; // 4 intcout sizeof(var2) endl; // 4 longcout sizeof(var3) endl; // 1 charcout sizeof(s) endl; // 32 stringcout sizeof(ex1) endl; // 48 structreturn 0;
}进行内存对齐的原因主要是硬件设备方面的问题
某些硬件设备只能存取对齐数据存取非对齐的数据可能会引发异常某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作相比于存取对齐的数据存取非对齐的数据需要花费更多的时间某些处理器虽然支持非对齐数据的访问但会引发对齐陷阱alignment trap某些硬件设备只支持简单数据指令非对齐存取不支持复杂数据指令的非对齐存取。
内存对齐的优点
便于在不同的平台之间进行移植因为有些硬件平台不能够支持任意地址的数据访问只能在某些地址处取某些特定的数据否则会抛出异常提高内存的访问效率因为 CPU 在读取内存时是一块一块的读取。
类的大小
类大小的计算
说明类的大小是指类的实例化对象的大小用 sizeof 对类型名操作时结果是该类型的对象的大小。
计算原则
遵循结构体的对齐原则。与普通成员变量有关与成员函数和静态成员无关。即普通成员函数静态成员函数静态数据成员静态常量数据成员均对类的大小无影响。因为静态数据成员被类的对象共享并不属于哪个具体的对象。虚函数对类的大小有影响是因为虚函数表指针的影响。虚继承对类的大小有影响是因为虚基表指针带来的影响。空类的大小是一个特殊情况空类的大小为 1当用 new 来创建一个空类的对象时为了保证不同对象的地址不同空类也占用存储空间。
实例
简单情况和空类情况
/*
说明程序是在 64 位编译器下测试的
*/
#include iostreamusing namespace std;class A
{
private:static int s_var; // 不影响类的大小const int c_var; // 4 字节int var; // 8 字节 4 4 (int) 8char var1; // 12 字节 8 1 (char) 3 (填充) 12
public:A(int temp) : c_var(temp) {} // 不影响类的大小~A() {} // 不影响类的大小
};class B
{
};
int main()
{A ex1(4);B ex2;cout sizeof(ex1) endl; // 12 字节cout sizeof(ex2) endl; // 1 字节return 0;
}带有虚函数的情况注意虚函数的个数并不影响所占内存的大小因为类对象的内存中只保存了指向虚函数表的指针。
/*
说明程序是在 64 位编译器下测试的
*/
#include iostreamusing namespace std;class A
{
private:static int s_var; // 不影响类的大小const int c_var; // 4 字节int var; // 8 字节 4 4 (int) 8char var1; // 12 字节 8 1 (char) 3 (填充) 12
public:A(int temp) : c_var(temp) {} // 不影响类的大小~A() {} // 不影响类的大小virtual void f() { cout A::f endl; }virtual void g() { cout A::g endl; }virtual void h() { cout A::h endl; } // 24 字节 12 4 (填充) 8 (指向虚函数的指针) 24
};int main()
{A ex1(4);A *p;cout sizeof(p) endl; // 8 字节 注意指针所占的空间和指针指向的数据类型无关cout sizeof(ex1) endl; // 24 字节return 0;
}内存泄露
什么是内存泄漏
内存泄漏由于疏忽或错误导致的程序未能释放已经不再使用的内存。
进一步解释 并非指内存从物理上消失而是指程序在运行过程中由于疏忽或错误而失去了对该内存的控制从而造成了内存的浪费。 常指堆内存泄漏因为堆是动态分配的而且是用户来控制的如果使用不当会产生内存泄漏。 使用 malloc、calloc、realloc、new 等分配内存时使用完后要调用相应的 free 或 delete 释放内存否则这块内存就会造成内存泄漏。 指针重新赋值 char *p (char *)malloc(10);
char *p1 (char *)malloc(10);
p np;开始时指针 p 和 p1 分别指向一块内存空间但指针 p 被重新赋值导致 p 初始时指向的那块内存空间无法找到从而发生了内存泄漏。 大概分为这么3类内存泄漏 堆内存泄漏new/mallc分配内存未使用对应的delete/free回收系统资源泄漏 Bitmap, handle,socket等资源未释放没有将基类析构函数定义称为虚函数使用基类指针或者引用指向派生类对象时派生类对象释放时将不能正确释放派生对象部分。 如何检测及防止内存泄漏
防止内存泄漏的方法 内部封装将内存的分配和释放封装到类中在构造的时候申请内存析构的时候释放内存。 #include iostream
#include cstringusing namespace std;class A
{
private:char *p;unsigned int p_size;public:A(unsigned int n 1) // 构造函数中分配内存空间{p new char[n];p_size n;};~A() // 析构函数中释放内存空间{if (p ! NULL){delete[] p; // 删除字符数组p NULL; // 防止出现野指针}};char *GetPointer(){return p;};
};
void fun()
{A ex(100);char *p ex.GetPointer();strcpy(p, Test);cout p endl;
}
int main()
{fun();return 0;
}说明但这样做并不是最佳的做法在类的对象复制时程序会出现同一块内存空间释放两次的情况请看如下程序 void fun1()
{A ex(100);A ex1 ex; char *p ex.GetPointer();strcpy(p, Test);cout p endl;
}简单解释对于 fun1 这个函数中定义的两个类的对象而言在离开该函数的作用域时会两次调用析构函数来释放空间但是这两个对象指向的是同一块内存空间所以导致同一块内存空间被释放两次可以通过增加计数机制来避免这种情况看如下程序 #include iostream
#include cstringusing namespace std;
class A
{
private:char *p;unsigned int p_size;int *p_count; // 计数变量
public:A(unsigned int n 1) // 在构造函数中申请内存{p new char[n];p_size n;p_count new int;*p_count 1;cout count is : *p_count endl;};A(const A temp){p temp.p;p_size temp.p_size;p_count temp.p_count;(*p_count); // 复制时计数变量 1cout count is : *p_count endl;}~A(){(*p_count)--; // 析构时计数变量 -1cout count is : *p_count endl; if (*p_count 0) // 只有当计数变量为 0 的时候才会释放该块内存空间{cout buf is deleted endl;if (p ! NULL) {delete[] p; // 删除字符数组p NULL; // 防止出现野指针if (p_count ! NULL){delete p_count;p_count NULL;}}}};char *GetPointer(){return p;};
};
void fun()
{A ex(100);char *p ex.GetPointer();strcpy(p, Test);cout p endl;A ex1 ex; // 此时计数变量会 1cout ex1.p ex1.GetPointer() endl;
}
int main()
{fun();return 0;
}程序运行结果 count is : 1
Test
count is : 2
ex1.p Test
count is : 1
count is : 0
buf is deleted解释下程序运行结果的倒数 2、3 行是调用两次析构函数时进行的操作在第二次调用析构函数时进行内存空间的释放从而会有倒数第 1 行的输出结果。 智能指针 智能指针是 C 中已经对内存泄漏封装好了一个工具可以直接拿来使用将在下一个问题中对智能指针进行详细的解释。
内存泄漏检测工具的实现原理
内存检测工具有很多这里重点介绍下 valgrind 。 valgrind 是一套 Linux 下开放源代码GPL V2的仿真调试工具的集合包括以下工具
Memcheck内存检查器valgrind 应用最广泛的工具能够发现开发中绝大多数内存错误的使用情况比如使用未初始化的内存使用已经释放了的内存内存访问越界等。Callgrind检查程序中函数调用过程中出现的问题。Cachegrind检查程序中缓存使用出现的问题。Helgrind检查多线程程序中出现的竞争问题。Massif检查程序中堆栈使用中出现的问题。Extension可以利用 core 提供的功能自己编写特定的内存调试工具。
Memcheck 能够检测出内存问题关键在于其建立了两个全局表 Valid-Value 表对于进程的整个地址空间中的每一个字节byte都有与之对应的 8 个 bits 对于 CPU 的每个寄存器也有一个与之对应的 bit 向量。这些 bits 负责记录该字节或者寄存器值是否具有有效的、已初始化的值。 Valid-Address 表对于进程整个地址空间中的每一个字节byte还有与之对应的 1 个 bit负责记录该地址是否能够被读写。
检测原理
当要读写内存中某个字节时首先检查这个字节对应的 Valid-Address 表中对应的 bit。如果该 bit 显示该位置是无效位置Memcheck 则报告读写错误。内核core类似于一个虚拟的 CPU 环境这样当内存中的某个字节被加载到真实的 CPU 中时该字节在 Valid-Value 表对应的 bits 也被加载到虚拟的 CPU 环境中。一旦寄存器中的值被用来产生内存地址或者该值能够影响程序输出则 Memcheck 会检查 Valid-Value 表对应的 bits如果该值尚未初始化则会报告使用未初始化内存错误。
智能指针
智能指针是为了解决动态内存分配时带来的内存泄漏以及多次释放同一块内存空间而提出的。C11 中封装在了 memory 头文件中。
C11 中智能指针包括以下三种
共享指针shared_ptr资源可以被多个指针共享使用计数机制表明资源被几个指针共享。通过 use_count() 查看资源的所有者的个数可以通过 unique_ptr、weak_ptr 来构造调用 release() 释放资源的所有权计数减一当计数减为 0 时会自动释放内存空间从而避免了内存泄漏。独占指针unique_ptr独享所有权的智能指针资源只能被一个指针占有该指针不能拷贝构造和赋值。但可以进行移动构造和移动赋值构造调用 move() 函数即一个 unique_ptr 对象赋值给另一个 unique_ptr 对象可以通过该方法进行赋值。弱指针weak_ptr指向 share_ptr 指向的对象能够解决由shared_ptr带来的循环引用问题。 智能指针的实现原理 计数原理。
智能指针的实现原理 计数原理。
#include iostream
#include memorytemplate typename T
class SmartPtr
{
private : T *_ptr;size_t *_count;public:SmartPtr(T *ptr nullptr) : _ptr(ptr){if (_ptr){_count new size_t(1);}else{_count new size_t(0);}}~SmartPtr(){(*this-_count)--;if (*this-_count 0){delete this-_ptr;delete this-_count;}}SmartPtr(const SmartPtr ptr) // 拷贝构造计数 1{if (this ! ptr){this-_ptr ptr._ptr;this-_count ptr._count;(*this-_count);}}SmartPtr operator(const SmartPtr ptr) // 赋值运算符重载 {if (this-_ptr ptr._ptr){return *this;}if (this-_ptr) // 将当前的 ptr 指向的原来的空间的计数 -1{(*this-_count)--;if (this-_count 0){delete this-_ptr;delete this-_count;}}this-_ptr ptr._ptr;this-_count ptr._count;(*this-_count); // 此时 ptr 指向了新赋值的空间该空间的计数 1return *this;}T operator*(){assert(this-_ptr nullptr);return *(this-_ptr);}T *operator-(){assert(this-_ptr nullptr);return this-_ptr;}size_t use_count(){return *this-count;}
};一个 unique_ptr 怎么赋值给另一个 unique_ptr 对象
借助 std::move() 可以实现将一个 unique_ptr 对象赋值给另一个 unique_ptr 对象其目的是实现所有权的转移。
// A 作为一个类
std::unique_ptrA ptr1(new A());
std::unique_ptrA ptr2 std::move(ptr1);智能指针可能出现的问题循环引用
在如下例子中定义了两个类 Parent、Child在两个类中分别定义另一个类的对象的共享指针由于在程序结束后两个指针相互指向对方的内存空间导致内存无法释放。
#include iostream
#include memoryusing namespace std;class Child;
class Parent;class Parent {
private:shared_ptrChild ChildPtr;
public:void setChild(shared_ptrChild child) {this-ChildPtr child;}void doSomething() {if (this-ChildPtr.use_count()) {}}~Parent() {}
};class Child {
private:shared_ptrParent ParentPtr;
public:void setPartent(shared_ptrParent parent) {this-ParentPtr parent;}void doSomething() {if (this-ParentPtr.use_count()) {}}~Child() {}
};int main() {weak_ptrParent wpp;weak_ptrChild wpc;{shared_ptrParent p(new Parent);shared_ptrChild c(new Child);p-setChild(c);c-setPartent(p);wpp p;wpc c;cout p.use_count() endl; // 2cout c.use_count() endl; // 2}cout wpp.use_count() endl; // 1cout wpc.use_count() endl; // 1return 0;
}循环引用的解决方法 weak_ptr
循环引用该被调用的析构函数没有被调用从而出现了内存泄漏。
weak_ptr 对被 shared_ptr 管理的对象存在 非拥有性弱引用在访问所引用的对象前必须先转化为 shared_ptrweak_ptr 用来打断 shared_ptr 所管理对象的循环引用问题若这种环被孤立没有指向环中的外部共享指针shared_ptr 引用计数无法抵达 0内存被泄露令环中的指针之一为弱指针可以避免该情况weak_ptr 用来表达临时所有权的概念当某个对象只有存在时才需要被访问而且随时可能被他人删除可以用 weak_ptr 跟踪该对象需要获得所有权时将其转化为 shared_ptr此时如果原来的 shared_ptr 被销毁则该对象的生命期被延长至这个临时的 shared_ptr 同样被销毁。
#include iostream
#include memoryusing namespace std;class Child;
class Parent;class Parent {
private://shared_ptrChild ChildPtr;weak_ptrChild ChildPtr;
public:void setChild(shared_ptrChild child) {this-ChildPtr child;}void doSomething() {//new shared_ptrif (this-ChildPtr.lock()) {}}~Parent() {}
};class Child {
private:shared_ptrParent ParentPtr;
public:void setPartent(shared_ptrParent parent) {this-ParentPtr parent;}void doSomething() {if (this-ParentPtr.use_count()) {}}~Child() {}
};int main() {weak_ptrParent wpp;weak_ptrChild wpc;{shared_ptrParent p(new Parent);shared_ptrChild c(new Child);p-setChild(c);c-setPartent(p);wpp p;wpc c;cout p.use_count() endl; // 2cout c.use_count() endl; // 1}cout wpp.use_count() endl; // 0cout wpc.use_count() endl; // 0return 0;
}