前言

这里我们只做c++部分的总结, 还有一些和c语言不同的地方

版本

发布时间 通称 备注
2020 C++20, C++2a ISO/IEC 14882:2020
2017 C++17 第五个C++标准
2017 coroutines TS 协程库扩展
2017 ranges TS 提供范围机制
2017 library fundamentals TS 标准库扩展
2016 concurrency TS 用于并发计算的扩展
2015 concepts TS 概念库,用于优化编译期信息
2015 TM TS 事务性内存操作
2015 parallelism TS 用于并行计算的扩展
2015 filesystem TS 文件系统
2014 C++14 第四个C++标准
2011 - 十进制浮点数扩展
2011 C++11 第三个C++标准
2010 - 数学函数扩展
2007 C++TR1 C++技术报告:库扩展
2006 - C++性能技术报告
2003 C++03 第二个C++标准
1998 C++98 第一个C++标准

数据类型

多出了一些类型

  • bool
  • class

存储类

  • auto 自 C++ 11 以来,auto 关键字用于两种情况:声明变量时根据初始化表达式自动推断该变量的类型声明函数时函数返回值的占位符
  • register
  • static
  • extern
  • mutable
  • thread_local (C++11)

从 C++ 17 开始,auto 关键字不再是 C++ 存储类说明符,且 register 关键字被弃用。

引用

这个是c++多出来的东西, 它相当于一个变量的别名,操作它就像在操作它引用的变量一样, 这样可以传递引用数据给函数, 解决修改形参不能影响实参的问题

函数

  • c++ 中 函数是支持重载的
  • 函数参数可以有默认值, 但是必须是最右边连续的若干个参数
  • c++11多出了Lambda表达式

Lambda表达式

Lambda 表达式具体形式如下:

1
[capture](parameters)->return-type{body}

在Lambda表达式内可以访问当前作用域的变量,这是Lambda表达式的闭包(Closure)行为, 但是访问行为是通过capture控制的

1
2
3
4
5
6
[]      // 沒有定义任何变量。使用未定义变量会引发错误。
[x, &y] // x以传值方式传入(默认),y以引用方式传入。
[&] // 任何被使用到的外部变量都隐式地以引用方式加以引用。
[=] // 任何被使用到的外部变量都隐式地以传值方式加以引用。
[&, x] // x显式地以传值方式加以引用。其余变量以引用方式加以引用。
[=, &z] // z显式地以引用方式加以引用。其余变量以传值方式加以引用。

如果使用了 = 那么被捕获的变量是const类型的, 不能修改这个变量, 如果被捕获的是一个对象,那么是无法通过这个变量调用它的非常量成员函数的

通过引用方式传入一定要保证在Lambda函数使用期间这个值要存在,没有销毁

类和对象

一些函数

C++ 类中的构造函数、析构函数和其他相关函数是类设计的核心部分,它们负责对象的创建、初始化、复制、移动和销毁。下面是对这些函数的简要总结:

  1. 构造函数 (Constructor): 用于创建和初始化对象。构造函数可以有不同的形式:

    • 默认构造函数:无需任何参数,用于创建类的默认对象。
    • 带参数的构造函数:允许传递参数,用于根据这些参数初始化对象。
    • 拷贝构造函数:以同类的另一个对象为参数,用于创建一个新对象作为原对象的副本。
    • 移动构造函数:以同类的右值引用作为参数,用于支持将资源从一个对象转移到另一个对象。
  2. 析构函数 (Destructor): 用于在对象生命周期结束时进行清理。析构函数没有参数,也不返回任何值。它通常用于释放对象占用的资源,如动态分配的内存。

  3. 拷贝赋值运算符:允许将一个对象的内容复制到另一个已经存在的对象中。这通常涉及深拷贝和浅拷贝的考虑。

  4. 移动赋值运算符:允许将一个对象的资源“移动”到另一个已经存在的对象中。这通常用于优化性能,避免不必要的资源复制。

  5. 友元函数 (Friend Function): 虽然不是类的成员函数,但可以访问类的所有私有和保护成员。

this

在C++中,this是一个特殊的指针,它在每个类的非静态成员函数中都可用。this指针指向调用当前成员函数的对象。这意味着,通过this指针,成员函数可以访问调用它的对象的其他成员。

以下是关于this指针的一些重要点:

  1. 隐式参数this在所有非静态成员函数中都是可用的,即使你在函数参数列表中没有明确声明它。
  2. 常量指针this是一个常量指针,这意味着你不能改变this指向的对象。
  3. 成员访问this通常用于在成员函数内部访问对象的其他成员(包括数据成员和其他成员函数)。
  4. 链式调用this指针常常被用于实现链式函数调用。如果一个成员函数返回*this,那么可以连续调用同一对象的多个成员函数

C++编译器在编译类的成员函数时,会为这些函数添加一个额外的参数,通常是作为第一个参数,这个额外的参数就是this指针,它指向调用该成员函数的对象。

