前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【笔记】《C++Primer》—— 第三部分:类设计者的工具

【笔记】《C++Primer》—— 第三部分:类设计者的工具

作者头像
ZifengHuang
发布2020-07-29 16:30:47
1.7K0
发布2020-07-29 16:30:47
举报
文章被收录于专栏:未竟东方白未竟东方白

这篇是第三部分的总结,基本上就是回看了之前的4篇笔记并且重新翻翻书梳理了一下,内容基本都是从前面的章节复制来的,长度较长,难度可能也比较大。

下篇开始就到了最后一个部分第四部分,是关于C++的一些比较少用到的内容,700页的长跑终于来到最后一站。

13 拷贝控制

  • 拷贝构造函数会自动将每个非static成员依次拷贝到正在创建的对象中,其中内置类型会直接拷贝,数组会被逐元素地拷贝,类类型会调用拷贝构造函数来拷贝
  • 如果初始化值要求一个explicit构造函数来类型转换,则拷贝初始化还是直接初始化就无关紧要了
  • 析构函数的行为与构造函数相反,会自动销毁掉非static的成员和调用成员析构 析构函数没有参数列表,所以成员销毁时的行为完全依赖于成员自己
  • 析构会在变量离开作用域或母构件销毁时销毁,动态分配的对象指针需要手动delete销毁,临时对象在表达式执行完的时候销毁
  • 类应该被看作一个整体,“三五法则”就是指当一个类需要析构函数时,我们几乎肯定也要定义好拷贝和赋值函数,拷贝函数和赋值函数两者又是绑定出现的,如果需要拷贝操作时,最好定义好所有其他操作
  • 有时我们不希望用户使用一些函数,可以在函数名后加=delete表示删除(操作与=default一样),此时也不需要函数体,可以对任意函数标记,但要注意一定要在函数第一次声明的地方就标记delete。旧标准中我们使用private版本的构造函数来控制构造,但如今如果要控制拷贝最好用=delete
  • 我们对一个类的拷贝和资源管理通常表现为两种:像值的类,像指针的类。像值的类需要它拷贝前后两个对象完全独立,改变副本不会产生影响;像指针的类通常使用shared_ptr来管理,当需要手动管理时,一般采用引用计数法来保持指针引用记录
  • 管理资源的类通常额外定义了一个swap操作方便标准库使用
  • 移动操作的目的是解决对象资源所有权转移的问题,具体来说一般是直接接管源对象实现的,而为了完整达成移动语义需要解决临时变量标记问题,即我们需要指定将要被接管的对象是无用对象了,于是C11引入了右值引用类型。但引入了右值引用的根本目的是解决完美转发问题,即让我们在一些例如传参的时候可以直接使用临时变量本身的值来传递而不经过拷贝的性能消耗,由于我们要直接使用临时变量就打上了无用变量的标记。我们可以认为右值引用的目标对象都是将要被销毁且没有其他用户的,也就是可以自由使用其引用对象,正是这个特性让我们可以移动那些不可拷贝的值
  • 右值引用有与左值引用完全相反的特性,我们无法将右值引用绑定到左值上。但我们可以将const左值引用绑定到右值上
  • 移动构造的具体写法类似拷贝构造,但是构造参数是自己类型的右值引用,为了完成移动构造,我们需要保证移动后源对象处于可以无害销毁的状态,源对象的指针不再指向原先的资源,而且移动构造不应该抛出任何的异常,这是为了防止在移动构造的途中被打断了资源转移的过程,从而摧毁了原先的资源。用C11的新关键字noexcept来指出某个函数必然不会抛出异常
  • 移动赋值运算符的编写类似之前的拷贝赋值运算符,但要注意在一开始的时候用if(this!=&inp)来检测是否发生自赋值,若发生则不要进行内部的控制权转移部分
  • 移动后的源对象必须保证是有效且可安全析构的状态,而且不能假设这个源对象的任何值
  • 一个类可以既有移动拷贝也有拷贝构造,此时编译器将会进行最佳匹配,参数是左值使用拷贝,参数是右值或不可拷贝使用移动
  • 为了达成易用性与性能间的平衡,当我们定义自己的函数时,可以对其重载一个constX&参数的左值引用形式和X&&的右值引用形式
  • 引用限定符也可以用来区分重载,但要求如果某个函数出现了引用限定符,则其具有相同参数列表的所有版本都需要有引用限定符

