【Cpp】语言基础

Cpp 学习笔记,内容来自卡特网语言基础课OI WIKI

预处理命令

预处理命令是预处理器所接受的命令,用于对代码进行初步的文本替换。

#include

#include <cstdio>
#include <iostream>

int main() {
    return 0;
}

#include 是一个预处理命令,<> 里的文件称为头文件。#include <iostream> 即将头文件 iostream 中的内容原封不动地粘贴到 #include <iostream> 这条语句所在的位置。

Cpp 头文件和 C 头文件的区别是不带 .h,且 Cpp 为了兼容 C,直接使用 C 的头文件,只不过将 xx.h 改为了 cxx

#define

#define 是一预处理命令,用来定义宏,本质是文本替换。

#include <iostream>
#define N 3+2
#define sum(a, b) (a + b) //宏可以带参数

int main() {
    std::cout << 2*N << std::endl; //输出8
    std::cout << 2*sum(1,2); //输出6
    return 0;
}

#define 是有风险的,可能导致文本被意外替换。较为推荐的做法是使用 const 限定符声明常量,使用函数代替带参宏。

#define 结合 #ifdef 等预处理指令:

#ifdef LINUX
//code for Linux
#else
//code for other os
#endif

可以在编译的时候通过 -DLINUX 来控制编译出的代码,而无需修改源文件。通过 -DLINUX 编译出的可执行文件里并没有其他 os 的代码,那些代码在预处理的时候已经被删除了。

注释

  1. 行内注释:
//行内注释
  1. 注释块:
/*
 * 注释块
 * 注释块
 */

输入和输出

cin 与 cout

#include <iostream> //cin、cout所在头文件

using namespace std;
int main() {
    char a, b;
    cin >> a >> b; //读入 a 和 b
    cout << a << endl <<b; //输出 a,换行,再输出 b
}

scanf 与 printf

scanf 与 printf 是 C 提供的函数,大多数情况下,它们的速度比 cin 和 cout 更快,并且能方便地控制输入输出格式。

#include <cstdio>

int main() {
    int a, b;
    scanf("%d%d", &a, &b);
    printf("a=%d\nb=%d", a, b);
    return 0;
}

基本数据类型

  • 基础类型
    • void 无类型
    • std::nullptr_t 空指针类型
    • int 整数类型
    • bool 布尔类型 true/false
    • char 字符类型
    • floatdouble 浮点类型
  • 复合类型

void

函数无返回值时,声明为 void 类型。

不能将一个变量声明为 void 类型。

整型

对于 int 关键字,可用如下修饰关键字进行修饰:
(1) 符号性:

  • signed 带符号
  • unsigned 无符号
    (2) 大小:
  • short
  • long
  • long long

Cpp 标准保证:

1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)

由于历史原因,整型的位宽有多种标准:

类型 常见位宽
char 8
short 16
int 32
long 32(64常见于6位的 Linux、macOS)
long long 64

为解决这一问题,C99/C++11引入了定宽整数类型。

定宽整数类型

定宽整数类型本质上是普通整数类型的类型别名。

<cstdint> 提供了若干定宽整数的类型和各定宽整数类型最大值、最小值等的宏常量。

定宽整数有如下几种:

  • intN_t:宽度恰好为 N 位的带符号整型,如 int32_t
  • int_fastN_t:宽度至少有 N 位的最快的带符号整型。
  • int_leastN_t:宽度至少有 N 位的最小的带符号整型。

无符号版本只需在有符号版本前加一个 u 即可,如 uint32_t

INT32_MAXINT32_MIN 分别为 int32_t 的最大值和最小值。

注意:混用定宽整数类型和普通整数类型可能会影响跨平台编译,例如:

#include <cstdint>
#include <iostream>

int main() {
    long long a;
    int64_t b;
    std::cin >> a >> b;
    std::cout << std::max(a, b) << std::endl;
    return 0;
}

int64_t 在64位 Windows 下一般为 long long int, 而在64位 Linux 下一般为 long int, 所以这段代码在使用64位 Linux 下的 GCC 时不能通过编译,而使用64位 Windows 下的 MSVC 时可以通过编译,因为 std::max 要求输入的两个参数类型必须相同。

字符类型

字符类型底层存储方式仍然是整数,一般通过 ASCII 码实现字符与整数的一一对应,

浮点类型

浮点类型有以下三种:

  • float:单精度浮点类型 32bit
  • double:双精度浮点类型 64bit
  • long double:扩展精度浮点类型 128bit

由于 float 类型表示范围较小,且精度不高,实际应用中常使用 double 类型

变量与常量

int g = 1; //全局变量

int main() {
    int a = 2; //局部变量
    const int b = 3; //常量
    return 0;
}
  • 全局变量的作用域是自其定义之处起至文件结束位置为止。
  • 局部变量的作用域是自其定义之处起至代码块结束位置为止。
  • 常量是固定值,在定义后不能被修改,

运算

算术运算符

+、-、*、/、%

算术运算中的类型转换

对于双目算术运算符,当参加运算的两个变量类型不同时,会发生类型转换,使两个变量的类型一致。如对于一个 int 型变量 x 和一个 double 型变量 y:x/y 的结果是 double 型。

自增自减运算符

int i = 10;
int j1 = i++; //先 j1 = i, 再 i = i + 1 
int j2 = ++i; //先 i = i + 1, 再 j1 = i
// i--、--i同理

位运算符

~、&、|、^、<<、>>

位运算的优先级低于算术运算符(除了取反);按位与、或及异或低于比较运算符。

条件运算符

a ? b :c

如果 a 为真,则表达式值为 b,否则为 c

比较运算符

==、!=、>=、<=、>、<

逻辑运算符

&&、||、!

复合赋值运算符

op += 1 //等价于 op = op + 1

逗号运算符

逗号运算符可将多个表达式分隔开来,被分隔开的表达式按从左至右的顺序依次计算,整个表达式的值是最后的表达式的值。逗号表达式的优先级在所有运算符中的优先级是最低的。

ret = 1 + 2, 3 + 4, 5 + 6; //ret=3
ret = (1 + 2, 3 + 4, 5 + 6); //ret=11

成员访问运算符

运算符 功能
[] 数组下标
. 对象成员
& 取地址/获取引用
* 间接寻址/解引用
-> 指针成员
.* 指向对象的成员的指针
->* 指向指针的成员的指针

流程控制语句

分支

if-else

if (条件1) {
    主体1;
} else if (条件2) {
    主体2;
} else {
    主体3;
}

switch

switch (选择句) { //选择句必须是一个整型表达式
    case 标签1: { //标签必须是整型常量
        主体1;
        break; //break 语句进行中断,否则将会往下执行
    }
    case 标签2: {
        主体2;
        break;
    }
    defaulf: {
        主体3;
    }
}

循环

for

for (初始化; 判断条件; 更新) {
    循环体;
}

while

while (判断条件) {
    循环体;
}

do...while

do {
    循环体;
} whilie (判断条件);

与 while 语句的区别在于,do...while 语句是先执行循环体再进行判断的。

break 与 continue