当我们调用一个成员函数时,例如 obj.func(), 实际上这会被编译成类似 func(&obj) 这样的形式。这样,即使我们没有明确写出this,成员函数内部也能通过this指针来访问对象的数据成员和其他成员函数。

静态成员

在 C++ 中,使用 static 关键字声明的类成员具有以下特性:

  1. 静态成员变量
    • 所有该类的对象共享同一个静态成员变量,即类的所有实例都会使用同一份数据。
    • 静态成员变量只有一个类级别的存储空间,不会随着对象的创建和销毁而创建或销毁。
    • 静态成员变量需要在类定义外进行单独的定义和初始化。(类里面的静态变量相当于只是声明,需要在外部定义) 站在c语言的角度其实就好理解了, 如果在类里面就能初始化这个静态变量的话, 那岂不是说这个变量是在这个文件里面定义, 那这样的话, 如果多个文件包含了这个头文件, 必然报错
    • 静态成员变量可以通过对象访问,也可以通过类名访问。
  2. 静态成员函数
    • 静态成员函数可以在不创建对象的情况下直接使用类名来调用,它只能访问静态成员变量和静态成员函数,不能访问非静态成员变量或非静态成员函数。
    • 静态成员函数不含有 this 指针。

友元

在C++中,friend关键字被用来声明友元,它能够让一个函数或者类访问其他类的私有和保护成员。以下是关于友元的一些主要特点:

  1. 友元函数:如果一个函数(可以是类的成员函数,也可以是全局函数)被声明为一个类的友元,那么这个函数可以访问这个类的所有成员(包括私有和保护成员)。友元函数不是这个类的成员,也不受类的访问控制规则的约束。友元函数在类外定义,但需要在类内通过friend关键字声明。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class OtherClass;

class MyClass {
private:
int value;
public:
MyClass(): value(10) {}
// 将OtherClass的特定成员函数声明为友元
friend void OtherClass::accessMyClass();
// 全局函数
friend void friendFunction(MyClass &obj);
};

class OtherClass {
public:
void accessMyClass(const MyClass& obj) {
std::cout << "Value in MyClass: " << obj.value << std::endl; // 可以访问私有成员
}
};

void friendFunction(MyClass &obj) {
obj.value = 10; // 可以访问私有成员
}

在这个例子中,friendFunction被声明为MyClass的友元函数,所以它可以访问MyClass的私有成员value

  1. 友元类:如果一个类被声明为另一个类的友元,那么这个类的所有成员函数都可以访问另一个类的所有成员(包括私有和保护成员)。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyOtherClass;

class MyClass {
private:
int value;
public:
friend class MyOtherClass;
};

class MyOtherClass {
public:
void function(MyClass &obj) {
obj.value = 10; // 可以访问私有成员
}
};

在这个例子中,MyOtherClass被声明为MyClass的友元类,所以MyOtherClass的成员函数function可以访问MyClass的私有成员value

  1. 友元的使用:虽然友元提供了一种突破类的封装性的方法,但是它应该谨慎使用。过度使用友元可能会破坏封装性和隐藏性,这可能会导致代码更难理解和维护。在大多数情况下,可以通过其他方式(如公有成员函数)来访问私有和保护成员。

  2. 友元和继承:友元关系不能被继承。如果类B是类A的友元,并且类C继承了类A,那么类B不是类C的友元。同样,如果类B是类A的友元,并且类B继承了类D,那么类D不是类A的友元。

常量对象,常量成员函数

在C++中,常量对象和常量成员函数有如下的特性:

  1. 常量对象如果一个对象被声明为常量,那么在其生命周期内,它的值都不能被改变。这种对象常常用于保存不应该被修改的值,例如配置信息或者全局设置。常量对象必须在创建时初始化。例如:

    1
    2
    const int x = 10; // x现在是一个常量,不能被修改
    const MyClass obj; // obj是一个常量对象,不能修改其任何成员变量
  2. 常量成员函数:如果一个成员函数被声明为常量,那么在该成员函数内,不能修改任何非静态成员变量的值。常量成员函数通过在函数参数列表后添加const关键字来声明。这种函数常常用于定义在逻辑上应该不修改成员变量值的操作,例如访问器(getter)函数。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class MyClass {
    private:
    int value;
    public:
    MyClass(int v) : value(v) {}

    // 常量成员函数
    int getValue() const {
    return value;
    }
    };

    在这个例子中,getValue是一个常量成员函数,它不能修改value成员变量的值。

  3. 常量对象和常量成员函数的关系常量对象只能调用常量成员函数,非常量对象可以调用常量成员函数和非常量成员函数。这是因为常量对象不能修改其成员变量的值,而常量成员函数保证了它不会修改成员变量的值,所以只有常量成员函数才能在常量对象上调用。

一个常量成员函数和一个非常量成员函数,即使他们的参数完全相同,也被视为重载(overloaded)。在 C++ 中,成员函数的常量性被视为函数签名的一部分,因此,下面两个函数会被视为两个不同的函数:

1
2
3
4
5
class MyClass {
public:
void func() { /* ... */ } // 非常量成员函数
void func() const { /* ... */ } // 常量成员函数
};