14 重载运算和类型转换

  • 重载运算符的参数数量必须和这个运算符默认情况下的参数一致,而且其优先级和结合律无法改变即与默认情况一致
  • 有的运算符(如加号+)有一元版本和二元版本,我们用参数数量区分它们
  • 我们不能创建新的运算符
  • 重载运算符的本质是函数调用,因此向逻辑运算符,逗号运算符之类的运算符内在的短路求值特性等将被舍弃,参数们都会被以未定义的顺序求值
  • 我们应只在操作含义清晰明了时才重载运算符,且有些运算符我们最好还限定其成员性:
    • 赋值,下标,调用,箭头 应该是成员
    • 复合赋值 一般是成员
    • 递增递减等会改变对象本身的应该是成员
    • 参数两端可以改变顺序的如算数,相等,关系,位运算 一般不是成员
    • 混合类型表达式,注意一定要是非成员
  • 输入运算符必须处理可能失败的情况,生成符合规范的元素输入或其他方法,要尽可能保持流的正常工作且负责让流从错误中恢复
  • 如果定义了==,一般也要定义!=,而且为了保证正确性,应该把其中一个的实现委托给另一个,通常先定义==再实现!=
  • 赋值运算符的参数一般都是const的
  • 定义递增递减时应该同时定义好前置版本和后置版本,其中前置版本就是普通的定义方法,后置版本为了区分会附加一个int参数,这个参数一般不对其命名也不对其运算,其值是编译器自动传入的0。当我们需要显式调用递增递减运算符时,我们需要对后置版本的参数传递一个任意int值,这样编译器才能区分出版本
  • 重载函数调用运算符让我们可以像使用函数一样使用类的对象,称为函数对象,函数对象常常用作泛型算法的实参,我们之前使用的lambda表达式就是一种自动生成的函数对象
  • lambda表达式就是一种自动生成的函数对象。默认情况下lambda表达式不能改变它捕获的变量因为它的函数调用运算符被重载为const的
  • 标准库functional中定义了一系列表示算术运算符,赋值运算符和默认析构函数的模板类,我们可以用这些类替换掉默认的运算符改变容器的操作
  • 有时候我们想要有一个函数表储存指向可调用对象的指针,然后按照所需的调用形式来检索对象,对此我们可以用map来实现,让运算符为key,可调用对象为value但是此时会发现我们希望可调用对象为value,但不同的可调用对象有不同的类型,如函数指针与函数对象是不同的。标准库的functional针对这个问题定义了function类,function类接受一个可调用对象为模板,然后可以按需求返回其返回类型,参数类型等等信息,并且可以按照相同的方式调用这些不同类型的可调用对象
  • 我们一样可以自定义所需的类型转换运算符,方法是一个没有返回类型也没有形参的必须定义为成员函数的operator type() const函数
  • 尽管编译器对一个对象一次只进行一次自定义的类型转换,但这个转换还可以放在一次内置转换之前或之后,也就是对象最多可能隐式发生两次转换,这就容易导致二义性问题
  • 如果调用重载时我们需要显式写出转换或用强制类型转换,则常常说明我们的设计有不足
  • 类型转换中有一个转换比较特别,那就是bool类型的转换。将类朝bool类型进行转换是最常见的一种做法,但C11加入了显式类型转换来限制它,编译器不会隐式进行这个转换,也就是我们必须使用强制类型转换才能使用。但对于bool又有特殊对待,在一些条件表达中,例如if,for,逻辑运算符和三目运算符中,bool转换会自动进行显式转换

