c++概念回顾

c++概念回顾

不知不觉用了c++八年了,近期笔者在准备面试,希望借写这篇文章,复习一下这些年所学的c++知识,难免会有差错,欢迎勘误:

一些基本概念

  1. 空类,其实并不空,c++会自行生成四种特殊成员函数,分别是默认构造,析构,复制构造以及复制赋值运算符,不过只在需要的时候才会生成。c++11中多了两种,移动构造与移动赋值。这里稍微有点绕的地方在于,两个复制函数相对独立,如果声明了一个复制函数,不会影响编译器自行生成另一个复制函数。移动函数则不是,如果声明了其中一个,编译器就不会再默认生成另一个,甚至如果声明了复制类的函数,编译器就不会自行生成移动类的函数,反过来同理,这里面的思想在于如果声明了自己的复制类函数,就代表有着特殊的内存管理需求,那么默认生成的移动函数就极有可能有问题,所以编译器并不会过多画蛇添足。关于内存管理,也要注意深拷贝浅拷贝的问题。
  2. 对空类sizeof,结果也并不是0,因为c++标准保证任何不同的对象不能拥有相同的地址,所以编译器会给空类添加一个或多个虚设的字节。
  3. 有一些特殊成员函数不准备实现但又不想调用者使用默认函数,在c++98的做法是声明为私有成员函数,不做实现,这样调用者就不能访问他们了。c++11上则是用delete关键字。
  4. 对于不想隐式转换的单参数构造函数,可以加explici关键字,这样就不能隐式转换了,并且对单参数的构造函数,总是推荐加explicit。
  5. virtual关键字修饰成员函数,代表虚函数,实现动态多态。基类函数如果没有virtual修饰,则在子类中基类的函数总是被覆盖了的,无论子类是否加关键字。这里有一个重要的概念是虚函数表,虚函数是通过查找类的虚函数表来实现多态的,所以会有一个自己的虚函数表指针,指向虚函数表。如果一个类没有成员变量但有虚函数,那么他的大小就是一个指针的大小。说到这里讲一个插曲,曾经同事遇到一个问题,让笔者一起看看,一个类的实例调用某个成员函数会莫名crash,调用其他成员函数正常,没有递归不太可能是调用堆栈用尽,实例的指针也不为空,就是不知道为什么会crash。笔者也是看了一会才想到,这个函数是不是虚函数,一看果然是。这个指针在前面的调用已经被delete掉但是没有设置为nullptr,在调用普通成员函数的时候,只要去代码段找到对应的函数实现就可以,不会有问题。但是虚函数需要取虚函数指针,此时原有内存已经被析构掉,访问自然会crash。
  6. 如果一个虚函数声明的时候 在后面加上“=0”,并且没有函数体,就是纯虚函数。有着纯虚函数的类称为抽象类,抽象类不能实例化。只有抽象类的子类,且实现纯虚函数才能实例。
  7. override关键字,在基类中修饰成员函数,代表要重写基类的同名函数。如果要重写基类的同名函数,总是使用这个关键字,避免一些笔误的出现。
  8. static关键字也是很重要的概念,限于篇幅只写比较重要的一些。static修饰的全局变量,在main函数前就会进行初始化,有初始化值的存放到data段,没有初始化值的存放到.bss段。但是局部静态变量是在第一次进入函数的时候初始化的。对于类,static修饰的成员变量是所有实例共享,成员函数可以不用实例就调用,但是只能调用static变量,这也是很正常的,毕竟没有实例就没有this指针。
  9. const关键字修饰的变量,简单说就是不能修改。经常碰见的题是const in const *p = 1;前面那个const修饰的是指针指向的内容,后面的const修饰的是指针本身,就是不可以指向其他内容。与const对应的是mutable,用const修饰成员函数,意味着这个函数不可以改变成员变量,但是有些时候函数里又必须改变某个变量,这时候可以用mutable。还有就是可以通过const_cast去掉cosnt属性,不过很多时候不推荐使用。
  10. 说到const就想到constexpr,用来表明某个编译期就可以确定值的对象或者表达式。但事实上在函数上并不总如此,只有在参数都是constexpr的时候,结果才会是编译期确定的。推荐在能用constexpr的地方都用上,也就是能编译期确认的值就别放到运行时去。这里也引出模板元编程的概念,感兴趣的读者可以去研究一下。
  11. inline关键字,用来表示成员函数内联。内联函数的优势在于编译期在函数调用的地方直接替换代码,避免了函数调用,但是也会造成代码膨胀。可能会问虚函数可以内联吗?在虚函数没有表现多态的时候,也就是明确知道对象是哪个类的时候是可以的,但是多态的时候不可以。
  12. auto关键字,类似其他语言的var。很多时候是推荐能用auto就用的,少打代码是一个原因,还有因为有时候自己以为的类型与实际类型并不一致,类型隐式转换就会导致性能损失。不过auto用的多了也真的影响代码的阅读体验。
  13. decltype,多用来推导表达式类型。它与auto不同的地方在于,auto会推导出类型但忽略修饰词,const,&等,而decltype不会,并且decltype不会实际执行这个表达式。简单说,大多数时候,声明的是什么类型,decltype就推导出什么类型。
  14. lambda表达式。极大的方便了代码的编写,注意编译器最终生成的是一个匿名类。

