• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2020-01-10 07:32 Aet 隐藏边栏 |   抢沙发  15 
文章评分 3 次,平均分 5.0

导读

explicit

将构造函数声明为explicit,禁止编译器执行非预期(不是我们想要的)的类型转换。
除非我们有一个很好的理由,允许构造函数被用来进行隐式类型转换,否则我们应该把它声明为explicit的。

拷贝构造函数&&拷贝赋值操作符

拷贝构造函数被用来以同类型的对象初始化自我对象。
拷贝赋值操作符被用来从另一个同类型的对象中,拷贝对方的值到自我对象。

如上述代码,我们有可能会不确定到底调用了拷贝赋值操作符还是拷贝构造函数。
其实,是很好区分的:
如果一个新对象被定义,那么一定会有一个构造函数被调用。如果没有新对象被定义,那么就不会有构造函数被调用(也就是赋值操作符会被调用)
从上面这段话里面,很明显能看出,确定是不是调用了构造的决定性因素是新对象被定义,我们再回过去看上面的代码,这个时候,应该就能一眼看出只有倒数第二行没有新对象被定义。

C++组成

  1. C
    1. C部分
  2. Object-Oriented C++
    1. 面向对象部分(涵盖了构造析构、封装继承、虚函数多态等特性)
  3. Template C++
    1. 泛型编程部分(包括模板元编程)
  4. STL
    1. 模板程序库部分(包括容器、迭代器、算法、函数对象等特征)

2 尽量以const,enum,inline代替#define

概述

  1. 按标题的叙述,可以换句话为:宁以编译器替换预编译器
  2. 为什么要替换,因为,类似于#define这类的东西,是不被视作语言的一部分的

const常量

  1. 问题:
    1. 使用 #define 定义的常量没有类型信息
    2. #define 常量不受作用域限制,可能导致名字冲突和难以调试

  1. 解析:

    1. ASPECT_RATIO这个符号也许从未被编译器看见过,因为很有可能在编译器处理源码之前,它就被预处理器移走了
    2. 宏这种东西,我们应该都不陌生,我们把一个东西定义为宏,或许是为了便于我们区分或记忆,更甚于为了代码的可读性
    3. 但是,它的本质,是符号替换,这个是宏的核心。就如上面所说的,很可能在我们定义的这个宏被预处理器替换为对应的内容时候,都没到编译器出手的时候,也就是编译器很可能都没有见过它
    4. 如果,在代码某个地方出错了,我们看到的将是1.56,而不是ASPECT_RATIO,那么在这种情况下,这个1.56就会显得很突兀了,我们很可能一时半会儿定位不到出问题的地方。
    5. 但是,常量就不一样了,作为一个常量,编译器肯定会把这个常量记住的。
  2. 常量替换#define的情况:

    1. 定义常量指针
    2. 类专属常量