break:退出循环,终止离它最近的whilefor语句;
continue:跳过循环体的余下部分,然后继续下一次迭代。
一般来说,break 与 continue 语句用于让代码的逻辑更加清晰,例如:

// 逻辑较为不清晰,大括号层次复杂

for (int i = 1; i <= n; ++i) {
  if (i != x) {
    for (int j = 1; j <= n; ++j) {
      if (j != x) {
        // do something...
      }
    }
  }
}

// 逻辑更加清晰,大括号层次简单明了

for (int i = 1; i <= n; ++i) {
  if (i == x) continue;
  for (int j = 1; j <= n; ++j) {
    if (j == x) continue;
    // do something...
  }
}
// for 语句判断条件复杂,没有体现「枚举」的本质

for (int i = l; i <= r && i % 10 != 0; ++i) {
  // do something...
}

// for 语句用于枚举,break 用于「到何时为止」

for (int i = l; i <= r; ++i) {
  if (i % 10 == 0) break;
  // do something...
}
// 语句重复,顺序不自然

statement1;
while (statement3) {
  statement2;
  statement1;
}

// 没有重复语句,顺序自然

while (1) {
  statement1;
  if (!statement3) break;
  statement2;
}

函数

  1. 函数声明:
返回值类型 函数名 (形参列表);
  1. 函数定义:
返回值类型 函数名 (形参列表) {
    函数体;
}
  1. 函数调用:函数需先声明,才能使用。
    举例:
#include <iostream>

int add(int, int); //函数声明

void say() { //函数声明时就完成定义
    std::cout << "hello" << std::endl;
}

int main() {
    say(); //函数调用
    int ret = add(1, 2); //函数调用
    std::cout << "ret=" << ret;
    return 0;
}

int add(int x, int y) { //函数定义
    return x + y;
}

注意:

  • 函数是值传递。
  • 每个 C/Cpp程序都要有一个 main 主函数,任何程序都从 main 函数开始运行。

数组

数组是存储相同类型对象的容器。数组的大小是固定的,数组中存放的对象没有名字,要通过下标访问。

定义数组

//arr[n] n为整型常量表达式
int n1 = 10;
const int n2 = 10;
int arr1[n1]; //错误:n1不是常量表达式
int arr2[n2];

访问数组元素

通过 [] 小标运算符访问数组元素,索引从 0 开始。

int arr[3] = {1, 2, 3}; // {} 初始化数组
cout << arr[1];

多维数组

多维数组的实质是“数组的数组”。

int arr[3][4];//一个长度为3的数组,它的元素是长度为4的整型数组
arr[1][3] = 15; //访问二维数组

拓展:如果数组过大,应定义为全局变量,防止爆栈。

结构体

Cpp 中的 struct 不同于 C 中的 struct,cpp 的 struct 被扩展为类似 class 的类说明符。

结构体是一系列成员元素的组合体,允许存储不同类型的数据项,成员变量可以是各种数据类型,包括整数、浮点数、字符串、其他结构体等,所以你可以根据需要定义自己的结构体来组织数据。

定义结构体

struct MyStruct { //定义结构体
    int val1;
    char val2;
    MyStruct *p; //定义结构体指针
};

const MyStruct a; //定义结构体常量
MyStruct b, arr[n]; //定义结构体变量、结构体数组
MyStruct *c; //定义结构体指针 

访问成员

访问成员的几种方式:

  • 结构体变量名.成员名
  • 结构体指针名->成员名
  • (*结构体指针名).成员名

拓展:为了提高访存效率,编译器在处理结构体中成员的存储位置时会进行结构体内存对齐。

联合体

联合体是特殊的类类型,它在一个时刻只能保有其一个非静态数据成员。

union MyUnion {
    int x;
    long long y;
};

联合体所占用的内存空间不小于其最大的成员大小,所有成员共用内存空间和地址。当一个成员被赋值时,由于内存共享,该联合体的其他成员都会被覆盖。

指针

指针变量是存放地址所用的变量类型,有时也简称指针。

指针的声明与使用

指针的声明:

类型 *指针变量名
  • & 取地址运算符可以获取一个变量的地址。
  • * 对指针变量进行解引用,访问指针所指向的空间。
    例如:
int i = 1;  //i: 1
int *p = &i; 
*p = 2; //i: 2

在使用结构体指针变量访问成员时,有一种更简便的写法即 -> 运算符。

struct MyStruct {
    char c;
    int i;
};

int main() {
    MyStruct myStruct{'a', 10};
    MyStruct *p = &myStruct;
    (*p).c = 'b'; //myStruct: {'b', 10}
    p->i = 20; //myStruct: {'b', 20}
    return 0;
}

空指针

空指针用于表示指针不指向任何有效的内存地址。

在 C++11 之前,C++ 和 C 一样使用 NULL 宏表示空指针常量,C++ 中 NULL 的实现一般如下:

#define NULL 0;
//或者是 #define NULL 0L;

空指针和整数 0 的混用在 C++ 中会导致许多问题,比如:

int f(int x);
int f(int* p);

在调用 f(NULL); 时,显然我们是想调用第二个函数,但实现不了,NULL是一个宏,归根结底不是一个指针,有两种结果:

  • 如果 NULL 被定义为0,那么编译器会调用第一个函数
  • 如果 NULL 被定义为0L或者其他形式的整型零,编译会报错,因为 0L 可以被等同地转化为空指针或者 int,故产生了二义性。

为解决这个问题,C++11 引入了 nullptr 关键字作为空指针常量。
nullptr 的类型是 std::nullptr_t,可以隐式转换为任何指针类型。

指针的使用

指针的偏移

指针变量也可以进行加减操作,以访问内存中的不同位置。例如,int 型指针,每次加1,其指向的地址向后偏移4字节;每次减1,其指向的地址向前偏移4字节。
使用指针偏移访问数组:

int main() {
    int a[3] = {1, 2, 3};
    int *p = a;  //p 指向 a[0]
    *p = 4; //a: [4, 2, 3] ; 等价于 p[0] = 4;
    p += 1; //p 指向 a[1]
    *p = 5; //a: [4, 5, 3]
    p++; //p 指向 a[2]
    *p = 6; //a: [4, 5, 6]
}

数组名可以隐式地退化成首个元素的指针

指针作为参数

这是最常用的方法,使用指针使得可以在别的作用域操作数据。

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = t;
}

动态实例化

使用 mallocnew 动态申请内存空间需要用到指针变量接收返回所申请的空间的地址。

class A {
public:
    A(int a_) : a(a_) {}
private:
    int a;
};

struct TwoInt {
    int a;
    int b;
};

int main() {
    int *p_int = new int(123);
    int *p_arr = new int[5];
    A *p_a = new A(123);
    TwoInt *p_two_int = new TwoInt(1, 2);
    delete p_int;
    delete p_arr;
    delete p_a;
    delete p_two_int;
}

引用

引用实际上是为变量起别名,对引用的一切操作都相当于对原对象操作。引用可以看成是 Cpp 封装的指针。

引用必须在声明时进行初始化,并且一旦初始化就不能改变引用的目标,用 & 声明引用:

int a = 10;
int &ref_a = a;

引用常常作为形参来使用,以便在函数内修改实参。

