C++中const、volatile、mutable、explicit的用法

本文详细解析了C++中的const、volatile、mutable等修饰符的使用方法及其应用场景,包括它们如何影响变量、指针及成员函数的行为。同时,还介绍了explicit关键字在构造函数中的作用。

Const用法


const修饰普通变量和指针

const修饰变量,一般有两种写法:

const TYPE value;
TYPE const value;

这两种写法在本质上是一样的。它的含义是:const修饰的类型为TYPE的变量value是不可变的。对于一个非指针的类型TYPE,无论怎么写,都是一个含义,即value值不可变。 例如:

const int nValue;    //nValue是const
int const nValue;    //nValue是const

但是对于指针类型的TYPE,不同的写法会有不同情况:
1. 指针本身是常量不可变

(char*) const pContent;

2. 指针所指向的内容是常量不可变

const (char) *pContent;
(char) const *pContent;

3. 两者都不可变

const char* const pContent;

识别const到底是修饰指针还是指针所指的对象,还有一个较为简便的方法,也就是沿着*号划一条线:
如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;
如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量。

const修饰函数参数

const修饰函数参数是它最广泛的一种用途,它表示在函数体中不能修改参数的值(包括参数本身的值或者参数其中包含的值):

void function(const int Var);     //传递过来的参数在函数内不可以改变(无意义,该函数以传值的方式调用)
void function(const char* Var);   //参数指针所指内容为常量不可变
void function(char* const Var);   //参数指针本身为常量不可变(也无意义,var本身也是通过传值的形式赋值的)
void function(const Class& Var); //引用参数在函数内不可以改变

参数const通常用于参数为指针或引用的情况,若输入参数采用“值传递”方式,由于函数将自动产生临时变量用于复制该参数,该参数本就不需要保护,所以不用const修饰。

const修饰类对象/对象指针/对象引用

const修饰类对象表示该对象为常量对象,其中的任何成员都不能被修改。对于对象指针和对象引用也是一样。
const修饰的对象,该对象的任何非const成员函数都不能被调用,因为任何非const成员函数会有修改成员变量的企图。
例如:

class AAA
{
    void func1();
    void func2() const;
};
const AAA aObj;
aObj.func1(); //错误
aObj.func2(); //正确

const AAA* aObj = new AAA();
aObj->func1(); //错误
aObj->func2(); //正确

const修饰数据成员

const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类声明中初始化const数据成员,因为类的对象未被创建时,编译器不知道const 数据成员的值是什么,例如:

class A
{
    const int size = 100; //错误
    int array[size];       //错误,未知的size
}

const数据成员的初始化只能在类的构造函数的初始化列表中进行。要想建立在整个类中都恒定的常量,可以用类中的枚举常量来实现,例如:

class A
{
…
  enum {size1=100, size2 = 200 };
  int array1[size1];
  int array2[size2];
…
}

枚举常量不会占用对象的存储空间,他们在编译时被全部求值。但是枚举常量的隐含数据类型是整数,其最大值有限,且不能表示浮点数。

const修饰成员函数

const修饰类的成员函数,用const修饰的成员函数不能改变对象的成员变量。一般把const写在成员函数的最后:

class A
{void function()const; //常成员函数, 它不改变对象的成员变量. 也不能调用类中任何非const成员函数。
}

对于const类对象/指针/引用,只能调用类的const成员函数。

const修饰成员函数的返回值

  1. 一般情况下,函数的返回值为某个对象时,如果将其声明为const时,多用于操作符的重载。通常,不建议用const修饰函数的返回值类型为某个对象或对某个对象引用的情况。原因如下:如果返回const对象,或返回const对象的引用,则返回值具有const属性,返回实例只能访问类A中的公有(保护)数据成员和const成员函数,并且不允许对其进行赋值操作,这在一般情况下很少用到。

  2. 如果给采用“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针所指的内容)不能被修改,该返回值只能被赋给加const 修饰的同类型指针:

    const char * GetString(void);
    如下语句将出现编译错误:

    char *str=GetString();
    正确的用法是:

    const char *str=GetString();

  3. 函数返回值采用“引用传递”的场合不多,这种方式一般只出现在类的赙值函数中,目的是为了实现链式表达。如:
class A
{
…
    A &operate= (const A &other); //赋值函数
}
A a,b,c; //a,b,c为A的对象
…
a=b=c;   //正常
(a=B)=c; //不正常,但是合法