在这个例子中,func()func() const是两个不同的函数,一个是常量成员函数,一个是非常量成员函数。当你有一个非常量对象时,两者都可以被调用,但是如果有一个具体的操作需要改变对象状态,那么非常量版本的函数将被调用,如果要调用常量版本函数,需要转化成常量对象;如果操作不需要改变对象状态,常量版本的函数将被调用。如果你有一个常量对象,只有常量成员函数可以被调用,尝试调用非常量成员函数将导致编译错误。

以上是关于C++中常量对象和常量成员函数的基本介绍,理解这些概念对于编写更安全和更高效的C++代码非常重要。

运算符重载

C++允许我们对大多数内置的运算符进行重载,从而使我们可以使用自然的符号来操作自定义数据类型。下面是关于C++运算符重载的一些重要内容:

  1. 基本规则:你可以为任何自定义数据类型(比如类或结构体)重载运算符。运算符重载函数可以是成员函数或非成员(但需声明为友元)函数。需要注意的是,运算符重载并不改变运算符的优先级。

  2. 成员与非成员:大多数运算符可以通过成员函数或非成员函数进行重载。但有几个例外:赋值运算符(= )、下标运算符([])、函数调用运算符(())和成员访问运算符(->)只能通过成员函数重载。如果存在成员运算符重载和非成员运算符重载,编译器会优先调用成员运算符重载

  3. 一元和二元运算符:一元运算符(例如++---!等)通常作为成员函数重载,没有参数。二元运算符(例如+-*/%等)通常有一个参数作为成员函数重载,或两个参数作为非成员函数重载。

  4. 复合赋值运算符:像+=-=*=等复合赋值运算符也可以被重载。它们通常作为成员函数重载,有一个参数。

  5. 关系和逻辑运算符:关系运算符(==!=<><=>=)和逻辑运算符(&&||)通常作为非成员函数重载,以便对两个参数进行操作。尽管这些可以作为成员函数重载,但在使用中可能会产生歧义。

  6. 输入/输出运算符:流插入运算符<<和流提取运算符>>通常作为非成员函数重载,因为它们的左操作数通常是std::ostreamstd::istream对象,而不是自定义类型。

  7. 限制:并非所有的运算符都可以重载。有些运算符,例如.(点运算符),.*(点星运算符),::(作用域解析运算符),sizeof(大小运算符)以及三目运算符?:不能被重载。

  8. 自然性和一致性:虽然运算符重载提供了大量的自由度,但是滥用运算符重载可能会导致代码难以理解和维护。当你选择重载一个运算符时,你应该让它的行为尽可能地符合程序员对这个运算符的直觉。例如,+运算符应该总是表示一种合并或添加操作,而不应该被用来表示一种完全不相关的操作。

以上就是关于C++运算符重载的基本介绍,理解这个概念对于编写更自然和更高效的C++代码非常重要。

前置运算符++ 和 – 作为一元运算符 后置 运算符++ 和 – 作为二元运算符

继承

C++的继承是面向对象编程的一个重要特性,它允许我们创建一个新的类(派生类)来继承一个已有类(基类)的特性,并可以增加新的特性。以下是关于C++继承的一些关键内容:

  1. 基础:在C++中,一个类可以从一个或多个已有的类继承特性。单继承表示一个类只能从一个类继承,多继承表示一个类可以从多个类继承。

  2. 访问控制派生类可以访问基类的公有和保护成员,但不能访问基类的私有成员

  3. 继承类型:C++支持三种类型的继承:公有继承(public)、保护继承(protected)和私有继承(private)。公有继承是最常用的,它表示公有和保护成员的访问级别在派生类中保持不变。保护继承意味着所有基类的公有和保护成员在派生类中都变为保护的。私有继承表示所有基类的公有和保护成员在派生类中都变为私有的。

  4. 构造和析构派生类的构造函数会自动调用基类的默认构造函数,然后执行派生类自己的构造函数。如果需要调用基类的其他构造函数,你需要在派生类的构造函数初始化列表中显式调用。析构的顺序与构造的顺序相反,首先执行派生类的析构函数,然后执行基类的析构函数。

  5. 函数重载和覆盖:如果派生类定义了一个与基类中的函数具有相同名称和参数的函数,那么这个函数会覆盖基类的函数,这就是所谓的函数覆盖。如果派生类的函数与基类的函数名相同,但参数不同,那么它就是函数重载

  6. 虚函数和多态:虚函数允许我们利用指向基类的指针或引用来调用派生类中的函数,这就是多态的基础。如果基类指针或引用指向的是派生类对象,那么调用的是派生类的函数。这就是所谓的运行时多态性。

  7. 抽象类:如果一个类包含至少一个纯虚函数(用= 0声明的虚函数),那么这个类就是抽象类。抽象类不能被实例化,只能作为基类。派生类必须实现所有的纯虚函数,除非派生类也是抽象类。

以上就是关于C++继承的主要内容。理解和使用继承是掌握面向对象编程的关键。

