目录
- 1. 指针和动态数组
- 1.1 栈和自由存储区
- 1.2 使用指针
- 1.3 动态分配的数组
- 1.4 空指针常量
- 2. const
- 2.1 const修饰类型
- 2.2 const与指针
- 2.3 使用const保护参数
- 2.4 const方法(建议)
- 3. constexpr
- 4. 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。
delete myIntegerPointer;
myIntegerPointer = nullptr;
警告:在解引用之前指针必须有效。对null或未初始化的指针解引用会导致未定义的行为。程序可能崩溃,也可能继续运行,却给出奇怪的结果。
指针并非总是指向自由存储区内存,可声明一个指向栈中变量甚至指向其他指针的指针。为让指针指向某个变量,需要使用取址运算符&。
int i { 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::cout << anEmployee->salary << "\n";
逻辑短路可与指针一起使用,以免使用无效指针,如下所示。
bool isValidSalary { ( anEmployee && anEmployee->salary > 0 ) };
或者稍微详细一点:
bool isValidSalary { ( anEmployee != nullptr && anEmployee->salary > 0 ) };
仅当anEmployee有效时,才对其进行解引用以获取salary。如果它是一个空指针,则逻辑运算短路,不再解引用anEmployee指针。
1.3 动态分配的数组
自由存储区也可以用于动态分配数组。使用new[]操作符可给数组分配内存:
int arraySize { 8 };
int* myVariableSizedArray { new int[arraySize] };
这条语句分配足够的内存,用于存储arraySize个整数。下图展示了执行这条语句后栈和自由存储区的情况。可以看到指针变量仍在栈中,当动态创建的数组在自由存储区中。
现在已经分配了内存,可将myVarialbeSizedArray当作基于栈的普通数组使用。
myVariableSizedArray[3] = 2;
使用完这个数组后,应该将其从自由存储区中删除,这样其他变量就可以使用这块内存。在C++中,可使用delete[]操作符完成这一任务。
delete[] myVariableSizedArray;
myVariableSizedArray = nullptr;
delete后的方括号表明所删除的是一个数组!
注意:避免使用C中的malloc()和free(),而使用new和delete,或者使用new[]和delete[]。
警告:在C++中,每次调用new时,都必须相应地调用delete;每次调用new[]时,都必须相应地调用delete[],以避免内存泄漏。如果未调用delete或delete[],或调用不匹配,会导致内存泄漏。
1.4 空指针常量
在C++11之前,常量NULL用于表示空指针。NULL只是简单地定义为常量0,这会导致一些问题。分析下面的例子:
void func(int i) {
std::cout << "func(int)" << "\n";
}
int main() {
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定义常量就像定义变量一样,只是编译器保证代码不会改变这个值。实例如下:
const int versionNumberMajor { 2 };
const int versionNumberMinor { 1 };
const std::string productName { "Super Hyper Net Modulator" };
const double PI { 3.141592653589793238462 };
可以将任何变量标记为const,包括全局变量和类中的数据成员。
2.2 const与指针
当变量通过指针包含一层或多层间接时,应用const将变得棘手。考虑以下代码:
int* ip { nullptr };
ip = new int[10];
ip[4] = 5;
假设你决定对ip使用const。暂时不要考虑这样做的用处,考虑它意味着什么。你要是阻止ip变量本身被更改,还是要阻止其指向的值被更改?也就是说,你要阻止第二行还是第三行?
为了防止指向的值被修改,可以用下面这种方式将const添加到ip的声明中。
const int* ip { nullptr };
ip = new int[10];
ip[4] = 5; // does not compile!
现在,你无法修改ip指向的值。一种替代的但在语义上等效的书写方式如下:
int const* ip { nullptr };
ip = new int[10];
ip[4] = 5; // does not compile!
将const放在int之前还是之后在功能上没有区别。
如果想将ip本身标记为const,而不是它指向的值,需要这样写:
int* const ip { nullptr };
ip = new int[10]; // does not compile!
ip[4] = 5; // error: dereferencing a null pointer.
现在,ip本身无法更改,编译器要求你在声明它时对其进行初始化,可以使用如先前代码中的nullptr或如下所示的新分配的内存。
int* const ip { new int[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表明**ip解引用后的值为常量,不能修改。
2.3 使用const保护参数
在C++中,可将非const变量转换为const变量。为什么想这样做呢?这提供了一定程度的保护,防止其他代码修改变量。如果你调用同事编写的一个函数,并且想确保这个函数不会传递改变给它的实参,可以告诉同事让函数采用const参数。如果这个函数试图改变参数的值,就不会让编译通过。
在下面的代码中,调用mysteryFunction()时string自动转换为const string。如果编写mysteryFunction()的人员试图修改所传递字符串的值,代码将无法编译。有绕过这个限制的方法,但是需要有意识地这么做,C++只是阻止无意义地修改const变量。
void mysteryFunction(const std::string* someString) {
*someString = "Test"; // will not compile.
}
int main() {
std::string myString { "The string" };
mysteryFunction(&myString);
}
还可以在原始类型参数上使用const,以防止在函数体中意外修改它们。例如,以下函数具有const整型参数。在函数体中,无法修改整数param。如果尝试对其修改,则编译器将生成错误。
void func(const int param) {
/* not allowed to change param... */
}
2.4 const方法(建议)
const关键字的第二个用途是将类方法标记为const,以防止它们修改类的数据成员。可以修改前面介绍的AirlineTicket类,以将所有只读方法标记为const。如果任何const方法尝试修改AirlineTicket数据成员之一,则编译器将提示错误。
export class AirlineTicket {
public:
double calculatePriceInDollars() const;
std::string getPassengerName() const;
void setPassengerName(std::string name);
int getNumberOfMiles() const;
void setNumberOfMiles(int miles);
bool hasEliteSuperRewardsStatus() const;
void setHasEliteSuperRewardsStatus(bool status);
private:
std::string m_passengerName { "Unknown Passenger" };
int m_numberOfMiles { 0 };
bool m_hasEliteSuperRewardsStatus { false };
};
// all methods omitted...
注意:为了遵循const-correctness原则,建议将不改变对象的任何数据成员的成员函数声明为const。与非const成员函数也被称为赋值函数相对,这些成员函数也称为检查器。
3. constexpr
C++中一直有常量表达式的概念,即在编译器求值的表达式。在某些情况下,必须使用常量表达式。例如,定义数组时,数组的大小需要为常量表达式。由于此限制,以下代码在C++中无效。
const int getArraySize() {
return 32;
}
int main() {
std::array<int, getArraySize()> myArray {}; // invalid in C++.
}
使用constexpr关键字,getArraySize()函数可以被重定义,允许在常量表达式中调用它。
constexpr int getArraySize() {
return 32;
}
int main() {
std::array<int, getArraySize()> myArray {}; // ok.
}
你甚至可以这样做:
int myArray[getArraySize() + 1]; // ok.
将函数声明为constexpr对函数的功能施加了很多限制,因为编译器必须能够在编译期对函数求值。例如,允许constexpr函数调用其他constexpr函数,但不允许调用任何非constexpr函数。这样的函数不允许有任何副作用,也不能引发任何异常。
通过定义constexpr构造函数,可以创建用户自定义类型的常量表达式变量。与constexpr函数一样,constexpr类也有很多限制。下面的Rect类定义了constexpr构造函数,他还定义了执行一些计算的constexpr getArea()方法。
class Rect {
public:
constexpr Rect(std::size_t width, std::size_t height) :
m_width { width }, m_height { height } {
}
constexpr std::size_t getArea() const {
return m_width * m_height;
}
private:
std::size_t m_width {}, m_height {};
}
使用这个类声明constexpr对象是非常容易的。
constexpr Rect r { 8, 2 };
std::array<int, r.getArea()> myArray {}; // ok.
4. consteval
上一节讨论的constexpr关键字指定函数在编译期执行,但不能保证一定在编译期执行。采用以下constexpr函数:
constexpr double inchToMm(double inch) {
return inch * 25.4;
}
如果按以下方式调用,则会在需要时在编译期对函数求值。
constexpr double const_inch { 6.0 };
constexpr double mml { inchToMm(const_inch) }; // at compile time.
然而,如果按以下方式调用,函数将不会在编译期被求值,而是在运行时。
double dynamic_inch { 8.0 };
double mm2 { inchToMm(dynamic_inch) }; // at run time.
如果确实希望保证始终在编译期对函数进行求值,则需要使用C++20的consteval关键字将函数转换为所谓的立即函数。可以按照如下方式更改inchToMm()函数:
consteval double inchToMm(double inch) {
return inch * 25.4;
}
参考
[比] 马克·格雷戈勒著 程序喵大人 惠惠 墨梵 译 C++20高级编程(第五版)