C++入门与STL
C++具有比C更加庞大的函数库和更快捷的处理方式,但在算法竞赛中我们一般用不到C++中一些复杂的东西,相比之下,我们用的更多的是C++与C语言中重叠的部分,以及少量的标准模板库(Standard Template Library)俗称STL
新的改变
首先我们要将文件的后缀由.c改为.cpp。然后平时使用的头文件<stdio.h><ctype.h>等均改为< cstdio> < cctype>等,同时还有一些新的头文件的加入。
对于算法竞赛来说,c++还有一个万能头文件#include<bits/stdc++.h>
基本框架
#include <cstdio>
#include <iostream>//包含了c++中的一些输入输出流;
using namespace std;//改进输入输出的方式;
int main() {
// do something...
return 0;
}
**基本框架与C比起来十分相似,事实上,C++中的大多数都是与C语言相同的;**
新的输入输出方式
#include <iostream>
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)//加快输入输出的速度
int main() {
int x, y; // 声明变量
std::cin >> x >> y; // 读入 x 和 y
std::cout << y << std::endl << x; // 输出 y,换行,再输出 x
return 0; // 结束主函数
}
如上图,std::cin>>或<<是c++中崭新的输入输出方式,它可以避免使用%d等占位符,当然,原先的C中scanf与printf的方式依然可以使用。
这种新增的输入输出方式最大的弊端就是会耗费一定的时间(除非特别要求,否则绝对不多)。
而上面提到的using namespace std;在此声明后,输入输出会变得更加简洁;
```cpp
using namespace std;
int main()
{
int x,y;
cin>>x>>y;
cout<<x<<y;
return 0;
}```
引用
*在C++中新增了引用的方法,主要运用在函数传参方面,与C语言的指针有些类似,但C++也有指针(几乎与C相同),引用只不过是新增的方式,使程序写起来更加方便,具体代码如下:
#include<iostream>
using namespace std;
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
void swap2(int& a, int& b) {
int t = a; a = b; b = t;
}
int main() {
int a = 3, b = 4;
swap2(a, b);
cout << a << " " << b << "\n";
return 0;
}
**只需在传参时函数上加上&符,就能做到传入实参,在函数中改变a,b的值也会影响原来的值;它可以在一定程度上代替C中的指针。
而之所以函数名采用swap2而不用swap,是因为swap已经包含在c++的库中,是c++自带的交换函数,举个例子:
**`
#include<bits/stdc++.h>
using namespace std;
int main() {
int a = 3, b = 4;
swap(a, b);
cout << a << " " << b << "\n";
string x="hfx";
string y="sllh";
swap(x,y);
cout<<x<<" "<<y<<endl;//endl用在这里指另起一行类似于“\n”.
return 0;
}`
当中用的string,我们在下面进行介绍;
迭代器
迭代器(iterator)是一种可以遍历容器元素的数据类型,是针对于stl容器的一种变量,与C语言中的指针类似(当然C++也有指针)。
迭代器的模板如下
std::vector<int> ::iterator it; //it能读写vector<int>的元素
std::vector<int>::const_iterator it;//it只能读vector<int>的元素,不可以修改vector<int>中的元素
//如果有using namespace std,std::可省略;
//vector<int >可根据不同需要更改容器名和类型名;
就像在c语言中用指针遍历数组一样,迭代器也是这样遍历容器.。
for( it = vector.begin(); it != vector.end(); it++ )
cout<<*it<<endl;
//当然还可以反向迭代;
for( std::vector<int>::reverse_iterator it = v.rbegin(); it!=v.rend();it++ )
cout<<*it<<endl;
//reverse_iterator就是反向迭代器,这里的++就相当于往前走;
这里看不懂很正常,只需知道有这个东西,在后面看到相关在回来看看.。(注意,并不是所有的STL容器都有迭代器)!
字符串string
C语言中的字符串就是字符数组,在C++中依然有字符数组这种形式。字符数组的通常用来输入字符串,对于平时操作比较麻烦,比如:
char zfc1[10]="114",zfc2[10]="514";
strcat(zfc1,zfc2);//链接两个字符串;
几乎对字符串的操作,不是循环就是函数,而在C++中新增了一种字符串方式string,就带来了很多方便的操作;同时它需要头文件< string>(用万能头的时候就不用加了)。
输入
首先我们先来介绍一下字符串在C++中的输入;除了原本的scanf(“%s”,zfc1)外,我们还可以用cin<<zfc1;但是,它的缺陷还与C语言一样,遇到空字符就停下了。例如输入eiyou niganman;实际上保存的只有eiyou;在c++中又提供了两种读字符串的方式getline()和get();而getline对于输入字符数组char[]与字符串类型string又有所不同,在此,我们只介绍string中的;
getline();
模板;getling(cin,字符串名);与gets()的效果差不多,自动将‘\n’转换为’\0’,即不保存回车;string str; getline(cin,str);
String类
与日常声明变量一样,在定义的字符串名称前加string
#include<iostream>
#include<string>
using namespace std;
int main()
{
char charr1[100] = "hi ";
char charr2[100] = "csdn";
char charr3[100];
string str1 = "hello ";
string str2 = "world";
string str;
/*赋值操作*/
*charr3 = charr1;***//错误,根据数组学习,数组之间不可直接赋值**
strcpy(charr3,charr1);//正确,赋值charr1给charr3
str = str1;//正确,string是一个类,str为对象,对象之间可以赋值
/*拼接操作*/
str = str1 + str2;//此时str为 "hello world"
/*附加操作*/
strcat(charr1,"xiao mei");//charr1此时为"hi xiao mei"
str1 += str2;//此时str1为"hello world"
}
看了上面的代码,大家可能对string有了一定的了解,相当与直接声明了一个字符串,无需声明大小,系统自动截取。之后从上面的代码我们可以看出来,无论是赋值还是拼接,都比字符数组要方便的多;
下面我们再来介绍一些特性:
1.string(size_type n,char c) :创建一个包含 n 个元素的 string 对象,其中每个元素都被初始化为字符 c
string str(10, 'a');
2.获取string对象的长度,C语言中使用strlen()来获取字符串长度,C++中使用str.size()或str.length().
string str("hello!");//这也是一种string初始化的方式
int len1 = str.size();
int len2 = str.length();
3.string.append() 函数,在string对象后添加一个string对象或c风格字符串。
string str("hello");
string str2("world");
str.append(str2);
str.append("abcd");
4.string.push_back() 函数来在一个 string 对象后面附加一个字符
string str("hello");
char ch = 'a';
str.push_back(ch);
5.对于string对象的比较,可以直接使用关系运算符。
string str1("abcd");
string str2("abcd");
if(str1 == str2)
break;
6.访问 string 字符串的元素
string str("hello");
cout << str[2] << endl;
cout << str.at(2) << endl;
7.string中的find()函数
(1)string的find()函数用于找出字母在字符串中的位置。
find(str,position);//str是要找的字符串;
//position:字符串中的某个位置,表示从从这个位置开始的字符串中找指定元素(不填第二/个参数,默认从字符串的开头进行查找)
返回值为目标字符的位置(第一个字符位置为0),当没有找到目标字符时返回npos(就是返回npos,例如if(str.find(x)!=steing::npos)就是没找到的意思)
(2) 延伸用法
找到目标字符第一次出现和最后一次出现的位置
string s = "hello world!";
cout <<s.find_first_of("l") << endl;//第一次出现的位置
cout << s.find_last_of("l") << endl;//最后一次出现的位置
反向查找
string s = "hello world!";
cout << s.rfind("l") << endl;//即从后往前第一次出现"l"的位置
最后,如果要清空string中的字符串可以用strName.clear()函数;
这里只是对string做了一个简单的介绍,都是平时在做题中经常用到的,如果想了解的更仔细的话可以自行查询,这里不再过多赘述了。
结构体的运用
C++中的结构体与C中略有不同,在这我们只说一小部分经常用到的内容。结构体(struct),可以看做是一系列称为成员元素的组合体。可以看做是自定义的数据类型。在 C++ 中 struct 被扩展为类似 class 的类说明符。class是与结构体相似的一个类。在工程中,一般用struct定义“纯数据”的类型,只包含较少的辅助成员函数,而用class定义“拥有复杂行为”的类型。为了简单起见,我们在这里只谈论struct;
C++中的结构体除了可以拥有成员变量(用a.x的方式访问)之外,还可以
拥有成员函数(用a.add(1,2)的方式访问)。
#include<iostream>
using namespace std;
struct Point {
int x, y;
Point(int x=0, int y=0):x(x),y(y) {
}
};
int main() {
Point a, b(1,2);
a.x = 3;
cout << a+b << "\n";
return 0;
}
声明Pointa,b(1,2)时,分别调用了Point( )和Point(1,2)。注意这个构造函数的两个参数后面都有“=0”字样,其中0为默认值。也就是说,如果没有指明这两个参数的值,就按0处理,因此Point( )相当于Point(0,0)。“:x(x),y(y)”则是一个简单的写法,表示“把成员变量x初始化为参数x,成员变量y初始化为参数y”。也可以写成:
Point(intx=0,inty=0){this->x=x;this->y=y;}
这里的“this”是指向当前对象的指针。this->x的意思是“当前对象的成员变量x”,即
(*this).x。
此次仅简单介绍可以在结构体中定义函数,对于C++中的结构体还有更多 的妙用,再次不做无关题目的赘述。
内存分配new delete
在C语言中,我们用malloc和free进行内存的动态分配和释放。在C++中,新增了new与delete这种分配与释放的方式。
new
new用于内存的分配,与malloc相似。
1.T *p = new T;
其中,T为任意类型名,比如int、char等等,p为类型T的指针。上面的代码会在堆上动态的分配出大小为sizeof(T)的内存空间,并且将该空间的起始地址赋值给p。
2.T *p = new T[N]
;
其中,T 是任意类型名,p 是类型为 T* 的指针,N 代表“元素个数”,可以是任何值为正整数的表达式,表达式中可以包含变量、函数调用等。这样的语句动态分配出 N × sizeof(T) 个字节的内存空间,这片空间的起始地址被赋值给 p。
Delete
delete用于内存的释放,与free相似。
delete p;//对应new的第一种
delete[] p;//对应第二种
注意:1.分配过的内存一定要释放;
2.new分配到不能分配时返回的不是NULL。
不定长数组vector
vector为可变长数组(动态数组),定义的vector数组可以随时添加数值和删除元素。
注意:在局部区域中(比如局部函数里面)开vector数组,是在堆空间里面开的。
在局部区域开数组是在栈空间开的,而栈空间比较小,如果开了非常长的数组就会发生爆栈。``
故局部区域不可以开大长度数组,但是可以开大长度vector。使用时需要头文件< vector>;
1.定义
vector<int>a;//定义一个int类型的不定长数组a,int可换为任意类型`
vector<int>b(n,1);//定义一个int类型长度为n的数组,里面的值都为1;
vector<int>c[6];//定义一个行数为六的二维数组;
vector<vector<int>>m;//定义一个行列都不定长的二维数组;
vector<int>s(a);//定义一个数组s与a大小和值都相同;
vector也可以想平常数组一样赋初值;
vector<int>c={
1,2,3,4,5};
注:不定长数组并不是指它的空间无限大,而是通过插入数值来扩大空间。如果初始没赋大小,那它就是0;在有初始空间情况下可以cin输入,如果超出空间就要用到c.push_back()等函数向内填值;
2.相关函数
pug:这是我见过的最好一张图了,描述的很详细了,我感觉自己很难描述成这样。。。
平常访问值的时候依然可以像数组一样访问如:a[i];这里我只说这一种方式,对于未接触过c++的人来说就这样访问就可以了,很方便;(C++中常见的访问方式是迭代器访问,有兴趣可自行学习)
其实这跟平常数组也差不多,但是省了很多事,十分方便;
栈stack
栈是STL中实现的一个后进先出的容器,称作:stack。使用时需要头文件;它就像一个试管,你把一颗颗珠子放进去。要拿出来是只能先拿最上面的,也就是最后放进去的。栈就是这样一种容器。
stack<int>a;//栈的定义
之后不管是放入还是取出,都需要用到函数。
话不多说,直接上代码。
int main() {
int a,b;
stack<int>n;
for (int i = 0; i < 6; ++i) {
cin>>a;
n.push(a);//在顶部添加一个元素;
}
cout<<n.size()<<endl;
for (int i = 0;; ++i) {
if(n.empty())break;
cout<<n.top()<<" ";
n.pop();//删除栈顶元素;
}
puts("");
b=(int)n.size();//这里用类型转换的原因是size返回的是unsigned类型,所以转化一下。
cout<<b;
return 0;
}
对于栈的讲解只有这么多,是不是很简单。。。
队列
STL的队列容器主要分为三种,queue,deque,priority_queue
queue
queue队列与栈不同,秉持一个先进先出的原则。就像一个漏斗,你先放进去的,肯定先出来。学了这么多,大家应该不难看出STL容器的声明方式十分类似,下一步我们先看函数,然后直接上代码。
注意:是从尾部添加元素,可以返回尾部和头部的元素,但只能从头部往下走(就是每次只能删除头部元素)。
int main(){
queue<int>a;
int n,c;
cin>>n;
for (int i = 0; i < n; ++i) {
cin>>c;
a.push(c);
}//向容器中放入元素
cout<<a.size()<<endl;//输出容器大小
for (int i = 0; i <n ; ++i) {
cout<<a.front()<<" "<<a.back()<<endl;
a.pop();//删除第一个
}//无清空函数;
return 0;
}
deque
deque被称做双端队列,与queue不同的是,它既可以从头部插入(或删除)元素,也可以从尾部插入(或删除)元素。比起queue,我感觉它更像vector,区别在于它不能从中间插入元素,也不能用下标访问。
其中包括begin(),end()函数返回首地址和尾地址的下一位!!!
注意:其中iterator是迭代器部分的东西,我们在前面也提到过。由于没有学习C++,从C语言 的角度来说,就像C中的指针,指向的迭代器就像地址一样。这里是说erase()中放的是地址,只要注意这一点也可以灵活运用了。
注:deque可以使用sort排序默认由小到大;
int main(){
deque<int >s;
deque<int>::iterator it;//可以不用理解(迭代器的声明,与指针类似)
int n,x;
cin>>n;
for (int i = 0; i <n ; ++i) {
cin>>x;
s.push_back(x);
s.push_front(x);
}//我们在这里从前后同时插入元素
it=s.begin();//可以不用理解
s.erase(it+2,s.end()-3);//可以不用理解
cout<<s.size()<<endl;
sort(s.begin(),s.end());//排序
for (int i = 0;i<n ; ++i) {
cout<<s.front()<<" "<<s.back()<<endl;
s.pop_back();
s.pop_front();//删除最后/最前的元素;
}
s.clear();
return 0;
}
priority_queue
priority_queue被称作优先队列,如果按我的理解来的话,这个东西就是自动将queue排了个序,每次输出最大或最小的那个。这是一种“堆”的容器,你可以把它理解为金字塔,每次只拿塔尖的东西(奇妙比喻)。
我们先看一下相关函数,可以先浏览一下,之后对照下面的代码理解。
每次只能取塔尖哦!
int main(){
//标准的定义方式:priority_queue<类型,容器类型,greater<>或less<>>a;
priority_queue<int >a;//默认大根堆(less<>就是越来越小,每次输出最大的),默认容器是vector
priority_queue<int,vector<int>, greater<>>b;//小根堆
int n,x;
cin>>n;
for (int i = 0; i < n; ++i) {
cin>>x;
a.push(x);//放入元素
}
cout<<a.size()<<endl;
for (int i = 0; i < n; ++i) {
cout<<a.top()<<" ";//每次输出的就是优先级最高的
a.pop();//删除优先级最高的
}
return 0;
}
看完代码,你是不是觉得更加无法理解了,因为这个东西定义太复杂了。你先要记住,这玩意是你可以指定它的顺序和内置容器(一般是vector和deque,不能是list,初学用vector即可,模板第三行代码已给出),不用过多的理解,因为它就是这样定义的。如果你明白了定义,那么后面的就是一些简单 的应用,很容易掌握。
注:优先级可以通过重载自定义,需要一定的C++基础,而且一般情况下这两种就够用了,在这里就不过多赘述。
然而到这还没完,因为优先队列里还可以放入pair(你就理解为是一个地方放了两个值)。就像一个结构体数组,有两个值。
priority_queue<pair<int ,int>>q;//这样定义,pair的类型可以自定义(但必须可比较大小)。
//它内部排序就是先比前一个,如果相等再比后一个。
具体应用我们来看下。
int main(){
priority_queue<pair<int ,int>>q;
q.emplace(1,2);//插入值,这个值的前半部分是1,后半部分是2;
q.emplace(3,4);
cout<<q.top().first<<" "<<q.top().second;//由于是两个值,输出是就像结构体输出一样,前面的叫first,后面叫second
return 0;
}
由于是一次进入两个值,所以我们这里用到emplace函数(不用去理解,用法就是代码里的。。)。也有其它的插入方式。
q.push({
7, 8});
q.push({
7, 9});
q.push(make_pair(8, 7));//用法如图所示
这里你学习了很多新的函数,不用去担心,因为它就是换汤不还药,用法很朴实无华,就像你在C中有%d,%c一样,不同类型对应输入方式不同,记住就好了。
映射map
map是一种对应关系,每一个键对应一个值。
模板:map<t,t>;//t是类型
map<string,string> mp1;
map<string,int> mp2;
map<int,node> mp3;//node是结构体类型
看上面的说明,我们就大概了解了map的定义;<a,b>中的a就相当与数组arr[i]中[]的i;而b就相当与arr[i]对应的值。
那么map有什么用呢,它可以让字符对应数值mp["ikun"]=666;
而且我们知道,平时开数组时,我们能开的范围是有限的。但用map就可以让键值达到long long范围;
#define ll long long
map<ll,ll>map1;
ll arr[x];//x过大会导致运行错误;
下面我们就看看它相关的函数;
我们直接上代码;
#include <bits/stdc++.h>
#define ll long long
using namespace std;
map<ll,ll>c ;//<键,值>
map<string,ll>s;
int main(){
int n,y;
cin>>n;
s["nikezhenliua"]=5;
cout<<s["nikezhenliua"];
for (auto i = 0; i < n; ++i) {
cin>>y;
c.insert(pair<ll,ll>(i,y));//构造键值对,使用pair
//也可以c[i]=y;
}
cout<<c.size()<<endl;
if(c.count(3))cout<<c.find(3)->second<<endl;//查找与打印;
for ( map<ll,ll>::iterator its=c.begin();its!=c.end();its++) {
cout<<its->first<<" "<<its->second<<endl;
}//遍历输出
cout<<c.lower_bound(4)->second<<endl;//大于等于键值
cout<<c.upper_bound(4)->second<<endl;//大于该键值的第一个
c.clear();
return 0;
}
这里面有很多与之前都相似,值得注意的是,lower_bound()中放的是键值,就是前一个值;
map还有很多进阶的用法,例如二维map,一键对应二值等,有些因为太过深入,有些是因为不常用,这里就不过多阐述了。
set
set容器中的元素不会重复,当插入集合中已有的元素时,并不会插入进去,而且set容器里的元素自动从小到大排序。也就是说,set中的东西有且只有一个;
像以往的定义一样,set的定义方式没有什么特别。
set<int> s;//<>中可以是任意类型;
下面我们来看相关函数:
下面我们来看相关代码;
#include <bits/stdc++.h>
using namespace std;
#define ll long long
int main(){
set<ll>shit;//默认从小到大
set<ll,greater<> >ui;//从大到小;
ll n,x;
cin>>n;
for (int i = 0; i < n; ++i) {
cin>>x;
shit.insert(x);//插入一个元素 ,像这种自动排序的容器就不用push了;
}
shit.erase(shit.begin());
shit.erase(1);//erase中既可以写元素值,也可以放迭代器。
if(!shit.empty())
cout<<shit.size()<<endl;
cout<<shit.count(3)<<endl;//返回3的个数,由于set不允许重复存在,所以为一;
auto kun=shit.find(3);
cout<<*kun<<endl;
for (auto &it:shit) {
//相当与定义一个迭代器it遍历shit
cout<<it<<" ";
}
shit.clear();
return 0;
}
除此之外,set还可以通过重载或函数的方式进行更复杂的排序,在此我们只做最基础的介绍
二进制容器bitset
这个容器是专门存放二进制的,对于一些复杂的二进制运算有良好的效果,它的声明和之前不太一样。
bitset<n>bit1;//n是常数或者常量表达式,表示长度为n,bit1是名字,默认初始值是0;
bitset<n>bit2(s)//s可以是一个数(自动转为二进制,前面补零),或者一个字符串(由0和1组成);
bitset声明的容器可以用下标访问,并且可以直接进行位运算。
bitset<8>bit2(14);
cout<<(bit2>>1)<<endl;
cout<<(~bit2)<<endl;
cout<<bit2[3]<<endl;
//输出为00000111
//11110001
//1
之后我们来看一下相关函数;
虽然看起来很多,但是都是熟悉的使用方式。注意,.size()返回的就是你初始时n的值;
我们来看代码;
#include<bits/stdc++.h>
#define ll long long
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
using namespace std;
int main(){
//可通过下标访问
string s;
cin>>s;
int x;
const int n=10;
bitset<n>bit1(s);
if(bit1.any())//是否含有1;
cout<<"1的个数为"<<bit1.count()<<endl;
cout<<"二进制位的个数为"<<bit1.size()<<endl;
cin>>x;
if(bit1.test(x))
cout<<x<<"的位置是1"<<endl;
else bit1.set(x);
cout<<bit1<<endl;
cout<<bit1.to_ulong()<<endl;
bit1.flip();
cout<<bit1<<endl;
cout<<bit1.to_ulong()<<endl;
return 0;
}
下面是输出。
10101110
1的个数为5
二进制位的个数为10
6
0011101110
238
1100010001
785
其实就是相当与一个bool的数组,可以位运算,很容易理解。
STL容器其实不止这些,还有更多的容器需要大家自己去拓展,而且有很多的stl容器在功能方面有诸多重复,所以这里所介绍的都是常用的容器,只供初步的学习。
而学习的路是很漫长的,我认为不论哪一本算法书或着那一篇文章都不能让你一遍看懂,想要真正掌握一门知识还需要自己动手去写出来,每一个代码都自己去写一遍,我认为这便是最快的理解与进步方式!!!
到此为止,我们对STL 的初步介绍就完成了,其中肯定有很多不完备的地方,希望与大家共同交流与进步。
本文参考的资料有:《算法竞赛入门经典第二版》《C++ primer plus》以及众多csdn的文章
本文仅供交流学习使用