#include <iostream>
using namespace std;

void changeVal(int &a) {
    a = 100;
}

int main() {
    int a = 10;
    cout << a << endl;
    changeVal(a);
    cout << a << endl;
}

函数传递参数是值传递,会创建实参的副本给形参,使用引用参数可以避免参数拷贝,特别是对大型数据结构(如数组),能显著减少开销。

引用不是对象,因此不存在引用的数组、无法获取引用的指针、也不存在引用的引用。

const

被常量修饰符 const 修饰的对象或者类型都是只读量,只读量的内存存储与一般变量无任何区别,但是编译器会在编译期进行冲突检查,避免对只读量的修改。

  1. 在类型的名字前增加 const 会将该类型的变量标记为不可变的,具体使用情况有常量和常引用(指针)两种。
//这里的常量即常变量,指的是类型被 const 修饰的变量。
//常量在声明后便不可重新赋值,也不可访问其可变成员,只能访问其常成员
const int a = 0;
//a = 1; //报错
//-----------------------------------------
//常引用、常指针
int a = 0;
const int b = 0;

int *p1 = &a;
*p1 = 1;
const int *p2 = &a;
// *p2 = 2; //报错,不能通过常指针修改变量
// int *p3 = &b; //报错,不能用普通指针指向 const 变量
const int *p4 = &b;

int &r1 = a;
r1 = 1;
const int &r2 = a;
// r2 = 2; //报错,不能通过常引用修改变量
// int &r3 = b; //报错,不能用普通引用指向 const 变量
const int &r4 = b;
  1. 区分 指向对象的常指针指向常对象的指针
struct MyStruct {
    int val;
    string str;
    MyStruct(int val_, string str_) : val(val_), str(str_){}
};

int main() {
    MyStruct myStruct1(1, "hhhh");
    MyStruct myStruct2(2, "zzzz");
    // 指向常对象的指针不能改变对象的成员内容
    const MyStruct *p1 = &myStruct1;
//    (*p1).val = 2; //编译出错
    p1 = &myStruct2;

    //指向对象的常指针不能改变指针指向,但能改变指向对象的值
    MyStruct *const p2 = &myStruct1;
    (*p2).val = 2;
//    p2 = &myStruct2; //报错
    return 0;
}
  1. 在函数参数里限定参数为常类型可以避免在函数里意外修改参数,通常用于引用参数。此外,在类型参数中添加 const 修饰符还能增加代码可读性,能区分输入和输出参数。
//输入参数 data,输出参数 total,使用引用参数而不通过函数返回值可以避免参数拷贝
void sum(const std::vector<int> &data, int &total) {
  for (auto iter = data.begin(); iter != data.end(); ++iter)
    total += *iter;  // iter 是 const 迭代器,解引用后的类型是 const int
}
  1. 常成员函数
struct MyStruct {
    int val;
    string str;
    MyStruct(int val_, string str_) : val(val_), str(str_){}
    
    void func() {} //普通成员函数
    void constFunc1() const {} //常成员函数
    void constFunc2(int val_) const {
//        func(); // 报错,常成员函数不能调用普通成员函数
        constFunc1(); //可以调用常成员函数
//        val = val_; //常成员函数不能改变成员变量 
    }
};

注意:常对象只能调用常成员函数。

类和面向对象

Cpp 是一种多范式编程语言,支持过程编程和面向对象编程,它将数据和操作数据的方法组织为类和对象。
面向对象有三大特性:封装、继承、多态。

类 class

类是结构体的扩展,不仅有成员元素,还有成员函数。对象是类的实例,也就是变量。

  1. 定义类
class 类名 {
访问修饰符:
    //成员变量,表示类的属性
    //成员方法,表示类的行为

}; //分号结束一个类

访问修饰符:

  • private: 私有,只能在定义该成员的类的内部访问。
  • public: 公有,在类的内部、派生类(子类)的内部和类的对象外部均可访问。
  • protected: 受保护,只能在定义该成员的类的内部以及派生类的内部进行访问。
    默认是 private
  1. Cpp 是支持重载的。

举例:

class MyClass {
public:
    //成员变量
    int attr;

    //成员方法
    void myMethod() { //声明并实现
        //方法实现
    }
    void myMethod(int); //先声明
};

void MyClass::myMethod(int attr_) { //再定义
    attr = attr_;
}

int main() {
    //创建对象
    MyClass obj;

    //访问属性
    obj.attr = 1;

    //调用方法
    obj.myMethod();

    return 0;
}

构造函数

如果没有定义任何构造函数,C++编译器会自动创建一个默认构造函数。
如果已经定义了一个构造函数,编译器不会自动创建默认构造函数,只能显式调用该构造函数。

class MyClass {
public:
    //默认构造函数
    MyClass() {
        val1 = 0;
        val2 = " ";
    }
    //带参构造函数
    MyClass(int _val1, string _val2) {
        val1 = _val1;
        val2 = _val2;
    }
    //构成函数的成员初始化列表写法
    //: val1(_val1), val2(_val2) 表示 val1 = _val1; val2 = _val2;
    MyClass(int _val1, string _val2) : val1(_val1), val2(_val2) {

    }

private:
    int val1;
    string val2;

};

int main() {
    //使用默认构造函数创建对象
    MyClass obj1;

    //使用带参构造函数创建对象
    MyClass obj2(10, "abc");

    return 0;
}

成员初始化列表

Cpp中,实例化时调用构造函数,对象将在构造函数函数体中的代码被执行之前被创建,因此,调用构造函数将导致程序先给其成员变量分配内存,然后进入构造函数函数体中使用常规的赋值方式将值存储到内存中。

成员初始化列表只能在构造函数中使用,允许在进入构造函数主体之前对类成员进行初始化,以冒号开头,后跟一系列以逗号分隔的初始化字段:

必须使用成员初始化化列表的情况:

  • const 类成员或者声明为引用的类成员,因为 const 对象或引用类型只能初始化,不能对他们赋值。
class MyClass {
public:
    //构成函数的成员初始化列表写法
    //: val1(_val1), val2(_val2) 表示 val1 = _val1; val2 = _val2;
    MyClass(int _val1, string _val2) : val1(_val1), val2(_val2) {}

private:
    const int val1;
    string &val2;

};
 
MyClass obj(10, "abc");
  • 成员类型是没有默认构造函数的类。
  • 继承的基类没有默认构造函数。

推荐使用初始化列表,它会比在函数体内初始化类成员更快,因为在分配内存后,在函数体内又多进行了一次赋值操作。

初始化列表并不能指定初始化的顺序,正确的顺序是,首先初始化基类,其次根据类成员声明次序依次初始化。

拷贝构造函数

拷贝构造函数是一种特殊的函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。

如果在类中没有定义拷贝构造函数,编译器会自行定义一个,此时进行的赋值是对应元素间的浅拷贝。如果类带有指针变量成员,并有动态内存分配,则它必须有一个拷贝构造函数,否则两个对象的成员指针具有相同的地址。

拷贝构造函数的最常见形式如下:

#include <iostream>

