effective Morden cpp学习笔记

1.理解类型推导

1.1如下伪代码:

template <typename T>
void f(paramtype param);//函数的大致模板

//调用形式如下
f(expr);

对于模板推导,T不仅仅依赖于expr类型,还依赖与paramtype类型,这点很重要。

一般分为三种类型:

  1)paramType是一个指针或者引用类型,不是万能引用。此时的推导结果如下:

如上,常量(const)和关键字violate修饰会被保存,但是引用形式会被T给清除。若param自带上述前面的两种,则直接推导出就是普通类型。

2)parmaType是一个万能引用 则推导结果parmaType一定是一个引用类型,且保留const/violate关键字(还要通过引用折叠),而T推导也会保留且保留const/violate关键字,若传入的是左值,T则推导为左值引用,若传入的是右值,则推导为常规类型(不是右值引用)

3)parmaType既不是指针,也不是引用,则推导结果的T是去除const/violate关键字和引用指针。(模板参数的类型就是T,没有引用和关键字修饰)

1.2对于传入的是函数类型或者是数组类型,则模板推导中,前者会退化为指针,或者退化为函数指针。

const char name[ ]="hello";
const char* p="hahah";

template <typename T>
void f(T QQ);

f(name)------>推导为const char* 的指针,数组进行退化

template <typename T>
void f(T& QQ);

f(name)-------->推导为const char (&) [],加上引用后这种情况下就会推导为实际的数组类型。


观察下列函数类型

void sumFunc(int,double);函数类型

上述推导和数组一致。

2.理解auto自动推导和其使用好处

2.1)auto推导的方式和模板的推导方式几乎一样。

auto& func=someFunc----->类型推导是void(&)(int,double),就是函数类型,不是函数指针

2.2)在写程序的时候,很有可能会遇见类型不匹配的问题,使得=右边的类型转换为=左边的类型,使用auto推导则可以避免如此问题(类型不匹配意味着编译器会再次进行复制操作,在转化为=左边的类型,降低效率,同时还可以提高代码的美观程度)。

2.3)auto有一点不同的就是注意小括号和大括号初始化

回忆std::vector<>类初始化
std::vector<int> arr1(10,9);---->10个元素,每个元素都是9
std::vector<int> arr2{10,9};---->两个元素,一个10,一个9.

同理auto也有大小括号之分。
auto p(3)----->p是int类型的值,值为3
auto p1={1,2,3,4}------->这里auto自动推导结果为
std::initializer_list<int>类,元素合计为1,2,3,4
std::initializer_list<T>是一个模板。且大括号初始化产物的类型必须相同。

auto p2={1.0,2,4,5};这样又有int,又有double类型,无法通过编译

对于函数返回值使用auto进行推导,如果返回的是大括号产物,则无法推导。同理auto用来指定λ表达式的形参类型时,可不能使用大括号括起的初始化表达式。

注意要点2,若一个函数返回的是一个引用类型,而返回值是auto。结果推导则是去掉引用。想要保留类型结果就要用到decltype(auto).

2.4)优先选用auto,而不是显示类型声明。

auto p;//编译错误
auto p=1;------->auto声明的变量必须要进行初始化。

采用auto定义的λ表达式比std::function的闭包函数(创建的拉姆达表达式复制初始化给std::function对象)要更加节约内存和使用更快,所以优先采用auto,而不是std::function<>的闭包函数。

2.5)当auto推导类型不合法(不符合预期的时候),尽量使用带显式类别的初始化物习惯用法,或用强转进行转化为用户期望的类型。

这里注意,代理类型,比如long,将其进行“包装”为代理类型为size_t。这样经过包装的类型,尽量不要用auto,而是用static_cast<>进行显示强转换为想要的类型。

static_cast<>也可以进行const/非const之间的强转,但关于const/非const之间,最好用const_cast<>.

3.理解decltype关键字推导。

如下图片将直观讲述decltype的作用:

由于auto推导方式是模板方式。单单一个auto,其推导结果是去掉引用或者是去掉const/violate关键字等等。如果要想保留上述信息,则需要decltype(auto),如下图,同时还是模板编程中运用最广泛的形式,下面书写方式牢记。

注意上面decltype推导的特殊情况,对名字加上小括号(),推导结果确实引用类型。

        4.在创建对象时注意区分()和{}

C++初始化中分为大括号初始化,小括号初始化,=初始化。对于大括号初始化,其不允许在内建类型之间进行隐式窄化类型转换(上往下的类型转换)

int p{1.2};//double-->int的窄式隐式转化不能
int p(1.2);//可以

如果构造函数有std::initializer_list类的形参变量,则大括号进行初始化调用的构造函数一定会是带有std::initializer_list类的形参变量的构造函数。

上面可知,大括号初始化会优先匹配带有std::initializer_list<>的构造函数即使类型不匹配,编译器也会将其强转为std::initializer_list<>中模板的变量类型。

5.优先选用nullptr,而非NULL或者0

因为NULL有整型数值0,在一些想不到的情况下,带NULL的指针会进行隐式转换为0,指针带有整型数值,这是会报错的,但nullptr更加安全,没有数值对其对应。并且要避免整型与指针之间的重载,这说危险的。

6.优先使用using别名,而不是typedef

别名名称using 取名字可以模板化,但是typedef不能模板化,想要将typedef别名进行模板化还需要再一次进行封闭为结构体,这样过于麻烦。

理解使用using/typedef定义的函数指针,如下:

 加上typename的作用是让编译器能进行识别,从而编译通过。

7.优先选定有作用域限制的枚举类型,而不是无作用域限制的枚举类型,枚举变量一般和switch一起使用,限定范围的枚举类型要+class关键字

无作用域限制的枚举类型,其中的枚举变量申明了,后面整个代码取名字时,不能与其重名。

对于条款2:

此例子中,非限定枚举在传递给函数时,就从color隐式转换为size_t。

 上面则是限定作用域的类情况无法进行隐式转换,想要转换只能进行强转。

限定作用域的枚举类型可以前置申明,但是未限定作用域的枚举类型不能前置申明(其没有默认底层类型),要想前置声明,必须默认底层类型指定.

   

虽然限制范围的枚举型底层类型是int,但是还是不能与int类计算,参与所以计算都必须强转.

8.想要禁止调用者调用某种函数,优先使用关键字delete,而非将其申明在private中

      9.在继承中,想要为虚函数重写时,加上关键字override声明

9.1)首先子类重写的虚函数,其函数必须和父类完全一样(函数形式和形参形式都必须要相同,函数形式可能带有const,explicit,noexpect关键字等修饰,这些也必须要带上)。

9.2)函数被引用修饰

被左值引用修饰的函数,该类的左值变量才能调用,被右值引用修饰的函数,该类的右值变量才能调用。

比如下面例子:

w是左值,所以调用的是左值引用版本的函数,返回的是左值引用,变量进行复制构造。

makeWidget()返回的是右值,所以调用右值版本的函数,返回的是右值引用,变量进行移动构造

10.优先使用const_iterator,常量类型的迭代器。

                          11.只要函数不会抛出异常,就使用noexpect进行修饰

noexcept关键字修饰能明确标示函数是否会抛出异常,如果违反了noexcept的承诺(如标记为noexcept 的函数抛出了异常),程序将调用std::terminate(),立即终止。这种严格的检查有助于开发者确保他们的代码符合noexcept的约束,从而避免潜在的问题。noexpect多用于移动操作,内存释放等等。一般使用后,编译器会对其进行优化。

12.只要有可能使用constexpr,那就使用它。

第一种情况是调用的是constexpr函数的,其返回值赋值给另一个constexpr修饰的对象,这种情况下想要编译通过,必须函数涉及的参数都是编译时期已知的,不然无法编译。

如上图a,b.只有在运行期间才知道,所以复制给constexpr是不行的。

