C++面试突击
编译内存相关
C++程序编译过程
编译过程分为四个过程:编译(编译预处理、编译、优化),汇编,链接。
- 编译预处理:处理以
#
开头的指令; - 编译、优化:将源码
.cpp
文件翻译成.s
汇编代码; - 汇编:将汇编代码
.s
翻译成机器指令.o
文件; - 链接:汇编程序生成的目标文件,即
.o
文件,并不会立即执行,因为可能会出现:.cpp
文件中的函数引用了另一个.cpp
文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成可执行的程序.exe
文件。
链接分为两种:
- 静态链接:代码从其所在的静态链接库中拷贝到最终的可执行程序中,在该程序被执行时,这些代码会被装入到该进程的虚拟地址空间中。
- 动态链接:代码被放到动态链接库或共享对象的某个目标文件中,链接程序只是在最终的可执行程序中记录了共享对象的名字等一些信息。在程序执行时,动态链接库的全部内容会被映射到运行时相应进行的虚拟地址的空间。
二者的优缺点:
- 静态链接:浪费空间,每个可执行程序都会有目标文件的一个副本,这样如果目标文件进行了更新操作,就需要重新进行编译链接生成可执行程序(更新困难);优点就是执行的时候运行速度快,因为可执行程序具备了程序运行的所有内容。
- 动态链接:节省内存、更新方便,但是动态链接是在程序运行时,每次执行都需要链接,相比静态链接会有一定的性能损失。
C++内存管理
C++ 内存分区:栈、堆、全局/静态存储区、常量存储区、代码区。
- 栈:存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。
- 堆:动态申请的内存空间,就是由
malloc()
分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。 - 全局区/静态存储区(
.bss
段和.data
段):存放全局变量和静态变量,程序运行结束操作系统自动释放,在C语言中,未初始化的放在.bss
段中,初始化的放在.data
段中,C++中不再区分了。 - 常量存储区(
.data
段):存放的是常量,不允许修改,程序运行结束自动释放。 - 代码区(
.text
段):存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。
栈和堆的区别
申请方式:栈是系统自动分配,堆是程序员主动申请。
申请后系统响应:分配栈空间,如果剩余空间大于申请空间则分配成功,否则分配失败栈溢出;申请堆空间,堆在内存中呈现的方式类似于链表(记录空闲地址空间的链表),在链表上寻找第一个大于申请空间的节点分配给程序,将该节点从链表中删除,大多数系统中该块空间的首地址存放的是本次分配空间的大小,便于释放,将该块空间上的剩余空间再次连接在空闲链表上。
栈在内存中是连续的一块空间(向低地址扩展)最大容量是系统预定好的,堆在内存中的空间(向高地址扩展)是不连续的。
申请效率:栈是有系统自动分配,申请效率高,但程序员无法控制;堆是由程序员主动申请,效率低,使用起来方便但是容易产生碎片。
存放的内容:栈中存放的是局部变量,函数的参数;堆中存放的内容由程序员控制。
变量的区别
全局变量、局部变量、静态全局变量、静态局部变量的区别:
C++变量根据定义的位置的不同的生命周期,具有不同的作用域,作用域可分为 6 种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。
从作用域看:
- 全局变量:具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。
- 静态全局变量:具有文件作用域。它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static 关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相 同名字的静态全局变量,它们也是不同的变量。
- 局部变量:具有局部作用域。它是自动对象(auto),在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。
- 静态局部变量:具有局部作用域。它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。
从分配内存空间看:
- 静态存储区:全局变量,静态局部变量,静态全局变量。
- 栈:局部变量。
全局变量定义在头文件中有什么问题?
如果在头文件中定义全局变量,当该头文件被多个文件include
时,该头文件中的全局变量就会被定义多次,导致重复定义,因此不能再头文件中定义全局变量。
对象创建限制在堆或栈
内存对齐
什么是内存对齐?内存对齐的原则?为什么要进行内存对齐,有 什么优点?
内存对齐:编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中
内存对齐的原则:
- 结构体变量的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除;
- 结构体每个成员相对于结构体首地址的偏移量 (offset)都是该成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在成员之间加上填充字节 (internal padding);
- 结构体的总大小为结构体最宽基本类型成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)
进行内存对齐的原因:(主要是硬件设备方面的问题)
- 某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常;
- 某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作;
- 相比于存取对齐的数据,存取非对齐的数据需要花费更多的时间;
- 某些处理器虽然支持非对齐数据的访问,但会引发对齐陷阱(alignmenttrap);
- 某些硬件设备只支持简单数据指令非对齐存取,不支持复杂数据指令的非对齐存取。
内存对齐的优点:
- 便于在不同的平台之间进行移植,因为有些硬件平台不能够支持任意地址的数据访问,只能在某些地址处取某些特定的数据,否则会抛出异常;
- 提高内存的访问效率,因为 CPU 在读取内存时,是一块一块的读取。
类的大小
什么是内存泄漏
内存泄漏:由于疏忽或错误导致的程序未能释放已经不再使用的内存。
进一步解释:
-
并非指内存从物理上消失,而是指程序在运行过程中,由于疏忽或错误而失去了对该内存的控制,从而造成了内存的浪费。
-
常指堆内存泄漏,因为堆是动态分配的,而且是用户来控制的,如果使用不当,会产生内存泄漏。
-
使用
malloc
、calloc
、realloc
、new
等分配内存时,使用完后要调用相应的free
或delete
释放内存,否则这块内存就会造成内存泄漏。 -
指针重新赋值
char *p = (char *)malloc(10);
char *p1 = (char *)malloc(10);
p = np;开始时,指针
p
和p1
分别指向一块内存空间,但指针p
被重新赋值,导致p
初始时指向的那块内存空间无法找到,从而发生了内存泄漏。
怎么防止内存泄漏?内存泄漏检测工具的原理?
防止内存泄漏的方法:
内部封装:将内存的分配和释放封装到类中,在构造的时候申请内存,析构的时候释放内存。RAII(说明:但这样做并不是最佳的做法,在类的对象复制时,程序会出现同一块内存空间释放两次的情况)
智能指针:智能指针是 C++ 中已经对内存泄漏封装好了一个工具,可以直接拿来使用,将在下一个问题中对智能指针进行详细的解释。
内存泄漏检测工具的实现原理:
内存检测工具有很多,这里重点介绍下 valgrind 。
智能指针有哪几种?智能指针的实现原理?
智能指针是为了解决动态内存分配时带来的内存泄漏以及多次释放同一块内存空间而提出的。C++11中封装在了<memory>
头文件中。
C++11 中智能指针包括以下三种:
-
共享指针(
shared_ptr
):资源可以被多个指针共享,使用计数机制表明资源被几个指针共享。通过use_count()
查看资源的所有者的个数,可以通过unique_ptr
、weak_ptr
来构造,调用release()
释放资源的所有权,计数减一,当计数减为 0 时,会自动释放内存空间,从而避免了内存泄漏。 -
独占指针(
unique_ptr
):独享所有权的智能指针,资源只能被一个指针占有,该指针不能拷贝构造和赋值。但可以进行移动构造和移动赋值构造(调用move()
函数),即一个unique_ptr
对象赋值给另一个unique_ptr
对象,可以通过该方法进行赋值。 -
弱指针(
weak_ptr
):指向shared_ptr
指向的对象,能够解决由shared_ptr
带来的循环引用问题。
智能指针的实现原理: 计数原理。
#include <iostream>
#include <memory>
template <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 指向了新赋值的空间,该空间的计数 +1
return *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_ptr<A> ptr1(new A());
std::unique_ptr<A> ptr2 = std::move(ptr1);
使用智能指针会出现什么问题?怎么解决?
智能指针可能出现的问题:循环引用
在如下例子中定义了两个类 Parent、Child,在两个类中分别定义另一个类的对象的共享指针,由于在程序结束后,两个指针相互指向对方的内存空间,导致内存无法释放。
#include <iostream>
#include <memory>
using namespace std;
class Child;
class Parent;
class Parent {
private:
shared_ptr<Child> ChildPtr;
public:
void setChild(shared_ptr<Child> child) {
this->ChildPtr = child;
}
void doSomething() {
if (this->ChildPtr.use_count()) { }
}
~Parent() { }
};
class Child {
private:
shared_ptr<Parent> ParentPtr;
public:
void setPartent(shared_ptr<Parent> parent) {
this->ParentPtr = parent;
}
void doSomething() {
if (this->ParentPtr.use_count()) { }
}
~Child() { }
};
int main() {
weak_ptr<Parent> wpp;
weak_ptr<Child> wpc;
{
shared_ptr<Parent> p(new Parent);
shared_ptr<Child> c(new Child);
p->setChild(c);
c->setPartent(p);
wpp = p;
wpc = c;
cout << p.use_count() << endl; // 2
cout << c.use_count() << endl; // 2
}
cout << wpp.use_count() << endl; // 1
cout << wpc.use_count() << endl; // 1
return 0;
}
循环引用的解决方法:weak_ptr
循环引用:该被调用的析构函数没有被调用,从而出现了内存泄漏。
weak_ptr
对被shared_ptr
管理的对象存在非拥有性(弱)引用,在访问所引用的对象前必须先转化为shared_ptr
;weak_ptr
用来打断shared_ptr
所管理对象的循环引用问题,若这种环被孤立(没有指向环中的外部共享指针),shared_ptr
引用计数无法抵达 0,内存被泄露;令环中的指针之一为弱指针可以避免该情况;weak_ptr
用来表达临时所有权的概念,当某个对象只有存在时才需要被访问,而且随时可能被他人删除,可以用weak_ptr
跟踪该对象;需要获得所有权时将其转化为shared_ptr
,此时如果原来的shared_ptr
被销毁,则该对象的生命期被延长至这个临时的shared_ptr
同样被销毁。
#include <iostream>
#include <memory>
using namespace std;
class Child;
class Parent;
class Parent {
private:
//shared_ptr<Child> ChildPtr;
weak_ptr<Child> ChildPtr;
public:
void setChild(shared_ptr<Child> child) {
this->ChildPtr = child;
}
void doSomething() {
if (this->ChildPtr.lock()) { }//new shared_ptr
}
~Parent() { }
};
class Child {
private:
shared_ptr<Parent> ParentPtr;
public:
void setPartent(shared_ptr<Parent> parent) {
this->ParentPtr = parent;
}
void doSomething() {
if (this->ParentPtr.use_count()) { }
}
~Child() {
}
};
int main() {
weak_ptr<Parent> wpp;
weak_ptr<Child> wpc;
{
shared_ptr<Parent> p(new Parent);
shared_ptr<Child> c(new Child);
p->setChild(c);
c->setPartent(p);
wpp = p;
wpc = c;
cout << p.use_count() << endl; // 2
cout << c.use_count() << endl; // 1
}
cout << wpp.use_count() << endl; // 0
cout << wpc.use_count() << endl; // 0
return 0;
}
语言对比
C++11新特性
-
auto 类型推导
auto 关键字:自动类型推导,编译器会在 编译期间 通过初始值推导出变量的类型,通过 auto 定义的变量必须有初始值。
auto 关键字基本的使用语法如下:
-
decltype 类型推导
decltype 关键字:decltype 是“declare type”的缩写,译为“声明类型”。和 auto 的功能一样,都用来在编译时期进行自动类型推导。如果希望从表达式中推断出要定义的变量的类型,但是不想用该表达式的值初始化变量,这时就不能再用 auto。decltype 作用是选择并返回操作数的数据类型。
区别:
auto var = val1 + val2;
decltype(val1 + val2) var1 = 0;
auto 根据 = 右边的初始值 val1 + val2 推导出变量的类型,并将该初始值赋值给变量 var;decltype 根据 val1 + val2 表达式推导出变量的类型,变量的初始值和与表达式的值无关。
auto 要求变量必须初始化,因为它是根据初始化的值推导出变量的类型,而 decltype 不要求,定义变量的时候可初始化也可以不初始化。
-
lambda 表达式 lambda 表达式,又被称为 lambda 函数或者 lambda 匿名函数。
lambda匿名函数的定义:
[capture list] (parameter list) -> return type {
function body;
};其中:
capture list:捕获列表,指 lambda 所在函数中定义的局部变量的列表,通常为空。
return type、parameter list、function body:分别表示返回值类型、参数列表、函数体,和普通函数一样。
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int arr[4] = {4, 2, 3, 1};
//对 a 数组中的元素进行升序排序
sort(arr, arr+4, [=](int x, int y) -> bool{ return x < y; } );
for(int n : arr){
cout << n << " ";
}
return 0;
} -
范围 for 语句
for (declaration : expression){
statement
}参数的含义:
expression:必须是一个序列,例如用花括号括起来的初始值列表、数组、vector ,string等,这些类型的共同特点是拥有能返回迭代器的 beign、end 成员。
declaration:此处定义一个变量,序列中的每一个元素都能转化成该变量的类型,常用 auto 类型说明符。
-
右值引用
-
右值引用:绑定到右值的引用,用 && 来获得右值引用,右值引用只能绑定到要销毁的对象。为了和右值引用区分开,常规的引用称为左值引用。
#include <iostream>
#include <vector>
using namespace std;
int main() {
int var = 42;
int &l_var = var;
// error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int'
// 错误:不能将右值引用绑定到左值上
int &&r_var = var;
int &&r_var2 = var + 40; // 正确:将 r_var2 绑定到求和结果上
return 0;
} -
标准库
move()
函数move()
函数:通过该函数可获得绑定到左值上的右值引用,该函数包括在utility
头文件中。该知识点会在后续的章节中做详细的说明。 -
智能指针 相关知识已在第一章中进行了详细的说明,这里不再重复。
-
delete 函数和 default 函数
delete 函数:
=delete
表示该函数不能被调用。default 函数:
=default
表示编译器生成默认的函数,例如:生成默认的构造函数。#include <iostream>
using namespace std;
class A {
public:
A() = default; // 表示使用默认的构造函数
~A() = default; // 表示使用默认的析构函数
A(const A &) = delete; // 表示类的对象禁止拷贝构造
A &operator=(const A &) = delete; // 表示类的对象禁止拷贝赋值
};
int main() {
A ex1;
A ex2 = ex1; // error: use of deleted function 'A::A(const A&)'
A ex3;
ex3 = ex1; // error: use of deleted function 'A& A::operator=(const A&)'
return 0;
}
C和C++的区别
首先说一下面向对象和面向过程:
- 面向过程的思路:分析解决问题所需的步骤,用函数把这些步骤依次实现。
- 面向对象的思路:把构成问题的事务分解为各个对象,建立对象的目的,不是完成一个步骤,而是描述某个事务在解决整个问题步骤中的行为。
区别和联系:
-
语言自身:C 语言是面向过程的编程,它最重要的特点是函数,通过
main()
函数来调用各个子函数。程序运行的顺序都是程序员事先决定好的。C++ 是面向对象的编程,类是它的主要特点,在程序执行过程中,先由主 main 函数进入,定义一些类,根据需要执行类的成员函数,过程的概念被淡化了(实际上过程还是有的,就是主函数的那些语句。),以类驱动程序运行,类就是对象,所以我们称之为面向对象程序设计。面向对象在分析和解决问题的时候,将涉及到的数据和数据的操作封装在类中,通过类可以创建对象,以事件或消息来驱动对象执行处理。 -
应用领域:C 语言主要用于嵌 入式领域,驱动开发等与硬件直接打交道的领域,C++ 可以用于应用层开发,用户界面开发等与操作系统打交道的领域。
-
C++ 既继承了 C 强大的底层操作特性,又被赋予了面向对象机制。它特性繁多,面向对象语言的多继承,对值传递与引用传递的区分以及 const 关键字,等等。
-
C++ 对 C 的“增强”,表现在以下几个方面:类型检查更为严格。增加了面向对象的机制、泛型编程的机制(Template)、异常处理、运算符重载、标准模板库(STL)、命名空间(避免全局命名冲突)。
Java和C++的区别
Python和C++的区别
-
语言自身:Python 为脚本语言,解释执行,不需要经过编译;C++ 是一种需要编译后才能运行的语言,在特定的机器上编译后运行。
-
运行效率:C++ 运行效率高,安全稳定。原因:Python 代码和 C++ 最终都会变成 CPU指令来跑,但一般情况下,比如反转和合并两个字符串,Python 最终转换出来的 CPU 指令会比 C++ 多很多。首先,Python中涉及的内容比 C++ 多,经过了更多层,Python 中甚至连数字都是 object ;其次,Python 是解释执行的,和物理机CPU 之间多了解释器这层,而 C++ 是编译执行的,直接就是机器码,编译的时候编译器又可以进行一些优化。
-
开发效率:Python 开发效率高。原因:Python 一两句代码就能实现的功能,C++ 往往需要更多的代码才能实现。
-
书写格式和语法不同:Python 的语法格式不同于其 C++ 定义声明才能使用,而且极其灵活,完全面向更上层的开发者。
面向对象
什么是面向对象?面向对象的三大特性
面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含数据(成员变量)和动作(成员方法)。
面向对象的三大特性:
-
封装:将具体的实现过程和数据封装成一个函数,只能通过接口进行访问,降低耦合性。
-
继承:子类继承父类的特征和行为,子类有父类的非 private 方法或成员变量,子类可以对父类的方法进行重写,增强了类之间的耦合性,但是当父类中的成员变量、成员函数或者类本身被 final 关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。
-
多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。