多态

多态是面向对象编程的三大特性之一(封装、继承和多态)。在C++中,多态可以分为两种主要类型:编译时多态(也称为静态多态)和运行时多态(也称为动态多态)。

1. 编译时多态:

编译时多态主要是通过函数重载和模板实现的。函数重载允许在同一作用域内有多个名称相同但参数列表不同的函数。编译器会根据函数的调用上下文(具体的参数类型和数量)在编译时确定调用哪个函数。模板则是一种泛型编程机制,它允许你编写能够处理不同类型的数据的代码,而具体的类型在编译时确定。

2. 运行时多态:

运行时多态是通过虚函数实现的。如果一个类(基类)有一个或多个虚函数,并且有另一个类(派生类)继承了这个类并重写了这些虚函数,那么通过基类的指针或引用调用这些函数时,实际上调用的是派生类的版本。这个决定是在运行时作出的,因此称为运行时多态。

以下是一个运行时多态的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base {
public:
virtual void print() { cout << "Base" << endl; }
};

class Derived : public Base {
public:
void print() override { cout << "Derived" << endl; }
};

int main() {
Derived derived;
Base* basePtr = &derived;
basePtr->print(); // 输出 "Derived"
return 0;
}

在这个例子中,print()Base类的虚函数,Derived类重写了这个函数。我们使用Base类的指针basePtr指向一个Derived类的对象,然后通过这个指针调用print()函数,实际上调用的是Derived类的版本。

值得注意的是,运行时多态在C++中需要满足以下条件:

  1. 必须存在继承关系。
  2. 基类中的函数必须是虚函数,并在派生类中被重写。
  3. 必须通过基类的指针或引用来调用这些函数。

多态的原理

在C++中,运行时多态是通过虚函数表(vtable)和虚函数表指针(vptr)实现的。虚函数表是一个存储类的虚函数地址的表,每一个具有虚函数的类都有自己的虚函数表;虚函数表指针是一个指向虚函数表的指针,每一个具有虚函数的对象都有一个虚函数表指针。

以下是具体的工作原理:

  1. 虚函数表的创建:当一个类被定义时,如果这个类有虚函数(包括从基类继承来的虚函数),编译器就会为这个类生成一个虚函数表。虚函数表是一个存储函数地址的数组,其中每个条目对应于一个虚函数。虚函数表的确切布局和内容取决于虚函数的声明顺序和继承结构。
  2. 虚函数表指针的创建:当一个对象被创建时,如果这个对象的类有虚函数,那么这个对象就会包含一个虚函数表指针。这个指针指向这个对象的类的虚函数表。
  3. 调用虚函数:当通过指针或引用调用虚函数时,编译器会生成代码来获取对象的虚函数表指针,然后通过这个指针查找虚函数表,找到虚函数的地址,然后调用这个函数。

这种机制使得编译器在编译时无需知道对象的确切类型,只需要知道对象有一个虚函数表指针,并且知道如何通过这个指针和虚函数的声明找到虚函数的地址,就可以在运行时动态地调用正确的函数。这就是C++运行时多态的原理

虚析构函数 抽象类

1. 虚析构函数

在C++中,如果基类指针指向派生类对象,并且通过基类指针来删除派生类对象如果基类的析构函数不是虚函数,那么就只会调用基类的析构函数,而不会调用派生类的析构函数,这可能会导致资源泄露。为了解决这个问题,如果一个类可能被用作基类,并且可能通过基类指针来删除派生类对象,那么应该将析构函数声明为虚函数。

2. 纯虚函数

纯虚函数是没有定义的虚函数,声明时在函数的结尾处添加 = 0。纯虚函数主要用于定义接口,它们在基类中没有定义,在派生类中必须被重写。例如:

1
2
3
4
class AbstractClass {
public:
virtual void pureVirtualFunction() = 0; // 纯虚函数
};

3. 抽象类

包含纯虚函数的类被称为抽象类。抽象类不能实例化,它的主要目的是作为基类,定义接口供派生类实现。例如:

1
2
3
4
5
6
7
8
9
10
11
class AbstractClass {
public:
virtual void pureVirtualFunction() = 0; // 纯虚函数
};

class ConcreteClass : public AbstractClass {
public:
void pureVirtualFunction() override {
cout << "ConcreteClass::pureVirtualFunction()" << endl;
}
};

在这个例子中,AbstractClass是一个抽象类,ConcreteClass是一个派生类,它实现了AbstractClass的纯虚函数pureVirtualFunction。因此,我们可以创建ConcreteClass的实例,但不能创建AbstractClass的实例。

模板

在C++中,模板是用于实现泛型编程的工具。它们允许程序员创建可以处理不同数据类型的函数或类,而不必为每个类型都编写单独的代码。C++模板包括两种类型:函数模板和类模板。

  1. 函数模板:函数模板用于创建可以适应多种类型的函数。基本语法如下:
1
2
3
4
5
6
7
8
9
template <typename T>
void func(T param) {
// 函数体
}

// 通过参数生成
func(1);
// 显式指定
func<int>(1);