若赋值函数的返回值加const修饰,那么该返回值的内容不允许修改,上例中a=b=c依然正确。(a=b)=c就不正确了。

const常量与define宏定义的区别

  1. 编译器处理方式不同
    define宏是在预处理阶段展开。
    const常量是编译运行阶段使用。
  2. 类型和安全检查不同
    define宏没有类型,不做任何类型检查,仅仅是展开。
    const常量有具体的类型,在编译阶段会执行类型检查。
  3. 存储方式不同
    define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。
    const常量会在内存中分配(可以是堆中也可以是栈中)。

volatile关键字


volatile的本意是“易变的”,volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

当要求使用volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被寄存。例如:

volatile int i=10;
int a = i;
。。。//其他代码,并未明确告诉编译器,对i进行过操作
int b = i;

volatile 指出 i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在b中。而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在b中。而不是重新从i里面读。这样以来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问。

注意,在vc6中,一般调试模式没有进行代码优化,所以这个关键字的作用看不出来。下面通过插入汇编代码,测试有无volatile关键字,对程序最终代码的影响。首先用classwizard建一个win32 console工程,插入一个voltest.cpp文件,输入下面的代码:

#include <stdio.h>
void main()
{
int i=10;
int a = i;

printf("i= %d/n",a);
//下面汇编语句的作用就是改变内存中i的值,但是又不让编译器知道
__asm {
  mov dword ptr [ebp-4], 20h
}

int b = i;
printf("i= %d/n",b);
}

然后,在调试版本模式运行程序,输出结果如下:

i = 10
i = 32

然后,在release版本模式运行程序,输出结果如下:

i = 10
i = 10

输出的结果明显表明,release模式下,编译器对代码进行了优化,第二次没有输出正确的i值。下面,我们把 i的声明加上volatile关键字,看看有什么变化:

#include <stdio.h>
void main()
{
volatile int i=10;
int a = i;

printf("i= %d/n",a);
__asm {
  mov dword ptr [ebp-4], 20h
}

int b = i;
printf("i= %d/n",b);
}

分别在调试版本和release版本运行程序,输出都是:

i = 10
i = 32

这说明这个关键字发挥了它的作用!

