C++尽量不要使用#define

2024-01-27 16:58
文章标签 c++ 使用 不要 define 尽量

本文主要是介绍C++尽量不要使用#define,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

为什么这么说呢?或许很多程序员已经习惯在文件开始使用大量的#define语句
例如:这里程序文件开头有如下#define语句

复制代码 代码如下:

#define N 10
#define PI 3.14
#define MAX 10000
#define Heigth 6.65
...
...


假设这里程序运行出错误,而且就是在我们使用这些常量有错误,此时编辑器应该会抛出错误信息。如果该信息提示6.65这里有错误,Ok如果你运气好你正好记得或者程序简单一眼能找到6.65表示什么,如果程序很复杂,而且报出6.65的文件是引用该文件,不记得,那么你会困惑这是什么?或许会花大量时间去追踪6.65是什么?进而定位问题。
为什么错误信息是6.65呢?而不是Heith呢?因为在预编译阶段,已经用5.65来代替Heigth,Height没有进入记号表(system table)内。解决之道是可以使用下面语句进行替换
const double treeHeight=5.68;
   作为一个语言常量,treeHeight肯定会被编译器获知,并且进入记号表内。报出的错误不在是数字而是变量名称,这样有利于定位问题。
这里特别说明一下常量替换#define有两种特殊情况。
   第一个是定义常量指针。这里要将指针定义为常量指针,同时该指针也是指向一个常量,所以是下面的形式:
const char * const HZ="Hang Zhou";
在C++中最好使用string对象来替换掉char*形式:
const std::string HZ ("Hang Zhou");
第二个值得注意的就是class专属常量。首先将作用于限制到类内,必须将其声明为其成员。其次确保此常量至多只有一份实体,必须让它称为static成员。例如:
复制代码 代码如下:

class People
{
private:
static const int Number=10;
int phoneNumbers[Number];
......
}

这看到的是声明式,而非定义式。通常C++要求你对使用的任何东西提供一个定义式。 或者使用enum,对于形式函数的宏,尽可能用inline或者template来代替。但是如果它是个class专属常量又是static且为整数类型(int,char,bool)则需特殊处理。只要不娶它们地址,则只用声明而不用提供定义式子。但是如果取class专属常量地址,纵使不取其地址编译器就要你提供定义式子。
static const int People::Number
这里定义不设初始值,是因为声明的时候已经获取了初值。

这里可以使用enum完成类似的功能
复制代码 代码如下:

class People
{
private:
enum { Number = 10 };
int phoneNumbers[Number];
....
}

enum比较像#define而不像const。因为取const的地址是合法的,取一个enum的地址就不合法,取#define地址通常就不合法。所以可以通过enum来实现不让他人取得某个常量的地址。

下面介绍一道笔试题目
复制代码 代码如下:

#define PRODUCT(a,b) a*b
....
int a=5,b=3,c;
c=PRODUCT(a+3,b+4);

那么c的值为多少?c=5+3*3+4=18而不是程序员预想的56,如果想达到预期的结果必须这样写

#define PRODUCT(a,b) ((a)*(b))
或许这样你还会坚持会写宏函数,因为你想说只用写一个宏函数就能完成int,flaot,double等类型的乘积运算。那么在看看下面例子

#define MAX(a,b) ((a)>(b)?(a):(b))

int a=5,b=3

MAX(++a,b); //a被加了两次

MAX(++a,b+4); //a被加了一次
a被加的结果可能不是预期的,完全可以用template inline函数达到宏的预期效果,并且效率与宏差不多。
复制代码 代码如下:

template<typename T>
inline void Max(const T& a,const T& b)
{
f(a>b?a:b);
}

inline函数是一种编译机制,有点从代码上是看不出来的,但是从程序的执行效率上有差别,通常编译器对函数调用的处理是一种类似中断的方式,即当执行到函数调用语句时,会将当前所有信息存储到寄存器中,在去执行函数的代码,执行完后取回寄存器值,恢复到调用函数开始的状态,继续执行代码。声明为 inline 函数后,编译器不把函数编译成调用函数而是将代码拷贝到被调用的地方。所以效率上要比普通函数高一些,少了存寄存器与取寄存器信息的步骤。
另外需要注意的是inline函数最好写到.h文件中去,或者直接写到类中去。
const允许程序员指定一个语义约束,即不能被改动,编译器会强制实施这项约束。它表示被它修饰的值是不变的。const可以在classes外部修饰global或namespace作用域中的常量,或修饰文件、函数或者static对象以及指针。在const应用在指针中要注意关键字const出现在“*”的什么地方,如果在左边表示被指向的值是常量,如果在右边表示指针自身是常量。出现两边表示两者的功能的并集。这里特别说以下几点:
(1)迭代器中的cosnt
const std::vector<int>::iterator iter=vec.begin(); //相当于iter不能改变