15 面向对象程序设计

  • 强调下两个术语:
    • 覆盖(重写) override,指重新实现了一个名称和参数都一样的方法
    • 重载 overload,指对一个同名方法进行了几种不同参数的实现
  • 可以将一个派生类对象转换为基类对象,此时派生类独有的部分将被截断,其基类部分被处理而派生类部分被忽略
  • 有时我们不希望派生类独有的部分被截断则需要使用类指针来调用重载的函数或使用指针所指的成员。如果此时我们只是普通地重载了函数,那么我们根据指针所调用的函数是和指针的类型相同的,这称为静态绑定。我们很多时候希望的是我们通过将基类指针指向派生类,然后可以动态调用派生类的函数,这时我们可以将基类的对应函数写为虚(virtual)函数来实现,此时发生的称为动态绑定
  • 派生类可以继承多个基类,称为多继承。每次继承一个基类就会在内存中生成一个子对象,存放了基类的成员,也正是因为这个原因派生类可以转换为基类
  • 派生类的构造函数需要负责所有成员的初始化,尽管派生类也可以初始化继承来的基类成员,但是这不符合通常的编码思路,派生类一般在构造函数开始的地方调用基类的构造函数,让基类来初始化自己的成员
  • 静态类型是变量本身代码中的类型,在编译时决定,动态类型是变量在内存中的对象的类型,在运行时才能决定。如果表达式不是引用也不是指针,则其动态类型永远与静态类型一致
  • 派生类可以往基类类型转换,而基类不能隐式反向转换
  • 一个派生类的函数如果想要覆盖继承来的虚函数,那必须名称和形参都一致,否则编译器会认为这两个函数是独立的,而不会产生覆盖。虚函数的返回类型也需要与基类一致,除非虚函数的返回类型是类本身的引用或指针时为了多态性会有特例
  • 如果想要保证基类中某个虚函数一定会被覆盖,则可以在想要用来覆盖的函数后面加上override关键字
  • 函数后面加上final,表示不允许继续覆盖
  • 有时候希望某个基类只用来被继承而不允许实例化,这时候我们可以给这个基类加入纯虚函数,拥有纯虚函数的基类叫抽象基类,不能被直接创建。定义方法是在声明函数名的时候函数体类似显式默认构造的写法改写为=0,只能对虚函数使用这个写法
  • 友元只对被声明的类有效,友元的基类或派生类都不是友元
  • 某个类对其继承来的成员的访问权限受到两个因素的影响:
    • 基类中此成员的访问说明符,这是最重要的一部分
    • 派生类的派生列表的访问说明符,这一项决定的是派生类对继承来的成员对外表现出的最高权限
  • 有时候我们需要改变外部对派生类继承的某个名字的访问级别,可以在自己的访问控制符处用using声明需要改变的名称(用::作用域符特指名称)
  • 派生类对基类的转换也与派生列表的访问说明符有关,本质上与类型转换函数的权限有关
  • 名称查找是根据编译时的目标的静态类型从内到外进行查找的,目标的静态类型决定了其是否可见
  • 如果内层某个成员与外层成员同名,即使它们的形参列表可能不一致也会因为名称查找而被隐藏,因为一旦找到名称编译器便会停止查找。因此除了重载虚函数外最好不要让名称同名
  • 派生类可以覆盖基类重载的函数,但是如果派生类希望基类重载的几个函数都在派生类中可见的话:一种方法是不覆盖任何一个重载函数或将所有重载函数都进行一次覆盖;另一种方法是为需要重载的函数名使用using语句并不要指定形参列表,可以将所有重载函数都加入派生类的作用域中
  • 继承体系中,最关键的是基类通常需要定义一个虚析构函数,这样我们才能动态分配体系中的对象,确保delete时能够执行正确的析构函数版本,对于实现的内容我们一样可以使用=default简化
  • 如果基类中的基本操作函数不可访问或被删除,则派生类中的对应成员是被删除的因为我们无法使用基类来操作那些成员
  • C11中,我们可以用using重用基类定义的构造函数,写法和15.6中指明重载的基类函数一样,效果与定义一个空的构造函数然后列表中调用基类构造函数一致
  • 和普通函数的using不同,对构造函数的using不会改变构造函数的访问级别
  • 当基类构造函数中有默认实参时,这些实参不会被继承,而是派生类会得到多个继承的构造函数,每个构造函数省略一个有默认实参的形参
  • 当我们想要把继承体系的对象存放到容器中时,最好使用间接存储也就是存放基类指针(智能指针就更好了)

