Author:黄金发の魔理沙

前言

C++语法备忘

C++语法——数据抽象与类型系统

C++类型系统

某个变量的引用等价于这个变量,相当于一个别名,通过引用可以直接操作原变量。引用类型的变量必须在定义时初始化,且不能再引用其他变量。

1
2
3
4
5
6
7
int n = 4;
int & r = n; // r引用了n, r的类型是 int &
r = 3;
cout << r <<endl;
cout << n <<endl;
n = 5;
cout << r <<endl; // 3 3 5

C语言的函数参数传递均为值传递,而C++中的引用类型作为函数参数可以提供引用传递机制,这样可以直接修改实参的值。

1
2
3
4
5
6
7
8
9
void swap(int &a, int &b) {
//参数a和b就是对实际传入的整数变量的引用,这意味着在函数内部对a和b的任何修改都会直接影响到传入的实际参数。
int tmp;
tmp = a;
a = b;
b = tmp;
}
int n1 = 5, n2 = 10;
swap(n1, n2); // n1, n2的值被交换

函数的返回值可以是引用

1
2
3
4
5
6
7
8
int n = 14;
int &SetValue(){
return n;
}
int main(){
SetValue() = 40; //由于SetValue返回的是对全局变量n的引用,因此这实际上是将n的值修改为40。
cout << n <<endl; // 40
}

C++类型推导

C++11引入了auto关键字,用于自动推导变量的类型。auto关键字可以自动推导出变量的类型,但是不能用于数组,函数参数(C++20后允许)、类的非静态成员变量、类的非静态成员函数等场景。

1
2
3
4
5
auto i = 100; // i 是 int
auto p = new A(); // p 是 A *
auto k = 34343LL; // k 是 long long
auto s[] = "hello"; //error
void func(auto param); //error

统一初始化

C++11的统一初始化:所有数据类型允许使用花括号{}来初始化。
会进行严格的类型检查,防止窄化转换导致丢失数据精度。

1
2
3
4
5
6
int n{5};
char c{'x'};
double d{3.14};
char c1 = 11111; //警告
char c2{11111}; //错误
char c3{100}; //允许

常量与只读变量

const修饰类型说明符,在运行过程中该变量的值不能改变

1
2
3
4
const int d; // error
const int a = 10;
const int b{a};
const int c{a+b};

const修饰引用类型:只读引用。不能通过只读引用去修改引用的内容(可以用别的办法修改)

1
2
3
4
int n = 10;
const int &r = n;
r = 20; // error
n = 20; // ok

TODO:const与指针、引用

动态内存分配

new,delete

1
2
3
4
5
6
7
int *p = new int;
*p = 5;
delete p;
delete p; //运行时异常,一片空间不能被delete多次
int *p = new int[20];
p[0] = 1;
delete [] p;//释放动态分配的数组要加[]

过程抽象与函数

缺省参数

C++允许函数的参数有默认值,这样在调用函数时可以省略有默认值的参数。

1
2
3
4
5
void func(int a, int b = 10, int c = 20){
cout << a << " " << b << " " << c << endl;
}
func(1); // 1 10 20
func(1, 2); // 1 2 20

函数重载

同一作用域内名字相同,形参列表不同的多个函数

内联函数

将整个函数代码插入到调用语句处,节省函数调用和返回的开销

C++的inline关键字建议编译器实现内联函数处理。使用方法:在函数定义前加inline关键字

函数指针

定义:

1
2
3
int (*ptr)(int, int) = add; // 括号不能省略
// ptr是一个函数指针,指向一个
// int name (int,int) 这样的的函数

使用方法:

1
2
3
4
5
6
int add(int a, int b){
return a + b;
}
int (*ptr)(int, int) = add;
(*ptr)(1, 2);
ptr(1, 2); // 允许不解地址直接调用所指向的函数

函数指针也可作为形参。

Lambda表达式(TODO)

只调用一次的简单匿名函数
声明方式:

1
2
3
//[捕获列表](参数列表) -> 返回值类型 {函数体}
auto f = [](int a, int b) -> int {return a + b;};
cout << f(1, 2) << endl;

C++的面向对象

C++的类,对象,成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 类的定义
class CRectangle{
public:
int w,h;
int area(){return w*h;}
int perimeter(){return 2*(w+h);}

private:
//成员1
//成员2

}; // 注意一个类结束有分号

// 成员的访问
CRectangle r;
// 对象名(引用名).成员名
r.w = 5;
// 对象指针->成员名
CRectangle *p = &r;
p->w = 5;

成员函数既可以在类内部实现,也可以在类外部实现(这是通常的做法)。
成员函数(又称为方法)的内部实现形式为:

1
2
3
4
5
6
7
//返回值类型 类名::函数名(参数列表){/*实现*/}
class Fruit{
public:void peel(){printf("in peel");}
slice();
juice();
private:int weight,calories_per_oz;
};

这种形式编译时在声明处自动展开(同时会使编译后代码变长,所以只适用于非常简短的函数),运行时不必付出函数调用的代价。
成员函数(又称为方法)的外部实现形式为:

1
2
3
4
5
6
7
8
//返回值类型 类名::函数名(参数列表){/*实现*/}
class Fruit{
public:void peel();
slice();
juice();
private:int weight,calories_per_oz;
};
void Fruit::peek(){printf("in peel");}

::前面的标识符就是查找范围。如果前面没有标识符,表示查找范围为全局范围。这种形式更常见,好处是可以通过使用头文件使得源代码的组织形式更为清晰。

调用一个类对象的成员函数相当于OOP编程语言的“向对象发送一条信息”这个术语。此外,每个成员函数都有一个this指针参数(并未显式出现),允许对象在成员函数内部引用对象本身。

1
2
3
4
5
6
7
8
9
10
11
class Fruit{
public:void peel();
private:int weight,calories_per_oz;
};
void Fruit::peel(){
printf("this ptr = %p",this);
this->weight--;
}
Fruit melon,orange,banana,apple;
printf("address of apple=%x",&apple);
apple.peel();

struct和class定义类的区别:struct默认公有成员,class默认private成员

对象的生命周期

声明和定义→内存分配→初始化→使用→销毁

构造函数与析构函数

可以通过重载定义多个构造函数
一个特殊的构造函数:复制构造函数。只有一个参数,这个参数史对同类对象的引用:
形如 X::X( X& ) 或 X::X( const X& ) 二者选一,后者能以常量对象作为参数。如果不定义复制构造函数的话编译器会默认生成一个.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Complex {
private:
double real, imag;
public:
Complex(double r, double i = 0); //构造函数
Complex(const Complex &c) {
real = c.real;
imag = c.imag;
cout << "Copy Constructor called";
} //复制构造函数

};

// 构造函数在类声明外部实现
Complex::Complex(double r, double i) {
real = r;
imag = i;
}

Complex c1; // error, 没有参数
Complex *pc = new Complex; // error, 没有参数
Complex c2(2); // OK, Complex c2(2,0)
Complex c3(2, 4), c4(3, 5); // OK
Complex *pc = new Complex(3, 4); // OK
Complex c5(c3); // OK, 复制构造函数

绝大多数类至少具有一个构造函数。当创建类的一个对象时,会隐式地调用构造函数,负责对象的初始化。相对应地,类也存在一个清理函数,称为析构函数。

1
2
3
4
5
6
7
8
9
10
11
12
class Fruit{
public:void peel();
Fruit(int i,int j);//构造函数,名字总是和类的名字一样
~Fruit();//析构函数
private:int weight,calories_per_oz;
};
//构造函数体
void Fruit::Fruit(int i,int j){
weight=i;calories_per_oz=j;
}
//声明时由构造函数进行初始化
Fruit melon(4,5),orange(12,6);