这里的T是一个占位符,表示任何数据类型。当函数被调用时,编译器会根据传入参数的实际类型生成相应的函数。

  1. 类模板:类模板允许程序员创建能够处理任意数据类型的类。基本语法如下:
1
2
3
4
template <typename T>
class ClassName {
// 类定义
};

和函数模板一样,这里的T也是一个占位符,表示任何数据类型。当创建类的实例时,编译器会根据指定的实际类型生成相应的类。

在使用模板时,还需要注意以下几点:

  • 模板参数不仅可以是typename,也可以是class。在模板参数中,它们是相同的。
  • 模板可以有多个参数,它们用逗号隔开。例如,template <typename T, typename U>
  • 在调用模板函数或实例化模板类时,如果编译器能够自动推断模板参数类型,可以不明确指定模板参数类型。例如,func(5)对于上述的func模板,编译器会自动推断Tint
  • 特化是模板的一个重要特性。它允许程序员为特定类型提供模板的特殊实现。例如,程序员可以为上述的func模板提供一个特别处理int类型的版本。

这就是C++模板的基本概念。模板是C++中非常强大的特性,它使得C++能够支持泛型编程。

输入输出

在C++的iostream库中,有很多用于输入/输出的函数。这个库提供了四个主要的数据流对象:cincoutcerrclog,分别对应于标准输入,标准输出,标准错误和标准日志。

istream常用成员函数

istream类在C++标准库中用于读取数据。这里是istream类的一些常用成员函数:

  1. 读取操作

    • istream& operator>>(istream& is, T& value): 重载的输入运算符用于从输入流中读取不同类型的数据(例如整数、浮点数、字符串等)。
    • istream& getline(istream& is, string& str, char delim): 从输入流中读取一行,直到遇到分隔符(默认为’\n’)为止,并将结果存储在字符串中。
    • istream& get(char& c): 从输入流中读取一个字符并存储在传入的字符变量中。
  2. 流状态查询

    • bool good() const: 如果流未遇到任何错误,则返回true
    • bool eof() const: 如果流到达了文件末尾,则返回true
    • bool fail() const: 如果流在非致命错误(如格式错误)后失败,则返回true
    • bool bad() const: 如果流在致命错误(如读/写操作失败)后失败,则返回true
  3. 位置和格式控制

    • streampos tellg(): 返回当前的输入流位置。
    • istream& seekg(streampos pos): 设置输入流的位置到指定的pos
    • istream& ignore(streamsize n = 1, int delim = EOF): 从输入流中忽略最多n个字符,或者直到遇到delim字符为止。

这只是istream类的一部分,它还包含许多其他方法,例如对于不同数据类型的特殊化输入运算符,以及控制流格式和错误处理的方法。

STL

C++标准模板库(STL,Standard Template Library)提供了一套功能丰富的模板类和函数,这些模板类和函数主要分为四大部分:容器(containers)、算法(algorithms)、迭代器(iterators)和函数对象(functors)。

  1. 容器(Containers):容器是用来管理某一类对象的集合。STL提供了多种类型的容器,可以分为序列容器和关联容器两大类。

    • 序列容器:包括vectordequelistforward_list(C++11引入)、array(C++11引入)和string

    • 关联容器:包括setmultisetmapmultimap、以及它们的无序版本(C++11引入):unordered_setunordered_multisetunordered_mapunordered_multimap

    • 容器适配器:包括stackqueuepriority_queue

  2. 算法(Algorithms):STL提供了一大批算法,包括对序列进行操作的算法(如sortfindcountreplace等),以及对数值进行操作的算法(如accumulateinner_product等)。

  3. 迭代器(Iterators):迭代器提供了一种方法,使得程序员能够依次访问容器中的元素,而不需要关注容器的内部结构。根据功能的不同,迭代器可以分为五种类型:输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器。

  4. 函数对象(Functors):函数对象是行为类似函数的对象,其类定义了operator()。STL中提供了一些预定义的函数对象,如lessgreaterplusminus等,它们都定义在<functional>头文件中。

此外,STL还包括一些辅助功能:

  • 分配器(Allocators):用于控制容器中的内存分配。

  • 适配器(Adapters):包括容器适配器(如stackqueuepriority_queue)、迭代器适配器(如reverse_iteratormove_iterator)、函数适配器(如bindnegate)。

  • 类型特性(Type Traits):是一种模板编程技巧,用于在编译时推断类型的属性。例如,is_integral可以检查一个类型是否是整数类型。

以上是STL的主要组成部分,这些模板库共同构成了C++的强大功能,使其在许多场合下都可以进行高效、方便的编程。

迭代器

迭代器(Iterators)是一种在C++中用于遍历容器(如数组和链表)的元素的方法。迭代器的工作方式类似于指针:通过迭代器,你可以访问和修改其指向的元素。

迭代器提供了一种通用的接口,无论容器的实现方式如何,你都可以使用相同的方式来遍历和操作容器中的元素。这使得编写泛型代码更加简单和方便。

