C++八股

1. shared_ptr & weak_ptr

shared_ptr是强引用,类比于用铁丝绑住堆上的对象,只要有一个指向对象x的shared_ptr存在,x就不会被析构;

weak_ptr是弱引用,类比于用棉线系住堆上的对象,它并不控制对象的生命周期,但是可以被提升(promote)为shared_ptr,如果对象不存在则提升失败。提升/lock()行为是线程安全的。weak_ptr有两个最常见的应用场合:

  • 用于取代对象内部的shared_ptr防止循环引用导致资源无法正常释放;
  • 多线程下保证线程安全(判断悬空指针):用weak_ptr可以线程安全地判断该对象是否依然存在。

1.1 shared_ptr

The ownership of an object can only be shared with another shared_ptr by copy constructing or copy assigning its value to another shared_ptr. If two are constructed (or made) from the same (non-) pointer, they will both be owning the pointer without sharing it, causing potential access problems when one of them releases it (deleting its managed object) and leaving the other pointing to an invalid location.

In a typical implementation, shared_ptr holds only two pointers:

  • the stored pointer (one returned by get());
  • a pointer to control block.

The control block is a dynamically-allocated object that holds:

  • either a pointer to the managed object or the managed object itself;
  • the deleter (type-erased);
  • the allocator (type-erased);
  • the number of shared_ptrs that own the managed object;
  • the number of weak_ptrs that refer to the managed object.

Control Block是在堆上的数据,共享同一个managed object的shared_ptr的control block是共享的。

The pointer held by the shared_ptr directly is the one returned by get(), while the pointer/object held by the control block is the one that will be deleted when the number of shared owners reaches zero. These pointers are not necessarily equal.

Create shared_ptr

std::shared_ptr<int> foo = std::make_shared<int> (10);
std::shared_ptr<int> foo2(new int(10));
  • When shared_ptr is created by calling std::make_shared  or std::allocate_shared, the memory for both the control block and the managed object is created with a single allocation, where the managed object is constructed in-place in a data member of the control block.(cache效率更高)
  • When shared_ptr is created via one of the shared_ptr constructors, the managed object and the control block must be allocated separately. In this case, the control block stores a pointer to the managed object.

shared_ptr不需要虚析构也可以正确地释放资源。

Thread Safety

1. It is only the control block itself which is thread-safe.

引用计数的增加或减少是线程安全的:

To satisfy thread safety requirements, the reference counters are typically incremented using an equivalent of std::atomic::fetch_add with std::memory_order_relaxed (decrementing requires stronger ordering to safely destroy the control block).

2. 多线程对shared_ptr进行读(拷贝)写(更改指向)操作并非安全的,需要加锁保护。

如果多个线程共享一个智能指针且该共享智能指针发生了指向修改

void fn(shared_ptr<A>& sp) {
    ...
    if (..) {
        sp = other_sp;
    } else if (...) {
        sp = other_sp2;
    }
}

多个线程用的是一个shared_ptr(因为是引用),而修改指向会发生两件事:

  • refcnt--;
  • rawptr更新;

这两件事并不是原子性的,因此可能发生某个线程在更改指向的过程中,另一个线程导致对象析构,这里把新指向的对象析构了。

1. 两个线程共享一个shared_ptr g;

2. 线程1中执行x = g

执行到一半schedule到线程2;

3. 线程2中执行g = n,一次性执行成功;

4. 线程1继续执行,指向的对象是空的;

如果多个线程的输入不是引用而是拷贝的就没事了。

1.2 weak_ptr

Like std::shared_ptr, a typical implementation of weak_ptr stores two pointers:

  • a pointer to the control block; and
  • the stored pointer of the shared_ptr it was constructed from.

weak_ptr默认构造为空,可以由weak_ptr or shared_ptr构造或赋值。

weak_ptr没有重载*->,提供了以下接口:

  • use_count(): returns the number of shared_ptr objects that manage the object;
  • lock(): creates a shared_ptr that manages the referenced object; If there is no managed object, i.e. *this is empty, then the returned shared_ptr also is empty.
  • expired(): checks whether the referenced object was already deleted.

