1. 类的默认成员函数
包括6个:构造函数、析构函数、拷贝构造函数、赋值运算符函数、取址运算符函数、const取址运算符函数,用一个自己实现的string类来展示这几个默认成员函数。C++11又增加了默认的移动构造函数和移动赋值运算符函数。
class String
{
public:
virtual ~String();
String(const char * str);
String(const String & other);
String & operator = (const String & other);
String * operator & ();
const String * operator & () const;
private:
char * m_data;
};
String::String(const char * str)
{
if (nullptr == str)
{
m_data = new char[1];
*m_data = '\0';
}
else
{
m_data = new char[strlen(str) + 1];
strcpy(m_data, str);
}
}
String::~String()
{
delete [] m_data;
m_data = nullptr;
}
String::String(const String & other)
{
m_data = new char[strlen(other.m_data)+1];
strcpy(m_data, other.m_data);
}
String & String::operator = (const String & other)
{
if (this == &other) return *this;
delete [] m_data;
m_data = new char[strlen(other.m_data)+1];
strcpy(m_data, other.m_data);
return *this;
}
String * String::operator & ()
{
return this;
}
const String * String::operator & () const
{
return this;
}
2.C++11 的default和delete
C++11引入了default和delete对类的默认成员函数进行控制,这样比将某些不想实现或者暴露的默认成员函数置为私有成员更优雅一些。
3.初始化表
,是构造函数的一种初始化方式,使用初始化表对非内部数据类型的类成员进行初始化的效率更高,因为不需要额外调用该类型成员的无参构造函数,只需要调用拷贝构造函数即可完成初始化。另外,如果类存在继承关系,派生类必须在其初始化表中调用基类的构造函数。
4.奇异递归模板模式
(CRTP,Curiously Recurring Template Pattern)
这篇写的不错:
5.RAII
Resource Acquisition is Initialization,意为“资源获取即初始化”,智能指针shared_ptr与auto_ptr是RAII最具代表性的实现,另外一个很有用的地方是使用std::unique_lock或者std::lock_guard对互斥量std:: mutex进行状态管理。
6.C++11提供了auto关键字和区间迭代
结合stl容器使用,能简化for循环写法,非常优雅。
std::map hash_map = {{1, “c++”}, {2, “java”}, {3, “python”}};
for(auto it : hash_map) {
std::cout << it.first << “\t” << it.second << std::endl;
}
7.虚函数
主要为了实现多态,可以让父类指针方便调用子类的实现。如果类中存在虚函数,那么在类对象的内存起始处会建立虚表v-table,所有虚函数的地址可以在虚表中找到,存在继承的情况下,如果有函数覆盖,那么在子类对象的虚表中,子类的虚函数地址会替换掉父类的虚函数地址。vtable在32位程序下占用4字节,在64位程序下占用8字节。
虚函数表的存储位置,可能根据编译器的实现各有不同,根据网友深挖,其在gcc编译器下位于只读数据段.rodata中,而在vs编译器下位于常量段中。
纯虚函数,就像一个接口定义一样,定义的时候是不做实现的,必须在子类中实现,拥有纯虚函数的类不能生成对象,叫抽象类。
8.内存布局
包括程序的内存布局和类对象的内存布局,熟悉经典的钻石继承,可以方便了解类对象的内存布局,引入虚拟继承,可以解决钻石继承面临的二义性问题,而且节省了变量存储空间。
9.sizeof,是在编译的时候计算大小。
空类/空结构体的sizeof值为1。试想一个“不占空间“的变量如何被取地址、两个不同的“空结构体”变量又如何得以区分呢,于是,“空结构体”变量也得被存储,这样编译器也就只能为其分配一个字节的空间用于占位了。sizeof的值不包含静态变量。
10.字节对齐,
也叫内存对齐,关于字节对齐,这篇文章写的不错:
字节对齐原因:
总线从内存读取数据,并不是一个字节一个字节的读取,而是一次读取多个字节,不同计算机实现可能不同,导致cpu字长不同,一般都是2字节的整数倍字节数来读取,为了避免一个变量因为地址偏移不规整而被多次读取影响效率,需要靠字节对齐来解决这种问题。
字节对齐原则:
结构体变量的首地址能够被其对齐字节数大小所整除。
结构体每个成员相对结构体首地址的偏移都是成员大小的整数倍,如不满足,对前一个成员填充字节以满足。
结构体的总大小为结构体对齐字节数大小的整数倍,如不满足,最后填充字节以满足。
字节对齐跨平台问题:
不同平台的对齐方案可能不同,所以如果是跨平台传递数据,需要编译器介入,改变各平台的对齐方式为约定方式,比如#pragma pack(1),设置为1字节对齐。
11.return局部变量的原理,
如果是内置数据类型的局部变量,会先将变量值寄存到寄存器中,然后将寄存器中的值拷贝给调用方,如果是类对象,那么编译器会先在栈中生成一个匿名对象,然后调用复制构造函数将局部变量值赋值给匿名对象,再将匿名对象的地址存入寄存器,然后再将寄存器地址拷贝给调用方使用。
很明显,上面这种方式,产生了临时变量,是很影响效率的,所以编译器进行了返回值优化RVO和具名返回值优化NRVO,优化之后,不产生临时变量,也能达到原有效果!
12.C++11是从gcc4.7版本以后才开始支持,
这几年版本更新比较多,出现了C++14和C++17,可能在明年2020年会出台C++20标准了,C++20要支持协程!
13.32位系统与64位的区别
第一个是cpu运算器和寄存器支持的数据范围更大,第二个是寻址空间更大,突破4GB的限制,到达4EB,所以可以支持更大的内存,以及更大的视频文件,第三个是cpu字长更长,使得大数的计算速度更快。
处理器中的寄存器通常可分为三种︰整数、浮点数、其它。在所有常见的主流处理器中,只有整数寄存器(integer register)才可存放指针值(内存数据的地址)。非整数寄存器不能存放指针来读写内存,因此不能用来避开任何受到整数寄存器大小所影响的内存限制。
32位程序迁移到64位系统上编译,如果程序写法上不规范,可能带来错误,比如把long赋值给int,或者没有使用sizeof来获取长度而是在代码里写死。
64位系统相对于32位系统的缺点,主要是相同的数据会消耗更多的内存空间,比如long类型数据,如果没有超过4.2亿以上的数字需要存储,那么内存空间会浪费一倍。所以64位进程对内存的需求量更大。
14.严格弱序,
这是stl对有序的关联容器的要求,如set/map/multiset/multimap等容器,严格弱序的要求是:
两个关键字不能同时“严格弱序”于对方
如果a“严格弱序”于b,且b“严格弱序”于c,则a必须“严格弱序”于c
如果存在两个关键字,任何一个都不“严格弱序”于另一个,则这两个关键字是相等的。
如果是自定义结构用于做有序的关联容器key,那么应该重载<运算符或者定义一个比大小的仿函数,其逻辑需要遵守“严格弱序”规则。如果自定义结构不遵守严格弱序规则,那么会导致无法查找到指定key的问题。
对于无序的关联容器,如果要将自定义类型作为unordered_map的键值,需如下两个步骤:
定义哈希函数的函数对象;
定义等比函数的函数对象或者在自定义类里重载operator==()
参照:C++ STL:unordered_map 自定义键值类型
15.智能指针,
基于RAII原理,帮助程序员管理堆内存,避免内存泄漏。C++11包含unique_ptr、shared_ptr和weak_ptr三种智能指针,已经弃用auto_ptr指针。unique_ptr独占对象,支持move语义,shared_ptr通过引用计数,来共享对象,当引用计数为0时,销毁对象,weak_ptr通常配合shared_ptr使用,其不会增加对象的引用计数,可以解决环状引用问题。
16.const用法,
const可以用来定义常量,常量必须在定义的时候完成初始化。
const修饰变量或者函数的时候,可以从右向左来查看修饰的目标,比如const位于函数的右侧,那么就是修饰一个函数,const位于指针的右侧,那么就是修饰一个指针,const位于指针的左侧,那么修饰的就是指针指向的对象,const位于引用的左侧,那么修饰的就是引用的对象。
const成员变量,只能在构造函数的初始化列表中进行初始化,这也就意味着const成员变量是与对象绑定的。
const对象,只能调用const成员函数。
const成员函数,可以理解为是对*this对象进行了const修饰,使得无法对成员变量进行修改操作。
17.template,
模板是可以递归调用的,能实现在编译期完成计算,经典的求累加代码:
template <int n>
class Fac
{
public:
enum { sum = Fac<n-1>::sum + n };
};
template <>
class Fac<1>
{
public:
enum { sum = 1 };
};
18.进程间通信,
有5种方式:管道(包括无名管道pipe和有名管道fifo)、消息队列、信号量、共享内存以及socket。
使用消息队列,需要经历内核与用户空间的拷贝,1是用户空间到内核,2是内核到内存,3是内存到内核,4是内核到用户空间。
使用共享内存,不需要经历内核与用户空间的拷贝,1是用户空间到内存,2是内存到用户空间。
mmap和shmget都可以实现共享内存,mmap是把文件物理地址映射到进程的虚拟地址空间中,对文件的操作与跟内存的操作一样,这样机器重启不会导致内容丢失,而且可以保存很大的数据。shmget是在内存中开辟出来的一块共享空间,机器重启后数据就丢失了。
mmap只从物理磁盘加载使用到的部分数据到内存中操作,而且系统会自动将脏数据写回磁盘,因此可以不受限于内存大小,来快速操作很大的文件内容。
19.虚拟内存,物理内存,共享内存
top中对应VIRT,RES,SHR。
VIRT表示程序运行中需要的总内存大小,这部分内存不会一次性全部映射到物理内存,只有被频繁访问的数据才会进入物理内存。如果VIRT一直变大,可能有内存泄漏发生。
RES是程序实际占用的物理内存大小,也就是常驻内存(RSS),其在运行中可能会逐渐增大或缩小。看一个程序实际占用的内存大小,一般看RES。
SHR是程序共享的各种动态库占用的内存大小,每个库在内存中只保存一份,多个调用的程序会共用这些内存。
swap是在磁盘上开辟的一块空间,用于在物理内存不足的时候,操作系统将一部分内存数据交换到swap区,如果出现频繁的swap换入换出,带来大量磁盘操作,会使得系统性能下降。通常拷贝大文件的时候,可能会引起swap交换。
20.fork原理
父进程通过fork函数创建子进程,子进程拥有新的进程号,父子进程拥有各自独立的虚拟地址空间,但是共享相同的物理空间,只有当一方对共享数据进行修改时,才根据copy on write机制,先拷贝一份,然后在拷贝的地址上做修改。
21.文件的打开与关闭原理,各种操作文件函数对比,为何需要关闭打开的文件?
22.磁盘管理
23.core文件
24.程序是如何生成的?
源代码需要经过预编译处理、编译、汇编、链接等4个步骤来生成一个可执行文件。
预编译处理生成.i文件,是将一些include引用的代码加入到代码中,去掉注释,展开宏,处理条件编译指令,添加行号等。
编译步骤会生成.S文件,将源代码转换为汇编代码,这个过程会进行词法分析、语法分析、语义分析和编译优化。
汇编步骤会生成.o文件,将汇编代码转换为二进制代码。
链接步骤会将各种.o文件链接起来,生成可执行文件,包含静态链接和动态链接两种方式。
25.程序是如何执行的?
26.静态链接与动态链接
27.内存泄漏