扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
在这一篇中,我们会讲一讲C++的基本语法,事实上这一章讲的更多的应该是从C语言到C++的语法演变,看完这一篇之后,你应该可以写出更加符合C++习惯的代码。
C++由贝尔实验室的Bjarne Stroustrup发明,没错,又是贝尔实验室,C语言也是在贝尔实验室诞生的。C++最初名为C with classes,是比较简单地在C语言的基础上增加了面向对象的程序设计模式。
在1998年,C++98标准推出,这也是第一个由国际标准化组织负责的C++标准,后续推出的C++03,C++11,C++14等标准为C++引入了各种各样的全新特性,关于C++标准中的一些特性,你可以参考:cppreference.com,网站中有着对各个标准的特性的简明解释以及各编译器对相应特性的支持情况。
在C语言当中,我们用0表示假,非0的任何数字表示真,有的时候可能会有点别扭,实际上我们可以在C中引入stdbool.h这个头文件来增加true和false两个宏定义,不过终究只是宏定义,不算是真正的bool类型。
C++中引入了真正的bool类型,我们可以使用以下的方式声明并定义一个变量:
bool a = true;
bool b{false};
用小写的true和false表示真和假,关于第二行的初始化方式我们之后会提到。
(2). auto—自动推断变量类型(C++11) C++11标准中将auto关键字的语义做了修改,事实上是很大的改动,在C以及C++11之前的标准中,auto代表由编译器决定变量存放在内存的位置,与之对应的是register关键字,代表希望将变量存放在寄存器当中,当然,编译器不一定会听哈。
不过好像,我们在C语言中声明变量的时候没有写过这个auto啊?没错,如果一个变量声明的时候不写auto,它就默认是auto的,只有声明了是register的情况下才会是register,所以好像,把auto关键字去掉也不是什么大事对吧?
register在上一部教程里貌似没提到,如今的计算机运算速度很快,一般写程序是不太会用到的,不过如果你有一个OIer同学,他肯定知道这个东西有什么用(常数优化),我们就不细讲了。
接下来就正式引入一下C++11标准中的auto,编译器还是具备相当的智能的,虽然这么说也不至于,因为早就有了一套明确的推断字面量类型的规则了,例如一个不超过int的数字字面量就为int,bool就是bool,一般的浮点数字面量为double;后面带标识符的比如1.23f就是float,123ll就是long long,就是这么简单,所以只要判断一下就行,还是相当简单的:
#includeint main()
{auto a = 1.23f;
auto b = 234;
std::cout<< "Type of a is "<< typeid(a).name()<< std::endl<< "Type of b is "<< typeid(b).name()<< std::endl;
return 0;
}
typeid(var).name()可以返回变量的类型名,我这里是使用了VS,如果是gcc编译器,结果可能是f和i,不过不影响就是了。
使用auto关键字有两个比较重要的点,首先是auto关键字只能在变量定义的时候使用,因为auto关键字说到底也只是根据被赋予的初始值在编译器确定类型,因此在C++中不存在 “自由变量”,可以像python一样任意变换类型。
二来是如果多个变量在同一行定义,他们必须保证是同一个类型的,即便前面是auto,例如:
auto a = 1.23f, b = 234;
在VS中会直接提示代码错误,而在Dev-C++中,会在编译的时候报错,我们必须保证同一行的所有变量的类型都是一致的,否则就只能分行写了。
所以虽然auto看起来很令人心潮澎湃,它还是把C++限制在了同一变量名不可改变的规则之下,如果真的要变,你可能得参考一下C++的模板(template),之后也会讲到。
听到这里其实还是挺心潮澎湃的,至少C++向着自动化的方向迈进了一大步,那auto能不能作为函数返回的类型呢?在C++11里不能,C++14里就可以了,比如下面这一段代码:
auto func(auto a, auto b)
{return a + b;
}
这段代码在C++14中可以编译通过,C++11中不行,C++11中得这么写:
templateauto func(T1 a, T2 b) ->decltype(a+b)
{return a + b;
}
看完之后内心:…,还是别这么写了吧,要么用C++14,要么就好好写变量类型,不过我还是要解释一下,template是模板关键字,C++11中因为auto在a和b之前出现,没有办法推断类型,因此要在后面加一个->表示返回类型,decltype()下一节会说,是表示函数内表达式的值的类型。真的,相当麻烦,别这么写了就。
还有一个要注意的点是,auto不能作为数组的类型出现,即便后面有对它的初始化也不行。
(3). decltype()—根据某个值推断类型decltype()是和auto同时出现的,跟auto有点不一样,decltype()可以填入一个表达式,从而表示这个表达式的结果的类型,举个例子:
decltype(1) func(int a, int b)
{return a + b;
}
decltype(3.0f) f;
用decltype()的好处在于可以只声明不定义,因为变量的类型是已经确定了的,或者就算不确定,也可以通过其他一系列方式推断出类型,在编译器依旧可以确定下来。
(4). namespace—命名空间命名空间是一个比较重要的概念,在C语言中我们曾经讲过这么一段示例代码:
#includeint main()
{int a = 0;
{int a = 10;
printf("a = %d\n", a);
}
printf("a = %d\n", a);
return 0;
}
结果是第一行10,第二行0,我就不再演示了,在main()函数里面的{}括起来的局部作用域再次定义了一个变量a,值为10,和main的作用域中的不一样,在C中我们是没有办法利用这个局部作用域中的变量的,但是C++扩展了这个,给予了我们一项能力:给一个作用域命名,例如:
namespace ns
{int a = 3;
int add(int a, int b)
{return a + b;
}
}
我们可以在一个命名空间中定义变量与函数(类也可以,之后再说),这样一来即便有重名的函数和变量我们也可以指明他们所在的命名空间来加以区别,我们其实已经见过命名空间了,比如:
std::cout
这里的cout这个std::ostream对象就是定义在std命名空间中的,有点套娃的那意思了,cout对应的这个类ostream,也定义在std命名空间中。std是C++的标准命名空间,很多标准库中的内容都定义在这个命名空间中,不仅是常用的这些函数之类的,还有我们之后会讲到的标准模板库(STL, Standard Template Library),其中的容器、容器适配器和算法都存放于std命名空间当中,关于STL的内容我们之后会单独有几节来讲,这是C++中非常重要的一个部分。
这里还要提一个语法,namespace不仅可以定义命名空间,还可以起别名,例如:
namespace ns
{int a = 0;
}
namespace n = ns;
我们之后讲到std::filesystem的时候还会用到,这里就不多说了。
(5). ::—域解析操作符当然,我们上一节没有解释这个::,其实我在引言当中已经说了,::叫做域解析操作符,是针对于命名空间和类的一个操作符,假设变量a存在于命名空间ns中:
namespace ns
{int a = 0;
}
那么我们可以通过以下的方式调用ns中的a:
#includenamespace ns
{int a = 0;
}
int main()
{std::cout<< ns::a<< std::endl;
ns::a++;
std::cout<< ns::a<< std::endl;
return 0;
}
是吧,还是很easy的,调用ns中的其他东西也一样可以采用::,例如我们的std::cout,就是调用std命名空间中的名为cout的std::ostream对象。
有的时候还有人会这么写:
#includeint a = 10;
int main()
{std::cout<< ::a<< std::endl;
return 0;
}
简单来说,前面如果不写内容,就说明是从全局命名空间中调用某变量或某函数。
(6). using—typedef的C++版本每次打印都要写std::cout真的会很不爽,假设你是一个OIer或者ACMer,你肯定会很习惯输入这一行语句:
using namespace std;
#1.using + 对象/函数/变量;using语句是一个很强的语句,首先可以引入某个命名空间的函数与对象,例如:
#includeusing std::cout;
using std::endl;
int main()
{cout<< "Hello, world!"<< endl;
return 0;
}
这一段代码也可以打印出Hello, world!,而且不需要再输入std::了,很方便是吧?
#2.using namespace + 命名空间; 这之后就是using namespace句式了,例如之前说过的using namespace std;就是将整个std命名空间全部引入,这样的好处是,我们之后用到的所有处于标准命名空间中的内容都不需要在加std::了。
对于OIer和ACMer来说:这是省时间的利器,但如果你是写一个工程,请一定要慎用,随意地使用using namespace可能会导致命名空间污染的出现,本身namespace就是避免同名函数冲突的一种解决方案,举个例子,你定义了这么一个函数:
int max(int a, int b)
{retuan a >b ? a : b;
}
同时还有一个命名空间n,在其中也有一个max函数,当你编译运行的时候
#includenamespace n
{int max(int a, int b)
{return a - b;
}
}
using namespace n;
using namespace std;
int max(int a, int b)
{return a >b ? a : b;
}
int main()
{auto a = 1, b = 2;
cout<< max(a, b)<< endl;
return 0;
}
VS提示了一些错误呢,基本上就是说,在调用int max这个函数,且参数列表是(int, int)的时候,遇到了好几个不同的函数定义,这样就引发了冲突,这就是命名空间污染的一种表现。因此在工程中不要随便使用using namespace语句!
#3.using A = B;using语句的最后一个用途是和C语言的typedef一样,给某个已存在的名字赋一个别名,当然,typedef在C++中也被保留了,你想用也可以,但是真的会很奇怪,我会更加建议使用using语句:
typedef unsigned long long size_t;
using size_t = unsigned long long;
以上的两条是一样的意思,但是看起来还是using语句会更直观一点对吧,事实上每次我在写代码的时候如果要用到typedef都要想一想哪个在前哪个在后,还是有一点麻烦的。
(7). {}—初始化列表(C++11)C++中有很多新的类型,初始化列表就是其中之一,其实初始化列表这个东西在C语言中也出现过,就比如:
int a[10] = {1, 2, 3, 4};
这个{1, 2, 3, 4}在C++中演变成了初始化列表,是作为C++标准库的一个类型出现的,类型的名字为:
std::initialize_list
其中T是一个模板,不过你不用知道那么多,它在这里存在的意义就是,任何一个类型都可以填进去,比如对于{1, 2, 3, 4},它可以是:
std::initialize_list
而对于{1.2, 2.3, 3.4, 4.5},它又可以是:
std::initialize_list
总而言之,它不会受类型限制,我们把这种类叫做模板类,之后在讲模板的时候还会具体讲。我们这里讲一些基本的用法就好了。
在C++中,现在有四种方式定义变量了:
int a = 1;
int a = {1};
int a{1};
int a(1);
在类与对象的程序设计当中,前两种方法会先根据参数表直接调用构造函数,再调用类的拷贝构造函数(Copy Constructor),而后面两种则是直接调用对应参数表的构造函数,后面两种方法相比前两种调用构造函数的次数更少。这里暂时不细讲,我们之后在类与对象部分的构造函数中还会更加细致地讲解这部分的内容。
不过你可能想了,有没有这样一种可能,auto关键字也可以用于以上的初始化方式:
auto a = 1;
auto a = {1};
auto a{1};
这里可能要让你失望了,在C++11中,auto的推导准则是非常机械的,对于前面两种,规则很固定,第一个a的类型为int,第二个a的类型为std::initializer_list,这个没啥问题,关键就在第三个了,C++11将a的类型也推断为std::initializer_list,这个很怪,但是是真的,在C++11中我们不能让auto和初始化列表同时出现。
这个问题在C++17中得到了解决,auto的推断机制变得更加直观,对于auto a{1};这样的写法,a的类型被推断为int,同时在C++11中成立的auto a{1, 2};这样的写法在C++17中也变为语法错误,当然,auto a = {1, 2};是没有问题的。
说了这么多,初始化列表的好处是什么呢?初始化列表可以防止数据窄化,在利用初始化列表定义变量的时候,初始化列表内的变量类型与变量的类型必须唯一匹配,即类型并非auto的情况下,以下语句是不合法的:
double a{1};
除此之外,在类的构造函数中,我们可以使用初始化列表来接收任意个数的参数,这个之后再讲面向对象的时候还会提到。
(8). &与&&—引用类型 #1.没见过的奇怪表示在C语言中,你见过以下的表示:
int a;
int* a;
int** a;
int*** a;
三重指针可能没见过,但是一般就两种,一般变量和指针。然后有一天你看了一些C++代码,发现了这样的代码:
int b = 1;
int& a = b;
int&& a = 1;
const int c = 3;
const int& d = c;
好怪,不仅有把&写在类型的位置上的,还有写&&的。在C语言中&有几个用途:取地址(&)、位与(&)和逻辑与(&&),C++中又增加了一种&的用途:引用类型(Reference)。
#2.引用(Reference)的概念C++的引用可以理解为通过另一个变量对某一个变量进行引用,比如我们看到以下代码:
#includeusing std::cout;
using std::endl;
int main()
{auto a{10};
auto& b{a};
cout<< "a == b is "<< std::boolalpha<< (a == b)<< endl;
cout<< "&a = "<< &a<< ", &b = "<< &b<< endl;
cout<< "a = "<< a<< ", b = "<< b<< endl;
a++;
cout<< "a = "<< a<< ", b = "<< b<< endl;
return 0;
}
先解释一下auto& b{a}; auto关键字虽然能够进行自动类型推断,但不会推断出引用类型,因为这样会有混淆的问题存在,比如auto b{a}; 到底是推断为引用还是一般变量呢?因此,使用auto推断引用类型的时候,一定要加上&。
第二个是std::boolalpha,std::boolalpha可以帮助我们在使用cout打印的时候显示出bool类型变量的关键字表示,比如这里a==b的结果为1,显示出来的就是true。
接下来我们就可以看看这段代码了,b是a的引用,b与a虽然变量名不同,但是地址相同,值相同,对a做值的更改,b中也会有相同的更改,这就是引用的大特征,我们可以用引用给一个变量起“别名”,例如这里的b就是a的别名。
引用的本质是指针常量,指针常量 (即T* const) 是:指针变量本身的值(地址)不可变,但地址对应的值可变,例如:
int a = 0;
int* const p = &a;
(*p)++; // 合法,因为a不是常量
p++; // 不合法,因为p是一个常量
假设a的地址是0x123,那么p的值只能为0x123,不能变化,但是地址0x123存储的值可以是任意的。
正是因为有了常量的特性,所以引用类型只能出现在变量定义中,一旦出现就和被引用的变量绑定,直到生存期结束为止。
在C++中的数据有左值(lvalue),将亡值(xvalue)和纯右值(prvalue) 三种,其中 将亡值(xvalue)和纯右值(prvalue)统称为右值。
简单来说,左值就是可以出现在等号(赋值操作) 左边的值,他们都是可以进行取地址操作的,例如各种变量(还有字符串字面量)。
将亡值的范围比较抽象:返回右值引用的调用表达式以及转换为右值引用的转换函数的调用表达式,关于将亡值我们在类与对象的移动语义(移动构造函数)与完美转发中还会提到。
在这里,简单而言,一切不是左值的值都是右值,例如除了字符串字面量之外的所有字面量,通过构造函数构造出的临时对象(没有被绑定到某一个变量上)。
提到左右值是因为引用的本质是一个指针常量,因此我们之前提到的所有引用都是左值引用;对于右值,按理说是取不到地址的,但是C++用了一些特殊的方法来处理。
#5.常量引用(Const Reference)常量当然是可以取到地址的,只是不能修改罢了,常量也属于左值,那这么一想,我是不是可以这么写代码:
const int a = 10;
int& b = a;
这一来b作为a的引用,就可以直接修改a的值了,这很危险啊,我只要通过引用就可以修改一个常量的值了。
当然不行!这样的代码在编译的时候是会报错的,C++的设计者当然考虑到了,不过就这样直接让常量不能具备引用,好像也不太好,所以C++中引入了常量引用实现了对于常量和字面量的引用。
const int c = 3;
const int& d = c;
const int& e = 1;
以上的三条语句都是合法的,这样就补齐了对于常量和部分右值的引用。不过在C++11中,出现了一个新的语法特性—右值引用,与之对应出现的还有移动语义(Move Semantics)和完美转发(Perfect Forwarding)。
#6.右值引用我们好像在开头的代码演示里还有这么一条语句:
int&& a = 1;
这个&&表示的意思就是右值引用,通过&&我们可以对一个右值实现引用,右值引用允许我们对右值进行一些操作,例如:
#includeusing namespace std;
int main()
{int&& a = 1;
cout<< a<< endl;
a++;
cout<< a<< endl;
return 0;
}
右值引用和常量引用都可以作为非字符串字面量的引用,他们大的区别在于,右值引用可以对其进行赋值等等操作,而常量引用不行。
#7.传引用的函数那这有什么用呢?在C++中出现的引用类型还是相当有用的,我记得好像我之前在C语言教程中那儿讲swap函数的时候就有提到:
void swap(int* a, int* b)
{int* c = *a;
*a = *b;
*b = *a;
}
还要不停地操作指针,感觉有点麻烦,有了引用之后,我们可以这么写:
void swap(int& a, int& b)
{int c = a;
a = b;
b = c;
}
简洁明了,一个指针都没有出现,就完成了交换的操作:
在调用这个函数的过程当中,两个参数都是以引用的形式传入的,我们简称为传引用(Call by Reference),而在C语言中,所以的函数都是传值(Call by Value) 的,因此对于swap函数,在C语言中必须通过指针完成。
引用的出现意义重大,我们可以自行决定一个函数是传值还是传引,这里要批评一下python,python中的函数对于传参是传值还是传引用有这样的规则:简单数据类型如int,float传值,复杂数据类型如list,class等就传引用,虽然python有它的道理,但是实际应用的过程当中还是会有不小的问题,去年年底我们小组在用python做小游戏的时候就发现有的函数会改变传入参数的值,后来才发现了这个问题。
引用类型也是可以作为函数的返回类型的,例如:
int& func(int& a)
{return a;
}
这个函数当然做不了什么事情,但是你可以这么用。不过要注意的是以下两个用法是危险的:
int& func() // 1.返回局部变量的引用
{int b = 10;
return b;
}
这个用法中返回了func函数内部定义的局部变量b的引用,但是b作为一个局部变量,在函数调用结束之后是可能会被释放掉的,这时对b的引用写入就如同往一个野指针操作,编译器不会报错,只会有警告。
第二个用法和上一个其实类似,但是函数本身没有问题:
#includeint func()
{int b = 10;
return b;
}
int main()
{int& b = func(); // 2.对返回值取引用
return 0;
}
这次不是警告了,直接编译不能通过了,VS中的提示是:非常量引用的初始值必须为左值,这里的func()函数返回的并非左值,因此我们不能对func()的返回值使用左值引用 (常量引用和右值引用是可以的),千万不要这么用。
小结基础篇的内容比较长,因此我分成上下两篇来发布,下篇中会介绍: cin与cout、constexpr、static_cast()、基于范围的for循环、new与delete、nullptr、class和被扩充的struct—C++的面向对象、函数重载
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流