C++ STL提供了几种不同类型的迭代器,这些迭代器按照支持的操作可以分为五类:

  1. 输入迭代器(Input Iterators):输入迭代器可以用来从容器读取元素,但不能用来修改元素。输入迭代器只能一次向前移动。

  2. 输出迭代器(Output Iterators):输出迭代器可以用来向容器写入元素,但不能用来读取元素。输出迭代器只能一次向前移动。

  3. 前向迭代器(Forward Iterators):前向迭代器可以用来读取和写入元素。前向迭代器只能一次向前移动。

  4. 双向迭代器(Bidirectional Iterators):双向迭代器可以用来读取和写入元素。双向迭代器可以向前或向后移动。

  5. 随机访问迭代器(Random Access Iterators):随机访问迭代器可以用来读取和写入元素。随机访问迭代器可以进行任意的跳转

双向迭代器(Bidirectional Iterators)和随机访问迭代器(Random Access Iterators)都是C++ STL中的迭代器种类,它们都可以用于访问和修改容器中的元素。

共同功能:

  1. 正向迭代:双向迭代器和随机访问迭代器都可以用++操作符来向前移动,例如++iteriter++

  2. 反向迭代:双向迭代器和随机访问迭代器都可以用--操作符来向后移动,例如--iteriter--

  3. 解引用:双向迭代器和随机访问迭代器都可以用*操作符来访问当前元素,例如*iter

  4. 成员访问:双向迭代器和随机访问迭代器都可以用->操作符来访问当前元素的成员,例如iter->member

  5. 比较:双向迭代器和随机访问迭代器都可以用==!=操作符来比较两个迭代器是否相等。

不同功能:

  1. 跳跃迭代:只有随机访问迭代器支持跳跃迭代,也就是一次移动多个位置。你可以用+-操作符,或者用+=-=操作符来移动迭代器。例如,iter + 5iter - 5iter += 5iter -= 5

  2. 比较顺序:只有随机访问迭代器支持<><=>=操作符来比较两个迭代器的顺序。

  3. 随机访问:只有随机访问迭代器支持[]操作符来随机访问元素,例如iter[5]

双向迭代器主要用于可以前后移动的容器,如listsetmap等,而随机访问迭代器用于可以随机访问的容器,如vectordequearray等。

排序规则

对于大多数的容器(set,map) 和 算法 (sort) 来说, 默认排序规则是从小到大排序 而对于 priority_queue来说是从大到小排序, 他们所谓的排序规则是指 我们提供的函数指针和函数对象 执行后的返回值, 如果返回true 那么认为前一个元素比后一个元素小, 否则就认为前一个元素比后一个元素大, 所以对于大多数容器和算法来说 如果排序规则返回了 true ,就认为两个元素相对位置(这里的相对位置只有前后一说)不变, 否则相对位置就要改变, 但是对于 priority_queue 来说 如果排序规则返回了 true 就需要改变位置, 否则不变

容器

C++标准模板库(STL)提供了一系列容器,它们可以用来存储和操作数据。下面是一些主要的C++容器:

  1. 序列容器:存储元素的线性集合。

    • std::vector:动态数组,提供快速的随机访问,而且在末尾添加或删除元素也很快。但在中间位置插入或删除元素较慢。

    • std::deque:双端队列,提供快速随机访问,且在头部或尾部添加或删除元素都很快。但在中间位置插入或删除元素较慢。

    • std::list:双向链表,提供快速的插入和删除,但不支持随机访问。

    • std::forward_list:单向链表,只支持向前迭代,插入和删除速度快,但不支持随机访问。

    • std::array:固定大小的数组,提供快速随机访问,但大小在编译时需要确定,且不能改变。

  2. 容器适配器:使用其他容器进行封装,提供特定的接口。

    • std::stack:提供后入先出(LIFO)的数据访问模式。

    • std::queue:提供先入先出(FIFO)的数据访问模式。

    • std::priority_queue:队列中的每个元素都有一个优先级,优先级最高的元素先出队。

  3. 关联容器:使用关键字进行数据访问。

    • std::set:存储键的集合,每个键只出现一次,键自动排序。

    • std::multiset:存储键的集合,允许键有多个重复,键自动排序。

    • std::map:键值对集合,每个键只出现一次,键自动排序。

    • std::multimap:键值对集合,允许键有多个重复,键自动排序。

  4. 无序关联容器:使用哈希函数进行数据访问,不自动排序。

    • std::unordered_set:存储键的集合,每个键只出现一次,键不自动排序。

    • std::unordered_multiset:存储键的集合,允许键有多个重复,键不自动排序。

    • std::unordered_map:键值对集合,每个键只出现一次,键不自动排序。

    • std::unordered_multimap:键值对集合,允许键有多个重复,键不自动排序。

以上这些容器都在<container名字>头文件中定义,例如,std::vector<vector>头文件中定义,std::map<map>头文件中定义。

每种容器都有其特定的特性和使用场景,选择合适的容器可以优化性能和资源使用。

函数算法