若类成员函数中有一个函数被constexpr修饰,想要在编译时期成功调用此函数,则需要确保此函数操作的所有变量都能在编译阶段是可知的/被constexpr修饰,这点很重要,所以在类中运用constexpr修饰成员函数,想确保其在编译时期就能返回,要谨慎。

14.保证const成员函数的线程安全

但使用无锁数据结构不一定比有锁的效率高

lambda表达式的补充:

lambda表达式按值捕获,捕获的是const类对象,不可改变,想改变可以添加mutable进行修饰.

在模板推到类型的返回值中,接受返回值最好用decltype(auto),比如返回一个引用类型,但接受如果是auto类,就会去掉引用。

单个auto相当于模板中的T,会去掉const 引用 指针类。

若模板参数或者auto包含引用或指针,则会保留const ,形参推导(整体,包含引用和指针)则包含const 引用 指针类。

万能引用能保留const属性,且推到出来一定是引用类。

                                    

                           15.理解类成员变量由编译器自动生成的时期

15.1:类B(左值或者函数返回变量)使用“=”去初始化类A变量时,调用的是拷贝构造函数或者移动构造函数。重载运算符不能用于初始化,而是用于已知的类变量。

15.2:移动构造和移动赋值操作函数仅在需要的时候,编译器才会进行生成,其只能作用非静态类的成员函数。

15.3:即使进行了std::move()进行右值引用初始化一个类成员也不一定走移动构造,也可能走复制构造函数。

15.4:对于复制构造函数和复制运算符,如下:

复制操作的两种函数,不会相互抑制,但移动操作的两种函数,会相互抑制。这里的生成函数指的是编译器帮你生成的,而不是你自己显示生成的.

15.5:C++11中的新规定:只要生成了析构函数,编译器就不会默认生成移动操作.

15.6:编译器帮你执行移动操作当且满足下面三个条件

15.7:

第二条的移动操作是指的是编译器自己构造移动操作相关的函数帮你进行移动,自己定义的移动操作函数肯定自己要实现移动操作。注意,自己定义了自己行为的移动操作后,编译器便不会参与。

第三条是如果你自己定义了移动操作相关的函数,则编译器不会帮你生成默认的复制构造函数(在未定义申明的情况下).

第四条是编译器自己给你实现移动赋值运算符或者移动构造函数.

用户自己声明移动构造或者移动赋值运算符时,尽量删除拷贝函数和拷贝赋值运算符。即赋值操作和移动操作尽量分开,一个类尽量不要两者都进行自己定义申明(当然都可以进行定义和申明)。

16:使用std::unique_ptr<>管理具备专属所有权的资源

16.1:比如构建链表或者其他数据结构时,用到链式结构就用std::unique_ptr管理结点,std::shared_ptr管理资源内容。

16.2:std::unique_ptr仅支持移动,可以在多态中的基类运用std::unique_ptr<>去管理子类.

16:使用std::shared_ptr<>管理共享资源所有权的资源

注意:第一个条件,涉及引用计数的裸指针就是动态开辟的内存块

注意上述何时创造控制块,1:调用make_shared<>会创建一个控制块,2给智能指针传递裸指针给构造函数,则会创建一次控制块。3.条款二说明的是通过std::move()创建的智能指针,会进行智能指针的所属权的交换,这样不会创建新的控制块。给智能指针构造函数传递智能指针类的参数时不会进行控制块的构造,但是会增加引用计数.

如上图,同一个资源涉及两个控制块,但是控制块中涉及了资源的析构,在程序结束时,则会进行两次析构,这是绝对不允许的,所以创建一个智能指针类,不要用裸指针进行初始化,而是用智能指针类,或者直接在构造函数直接new,而不传递一个裸指针

可以用std::unique_ptr初始化创建一个std::shared_ptr(指针类型提升),反过来不行。注意不要将裸指针和只能指针混用和初始化。