2. STL相关

1. clear()

clear()的时间复杂度并非一定是O(1),对于存储基本类型的vector,clear()仅仅是修改size而已,但是对于有析构函数的类型,需要O(N)时间复杂度执行析构。

clear()并不会释放vector的内存。

2. vector::erase() vs std::remove()

erase()会删除元素(析构),将后面的元素向前移动,改变vector.size()返回指向原本容器内被删除元素的下一个元素的迭代器;

std::remove()不改变vector.size(),将需要删除的元素用后面的无需删除的元素覆盖,返回新的end()。新end()与end()间的元素处于未定义状态。

3. size()返回的是无符号数

for (int i = 0; i < vec.size(); ++i) {
    ...
}

for (int i = 0; i <= vec.size() - 1; ++i) {
    ...
}

如果vec是空,第二种写法会死循环,因为无符号0减去一会得到最大值。

4. list迭代器

list的迭代器是bidirectional iterator,本身只具有++和--操作,如果想获取下一个/上一个迭代器而不改变当前迭代器的值,需#include <iterator>使用std::next(it, n)std::prev(it, n)

5. map的[] vs at

都是接收key,返回value的引用。

区别:

  1. 如果key不存在,[]会插入,at会抛出异常;
  2. const map可以使用at,不能使用[]。

6. 容器类的移动构造

STL中的容器类的实际数据其实是在堆上的,如果函数返回一个容器类,并不会出现大规模拷贝,移动构造只拷贝一个指针。

3. Lambda的原理

编译器眼里,lambda 其实就是个跟仿函数一样的东西。Lambda的捕获列表就是仿函数类中的成员变量。

4. static数据的初始化

在C++中,静态变量分为全局静态变量(又称全局变量)、局部静态变量(函数中的静态变量)和类中静态成员变量:

  • 对全局变量加static只影响变量的可见性(生命周期本来就到程序终止);
  • 局部变量加static只影响其生命周期(可见性本来就受限);

按照初始化的方式分为静态初始化static initialization)和动态初始化(dynamic initialization):

  • 静态初始化:用常量来对静态变量进行初始化,包括zero initialization和const initialization,其中zero initialization的变量会保存在.bss段(未初始化静态变量,以及初始化为0的静态变量);const initialization的变量保存在.data段(已经初始化为非0的静态变量)。对于静态初始化的变量(请注意:包括在函数中采用静态初始化的静态变量),是在程序加载时完成的初始化。
  • 动态初始化:指的是需要调用函数才能完成的初始化(包括类的构造函数),在程序运行时完成的初始化
  • 对于全局或者类的静态成员变量,是在main()函数执行前由运行时调用相应的代码进行初始化的,也就是说动态初始化static变量的函数会在main前执行;
  • 对于局部静态变量,是在函数执行至此初始化语句时才开始执行的初始化。

5. 虚函数开销

除了微不足道的vtable和vptr的内存开销以及查表的时间开销外,真正的性能影响在于:虚函数无法被编译器优化(不可以被inline),且分支预测率也会下降。

6. 什么函数不可以是虚函数?

构造函数

模板成员函数

静态成员函数

7. 引用和指针区别

引用创建时必须初始化,指针可以不初始化;

引用不可更改指向,指针可以更改指向;

引用只有一级引用,没有多级引用;

8. new与malloc

new是expression,malloc是库函数。

expression new内部的执行步骤:

  • 调用operator new
  • operator new可以重载(可以重载某个类的operator new,也可以重载全局的);
  • operator new内部会调用mallocplacement new不调用);
  • 可能会抛出异常
  • static_castvoid*转换为对应类型;
  • 调用ctor

9. atomic

互斥锁用于保证critical section的代码不会出现race condition,但是互斥锁本身开销较大,对于int自增等等比较轻量级的操作,可以用atomic来保证线程安全。

aotmic使用CAS来保证原子性,是一种无锁操作(不在代码的层面加锁,在硬件上对于总线加锁)。

10. 对象优化

