改善C++程序与设计的55个具体做法——2.尽量以const,enum,inline替换#define

const和#define

这个条款或许改为“宁可以编译器替换预处理器”比较好,因为或许#define不被视为语言的一部分。那正是它的问题所在。当你做出这样的事情:

#define ASPECT RATIO 1.653

记号名称ASPECT_RATIO也许从未被编译器看见;也许在编译器开始处理源码之前它就被预处理器移走了。于是记号名称ASPECT RATIO有可能没进入记号表(symbol table)内。于是当你运用此常量但获得一个编译错误信息时,可能会带来困惑,因为这个错误信息也许会提到1.653而不是 ASPECT RATIO。如果ASPECT RATIO被定义在一个非你所写的头文件内,你肯定对1.653以及它来自何处毫无概念,于是你将因为追踪它而浪费时间。这个问题也可能出现在记号式调试器(symbolic debugger)中,原因相同:你所使用的名称可能并未进入记号表(symboltable)。
解决之道是以一个常量替换上述的宏(#define):

const double AspectRatio = 1.653; //大写名称通常用于宏,
//因此这里改变名称写法。


作为一个语言常量,AspectRatio肯定会被编译器看到,当然就会进入记号表内。此外对浮点常量(floating point constant,就像本例)而言,使用常量可能比使用#define导致较小量的码,因为预处理器“盲目地将宏名称ASPECT_RATIO替换为1.653”可能导致目标码(object code)出现多份1.653,若改用常量AspectRatio绝不会出现相同情况。

以常量替换#define的两种特殊情况

第一是定义常量指针

由于常量定义式通常被放在头文件内(以便被不同的源码含入),因此有必要将指针(而不只是指针所指之物)声明为const。

例如着要在文件内定义一个常量的(不变的)char*-based字符串,你必须写 const两次

const char* const authorName-"Scott Meyers";


关于const的意义和使用(特别是当它与指针结合时),条款3有完整的讨论。这里值得先提醒你的是,string对象通部比其前辈chart-based合宜,所以上选的authorName 往往定义成这样更好些:

const std::string authorName("Scott Meyers");


第二个是class专属常量

实现类专属常量,我们就得使用static,那我们就不得不来了解一下static的使用规则了:不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。通常静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化时使用作用域运算符来指出静态成员所属的类。但是如果静态成员是const整数类型或者枚举型,则可以在类声明中初始化。

为了将常量的作用域(scope)限制于class内,你必须让它成为class的一个成员;而为确保此常量至多只有一份实体,你必须让它成为一个static成员:

class GamePlayer {
private:
	static const int NumTuns = 5; //常量声明式
	int scores[NumTuns]; //使用该常量
};


然而你所看到的是NumTurns的声明式而非定义式通常C++要求你对你所使用的任何东西提供一个定义式,但如果它是个class专属常量又是static且为整数类型(例如ints,chars,bools),则情况就有点不一样了。

如下所示:

class MyClass {
public:
    static const int MY_CONSTANT = 10;
    static const char MY_CHAR_CONSTANT = 'A';
    static const bool MY_BOOL_CONSTANT = true;
};

在这个例子中,MY_CONSTANT是一个整型常量,MY_CHAR_CONSTANT是一个字符型常量,MY_BOOL_CONSTANT是一个布尔型常量。它们都被声明为staticconst,并在声明时进行了初始化。

这样,在其他代码中,你可以直接使用这些常量,而无需提供额外的定义式。例如:

int main() {
    int value = MyClass::MY_CONSTANT + 5;
    char character = MyClass::MY_CHAR_CONSTANT;
    bool flag = MyClass::MY_BOOL_CONSTANT;

    // 其他代码...
    return 0;
}

在这个例子中,我们直接使用了MyClass中声明的常量,并对它们进行了不同的操作。这是因为这些常量是类的一部分,并且在声明时就被初始化了,所以可以直接使用,而无需提供定义式。

需要注意的是,对于以上的常量,你仍然不能获取它们的地址,否则编译器将会产生错误。

#include<iostream>
using namespace std;
class AA {
private:
	static const int a=9;//常量声明式
public:
	void aa()
	{
		
		cout << & a << endl;
	}
};
int main()
{
	AA i;
	i.aa();
}

结果

00007FF75FDAABB4

 我们换成char类型试试看

#include<iostream>
using namespace std;
class AA {
private:
	static const char a=1;//常量声明式
public:
	void aa()
	{
		
		cout << & a << endl;
	}
};

int main()
{
	AA i;
	i.aa();
}

结果是

      

 我们会发现这为什么不打印出地址了呢?

我们可以通过调试看看

发现rax寄存器为1

 

我们加上volatile关键字保持内存可见性,打印的结果也是1

所以char类型不打印地址是编译器优化的结果,char类型就一个字节,所以打印的时候直接把它优化到了寄存器里面

只要不取它们的地址,你可以声明并使用它们而无须提供定义式。

如果你取某个class专属常量的地址,或纵使你不取其地址而你的编译器却(不正确地)坚持要看到一个定义式,你就必须另外提供定义式如下:

const int GamePlayer::NumTurns; //NumTurns的定义;
//下面告诉你为什么没有给予数值


请把这个式子放进一个实现文件而非头文件由于class常量已在声明时获得初值(例如先前声明Numturns时为它设初值5,注意声明时给初值这只适用于整型),因此定义时不可以再设初值顺带一提,请注意,我们无法利用#define 创建一个class 专属常量,因为#defines并不重视作用域(scope)。一旦宏被定义,它就在其后的编译过程中有效(除非在某处被#undef)。这意味#defines不仅不能够用来定义class专属常量,也不能够提供任何封装性,也就是说没有所谓private #define这样的东西。而当然const成员变量是可以被封装的,NumTurns就是。
旧式编译器也许不支持上述语法,它们不允许static成员在其声明式上获得初值。此外所谓的“in-class 初值设定”也只允许对整数常量进行。

也就是说除了整型(int,char,bool)可以在类内给初值,其他的都不行

#include<iostream>
using namespace std;
class AA {
	static const float a=1.0;//不可以
	
static const double a=1.0;//不可以
};

我们只能通过下面这个来对其进行初始化

class CostEstimate {
private:
static const double FudgeFactor; //static class 常量声明
//位于头文件内
};
const double CostEstimate::FudgeFactor =1.35;static class常量定义,位于实现文件内


这几乎是你在任何时候唯一需要做的事。

enum和#define

当你在class 编译期间需要一个class常量值,例如在上述的GamePlayer::scores的数组声明式中(是的,编译器坚持必须在编译期间知道数组的大小)。

我们可以用“static 整数型class 常量”完成“in class初值设定”

class GamePlayer {
private:
	static const int NumTuns = 5; //常量声明式
	int scores[NumTuns]; //使用该常量
};

但是如果这时候万一你的编译器(错误地)不允许“static 整数型class 常量”完成“in class初值设定”,可改用所谓的"the enumhack”补偿做法。其理论基础是:“一个属于枚举类型(enumerated type)的数值可权充ints被使用”,于是GamePlayer可定义如下:

class GamePlayer {
private:
enum ( NumTurns=5 ); //"the enum hack"—令 NumTurns
//成为5的一个记号名称.
int scores [NumTurns]; //这就没问题了.
};


基于数个理由 enum back值得我们认识。第一,enum hack的行为某方面说比较像 #define而不像const,有时候这正是你想要的。例如取一个const的地址是合法的,但取一个enum的地址就不合法,而取一个#define的地址通常也不合法。

#include<iostream>
using namespace std;
class AA
{
	enum{a=3};
public:
	void aa()
	{
		cout << &a;//这是不行的
	}

};
int main()
{
	AA y;
	y.aa();
}

如果你不想让别人获得一个pointer或referenee指向你的某个整数常量,enum可以帮助你实现这个约束。此外虽然优秀的编译器不会为“整数型const对象”设定另外的存物空间(除非你创建一个pointer或reference指向该对象),不够优秀的编译器却能如此,而这可能是你不想要的。enum 和define一样绝不会导致非必要的内有分配。


认识enum hack的第二个理由纯粹是为了实用主义。许多代码用了它,所以看到它时你必须认识它。事实上"enum hack”是template metaprogramming (模板元编程,见条款48)的基础技术。
把焦点拉回预处理器。

inline和#define

另一个常见的#define误用情况是以它实现宏(macros)。宏看起来像函数,但不会招致函数调用(function call)带来的额外开销。下面这个宏夹带着宏实参,调用函数:

//以a和b的较大值调用f
#define CALL_WITH_MAX(a,b) f((a)> (b)? (a):(b))


这般长相的宏有着太多缺点,光是想到它们就让人痛苦不堪。
无论何时当你写出这种宏,你必须记住为宏中的所有实参加上小括号,否则某些人在表达式中调用这个宏时可能会遭遇麻烦。但纵使你为所有实参加上小括号,看看下面不可思议的事情:

int a=5,b=0;
CALL_WITH MAX(t+a, b); //a 被累加二次
CALL WITH MAX(++a,b+10); //a被累加一次


在这里,调用f之前,a的递增次数竟然取决于“它被拿来和谁比较”!
幸运的是你不需要对这种无聊事情提供温床。你可以获得宏带来的效率以及一般函数的所有可预料行为和类型安全性(typesafety)——只要你写出template inline函数(见条款30):

template<typename r> //由于我们不知道
inline void callWithMax(const T& a, const T& b)//T是什么,所以采用( 一 f(a>b? a: b); //pass by reference-to-const.
//见条款 20.


这个template产出一整群函数,每个函数都接受两个同型对象,并以其中较大者调用f。这里不需要在函数本体中为参数加上括号,也不需要操心参数被核算(求值)多次……等等。此外由于cal1withMax是个真正的函数,它遵守作用域(scope)和访问规则。例如你绝对可以写出一个“class内的private inline函数”。一般而言宏无法完成此事。
Hlefine

有了consts、enums 和inlines,我们对预处理器(特别是#define)的需求降低了,但并非完全消除。#include仍然是必需品,而#ifdef/#ifndef也继续扮演控制编译的重要角色。目前还不到预处理器全面引退的时候,但你应该明确地给予它更长更频繁的假期。

总结 

对于单纯常量,最好以const对象或enums替换#defines。
对于形似函数的宏(macros),最好改用inline函数替换#defines。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

操作系统——处理机调度

文章目录 进程调度0.概念1.调度分类高级调度低级调度中级调度七状态模型调度对比 2.进程调度进程调度的时机进程调度的方式进程的切换方式调度器/调度程序闲逛进程 3. 调度算法的评价指标CPU利用率系统吞吐量周转时间等待时间响应时间 4. 调度算法先来先服务(FCFS)短作业优先(S…

装饰模式(Decorator Pattern)

定义 装饰模式&#xff08;Decorator Pattern&#xff09;是一种结构型设计模式&#xff0c;它允许通过将对象包装在装饰器类的实例中来动态地添加新的行为和责任。这种模式可以在不修改现有代码的情况下&#xff0c;灵活地扩展对象的功能。 示例 考虑一个咖啡店的场景&…

设计模式(六)代理模式

相关文章设计模式系列 1.代理模式简介 代理模式介绍 代理模式也叫委托模式&#xff0c;是结构型设计模式的一种。在现实生活中我们用到类似代理模式的场景有很多&#xff0c;比如代购、代理上网、打官司等。 定义 为其他对象提供一种代理以控制这个对象的访问。 代理模式…

老吕在CSDN写文章之必学【Markdown编辑器教程】

老吕在CSDN写文章之必学【Markdown编辑器教程】 一、作者前言二、介绍Markdown1.Markdown简介2.Markdown优势2.1 使用 Markdown 的优点 3.Markdown发展历程3.1 标准化3.2 CommonMark3.3 GFM3.4 Markdown Extra 4.Markdown应用场景4.1 在线阅读4.2 文本编辑 5.Markdown适用人群 …

搜维尔科技:第九届元宇宙数字人大赛,参赛小组报名确认公告

各位参赛选手大家好&#xff0c;近期已收到新增报名信息如下表&#xff0c;请各位参赛选手确认&#xff0c;如果信息有误或信息不完整请电话联系赛务组工作人员进行更正 随着元宇宙时代的来临&#xff0c;数字人设计成为了创新前沿领域之一。为了提高大学生元宇宙虚拟人角色策划…

js:通过input标签或Drag拖拽文件实现浏览器文件上传获取File文件对象

文档 https://developer.mozilla.org/zh-CN/docs/Web/API/Filehttps://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement/drag_event 通过读取文件可以获取File对象的信息 lastModified: 1707210706000 lastModifiedDate: Tue Feb 06 2024 17:11:46 GMT0800 (中国标准…

pthread_exit和pehread_join函数

pthread_exit&#xff1a; 在线程中禁止调用exit函数&#xff0c;否则会导致整个进程退出&#xff0c;取而代之的是调用pthread_exit函数&#xff0c;这个函数只会使一个线程退出&#xff0c;如果主线程使用pthread_exit函数也不会使整个进程退出&#xff0c;不会影响其他线程…

uniapp播放mp4省流方案

背景&#xff1a; 因为项目要播放一个宣传和讲解视频&#xff0c;视频文件过大&#xff0c;同时还为了节省存储流量&#xff0c;想到了一个方案&#xff0c;用m3u8切片替代mp4。 m3u8&#xff1a;切片播放&#xff0c;可以理解为一个1G的视频文件&#xff0c;自行设置文…

进程间通信:共享内存与信号灯集(2024/2/26)

作业1&#xff1a;共享内存 shmsnd.c: #include <myhead.h> #define PAGE_SIZE 4096int main(int argc, const char *argv[]) {//一、创建key值key_t key-1;if((keyftok("/",k))-1){perror("ftok error");return -1;}//二、根据key值创建共享内存i…

JS防抖函数

场景 频繁触发耗时操作&#xff0c;仅关心最后一次的触发时使用防抖函数 代码 function debounce(fn , delay){let timer;return () > {clearTimeout(timer);timer setTimerout(()>{fn()},delay)} } 详解 触发一次函数&#xff0c;然后执行后续操作 function deboun…

matplotlib plt.show()却弹出空白框并之后自动退出程序的原因及解决方法

运行下列代码并使用plt.show()进行展示时候&#xff0c;cmd输出如下&#xff1a; 先弹出空白框&#xff1a; 而后直接退出程序&#xff1a; 之前遇到过很多次&#xff0c;由于不输出Traceback&#xff0c;完全不知道什么原因。结果发现是因为没有导入torch导致的。 解决办法就…

【MySQL】学习和总结联合查询

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-OPj5g6evbkm5ol0U {font-family:"trebuchet ms",verdana,arial,sans-serif;font-siz…

蜘蛛蜂优化算法SWO求解不闭合MD-MTSP,可以修改旅行商个数及起点(提供MATLAB代码)

1、蜘蛛蜂优化算法SWO 蜘蛛蜂优化算法&#xff08;Spider wasp optimizer&#xff0c;SWO&#xff09;由Mohamed Abdel-Basset等人于2023年提出&#xff0c;该算法模型雌性蜘蛛蜂的狩猎、筑巢和交配行为&#xff0c;具有搜索速度快&#xff0c;求解精度高的优势。VRPTW&#x…

微信小程序本地开发

微信小程序本地开发时不需要在小程序后台配置服务器域名直接在小程序项目中填写后端在本机的IP地址和端口号 如图&#xff08;第一步&#xff09; 填写地址后发现报错&#xff0c;url不是合法域名&#xff0c;则在详情设置不校验合法域名 如图&#xff08;第二歩&#xff09;…

如何本地部署LightPicture结合cpolar内网穿透打造个人云图床

文章目录 1.前言2. Lightpicture网站搭建2.1. Lightpicture下载和安装2.2. Lightpicture网页测试2.3.cpolar的安装和注册 3.本地网页发布3.1.Cpolar云端设置3.2.Cpolar本地设置 4.公网访问测试5.结语 1.前言 现在的手机越来越先进&#xff0c;功能也越来越多&#xff0c;而手机…

VM-UNet:视觉Mamba UNet用来医学图像分割 论文及代码解读

VM-UNet 期刊分析摘要贡献方法整体框架1. Vision Mamba UNet (VM-UNet)2. VSS block3. Loss function 实验1.对比实验2.消融实验 可借鉴参考代码使用 期刊分析 期刊名&#xff1a; Arxiv 论文主页&#xff1a; VM-UNet 代码&#xff1a; Code 摘要 在医学图像分割领域&#x…

Python 读取创建word文档

本篇文章内容为使用python 读取word文档和创建word文档 读取doc文件 引入类库 示例如下&#xff1a; import win32com import win32com.client import os 读取doc文件 通过得到的doc文件路径调用系统word功能。 打开文件获取其中的文本信息&#xff0c;输出文本信息&#…

【Java程序员面试专栏 算法思维】五 高频面试算法题:贪心算法

一轮的算法训练完成后,对相关的题目有了一个初步理解了,接下来进行专题训练,以下这些题目就是汇总的高频题目,本篇主要聊聊贪心算法,所以放到一篇Blog中集中练习 题目关键字解题思路时间空间买卖股票的最佳时机 II贪心算法遍历整个股票交易日价格列表 price,并执行贪心策…

算法-计算机基础知识

1&#xff0c;坐标系与数学不同&#xff0c;x轴向下&#xff0c;y轴向右 2.案例&#xff1a;螺旋矩阵 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 class Solution {public List<Integer> spiralOrder(int[][] matrix) { List<Integer&…

Android 面试问题 2024 版(其三)

Android 面试问题 2024 版&#xff08;其三&#xff09; 十一、版本控制十二、Play 商店和应用程序部署十三、无障碍十四、第三方库和 API十五、解决问题的能力十六、基于 JD 的非常高级别的问题 十一、版本控制 什么是版本控制&#xff0c;为什么它在软件开发中很重要&#x…