C++和标准库速成(八)——指针、动态数组、const、constexpr和consteval

news2025/3/19 23:11:42

目录

  • 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高级编程(第五版)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2318020.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

超声重建,3D重建 超声三维重建,三维可视化平台 UR 3D Reconstruction

1. 超声波3D重建技术的实现方法与算法 技术概述 3D超声重建是一种基于2D超声图像生成3D体积数据的技术&#xff0c;广泛应用于医学影像领域。通过重建和可视化三维结构&#xff0c;3D超声能够显著提高诊断精度和效率&#xff0c;同时减少医生的脑力负担。本技术文档将详细阐述…

[HelloCTF]PHPinclude-labs超详细WP-Level 6Level 7Level 8Level 9-php://协议

由于Level 6-9 关的原理都是通用的, 这里就拿第6关举例, 其他的关卡同理 源码分析 定位到代码 isset($_GET[wrappers]) ? include("php://".$_GET[wrappers]) : ; 与前几关发生变化的就是 php:// 解题分析 这一关要求我们使用 php协议 php:// 协议 php://filte…

【Linux】Bash是什么?怎么使用?

李升伟 整理 什么是 Bash&#xff1f; Bash&#xff08;Bourne Again Shell&#xff09;是一种 命令行解释器&#xff08;Shell&#xff09;&#xff0c;广泛用于 Unix 和 Linux 操作系统。它是 Bourne Shell&#xff08;sh&#xff09; 的增强版&#xff0c;提供了更多的功能…

如何创建并保存HTML文件?零基础入门教程

原文&#xff1a;如何创建并保存HTML文件&#xff1f;零基础入门教程 | w3cschool笔记 本文将以Windows系统为例&#xff0c;教你用最简单的记事本创建并保存第一个HTML网页。 &#x1f4dd; 第一步&#xff1a;准备工具 文本编辑器&#xff1a;使用系统自带的记事本&#xff…

React19源码系列之FiberRoot节点和Fiber节点

在上一篇文章&#xff0c;看了createRoot函数的大致流程。 createContainer函数创建并返回了FiberRoot 。FiberRoot是由createFiberRoot函数创建&#xff0c; createFiberRoot函数还将 FiberRoot和 根Fiber 通过current属性建立起了联系。将FiberRoot作为参数传给 ReactDOMRoo…

TCP协议的多线程应用、多线程下的网络编程

DAY13.2 Java核心基础 多线程下的网络编程 基于单点连接的方式&#xff0c;一个服务端对应一个客户端&#xff0c;实际运行环境中是一个服务端需要对应多个客户端 创建ServerSocketNable类&#xff0c;多线程接收socket对象 public class ServerSocketNable implements Run…

华为中小型企业项目案例

实验目的(1) 熟悉华为交换机和路由器的应用场景 (2) 掌握华为交换机和路由器的配置方法 实验拓扑实验拓扑如图所示。 华为中小型企业项目案例拓扑图 实验配置市场部和技术部的配置创建VLANLSW1的配置 [LSW1]vlan batch 10 20 [LSW1]q…

LabVIEW VI Scripting随机数波形图自动生成

通过LabVIEW VI Scripting 技术&#xff0c;实现从零开始编程化创建并运行一个随机数波形监测VI。核心功能包括自动化生成VI框架、添加控件与函数、配置数据流逻辑及界面布局优化&#xff0c;适用于批量生成测试工具、教学模板开发或复杂系统的模块化构建。通过脚本化操作&…

MATLAB 控制系统设计与仿真 - 26

状态空间控制系统概述 状态空间描述 现代控制理论是建立在状态空间基础上的控制系统分析和设计理论&#xff0c;它用状态变量来刻画系统的内部特征&#xff0c;用‘一节微分方程组’来描述系统的动态特性。系统的状态空间模型描述了系统输入/输出与内部状态之间的关系&#x…

Python----计算机视觉处理(Opencv:图像镜像旋转)

一、图像镜像旋转 图像的旋转是围绕一个特定点进行的&#xff0c;而图像的镜像旋转则是围绕坐标轴进行的。图像镜像旋转&#xff0c;也可 以叫做图像翻转&#xff0c;分为水平翻转、垂直翻转、水平垂直翻转三种。 通俗的理解为&#xff0c;当以图片的中垂线为x轴和y轴时&#x…

