C++常见问题
- 引用
- 模板
- STL
- vector原理
- 移动语义与右值引用
- New delete与malloc free
- inline
- const
- static
- explicit 的作用
- lambda 表达式
- 友元
- public、protected、private的区别
- 封装
- 继承
- 多态
- 虚函数
- 重载、重写、隐藏的区别
- 智能指针
- C++ 11新特性
- 深拷贝与浅拷贝
- 虚拟内存
- 内存对齐及内存泄漏
- C++内存管理
- C++程序编译过程
- 其他
- C++的性能优化
- 参考
引用
引用即对象的“别名”(alias),必须初始化建立绑定,引用本身不占存储单元,不能建立数组的引用。
- 常引用声明方式:const 类型标识符 &引用名=目标变量名。
- 引用作为函数返回值类型,不能返回局部变量的引用。
- 引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。
- 流操作符<<和>>、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。
使用引用的原因:程序员能够修改调用函数中的数据对象,通过传递引用而不是整个数据对象,可以提高程序的运行速度。
模板
模板:创建类或者函数的蓝图或者公式,是实现泛型编程的基础,分为函数模板和类模板。
- C++的模板(Template)是一种支持泛型编程的机制,可以实现函数模板和类模板。通过模板,可以编写通用的代码,同时可以避免代码重复和类型不匹配的问题。
- C++的函数模板(Function Template)是一种定义通用函数的方法,可以接受不同类型的参数,实现代码的复用和扩展。函数模板的定义以template关键字开始,后面跟着参数列表和函数体。
- C++的类模板(Class Template)是一种定义通用类的方法,可以接受不同类型的模板参数,实现代码的复用和扩展。类模板的定义以template关键字开始,后面跟着参数列表和类体。
- C++的模板可以实现通用的算法和数据结构,同时也可以支持多态性和动态绑定。但需要注意模板的使用和实例化的方式,以及模板的代码量和可读性问题。
模板参数列表不能为空;且模板类型参数前必须使用关键字class或者typename,在模板参数列表中这两个关键字含义相同,可互换使用。
-
函数模板:通过定义一个函数模板,可以避免为每一种类型定义一个新函数。
- 对于函数模板而言,模板类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。
- 函数模板实例化:当调用一个模板时,编译器用函数实参来推断模板实参,从而使用实参的类型来确定绑定到模板参数的类型
-
类模板:类似函数模板,类模板以关键字 template 开始,后跟模板参数列表。但是,编译器不能为类模板推断模板参数类型,需要在使用该类模板时,在模板名后面的尖括号中指明类型。
-
函数模板和类模板的区别
- 实例化方式不同:函数模板实例化由编译程序在处理函数调用时自动完成,类模板实例化需要在程序中显式指定。
- 实例化的结果不同:函数模板实例化后是一个函数,类模板实例化后是一个类。
- 默认参数:类模板在模板参数列表中可以有默认参数。
- 特化:函数模板只能全特化;而类模板可以全特化,也可以偏特化。
- 调用方式不同:函数模板可以隐式调用,也可以显式调用;类模板只能显式调用。
-
可变参数模板。接受可变数目参数的模板函数或模板类。将可变数目的参数被称为参数包,包括模板参数包和函数参数包。
-
模板特化。
模板特化:模板参数在某种特定类型下的具体实现, 应对通用模板的定义对特定类型不合适的情况。分为函数模板特化和类模板特化。- 函数模板特化:将函数模板中的全部类型进行特例化,称为函数模板特化。
- 类模板特化:将类模板中的部分或全部类型进行特例化,称为类模板特化。
- 特化分为全特化和偏特化:
全特化:模板中的模板参数全部特例化。
偏特化:模板中的模板参数只确定了一部分,剩余部分需要在编译器编译时确定。 - 函数重载与函数模板特化
定义函数模板的特化版本,本质上是接管了编译器的工作,为原函数模板定义了一个特殊实例,而不是函数重载,函数模板特化并不影响函数匹配。
STL
C++的STL(Standard Template Library)是一组通用的模板类和函数的集合,包括容器、算法、迭代器和函数对象等。STL提供了高效、灵活和可复用的工具,方便开发人员进行数据结构和算法的实现。
C++的STL包含以下几个部分:
- 容器(Container):STL提供了多种容器,包括序列容器、关联容器和容器适配器等。容器用于存储数据,并提供了多种操作方法,如插入、删除、查找、排序等。
- 迭代器(Iterator):STL提供了多种迭代器,包括输入迭代器、输出迭代器、前向迭代器、双向迭代器和随机访问迭代器等。迭代器用于遍历容器中的元素,并提供了多种操作方法,如移动、比较、解引用等。
- 算法(Algorithm):STL提供了多种算法,包括查找、排序、变换、合并和复制等。算法用于对容器中的元素进行操作,并提供了多种函数接口,如for_each、find_if、count_if等。
- 函数对象(Function Object):STL提供了多种函数对象,包括一元函数对象、二元函数对象和谓词函数对象等。函数对象用于封装函数,并提供了多种操作方法,如调用、比较、合并等。
C++的STL提供了高效、灵活和可复用的数据结构和算法,可以大大简化程序的开发和维护,同时也需要注意对内存管理和性能优化的影响。
vector原理
- 为何map和set不能像vector一样有个reserve函数来预分配数据
在map和set内部存储的已经不是元素本身了,而是包含元素的结点。也就是说map内部使用的Alloc并不是map<Key, Data, Compare, Alloc>声明的时候从参数中传入的Alloc。
移动语义与右值引用
右值:表达式结束就不再存在的临时对象。右值引用必须绑定到右值的引用,通过 && 获得。右值引用只能绑定到一个将要销毁的对象上,因此可以自由地移动其资源。std::move可以将一个左值强制转化为右值,继而可以通过右值引用使用该值,以用于移动语义。
C++的移动语义是一种优化技术,它允许对象的资源所有权从一个对象转移到另一个对象,而无需进行深拷贝或浅拷贝。移动语义可以大大提高程序的性能和效率,特别是在处理大量数据时。移动语义可以通过移动构造函数和移动赋值运算符来实现,它们使用右值引用作为参数,可以在不复制数据的情况下将对象的资源所有权转移给另一个对象。
// std::move() 函数原型
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type &&>(t);
}
-
引用折叠原理
- 右值传递给函数的形参 T&& 依然是右值,即 T&& && 相当于 T&&。
- 左值传递给函数的形参 T&& 依然是左值,即 T&& & 相当于 T&。
小结:通过引用折叠原理可以知道,move() 函数的形参既可以是左值也可以是右值。
-
std::move() 实现原理:
- 利用引用折叠原理将右值经过 T&& 传递类型保持不变还是右值,而左值经过 T&&变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变;
- 通过 remove_refrence 移除引用,得到具体的类型 T;
- 最后通过 static_cast<> 进行强制类型转换,返回 T&& 右值引用。
New delete与malloc free
malloc/free是C/C++标准库函数,new/delete是C++运算符。他们都可以用于动态申请和释放内存。
-
对于内置类型数据而言,二者没有多大区别。malloc申请内存的时候要制定分配内存的字节数,而且不会做初始化;new申请的时候有默认的初始化,同时可以指定初始化;
-
对于类类型的对象而言,用malloc/free无法满足要求的。对象在创建的时候要自动执行构造函数,消亡之前要调用析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制之内,不能把执行构造函数和析构函数的任务强加给它,因此,C++还需要new/delete。
-
都是在堆(heap)上进行动态的内存操作。用malloc函数需要指定内存分配的字节数并且不能初始化对象,new会自动调用对象的构造函数。delete会调用对象的destructor,而free不会调用对象的destructor。delete首先执行该对象所属类的析构函数;进而通过调用 operator delete 的标准库函数来释放所占的内存空间。
-
new和malloc是否申请到内存判断
malloc :成功申请到内存,返回指向该内存的指针;分配失败,返回 NULL 指针。
new :内存分配成功,返回该对象类型的指针;分配失败,抛出 bac_alloc 异常。 -
delete 和 delete [] 的区别:
delete 用来释放单个对象所占的空间,只会调用一次析构函数;
delete [] 用来释放数组空间,会对数组中的每个成员都调用一次析构函数。
inline
-
类内定义成员函数默认是内联函数。
-
类外定义成员函数,若想定义为内联函数,需用关键字声明。
-
当在类内声明函数,在类外定义函数时,如果想将该函数定义为内联函数,则可以在类内声明时不加 inline 关键字,而在类外定义函数时加上 inline 关键字。
-
内联函数是真正的函数,和普通函数调用的方法一样,在调用点处直接展开,避免了函数的参数压栈操作,减少了调用的开销。内联函数是在编译时展开,而宏在编译预处理时展开;在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。
const
-
const 修饰成员变量,定义成 const 常量,相较于宏常量,可进行类型检查,节省内存空间,提高了效率。
-
const 修饰函数参数,使得传递过来的函数参数的值不能改变。
-
const 修饰类的成员变量。该成员变量只能在类内声明、定义,在构造函数初始化列表中初始化,只在某个对象的生存周期内是常量,对于整个类而言却是可变的,因为类可以创建多个对象,不同类的 const成员变量的值是不同的。因此不能在类的声明中初始化 const 成员变量,类的对象还没有创建,编译器不知道他的值。
-
const 修饰类的成员函数。该成员函数不能修改任何类型的成员变量(mutable 修饰的变量除外),也不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量。
static
static 作用于变量影响变量的存储方式、作用域及默认初始化值。
static 作用于类的成员变量和类的成员函数,使得类变量或者类成员函数和类有关,即可以不定义类的对象就可以通过类访问这些静态成员。注意:类的静态成员函数中只能访问静态成员变量或者静态成员函数,不能将静态成员函数定义成虚函数。
-
static 静态成员变量:
- 静态成员变量是在类内进行声明,在类外进行定义和初始化,在类外进行定义和初始化的时候不要 出现 static关键字和private、public、protected 访问规则。
- 静态成员变量相当于类域中的全局变量,被类的所有对象所共享,包括派生类的对象。
- 静态成员变量可以作为成员函数的参数,而普通成员变量不可以。
- 静态数据成员的类型可以是所属类的类型,而普通数据成员的类型只能是该类类型的指针或引用。
- static数据成员独立于该类的任意对象而存在;每个static数据成员是与类关联的对象,并不与该类的对象相关联。Static数据成员(const static数据成员除外)必须在类定义体的外部定义。不像普通数据成员,static成员不是通过类的构造函数进行初始化,而是应该在定义时进行初始化。
-
static 静态成员函数
- 静态成员函数不能调用非静态成员变量或者非静态成员函数,因为静态成员函数不属于任何对象的组成部分,没有 this 指针。静态成员函数做为类作用域的全局函数。
- 静态成员函数不能声明成虚函数(virtual)、const 函数和 volatile 函数,因为static成员不是任何对象的组成部分。
explicit 的作用
用来声明类构造函数是显示调用的,而非隐式调用,可以阻止调用构造函数时进行隐式转换。只可用于修饰单参构造函数,因为无参构造函数和多参构造函数本身就是显示调用的,再加上 explicit 关键字也没有什么意义。
lambda 表达式
C++11引入了Lambda表达式,Lambda表达式是一种匿名函数,可以在函数内部定义函数,并且可以访问外部的变量和函数。Lambda表达式可以作为函数参数、返回值和本地变量使用,可以简化程序的编写和阅读,特别是在使用STL算法和多线程编程时。
// lambda 表达式的定义形式如下:
[capture list] (parameter list) -> reurn type
{
function body
}
友元
C++的友元函数(Friend Function)和友元类(Friend Class)是一种允许在类外部访问类的私有成员的机制。通过友元函数和友元类,可以实现不同类之间的信息交流和协作,同时保护类的封装性。
- C++的友元函数需要在类中进行声明,表示该函数是类的友元,可以访问类的私有成员。友元函数可以是全局函数、类成员函数和其他类的成员函数,但不属于类的成员函数。
- C++的友元类需要在类中进行声明,表示该类是当前类的友元,可以访问当前类的私有成员。友元类可以访问当前类的所有成员函数和数据成员,但不属于当前类的成员函数。
C++的友元函数和友元类可以提高不同类之间的信息交流和协作,但也需要注意对类的封装性和数据安全的影响。同时,过度使用友元函数和友元类也可能导致代码的可读性和可维护性降低。
public、protected、private的区别
- private,public,protected的访问范围:
- private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问
- protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
- public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问
- 类的继承后方法属性变化:
- 使用private继承,父类的所有方法在子类中变为private;
- 使用protected继承,父类的protected和public方法在子类中变为protected,而private方法不变;
- 使用public继承,父类中的方法属性不发生改变;
封装
面向对象编程中的封装是指将数据和行为组合在一起,并限制对它们的访问。它可以帮助我们隐藏内部实现细节,从而提高代码的可维护性和可重用性。
在面向对象编程中,封装有以下几种形式:
- 公有(public):公有成员可以被类的内部和外部访问,它们是类的接口,用于与外部交互。
- 私有(private):私有成员只能被类的内部访问,它们用于实现类的内部逻辑,隐藏实现细节。
- 受保护的(protected):受保护的成员可以被类的内部和派生类访问,它们用于实现继承关系。
封装的目的是为了保护数据的完整性和安全性,同时也可以提高代码的可维护性和可重用性。通过封装,我们可以将数据和行为组合在一起,形成一个独立的单元,这样就可以将复杂的系统分解成更小的模块,从而更容易进行维护和更新。
类是“汇总”“隐藏”和“创建很多个”的结构,抽象数据类型。
- “汇总”子程序和变量。
- “隐藏”只在类内部使用的变量和子程序。
- 从一个类“创建很多个”实例
继承
允许派生类类继承基类的属性和方法。通过继承,子类可以获取父类的所有公共属性和方法,而不需要重新编写它们。这使得代码更加简洁和易于维护。子类还可以添加自己的属性和方法,或者重写父类的方法以实现特定的行为。
继承可以用来实现多态性,这是面向对象编程中的另一个重要概念。多态性允许不同的对象对相同的消息作出不同的响应。通过继承,子类可以继承父类的方法,并且可以重写这些方法以实现不同的行为。这使得我们可以使用相同的方法调用来处理不同类型的对象。
总之,继承是面向对象编程中的一个重要概念,它允许我们创建更加灵活和易于维护的代码。
具体来说,在超类中定义全集共同的性质,在子类中定义子集特有的性质,这样我们就可以不用重复定义非常相似的、只存在细微差别的类的共同点和不同点了。像这样将共同的性质定义在超类中,再创建子类就很轻松了。
多态
多态是指的是同一个方法可以被不同的对象调用,产生不同的结果。多态使得代码可以更加灵活和可扩展。
继承是指子类可以继承父类的属性和方法,子类可以重写父类的方法。方法重写是指子类可以重写父类的方法,使得同名方法在子类中实现不同的功能。
C++的多态性用一句话概括:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
this指向类占内存块的首地址。
每一个含有虚函数(无论是其本身的,还是继承而来的)的类都至少有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的函数指针,每个类的对象中有一个指向虚表开始的虚指针。
- 静态多态:编译期间的多态,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。
- 动态多态(动态绑定):即运行时的多态,在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。
虚函数
C++的虚函数(Virtual Function)是一种允许在派生类中重写基类成员函数的编程技术。
虚函数是一个在基类中被 virtual 关键字修饰的成员函数,在派生类中被重写(覆盖),允许动态地选择在运行时调用的函数实现。通过使用虚函数,可以在派生类中实现特定的行为,并通过基类指针或引用来调用派生类中的函数。
每个拥有虚函数的类对象都要维护一个虚函数表,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为“虚表指针”),通过虚表指针可以找到类对应的虚函数表,虚函数的地址保存在虚函数表中,因此在使用虚函数的时候都会产生一定的系统开销。
虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,这张虚函数表就指明了实际应该调用的函数。
虚函数表在编译阶段建立,虚函数表和类绑定,虚表指针和对象绑定。即类的不同的对象的虚函数表是一样的,但是每个对象都有自己的虚表指针,来指向类的虚函数表。虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。
编译器处理虚函数表:
- 编译器将虚函数表的指针放在类的实例对象的内存空间中,该对象调用该类的虚函数时,通过指针找到虚函数表,根据虚函数表中存放的虚函数的地址找到对应的虚函数。
- 派生类没有重新定义基类的虚函数 A,则派生类的虚函数表中保存的是基类的虚函数 A 的地址,也就是说基类和派生类的虚函数 A 的地址是一样的。
- 派生类重写了基类的某个虚函数 B,则派生的虚函数表中保存的是重写后的虚函数 B 的地址,也就是说虚函数 B 有两个版本,分别存放在基类和派生类的虚函数表中。
- 如派生类重新定义了新的虚函数 C,派生类的虚函数表保存新的虚函数 C 的地址。
构造函数不能是虚函数。虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数——构造函数了。
析构函数最好定义为虚函数,防止内存泄露。
- 若基类的析构函数是虚函数,则delete p(基类)的时候,它很机智的先执行了派生类的析构函数,然后执行了基类的析构函数。若基类的析构函数不是虚函数,在delete p(基类)时,调用析构函数时,只会看指针的数据类型,而不会去看赋值的对象,只调用基类的析构函数,这样就会造成内存泄露。
- 编译器总是根据类型来调用类成员函数。但是一个派生类的指针可以安全地转化为一个基类的指针。这样删除一个基类的指针的时候,C++不管这个指针指向一个基类对象还是一个派生类的对象,调用的都是基类的析构函数而不是派生类的。如果你依赖于派生类的析构函数的代码来释放资源,而没有重载析构函数,那么会有资源泄漏。
不能声明为虚函数的有:1、静态成员函数; 2、类外的普通函数; 3、构造函数; 4、友元函数
虚函数是为了实现多态特性的。虚函数的调用只有在程序运行的时候才能知道到底调用的是哪个函数,其是有有如下几点需要注意:
- 类的构造函数。构造函数是为了构造对象的,所以在调用构造函数时候必然知道是哪个对象调用了构造函数,所以构造函数不能为虚函数。
- 类的静态成员函数。类的静态成员函数是该类共用的,与该类的对象无关,静态函数里没有this指针,所以不能为虚函数。
- 内联函数inline。内联函数的目的是为了减少函数调用时间。它是把内联函数的函数体在编译器预处理的时候替换到函数调用处,这样代码运行到这里时候就不需要花时间去调用函数。inline是在编译器将函数类容替换到函数调用处,是静态编译的。而虚函数是动态调用的,在编译器并不知道需要调用的是父类还是子类的虚函数,所以不能够inline声明展开,所以编译器会忽略。
- 元函数。友元函数与该类无关,没有this指针,所以不能为虚函数。
纯虚函数在类中声明时,加上 =0。含有纯虚函数的类称为抽象类(只要含有纯虚函数这个类就是抽象类),类中只有接口,没有具体的实现方法;继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。
- 抽象类对象不能作为函数的参数,不能创建对象,不能作为函数返回类型;
- 可以声明抽象类指针,可以声明抽象类的引用;
- 子类必须继承父类的纯虚函数,并全部实现后,才能创建子类的对象。
重载、重写、隐藏的区别
- 重载,与多态无关。是指同一可访问区内被声明几个具有不同参数列(参数的类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
- (1)相同的范围(在同一个类中);
- (2)函数名字相同;
- (3)参数不同;
- (4)virtual 关键字可有可无。
- 覆盖(重写)是指派生类函数覆盖基类函数,与多态真正相关。是指派生类中存在重新定义的函数。函数名、参数列表、返回值类型都必须同基类中被重写的函数一致,只有函数体不同。派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有 virtual 修饰。
- (1)不同的范围(分别位于派生类与基类);
- (2)函数名字相同;
- (3)参数相同;
- (4)基类函数必须有virtual 关键字。
- “隐藏”是指派生类的函数屏蔽了与其同名的基类函数。是指派生类的函数屏蔽了与其同名的基类函数,主要只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
- (1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
- (2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)
智能指针
RAII全程为Resource Acquisition Is Initialization(资源获取即初始化),RAII是C++语法体系中的一种常用的合理管理资源避免出现内存泄漏的常用方法。以对象管理资源,利用的就是C++构造的对象最终会被对象的析构函数销毁的原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。
C++的智能指针是一种RAII技术的应用,它提供了一种安全、自动的内存管理方式。智能指针可以自动管理动态分配的内存,并在不再使用时自动释放内存,解决了动态内存分配时忘记释放内存导致的内存泄漏以及多次释放同一块内存空间的问题。
C++标准库提供了三种智能指针:unique_ptr、shared_ptr和weak_ptr。C++11 中封装在了 #include < memory > 头文件中。
-
共享指针(shared_ptr):共享资源所有权的指针。资源可以被多个指针共享,使用计数机制表明资源被几个指针共享。通过 use_count() 查看资源的所有者的个数,可以通过 unique_ptr、weak_ptr 来构造,调用 release() 释放资源的所有权,计数减一,当计数减为 0 时,会自动释放内存空间,从而避免了内存泄漏。
-
独占指针(unique_ptr):独享所有权的智能指针,资源只能被一个指针占有,该指针不能拷贝构造和赋值。但可以进行移动构造和移动赋值构造(调用move() 函数),即一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,可以通过该方法进行赋值。
-
弱指针(weak_ptr):共享资源的观察者,需要和 std::shared_ptr 一起使用,不影响资源的生命周期。指向 shared_ptr 指向的对象,能够解决由shared_ptr带来的循环引用问题。当 shared_ptr 管理的资源被释放时,weak_ptr 会自动变成 nullptr。
-
智能指针循环引用问题
如定义了两个类 Parent、Child,在两个类中分别定义另一个类的对象的共享指针,由于在程序结束后,两个指针相互指向对方的内存空间,导致内存无法释放。循环引用:该被调用的析构函数没有被调用,从而出现了内存泄漏。
weak_ptr 对被 shared_ptr 管理的对象存在非拥有性(弱)引用,在访问所引用的对象前必须先转化为 shared_ptr;
weak_ptr 用来打断 shared_ptr 所管理对象的循环引用问题,若这种环被孤立(没有指向环中的外部共享指针),shared_ptr 引用计数无法抵达 0,内存被泄露;令环中的指针之一为弱指针可以避免该情况;
weak_ptr 用来表达临时所有权的概念,当某个对象只有存在时才需要被访问,而且随时可能被他人删除,可以用 weak_ptr 跟踪该对象;需要获得所有权时将其转化为 shared_ptr,此时如果原来的 shared_ptr 被销毁,则该对象的生命期被延长至这个临时的 shared_ptr 同样被销毁。
C++ 11新特性
- auto 关键字:自动类型推导,编译器会在 编译期间 通过初始值推导出变量的类型,通过 auto 定义的变量必须有初始值。
- decltype 关键字:decltype 是“declare type”的缩写,译为“声明类型”。和 auto 的功能一样,都用来在编译时期进行自动类型推导。如果希望从表达式中推断出要定义的变量的类型,但是不想用该表达式的值初始化变量,这时就不能再用 auto。decltype 作用是选择并返回操作数的数据类型。
- lambda 表达式
- 范围 for 语句
- 右值引用与移动语义( 标准库 move() 函数)
- 智能指针
- delete 函数和 default 函数
深拷贝与浅拷贝
-
浅拷贝就是对象的数据成员之间的简单赋值,如你设计了一个类而没有提供它的复制构造函数,当用该类的一个对象去给另一个对象赋值时所执行的过程就是浅拷贝。当数据成员中没有指针时,浅拷贝是可行的;但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象,所以,此时,必须采用深拷贝。c++默认的拷贝构造函数是浅拷贝。
-
深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,而不是一个简单的赋值过程,从而也就解决了指针悬挂的问题。
虚拟内存
- 进程创建时,内核会为进程分配4G的虚拟内存,当进程还没有开始运行时,这只是一个内存布局。实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射)。这个时候数据和代码还是在磁盘上的。当运行到对应的程序时,进程去寻找页表,发现页表中地址没有存放在物理内存上,而是在磁盘上,于是发生缺页异常,于是将磁盘上的数据拷贝到物理内存中。
- 进程运行中,要通过malloc来动态分配内存时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。
- 虚拟空间映射到磁盘空间(事实上也是按需要映射到磁盘空间上,通过mmap,mmap是用来建立虚拟空间和磁盘空间的映射关系的)。
- 虚拟内存机制优点
- 每个程序进程的内存空间都是一致而且固定的(32位平台下都是4G),所以链接器在链接可执行文件时,可以设定内存地址,而不用去管这些数据最终实际内存地址,这交给内核来完成映射关系,便于应用程序开发。
- 当不同的进程使用同一段代码时,比如库文件的代码,在物理内存中可以只存储一份这样的代码,不同进程只要将自己的虚拟内存映射过去就好了,这样可以节省物理内存。
- 在程序需要分配连续空间的时候,只需要在虚拟内存分配连续空间,而不需要物理内存时连续的,实际上,往往物理内存都是断断续续的内存碎片。这样就可以有效地利用我们的物理内存。
内存对齐及内存泄漏
- 进行内存对齐的原因:
- 某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常;
- 某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作,相比于存取对齐的数据,存取非对齐的数据需要花费更多的时间;
- 内存泄漏:由于疏忽或错误导致的程序未能释放已经不再使用的内存。
防止内存泄漏的方法:
- 内部封装:将内存的分配和释放封装到类中,在构造的时候申请内存,析构的时候释放内存。在类的对象复制时,程序会出现同一块内存空间释放两次的情况)
- 智能指针:智能指针是 C++ 中已经对内存泄漏封装好了一个工具,可以直接用。
C++内存管理
内存分区:栈、堆、全局/静态存储区、常量存储区、代码区。
栈:存放函数的局部变量、函数参数、返回地址等,由系统自动分配和释放。栈从高地址向低地址增长。是一块连续的空间。栈一般分配几M大小的内存。
堆:动态申请的内存空间,由程序员控制它的分配和释放( malloc /free),如果程序执行结束还没有释放,操作系统会自动回收。堆从低地址向高地址增长。一般可以分配几个G大小的内存。在堆栈之间有一个 共享区(文件映射区)。
全局区/静态存储区(.BSS 段和 .data 段):存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,程序中未初始化的全局变量和静态变量存放在.BSS 段中,已初始化的全局变量和静态变量存放在 .data 段中,C++ 中不再区分了。
常量存储区(.rodata):存放的是常量,不允许修改,程序运行结束自动释放。
代码区(.text 段):存放程序执行代码的一块内存区域。只读,不允许修改,但可以执行。编译后的二进制文件存放在这里。代码段的头部还会包含一些只读的常量,如字符串常量字面值(注意:const变量虽然属于常量,但是本质还是变量,不存储于代码段)
C++程序编译过程
计算机的编译链接过程
- 预处理(preprocessing):宏(#define/#include/#if)、注释、行号标记,>>(预处理命令)>>组成新的程序。
- 编译(complication):通过此法分析、语法分析等步骤 >> 汇编代码。
- 汇编(assembly):将汇编代码转 >> 机器代码(.obj)。
- 链接(linking):(.obj) >> (.exe文件)。
-
编译预处理:处理以 # 开头的指令,产生 .i 文件;
主要的处理操作如下:对全部的#define进行宏展开。
处理全部的条件编译指令,比方#if、#ifdef、#elif、#else、#endif;
处理 #include 指令,这个过程是递归的,也就是说被包括的文件可能还包括其它文件;
删除全部的注释 // 和 /**/
加入行号和文件标识
保留全部的 #pragma 编译器指令由于全部的宏已经被展开,经过预处理后的 .i 文件不包括任何宏定义。而且包括的文件也已经被插入到 .i 文件里。
-
编译、优化:将源码 .cpp 文件翻译成 .s 汇编代码;
- 词法分析:扫描器(Scanner)将源代码的字符序列分割成一系列的记号(Token)。lex工具可实现词法扫描,Flex词法分析工具,将源代码的字符序列分割成一系列的记号。
- 语法分析:语法分析器将记号(Token)生成语法树(Syntax Tree)。yacc工具可实现语法分析(yacc: Yet Another Compiler Compiler)。Bison语法分析工具。
- 语义分析:判断表达式是否有意义。静态语义(在编译器可以确定的语义)、动态语义(只能在运行期才能确定的语义)。
- 代码优化:源代码优化器(Source Code Optimizer),将整个语法书转化为中间代码(Intermediate Code)(中间代码是与目标机器和运行环境无关的)。中间代码使得编译器被分为前端和后端。编译器前端负责产生机器无关的中间代码;编译器后端将中间代码转化为目标机器代码。
- 目标代码生成:代码生成器(Code Generator)生成汇编代码。
- 目标代码优化:目标代码优化器(Target Code Optimizer)。
编译会将源代码由文本形式转换成机器语言,编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。编译后的.s是ASCII码文件。
-
汇编:将汇编代码 .s 翻译成机器指令的 .o 或.obj 目标文件;
汇编过程调用汇编器AS来完成,是用于将汇编代码转换成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。
汇编后的.o文件是纯二进制文件。 -
链接:产生可运行文件
链接的主要内容是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确地衔接。链接的主要过程包括:地址和空间分配(Address and Storage Allocation),符号决议(Symbol Resolution),重定位(Relocation)等。
汇编程序生成的目标文件,即 .o 文件,并不会立即执行,因为可能会出现:.cpp 文件中的函数引用了另一个 .cpp文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成可执行的程序文件。
详细来说,链接是将所有的.o文件和库(动态库、静态库)链接在一起,得到可以运行的可执行文件。它的工作就是把一些指令对其他符号地址的引用加以修正。
静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行文件会比较大。动态链接则是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去。
-
链接分为两种:
静态链接:代码从其所在的静态链接库中拷贝到最终的可执行程序中,在该程序被执行时,这些代码会被装入到该进程的虚拟地址空间中。把目标程序运行时需要调用的函数代码直接链接到了生成的可执行文件中,程序在运行的时候不需要其他额外的库文件。Windows下以.lib为后缀,Linux下以.a为后缀。
动态链接:代码被放到动态链接库或共享对象的某个目标文件中,链接程序只是在最终的可执行程序中记录了共享对象的名字等一些信息。在程序执行时,动态链接库的全部内容会被映射到运行时相应进行的虚拟地址的空间。
动态 “动” 在了程序在执行阶段需要去寻找相应的函数代码,即在程序运行时才会将程序安装模块链接在一起。
动态链接就是把调⽤的函数所在⽂件模块和调⽤函数在⽂件中的位置等信息链接进目标程序,程序运⾏的时候再从动态库中寻找相应函数代码,因此需要相应⽂件的⽀持。包含函数重定位信息的文件,在Windows下以.dll为后缀,Linux下以.so为后缀。
-
二者的区别:
静态链接是 将各个模块的obj和库链接成一个完整的可执行程序;而动态链接是程序在运行的时候寻找动态库的函数符号(重定位),即动态库不必被包含在最终的可执行文件中。链接使用工具不同:
静态链接由称为“链接器”的工具完成。
动态链接由操作系统在程序运行时完成链接。
库包含限制:
静态链接库中不能再包含其他的动态链接库或者静态库。
动态链接库中还可以再包含其他的动态或静态链接库。
运行速度:
静态链接运行速度快(因为执行过程中不用重定位),可独立运行。
动态链接运行速度慢、不可独立运行。
二者的优缺点:
静态链接:浪费空间,每个可执行程序都会有目标文件的一个副本,这样如果目标文件进行了更新操作,就需要重新进行编译链接生成可执行程序(更新困难);优点就是执行的时候运行速度快,因为可执行程序具备了程序运行的所有内容。
动态链接:节省内存、更新方便,但是动态链接是在程序运行时,每次执行都需要链接,相比静态链接会有一定的性能损失。
-
其他
-
全局对象的构造函数会在main函数之前执行。
-
this指针本质是一个函数参数,只是编译器隐藏起形式的,this只能在成员函数中使用,全局函数、 静态函数都不能使用this。当调用一个类的成员函数时,编译器将类的指针作为函数的this参数传递进去。
this是个指向对象的“常指针”,因此无法改变,它是一个指向相应对象的指针。
-
C++如何阻止一个类被实例化?一般在什么时候将构造函数声明为private
(1)将类定义为抽象基类或者将构造函数声明为private;
(2)不允许类外部创建类对象,只能在类内部创建;
C++的性能优化
优化是一个复杂的过程,需要综合考虑代码、算法、数据结构、编译器和硬件等因素。常用的C++性能优化方法包括以下几个方面:
- 优化算法和数据结构,避免使用低效的算法和数据结构;
- 使用适当的编译器优化选项,例如-O2、-march=native等;
- 减少不必要的函数调用和内存分配,避免频繁的对象复制和构造;
- 使用局部变量和寄存器变量,避免频繁的内存访问和缓存失效;
- 使用内联函数和模板函数,避免函数调用和代码复制;
- 避免不必要的类型转换和对象拷贝,使用引用或指针传递参数;
- 使用正确的数据结构布局和内存对齐,避免内存碎片和缓存对齐问题;
- 使用多线程编程和并行算法,充分利用多核处理器和并行计算资源;
- 使用专业的性能分析工具和调试器,定位瓶颈和优化点。
参考
C++八股文面经总结(含答案),收藏起来慢慢看;