class MyClass {
public:
    MyClass(int p_val) : val(0) {
        p = new int;
        *p = p_val;
    }
    //拷贝构造函数
    MyClass(const MyClass &obj) {
        val = obj.val;
        p = new int;
        *p = *obj.p; //拷贝值
    }
    int getP() {
        return *p;
    }
private:
    int val;
    int *p;
};
 
void print(MyClass obj) { //用了拷贝构造函数
    std::cout << obj.getP();
}

int main() {
    MyClass obj(10);
    print(obj);
    MyClass obj2 = obj; //用了拷贝构造函数
    return 0;
}

析构函数

析构函数是在对象被销毁时被调用的。特别是当成员中有指针且指针指向一块动态申请的内存空间时,必须在析构函数中手动释放该动态空间,因为指针在被销毁时不会自动释放所指向的内存。

class MyClass {
public:
    MyClass() : val1(0){ }
    //析构函数
    ~MyClass() {
        delete p;
    }

private:
    int val;
    int *p;
};

封装

封装是为了保证数据的安全性。通过封装可以隐藏对象中一些不希望被外部所访问到的属性或方法。

class MyClass {
//公有属性和方法
public:
    // setXX 方法设置属性
    void setAttr(int attt) {
        //其他处理
        myAttr = attr;
    }

    // getXX 方法获取属性
    int getAttr() {
        //其他处理
        return myAttr;
    }

//私有属性和方法
private:
    int myAttr;
};

继承

继承使得一个类可以获取到其他类中的属性和方法。

在C++中,当创建一个对象时,编译器要保证调用了所有子对象的构造函数,这是C++强制要求的,也是它的一个机制。所以使用成员初始化列表显式地调用基类的构造函数。

class Shape {
protected:
    string type; //形状类型

public:
    //const string& shapeType 表示对 stringl 类型常量引用,
    //能进行传递字符串参数,但是不能在函数中修改这个参数的值。
    Shape(const string& shapeType) : type(shapeType) {}

    //使用 const 修饰表示该函数不会修改对象的状态,能保证对对象的访问是安全的。
    //求面积
    double getArea() const {
        return 0.0;
    }

    //获取形状类型
    string getType() const {
        return type;
    }
};

//Circle 类继承自 Shape 类
class Circle : public Shape {
public:
    //构造函数,调用 Shape 的构造函数,初始化 type 为 Circle
    Circle(int r) : Shape("Circle"), radius(r) {}

    //重写基类的方法
    double getArea() const override {
        return 3.14 * radius * radius;
    }

    int getRadius() const {
        return radius;
    }

private:
    int radius; //圆的半径
};

多态

多态常常和继承紧密联系在一起,它允许不同的对象使用相同的接口进行操作,但在运行时表现出不同的行为。多态性使得可以使用基类类型的指针或引用来引用派生类的对象,从而在运行时选择调用相应的派生类方法。

#include <iostream>
#include <vector>

//抽象类 Shape
class Shape {
public:
    //使用 virtual 关键字定义了一个虚函数,
    // =0 表示这是一个纯虚函数,即定义的函数在基类中没有实现,但是要求它的派生类都必须提供实现
    // 虚函数使得 Shape 类成为一个抽象基类,不能被实例化,只能被用作派生其他类的基类。
    virtual double getArea() const = 0;
};

// Ciclie 类继承 Shape 类
class Circle : public Shape {
public:
    Circle(int r) : radius(r) {}
    //重写 getArea()
    double getArea() const override {
        return 3.14 * radius * radius;
    }

private:
    int radius; //圆的半径
};

// Rectangle 类继承 Shape 类
class Rectangle : public Shape {
public:
    Rectangle(int w, int h) : width(w), height(h) {}
    //重写 getArea()
    //为了确保结果是一个浮点数,使用 static_cast<double> 显式转换为 double 类型
    double getArea() const override {
        return static_cast<double>(width * height);
    }

private:
    int width, height;
};

int main() {
    std::vector<Shape*> shapes;

    shapes.push_back(new Circle(3));
    shapes.push_back(new Rectangle(4, 5));

    for (const Shape* shape : shapes) {
        std::cout << "Area: " << shape->getArea() << std::endl;
    }
    return 0;
}

重载运算符

重载运算符是重载函数的特殊情况,通过对运算符的重新定义,使得其支持特定数据类型的运算操作。
Cpp 自带的运算符自定义了一些基本类型的运算规则,当需要在自定义的数据类型上使用这些运算符时就需要定义运算符在这些特定类型上的运算方式。

  1. 重载运算符存在如下限制:
  • 只能对现有运算符进行重载
  • 不能被重载的运算符:::..*?:
  • 重载后的运算符优先级、运算操作数、结合方向不变
  • &&|| 的重载失去短路求值特性
  1. 举例:
// 当用户的类重载了函数调用运算符,它就成为了函数对象类型
// 此类型的对象表示一个变量的线性函数 a * x + b
struct Linear {
    double a, b;

    double operator() (double x) const {
        return a * x + b;
    }
};

int main() {
    Linear f{2, 1}; //表示函数 2x+1
    Linear g{-1, 0}; //表示函数 -x
    //f 和 g 是能像函数一样使用的对象
    double f_0 = f(0);
    double f_1 = f(1);
    double g_0 = g(0);
}

STL

STL 即 Standard Template Library,标准模板库。STL 包含了一些模板化的通用的数据结构和算法。由于其模板化的特点,能够兼容自定义的数据类型,避免大量的造轮子工作。STL 主要包含 STL 容器和 STL 算法。

iterator

在 STL 中,迭代器 iterator 提供了一种类似指针的接口,可以用来遍历访问容器(如数组、集合等)中的元素,并执行各种操作。

  1. 迭代器主要支持两个运算符:自增和解引用,自增用来移动迭代器,解引用可以访问它指向的元素。
  2. 指向某个 STL 容器中元素的迭代器的类型一般为 container::iterator
  3. STL 容器拥有 begin()end() 两个成员函数,表示指向第一个元素和最后一个元素的下一个元素的迭代器(尾后迭代器),如果容器为空,则 begin()end() 返回的是同一个迭代器。

举例,遍历 vector :

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> myVector = {1, 2, 3, 4, 5};

    //使用迭代器遍历容器
    for (vector<int>::iterator it = myVector.begin(); it != myVector.end(); ++it) {
        cout << *it << " ";
    }
    return 0;
}

STL 中与 iterator 有关的函数:

  • std::advance(it, n):将迭代器 it 向后移动 n 步;如果 n 为负数,则向前移动。
  • std::next(it):获得向前迭代器 it 的后继。
  • std::next(it, n):获得向前迭代器 it 的第 n 个后继。
  • std::prev(it):获得双向迭代器 it 的前驱。
  • std::prev(it, n):获得双向迭代器 it 的第 n 个前驱。
  • std::begin(container) :获得指向容器第一个元素的迭代器。
  • std::rbegin(container):获得指向容器最后一个元素的反向迭代器
  • std::end(container):获得指向容器尾端的迭代器,尾端的前驱是容器里的最后一个元素。
  • std::rend(container): 获得指向容器前端的反向迭代器,前端的后驱是容器里的第一个元素。

STL 容器

