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的用法:

  1. 首先在函数内声明va_list型的变量,这个变量是指向参数的指针,通过指针运算来调整访问的对象
  2. 然后用va_start宏初始化变量刚定义的va_list变量,实际上就是用va_list去指向函数的最后一个具名的参数
  3. 然后用va_arg宏返回可变的参数,va_arg的第二个参数是你要返回的参数的类型(如果函数有多个可变参数的,依次调用va_arg获取各个参数)
  4. 因为栈地址是从高到低延伸的,所以加上你要的参数类型大小,就意味着栈顶指针指向你所要的参数,便可通过底层pop得到.
  5. 最后用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() 的方法,该方法返回一个字符串,其中包含对该异常的简要说明.


  1. 用于查询某一数据类型占用字节数 ↩︎

  2. 类似于枚举,但是无法使用于switch或using语句中 ↩︎ ↩︎ ↩︎

  3. 通常为一个初始化声明语句 ↩︎

posted @ 2023-11-28 18:29  Mesonoxian  阅读(137)  评论(0编辑  收藏  举报