当创建类的一个对象时,会自动调用构造函数,程序员永远不应该显示调用构造函数。至于全局和静态对象,程序开始时会自动调用它们的构造函数,而当程序终止时,会自动调用它们的析构函数。构造函数和析构函数都违反了C语言中“一切自己负责”,“语言的任何部分都不应该通过隐藏的运行时程序来实现”的设计哲学。

继承——复用已定义的操作

  • 继承类似于科学分类法,每一层新分类都是对上一层的细化,比如从水果类派生出苹果类,同时多一些自身独有的操作(成员函数)

  • 注意:继承类与嵌套类(类中类)不同,继承表示一个对象时另一个更为普遍的父对象的特型:我们不会认为哺乳动物内嵌套了一条狗,而是会认为狗继承了哺乳动物的特征。

  • C++实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Fruit{
    public:void peel();
    private:int weight,calories_per_oz;
    };
    //派生类:
    class Apple:public Fruit{
    public:
    void make_candy_apple(float weight);
    };
    //派生类对象的声明:
    Apple teachers;
  • 嵌套通常用于实现容器类(栈,队列等)。现在C++增加了模板这个特性,也用于实现容器类

重载——作用于不同类型的同一操作具有相同的名字

  • 重载就是简单地复用一个现存的名字,但它操作的是一个不同的类型。它可以是函数的名字,也可以是一个操作符
  • 尽量为相似的操作进行操作符重载,不要做一些反人类的操作,例如把乘法重载为除法

C++操作符重载实现

首先,增加水果类的加法操作符原型

1
2
3
4
5
6
7
8
9
10
11
12
13
class Fruit{
public:void peel();
int operator+(Fruit &f);//重载+操作符
private:int weight,calories_per_oz;
};
//为重载的操作符函数提供一个函数体
int Fruit::operator+(Fruit &f){
printf("Calling fruit addition\n");//这样我们就能看到它被调用
return weight + f.weight;
}
Apple apple;
Fruit orange;
int ounces = apple + orange;

&f表示它是通过传址调用的。重载在C++的I/O中也非常方便,详见后述。

C++函数重载的实现

  • 同名函数,不同类型形参:调用时根据传入实参类型找对应函数
  • 同名函数,不匹配的参数类型:编译器会做出判断,把实参隐式转换为匹配的形参类型,再调用该函数
  • 如果上述两种方法都不适用,即编译器无法确定哪个唯一的函数该被调用,则无法通过编译

多态——运行时绑定

  • 多态在C++中的意思是支持相关的对象具有不同的成员函数(但原型相同),并允许对象与适当的成员函数进行运行时绑定。

  • 这句话看不懂没关系,看第二种解释:多态是指一个函数/操作符只有一个名字,却能用于几个不同的派生类型。每个对象都实现这个操作的一种变型,表现一种最适合自身的行为

  • 对同一个操作复用,但是稍加改变,这样不同的对象都能用这个操作

  • 还不懂?没事,看代码:

    1
    2
    3
    4
    5
    6
    7
    class Fruit{
    public:void peel();
    slice();
    juice();
    private:int weight,calories_per_oz;
    };
    void Fruit::peel(){printf("fruit in peel");}

    现在声明一个水果对象,并调用peel()成员函数:

    1
    2
    Fruit banana;
    banana.peel();

    将得到一条信息。现在从水果类派生出苹果类,并实现苹果类自己的peel()自己的成员函数:

    1
    2
    3
    4
    5
    class Apple:public Fruit{
    public:void peel(); //可以与基类重名,因为C++会覆盖处理
    private:int weight,calories_per_oz;
    };
    void Fruit::peel(){printf("apple in peel");}

    这时候声明一个指向水果类的指针,并让它指向一个苹果对象(它继承于苹果类):

    1
    2
    Fruit *p=new Apple;
    p->peel();

    打印结果是fruit in peel!看来真正被调用的是基类的peel成员函数。这是因为,如果想使用派生类的成员函数取代基类的成员函数时,C++要求你必须预先通知编译器,也就是在可能会被取代的成员函数前加上virtual关键字,意思为“不让用户看到事实上存在的东西(基类的成员函数)”。
    所谓虚函数,就是在基类中声明函数是虚拟的,并不是实际存在的函数,然后在派生类中才正式定义这个函数。程序运行时,使用指针指向某一派生类对象,这样就能调用指针所指的该派生类对象中的函数,而不会调用其他派生类中的函数。
    此外,无论怎样,如果想调用基类的成员函数,可以使用这个调用方法:

    1
    p->Fruit::peel();

    既然并不是每个成员函数调用都需要这种运行时的间接模式,就应该显式告诉编译器哪些成员函数需要多态:

    1
    2
    3
    4
    5
    class Apple:public Fruit{
    public:virtual void peel(); //加一个virtual
    private:int weight,calories_per_oz;
    };
    void Fruit::peel(){printf("apple in peel");}

    这样再调用输出就是apple in peel了。运行时系统将查看调用虚拟函数的对象,并选择适合该对象的成员函数,如果是一个派生类对象,调用时就不会调用基类版本的成员函数,而是调用派生类的成员函数。

