Cpp基础

Cpp基础

fetch150zy

关于C++中的一些细节之处

base

overloading

在函数重载中,不能根据函数返回值来区分重载。

对于普通函数,不能单纯使用const修饰来区分重载(const并不会影响函数签名,普通函数的重载完全依赖于参数类型、数量、顺序);而对于类成员函数,可以通过const来区分重载,成员函数可以作用于 const 对象或非 const 对象,因此可以被视为不同的重载。

  • 关于函数重载的原理

    函数重载在C++中的实现主要依靠编译器的名称修饰(Name Mangling)和函数匹配机制

    名称修饰是一个将函数的名称和参数列表编码为唯一表示的过程。这样,即使多个函数有相同的基本名称,它们在编译后的二进制代码中也有不同的标识符。

    当调用一个重载的函数时,编译器会根据调用中提供的参数类型、数量和顺序来确定应该使用哪个函数版本。这个过程称为函数匹配。编译器会查看所有重载函数的候选,并选择与提供的参数最匹配的那个。

    最佳匹配:如果有多个重载函数候选,编译器会根据一系列规则来选择“最佳匹配”。这些规则包括参数类型的精确匹配、需要最少隐式转换的匹配等。如果编译器不能确定最佳匹配,或者找到两个或更多同样匹配的候选,它会报错,表示有歧义。

    整个重载解析是在编译期完成的,不会影响性能

  • 关于函数的最佳实践

    1. Keep function short
    2. Do one logical thing(single-responsibility principle)
    3. Use expressive names
    4. Document non-trivial functions

[[fallthrough]] attribute

Since C++ 17, compilers are encouraged to warn on fall-through

  • example

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    switch (c) {
    case 'a':
    f(); // Warning emitted
    case 'b': // Warning probably suppressed
    case 'c':
    g();
    [[fallthrough]]; // Warning suppressed
    case 'd':
    h();
    }

range-based loops

Allows to limit variable scope in range-based loops

1
2
3
4
5
6
// in c++ 17
std::array data = {"hello", ",", "world"};
std::size_t i = 0;
for (auto & d: data) {
std::cout << i++ << ' ' << d << '\n';
}
1
2
3
4
// in c++ 20
for (std::size_t i = 0; auto const & d: data) {
std::cout << i++ << ' ' << d << '\n';
}

Inline keyword

Inline function originally

  • Applies to a function to tell the compiler to inline it

    That is, replace function calls by the function’s content(similar to how a macro works)

  • Only a hint, compiler can still choose to not inline

  • Avoids call overhead at the cost of increasing binary size

Major side effect

  • The linker reduces the duplicated functions into one
  • An inline function definition can thus live in header files

Inline functions nowadays

  • Compilers can judge far better when to inline or not
  • Putting functions into headers became main purpose
  • Many types of functions are marked inline by default:
    • function templates
    • constexpr functions
    • class member functions

Assertions

Checking invariants in a program

  • An invariant is a property that is guaranteed to be true during certain phases of a program, and the program might crash or yield worong results if it is violated
  • This can be checked using assert
  • The program will be aborted if the assertion fails

Good practice: Assert

  • Assertions are mostly for developers and debugging
  • Use them to check import invariants of your program
  • Prefer handling user-facing errors with help error messages/exceptions
  • Assertions can impact the speed of a program
    • Assertions are disabled when the macro NDEBUG is defined
    • Decide if you want to disable them when you release code

Better: Static Assert

Checking invariants at compile time

  • To check invariants at compile time, use static_assert
  • The assertion can be any constant expression
  • The message argument is optional in C++ 17 and later
1
2
3
4
5
6
double f(UserType a) {
static_assert(
std::is_floating_point_v<UserType>,
"This fucntion expects floating-point types.");
return std::sqrt(a);
}

:astonished: Good practice

使用 assert 来验证运行时条件,如函数参数的有效性或算法的中间状态。

使用 static_assert 来验证编译时的条件,如模板类型的属性、数组大小或类型支持的操作。

Object orientation

Objects and Classes

Implementing methods

:kissing_heart: Good practice: Implementing methods

  • usually in .cpp, outside of class declaration
  • using the class name as “namespace”
  • short member functions can be in the header
  • some functions (templates, constexpr) must be in the header

final keyword