内存管理

  1. new/delete,不用多说,永远保证一个new就有一个detete,但是人不是机器,怎么能保证不犯错?所以应该使用智能指针,unique_ptr, shared_ptr, weak_ptr. 智能指针也是RAII的一种典型表现。具体使用方法限于篇幅不展开,说些需要注意的地方。虽然智能指针会帮我们回收内存,但是我们也不能大意,同一块内存赋给两个智能指针,或者两个shared_ptr互相持有循环引用都会造成内存问题。
  2. 还有就是大多数时候都应该使用make_unique, make_shared。某些情况下可以避免内存泄漏和性能损失。
  3. 虽然shred_ptr的引用步数是原子的,但是shared_ptr本身并不是线程安全的。
  4. weak_ptr也有着不小的作用,譬如当一块内存过大不适合长期持有,但又可能在后期用到,这时候就可以用weak_ptr,如果这块内存没有被释放掉就获取之。
  5. 正常情况下unique_ptr的尺寸和一个裸指针是一样的,不过在有自定义析构器的情况下就不一定了,如果析构器有太多的成员,unique_ptr的尺寸会迅速膨胀。而share_ptr正常情况下尺寸是两个指针的大小,析构器也并不会增加尺寸。
  6. c++不注意就有性能损失的地方,传参算是一个。如果传入的是一个临时变量,可能要经历构造,复制,析构的过程。所以c++11引入了右值引用的概念,也就是typename&&,这个时候就可以t通过std::move将左值转换成右值,告诉编译器这虽然是一个左值,但是我们希望当成右值来处理,从而直接转移内存所有权,避免了复制析构的性能损失,但是move后的变量不再做任何保证,不能再次使用。所以move永远只用在即将销毁的变量上。
  7. std::move, 移动语义,补充一点。理解左值右值的概念。字面意思是等号左边与右边的值,概括来说,左值可以取址,右值是匿名的不能取址。而move并不真的移动什么东西,它只是把左值强制转换成右值,并且是在编译期就做完了,运行期不做任何事情另外move并不总是能如愿“move”的,在不能move的时候就只能复制了。
  8. std::forward完美转发。意思是一个函数转发自己的形参给另一个函数,同时保留原有特征,左值,右值,类型等。同样也是编译期完成,运行期不做任何事。需要注意的是只有在实参是用右值完成初始化,才会执行向右值类型的强制类型转换。

