C++20高级编程 第一章 C++速成
第一章 C++速成
模块导入
可以使用最新的导入方法
import <iostream>;
但是要设置 项目/属性/"C/C++"/常规/扫描源以查找模块依赖关系
而使用头文件引入可以解决一些C语言库缺乏标头文件的问题
#include <iostream>
#include <windows.h>
遇到常见的模块导入问题时,可以查询模块导入教程
常用的预处理指令
预处理指令 | 功能 |
---|---|
#define[file] | 插入指定文件 |
#define[id][value] | 将每个指定标识符都替换 |
#ifnef #endif #ifndef[id] #endif |
使得对应代码块中的代码 有条件地包含或舍弃 |
#pragma[xyz] | 通常会显示警告或错误信息 |
名称空间
示例1
namespace mycode{
void foo()
{
return;
}
}
mycode::foo();
上面给出了一个一般性的名称空间的声明方式
同时可以注意的,名称空间也可以通过嵌套方式实现
namespace MyLibraries{
namespace Networking{
namespace FTP{
//...
}
}
}
//...
自从C++17标准后,对于上面那种嵌套名称空间的声明方法有一种等效的声明方法
namespace MyLibraries::Networking::FTP{
//...
}
通常而言这是完全等效的
对于嵌套名称空间,可以通过 inline 关键字将其中一部分子空间声明为内联名称空间
namespace MyLibraries{
inline namespace Networking{
inline namespace FTP{
//...
}
}
}
在内联名称空间中声明的所欲内容都会自动在父名称空间中可用.
例如,如果需要using上面的FTP,下面四种using方式都可以达到效果
using namespace MyLibraries;
using namespace Networking;
using namespace FTP;
using namespace MyLibraries::Networking::FTP;
using语句
对于名称空间来说,通常可以使用using语句来达到和一般函数等效的作用
using namespace mycode;
foo();
//与mycode::foo()完全等价
对于namespace,另外一个非常重要的内容是using语句的使用
using namespace mycode;
//对namespace的using,可以自动using其中的所有对象
using mycode::foo();
//对namespace里的对象的索引
using语句也可以用于名称空间的空间别名
namespace MyFTP = MyLibraries::Networking::FTP
作用域解析
作为C++程序员,需要熟悉 作用域(scope) 的概念
有时,作用域中的名称会覆盖其他作用域中相同的名称;有时,你所需的作用域不是程序中某特定行的默认作用域
class Demo
{
public:
int get(){return 5;}
}
int get(){return 10;}
namespace demo_space
{
int get(){return 15;}
}
int main
{
Demo d;
std::cout<<d.get()<<std::endl;//5
std::cout<<demo_space::get()<<std::endl;//15
std::cout<<::get()<<std::endl;//10
std::cout<<get()<<std::endl;//10
}
如果在其中将demo_space作为匿名的名称空间,则对get()的调用会引发歧义.
同理,在main函数前调用using namespace demo_space也是同理.
零初始化
通常来说,可以使用一个{0}的统一初始化器将变量初始化为0.
{},称为零初始化器.
- 整数: 零初始化器会将原始的整数类型初始化为0,
- 浮点数: 将原始的浮点类型给初始化为0.0,
- 指针: 将指针类型初始化为nullptr.
下列是使用零初始化器的例子:
float myFloat {};
int myInt{};
char* myChar_ptr{};
通常而言,该过程等价于
float myFloat = 0.0;
int myInt = 0;
char* myChar_ptr = nullptr;
统一初始化
在C++11前,各类型的初始化并非总是统一的.例如内容可能一致的结构体与类:
circleStruct my_circle1 = {10,10,2.5};
circleClass mycircle2(10,10,2.5);
自C++11后,允许一律使用 {...} 语法初始化类型:
circleStruct my_circle3 = {10,10,2.5};
circleClass mycircle4 = {10,10,2.5};
统一初始化还可以用于对变量进行零初始化
int e = {};
使用统一初始化的一个好处是可以避免 窄化(narrowing) :
当使用统一初始化时,出现窄化情景会导致编译错误.
int pi = 3.14;
//这是合法的,3.14隐式窄化为int型的3
int pi {3.14};
//这是非法的,编译器会报错
统一初始化还可以用于初始化动态分配的数组:
int* arr = new int[4]{0,1,2,3};
int* new_arr = new int[] {0,1,2,3};
//自C++20标准后,甚至可以省略数组大小
事实上,在C++中还有两种使用大括号初始化列表的初始化方式:
- 拷贝列表初始化: T obj = {arg1,arg2,...}
- 直接列表初始化: T obj {arg1,arg2,...}
这两种初始化方式在C++17后与自动类型推断,初始化列表结合后,存在一些较大差异
auto a = {11};//initializer_list<int>
auto b = {11,22};//initializer_list<int>
auto c{11};//int
auto d{11,22};//由于参数过多导致报错
auto e = {11,22.33};//由于元素类型不统一而报错
之所以需要在这里将其单独拿出来讨论,是因为在早先的标准中(如C++11/14),这两种方式并没有区别,都将推导出initializer_list<int>
指派初始化器
C++20引入了 指派初始化器 ,以使用它们名称初始化所谓聚合的数据成员.
- 拥有类内初始化器的数据成员会得到该值
- 没有类内初始化器的数据成员会被零初始化
struct Employee{
char firstInitial;
char lastInitial;
int employeeNumber;
int salary;
};
//使用统一初始化语法
Employee em1 {'J','D',42,80000};
//使用指派初始化器
Employee em2{
.firstInitial = 'J',
.iastInitial = 'D',
.employeeNumber = 42,
.salary = 80000
};
使用指派初始化器的好处是,如果你对某些成员的默认值感到满意,你可以跳过它们的初始化--而统一初始化语法无法做到.且当新的数据成员被添加进数据结构后,它将继续起作用.(当然,这样显然也引入了潜在的隐患)
数据类型的bytes
类型 | 大小(bytes) |
---|---|
(signed)int | 4 |
(signed)short(int) | 2 |
(signed)long(int) | 4 |
(singed)long long(int) | 8 |
unsigned int | 4 |
unsigned short(int) | 2 |
unsigned long(int) | 4 |
unsigned long long(int) | 8 |
float | 4 |
double | 8 |
long double | 8 |
char | 1 |
unsigned char | 1 |
signed char | 1 |
char8_t | 1 |
char16_t | 2 |
char32_t | 4 |
wchar_t | 2 |
bool | 1 |
类型字节数查询
通常而言,可以使用 [1] 关键字查询某一数据类型占用字节数
而为了查询某一数据类型的表示范围,可以考虑使用 <limits> 中的类模板 std::numeric_limits
下面给出一些例子:
//double:
std::cout<<std::numeric_limits<double>::max()<<std::endl;
std::cout<<std::numeric_limits<double>::min()<<std::endl;
std::cout<<std::numeric_limits<double>::lowest()<<std::endl;
以上代码的运行结果如下:
1.79769e+308
//max double value 最大正数
2.22507e-308
//min double value 最小正数
-1.79769e+308
//lowest double value 最小的数值
特殊数的处理与使用
通常来说,在某些特殊的运算中,会出现一些难以处理的数,例如:
- 无穷: +/-infinity,表示正负无穷
- 非数: NaN,通常出现在出现未定式0/0,inf/inf时
为了处理这类数,有以下在
- std::isnan(): 判断一个数是否为非数字
- std::isinf(): 判断一个数是否为无穷
为了获得这类数,可以使用在numeric_limits
- numeric_limits
::infinity() : 一个获得无穷用的函数 - numeric_limits
::quite_NaN() : 一个获得非数的函数
枚举数据类型
整数代表某个数字序列中的值,枚举类型允许你定义你自己的序列,这样你就能使用这个序列
举例,在一个国际象棋程序中,你可以用int代表所有的棋子
const int PieceTypeKing{0};
const int PieceTypeQueen{1};
const int PieceTypeRookP{10};
const int PieceTypePawn{11};
int myPiece{PieceTypeKing};
然而如此的表示法存在一定风险,因为棋子只是一个int,如果另一个程序增加棋子的值,那么会发生什么?
因而为了限制变量的取值范围与操作,c++给出了枚举类型供操作
例如,为了表示上述的棋子:
enum class PieceType
{
King =1,
Queen,
Rook=10,
Pawn
};
不难发现,其实该代码与上面是等价的
虽然对应会有整数值,但是它并不会自动转换为整数,例如:
if(PieceType::Queen==2)
//this will never true
默认情况枚举类型都为整型,但是可以通过显式方式加以改变
enum class PieceType : unsigned long long
{
King=1,
Queen,
Rook=10,
Pawn
};
在使用枚举类时,通常需要按类似于名称空间的方式来对待:
PieceType piece{PieceType::King};
//before using
using enum PieceType;
PieceType piece{King};
if(piece==PieceType::King)
//always true
值得一提的,using enum声明从C++20标准才开始实现
在visual studio中,目前的标准使用.ixx后缀的文件来标识模块接口文件
为了能够提供自己的模块,应当使用export关键字与module关键字,例如:
export module employee;
//offer a export of this module
export struct Employee
{
char firstInitial;
char lastInitial;
int employeeNumber;
int salary;
};
如此的一段代码存储在名为employee.ixx的文件中,再通过解决方案资源管理器将其加入源文件目录,即可通过下面的语句来访问
import employee;
类型别名
类型别名(type alias)为现有的类型声明提供新名称.
通常使用 using 语句与 typedef 语句实现
using std::vector<std::basic_string<char>> = vstr;
typedef vstr std::vector<std::basic_string<char>>;
类型别名与typedef并不完全等效,因而应该尽量使用using.
三向比较运算符
三向比较运算符, <=> ,又称飞碟运算符,其不返回布尔值,返回一个[2]类型的值,其定义于 <compare> 和std名称空间中
三向比较运算符返回的[2:1]类型根据操作数的不同而不同,最主要的有强序(strong_ordering),偏序(partial_ordering)和弱序(weak_ordering)
其中,强序对应整型,偏序对应浮点型,弱序一般用于用户自定义的类型.
strong_ordering
- strong_ordering::less
- strong_ordering::greater
- strong_ordering::equal
partial_ordering
- partial_ordering::less
- partial_ordering::greater
- partial_ordering::equalvalent
- partial_ordering::unordered
weak_ordering
- weak_ordering::less
- weak_ordering::greater
- weak_ordering::equal
使用三向比较运算符的一个好处在于,重载一个三向比较运算符后,相应的>,>=,<,<=都会相应重载.
为了配合以上几种[2:2]类型,<compare>还提供了is_eq(),is_neq(),is_lt(),is_lteq(),is_gt(),is_gteq()来分别解释==,!=,<,<=,>,>=
一些结构的微小变化
在C++20中,C++对一些结构添加了一些小小的特性,并且这不是标准委员会第一次这么做
就像他们在C语言的for基础上添加了初始化语句一样,这次他们将初始化语句加到了if语句和switch语句中(虽然不知道为什么他们没有为似乎更应该添加的while语句添加( ))
下面是改进后的if语句和switch语句结构
if(<initializer>;<conditional_expression>)
<if_body>
else if(<else_if_expression>)
<else_if_body>
else
<else_body>
switch(<initializer>;<expression>)
{
<body>
}
其中的 [3] 就是初始化器
同时,modern C++同时提供了一种名为 范围for语句 的结构,通常结合STL使用
std::vector<Employee>ems;
for(auto iter : ems)
std::cout<<iter.employeeNumber<<std::endl;
//遍历整个ems中的employeeNumber
同样的,在C++20中,标准委员会为C++11标准引入的范围for语句补充了一个有点尴尬的初始化器
for(<initializer>;<for-range-declaration>:<for-range-initializer>){
<body>
}
[特性补充]
通常而言,缺失break语句的case都会引发警告
C++20为了适应特应情况,提供了 [[fallthrough]] 特性switch(mode){ case Mode::Custom: value=84; [[fallthrough]]//避免了来自编译器的警告 case Mode::Standard: case Mode::Default: //do something break; }
类型推导相关
auto关键字
在古早的C++标准时代,auto就作为一种存储类型标记符出现,而C++11后,auto作为一个自动推导类型的类型指示符应用于C++的各个相关场景中
auto与auto:在声明指针时,auto与auto没有区别
auto与auto&:在声明引用时,必须写为auto&
auto与const auto:在声明常数时,必须写为const auto
当利用auto在同一行定义多个变量时,这些变量的类型必须相同,编译器是根据第一个变量的表达式进行推导的,然后根据推导出来的变量类型定义其他变量.如果不相同,编译器就会报错.
auto a = 1, b = 2;
//正确的
auto c = 1, d = 1.1;
//将会导致错误
结构化绑定允许声明多个变量,这些变量使用std::array,struct,std::pair或者元组中的元素初始化
例如:
std::array<int,3> values{11,22,33};
auto [x,y,z] {values};
//等同于 int x=values[0],y=values[1],z=values[2];
针对std::pair的结构化绑定也是同理
std::pair<std::string,int> my_pair{"string",5};
auto [the_str,the_int] {my_pair};
//等同于std::string the_str = my_pair.first();int the_int = my_pair.second();
若使用auto&或者const auto&代替auto,还可以通过结构化绑定创建一组非const引用或const引用.
auto关键字的主要应用情景
- 范围for语句中的迭代器
- 函数返回值类型的自动推导
- 结构化绑定
decltype关键字
关键字 decltype 将表达式作为实参,计算出该表达式的类型.例如:
int x{123};
decltype(x) y{456};
其中decltype起到了为y提供类型int的作用
当使用template进行相关设计时,decltype的作用将进一步显现
const的用法
const修饰类型
在C++中,鼓励程序员使用 const 取代 #define 定义常量,使用const定义常量就像定义变量一样,只是编译器保证代码不会改变这个值.例如:
const int NUM {1};
const std::string STR {"temp string"};
const double PI{3.14159265};
const与指针
当变量通过指针包含一层或者多层简介时,应用const将变得棘手.
通常来说,const与指针结合时,有两种基本用法:
int n = 10;
const int* ip{&n};
int const* pi{&n};
//上述两种写法将导致你无法通过指针更改n的值
int* const cp{&n};
//上面的写法将导致你无法修改cp所指向的对象
当然,你也可以将二者组合起来,这个组合可以拓展到任意级别的间接等级
const int* const ip{&n};
//结合了二者,现在既不能修改值也不能修改指向对象
const int* const * const* const cp{&n};
//这个规则可以拓展到任意级别的间接等级
const方法
const关键字的第二个用途是将类方法标记为const,以防止它们修改类的数据成员.
export class AirlineTicket
{
public:
double calculatePriceInDollars()const;
std::string getPassengerName()const;
void setPassengerName(std::string name);
int getNumberOfMiles()const;
void setHasEliteSuperRewardsStatus(bool status);
private:
int m_numberOfMiles{0};
std::string m_passengerName{"Unknown Passenger"};
}
std::string AirlineTicket::getPassengerName()const
{
return m_passengerName;
}
注意:
为了遵循const-correctness原则,建议将不改变对象的任何数据成员的成员函数声明为const.与非const成员函数也被称为赋值函数(mutator)相对,这些成员函数也称为检查器(inspector)
constexpr与consteval关键字
C++一直有常量表达式的概念,即在某些情况下,必须使用常量表达式.例如,定义数组时数组的大小必须为常量表达式.
使用 constexpr 关键字,可以将函数作为常量表达式使用.
当一个函数被声明为constexpr函数时,其内部将只能调用constexpr函数而不允许再调用其他非constexpr函数.
constexpr int getArraySize(){return 32;}
int main()
{
int myArray[getArraySize()+1];
}
不过需要注意的,constexpr函数 可以 在编译期执行,但是无法保证 一定 在编译器执行.
constexpr double inchToMm(double inch){return inch*25.4;}
constexpr double const_inch{6.0};
constexpr double mml{inchToMm(const_inch)};
//如此调用,函数将在编译器对函数求值
double const_inch{6.0};
double mml{inchToMm(const_inch)};
//如此调用,函数将在运行期对函数求值!
如果确实希望保证始终在编译期对函数进行求值,则需要使用C++20的 consteval 关键字将函数转换为所谓的立即函数(immediate function),例如
consteval double inchToMm(double inch){return inch*25.4;}
现在,对其的第一次调用仍然可以通过正常编译,并且可以在编译期进行求值.但是第二个调用现在会导致编译错误,因为无法在编译器对其求值.
函数相关的内容
众所周知的,每个函数都有一个预定义的局部变量 __func__ ,其中包含当前函数的名称
通过std::cout便能输出
std::cout<<__func__<<std::endl;
属性是一种将可选的和/或特定于编译器厂商的信息添加到源代码中的机制.
自C++11标准后,属性的格式统一为 [[attribute]],
前文中提到的[[fallthrough]]便是一种用于switch语句的属性.
常见的属性
- [[nodiscard]]:若函数未对返回值进行任何处理,由编译器抛出警告
- [[maybe_unused]]:用于禁止编译器在未使用某些内容时发出警告
- [[noreturn]]:用于显式表明函数永远不会将控制权返回调用点
- [[deprecated]]:用于标记某些内容已弃用,当被调用时通过编译器反馈
- [[likely]]与[[unlikely]]:用于标记某分支可能很少/多调用
va_list与不定形参函数
va_list实际上是在C语言中用于解决不定量形参的一组宏
VA_LIST的用法:
- 首先在函数内声明va_list型的变量,这个变量是指向参数的指针,通过指针运算来调整访问的对象
- 然后用va_start宏初始化变量刚定义的va_list变量,实际上就是用va_list去指向函数的最后一个具名的参数
- 然后用va_arg宏返回可变的参数,va_arg的第二个参数是你要返回的参数的类型(如果函数有多个可变参数的,依次调用va_arg获取各个参数)
- 因为栈地址是从高到低延伸的,所以加上你要的参数类型大小,就意味着栈顶指针指向你所要的参数,便可通过底层pop得到.
- 最后用va_end宏结束可变参数的获取,即清空va_list
typedef char * va_list;
// TC中定义为void*
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
//为了满足需要内存对齐的系统
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
//ap指向第一个变参的位置,即将第一个变参的地址赋予ap
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
//获取变参的具体内容,t为变参的类型,如有多个参数,则通过移动ap的指针来获得变参的地址,从而获得内容
#define va_end(ap) ( ap = (va_list)0 )
//清空va_list,即结束变参的获取
一个实际例子:
void LogEvent::format(const char* fmt, ...)
{
va_list al;
va_start(al, fmt);
format(fmt, al);
va_end(al);
}
void LogEvent::format(const char* fmt, va_list al)
{
char* buf = nullptr;
//vasprintf:可以通过可变参数创建一个格式化的字符串,并将其存储在动态分配的内存中
//其对参数的处理方式与printf相同
int len = vasprintf(&buf, fmt, al);
if(len != -1) {
m_ss << std::string(buf, len);
free(buf);
}
}
初始化列表
初始化列表在 <initializer_list> 头文件中定义,其与va_list一样可以处理不定数量形参问题
且其使用方法非常简单明了:
import <initializer_list>;
int makeSum(std::initializer_list<int>values)
{
int total{0};
for(auto value:values)
total+=value;
return total;
}
//makeSum()函数拥有处理不定量int型参数的类型
int a{makeSum({1,2,3})};
int b{makeSum({1,2,3,4,5})};
//makeSum()函数的使用方式
一些STL相关内容介绍
CTAD(类模板参数推导):通常而言,大部分STL容器支持使用初始化器时进行类模板参数推导,例如:
std::vector v{11,22};
//与std::vector<int>v {11,22};等价
std::array
固定大小且不会被转换为指针的数组型对象.定义在 <array> 中.
其声明的格式为:
std::array<[type],[size]> arr;
如同其他的STL容器一样,array可以通过迭代器范围,这意味着可以对其使用范围for语句
for(auto iter:arr)
do_something(iter);
std::vector
向量提供了一种动态的容器,支持随机访问与首尾删除.定义在 <vector> 中.
vector一般通过 std::vector::push_back() 和 std::vector::pop_back() 来向其中添加或移除元素,
也支持通过 std::vector::operator[] 和 std::vector::erase() 来对其中元素进行随机访问与特定位元素删除.
std::vector<int>v;
v.push_back(11);
v.push_back(22);
std::cout<<v[0];//11
v.pop_back();
std::cout<<*(--v.end());//11
std::pair
pair将两个不同类型的数据组合在一起,可通过 std::pair::first() 与 std::pair::second() 访问这些值.定义在 <utility> 中.
std::pair<double,int> p{1.2,1};
std::optional
optional保留特定类型的值,或者不包含任何值.定义在 <optional> 中.
当optional不包含任何值时,其值与 std::nullopt 相同.
可以通过使用 std::has_value() 来判断optional是否有值,通过 std::optional::value() 或者 std::optional::value_or() 来对optional解引用.当对std::nullopt使用std::value()时,会抛出 std::bad_optional_access 异常.
std::optional<int>data1 {42};
std::optional<int>data2 {std::nullopt};
std::cout<<data1.value();//42
std::cout<<data2.value();//throw std::bad_optional_access
std::cout<<data2.value_or(41);//41
异常机制
C++是一种非常灵活的语言,但是并不是非常安全. 异常(exceptions) 机制就是试图增加一个安全等级的语言特性.
当某段代码检测到异常时,就会 抛出(throw) 一个异常.而另一段代码会 捕获(catch) 这个异常并执行恰当的操作.在 <stdexcept> 中,有许多标准异常.
double divideNumbers(double numerator, double denominator)
{
if(denominator==0)
throw invalid_argument{"Denominator cannot be 0."};
return numerator/denominator;
}
try{
cout<<"divideNumbers(2.5,0.5)"<<endl;
cout<<"divideNumbers(2.3,0)"<<endl;
cout<<"divideNumbers(4.5,2.5)"<<endl;
}
catch(const invalid_argument& exception){
cout<<format("Exception caught: {}",exception.what())<<endl;
}
所有标准库异常类中都有一个叫 what() 的方法,该方法返回一个字符串,其中包含对该异常的简要说明.