从C到C++

头文件

软院算法无脑用C++万能头文件就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <bits/stdc++.h>
#define eps 1e-10
#define ll long long
#define PI acos(-1)
#define N 100005 /* 存储空间初始分配量 */
using namespace std;
int main(){
int ans;
// Your code here.

cout<<ans;
return 0;
}

命名空间

C++ 的命名空间机制可以用来解决复杂项目中名字冲突的问题。
参见命名空间 - OI Wiki
算法上机等单个程序,建议使用using namespace std;,这样就不用每次都写std::了

C++输入输出流

  • C++的输入输出是通过流对象实现的,标准流对象cin,cout和流运算符等信息存放在头文件iostream中

  • “>>”称为流提取运算符,

  • “<<”称为流插入运算符。

  • 使用操作符而不是函数来操作IO有以下优点:

    1. 可以为任何类型定义操作符。这意味着不需要scanf的%d
    2. 输出信息时,可以用操作符把多个IO操作数联系在一起,如:
    1
    cout << "THE VALUE IS"<< i << endl;
    1. 简化了scanf这样的函数的格式控制和使用方法(笔者注:感觉这条和1一样)
    2. 对“>>”“<<”重载,在一个单一的操作读取和书写整个对象

string字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <string>
using namespace std;
int main(){
string s;
//ab def g hd
while(cin>>s){//不断读入字符串,“>>”输入操作符返回流对象的引用,读入失败返回false
//...
}

getline(cin,s);//读一行,包括空格,换行符
//还可以把string作为流读写,定义在sstream中
stringstream ss;
int x;
ss>>x;
}
  • 可以用cin,scanf等读入,都是忽略前面空白符,读到分隔符(空格,换行)停止
  • 可以和C字符串一样,s[0]看首位
  • 可以直接用来赋值,拼接等操作
  • 但是不能直接由字符数组赋值
  • STL的一些用法:
1
2
3
4
5
6
7
8
9
string a="HelloHello";
string b="Hello";
//查询子串b在a中出现的次数
int cnt1=0;
while (a.find(b, pos) != -1) {
cnt1++;
pos = a.find(b, pos) + b.length();
}

C++的其他要点

异常,模板,内联(inline)函数,new和delete操作符(比malloc和free更方便)

起步和学习C++

精通C++的某个特性没什么用,大多数程序员选择只使用C+++中较简单的一个子集。这个子集包括:

  • 尽量使用的C++特性:
    1. 构造函数和析构函数,但只限于函数体非常简单的例子
    2. 重载,包括操作符重载和IO
    3. 单重继承和多态
  • 避免使用的C++特性:
    1. 模板
    2. 异常
    3. 虚基类
    4. 多重继承

C++或许过于复杂,但是是唯一可行的方案