编译器对拷贝构造的优化

C++编译器会对对象的构造进行优化:

临时对象拷贝构造新对象时,直接构造新对象,不会产生临时对象

Test t1(1);
Test t2 = Test(1);

以上两行均只执行一次构造函数。

Test getObject1() {
    Test t(1);
    return t;
}

Test getObject2() {
    return Test(1);
}

int main() {
    Test t1, t2;
    t1 = getObject1();
    t2 = getObject2();
}

在没有移动构造的条件下:

t1:构造,拷贝构造,析构,拷贝赋值,析构;(-fno-elide-constructors -std=c++14

t2:构造,拷贝赋值,析构;

t1的过程中多生成一个临时对象(C++17后临时对象不会再生成了)

如果定义了移动构造,则拷贝构造变成移动构造。

Test getObject() {
    return Test(1);
}

int main() {
    Test t1 = getObject();
}

直接构造,只有一次构造函数。

11. Template

1. void可以作为模板参数

void可以作为模板参数

template<typename T>
T foo(T*)
{ }
void* vp = nullptr;
foo(vp); // OK: 模板参数被推断为 void
foo(void*)

2. 两阶段编译检查

在实例化模板的时候,如果模板参数类型不支持所有模板中用到的操作符,将会遇到编译期错误。在定义的地方并没有遇到错误提示。这是因为模板是被分两步编译的:

  1. 模板定义阶段:定义阶段的检查并不包含参数类型的检查(有些编译器在第一阶段不会执行任何检查);
  2. 模板实例化阶段:为确保所有代码都是有效的,模板会再次被检查,尤其是那些依赖于参数类型的部分;

对于普通函数,编译和链接是分离的,函数在编译阶段只需要声明就够了。但是当实例化一个模板的时候,编译器需要(一定程度上)看到模板的完整定义。

3. 类模板的类型

template<typename T>
class Stack {
  private:
  	std::vector<T> elems; // elements
  public:
  	void push(T const& elem); // push element
  	void pop(); // pop element
  	T const& top() const; // return top element
  	bool empty() const { // return whether the stack is empty
  		return elems.empty();
  	}
};

这个类的类型是 Stack<T>,其中 T 是模板参数。在将这个 Stack<T>类型用于声明的时候,除非可以推断出模板参数的类型,否则就必须使用 Stack<T>(Stack 后面必须跟着<T>)。不过,如果在类模板内部使用 Stack 而不是 Stack<T>,表明这个内部类的模板参数类型和模板类的参数类型相同。

比如,如果需要定义自己的复制构造函数和赋值构造函数,通常应该定义成这样:

template<typename T>
class Stack {
  …
  Stack (Stack const&); // copy constructor
  Stack& operator= (Stack const&); // assignment operator
  …
};

它和下面的定义是等效的:

template<typename T>
class Stack {
  …
  Stack (Stack<T> const&); // copy constructor
  Stack<T>& operator= (Stack<T> const&); // assignment operator
  …
};

如果在类模板的外面,就需要这样定义:

template<typename T>
bool operator== (Stack<T> const& lhs, Stack<T> const& rhs);

12. Explicit

class Point {
public:
    int x, y;

    // Constructor
    explicit Point(int xVal = 0, int yVal = 0) : x(xVal), y(yVal) {}

    void display() const {
        cout << "(" << x << ", " << y << ")" << endl;
    }
};

void printPoint(const Point& p) {
    p.display();
}

int main() {
    Point p1(10, 20);
    printPoint(p1); // OK

    // Point p2 = 30; // Error if constructor is explicit
    Point p2(30); // OK
    printPoint(p2);
    // printPoint(10); // Error if constructor is explicit

    return 0;
}

Usage of explicit

  • Constructors: The explicit keyword can be applied to constructors to prevent the compiler from using that constructor for implicit conversions.
  • Conversion Operators: Since C++11, the explicit keyword can also be used with conversion operators to prevent implicit conversions.

全部评论

相关推荐

07-24 12:30
湘潭大学 营销
点赞 评论 收藏
分享
评论
8
37
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务