模块导入
可以使用最新的导入方法
import ;
但是要设置 项目/属性/"C/C++"/常规/扫描源以查找模块依赖关系
而使用头文件引入可以解决一些C语言库缺乏标头文件的问题
include
include
遇到常见的模块导入问题时,可以查询模块导入教程
常用的预处理指令
预处理指令 功能
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
}//代码效果参考:http://www.zidongmutanji.com/zsjx/17857.html
如果在其中将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
auto b = {11,22};//initializer_list
auto c{11};//int
auto d{11,22};//由于参数过多导致报错
auto e = {11,22.33};//由于元素类型不统一而报错
之所以需要在这里将其单独拿出来讨论,是因为在早先的标准中(如C++11/14),这两种方式并没有区别,都将推导出initializer_list
指派初始化器
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] 关键字查询某一数据类型占用字节数
而为了查询某一数据类型的表示范围,可以考虑使用 中的类模板 std::numeric_limits
下面给出一些例子:
//double:
std::cout<::max()<::min()<::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
//代码效果参考:http://www.zidongmutanji.com/bxxx/593278.html
export struct Employee
{
char firstInitial;
char lastInitial;
int employeeNumber;
int salary;
};
如此的一段代码存储在名为employee.ixx的文件中,再通过解决方案资源管理器将其加入源文件目录,即可通过下面的语句来访问
import employee;
类型别名
类型别名(type alias)为现有的类型声明提供新名称.
通常使用 using 语句与 typedef 语句实现
using std::vector> = vstr;
typedef vstr std::vector>;
类型别名与typedef并不完全等效,因而应该尽量使用using.
三向比较运算符
三向比较运算符, <=> ,又称飞碟运算符,其不返回布尔值,返回一个[2]类型的值,其定义于 和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]类型,还提供了is_eq(),is_neq(),is_lt(),is_lteq(),is_gt(),is_gteq()来分别解释==,!=,<,<=,>,>=
一些结构的微小变化
在C++20中,C++对一些结构添加了一些小小的特性,并且这不是标准委员会第一次这么做
就像他们在C语言的for基础上添加了初始化语句一样,这次他们将初始化语句加到了if语句和switch语句中(虽然不知道为什么他们没有为似乎更应该添加的while语句添加( ))
下面是改进后的if语句和switch语句结构
if(;)
else if()
else
switch(;)
{
}
其中的 [3] 就是初始化器
同时,modern C++同时提供了一种名为 范围for语句 的结构,通常结合STL使用
std::vectorems;
for(auto iter : ems)
std::cout<<iter.employeeNumber<<std::endl;
//遍历整个ems中的employeeNumber
同样的,在C++20中,标准委员会为C++11标准引入的范围for语句补充了一个有点尴尬的初始化器
for(;:){
}
[特性补充]
通常而言,缺失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 values{11,22,33};
auto [x,y,z] {values};
//等同于 int x=values[0],y=values[1],z=values[2];
针对std::pair的结构化绑定也是同理
std::pair 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]]:用于标记某分支可能很少/多调用