关于volatile的补充信息:
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
1. 并行设备的硬件寄存器(如:状态寄存器)
2. 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3. 多线程应用中被几个任务共享的变量
我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所用这些都要求volatile变量。不懂得volatile内容将会带来灾难。假设被面试者正确地回答了这是问题(嗯,怀疑这否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile的重要性:
1. 一个参数既可以是const还可以是volatile吗?解释为什么。
2. 一个指针可以是volatile 吗?解释为什么。
3. 下面的函数有什么错误:

int square(volatile int *ptr)
{
    return *ptr * *ptr;
}

下面是答案:
1. 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2. 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。
3. 这段代码有个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:

int square(volatile int *ptr)
{
    int a,b;
    a = *ptr;
    b = *ptr;
    return a * b;
}

由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:

long square(volatile int *ptr)
{
    int a;
    a = *ptr;
    return a * a;
}

mutable关键字


mutable的中文意思是“可变的,易变的”,跟constant(即C++中的const)是反义词。在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量(mutable只能由于修饰类的非静态数据成员),将永远处于可变的状态,即使在一个const函数中。
我们知道,假如类的成员函数不会改变对象的状态,那么这个成员函数一般会声明为const。但是,有些时候,我们需要在const的函数里面修改一些跟类状态无关的数据成员,那么这个数据成员就应该被mutalbe来修饰。下面是一个小例子:

class ClxTest
{
 public:
  void Output() const;
};

void ClxTest::Output() const
{
 cout << "Output for test!" << endl;
}

void OutputTest(const ClxTest& lx)
{
 lx.Output();
}

类ClxTest的成员函数Output是用来输出的,不会修改类的状态,所以被声明为const。
函数OutputTest也是用来输出的,里面调用了对象lx的Output输出方法,为了防止在函数中调用成员函数修改任何成员变量,所以参数也被const修饰。
假如现在,我们要增添一个功能:计算每个对象的输出次数。假如用来计数的变量是普通的变量的话,那么在const成员函数Output里面是不能修改该变量的值的;而该变量跟对象的状态无关,所以应该为了修改该变量而去掉Output的const属性。这个时候,就该我们的mutable出场了,只要用mutalbe来修饰这个变量,所有问题就迎刃而解了。下面是修改过的代码:

class ClxTest
{
 public:
  ClxTest();
  ~ClxTest();

  void Output() const;
  int GetOutputTimes() const;

 private:
  mutable int m_iTimes;
};

ClxTest::ClxTest()
{
 m_iTimes = 0;
}

ClxTest::~ClxTest()
{}

void ClxTest::Output() const
{
 cout << "Output for test!" << endl;
 m_iTimes++;
}

int ClxTest::GetOutputTimes() const
{
 return m_iTimes;
}

void OutputTest(const ClxTest& lx)
{
 cout << lx.GetOutputTimes() << endl;
 lx.Output();
 cout << lx.GetOutputTimes() << endl;
}

计数器m_iTimes被mutable修饰,那么它就可以突破const的限制,在被const修饰的函数里面也能被修改。

explicit构造函数


按照默认规定,只有一个参数的构造函数也定义了一个隐式转换,将该构造函数对应数据类型的数据转换为该类对象,如下面所示:

class String {
String ( const char* p ); // 用C风格的字符串p作为初始化值
//…
}
String s1 = “hello”; //OK 隐式转换,等价于String s1 = String(“hello”);

但是有的时候可能会不需要这种隐式转换,如下:

class String {
       String ( int n ); //本意是预先分配n个字节给字符串
String ( const char* p ); // 用C风格的字符串p作为初始化值
//…
}

下面两种写法比较正常:

String s2 ( 10 );   //OK 分配10个字节的空字符串
String s3 = String ( 10 ); //OK 分配10个字节的空字符串

下面两种写法就比较疑惑了:

String s4 = 10; //编译通过,也是分配10个字节的空字符串
String s5 = ‘a’; //编译通过,分配int(‘a’)个字节的空字符串

s4 和s5 分别把一个int型和char型,隐式转换成了分配若干字节的空字符串,容易令人误解。
为了避免这种错误的发生,我们可以声明显示的转换,使用explicit 关键字:

class String {
    explicit String ( int n ); //本意是预先分配n个字节给字符串
    String ( const char* p ); // 用C风格的字符串p作为初始化值
    //…
}

加上explicit,就抑制了String ( int n )的隐式转换。

下面两种写法仍然正确:

String s2 ( 10 );   //OK 分配10个字节的空字符串
String s3 = String ( 10 ); //OK 分配10个字节的空字符串

下面两种写法就不允许了:

String s4 = 10; //编译不通过,不允许隐式的转换
String s5 = ‘a’; //编译不通过,不允许隐式的转换

因此,某些时候,explicit 可以有效得防止构造函数的隐式转换带来的错误或者误解

explicit 只对构造函数起作用,用来抑制隐式转换。如:

class A {  
    A(int a);   
};
int Function(A a);   

当调用Function(2)的时候,2会隐式转换为A类型。这种情况常常不是程序员想要的结果,所以,要避免之,就可以这样写:

class A {   
    explicit A(int a);
};
int Function(A a);

这样,当调用 Function(2) 的时候,编译器会给出错误信息(除非 Function 有个以 int 为参数的重载形式),这就避免了在程序员毫不知情的情况下出现错误。



转载自:
1. C++中const、volatile、mutable的用法
2. C++笔记(1)explicit构造函数

<think>我们被要求回答关于C++关键字的问题。根据引用资料,我们可以总结出一些关键点,并结合常见的C++关键字进行说明。注意,C++的关键字很多,我们不可能全部列出,但会涵盖重要和常用的部分。 首先,引用[1]提到,在C++中,class和typename在模板参数声明中功能相同,但后来引入了typename来解决一些歧义问题。引用[2]则列举了constmutable、static、extern、volatile、inline、explicit等关键字。引用[3]和[4]分别给出了static在类中的使用示例和new/delete关键字的使用。引用[5]提到了export关键字(注意:export在模板中的使用在C++11中已被弃用,且多数编译器不支持)。 下面我们将分类介绍C++中的关键字(注意:C++标准的关键字列表很长,这里我们主要讨论一些重要且常用的关键字,并参考引用资料)。 ### 一、C++关键字分类与说明 #### 1. 数据类型相关 - `int`, `char`, `float`, `double`, `bool`:基本数据类型。 - `void`:表示无类型。 - `auto`(C++11):自动类型推导。 - `decltype`(C++11):查询表达式的类型。 #### 2. 类型修饰符 - `const`:表示常量,不可修改(引用[2])。 - `volatile`:表示变量可能被意外修改,禁止编译器优化(引用[2])。 - `mutable`:用于类的成员变量,即使包含它的对象是const,该变量也可以被修改(引用[2])。 - `signed`/`unsigned`:修饰整数类型,表示有符号或无符号。 #### 3. 存储类说明符 - `static`: - 在函数内部:使局部变量在函数调用之间保持其值,且只初始化一次(引用[2])。 - 在类中:静态数据成员被类的所有对象共享,静态成员函数不依赖于对象而调用(引用[3])。 - `extern`:声明变量或函数在外部文件定义(引用[2])。 - `register`(C++17弃用):提示编译器将变量存储在寄存器中。 - `thread_local`(C++11):声明线程局部存储。 #### 4. 函数相关 - `inline`:建议编译器将函数内联展开(引用[2])。 - `virtual`:声明虚函数,支持多态。 - `override`(C++11):显式注明重写基类虚函数。 - `final`(C++11):禁止派生类重写虚函数或禁止类被继承。 - `explicit`:禁止构造函数进行隐式转换(引用[2])。 - `constexpr`(C++11):表示常量表达式,在编译时求值。 #### 5. 类与对象 - `class`:定义类(引用[1])。 - `struct`:定义结构体(默认成员public)。 - `union`:联合体,所有成员共享内存(引用[2])。 - `private`/`protected`/`public`:访问控制。 - `friend`:允许非成员函数或其他类访问私有成员。 #### 6. 模板编程 - `template`:定义模板。 - `typename`:在模板中声明类型参数(引用[1])。 - `using`(用于类型别名,C++11):替代typedef。 #### 7. 内存管理 - `new`/`delete`:动态内存分配和释放(引用[4])。 - `new[]`/`delete[]`:动态数组分配和释放。 #### 8. 异常处理 - `try`/`catch`/`throw`:异常处理机制。 #### 9. 控制流 - `if`/`else`:条件语句。 - `switch`/`case`/`default`:多分支选择。 - `for`/`while`/`do`:循环语句。 - `break`/`continue`:循环控制。 - `return`:函数返回。 - `goto`:无条件跳转(不推荐使用)。 #### 10. 命名空间 - `namespace`:定义命名空间。 - `using`:引入命名空间或成员。 #### 11. 其他 - `sizeof`:获取对象或类型的大小(引用[2])。 - `typedef`:定义类型别名(引用[2])。 - `typeid`:获取类型信息(用于RTTI)。 - `dynamic_cast`/`static_cast`/`const_cast`/`reinterpret_cast`:类型转换运算符。 - `asm`:内联汇编。 - `export`:在模板中用于分离模板定义(引用[5]),但已被弃用。 ### 二、关键字的详细说明(部分) #### 1. `const`(引用[2]) - 修饰变量:变量值不可修改。 - 修饰函数参数:函数内不能修改参数。 - 修饰成员函数:该函数不能修改类的成员变量(除非成员是mutable)。 - 修饰函数返回值:返回值不可被修改。 #### 2. `static`(引用[2][3]) - 局部静态变量:在函数内,变量在程序生命周期内存在,但作用域为函数内。 - 静态成员变量:属于类,所有对象共享,必须在类外定义和初始化。 - 静态成员函数:属于类,只能访问静态成员变量,没有this指针。 #### 3. `mutable`(引用[2]) 用于类的成员变量,即使对象是const,该成员也可以被修改。常用于需要记录内部状态的场景。 #### 4. `inline`(引用[2]) - 建议编译器将函数体嵌入到调用处,以减少函数调用开销。 - 注意:编译器可能忽略inline请求。 #### 5. `new`和`delete`(引用[4]) - `new`:在堆上动态分配内存,并调用构造函数。 - `delete`:释放由`new`分配的内存,并调用析构函数。 - 数组形式:`new[]`和`delete[]`。 #### 6. `typename`(引用[1]) 在模板中用于声明类型参数,也可以用于指示嵌套依赖类型名(告诉编译器后面是一个类型)。 ### 三、示例代码(部分关键字) #### static关键字示例(引用[3]) ```cpp #include <iostream> using namespace std; class Myclass { public: Myclass(int a, int b, int c); void GetSum(); private: int a, b, c; static int Sum; // 声明静态数据成员 }; int Myclass::Sum = 0; // 定义并初始化静态数据成员 Myclass::Myclass(int a, int b, int c) { this->a = a; this->b = b; this->c = c; Sum += a+b+c; } void Myclass::GetSum() { cout << "Sum=" << Sum << endl; } int main() { Myclass M(1,2,3); M.GetSum(); // 输出6 Myclass N(4,5,6); N.GetSum(); // 输出21(6+15) M.GetSum(); // 输出21,因为Sum是共享的 return 0; } ``` #### new和delete示例(引用[4]) ```cpp #include <iostream> int main() { int* pi = new int(5); // 动态分配一个int,初始化为5 int* pa = new int[5]; // 动态分配一个数组,大小为5 *pa = 1; // 赋值第一个元素 std::cout << "pi_value:" << *pi << std::endl; // 5 delete pi; // 释放单个对象 delete[] pa; // 释放数组 // 注意:释放后不应再访问,以下行为未定义 // std::cout << "pi_value:" << *pi << std::endl; return 0; } ``` ### 四、注意事项 - 关键字在不同上下文可能有不同含义(如`class`在类定义和模板参数中)。 - C++标准不断更新,新关键字不断引入(如C++11引入的`auto`、`decltype`等),旧关键字可能被重新定义或弃用(如`export`)。 ### § 相关问题 § 1. C++中的`const`和`constexpr`有什么区别? 2. `static`关键字在C语言和C++中的异同? 3. 为什么C++11弃用了`export`关键字? 4. `new`和`malloc`在内存分配上有何区别? 以上内容参考了提供的引用资料,并进行了扩展。 </think>### C++关键字详解 C++关键字是语言预定义的保留标识符,具有特殊语义功能。以下是主要分类及核心关键字说明: #### 一、基本类型修饰符 1. **`const`** - 修饰变量:表示常量,值不可修改 ```cpp const int MAX = 100; // 编译时常量 ``` - 修饰函数:成员函数不修改对象状态 ```cpp class Data { int get() const; // 承诺不修改成员变量[^2] }; ``` - 修饰指针:`const int*`(指向常量) vs `int* const`(常量指针) 2. **`volatile`** - 标识变量可能被外部修改(如硬件寄存器),禁止编译器优化 - 可与`const`组合:`const volatile`表示硬件只读寄存器[^2] 3. **`mutable`** - 允许const成员函数修改特定变量 ```cpp class Cache { mutable int accessCount; // const函数可修改 }; ``` #### 二、存储类说明符 1. **`static`** - 局部变量:生命周期贯穿程序执行,作用域限定在函数内 ```cpp void counter() { static int count = 0; // 只会初始化一次 count++; } ``` - 类成员:所有对象共享同一实例 ```cpp class MyClass { static int shared; // 声明静态成员[^3] }; int MyClass::shared = 0; // 类外定义初始化 ``` 2. **`extern`** - 声明外部链接的变量/函数 ```cpp extern int globalVar; // 定义在其他文件中 ``` 3. **`thread_local`** (C++11) - 线程局部存储,每个线程有独立副本 #### 三、面向对象相关 1. **`class`/`struct`** - 定义类类型(`class`默认private继承,`struct`默认public) 2. **`virtual`** - 实现多态:虚函数支持动态绑定 ```cpp class Base { virtual void func(); // 可被派生类覆盖 }; ``` 3. **`explicit`** - 禁止隐式构造函数转换 ```cpp class String { explicit String(int size); // 阻止 String s = 10; }; ``` #### 四、内存管理 1. **`new`/`delete`** - 动态内存分配与释放 ```cpp int* arr = new int[10]; // 堆分配数组 delete[] arr; // 必须配对使用[^4] ``` #### 五、模板与类型 1. **`typename`** - 声明模板类型参数(与`class`等价但更明确) ```cpp template<typename T> void swap(T& a, T& b); ``` 2. **`using`** (C++11) - 替代`typedef`定义类型别名 ```cpp using Vec = std::vector<int>; ``` #### 六、函数控制 1. **`inline`** - 编译器内联优化建议(非强制) - 替代宏函数,保留类型检查[^2] 2. **`noexcept`** (C++11) - 声明函数不抛出异常 #### 七、其他关键特性 | 关键字 | 用途 | 示例 | |--------------|--------------------------|-------------------------| | `sizeof` | 获取对象/类型大小 | `sizeof(int)` | | `typedef` | 定义类型别名 | `typedef int* IntPtr;` | | `union` | 联合体(共享内存) | [参考
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值