enum枚举

  1. 问题:

    1. #define 常量没有类型检查,可能引发难以发现的错误
  2. 一个属于枚举类型的数值可权充ints被使用。

  3. 枚举的行为某方面来讲比较像#define,而不是const

    1. (我们可以对const取地址,但不能对#define的东西和枚举取地址)

inline函数

  1. 问题:

    1. 使用 #define 定义的宏函数仅仅是文本替换,可能导致意想不到的副作用
    2. 宏函数没有类型检查
  2. 为了避免一些麻烦或为了代码的可读性,以及其他的理由,我们有可能会用#define来实现一个看起来像函数的宏。

    1. (一般这样的函数可能都是比较简洁,调用频繁)
    2. 一个比定义宏更好的方法就是把此类函数内联

3 尽可能使用const

概述

  1. const功能挺强大的,我们可以用它在类的外部修饰全局变量或命名空间作用域内的变量
  2. 也可以修饰文件、函数、区块作用域中被声明为static的对象
  3. 也可以修饰类内部的静态或非静态的成员变量
  4. 对于指针,经常会有const出现在星号左边或右边的情况:
    1. const星左,底被指
      const 在*左边,被指物是常量,并且这个const是一个底层const
    2. const星右,顶指针
      const 在*右边,指针本身是个常量指针,并且这个const是一个顶层const
    3. 星号两边都有const,被指物是常量,执行被指物的指针也是常量

const指针

  1. 问题:
    1. 指针和指针所指向的数据在代码中经常会被修改,使用const可以限制这种修改
  2. 指向常量数据的指针:

  1. 常量指针:

  1. 指向常量数据的常量指针:

const 成员函数

  1. 问题:
    1. 类的成员函数如果不修改对象的状态,应该声明为const,以便清楚地表明该函数不会修改对象

const函数参数和返回类型

  1. 函数参数如果在函数体内不被修改,应该声明为const,以提高代码的可读性和安全性

  1. 如果函数返回一个指针或引用,且不希望返回值被修改,也应使用const

const迭代器和容器

  1. 在使用标准库容器和迭代器时,使用const可以防止无意间修改容器内容

4 确保对象在使用之前已经初始化

概述

  1. 未初始化的对象可能包含垃圾值,导致未定义行为

内置类型的初始化

  1. 内置类型如intdoublechar等在定义时不会自动初始化,必须显式地进行初始化

类对象的初始化

  1. C++中,类对象可以通过构造函数进行初始化

  1. 提供默认构造函数以确保对象可以在不带参数时也能被正确初始化:

使用成员初始化列表

  1. 使用成员初始化列表初始化类成员,而不是在构造函数体内赋值
  2. 这不仅能提高效率,还能确保常量成员和引用成员被正确初始化

避免局部变量未初始化

  1. 局部变量在声明时不自动初始化,需要显式赋初值

使用智能指针

  1. 避免使用未初始化的指针
    1. 可以使用智能指针(如std::unique_ptrstd::shared_ptr)来确保指针在使用前已经初始化

5 了解C++编译器自动生成和调用的函数

概述

  1. 在C++中编译器会自动生成一些特殊成员函数,程序员应该了解这些函数,以便更好地控制对象的行为和避免潜在的问题

默认构造函数

  1. 如果没有定义任何构造函数,编译器会自动生成一个默认构造函数
  2. 用于创建对象,但不进行任何初始化

拷贝构造函数

  1. 当需要复制一个对象时,编译器会自动生成拷贝构造函数
  2. 逐位复制对象中的非静态成员

拷贝赋值运算符

  1. 当使用赋值运算符将一个对象的值赋给另一个对象时,编译器会自动生成拷贝赋值运算符
  2. 逐位复制对象中的非静态成员

析构函数

  1. 当对象的生命周期结束时,编译器会自动调用析构函数
  2. 释放对象占用的资源

移动构造函数和移动赋值运算符

  1. 当需要移动一个对象时,编译器会自动生成移动构造函数和移动赋值运算符
    1. (如果没有显式定义拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数)
  2. 通过移动对象的资源而不是复制,从而提高性能

6 显示禁止编译器生成你不想要的函数

概述

  1. 为什么需要显式禁止默认函数?
  2. 控制对象行为:
    1. 某些默认生成的函数(如拷贝构造函数、拷贝赋值运算符)可能不适用于你的类,使用这些函数会导致未定义行为或资源泄漏
  3. 提高代码安全性:
    1. 显式禁止这些函数可以防止不小心使用它们,从而避免潜在的错误
  4. 意图明确:
    1. 通过显式声明,可以清楚地表达程序员的意图,使代码更易读、易维护

禁止编译器生成函数的方法

  1. 使用= delete
    1. 用来显式删除编译器生成的函数。这样在使用这些函数时,编译器会产生错误

  1. 将函数声明为私有(适用于C++11之前的版本)
    1. C++11之前,可以通过将不需要的函数声明为私有并且不提供定义来禁止它们的使用

适用场景

  1. 禁止拷贝:
    1. 如果类管理资源(如文件句柄、网络连接、动态内存),拷贝可能会导致资源冲突或双重释放
  2. 禁止赋值:
    1. 类似地,如果赋值操作不安全或无意义,应该禁止
  3. 单例模式:
    1. 单例类通常禁止拷贝和赋值,以确保类的唯一实例

示例

7 在多态基类中声明虚析构函数

概述

  1. 虚析构函数确保在删除指向派生类对象的基类指针时,能够正确调用派生类的析构函数,从而避免资源泄漏和未定义行为

为什么需要虚析构函数

  1. C++中,如果你删除一个指向派生类对象的基类指针,而基类的析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数
  2. 这会导致派生类的资源没有被正确释放,进而引发资源泄漏

使用虚析构函数

  1. 为了确保正确调用派生类的析构函数,应该在基类中将析构函数声明为虚函数:

建议

  1. 总是为多态基类添加虚析构函数:

  1. 非多态基类不需要虚析构函数:
    1. 如果类不会被继承,或者不会通过基类指针删除对象,则不需要虚析构函数

8 防止异常逃离析构函数

概述

  1. 如果异常在析构函数中被抛出,可能会导致程序崩溃或资源泄漏
  2. 因此,需要确保异常在析构函数内部被捕获和处理

为什么不应在析构函数中抛出异常

  1. 程序终止:
    1. 如果在析构函数中抛出异常,且该异常未被捕获,程序将会调用std::terminate并终止运行
  2. 双重异常问题:
    1. 如果在栈展开过程中有另一个异常抛出(例如,在处理一个异常的过程中遇到另一个异常),程序也会终止

示例

  1. 以下代码,其中析构函数可能抛出异常:
  2. example函数中,构造一个Base对象并随后抛出一个异常
    1. 在栈展开过程中,当对象b的析构函数被调用时,又抛出了另一个异常,这将导致std::terminate被调用,程序异常终止

  1. 修改:

建议

  1. 捕获所有异常:
    1. 在析构函数中捕获所有可能的异常,确保不让它们传播出去
  2. 记录错误:
    1. 在捕获异常后,可以记录错误信息以便于调试和分析
  3. 释放资源:
    1. 在处理异常时,确保所有资源(如内存、文件句柄)都被正确释放

9 在构造和析构过程中不要调用虚函数

概述

  1. 在构造和析构过程中,虚函数的动态绑定机制无法正常工作

为什么不应该在构造和析构过程中调用虚函数

  1. 对象未完全构造:
    1. 在构造函数中,对象的基类部分可能已经构造,但派生类部分可能尚未构造完全
    2. 如果在此时调用虚函数,调用的将是基类版本的函数,而不是派生类版本的函数
  2. 对象未完全析构:
    1. 在析构函数中,派生类部分已经被析构,但基类部分可能尚未析构完全
    2. 如果在此时调用虚函数,调用的将是基类版本的函数,而不是派生类版本的函数
  3. 未定义行为:
    1. 在构造或析构过程中调用虚函数,会导致不可预测的行为和潜在的错误

错误示例

  1. 以下代码示例:

  1. 首先会调用基类的构造函数,然后再调用派生类的构造函数
    1. 然而,在Base的构造函数中调用init,输出结果是Base::init,而不是Derived::init,因为此时Derived部分尚未构造
  2. 首先调用派生类的析构函数,然后调用基类的析构函数
    1. 上述示例派生类没有析构函数,编译器生成默认的析构函数,该析构函数实现中会插入调用基类析构函数的代码
      第一次在Base的析构函数中调用cleanup,输出结果是Derived::cleanup,因为此时的派生类对象是构造完成的
    2. 再调用基类析构函数
      第二次在Base的析构函数中调用cleanup,输出结果是Base::cleanup,因为Derived部分已经被析构了

解决方案

  1. 使用非虚函数:
    1. 有问题,还是间接地在构造或析构函数中调用了虚函数
    2. 在构造函数和析构函数中调用非虚函数,这些非虚函数可以调用所需的虚函数

  1. 后期初始化:
    1. 无问题
    2. 将需要调用的虚函数放在构造函数之外,由外部代码在对象构造完成后显式调用

10 赋值运算符应该返回一个指向自身的引用

概述

  1. C++中定义赋值运算符时,应该返回一个对当前对象的引用
  2. 这样做不仅遵循了C++的惯例,还可以支持链式赋值操作

如何实现赋值运算符返回*this

示例代码

11 在赋值运算符中处理自我赋值

概述

  1. 自我赋值是指一个对象赋值给自己
    1. 例如a = a。如果不正确处理自我赋值,可能会导致资源管理问题或数据损坏

为什么需要处理自我赋值

  1. 避免资源泄漏:
    1. 在没有处理自我赋值的情况下,如果先释放了旧资源再进行赋值操作,可能会导致释放了自己正在使用的资源
  2. 确保数据完整性:
    1. 如果不正确处理自我赋值,可能会导致数据被部分或完全破坏

解决方法

  1. 在赋值运算符中,首先需要检查自我赋值,然后再执行赋值操作

  1. 在执行赋值操作之前,首先检查this指针是否与右值rhs的地址相同
    1. 如果相同,则是自我赋值,直接返回当前对象的引用即可

总结

  1. 避免资源泄漏和数据损坏:
    1. 在赋值运算符中正确处理自我赋值,可以避免资源泄漏和数据损坏
  2. 检查自我赋值:
    1. 通过比较this指针和右值的地址来检查自我赋值
  3. 正确管理资源:
    1. 在赋值运算符中,首先分配新资源,然后释放旧资源,最后更新数据指针并返回当前对象的引用

示例代码

  1. 以下是一个包含动态内存分配的类的完整示例,展示如何正确处理自我赋值:

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0 最后编辑于:2024-07-02
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享