C++从入门到入土(八)——多态的原理

目录 前言 多态的原理 动态绑定与静态绑定 虚函数表 小结 前言 在前面的文章中&#xff0c;我们介绍了C三大特性之一的多态&#xff0c;我们主要介绍了多态的构成条件&#xff0c;但是对于多态的原理我们探讨的是不够深入的&#xff0c;下面这这一篇文章&#xff0c;我们将…

PyCharm安装redis,python安装redis,PyCharm使用失败问题

报错信息 Usage: D:\wb2\wbrj_pys\venv\Scripts\python.exe -m pip install [options] [package-index-options] … D:\wb2\wbrj_pys\venv\Scripts\python.exe -m pip install [options] -r [package-index-options] … D:\wb2\wbrj_pys\venv\Scripts\python.exe -m pip instal…

保姆级离线TiDB V8+解释

以前学习的时候还是3版本&#xff0c;如今已经是8版本了 https://cn.pingcap.com/product-community/?_gl1ujh2l9_gcl_auMTI3MTI3NTM3NC4xNzM5MjU3ODE2_gaMTYwNzE2NTI4OC4xNzMzOTA1MjUz_ga_3JVXJ41175MTc0MTk1NTc1OC4xMS4xLjE3NDE5NTU3NjIuNTYuMC41NDk4MTMxNTM._ga_CPG2VW1Y4…

PyTorch 深度学习实战(17):Asynchronous Advantage Actor-Critic (A3C) 算法与并行训练

在上一篇文章中&#xff0c;我们深入探讨了 Soft Actor-Critic (SAC) 算法及其在平衡探索与利用方面的优势。本文将介绍强化学习领域的重要里程碑——Asynchronous Advantage Actor-Critic (A3C) 算法&#xff0c;并展示如何利用 PyTorch 实现并行化训练来加速学习过程。 一、A…

Docker换源加速(更换镜像源)详细教程(2025.3最新可用镜像,全网最详细)

文章目录 前言可用镜像源汇总换源方法1-临时换源换源方法2-永久换源&#xff08;推荐&#xff09;常见问题及对应解决方案1.换源后&#xff0c;可以成功pull&#xff0c;但是search会出错 补充1.如何测试镜像源是否可用2.Docker内的Linux换源教程 换源速通版&#xff08;可以直…

SpringData Redis:RedisTemplate配置与数据操作

文章目录 引言一、Redis概述与环境准备二、RedisTemplate基础配置三、连接属性配置四、操作String类型数据五、操作Hash类型数据六、操作List类型数据七、操作Set类型数据八、操作ZSet类型数据九、事务与管道操作总结 引言 Redis作为高性能的NoSQL数据库&#xff0c;在分布式系…

Qt按钮控件常用的API

1.创建按钮 QPushButton *btnnew QPushButton; 以顶层方式弹出窗口控件 代码&#xff1a; #include "widget.h" #include "ui_widget.h" #include"QPushButton"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui-&…

如何检查CMS建站系统的插件是否安全?

检查好CMS建站系统的插件安全是确保网站安全的重要环节&#xff0c;对于常见的安全检查&#xff0c;大家可以利用以下几种有效的方法和工具&#xff0c;来帮你评估插件的安全性。 1. 检查插件来源和开发者信誉 选择可信来源&#xff1a;仅从官方插件库或可信的第三方开发者处…

【Matlab GUI】封装matlab GUI为exe文件

注&#xff1a;封装后的exe还是需要有matlab环境才能运行 &#xff08;1&#xff09;安装MCRinstaller.exe文件&#xff0c;在matlab安装目录下的toolbox/compiler/deploy/win64文件夹里 &#xff08;2&#xff09;安装完MCRinstaller.exe&#xff0c;字命令窗口输入&#x…

【eNSP实战】(续)一个AC多个VAP的实现—将隧道转发改成直接转发

在 一个AC多个VAP的实现—CAPWAP隧道转发 此篇文章配置的基础上&#xff0c;将隧道转发改成直接转发 一、改成直接转发需要改动的配置 &#xff08;一&#xff09;将连接AP的接口改成trunk口&#xff0c;并允许vlan100、101、102通过 [AC1]interface GigabitEthernet 0/0/8 …