C++11引入:限制类的继承以及阻止成员函数的覆盖

  • 阻止类被继承

    1
    2
    class Base final { };
    class Derived : public Base { /* 编译错误:Base 是一个 final 类,不能被继承 */ };
  • 阻止派生类中覆盖方法

    1
    2
    3
    4
    5
    6
    7
    8
    class Base {
    public:
    virtual void foo() final { /* foo 函数不能在派生类中被覆盖 */ }
    };
    class Derived : public Base {
    public:
    void foo() override { /* 编译错误:foo 被声明为 final,不能被覆盖 */ }
    };

static members

类的静态成员是与类本身相关联的成员,而不是与类的各个实例相关联的

静态成员变量

  1. 共享:静态成员变量为类的所有对象所共享。无论创建了多少个类的实例,都只有一个静态成员变量的拷贝
  2. 独立于任何对象:即使没有创建类的实例,静态成员变量也存在,它们与类类型自身关联,而不是与特定的实例关联
  3. 初始化:静态成员变量需要在类定义外进行初始化(通常在源文件中),对于整型或枚举类型的常量静态成员,可以在类内直接初始化
  4. 访问:静态成员变量可以通过类名和作用域解析运算符 :: 访问,也可以通过类的对象或引用访问

静态成员函数

  1. 独立于对象:静态成员函数不依赖于类的任何特定实例。它们不能访问类的非静态成员变量,也不能调用非静态成员函数
  2. 全局访问:静态成员函数可以在不创建类的实例的情况下调用
  3. 访问限制:静态成员函数遵循正常的访问控制规则(publicprotectedprivate

:confused: example

1
2
3
4
5
6
7
8
9
10
class MyClass {
public:
static int staticVar; // 静态成员变量声明

static void staticFunc() { // 静态成员函数
// 可以访问静态成员变量,但不能访问非静态成员变量
}
};

int MyClass::staticVar = 0; // 静态成员变量定义和初始化

静态成员变量通常用于存储与类相关的信息,如对象计数器或类特定的常量值

静态成员函数常用于实现与类的具体实例无关的功能,如工厂方法或辅助函数

构造函数

Rule of 3/5

:astonished: Good practice: The rule of 3/5(C++98/C++11)

  • Rule of three

    如果你的类需要自定义析构函数、拷贝构造函数或拷贝赋值操作符中的任何一个,那么你很可能需要自定义所有这三个

    这是因为自定义析构函数通常意味着你的类在管理(如分配和释放)资源,这通常需要自定义的拷贝控制来正确地复制或赋值这些资源

  • Rule of five

    规则五包括规则三中的三个函数(析构函数、拷贝构造函数、拷贝赋值操作符)以及两个新的函数:移动构造函数和移动赋值操作符

    如果你需要自定义析构函数,你很可能也需要自定义所有五个函数,以正确处理移动语义,从而提高效率

构造函数中的初始化列表

:+1: 成员初始化列表提供的值优先于类定义处的默认值

成员初始化列表直接初始化成员变量,而不是先默认初始化然后赋值;对于某些类型(如引用和常量成员),这是必需的,因为它们不能被赋值

其次就是在效率方面:对于非内置类型的成员,使用初始化列表通常比在构造函数中赋值更高效,避免了额外的构造和赋值

默认构造函数

  • 定义:如果一个构造函数可以不带任何参数被调用(或者所有参数都有默认值),则称为默认构造函数

  • 作用:在不提供任何初始化值的情况下创建对象

  • 示例:

    1
    2
    3
    4
    5
    class Example {
    public:
    Example() = default; // 保持了特殊成员函数的自动生成:比如拷贝构造函数,析构函数等
    Example() { } // 破坏了类的聚合类型属性,可能会阻止编译器生成其他特殊成员函数
    };

行为:

  1. 对于内置类型的成员:默认构造函数不会初始化这些成员,它们的初始值是未定义的,除非它们被显示初始化
  2. 对于类类型的成员:这些成员的默认构造函数将被调用来初始化它们
  3. 对于继承的情况:如果类是从其他类继承的,基类的默认构造函数将被调用
  4. 对于带有默认成员初始化的成员:在C++11及更高版本中,可以在类定义中直接给成员变量赋初值,如果这样做了,即使是默认的构造函数,这些成员也会被初始化为指定的值

参数化构造函数

  • 定义:带有一个或多个参数的构造函数,用于根据提供的参数初始化对象

  • 作用:提供更灵活的初始化方式,允许在创建对象时指定初始值

  • 示例:

    1
    2
    3
    4
    class Example {
    public:
    Example(...args) { /* 构造函数体 */ }
    };

行为:

  1. 实现类型转换:如果参数化构造函数没有被声明为explicit,它还可能用于隐式类型转换

拷贝构造函数

  • 定义:参数为对同类型对象的引用(通常为常量引用)的构造函数

  • 作用:用于通过复制另一个同类型对象来初始化新对象

  • 示例:

    1
    2
    3
    4
    class Example {
    public:
    Example(const Example & other) { /* 拷贝构造函数体 */ }
    };

行为:

  1. 创建对象副本:拷贝构造函数通过接收一个同类型对象的引用作为参数来创建新对象的副本。这个过程包括复制现有对象所有的成员变量的值
  2. 被隐式调用:当函数通过值传递或者函数返回对象时,拷贝构造函数会被隐式调用

移动构造函数(C++11引入)

  • 定义:参数为同类型对象的右值引用的构造函数

  • 作用:允许资源的转移,提高效率,尤其用于临时对象

  • 示例:

    1
    2
    3
    4
    class Example {
    public:
    Example(Example && other) { /* 移动构造函数体 */ }
    };

行为:

  1. 参数类型:移动构造函数接受一个同类型对象的右值引用作为参考
  2. 资源转移:与拷贝构造函数(进行深拷贝)不同,移动构造函数设计用来窃取源对象的资源。这意味着它通过将资源从源对象转移到新创建的对象,来实现快速构造,而不是复制资源
  3. 优化临时对象:移动构造函数特别适用于临时对象或将要销毁的对象,因为这些情况下资源转移是安全的。比如在函数返回临时对象或进行对象赋值时,移动构造函数可以显著提高效率
  4. 自动生成规则:如果一个类没有自定义拷贝构造函数、拷贝赋值操作符、移动赋值操作符以及析构函数,编译器可能会自动生成移动构造函数

委托构造函数(C++11引入)

定义:一个构造函数在其初始化列表中调用同一类的另一个构造函数

  • 作用:简化多个构造函数的代码,避免重复

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    class Example {
    public:
    Example(int x): m_x(x) {}
    Example(int x, int y): Example(x), m_y(y) {} // 委托构造函数和成员初始化列表不能同时存在
    Example(int x, int y): Example(x) { this->m_y = y; } // ok
    private:
    int m_x, m_y;
    };

行为:

  1. 减少重复代码:委托构造函数可以减少在多个构造函数中重复相同的初始化代码。这有助于提高代码的可维护性和一致性
  2. 初始化顺序:在委托构造函数中,首先执行被调用的构造函数,然后才执行调用构造函数的主体部分
  3. 限制:不能在同一个构造函数中同时使用委托构造函数和成员初始化列表
  4. 使用场景:适合于那些有多个构造函数,且这些构造函数之间有共同初始化逻辑

显式构造函数

  • 定义:使用 explicit 关键字标记的构造函数

  • 作用:防止隐式类型转换和意外的构造函数调用

  • 示例:

    1
    2
    3
    4
    class Example {
    public:
    explicit Example(...) { /* 构造函数体 */ }
    };

隐式构造函数

  • 定义:未用 explicit 关键字标记的构造函数
  • 作用:允许编译器在需要时自动调用该构造函数进行类型转换

删除的构造函数(C++11引入)

  • 定义:使用 delete 关键字明确禁止的构造函数

  • 作用:防止生成默认的构造函数或禁止某些类型的对象构造

  • 示例:

    1
    2
    3
    4
    class Example {
    public:
    Example() = delete;
    };

继承的构造函数(C++11引入)

  • 定义:子类可以使用 using 语句继承基类的构造函数

  • 作用:允许子类继承基类的构造函数,简化代码

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    class Base {
    public:
    Base(...) {}
    };
    class Derived: public Base {
    using Base::Base;
    }

行为:

  1. 自动转发参数:当使用基类的构造函数创建派生类对象时,构造函数的参数会被自动转发给基类的对应构造函数

不支持函数重载

关于委托构造函数和直接初始化的一些细节:

在需要代码重用和保持初始化逻辑一致时有限考虑委托构造函数

构造函数初始化逻辑和独立且简单时优先考虑直接初始化

Polymorphism

Memory organization

聚合类型

在C++中,聚合类型是一种简单的数据结构,它允许用花括号初始化列表来初始化成员:聚合类型包括数组和具有以下特性的类或结构体

  • 无用户提供的构造函数:聚合类型不能有任何用户提供的构造函数,这意味着不能有任何形式的自定义构造函数,包括那些体为空的构造函数

  • 无私有或保护的非静态数据成员:聚合类型的所有非静态的数据成员都必须是公有的,私有或保护的成员会阻止类型被视为聚合

  • 无虚函数和虚基类:聚合类型不能有虚函数,也不能从虚基类派生

  • C++17开始,如果特殊成员函数被显式默认或删除(= default= delete),不影响类型的聚合性

  • C++14开始,聚合类型可以包含带有初始值设定项的非静态成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct Aggregate {
    int x = 1; // 带有初始值设定项的非静态成员
    double y = 2.0;
    };

    Aggregate a1; // a1.x = 1, a1.y = 2.0
    Aggregate a2 = {}; // a2.x = 1, a2.y = 2.0
    Aggregate a3 = {3}; // a3.x = 3, a3.y = 2.0
    Aggregate a4 = {4, 4.0};// a4.x = 4, a4.y = 4.0
  • 关于聚合类型的静态成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct Aggregate {
    static int staticValue; // 静态成员变量
    int x;
    double y;
    };

    int Aggregate::staticValue = 10;// 静态成员变量的定义

    Aggregate agg = {1, 2.0}; // 聚合初始化

实际开发中的用途:

  1. 简单数据结构:配置选项、坐标点、数据记录等通常只包含数据而无复杂的行为的简单数据结构
  2. 兼容C结构体:聚合类型在C++中与C语言的结构体兼容性良好,在涉及C和C++的混合编程时非常有用,可以简化数据结构的传递和共享
  3. 序列化和反序列化:聚合类型由于其结构简单,容易被映射到文件,网络数据或其他序列化形式,使得系列化和反序列化过称更加直接和高效
  4. POD类型:聚合类型通常也是平凡的(Trivial)和标准布局(Standard Layout),这使得它们成为处理低级数据操作的理想选择,如直接内存操作、与硬件接口交互等

POD类型

POD类型是一种具有简单内存布局的类型,类似于C中的数据结构。POD类型在内存布局、初始化和拷贝行为上都非常简单,这使得他们非常适合于低级的编程任务,如与C交互、直接操作内存、进行二进制IO等

简单内存布局

POD类型具有与C兼容的内存布局,这意味着没有复杂的类特性,像虚函数或虚基类。它们的内存布局是连续且预测的

两大类别

POD类型分为两大类别:标准布局类型(Standard-layout type)和平凡类型(Trivial type)

  • 标准布局类型:与C结构体有兼容的内存布局,而不关心构造、析构和拷贝的复杂性

    所有非静态成员有着相同的内存布局,继承结构简单(不能有虚基类),无虚函数,类的非静态成员都应该在同一访问控制区,简单的基类(被继承的基类也要是标准布局类型)

    C++11及更高版本提供了类型特征类,用于检测一个类型是否是标准布局类型。std::is_standard_layout<T>::value 可用来检查类型 T 是否是标准布局类型

  • 平凡类型:构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符都是平凡的

    无自定义构造函数,无自定义析构函数,拷贝构造函数和拷贝赋值运算符仅执行简单的位复制

    C++11及更高版本提供了类型特征类,用于检测一个类型是否是平凡类型。
    std::is_trivial<T>::value 可用来检查类型 T 是否是平凡类型

特性

  • 无自定义构造、析构和赋值函数:POD类型不能有自定义的构造函数、析构函数或赋值运算符
  • 无虚函数或虚基类:POD类型不能包含虚函数或继承自虚基类
  • 非静态成员都是POD:POD类型的非静态数据成员也必须是POD类型
  • 无非静态引用成员:POD类型不能包含非静态引用成员

用途

  • 与C语言的互操作:POD类型可以安全地在C和C++代码之间传递,因为它们的内存布局和行为与C结构体兼容
  • 性能敏感的应用:在性能敏感的应用中,比如嵌入式系统、系统编程或游戏开发,POD类型由于其简单性和高效性而受到青睐
  • 序列化和网络通信:由于POD类型可以通过简单的内存拷贝进行复制,它们非常适合于序列化和网络通信

检测POD类型

在C++11及更高版本中,可以使用 std::is_pod<T>::value 来检查一个类型 T 是否是POD类型

关于POD类型和聚合类型的的联系与区别

  • 联系:所有POD类型都是聚合类型,但并非所有的聚合类型都是POD类型,这意味着聚合类型是一个更广泛的概念
  • 区别:
    • 聚合类型主要关注于如何初始化对象(通过花括号初始化列表),而不关心对象的内部结构或构造/析构的复杂性
    • POD类型对类的内部结构和行为有更严格的要求,比如不允许有虚函数、必须是平凡的和标准布局的。这使得POD类型在内存布局和行为上更接近于C语言的结构体