泛型

  1. 终于说到了泛型,展开了说又是很大的篇幅,还是只说一些需要注意的地方。泛型是静态多态,与重载动态多态对应。
  2. 对于某个特定的类型需要特殊的实现,可以模板特化,也就是手动实现这个类型。对应的不需要某个指定类型,可以用delete关键字,这样就不可以使用这个特定类型具现了。
  3. 变长参数模板是递归调用每个参数的。
  4. 模板函数也是不能是虚函数的,同样的原因,模板是编译期确认,虚函数是运行期才能确定类型。
  5. 先说一下引用折叠,也就是&&与&。概括来说就是,只有右值引用折叠右值引用,才是右值引用,其他折叠后都是左值引用。
  6. 为了避免传参的时候传值引起的性能损耗,相信会有很多参数是T&的模板参数。但是这样就会有个问题,在于如果调用者用的时候传入的是“tt”这样的右值的时候,函数实现将这个字符串赋值给另一个变量,所以又是构造,复制,析构的流程,为什么不能将“tt”这个临时变量直接转移给需要赋值的变量,从而避开这样的损耗?universal reference可以,在模板参数是T&&类型,并且T的类型是推导而来的时候,这个形参的类型就是universal reference。对于参数是T&& str的函数,内部实现std::string s = std::forward(str),就能避免这样的损耗。因为根据引用折叠的规则,如果实参不是右值引用,折叠后必然不是右值引用,forward就会转发失败,也就不需要担心转移了左值。而实参是右值的话,则会转发成功,避免了损耗。

多线程

  1. c++11增加了对多线程的支持,future,async,packaged_task都非常好用。以前的思想可能是基于线程,现在更推荐基于task。具体到c++11上,就是更推荐用async,相较于thread,因为前者是STD帮我们在做线程调度避免超定,后者则是肯定会创建一个新线程。
  2. 一个好消息是c++11中初始化被定义为只发生在同一个线程上,这样就再也不用写双重校验锁double-checked locking了。而只需要声明一个局部静态变量,返回它的引用就好。当然用std::call_once()也可以
  3. 对于死锁的处理。可以通过对不同锁设置不同的id,每次总是按顺序要求锁,是一个很好的思路,当然也可以用unique_lock设置deffer_lock,可以一次性锁两个unique_lock而不死锁
  4. 用std::condition_variable等待条件达成。需要注意的是条件达成的函数会调用很多次,所以注意这个函数不要有side effect
  5. volatile关键字放在这里说,是因为这个关键字本身并不保证顺序性和原子性,并不能用来同步。但是vs做了扩展,加了顺序性。所以同样的代码vs编译的可能有效,但是gcc就不行
  6. thread_local变量在新的线程上会重新初始化
  7. c++11也增加了对原子操作的支持,也引出了无锁编程的概念,happen before是核心理论。主要还是在于不同线程write-relase与read-acquire形成synchronizes-with的关系,读操作发生于写操作后,写操作前的代码必然在读操作以及读操作后的代码前生效。无锁编程在于线程不需要切换内核态用户态,没有上下文切换的性能损失。
  8. CAS要注意ABA的问题。概括来说,A有存款100元,因为某个技术问题,他取款50的操作提交了两遍,此时一个线程提交成功另一个线程阻塞,存款变为50,正在这时候B给他汇款50元成功,存款变为100元,此时阻塞的线程就提交成功。这样就丢失了50元。
  9. 后面如果有时间多线程和无锁编程单独写一篇。

STD

  1. vector注意扩容的时候,需要复制每个元素,性能影响很大,相较于push_back,推荐使用emplace_back,因为后者用的std::forward,参考26
  2. 尽量使用const_iterator
  3. map,multimap底层是红黑树,而unordered_map用的是hash。前者是O(logn),后者最差是O(n),但是建立hash表比较费时。另外前者内部数据是有序的,后者是无序的。在数量不大的情况下,推荐使用map。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值