std::vector<int>::const_iterator citer=vec.begin(); //iter所指向的内容无法改变
(2)将函数返回值声明为常量,不仅可以降低因程序员错误造成的不可预料的情况,并且不用放弃安全性和高效性。例如:

const operater *(const &lhs,const &rhs);
if((a * b = c);//本意是if(a*b==c)因程序员马虎而写成这样
如果a和b都是内置类型,此代码就不合理,但是是我们自定义类型,就可能行得通,如果声明返回值是cosnt,就可以预防这么做了。

(3)const成员函数,它是为了确认成员函数可作用于const对象。而且两个成员函数如果只是常量性不同,是可以重载的。成员函数后面跟const,表示该函数不能更改类的成员变量(下面有代码验证,如果尝试对其成员赋值,编译器会爆出错误)。原理就是编译器将其认为是只读变量。并且大多数const对象用于引用传递或者指针传递。
复制代码 代码如下:

#include <iostream>
#include <string>

class People
{
public:
People():m_sName(""),m_iAge(0){}
People(std::string name,int age):m_sName(name),m_iAge(age){}
void set(int age)
{
this->m_iAge=age;
}

void set2(int age) const
{
this->m_iAge=age;
}

int get()
{
return this->m_iAge;
}
private:
std::string m_sName;
int m_iAge;
};

int main(int argc,char **argv)
{
People* p=new People("sky",8);
p->set(10);
std::cout<<p->get()<<std::endl;
p->set2(12);
std::cout<<p->get()<<std::endl;
delete p;
return 0;
}


编译该文件会报以下错误信息
const_test.cpp: In member function `void People::set2(int) const':
const_test.cpp:16: error: assignment of data-member `People::m_iAge' in read-only structure
const_test.cpp:36:2: warning: no newline at end of file
cosnt重载(注意:仅当形参是引用或者指针时候,形参是否为const才会有影响)。可以试一试我们将下面代码的&去掉,传入const_int其实调用的是void set(int age)函数,说明形参的const是没有起作用的。下面是验证代码
复制代码 代码如下:

#include <iostream>
#include <string>
class People
{
public:
People():m_sName(""),m_iAge(0){}
People(std::string name,int age):m_sName(name),m_iAge(age){}
void set(const int& age) const
{
std::cout<<"this is const"<<std::endl;
}

void test(int& age)
{
std::cout<<"this is non-const"<<std::endl;
}

void test(short age)
{
std::cout<<"this is non-const"<<std::endl;
}

int get()
{
return this->m_iAge;
}
private:
std::string m_sName;
int m_iAge;
};

int main(int argc,char **argv)
{
People* p=new People("sky",8);
const int const_int=12;
p->test(const_int);
std::cout<<p->get()<<std::endl;
delete p;
}

(4)关于重载函数代码重复的问题。由经验可以得出我们通过const重载的函数往往有大量的代码是重复,甚至是一样的。如果大部分重复代码,我们可以将这些重复的代码写成一个函数,在由它们分别调用。如果是一样的,如下代码,我们就可以在non-const函数中调用const函数,来解决代码重复。
复制代码 代码如下:

class People
{
public:
People():m_sName(""),m_iAge(0){}
People(std::string name,int age):m_sName(name),m_iAge(age){}
void eat(const People & Person) const
{
std::cout<<"this person info is:{age ="<<Person.m_iAge()<<",name ="<<Person.m_sName()<<std::endl;
std::cout<<"eating"<<std::endl;
std::cout<<"end"<<std::endl;
}

void eat ( People & Person)
{
std::cout<<"this person info is:{age ="<<Person.m_iAge()<<",name ="<<Person.m_sName()<<std::endl;
std::cout<<"eating"<<std::endl;
std::cout<<"end"<<std::endl;
}
private:
std::string m_sName;
int m_iAge;
};

然后在non-const eat函数中首先将*this类型由People&显示的转化为const People&让其调用const函数,即函数重载
复制代码 代码如下:

#include <iostream>
#include <string>
class People
{
public:
People():m_sName(""),m_iAge(0){}
People(std::string name,int age):m_sName(name),m_iAge(age){}
void eat(const People & Person) const
{
std::cout<<"this person info is:{age ="<<Person.m_iAge<<",name ="<<Person.m_sName<<"}"<<std::endl;
std::cout<<"eating"<<std::endl;
std::cout<<"end"<<std::endl;
}

void eat(People & Person)
{
static_cast<const People&>(*this).eat(Person);
}
private:
std::string m_sName;
int m_iAge;
};

int main(int argc,char **argv)
{
People Person("sky",8);
Person.eat(Person);
}

运行的结果为

this person info is:{age =8,name =sky
eating
end 

这篇关于C++尽量不要使用#define的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/650902

相关文章

使用DeepSeek API 结合VSCode提升开发效率

《使用DeepSeekAPI结合VSCode提升开发效率》:本文主要介绍DeepSeekAPI与VisualStudioCode(VSCode)结合使用,以提升软件开发效率,具有一定的参考价值... 目录引言准备工作安装必要的 VSCode 扩展配置 DeepSeek API1. 创建 API 请求文件2.

使用TomCat,service输出台出现乱码的解决

《使用TomCat,service输出台出现乱码的解决》本文介绍了解决Tomcat服务输出台中文乱码问题的两种方法,第一种方法是修改`logging.properties`文件中的`prefix`和`... 目录使用TomCat,service输出台出现乱码问题1解决方案问题2解决方案总结使用TomCat,

解决IDEA使用springBoot创建项目,lombok标注实体类后编译无报错,但是运行时报错问题

《解决IDEA使用springBoot创建项目,lombok标注实体类后编译无报错,但是运行时报错问题》文章详细描述了在使用lombok的@Data注解标注实体类时遇到编译无误但运行时报错的问题,分析... 目录问题分析问题解决方案步骤一步骤二步骤三总结问题使用lombok注解@Data标注实体类,编译时

Java中使用Java Mail实现邮件服务功能示例

《Java中使用JavaMail实现邮件服务功能示例》:本文主要介绍Java中使用JavaMail实现邮件服务功能的相关资料,文章还提供了一个发送邮件的示例代码,包括创建参数类、邮件类和执行结... 目录前言一、历史背景二编程、pom依赖三、API说明(一)Session (会话)(二)Message编程客

C++中使用vector存储并遍历数据的基本步骤

《C++中使用vector存储并遍历数据的基本步骤》C++标准模板库(STL)提供了多种容器类型,包括顺序容器、关联容器、无序关联容器和容器适配器,每种容器都有其特定的用途和特性,:本文主要介绍C... 目录(1)容器及简要描述‌php顺序容器‌‌关联容器‌‌无序关联容器‌(基于哈希表):‌容器适配器‌:(

使用Python实现高效的端口扫描器

《使用Python实现高效的端口扫描器》在网络安全领域,端口扫描是一项基本而重要的技能,通过端口扫描,可以发现目标主机上开放的服务和端口,这对于安全评估、渗透测试等有着不可忽视的作用,本文将介绍如何使... 目录1. 端口扫描的基本原理2. 使用python实现端口扫描2.1 安装必要的库2.2 编写端口扫

使用Python实现操作mongodb详解

《使用Python实现操作mongodb详解》这篇文章主要为大家详细介绍了使用Python实现操作mongodb的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、示例二、常用指令三、遇到的问题一、示例from pymongo import MongoClientf

SQL Server使用SELECT INTO实现表备份的代码示例

《SQLServer使用SELECTINTO实现表备份的代码示例》在数据库管理过程中,有时我们需要对表进行备份,以防数据丢失或修改错误,在SQLServer中,可以使用SELECTINT... 在数据库管理过程中,有时我们需要对表进行备份,以防数据丢失或修改错误。在 SQL Server 中,可以使用 SE

使用Python合并 Excel单元格指定行列或单元格范围

《使用Python合并Excel单元格指定行列或单元格范围》合并Excel单元格是Excel数据处理和表格设计中的一项常用操作,本文将介绍如何通过Python合并Excel中的指定行列或单... 目录python Excel库安装Python合并Excel 中的指定行Python合并Excel 中的指定列P

浅析Rust多线程中如何安全的使用变量

《浅析Rust多线程中如何安全的使用变量》这篇文章主要为大家详细介绍了Rust如何在线程的闭包中安全的使用变量,包括共享变量和修改变量,文中的示例代码讲解详细,有需要的小伙伴可以参考下... 目录1. 向线程传递变量2. 多线程共享变量引用3. 多线程中修改变量4. 总结在Rust语言中,一个既引人入胜又可