常变量
(#define和const的区别)C++推荐使用const对象或 enum class 定义常量,不推荐使用#define。
(1)#define宏定义在预处理阶段做一个简单的文本替换,不携带任何类型信息,不做类型检查。const在编译器会进行类型安全检查。const对象在编译期间就能检测出错误的使用方式,例如尝试修改const对象的值。而宏定义在编译阶段无法进行这种类型和作用域的检查,错误可能直到运行时才显现出来。
例如,#define PI 3.14,在编译时PI将被替换为3.14,但它不具备类型,如果误用可能导致类型不匹配的问题。相反,使用const对象如const double PI = 3.14。编译器会检查PI的类型,确保在使用时类型一致。
(2)宏定义在整个预处理范围内都是可见的,容易造成命名冲突。而const对象或枚举成员在作用域内定义,只在该作用域内可见,有助于减少命名冲突。
常量指针与指针常量
常量指针(const int *m1 或 int const *m1 )表示指针所指向的数据内容是不可修改的,但指针自身可以改变指向其它内存区域。
指针常量(int* const m2)表示指针自身是不可修改的,即始终指向同一块内存区域,但可以通过该指针修改其指向内存区域的内容。
在函数参数中,使用指针常量可以限制实参在函数内部改变指向;
使用常量指针则限制在函数内部通过该指针修改实参的值。
const引用传参
将函数传入参数声明为const,以指明使用这种参数不仅仅是为了效率的原因,而不是想让调用函数能够修改对象的值。同理,将指针参数声明为const,函数将不修改由这个参数所指的对象。
通常修饰指针参数和引用参数:
void Fun( const A *in); //修饰指针型传入参数
void Fun(const A &in); //修饰引用型传入参数
对于非内置类型(例如自定义的类或结构体),它们通常包含多个数据成员,构造、拷贝和析构过程可能会涉及复杂的操作,尤其是在类中包含指针、动态分配的内存或其他资源时,拷贝的成本会相对较高。当我们作为函数参数传递这样的非内置类型时,如果不采用引用传递,而是采取值传递的方式,编译器会自动创建原对象的一个副本,这个过程可能会消耗更多的时间和内存资源。
对于内置类型(如int、double等),因其拷贝成本较低,通常不需要使用const引用传递。直接使用值传递方式即可,void func(const int x)改写为void func(const int &x)并不能带来性能提升,反而可能会降低代码的可读性和直观性。
常成员函数
通过在成员函数声明末尾添加const关键字,将其定义为常成员函数。使用常量函数可以提高代码安全性,限制对象状态的修改。
编译器禁止常成员函数修改类的任何非mutable成员变量。
常量对象只能调用常量成员函数,非常量对象既可以调用常量成员函数,也能调用非常量成员函数。这是因为常量对象this指针实际上是对象的常量指针,故不能通过常量成员函数修改对象的非mutable成员。
C++引入了mutable关键字,用于标记某些数据成员即使在常量成员函数中也可以被修改。这对于那些不影响对象外部状态,但在内部需要变化的成员变量特别有用,如缓存或其他内部计算结果等。
- 当存在同名同参数及返回类型的常量成员函数和非常量成员函数时,调用哪个版本的函数取决于调用上下文:如果对象是常量,则调用常量版本;如果是非常量对象,则调用非常量版本。
常成员函数和非常成员函数之间的重载
常成员函数不能更新类的成员变量,也不能调用该类中没有用const修饰的成员函数,只能调用常成员函数。
常成员函数声明:<类型标志符>函数名(参数表)const。
const是函数类型的一部分,在实现部分也要带该关键字。
const关键字可以用于对重载函数的区分。
(4)非常量对象也可以调用常成员函数,但是如果有重载的非常成员函数则会调用非常成员函数。 在C++中,只有被声明为const的成员函数才能被一个const类对象调用。
常量返回值
很多时候,函数中会返回一个地址或者引用。调用这得到这个返回的地址或者引用后就可以修改所指向或者代表的对象。这个时候如果我们不希望这个函数的调用这修改这个返回的内容,就应该返回一个常量。可以阻止用户修改返回值,返回值也要相应的付给一个常量或常指针。
返回值为const的主要目的有两个:一是确保返回的对象不被意外修改,维持数据一致性;二是传达设计意图,告知使用者函数返回的是一个只读结果,不应对其进行修改。在运算符重载等场景下,通过返回const对象,可以有效防止产生临时对象后又被随意修改的问题。
当返回值类型为const A,如const A fun2()
,表示函数返回的是一个常量对象。调用该函数后,得到的结果对象是不可修改的,任何尝试修改该对象成员变量的操作都将导致编译错误。用户不能执行类似result.fun2().mutate();
这样的操作,如果mutate()
是一个会修改对象状态的成员函数。
当返回值类型为const A*,如const A* fun3()
,这意味着返回的是一个指向A对象的常量指针。调用该函数后,虽然可以通过指针访问对象,但不能通过该指针去修改对象的任何属性。例如,(fun3())->setValue(newValue);
这样的代码会导致编译错误。const 修饰返回的指针或者引用,是否返回一个指向const的指针,取决于我们想让用户干什么。
在运算符重载函数中,比如重载乘法操作符const Rational operator*(const Rational& lhs, const Rational& rhs)
,返回值被声明为const Rational,是因为乘法运算的结果通常是新的Rational对象,我们期望这个新产生的对象是不可变的。这样可以防止后续的赋值操作,例如(a * b) = c;
,因为 (a * b)
返回的是一个const对象,不能作为左值进行赋值。const 修饰自定义类型的作为返回值,此时返回的值不能作为左值使用,既不能被赋值,也不能被修改。
[思考]这样定义赋值操作符重载函数可以吗?
const A& operator=(const A& a);
在C++中,通常赋值操作符=(operator=)
的重载形式不带有返回值为const引用。正常的赋值操作符重载函数定义应如下:
这里的返回值类型是A的非const引用,而非const引用。返回*this主要是为了支持连续赋值操作,同时也表明该操作符修改了左侧对象的状态。
常数据成员
在C++中,当const关键字用来修饰类的数据成员时,它表示这些成员在对象生存期内是不可修改的。对于同一个类的不同对象,const数据成员的值可以不同,因为每个对象都有自己独立的const数据成员副本。
const数据成员不能在类的声明部分直接初始化,因为在创建类的对象之前,编译器无法得知其确切的初始化值。因此,我们必须在类的构造函数初始化列表中对其进行初始化。
关于在整个类中保持不变的常量,可以使用类内枚举常量来实现。枚举常量在编译时就被赋予了固定的值,它们不占用对象的存储空间,适用于定义一些固定数值(如数组大小等),但要注意枚举常量的隐式类型为整型,且所能表示的最大值有限制,无法直接表示浮点数。
这样定义的枚举常量size1和size2在整个程序中被视为固定值,不会随类的对象不同而变化,且不占据对象的存储空间。
如果有个成员函数想修改对象中的某一个成员怎么办?这时我们可以使用mutable关键字修饰这个成员,mutable的意思也是易变的,容易改变的意思,被mutable关键字修饰的成员可以处于不断变化中。
const数据成员的初始化
在C++中,const成员变量也不能在类定义处初始化,只能通过构造函数初始化列表进行,并且必须有构造函数。const数据成成员 只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类的声明中初始化const数据成员,因为类的对象没被创建时,编译器不知道const数据员的值是什么。
要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现,或者static const。
const定义的常量在超出其作用域之后其空间会被释放,而static定义的静态常量在函数执行后不释放其存储空间。static表示的是静态的。类的静态成员函数、静态成员变量是和类相关的,而不是和类的具体对象相关的。即使没有具体对象,也能调用类的静态成员函数和成员变量。一般类的静态函数几乎就是一个全局函数,只不过它的作用域限于包含它的文件中。