C++确实对C有一些改进,但也保留了C的一些缺陷,同时妥协了一部分C语言的设计哲学之一“所有特性都不需要隐式的运行时支持”。
C++对C的改进:

  • C++中,字符数组末尾不加\0是不合法的,C语言合法
  • C++允许使用一个常量整数(const int)来定义数组大小,C语言不允许

STL速成

  • 省流:每个C++标准,比如C++11等都有一些标准模板库(Standard Template Library,STL)将很多有用的内容封装过了,直接就能用,包括各类容器(如队列,栈等),算法(排序等)和一些其他功能。
  • 软院算法上机是C++14

容器

  • 可以用于存放各种类型的数据(基本类型的变量,对象等)的数据结构,都是类模板,但是涉及到排序的sort函数,set容器等,如果操作的是其他类型或者自定义排序方式,则需要重载比较运算符或者定义比较函数
  • 对象被插入容器中时,被插入的是对象的一个复制品

容器的分类

顺序容器

vector,deque,list,forward_list,array,string
一般vector(可变长度数组)和list(双向链表)用的最多

关联容器

强调key和value对应来构建关系。
有序关联容器(元素有序排列):set(集合),map(映射,有key/value,根据key对元素排序),multiset(允许相同元素),multimap(允许key相同的元素)
无序关联容器:元素按照特定顺序(乱序)存放:unordered_set,unordered_map,unordered_multiset,unordered_multimap

容器适配器

是容器模板类的实现,没有迭代器(类似指针)了:stack,queue,priority_queue

容器声明,初始化和赋值

1
2
3
4
5
6
7
8
9
10
vector<int> a{1, 2, 3};// 定义 vector 对象
vector b{4, 5};// 可省略类型实参(C++17),可惜软院OJ是C++14
cout<< a.size();//3
vector<int> pile[maxn];
pile[0].push_back(1);//在容器末尾增加新元素1,注意list不支持"[]"


set<string> dict;//string的集合
map<string,int> month_name;//string到int的映射,map也被称作关联数组,下标可以是任意类型,值也可以
month_name["january"]=1;//插入元素

如果涉及到排序的sort函数,set,priority_queue等,操作的是其他类型或者自定义排序方式,则需要重载<运算符或者定义比较方法(重载”()”)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sort(a,a+n,greater<int>);//降序排序  greater return x>y;
sort(a,a+n,less<int>);//升序排序
//判断x是否应比y靠前,就看pr(x,y)是否为 true(sort默认升序)
//greater是一个重载了()的类模板,里面return x>y;
sort(a,a+n,cmp);//自定义排序方法
struct node{string x;int num;}s[100];
bool cmp(node a,node b){
if(a.x.length()!=b.x.length())return a.x.length()>b.x.length();//a比b位数多时a在前面
return a.x>b.x;//位数相同,但a字典序排序比b大,返回true代表在前面
}

priority_queue<int, vector<int>, less<int> > q;//最大堆
priority_queue<int, vector<int>, greater<int> > q;//最小堆,越小的整数优先级越大
priority_queue<int,vector<int>,cmp> q;
struct cmp{
bool operator()(const int &a,const int &b) const {
//个位数大的整数优先级反而小
//a的优先级比b小时返回true(priority_queue)
return a%10>b%10;
}
};
//priority_queue排的是优先级的顺序,并且是在队尾拿取元素,因此与我们认为的顺序相反。
//于是,priority_queue与sort默认用less(<),但sort是升序,priority_queue是大根堆。

容器的操作

大部分标准库容器(指包含容器适配器)共有的成员函数

  • 相当于按词典顺序比较两个容器的运算符:=, < , <= , > , >=, ==
  • empty :判断容器中是否有元素
  • max_size :容器中最多能装多少元素
  • size :容器中元素个数
  • swap :交换两个容器的内容