重要:类类型中 不要给智能指针构造函数传递this参数。这就犯了传递裸指针的错误。想要在类内中使用智能指针管理this指针(管理此类对象),该类必须要继承enable_shared_from_this<>.然后在类中调用shared_from_this()函数,此函数返回std::shared_ptr<类>对象。


  class A : public enable_shared_from_this<A>
{
     //调用shared_from_this()

};

17.对于类似std::shared_ptr<>对象但有可能悬空的指针,使用std::weak_ptr<>.

std::weak_ptr<>和std::shared_ptr<>配套使用,前者指向std::shared_ptr<>管理的对象不会增加std::shared_ptr<>的引用计数.std::weak_ptr<>一般使用std::shared_ptr<>初始化.

注意,使用智能指针不要进行环列指向(即使用std::shared_ptr<>),可以用std::unique_ptr.

用std::weak_ptr<>来检测其关联的std::shared_ptr<>是否已经失联.

18.优先使用std::make_shared<>,std::make_unique<>,而不是用new.

使用make系列函数,不用new ,调用默认构造函数。传递参数时,什么都不传则打一个小括号。

对于第二种情况,make系列函数内部是以小括号进行new操作的,所以其不适用于大括号系列初始化。第三种情况,对于自定义类型的类最好不要用make_shared<>进行创建,而是用new。

new和make系列的区别,new出来的控制块内存和管理资源的内存是分开独立的,但make出来的内存两个是连在一起的,连续的。

上述表明,托管对象的内存直到与其关联的控制块也被析构时才会释放。即虽然两者开辟在同一内存块,但是只有当两者的析构函数都调用时,两者才会共同释放。

上面可知std::shared_ptr<>,std::weak_ptr<>析构不会相互影响。std::shared_ptr<>对象被析构,但是其控制块或者管理对象也不会被立即析构,这个要注意。

19:理解std::move()和std::forward<>

19.1)std::remove_reference的作用是移除引用特性,如果输入的类型T是一个引用类型,std::remove_reference<T>::type将返回一个去除引用的类型,否则返回的类型与T相同,结合万能引用推导一定是一个引用类型。在std::move()中,最后是将变量强转为std::remove_reference<T>::type&&,即原类型的右值引用进行返回。

19.2)移动构造函数只接受非常量的右值类,常量的右值类型会匹配到常量的拷贝构造函数上。std::move()不能移除常量类型。即使将常量std::move()去初始化另一个类变量,也是走的复制构造函数。

19.3)std::move()是转换为右值引用。并没有发生移动,移动是编译器干的事。

20.区分右值引用和万能引用

万能引用只能是T&&,auto&&类,T和auto不能有其他关键词修饰,例如;

21.针对右值引用使用std::move(),针对万能引用使用std::forward<>

21.1):对于指定函数返回的是普通类型或者右值引用类型,不是左值引用类,则其实际返回的是右值。

21.2):对不同构造函数进行不同的移动/赋值操作,不要混用。如下图

21.3):对于按值(返回的是函数内临时的左值变量或是右值)返回的函数,编译器可能会做优化,不一定是复制构造进入函数的返回值空间,也可能是进行移动构造进入函数的返回值空间(编译器对左值进行移动操作,不是std::move),减小开销。上述是针对于函数返回类型就是普通的类型,即没有引用等修饰。在编译器会进行优化的情况下,不要再对返回的临时变量使用std::move().

21.4):针对于万能引用,使用decltype来搭配使用std::forward<>,如下

21.5):当接受参数类型是引用(或者是万能引用类型时),返回函数是按普通类型返回时。我们返回时使用std::move()比直接按值返回效率更高,因为编译器将值进行移动到函数地返回空间,而不是赋值进返回空间。

21.6):操作者返回变量的类型是引用,但函数返回是普通类型,这时使用std::move()效率更高。但是若是返回局部变量,不是返回引用类型,函数的返回类型是普通类型,这样的情况下不要使用std::move()。结合21.3)

21.7):

如上操作,对单一函数内的某个对象不止一次地绑定右值引用或者万能引用,且希望在最后一次使用该变量之前不被移动走,在这种情况下,我们应该仅在最后一次使用该引用时,使用move和forward。