STL 容器都是模板类,分类:

  • 序列式容器
    • vector,向量:后端可高效增加元素的顺序表。
    • array,数组:定长的顺序表。
    • deque,双端队列:双端都可高效增加元素的顺序表。
    • list,列表:可以沿双向遍历的链表。
    • forward_list,单向列表:只能沿一个方向遍历的链表。
  • 关联式容器
    • set,集合:有序地存储互异元素的容器。其实现是由节点组成的红黑树,每个节点都包含着一个元素,节点之间以某种比较元素大小的谓词进行排列。
    • multiset,多重集合:有序地存储元素的容器,允许存在相等的元素。
    • map,映射:由{键,值}对组成的集合,以某种比较键大小关系的谓词进行排列。
    • multimap,多重映射:由{键,值}对组成的多重集合,亦即允许键有相等情况的映射。
  • 无序(关联式)容器
    • unordered_set/unordered_multiset,无序(多重)集合:与 set/multiset 的区别在于元素无序,只关心元素是否存在。
    • unordered_map/unordered_multimap,无序(多重)映射:与 map/multimap 的区别在于键 (key) 无序,只关心 "键与值的对应关系"。

谓词返回值为真或假的函数。STL 容器经常会用到谓词,作为模板参数。

容器共有的一些函数:

  • =:重载了赋值操作符。
  • begin():返回指向容器第一个元素的迭代器。
  • end():返回指向末尾元素的后继的迭代器,它不指向某个元素。
  • size():返回容器内的元素个数。
  • max_size():返回容器理论上能存储的最大元素个数,依容器类型和所存储变量的类型而变。
  • empty():返回容器是否为空。
  • swap():交换两个容器。
  • clear():清空容器。
  • ==!=<><=>=:按字典序比较两容器的大小。无序容器不支持 <><=>=

vector

vector 是 STL 提供的内存连续的、可变长度的数组(亦称列表)数据结构,能够提供线性复杂度的插入和删除,以及常数复杂度的随机访问。

vector 的优秀特性:

  • 支持动态扩容。
  • 重写了比较运算符及赋值运算符,方便判断两容器是否相等和数组拷贝。
  1. vector 的创建:
#include <vector>
using std::vector;

//vector<类型> 名称,无需指明长度,常数复杂度
vector<int> myVector;

//创建并初始化
vector<int> myVector = {1, 2, 3, 4, 5};

//创建一个初始空间为10的容器,值默认初始化为0,线性复杂度
vector<int> myVector(10);

//创建一个初始空间为10的容器,每个元素的值都是-1
vector<int> myVector(10, -1);

//创建一个 myVector 的拷贝 copyV,线性复杂度
vector<int> copyV(myVector);

//创建一个 myVector 的拷贝 copyV,其内容是 {myVector[1],myVector[2]},线性复杂度
vector<int> copyV(myVector.begin() + 1, myVector.begin() + 3);

//移动 myVector 到新创建的 moveV,不发生拷贝,常数复杂度
vector<int> moveV(std::move(myVector)); 
  1. 访问 vector 内元素:
//用下标操作符 `[]`,返回引用,不执行越界检查
myVector[0]; //获取 vector 第一个元素的值

// at(pos) 返回容器中下标为 pos 的引用,执行越界检查
myVector.at(2);

// 返回首元素的引用、
myVector.front();

//返回末尾元素的引用
myVector.back();

//返回指向数组第一个元素的指针
myVector.data();
  1. vector 提供的迭代器
//返回指向首元素的迭代器
myVector.begin();
myVector.cbegin(); //带 c 为只读迭代器,不能通过只读迭代器改变元素值

//返回指向数组尾端占位符的迭代器,没有元素
myVector.end();
myVector.cend();

//返回指向逆向数组的首元素的逆向迭代器,对应正向容器的末元素
myVector.rbegin();
myVector.crbegin();

//返回指向逆向数组末元素后一位置的迭代器,对应正向容器首元素的前一个位置,没有元素
myVector.rend();
myVector.crend();

如果一个 Vector 本身就是只读的,那么它的一般迭代器和只读迭代器完全等价。

  1. vector 常用的操作:
vector<int> myVector = {1, 2, 3, 4, 5};

//向容器的最末端添加数字6,平均复杂度为常数,最坏为线性复杂度
myVector.push_back(6); 

//支持在某个迭代器位置插入元素,可以插入多个。
//复杂度与 pos 距离末尾长度成线性
myVector.insert(myVector.begin()+2, 8);
myVector.insert(myVector.begin()+3,{9, 10, 11});

//删除 vector 末尾的元素,常数复杂度
myVector.pop_back();

//删除某个迭代器或者区间的元素,返回最后被删除的迭代器,复杂度同 insert
myVector.erase(myVector.begin()+2);
myVector.erase(myVector.begin()+2, myVector.end()-3);

//清空 vector 中的所有元素
myVector.clear();

//判断 vector 是否为空,如果是返回 true,否则返回 false
myVector.empty(); //即 myVector.begin() == myVector.end()

//获取 vector 的长度,即有效元素数量
myVector.size();

//获取 vector 的容量,即实际分配的内存长度
myVector.capacity();

//改变 vector 的长度
myVector.resize(3);

//预留10内存空间,避免不必要的拷贝
myVector.reserve(10);

//使 vector 的长度和容量一致
myVector.shrink_to_fit();

//交换两容器
swap(myVector, otherVector);

vector 的实现细节:
vector 的底层其实仍然是定长数组,它能够实现动态扩容的原因是增加了避免数量溢出的操作。首先需要指明的是 vector 中元素的数量(长度)n 与它已分配内存最多能包含元素的数量(容量)N 是不一致的,vector 会分开存储这两个量。当向 vector 中添加元素时,如发现 n>N,那么容器会分配一个尺寸为 2N 的数组,然后将旧数据从原本的位置拷贝到新的数组中,再将原来的内存释放。尽管这个操作的渐进复杂度是 O(n),但是可以证明其均摊复杂度为 O(1)。而在末尾删除元素和访问元素则都仍然是 O(1) 的开销。 因此,只要对 vector 的尺寸估计得当并善用 resize() 和 reserve(),就能使得 vector 的效率与定长数组不会有太大差距。

array

std::array 是 STL 提供的内存连续且固定长度的数组数据结构,是对原生数组的直接封装。例如通过 at() 访问元素支持越界检查。

array 相比 vector 牺牲了动态扩容的特性,但是换来了与原生数组几乎一致的性能。因此如果能使用 C++11 特性的情况下,能够使用原生数组的地方几乎都可以直接把定长数组都换成 array,而动态分配的数组可以替换为 vector。

#include <array>
using std::array;

//创建空array,长度为3,常数复杂度
array<int, 3> arr;

//创建 array 并初始化,常数复杂度
array<int, 3> arr{1, 2, 3};

//填充数组
arr.fill(1);

//访问数组
arr.at(2);
arr[2];
arr.front();
arr.back();
arr.data();

//返回有效元素个数
arr.size();

//检查 arr 是否为空
arr.empty();

还有一些容器的共有操作不再赘述。

