本文假设大家都学过C语言,知道程序的基本结构,能够编写函数,希望本文能让大家快速了解现代C++的基本使用和常用特性,备战蓝桥杯。
输入输出
首先算法竞赛肯定会涉及到数据的输入和答案的输出,在C++中,一个简单的输入输出的程序就像这样1
2
3
4
5
6
7
8
using namespace std;
int main(){
int a;
cin>>a;
cout<<a<<endl;
return 0;
}
一行行解释一下,第一行和C语言一样是引入头文件,io stream就是输入输出流的意思,C++的标准库的头文件一般不写.h后缀。
第二行是使用命名空间std,使用C++标准库中的代码(这段代码中的cin,cout,endl都是标准库的代码)都需要指定std命名空间。如果不使用using namespace也可以在函数或者类型前加上std::(如std::cin,std::cout,std::endl)。
cin是标准输入流,对于基本数据类型重载了>>运算符(两个右尖括号,读作“右移”运算符),这个运算符看着也是很形象,就像数据从cin流向了a,以及这个运算符可以连用,比如cin>>a>>b>>c;
输入的数据流会向右依次赋给变量a、b、c,默认会以空白字符分割(即不能用于读取空白字符)。
cout是标准输出流,跟cin相似,cout<<a<<b<<endl
,<<是左移运算符,a和b向左依次流向输出流。endl是end line的缩写,就是结束一行,它同时做了清空输出缓冲区和换行的工作。
函数重载
用过C语言scanf和printf的人或许发现了,C++的输入输出不需要什么%d、%f也能对不同的数据类型使用相同的方法来输入输出,这就得益于C++的函数重载功能。
在C++中编写函数名相同,但是参数列表不同的多个函数就叫重载(返回类型可以相同也可以不同)。编译器会自动选择最匹配的重载版本,试着运行一下下面的代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using namespace std;
void who(int a){
cout<<"int"<<endl;
}
void who(char a){
cout<<"char"<<endl;
}
void who(double a){
cout<<"double"<<endl;
}
int main(){
who(1);
who('1');
who(1.0);
return 0;
}
上面写了三个名叫who
的函数,它对于不同类型的参数会输出它们的类型名。不出意外运行的结果应该是1
2
3int
char
double
cin和cout的话是重载了>>和<<运算符,让它们可以根据变量的类型来选择输入和输出的方式。
引用类型
用过scanf来输入的应该都知道,scanf接受的是指针,也就是变量的地址,但是cin>>a并没有取地址符,它传递的是什么。
C++中还有一种常用的类型,引用。引用一般可以理解为变量的别名,写法为在变量名前用&修饰,这样就声明了一个引用变量,引用变量必须在声明的时候赋值,且之后不能再对应别的变量。1
2
3
4
5
6
7
8
9
10
11
12
using namespace std;
int main(){
int a=1;
int &b=a;
cout<<a<<" "<<b<<endl;
b=2;
cout<<a<<" "<<b<<endl;
a=3;
cout<<a<<" "<<b<<endl;
return 0;
}
这段代码应该会输出1
2
31 1
2 2
3 3
b就是a的别名,任何对b的操作都是对a的操作,以及只有a有实体,b只是a的别名,并没有真实存在,以及b声明之后就只能对应a这一个实体,不能对应其他变量。
引用最大的作用还是在函数的参数传递,学C语言的时候应该都知道传值和传指针,按值传递的话传递的是变量的拷贝,传指针的话传的是变量的地址,如果要对变量进行修改需要传指针,对形参的修改不会影响到实参。
综上所述,C++中传引用的好处主要就是避免了按值传递时,变量拷贝产生的额外开销,以及在函数需要对变量进行修改时,传引用比指针方便,比如下面这个引用版本的swap。
1 |
|
但是也不能什么函数都传引用,首先参数得要是变量,如果函数的参数声明是引用类型,但是传了个常量,那就会报错,可以试试swap(a,1)
,肯定是会报错的。当然结合前面的函数重载,你也可以同一个函数写两个版本,引用版本和按值传递版本,但是很多时候没有必要。
按照个人经验,我对于基本类型(如int、double),在不需要对变量进行修改的情况下都是按值传递。对于复杂类型如结构体和元组,如果不需要修改,都是传常量引用。如果需要对传递的变量进行修改,就传引用。
注:在一些情况下使用右值引用更好,怕大家弄混,而且讲右值引用肯定还要讲移动语义,这些只是性能的优化,对于编写程序来说不是必需的
,而且现在编译器有时候会自动触发移动语义优化,所以不做介绍,大家感兴趣可以自己了解。
auto类型
C++中有很多方便的特性,自动推导类型auto也是一个(跟C语言中的auto不一样)。注意,auto类型主要是用于简化复杂类型的书写,而不是弱化类型,不显式的写出类型,但是我们要知道编译器类型推导它是什么类型。
就我个人认为,如果不熟悉,auto尽量只用在几个地方
- 接收lambda
- 结构化绑定
- 接收迭代器
现在只要知道有这个东西,后面会使用到。
lambda
lambda在C++中是匿名函数,在C语言中,我们都知道函数的定义不能嵌套,但是C++的lambda可以在函数中定义,并且lambda中也可以定义lambda。
1 |
|
可以看出lambda由三种括号组成,第二个和第三个括号和普通函数的意义相同,就是参数列表和函数体。lambda的调用方法也跟函数一样,也是名字后面加(),括号里面是参数。
最大的区别就是第一个括号。第一个括号[]是捕获列表,用于传入外部的变量(不是作为参数),多个变量之间用逗号隔开。捕获列表的变量在lambda初始化时就确定了。在f中是按值捕获了a,所以f相当于拷贝了一份外部的a=1在函数中,而g中的a是引用捕获,所以使用的是跟外部同一个a。可以理解为在函数体的前面加上了auto a=a;
或者auto &a=a;
。
为什么需要捕获列表,因为局部变量有作用域的问题,如果写成这样,大家应该就能看出问题了1
2
3
4
5
6
7
8
9
10
11
using namespace std;
int f(int x){return a+x;}
int g(int x){return a+x;}
int main(){
int a=1,b=2;
cout<<a<<" "<<f(b)<<" "<<g(b)endl;
a=2;
cout<<a<<" "<<f(b)<<" "<<g(b)endl;
return 0;
}
上面这段代码f和g是会报错a未定义的,因为a是定义在main里面的局部变量,在f和g里面没有定义,所以需要捕获列表,把需要用到的外部变量引入lambda的作用域。
复合数据类型
C++和C语言都有结构体struct,但是C++的结构体定义不需要写typedef。1
2
3
4
5
6
7
8
9
10
11
12
using namespace std;
struct T{
int a;
int b;
};
int main(){
T A;
A.a=1;A.b=2;
cout<<A.a<<" "<<A.b<<endl;
return 0;
}
跟C语言几乎一样,没有什么好说的。接下来是C++特有的复合数据类型,pair和tuple。
pair跟名字一样,就是一对数据的意思,先来看看它的使用1
2
3
4
5
6
7
8
9
using namespace std;
int main(){
pair<int,double>a{1,2.0};
cout<<a.first<<" "<<a.second<<endl;
a.first=2;a.second=3;
cout<<a.first<<" "<<a.second<<endl;
return 0;
}
pair后面有一对尖括号,尖括号里面的参数叫模板参数,在尖括号里面可以规定pair的两个数据的类型,然后我们可以通过.first和.second获取到它的两个数据的引用。
pair只能复合两个数据,看起来不太够用(虽然可以pair嵌套,但是没必要),所以有了tuple,使用tuple需要在开头加上#include<tuple>
1
2
3
4
5
6
7
8
9
10
using namespace std;
int main(){
tuple<int,int,int>t{1,2,3};
auto &[a,b,c]=t;
cout<<a<<" "<<b<<" "<<c<<endl;
return 0;
}
这里面需要解释的就一行,auto &[a,b,c]=t;
这个叫结构化绑定,这一行创建了3个引用,分别对应t的三个分量,因为这三个变量的类型不一定相同,所以结构化绑定只能用auto来自动推断绑定的类型,如果不加&的话就是拷贝创建值的变量。
如果想要把tuple中的数据赋给现有的变量,可以使用tie,1
2
3
4
5
6
7
8
9
10
11
12
using namespace std;
int main(){
int a=0,b=1,c=2;
tuple<int,int,int>t{1,2,3};
cout<<a<<" "<<b<<" "<<c<<endl;
tie(a,b,ignore)=t;
cout<<a<<" "<<b<<" "<<c<<endl;
return 0;
}
上面的代码中,我们把t的前两个数赋值给了a和b,同时用ignore忽略了第三个数。
认识模板
模板是C++中最重要的特性之一,上面的pair和tuple,都是使用了模板,类型后面跟了尖括号的都是使用了模板的。
就像我们写作文有模板一样,有个框架把具体内容填进去就实例化出了一篇考场作文,代码也有模板。以上面的pair作为例子,如果我们想要编写写一个能够保存两个int的pair,如果不使用模板,可能会这样写,1
2
3
4struct pair_int{
int first;
int second;
};
我们可以看到这个pair只能保存int类型的数据,如果想要保存其他类型的数据就要为其他类型再写一遍一样的代码。虽然也就是复制粘贴的事情,但是如果代码很复杂,复制粘贴也会很麻烦。如果用模板就可以这样写,
1 | template<typename T1,typename T2> |
上面的代码定义了一个模板类pair,它接受两个模板参数T1和T2,typename是类型名,我们使用时要在pair后面加一对尖括号,尖括号内为模板参数,如pair<int,int>
,编译时会把里面的T1和T2全部替换为我们使用的模板参数,并且实例化出一个对应的pair类型。
同理还有函数模板,比如我们可以写一个打印任意类型的pair的print函数,1
2
3
4template<typename T1,typename T2>
void print(pair<T1,T2> a){
cout<<a.first<<" "<<a.second<<endl;
}
前面函数重载的时候提到过,重名但是参数不同的函数叫做函数重载,会根据参数选择合适的函数,函数模板的每个实例化都是一个重载。接下来试试下面的代码,为了区分标准库中的pair,我们自己写的pair改了个名字,
1 |
|
模板参数也可以自动推导,所以在使用模板函数的时候可以省略模板参数,让它自动推导,上面的print就是省略了模板参数。
本节只是简单讲模板如何编写,让大家知道这个东西,模板还有很多神奇的用法,大家感兴趣可以自行了解。
类型别名
因为C++模板的存在,使得本来一个单词长度的类型名长度翻了好几倍,甚至本来就不短的类型名可能会长出屏幕。为了解决这个问题,还有简化输入,我们可以给类型起别名,上面的代码也可以写成这样1
2
3
4
5
6
7
8
9
10
11
12
using namespace std;
using T=tuple<int,int,int>;
int main(){
int a=0,b=1,c=2;
T t{1,2,3};
cout<<a<<" "<<b<<" "<<c<<endl;
tie(a,b,ignore)=t;
cout<<a<<" "<<b<<" "<<c<<endl;
return 0;
}
上面代码中using T=tuple<int,int,int>;
在全局范围内定义了T是tuple
总结
如果只是打竞赛,这些语法知识差不多就够用了,语法知识多使用很容易记得的,多自己写点代码,想到什么就写出来试一下,可以加深印象。