C++ STL(Standard Template Library)为我们提供了大量的算法函数,它们都在 <algorithm>, 关于详细信息可以查看官网

c++11新特性

  • 统一初始化的方式
  • 成员变量默认初始值
  • auto关键字 自动类型推断
  • 类型推断符 decltype , 这个对于函数模板返回值很有用 配合上auto关键字
  • 智能指针
  • 基于范围的for循环
  • 右值引用 和 move
  • 无序容器(通过hash表实现) unordered_map
  • 正则表达式
  • Lambda表达式
  • 强制类型转化 static_cast interpret_cast const_cast dynamic_cast
  • 异常处理

强制类型转化

  1. static_cast:这是最通用的强制类型转换操作符。它可以用于基本数据类型之间的转换,如整数到浮点数,浮点数到整数,也可以用于枚举类型到整数,甚至可以用于将一个指针转换为另一个指针类型,或将一个成员函数指针转换为另一个成员函数指针。需要注意的是,虽然 static_cast 比 C 风格的类型转换更为安全,但仍可能造成数据丢失或不可预见的结果,因此必须谨慎使用。

  2. dynamic_cast:这个操作符主要应用在多态类型的安全转换上。它通常用于将基类的指针或引用转换为派生类的指针或引用,但前提条件是这个基类需要有虚函数。如果转换失败,如试图将指针或引用转换为不正确的类型,dynamic_cast 会返回 nullptr(对于指针)或者抛出一个 bad_cast 异常(对于引用)。使用 dynamic_cast 可以增强程序的安全性,但由于其需要在运行时进行类型检查,因此性能开销较大。

  3. reinterpret_cast这是最不安全的类型转换操作符。它可以转换任意类型的指针为其他类型的指针也可以转换任意类型的整数为指针(反之亦然)。因为 reinterpret_cast 可能产生无法预见的结果,所以除非你完全确定自己在做什么,否则应当避免使用 reinterpret_cast

  4. const_cast:这个操作符用于移除对象的 constvolatile 或者 const volatile 修饰。需要注意的是,使用 const_cast 去除 const 修饰并进行修改的对象必须本身是一个非 const 对象,否则结果是未定义的

这些强制类型转换操作符提供了强大的工具,但是也需要谨慎使用。一般来说,你应该尽量避免使用强制类型转换,尤其是在不确定的情况下。在许多情况下,正确的类型设计和良好的编程习惯可以避免强制类型转换的需要。

额外补充

g++编译器版本对应c++版本

要查看你的 g++ 编译器支持的 C++ 版本,你可以使用 g++ --version 命令来查看你当前的 g++ 版本。

然后你可以参考下面的对应关系,这些是 g++ 编译器与支持的 C++ 标准之间的对应关系:

  • G++ 4.8.1 及以上版本支持 C++11。
  • G++ 4.9.2 及以上版本支持 C++14。
  • G++ 5.2 及以上版本支持 C++17。
  • G++ 8.1 及以上版本支持 C++20。

请注意,这些仅是最低版本要求,新的编译器版本通常会包含对老版本 C++ 标准的支持以及新版本中的 bug 修复。

你还可以在编译时指定所需的 C++ 版本。例如,如果你想使用 C++14,可以使用 -std=c++14 参数:

1
g++ -std=c++14 your_file.cpp

另外,你也可以用 -std=c++11-std=c++17-std=c++20 等来指定其他 C++ 版本。

不同的版本的 C++ 有不同的功能,因此在编写代码时,了解你的编译器支持的 C++ 版本是非常重要的。

默认成员函数

在C++中,类的特殊成员函数包括默认构造函数、拷贝构造函数、移动构造函数、析构函数、拷贝赋值运算符和移动赋值运算符。这些特殊成员函数在特定的情况下会被隐式定义(即自动生成)或者在某些条件下被禁用。了解它们的行为和触发条件对于正确使用C++面向对象编程非常重要。

1. 默认构造函数

  • 触发条件:当没有为类定义任何构造函数时,编译器会为类自动生成一个默认构造函数。
  • 行为:自动生成的默认构造函数会尝试初始化类的所有成员变量。对于类类型成员,将调用其默认构造函数;对于内置类型成员(如intdouble等),如果在类内部没有进行初始化,则它们将不被初始化(其值是未定义的)。

2. 拷贝构造函数

  • 触发条件:当一个对象以值传递的方式被函数接受,或从函数返回时;或者当一个对象通过另一个同类型对象进行初始化时。
  • 行为:自动生成的拷贝构造函数会逐个拷贝非静态成员变量,使用其对应类型的拷贝构造函数。

3. 移动构造函数(C++11及之后)

  • 触发条件:当对象被初始化为右值(例如临时对象或显式转换为右值的对象)时。
  • 行为:自动生成的移动构造函数会逐个“移动”非静态成员变量,使用其对应类型的移动构造函数。移动通常意味着资源的所有权从源对象转移到目标对象,源对象被置于有效但不确定的状态。