注意:使用 swap() 交换两 array 是 O(size) 数量级,而非与常规 STL 容器一样为 O(1)。

deque

std::deque是 STL 提供的双端队列数据结构,能够提供线性复杂度的插入和删除,以及常数复杂度的随机访问。

创建双端队列:

#include <deque>
using std::deque;

//定义一个int类型的空双端队列
deque<int> myDeque;
//定义一个初始大小为10的双端队列,线性复杂度
deque<int> myDeque(10);
//定义一个初始大小为10,值全为1的双端队列,线性复杂度
deque<int> myDeque(10, 1);
//复制已有的双端队列,线性复杂度
deque<int> copyD(myDeque);
//指定拷贝内容范围,线性复杂度
deque<int> copyD(myDeque.begin(), myDeque.begin()+3);
//移动 myDeque 到新创建的双端队列,不发生拷贝,常数复杂度
deque<int> moveD(std::move(myDeque));

deque 的操作

// 在头部插入一个元素,常数复杂度
myDeque.push_front(3);

//删除头部元素,常数复杂度
myDeque.push_front();

deque 别的操作同 vector,不再赘述。
其中 insert()erase() 操作的复杂度为 pos 与两端距离较小者成线性。

deque 的实现细节:
deque 通常的底层实现是多个不连续的缓冲区,而缓冲区中的内存是连续的。而每个缓冲区还会记录首指针和尾指针,用来标记有效数据的区间。当一个缓冲区填满之后便会在之前或者之后分配新的缓冲区来存储更多的数据。

list

std::list 是 STL 提供的双向链表数据结构,能提供线性复杂度的随机访问,以及常数复杂度的插入和删除。

list 的使用方法与 deque 基本相同,不再赘述。但是增删操作和访问的复杂度不同。

#include <list>
using std::list;

list<int> myList{1, 2, 3, 4}

//返回首元素的引用
myList.front();

//返回末尾元素的引用
myList.back();

list 的实现是链表,因此不提供随机访问接口,如果需要访问中间元素,则需要使用迭代器。

list 还提供了一些针对其特性实现的 STL 算法函数:

  • splice():从另一个 list 中移动元素
  • sort():对元素进行排序
  • unique():从容器中移除所有相继的重复元素。
  • merge():合并两个有序列表
  • remove():移除满足特定标准的元素
  • reverse():反转元素的顺序

forward_list

std::forward_list 是 STL 提供的单向链表数据结构,相比于 std::list 减少了空间开销,使用方法类似 list,只是迭代器只有单向。

set/unordered_set/multiset

set/unordered_set/multiset区别:

  • set 是关联式容器,用于存储一组不重复的元素,并且元素的值按照有序排列。set 基于红黑树实现,搜索、移除和插入拥有对数复杂度。
  • unordered_set 则不会按照元素的值进行排序,而是由哈希函数的结果决定,基于哈希表实现。使用哈希存储的特点使得无序关联式容器在平均情况下查找、插入和删除等操作都在常数时间复杂度内完成。
  • multiset 则是一个用于存储一组元素,基于红黑树实现,允许元素重复,并按照元素的值进行有序排列的集合。
  1. set 的使用
#include <set>
#include <unordered_set>

//创建一个存储整数的无序集合
std::unordered_set<int> uoset;

//创建一个存储整数的 set
std::set<int> mySet;

//创建一个存储整数的 multiset
std::multiset<int> mulSet;
  1. 集合相关操作
//向集合中插入元素,例如插入 1
mySet.insert(1);

//从集合中删除元素,例如删除 1
mySet.erase(1);
//删除迭代器为 pos 的元素
mySet.erase(pos);
//删除迭代器在[first, last)范围内的所有元素
mySet.erase(first, last);

//返回 set 内值为 x 的元素数量
mySet.count(x);

/*
  find() 用于查找特定元素是否存在于集合中,
  如果在则返回指向该元素的迭代器,
  否则返回一个指向集合的 end() 的迭代器
 */
if(mySet.find(i) != mySet.end()) {}

//返回指向首个大于等于给定值的元素的迭代器,如果不存在返回 end()
mySet.lower_bound(x);

//返回指向首个大于给定值的元素的迭代器,如果不存在返回 end()
mySet.upper_bound(x);

//判空
mySet.empty();

//返回容器内元素个数
mySet.size();
  1. set 在默认情况下的比较函数为 < (如果是非内置类型需要重载 < 运算符)。我们可以通过传入自定义比较器来自定义 set 内部的比较方式。
    具体来说,需要定义一个类,并重载 () 元素符。
//较大值靠前
struct cmp {
    bool operator()(int a, int b) { return a > b; }
};

set<int, cmp> s;

对于其他关联式存储器也可以用类似的方式实现自定义比较。

注意:

  • set 自带的 lower_boundupper_bound的时间复杂度为 O(log n),若使用 algorithm 库中的 lower_boundupper_bound 的时间复杂度为 O(n)。
  • algorithm 库中的 nth_element 函数查找第 k 大的元素时间复杂度为 O(n),set 没有提供时间复杂度为 O(log n)的 nth_element。如果需要实现平衡二叉树所具备的 O(log n) 查找第 k 大元素的功能,需要手写平衡二叉树或权值线段树,或者选择使用 pb_ds 库中的平衡二叉树。

举例:在贪心算法中经常会需要出现类似“找出并删除最小的大于等于某个值的元素”。这种操作能轻松地通过 set 来完成。

// 现存可用的元素
set<int> available;
// 需要大于等于的值
int x;

// 查找最小的大于等于x的元素
set<int>::iterator it = available.lower_bound(x);
if (it == available.end()) {
  // 不存在这样的元素,则进行相应操作……
} else {
  // 找到了这样的元素,将其从现存可用元素中移除
  available.erase(it);
  // 进行相应操作……
}

map/unordered_map/multimap

map 是有序键值对容器,将一个元素(key,键)和一个对应的值(value)关联起来。搜索、移除和插入操作拥有对数复杂度。

map/unordered_map/multimap区别:

  • map 基于红黑树实现,key 有序且唯一,不存在键相同的元素。
  • unordered_map 基于哈希表实现,key 无序、唯一,查找速度更快。
  • multimap 基于红黑树实现,key 有序,允许多个元素拥有一个键。
  1. 声明
#include <map>
#include <unordered_map>

//声明一个将字符串映射到整数的映射
std::map<string, int> myMap;

////声明一个整数映射到整数的无序映射
std::unordered_map<int, int> uMap;
  1. 映射相关操作
//插入键值对,可以使用 insert() 函数或者 [] 操作符
myMap["math"] = 100;
uMap[2] = 10;
myMap.insert(pair<string, int>("Cpp", 100));

//查找键的存在
if (myMap.find("math") != myMap.end()) {
    //键存在
} else {
    //键不存在
}

//Cpp 中的 pair 类型会将两个不同的值组合成一个单元
//常用来存储键值对
//声明并初始化
pair<string, int> kv("math", 100);
//访问成员
pair.first; // "math"
pair.second; // 100

