——指针、动态数组、const、constexpr和consteval)
目录1. 指针和动态数组1.1 栈和自由存储区1.2 使用指针1.3 动态分配的数组1.4 空指针常量2. const2.1 const修饰类型2.2 const与指针2.3 使用const保护参数2.4 const方法(建议3. constexpr4. consteval参考1. 指针和动态数组动态内存允许所创建的程序具有在编译期大小可变的数据大多数复杂程序都会以某种方式使用动态内存。1.1 栈和自由存储区C程序中的内存分为两部分——栈和自由存储区。将栈可视化的一种方式就是将其看作一幅纸牌当前顶部的牌代表程序的当前作用域通常是正在执行的函数。当前函数中声明的所有变量将占用顶部栈帧的内存。如果当前函数foo()调用了另一个函数bar()一张新牌就会被放在牌堆上面这样bar()就会拥有自己的栈帧供其运行。任何从foo()传递给bar()的参数都会从foo()栈帧复制到bar()栈帧。栈帧很好因为它为每个函数提供了独立的内存空间。如果在foo()栈帧中声明了一个变量那么除非专门要求否则调用bar()函数不会更改该变量。此外foo()函数执行完毕时栈帧就会消失该函数中声明的所有变量都不会再占用内存。在栈上分配内存的变量不需要由程序员释放内存这个过程是自动完成的。自由存储区是与当前函数或栈帧完全独立的内存区域。如果想在函数调用结束之后仍然保存其中声明的变量可以将变量放到自由存储区中。自由存储区的结构不如栈复杂可以将它当作一堆位。程序可在任何时候向其中添加新的位或修改已有的位。必须确保释放在自由存储区上分配的任何内存这个过程不会自动完成除非使用了智能指针。警告这里介绍指针是因为你将会遇到它们尤其是在遗留代码中。但是在新代码中仅在不涉及所有权的情况下才允许使用此类原始/裸指针。1.2 使用指针可以通过显式分配内存的方式将任何东西放到自由存储区中。例如要将一个整数放在自由存储区中需要为其分配内存但是首先需要声明一个指针int* myIntegerPointer;int类型后面的*表示所声明的变量引用/指向某个整数内存。可将指针看作指向动态分配自由存储区中内存的一个箭头它还没有指向任何内容因为你还没有把它指派给任何内容它是一个未初始化的变量。在任何时候都应避免使用未初始化的变量 尤其是未初始化的指针因为它们会指向内存中的某个随机位置。使用这种指针很可能使程序崩溃。这就是总是应同时声明和初始化指针的原因。如果不希望立即分配内存可以把它们初始化为空指针nullptr。int* myIntegerPointer { nullptr };空指针是一个特殊的默认值有效的指针都不含该值在布尔表达式中使用时会被转换成false。if(!myIntegerPointer){/* myIntegerPointer is a null pointer. */}使用new操作符分配内存myIntegerPointer new int;在此情况下指针指向一个整数值的地址。为访问这个值需要对指针解引用。可将解引用看作沿着指针箭头寻找自由存储区中实际的值。为给自由存储区中新分配的整数赋值可采用如下代码*myIntegerPointer 8;注意这并非将myIntegerPointer的值设置为8在此并没有改变指针而是改变了指针所指的内存。如果真要重新设置指针的值它将指向内存地址8这可能是一个随机的无用内存单元最终会导致程序崩溃。使用完动态分配的内存后需要使用delete操作符释放内存。为防止在释放指针所指的内存后再使用指针建议将指针设置为nullptr。deletemyIntegerPointer;myIntegerPointernullptr;警告在解引用之前指针必须有效。对null或未初始化的指针解引用会导致未定义的行为。程序可能崩溃也可能继续运行却给出奇怪的结果。指针并非总是指向自由存储区内存可声明一个指向栈中变量甚至指向其他指针的指针。为让指针指向某个变量需要使用取址运算符。inti{8};int*myIntegerPointer{i};// points to the variable with the value 8.C使用特殊语法处理指向结构体或类的指针。从技术上讲如果指针指向某个结构体或类可以首先用*对指针解引用然后使用普通的.语法访问其中的字段如下面的代码所示在此假定存在一个名为getEmployee()的函数它返回一个指向Employee实例的指针。Employee*anEmployee{getEmployee()};std::cout(*anEmployee).salary\n;此语法有一点混乱。-运算符允许同时对指针解引用并访问字段。下面的代码与前面的代码等效但阅读起来更方便。Employee*anEmployee{getEmployee()};std::coutanEmployee-salary\n;逻辑短路可与指针一起使用以免使用无效指针如下所示。bool isValidSalary { ( anEmployee anEmployee-salary 0 ) };或者稍微详细一点bool isValidSalary { ( anEmployee ! nullptr anEmployee-salary 0 ) };仅当anEmployee有效时才对其进行解引用以获取salary。如果它是一个空指针则逻辑运算短路不再解引用anEmployee指针。1.3 动态分配的数组自由存储区也可以用于动态分配数组。使用new[]操作符可给数组分配内存intarraySize{8};int*myVariableSizedArray{newint[arraySize]};这条语句分配足够的内存用于存储arraySize个整数。下图展示了执行这条语句后栈和自由存储区的情况。可以看到指针变量仍在栈中当动态创建的数组在自由存储区中。现在已经分配了内存可将myVarialbeSizedArray当作基于栈的普通数组使用。myVariableSizedArray[3] 2;使用完这个数组后应该将其从自由存储区中删除这样其他变量就可以使用这块内存。在C中可使用delete[]操作符完成这一任务。delete[]myVariableSizedArray;myVariableSizedArraynullptr;delete后的方括号表明所删除的是一个数组注意避免使用C中的malloc()和free()而使用new和delete或者使用new[]和delete[]。警告在C中每次调用new时都必须相应地调用delete每次调用new[]时都必须相应地调用delete[]以避免内存泄漏。如果未调用delete或delete[]或调用不匹配会导致内存泄漏。1.4 空指针常量在C11之前常量NULL用于表示空指针。NULL只是简单地定义为常量0这会导致一些问题。分析下面的例子voidfunc(inti){std::coutfunc(int)\n;}intmain(){func(NULL);}这段代码定义了一个func()函数它有一个整型参数。main()函数通过参数NULL调用func()NULL被当作一个空指针常量。但是NULL不是指针而等价于整数0所以实际调用的是func(int)。这可能不是预期的行为因此有些编译器会给出警告。可引入真正的空指针常量nullptr来解决这个问题。下面的代码使用了真正的空指针并且导致了编译错误因为我们没有重载参数为指针的func()版本。func(nullptr);2. const在C中有很多方法使用const关键字。所有用法都是相关的但存在微妙的差别。基本上const是constant的缩写它表示某些内容保持不变。编译器通过将任何试图将其更改的行为标记为错误用来保证此要求。此外启用优化后编译器可以利用此知识生成更好的代码。2.1 const修饰类型如果已经认为关键字const与常量有一定关系就正确地揭示了它的一种用法。在C语言中程序员经常使用预处理器的#define机制声明一个符号名称其值在程序执行时不会变化如版本号。在C中鼓励程序员使用const取代#define定义常量。使用const定义常量就像定义变量一样只是编译器保证代码不会改变这个值。实例如下constintversionNumberMajor{2};constintversionNumberMinor{1};conststd::string productName{Super Hyper Net Modulator};constdoublePI{3.141592653589793238462};可以将任何变量标记为const包括全局变量和类中的数据成员。2.2 const与指针当变量通过指针包含一层或多层间接时应用const将变得棘手。考虑以下代码int*ip{nullptr};ipnewint[10];ip[4]5;假设你决定对ip使用const。暂时不要考虑这样做的用处考虑它意味着什么。你要是阻止ip变量本身被更改还是要阻止其指向的值被更改也就是说你要阻止第二行还是第三行为了防止指向的值被修改可以用下面这种方式将const添加到ip的声明中。constint*ip{nullptr};ipnewint[10];ip[4]5;// does not compile!现在你无法修改ip指向的值。一种替代的但在语义上等效的书写方式如下intconst*ip{nullptr};ipnewint[10];ip[4]5;// does not compile!将const放在int之前还是之后在功能上没有区别。如果想将ip本身标记为const而不是它指向的值需要这样写int*constip{nullptr};ipnewint[10];// does not compile!ip[4]5;// error: dereferencing a null pointer.现在ip本身无法更改编译器要求你在声明它时对其进行初始化可以使用如先前代码中的nullptr或如下所示的新分配的内存。int*constip{newint[10]};ip[4]5;也可以像下面这样将指针本身和指针所指的值都标记为const。int const* const ip { nullptr };这是另一种等效的写法const int* const ip { nullptr };尽管此语法可能看起来令人困惑但实际上存在一个简单的规则const关键字作用于其直接左侧的内容。再次考虑这一行int const* const ip { nullptr };从左到右第一个const直接位于单词int的右侧因此它适用于ip指向的int指定你不能更改IP指向的值。第二个const直接位于*的右侧因此它适用于指向int的指针该指针是ip变量指定你不能更改ip指针本身。该规则令人困惑的原因是一个例外。也就是第一个const可以放在变量之前如下所示。const int* const ip { nullptr };这种例外语法比其他语法更常遇到。可以将这个规则扩展到任意级别的间接级别正如以下示例const int* const* const* const ip { nullptr };该声明中存在3个*表明这是一个三级指针从右到左第一个const直接位于*的右边表明第三级指针ip是常量不能修改它指向的地址第二个const直接位于*的右边表明第二级指针*ip是常量不能修改它指向的地址第三个const直接位于*的右边表明第一级指针**ip是常量不能修改它指向的地址第四个const(指的是开头第一个const int*的这个const)表明**ip解引用后的值为常量不能修改。2.3 使用const保护参数在C中可将非const变量转换为const变量。为什么想这样做呢这提供了一定程度的保护防止其他代码修改变量。如果你调用同事编写的一个函数并且想确保这个函数不会传递改变给它的实参可以告诉同事让函数采用const参数。如果这个函数试图改变参数的值就不会让编译通过。在下面的代码中调用mysteryFunction()时string*自动转换为const string*。如果编写mysteryFunction()的人员试图修改所传递字符串的值代码将无法编译。有绕过这个限制的方法但是需要有意识地这么做C只是阻止无意义地修改const变量。voidmysteryFunction(conststd::string*someString){*someStringTest;// will not compile.}intmain(){std::string myString{The string};mysteryFunction(myString);}还可以在原始类型参数上使用const以防止在函数体中意外修改它们。例如以下函数具有const整型参数。在函数体中无法修改整数param。如果尝试对其修改则编译器将生成错误。voidfunc(constintparam){/* not allowed to change param... */}2.4 const方法(建议const关键字的第二个用途是将类方法标记为const以防止它们修改类的数据成员。可以修改前面介绍的AirlineTicket类以将所有只读方法标记为const。如果任何const方法尝试修改AirlineTicket数据成员之一则编译器将提示错误。exportclassAirlineTicket{public:doublecalculatePriceInDollars()const;std::stringgetPassengerName()const;voidsetPassengerName(std::string name);intgetNumberOfMiles()const;voidsetNumberOfMiles(intmiles);boolhasEliteSuperRewardsStatus()const;voidsetHasEliteSuperRewardsStatus(boolstatus);private:std::string m_passengerName{Unknown Passenger};intm_numberOfMiles{0};boolm_hasEliteSuperRewardsStatus{false};};// all methods omitted...注意为了遵循const-correctness原则建议将不改变对象的任何数据成员的成员函数声明为const。与非const成员函数也被称为赋值函数相对这些成员函数也称为检查器。3. constexprC中一直有常量表达式的概念即在编译器求值的表达式。在某些情况下必须使用常量表达式。例如定义数组时数组的大小需要为常量表达式。由于此限制以下代码在C中无效。constintgetArraySize(){return32;}intmain(){std::arrayint,getArraySize()myArray{};// invalid in C.}使用constexpr关键字getArraySize()函数可以被重定义允许在常量表达式中调用它。constexprintgetArraySize(){return32;}intmain(){std::arrayint,getArraySize()myArray{};// ok.}你甚至可以这样做int myArray[getArraySize() 1]; // ok.将函数声明为constexpr对函数的功能施加了很多限制因为编译器必须能够在编译期对函数求值。例如允许constexpr函数调用其他constexpr函数但不允许调用任何非constexpr函数。这样的函数不允许有任何副作用也不能引发任何异常。通过定义constexpr构造函数可以创建用户自定义类型的常量表达式变量。与constexpr函数一样constexpr类也有很多限制。下面的Rect类定义了constexpr构造函数他还定义了执行一些计算的constexpr getArea()方法。classRect{public:constexprRect(std::size_t width,std::size_t height):m_width{width},m_height{height}{}constexprstd::size_tgetArea()const{returnm_width*m_height;}private:std::size_t m_width{},m_height{};}使用这个类声明constexpr对象是非常容易的。constexprRect r{8,2};std::arrayint,r.getArea()myArray{};// ok.4. consteval上一节讨论的constexpr关键字指定函数在编译期执行但不能保证一定在编译期执行。采用以下constexpr函数constexprdoubleinchToMm(doubleinch){returninch*25.4;}如果按以下方式调用则会在需要时在编译期对函数求值。constexprdoubleconst_inch{6.0};constexprdoublemml{inchToMm(const_inch)};// at compile time.然而如果按以下方式调用函数将不会在编译期被求值而是在运行时。doubledynamic_inch{8.0};doublemm2{inchToMm(dynamic_inch)};// at run time.如果确实希望保证始终在编译期对函数进行求值则需要使用C20的consteval关键字将函数转换为所谓的立即函数。可以按照如下方式更改inchToMm()函数constevaldoubleinchToMm(doubleinch){returninch*25.4;}参考[比] 马克·格雷戈勒著 程序喵大人 惠惠 墨梵 译 C20高级编程(第五版)