4. 析构函数

  • 触发条件:对象生命周期结束时(例如,局部对象的函数返回时,或通过delete释放动态分配的对象时)。
  • 行为:自动生成的析构函数会逐个调用非静态成员变量的析构函数。

5. 拷贝赋值运算符

  • 触发条件:当一个对象被赋值为同类型的另一个对象时。
  • 行为:自动生成的拷贝赋值运算符会逐个拷贝赋值非静态成员变量,使用其对应类型的拷贝赋值运算符。

6. 移动赋值运算符(C++11及之后)

  • 触发条件:当一个对象被赋值为同类型的右值时。
  • 行为:自动生成的移动赋值运算符会逐个移动赋值非静态成员变量,使用其对应类型的移动赋值运算符。

规则与细节

  • 规则如果你声明了任何构造函数,编译器不会自动生成默认构造函数如果你声明了拷贝构造函数、移动构造函数、拷贝赋值运算符或移动赋值运算符中的任何一个,编译器不会自动生成拷贝构造函数和拷贝赋值运算符。C++11中,声明拷贝构造函数或拷贝赋值运算符会阻止移动构造函数和移动赋值运算符的自动生成。
  • 删除的函数:你可以通过将特殊成员函数标记为= delete来显式禁用它们,例如MyClass(const MyClass&) = delete;
  • 默认的函数:C++11允许你通过= default来要求编译器生成默认的特殊成员函数实现,即使已经声明了其他构造函数,例如MyClass() = default;

理解这些规则对于设计符合预期行为的类至关重要,尤其是在涉及资源管理(如动态内存分配、文件句柄、互斥锁等)时,正确使用或禁用这些特殊成员函数可以避免资源泄露、双重释放和其他错误。

文本模式和二进制模式的区别

本质上它两没有区别, 在打开文件的时候都是二进制读取, 虽然所有文件都是以二进制形式存储的,但我们如何解释这些数据取决于我们以什么模式打开文件。

在文本模式中,我们把数据看作是字符,由换行符、制表符等特殊字符组成的行。例如,如果你在文本模式下读取一个文件,当你在windows上读取到一个代表换行的字符序列\r\n的时候,C++将把它转换为\n, 那这样就少了一个字符,当你在文本模式下在windows中写入一个文件的时候,如果遇到一个\n, C++会把它转化为 \r\n 那这样岂不是多了一个字符, 所以在处理非文本的时候,一定要使用二进制模式

在二进制模式中,我们不试图解释数据,只是直接读取和写入。这对于非文本文件(如图像或声音文件)或者需要精确控制你读写的数据的应用来说非常有用。

命名空间

C++ 的命名空间是一个范围,它主要被用于组织代码以避免名称冲突。在一个大型的软件项目中,如果不使用命名空间,那么当项目越来越大,名称冲突的可能性就越来越高。命名空间可以帮助我们更好地组织代码,提高代码的可读性和可维护性。

下面是关于命名空间的一些主要特点:

  1. 命名空间定义:命名空间通过关键字 namespace 定义,后面跟着命名空间的名称。命名空间的主体部分是一对大括号,其中包含了命名空间的所有成员。

    1
    2
    3
    4
    namespace MyNamespace {
    int a;
    void func() { /* ... */ }
    }
  2. 访问命名空间中的成员:可以使用命名空间名称和作用域解析运算符 :: 来访问命名空间中的成员。

    1
    2
    MyNamespace::a = 10;
    MyNamespace::func();
  3. using 声明和 using 指示using 声明可以让我们不必在每次访问命名空间中的成员时都使用命名空间名。using 指示使得命名空间中的所有名称都可以直接访问。

    1
    using namespace MyNamespace;  // now we can directly use a and func()
  4. 命名空间可以嵌套:你可以在一个命名空间中定义另一个命名空间。

    1
    2
    3
    4
    5
    6
    7
    8
    namespace Outer {
    int x;
    namespace Inner {
    int y;
    }
    }
    Outer::x = 10; // access the member of Outer namespace
    Outer::Inner::y = 20; // access the member of Inner namespace
  5. 命名空间别名:你可以为命名空间定义别名,以便于在代码中更方便地使用。

    1
    2
    namespace NS = MyVeryLongNamespaceName;
    NS::func(); // same as MyVeryLongNamespaceName::func()
  6. 匿名命名空间:如果一个命名空间没有给出名称,它就是匿名命名空间。在同一个文件中,匿名命名空间中的所有成员都可以直接访问,就像它们是全局范围的一样。

    1
    2
    3
    namespace {
    int a; // has internal linkage, can be directly accessed within the same file
    }
  7. 全局命名空间:所有没有在明确的命名空间中声明的代码都在全局命名空间中。

请注意,C++ 标准库中的所有内容都在 std 命名空间中。在使用标准库的类或函数时,我们通常需要使用 std:: 作为前缀,或者

使用 using 指示引入整个 std 命名空间(但在大型项目中,出于避免命名冲突的考虑,通常不推荐这样做)。

命名空间是一种重要的代码组织和设计工具,正确使用命名空间可以帮助我们编写出更清晰、更易于维护的代码。