// 使用范围 for 循环遍历 map 中的所有键值对
// const 关键字表示只能读取容器中的元素,而不能修改它们。
// & 使用引用,减少不必要的内存分配和拷贝操作
for(const pair<int, int>& kv : uMap) {
    ///操作 kv.first/kv.second
}

//返回指向首个大于等于给定键的元素的迭代器
myMap.lower_bound(key);
//返回指向首个大于给定键的元素的迭代器
myMap.upper_bound(key);

注意:在利用下标访问 map 中的某个元素时,如果 map 中不存在相应键的元素,会自动在 map 中插入一个新元素,并将其值设置为默认值(对于整数,值为零;对于有默认构造函数的类型,会调用默认构造函数进行初始化)。

当下标访问操作过于频繁时,容器中会出现大量无意义元素,影响 map 的效率。因此一般情况下推荐使用 find() 函数来寻找特定键的元素。

容器适配器

容器适配器本质上还是容器,只不过此容器模板类的实现,利用了大量其它基础容器模板类中已经写好的成员函数。当然,如果必要的话,容器适配器中也可以自创新的成员函数。容器适配器不具备容器的某些特点,如有迭代器、有 clear() 函数等。

适配器对容器进行包装,使其表现出另外一种行为,

  • stack,栈:后进先出的容器,默认是对 deque 的包装。
  • queue,队列:先进先出的容器,默认是对 deque 的包装。
  • priority_queue,优先队列:元素的次序是由作用于所存储的值对上的某种谓词决定的一种队列,默认是对 vector 的包装。

栈 stack

栈只允许在容器的一端进行插入和删除操作,是一种后进先出的线性表。cpp 中 std::stack的特点:

  • 只支持查询或删除最后一个加入的元素,即栈顶元素。
  • 不支持随机访问。
  • 不提供遍历功能,也不提供迭代器。
#include <stack>
using std::stack;

// 创建栈
stack<TypeName> st; //创建了一个 TypeName 类型的栈
stack<TypeName, Container> st; //使用 Container 作为底层容器
stack<TypeName> copy_st(st); //将 st 复制一份用于构造 copy_st

//以下所有函数均为常数复杂度。

//进栈,把元素 x 插入栈顶
st.push(x);

//出栈,删除栈顶元素
st.pop();

//获取栈顶元素但不会移除
int top = st.top();

//判断栈是否为空
bool isEmpty = st.empty(); //栈空返回 true,否则返回 false

//获取栈的长度
int stackSize = st.size();

队列 queue

队列只允许在容器的一端插入,在另一端删除,允许插入的一端为队尾,允许删除的一端为队头,是一种先进先出的线性表。cpp 中的 std::queue 的特点:

  • 仅支持查询或删除最先加入的元素,即队首元素。
  • 不支持随机访问。
  • 不提供遍历功能,也不提供迭代器。
#include <queue>
using std::queue;

// 创建队列
queue<TypeName> q; //创建了一个 TypeName 类型的队列
queue<TypeName, Container> q; //使用 Container 作为底层容器
queue<TypeName> copy_q(q); //将 q 复制一份用于构造 copy_q

//以下所有函数均为常数复杂度

//入队,把元素 x 插入队尾
q.push(x);

//出队,删除队首元素
q.pop()

//访问队列头部的元素但不会移除
string s = q.front();

//判断队列是否为空
bool isEmpty = q.empty(); // 如果队列为空,返回true;否则返回false

//获取队列的长度
int queueSize = q.size();

优先队列 priority_queue

优先队列 std::priority_queue 是一种堆,一般为 二叉堆。

#include <queue>
using std::priority_queue;

//创建
priority_queue<TypeName> pq; //数据类型为 TypeName
priority_queue<TypeName, Container> pq; //使用 Container 作为底层容器
//使用 Compare 作为比较类型,不可跳过 Container 直接传入 Compare
priority_queue<TypeName, Container, Compare> pq;

// 从 C++11 开始,如果使用 lambda 函数自定义 Compare
// 则需要将其作为构造函数的参数代入,如:
auto cmp = [](const pair<int, int> &l, const pair<int, int> &r) {
  return l.second < r.second;
};
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> pq(cmp);

//访问堆顶元素,常数复杂度
pq.top();

//判空,常数复杂度
pq.empty();

//查询容器中的元素数量,常数复杂度
pq.size();

//插入元素,并对底层容器排序,对数复杂度
pq.push(x);

//删除堆顶元素
pq.pop();

STL 算法

https://zh.cppreference.com/w/cpp/algorithm
STL 提供了许多模版函数,基本包含在 <algorithm> 之中,还有一部分包含在 <numeric><functional>

  • find(begin, end, value):顺序查找。
  • reverse(begin, end):翻转数组、字符串。
  • unique(ForwardIterator first, ForwardItetator last):去除容器中相邻的重复元素。与 sort 函数结合使用可以实现完整容器去重。
  • random_shuffle(begin, end):随机打乱数组。注:最新Cpp标准中已被移除,用 shuffle 函数。
  • sort(begin, end, cmp):排序,end 指向排序的数组的最后一个元素的后一位,cmp 为自定义的比较函数。
  • stable_sort(begin, end, cmp):稳定排序。
  • nth_element(begin, nth, end, cmp):在[begin,end)中查找第 nth 大的元素,使其左边均为小于它的数,右边均为大于它的数。
  • binary_search(begin, end, value):二分查找。
  • merge(v1.begin(), v1.end(), v2.begin(), v2.end(), back_inserter(v3)):将两个已排序的序列有序合并到第三个序列的插入迭代器上。
  • inplace_merge(v.begin(), v.begin()+middle, v.end()):将两个已排好序的范围 [begin, middle) 和 [middle, end) 原地合并为一个有序序列。
  • lower_bound(begin, end, x):在一个有序序列中进行二分查找,返回指向第一个大于等于 x 的元素的位置的迭代器。如果不存在则返回尾迭代器。
  • upper_bound(begin, end, x):在一个有序序列中进行二分查找,返回指向第一个大于 x 的元素的位置的迭代器。如果不存在则返回尾迭代器。
  • next_permutation(begin, end):将当前排列更改为全排列中的下一个排列。
  • prev_permutation(begin, end):将当前排列更改为全排列中的上一个排列。
  • partial_sum(src.begin(), src.end(), back_inserter(dst)):求前缀和。

bitset

std::bitset 是 STL 中的一个存储 0/1 的大小不可变容器。

由于内存地址是按字节寻址,一个 bool 类型是变量,虽然只能表示 0/1,但也占了一字节内存。

bitset 通过固定的优化,使得一个字节的八个比特能分别存储 0/1。对于一个4字节的 int 变量,在只存 0/1 的意义下,bitset 占用空间只是其 1/32,计算一些信息时,所需时间也是其 1/32。

#include <bitset>

//1000bit大小,实际占128字节内存空间
//调用默认构造函数,每一位都是 false
bitset<1000> bs;

//用 val 的二进制表达式初始化
unsigned long val = 8;
bitset<1000> bs(val);

//用 0/1 字符串表达式初始化
bitset<1000> bs("1000101");

// 下标操作符 [] 访问
bs[2];

//比较两个 bitset 内容是否完全一样
bool isSame = bs1 == bs2;