22.避免依万能引用类型进行重载

因为万能引用类会匹配绝大多数参数类型,所以有万能引用类型的重载函数,会劫持绝大多数其他的重载函数。

23.依靠万能引用进行重载的解决方案。

上述中建议一就是,重载函数都不适用万能引用。而是依靠不同情况进行不同重载。

24.理解引用折叠

24.1)引用折叠之所以发生是因为在万能引用的情况下,万能引用会将左值,右值的信息载入传递给模板。

24.2)

注意上述,传递右值,T推导出来就是非引用类的右值类型,不是右值引用。折叠后为右值引用。

传递左值,T推导出来就是左值引用,折叠后依然为左值引用。

比如传入3 ,T推到出来是int.不是int的右值引用。

24.3)

typedef的情况也适用于using

25.假定移动操作成本高,不存在或者未使用。

对于第三条,noexcept关键字本就是会让编译器为其进行优化,而且使用于移动,析构等与内存操作有关的情况。

26.完美转发失败的情况

不能将0和NULL以空指针之名传递给模板(给指针赋值为0/NULL),类型推导时一般会推导为整型,故将0和NULL作为空指针进行std::forward<>原样转发时改用nullptr即可,或以NULLPTR进行赋值。

对于大括号初始化产物,情况和auto大括号初始化类似。注意即可。

27.λ表达式避免默认捕获

27.1)默认捕获比如[&],[=]。这样的类型,前者按照引用的默认捕获可能会捕获到悬空引用,后者的按值捕获可能让你以为对悬空引用免疫,实则不然。因为你的闭包不一定独立。

27.2)lambda表达式的捕获能力被外部作用于限制,不是全局捕获能力,比如在一个局部函数内,以引用的方式捕获局部变量,然后开启一个线程detach.又可能线程在调用变量时,变量已经被回收,这时就会出现未定义问题。所以变量的生存周期是不确定的。

27.3)按值捕获的情况下,捕获的类型和闭包外的被捕获的变量类型都是一样的,但复制给另一个变量后,此变量类型和捕获类型可能就不同了(比如被复制变量类型是auto推导,而捕获的变量类型是const修饰类型,auto推导就会去掉const)。

28.使用初始化捕获将变量移入闭包

28.1)λ表达式以初始化值的方式进行捕获是效率最该的捕获方式。

等号左边不用进行指定变量类型,等号左边是该λ表达式内部的有效范围,右边就是捕获范围。

在C++14中 还可以捕获一个表达式的结果,如图

28.2)std::bind()结合λ表达式可以结合初始化捕获,std::bind(λ表达式,λ表达式的形参)。

     std::bind()在类内部使用时,绑定类成员函数时.在绑定函数后面要加上this指针。

上面注意,进行绑定对象进入std::bind()。除了只能移动的对象,其他对象其传给到函数里面进行绑定的都是绑定的实参副本。模拟移动捕获看上图第二操作步骤。

λ表达式的初始化捕获也包含小括号,进行初始化的时候,=左边的赋值变量不用申明类型。

29.对auto&&类别的形参使用decltype,然后使用后std::forward<>

即对万能引用推导后的参数,进行std::forward<decltype(参数类型)>(参数),传递给其他函数调用。

30.优先使用λ表达式,而非std::bind()

std::bind()之所以运行效率比λ表达式低是因为std::bind()返回的结果是std::function<>类,进行模板类生成要多一步消耗性能,而λ表达式不会。

31. 优先选用基于任务而非基于线程的程序设计。

32.如果异步执行是必要的,请指定std::launch::async

std::launch::saync制定后即会立刻执行。执行结果可以联结其关联的std::future对象。std::launch::deferred只有在其关联的std::future对象调用get/wait()方法时,才会执行。

使用std::async是,最好不要用默认启动方式,指明究竟是异步还是同步。

33.使std::thread对象在所有路径上都不可联结

33.1:

对于要点四,之所以在最后声明std::thread对象是因为一旦申明即开启线程开始运行,放在成员列表前面的话,线程运行可能会调用还未初始化的变量。最好的办法是用智能指针进行包裹,控制何时能启动线程。

33.2:通过RAII规则来管理线程类的析构和创建

补充:如果类成员变量是左值引用的话,那么构造函数进行初始化时,形参类型也为左值引用,构造直接括号初始化,如上图一。如果类变量不是引用,那么构造函数就是右值引用类型,初始化使用移动构造初始化,使用std::move() 两者注意区分。-----上述情况是只针对于只能进行移动的变量。比如std::unique_ptr,std::thread,std::future。

通用的:类成员变量有左值引用,初始化左值引用的变量,构造函数参数类型也必须是左值引用。

34.对变化多端的线程句柄保持关注

34.1:   std::promise的set_value方法用于给其他异步执行的线程去使用,std::promise返回std::future对象,获得执行结果。

34.2:   std_value()的值既不储存在被调用方(设置方),也不储存在调用方(调用std::promise::get()的一方),而是储存在”中间“的共享状态中。

34.3: 期望值对象------->std::future或std::shared_future对象的析构函数析构它们自己时,不会对任何东西进行join(),detach(),不会运行任何东西。

34.4: 对于在一个线程产生了一个std::packaged_task()对象,接着get_future()其关联的对象,然后再将std::packaged_task()转交给std::thread()。这过程中,若std::packaged_task()对象调用operator()()函数,那么异步执行结果由转交给std::thread()后立刻执行。执行结果给异步的关联的std::future()对象调用即可(前面get()出来得到的对象,可移动到其他线程中调用)。

上述可理解为期望值对象的析构函数与调用者的析构函数,没有相互关联。std::packaged_task对象的get_future()函数,获得的std::future()对象两者互不影响,一个执行程序,一个调用程序的运行结果,运行结果储存在之外,与两者互不关联,相当于隐式调用了dectach。共享区被析构当且仅当future的引用计数为零。若std::packaged_task对象未调用get_future()对象,则其关联的函数运行完毕,共享区域就会被回收。(std::promise也如此

34.5 :std::async()的异步执行却不同(称为特别行为)。但若异步执行还未执行完就调用std::future::get()方法,则会进行阻塞(若未显示的调用get方法,则std::async()返回的std::future()对象只会在异步函数执行完毕才会进行析构,未执行完毕则会在析构函数进行阻塞)。
相当于隐式的调用了一次join(),等待异步任务执行完毕。

总上:除了std::async(),其他的返回的future对象的析构与被调用方无关,但std::async()返回的future()返回的future对象则需要等待异步任务解除才会回收.

35.对并发使用std::atomic,对特种内存使用violate

36.针对可复制的形参,在移动成本比较低且一定会被复制的前提下,选择按值传递

36.1: 构造函数形参类型是万能引用,左值引用,右值引用类,按照类型进行传递(左值传递给左值版的构造函数,右值传递给右值版本的构造函数),这种将变量传递给形参时,不会进行构造。而形参类型是普通类型,不是引用类型,则传递什么类型引用的参数都会对形参进行一次调用复制构造函数。

对于第二种情况,比如传递实参去构造形参,若形参是右值引用,传递右值直接移动进入形参,其他方式则会调用复制构造函数进行一次开销复制给形参,效率变低​​​​​​​。

37.考虑置入而非插入

37.1 : 类型不匹配但可以相互转换的情况下进行调用std::vector<>的push_back(),一个类型会创建另一个类型的临时变量再进行插入。

37.2: 用智能指针传递给容器,调用置入或者插入函数时,调用std::move()

选用元素插入时,优先选择emplace_back(),而不是push_back(),前者多一步std::forward<>,能很大程度上避免因类型不匹配而创建临时对象进行插入的,多一步的开销。

38.条件变量和锁的运用

条件变量+互斥锁才能避免虚假唤醒(或者将λ表达式改用while循环)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值