16 模板与泛型编程

  • OOP让我们可以在运行时处理运行前未知的动态情况,而泛型模板编程让我们可以在编译时就处理好一些动态的情况
  • 当我们调用函数模板时,编译器和以前一样可以自动按照我们的实参来推断模板参数的类型
  • 可以由具体关键字带头声明非类型参数,非类型参数表示的是一个值而不是类型,因此非类型参数在编译时会被用户提供或编译器推断的一个常量代替,从而允许我们初始化数组之类
  • 非类型参数可以是整型或指向对象或函数的指针或左值引用,但是注意绑定到非类型整型必须是常量表达式,绑定到指针或引用的对象必须有静态的生存期(都是为了可以在编译期完成所要求的)
  • 模板的提供者必须保证模板实例化时依赖于模板参数的名字都必须有定义,其他的要保证对编译器可见
  • 类模板不会推断参数的类型
  • 类模板的成员函数只有在使用时才会实例化
  • 类模板与另一个模板直接最常见的友元是一对一的友元,首先模板需要声明所有需要用到的名字,然后在声明友元时标注出目标类的具体模板实参
  • 类模板也可以一对多友元,方法是直接将目标模板的名字标为友元,这样就与目标模板的所有实例都成为了友元。要注意如果声明了目标友元的模板实参标识符,这些标识符需要与自身类模板的标识符不同
  • 我们不能用typedef来起模板的类型别名,但是C11让我们可以用using来起模板的类型别名
  • C11允许我们为函数模板和类模板提供默认参数,做法和默认函数实参类似但是写在模板参数列表里,也只能出现在最右侧
  • 当需要在类外部定义类成员模板时,要注意此时需要两个template连用来说明标识符
  • extern显式实例化会实例化模板的所有成员,包括内联的成员函数
  • 与函数模板与普通非模板函数不太一样,编译器通常不对实参进行类型转换从而只有几个类型转换会应用在实参上,编译器偏向于生成新的模板实例来适配
  • 如果显式指定了实参类型,那么就可以自动正常进行类型转换
  • 有时我们需要使用编译确定下的参数类型来作为返回值的类型,我们可以用尾置返回来完成这个目标:
  • 标准库头文件type_traits中的类来进行特殊的类型转换能够动态地将这些语言特性消去从传入的参数中提取出我们想要的类型
  • 编译器是从模板函数的调用中推断具体的实参类型的要求在16.2中有详细介绍
  • 如果我们通过类型别名或模板参数之类的方法间接定义了引用的引用(正常情况下无法定义),会产生引用的“折叠”,(X&)&,(X&)&&,(X&&)&都折叠为X&,(X&&)&&折叠为X&&,也就是删去两个引用符
  • 左值如i传递给模板类型的右值引用时,编译器会推断参数类型为左值引用i&
  • 直接利用右值引用参数会丧失右值引用属性,这时我们可以通过让右值引用后进入函数的参数调用utility文件中的forward函数,能恢复被右值引用参数去除的右值引用属性
  • 在没有歧义的情况下,永远会调用发生了最少改变,最精确匹配,最不需要调用自定义类型转换,最不需要调用模板的那个重载,即“更特例化”
  • 可变参数模板就是一个能接受数目可变类型也可变的参数的类,那些可变的参数部分称为参数包,由省略号...标记
  • 可变参数的模板函数通常是一种递归函数,一般我们编写的时候都会递归地分析包中的内容并调用直到终止,将包中的内容分解成元素称为包扩展
  • 包扩展的一种用法是用来扩展提取输入的参数,另一种用法是对包中的每个元素都自动调用一个指定的函数,并返回处理后的返回值
  • 模板特例化的写法是将template尖括号中的需要特例化的内容删去,然后对下方用到的模板类型转为需要确定的类型。即使我们需要特例化所有的类型参数也要保留一个空的尖括号做标记
  • 完全的模板特例化的本质是模板的一个实例,而不是重载,因此特例化不会影响函数的匹配。但如果只是部分特例化的模板则仍然是模板,则依然会参与匹配
  • 我们也可以特例化类模板,此时必须在原模板定义的命名空间中特例化它。打开命名空间的方法是写namespace XXX{},这个大括号中的区域相当于目标命名空间内,我们可以在里面操作。常用的用法是打开std空间特例化标准库函数
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-06-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 未竟东方白 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档