//bitset 只能与 bitset 进行位运算,若要和整型进行位运算,要将整型转为 bitset
bs1 &= bs2;

//进行二进制左移/右移
bs >>= 2;

// 返回 true 即 1 的数量
bs.count();

//返回大小
bs.size();

//类似 vector 的 at() 函数,进行越界检查
bs.test(pos);

//若存在某一位是 true 则返回 true,否则返回 false。
bs.any();

//若所有位都是 false 则返回 true,否则返回 false
bs.none();

//若所有位都是 true 则返回 true,否则返回 false
bs.all();

//将所有位设为 true
bs.set();
//将某一位设置为 true/false
bs.set(pos, val);

//将所有位设为 false
bs.reset();
//将某一位设为 false,相当于 set(pos, false);
bs.reset(pos);

//翻转每一位,0<->1,相当于异或一个全是 1 的 bitset
bs.flip();
//翻转某一位
bs.flip(pos);

//返回转换成的字符串表达式
bs.to_string();
//返回转换成的 unsigned long 表达
bs.to_ulong();
//返回转换成的 unsigned long long 表达
bs.to_ullong();

// 返回第一个 true 的下标,若没有 true 则返回 bitset 的大小
bs._Find_first();
// 返回 pos 后第一个 true 的下标,若 pos 后没有 true 则返回 bitset 的大小
bs._Find_next(pos);

vector 的一个特化 vector<bool> 的存储方式同 bitset,区别在于其支持动态扩展空间,而 bitset 和普通数组一样是在编译时确定内存空间大小,不能动态扩展。
然而 bitset 有一些好用的库函数,不仅方便,而且有时可以避免使用 for 循环而没有实质的速度优化。因此,一般不使用 vector<bool>

string

std::string 是在标准库 <string> 中提供的一个类,用来存储字符串。

  1. 声明和初始化:
#include <string>
using std::string;

string s1; //默认初始化,s1 是空的字符串
string s2 = "hello"; //声明并初始化
string s3(5, 'a'); //连续5个字符 a 组成的字符串,即 "aaaaa"
  1. 字符串操作:
string s1 = "hello";
string s2 = "world";

//转 char 指针
s1.data(); //不保证末尾有空字符
s1.c_str(); //保证末尾有空字符

//字符串拼接
string s3 = s1 + " " + s2;

//获取字符串长度
int length = s1.size();
s.length();
strlen(s.c_str());

//使用 [] 访问字符串中的每一个字符
char c = s1[1];

// find(str, pos)  pos 默认为 0。
//查找字符串中一个字符/字符串在pos(含)之后第一次出现的位置,
//如果没有出现则返回 std::npos。
s1.find("l", 3);

// substr(pos, len)
// 从 pos 位置开始截取最多 len 个字符组成的字符串
s1.substr(2, 3);

// insert(index, count, ch)
//在 index 处连续插入 count 次字符 ch
s1.insert(5, 3, '!');

// insert(index, str)
//在 index 处插入字符串 str
s1.insert(5, s2);

//erase(index, count)
//将从 index(含)开始的 count 个字符删除,
//如果不传 count 则删除 index 及以后所有的字符
s1.erase(3);

//replace(pos, count, str)
//将从 pos 位置开始 count 个字符的子串替换为 str
s1.replace(2, 3, " ");

//replace(first, last, str)
//将以 first 开始(含),last 结束(不含)的子串替换为 str
//其中 first 和 last 均为迭代器
s1.replace(s1.begin(), s1.begin() + 2, "zzzz");

//判断字符串是否为空
s1.empty();
  1. 输入输出字符串
#include <iostream> 
#include <string>
using namespace std;

int main() {
    string word, line;
    while (cin >> word) { //读取单词(以空格分隔)
        cout << word << endl;
    }
    //获取输入的一行文本,并将其存储到 line 中
    getline(cin, line);
    cout << line << endl;
}

pair

std::pair 是 STL 中定义的一个类模板,用于将两个变量关联在一起,组成一个“对”。通过灵活使用 pair,可以轻松应对“需要将关联数据捆绑存储、处理”的场景。

类模板本身不是一个类,而是可以根据不同数据类型产生不同类的模板。
在使用时,编译器会根据传入的数据类型产生对应的类,再创建对应实例。

//定义并初始化
pair<string, int> p("math", 100);

//先定义后赋值
pair<int, double> p;
p.first = 1; //pair 通过两个成员 first 和 second 进行访问
p.second = 2.0;

//赋值
p1 = p2;

//交换
swap(p1, p2);
p1.swap(p2);

//使用 std::make_pair 函数完成初始化
pair<string, int> p = make_pair("math", 100);

//pair 重载了比较运算符,会先比较第一个变量,在其相等的情况下比较第二个变量。
if(p1 > p2) {}

与自定义的 struct 相比,pair不需要额外定义结构与重载运算符,使用起来更加简便。但面对需要将两个以上的变量进行关联,要求变量命名清晰的场景,显然自定义的 struct 会更合适。

Cpp 特性

主要参照 C++11 标准,别的标准会特别标出。

auto 类型说明符

auto 类型说明符用于自动推导变量的类型。

auto a = 1;        // a 是 int 类型
auto b = a + 0.1;  // b 是 double 类型

范围 for 循环

for (auto i : {1, 2, 3, 4}) {
    std::cout << i << " ";
}

Lambda 表达式

Lambda 表达式是一种语法糖,可以方便快捷地创建一个“函数对象”。

[capture] (parameters) mutable -> return-type {statement}

Lambda 表达式在表达能力上和仿函数是等价的。编译器一般也是通过自动生成类似仿函数的代码来实现 Lambda 表达式的。

// Lambda 表达式
auto Plus = [](int a, int b) { return a + b; };

//仿函数
class Plus {
public:
  int operator()(int a, int b) {
    return a + b;
  }   
};

Plus plus; 
std::cout << plus(1, 2) << std::endl;   // 输出 3

decltype 说明符

decltype 说明符可以推断表达式的类型。

#include <iostream>
#include <vector>

int main() {
  int a = 1926;
  decltype(a) b = a / 2 - 146;         // b 是 int 类型
  std::vector<decltype(b)> vec = {0};  // vec 是 std::vector <int> 类型
  std::cout << a << vec[0] << b << std::endl;
  return 0;
}

constexpr

constexpr 说明符可以在编译时对函数或者变量求值,这些变量和函数即可用于需要编译期常量表达式的地方。还可以用来替换宏定义的常量,规避宏定义的风险。

constexprconst的主要区别是一定会在编译时进行初始化。实际上将 const 理解成 readonly,将 constexpr 理解成 const 更加直观。

用于对象声明的 constexpr 说明符蕴含 const,用于函数声明的 constexpr 蕴含 inline

constexpr int NUM = 10; //直接定义常量

constexpr int FivePlus(int x) { return 5 + x; }

void test(const int x) {
  std::array<int, x> c1;            // 错误,x在编译期不可知
  std::array<int, FivePlus(6)> c2;  // 可行,FivePlus编译期可以推断
}
posted @ 2024-01-21 17:14  hzyuan  阅读(29)  评论(0编辑  收藏  举报