顺序容器和关联容器中都有的成员函数

  • begin 返回指向容器中第一个元素的迭代器
  • end 返回指向容器中最后一个元素后面的位置的迭代器
  • rbegin 返回指向容器中最后一个元素的迭代器
  • rend 返回指向容器中第一个元素前面的位置的迭代器
  • erase 从容器中删除一个或几个元素
  • clear 从容器中删除所有元素

注意:迭代器是左闭右开区间,所以尾部要多一个位置指向空白元素。r是反向。如果迭代器到达了容器中的最后一个元素的后面,此时再使用它,就会出错,类似于使用 nullptr 或未初始化的指针一样

顺序容器的常用成员函数

  • front : 返回容器中第一个元素的引用
  • back : 返回容器中最后一个元素的引用
  • push_back : 在容器末尾增加新元素
  • pop_back : 删除容器末尾的元素
  • erase : 删除迭代器指向的元素 可能会使该迭代器失效,或删除一个区间,返回被删除元素后面的那个元素的迭代器

关联容器的常用成员函数

  • find : 返回一个迭代器,指向第一个关键字等于给定值的元素,如果没有这样的元素,则返回尾后迭代器
  • insert : 向容器中插入一个元素,返回一个pair,包含一个迭代器和一个bool值,表示插入是否成功
  • count : 返回给定关键字出现的次数
  • remove : 删除容器中所有关键字等于给定值的元素

容器的迭代器(当成指针用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vector<int> a{1, 2, 3};
vector<int>::iterator it = a.begin();
//set<int>::iterator it = a.begin();
cout << *it << endl;//1
it++;
cout << *it << endl;//2
it = a.end();
cout << *it << endl;//0
vector<int> v; //一个存放int元素的vector容器对象,一开始里面没有元素
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
vector<int>::const iterator i; //常量迭代器
for(i = v.begin(); i != v.end(); i++){//判断条件不要用<,因为可能有的迭代器不支持
cout << *i <<","
}
cout << endl;

注意:最好不要在循环体内部增删元素否则迭代器会失效,除非调整迭代器指向的位置

算法

  • STL提供能在中提供能在各种容器中通用的算法,比如插入、删除、查找、排序等,大约有70种标准算法,大多数在algorithm头文件中定义
  • 算法就是一个个函数模板,通过迭代器来操纵容器中的元素
  • 有的算法返回一个迭代器
  • 算法可以处理容器,也可以处理普通数组,例如sort()需要重载”<”
  • 区间都为左闭右开,迭代器的最右一个位置指向空白元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
__gcd(a, b)                    // 求两个数的最大公因数
__builtin_popcount(a) // 求 int 的二进制里多少个 1

is_sorted(a, a + n) // 是否升序
is_sorted_until(a, a + n) // 到哪里是升序
sort(a, a + n) // 不稳定排序(默认升序)
sort(a, a + n, greater<int>()) // 降序排序
stable_sort(a, a + n) // 稳定排序
nth_element(a, a + k, a + n) // 将第 k 大元素放到 a[k]
unique(begin, end) // 对有序数组去重,返回末尾地址

max(a, b) // 返回较大值
min(a, b) // 返回较小值
max_element(a, a + n) // 返回最大值位置
min_element(a, a + n) // 返回最小值位置

lower_bound(a, a + n, key) // 返回第一个不小于 key 的元素的位置
upper_bound(a, a + n, key) // 返回第一个大于 key 的元素的位置
binary_search(a, a + n, key) // 二分查找是否存在

is_heap(a, a + n) // 判断是否为大顶堆
is_heap_until(a, a + n) // 到哪里是大顶堆
make_heap(a, a + n) // 区间建堆
push_heap(a, a + n) // 末尾元素入堆并调整,与 push_back() 配合
pop_heap(a, a + n) // 堆顶移到末尾并调整,与 pop_back() 配合
sort_heap(a, a + n) // 升序堆排序

is_permutation() // 两个序列是否互为另一个的排序
next_permutation() // 下一个排序
prev_permutation() // 上一个排序

fill(a, a + n, val) // 批量赋值
reverse(a, a + n) // 数组翻转