索引
前言
在C/C++中,常量是固定值或字面量,且在程序执行期间不会改变。常量可以是任何的基本数据类型,比如整数常量、浮点常量、字符常量,或字符串字面值,也有枚举常量;也可以是自定义类型。
const关键字
const关键字,也叫const限定符。用来声明或定义一个常变量。
语法:
extern const type name; // 声明
const type name = value; // 定义
// 说明 若type中不含指针类型,const与type的位置可以互换,即const int与int const是一样的
// 即定义了一个名为name的变量,它的类型为const int,值为value,且不可被修改
// 我们判断一个变量类型的时候,把变量名拿掉,剩下的就是它的类型
常量与普通变量的区别是,常量的值在定义后不能进行修改,且常量在定义时必须初始化。
接下来我们探讨一下const在C/C++中的用途,主要分为两部分,其一为const修饰限定变量或对象,其二为const修饰函数。
用途一:const关键字限定该变量的内容在程序运行期间不会改变。
代码片段:
const int a = 10;
a = 20; // 错误 a由const限定,不能给a赋值
我们知道,通过指针可以修改其指向变量的值,那么通过指针,可以修改由const限定变量的值吗?
const int a = 10;
int * p = (int *)&a;
*p = 20; // a现在的值是多少???
执行完*p = 20;之后,a的值是不是被修改为20了呢?
VC6、VS2013以及gcc 4.4.7上,a的值均保持原值不变,为10。
const对象a不能修改,尝试直接这么做是编译时错误,且尝试间接这么做(例如通过到非 const 类型的引用或指针修改 const 对象)导致未定义行为。
再看下面的例子(gcc环境下)
struct {int a; const int b; } s1 = {.b=1}, s2 = {.b=2};
s1 = s2; // 错误: s1 的类型无限定,但它有 const 成员
综上,我们可以看到,指代 const 限定类型对象的左值表达式,和指代拥有至少一个 const 限定类型成员(包含为聚合体或联合体所递归含有的成员)的结构体或联合体的左值表达式,不是可修改左值。具体而言,它们不可赋值。
A member of a const-qualified structure or union type acquires the qualification of the type it belongs to (both when accessed using the . operator or the -> operator).
cosnt 限定的结构体或联合体类型的成员,同样具有成员所属类型的限定类型。
struct s { int i; const int ci; } s;
// the type of s.i is int, the type of s.ci is const int
const struct s cs;
// the types of cs.i and cs.ci are both const int
再来看看const修饰数组会怎样:
const int arr[2] = {2, 3}; // 数组名本身即是常量,此处const限定符修饰的是数组成员
arr[0] = 1; // error arr[0]具有const属性
用途二:const修饰指针或引用
const与指针结合,是使用最为频繁的,也是使初学者望而却步的知识点之一。
下面以char * 类型的指针来举例说明:
我们先来看看const可以放在哪些位置,如下图:
const必须放置在变量名之前,所以,const只能放在编号为1、2或3的位置。在确定p的类型时,先把p拿掉,然后从右向左读,就可以确定其类型。
序号 | 组合 | 说明 |
---|---|---|
1 | const char * p = str | 定义了一个const char * 类型的指针p, 不能通过p修改str的内容, 但p可以指向其他的char * 或 const char *类型的变量或常量 |
2 | char const * p = str | 由定义常变量的语法可知,同1 |
3 | char * const p = str | 定义了一个char * const类型的变量, const限定的是变量p, p不能再指向其他的char * 或const char *的变量或常量 但可以通过p修改str的内容 |
4 | const char * const p = str | 定义了一个const char * const类型变量p, 左起第一个const表示p所指向的是一个常量类型的变量或常量, 第二个const限定了变量p的指向, 表示p只能够指向str,不能再指向别的变量或常量 |
5 | char const * const p = str | 同4 |
从这张表不难看出,只要正确分析出了变量p是什么类型的,就可以了解到它可以做哪些操作了。
下面来看一下C++中的引用:
int main(void)
{
int a = 10;
int & ref_a = a; // ok 定义了一个到int类型的引用,并与a关联
const int & ref_b = a; // ok 定义了一个到const int类型的引用,并与a关联
const int b = 20;
int & ref_c = b; // error 试图定义一个到int类型的引用,
// 并与const int类型的变量b关联
const int & ref_d = b; // ok 定义了一个到const int类型的引用,并与b关联
ref_a = 20; // ok 通过引用ref_a修改a的值
ref_b = 20; // error 虽然ref_b也是a的别名,但ref_b被const限定了
ref_d = 10; // error 无论b是否具有const属性,但ref_d被const限定了
// 那么怎么通过具有const的引用,来修改没有const限定变量的值呢?
// 我们可以通过const_cast来给相关变量去掉或添加上const属性
const_cast<int &>(ref_b) = 20; // ok 去掉ref_b的const约束,并通过ref_b来修改a的值
const_cast<int &>(ref_d) = 200; // ok 去掉ref_d的const约束,并试图通过ref_d来修改b的值
// 很遗憾,虽然语法正确,但其行为是未定义的。
return 0;
}
C++中,被const限定的引用可以指向该类型的普通变量,但普通引用不能指向被const限定的该类型的变量。即可以由更多限定的引用指向更少限定的变量,但不可以由更少限定的引用指向更多限定的变量。
type a;
const type b;
const type & ref_a = a; // ok
type & ref_b = b; // error
用途三:const修饰函数参数
语法:
return_type function_name(const type * var1, const type * var2); // C/C++
return_type function_name(const type & var1, const type & var2); // C++
const修饰形参列表时,表示不希望该参数在函数内部被修改。C中参数的传递方式有两种,一种是传值,另一种是传地址[其时也是传值,只不过这个值是地址],C++中又引入了第三种方式,即引用方式。在使用const修饰普通类型的形参变量时,由于编译器采取的是传值方式,编译器会复制一个临时变量传递到函数内部,这个临时变量除了值与实参一样外,再没有其他关联,因此,使用const修饰这种类型的形参,虽然语法上没有什么问题,但是没有实际意义。
void foo(int a)
{
a = 200;
}
// 使用const,表明p为输入参数,且不希望在函数内部修改它的值
void goo(const int * p)
{
// *p = 200; // 编译器可以保证p所指内容不被修改,这样写编译器会报错
int * q = (int *)p;
*q = 200; // 如果p所指的实参未被const限定,其值会被修改,否则不会被修改
}
int main(int argc, char ** argv)
{
const int a = 100;
int * p = (int *)&a;
foo(a);
goo(p);
return 0;
}
结合这张图,在调用foo(a)时,编译器会生成一个临时变量,我们设其为b,b与a的值相同,地址不同,是两个不同的变量,在foo()内部修改b的值,对实参a没有任何影响。调用goo(p)时,编译器依然会生成一个临时变量,假设为q[与goo()中的局部变量q不同],p与q的值都是实参a的地址,如果实参a没有被const限定,那么goo()中执行*q = 200;后,实参a将被修改为200。但是由于a被const限定了,即使调用了goo(),a的值仍然没有被修改。
下面来关注一下C++中对自定义类型的传值是怎么处理的。
class A
{
public:
A() { cout << "A ctor" << endl; }
A(const A &) { cout << "A copy ctor" << endl; }
A operator=(const A &) { cout << "A operator = " << endl; }
~A() { cout << "A dtor" << endl; }
// ...
};
void foo(A a)
{
}
void foo(A * a)
{
cout << "foo &a: " << static_cast<void *>(&a) << endl;
}
void goo(A & a)
{
cout << "goo &a: " << static_cast<void *>(&a) << endl;
}
int main(int argc, char ** argv)
{
A a; // 调用A的构造函数来创建对象a,main结束时调用A的析构函数
A * p = &a; // 定义指针变量不会调用构造函数
cout << "&a: " << static_cast<void *>(&a) << endl;
cout << "&p: " << static_cast<void *>(&p) << endl;
foo(a); // 调用A的拷贝构造函数,调用foo()结束时,会调用A的析构函数释放临时对象
foo(&a); // 不会调用A的拷贝构造函数
goo(a); // 不会调用A的拷贝构造函数
return 0;
}
运行结果
A ctor
&a: 0x7ffe14a01f16
&p: 0x7ffe14a01f08
A copy ctor
A dtor
foo &a: 0x7ffe14a01ee8
goo &a: 0x7ffe14a01f16
A dtor
可见,C++中对自定义类型的处理,与内建类型是一致的,即自定义类型的普通参数,也会产生临时对象,如果对象包含的成员信息比较多,那么复制操作将会是一笔可观的开销。而使用引用可以节省这笔额外的开销,但是又面临一个新的问题,引用的对象可能会在函数内部被修改。因此,如果希望尽可能的提高效率,且对象不被修改,需要使用const来修饰该自定义类型的引用。
示例中,对于指针类型及引用类型的参数列表没有使用const限定符,因此在函数内部,修改参数的内容,会影响到实参,在我们希望参数是输出参数,或既是输入又是输出参数时,不需要加const限定符;如果我们希望实参不在被调函数中修改,或用来说明参数仅仅是输入参数,则需要使用const限定符。
在C/C++中,如果函数参数[指针类型或引用]被const修饰了,则说明这是一个输入参数,且不希望该参数在函数内部被修改。
C99标准以后,函数声明中,const 可出现在用于声明数组类型函数参数的方括号内。它限定数组类型变换到的指针类型。下列二个声明声明同一函数:
void f(double x[const], const double y[const]); // C89环境下编译错误
void f(double * const x, const double * const y);
用途四:修饰类的成员变量
由于使用const限定的非外部变量必须初始化,因此,类的成员变量使用const修饰时,必须初始化该变量。可以使用两种方式来初始化const修饰的变量,其一是使用初始化列表,其二是同时将其定义成静态的,可转化成整型类型的变量,并初始化。
class A
{
public:
A() : a(20) {} // ok
// A() { a = 20; } // error
// ...
private:
const int a;
};
class B
{
public:
B() {} // ok
// B() : pdbl(&db) {} // ok
// B() {pdbl = &db; } // ok
// ...
private:
const static int b = 100; // ok
static const char c = 'a'; // ok
static const double = 200; // gcc ok, vs2013 failed,要求必须是整型
// const static char * p = "hello world"; // failed
// const string str = "hello"; // gcc failed, vs2013 ok
const char * p; // ok
const string str; // ok
const string * pstr; // ok
const double * pdbl; // ok
double db;
};
如果类的成员变量需要设置为某类型的常量,可以使用const限定符来修饰,并在初始化列表中将其初始化。
如果成员变量是整型的,且对类的所有对象均有效,则可将其定义为const static type类型的变量,type为整型或可转换为整型的类型,如char。
用途五:限定函数返回值
在什么情况下,函数的返回值需要使用const来限定呢?
当然是不希望返回值被修改的情况下使用const了。问题是,是不是所有的返回类型都可以使用const来限定呐?先来看下面一个例子:
int foo()
{
return 0;
}
int goo()
{
int ret = 10;
return ret;
}
这类函数返回的并非左值,而const是用来限定一个左值的,表示该变量的值在运行期间不会被修改。因此,此类函数的返回值没有必要使用const来限定。
接下来看一下指针类型的返回值[C/C++]:
int * foo()
{
int a[2] = {10};
return a; // 这种返回一个局部变量地址的函数,是存在问题的,
// 调用结束后,函数的栈空间已被销毁,加上const也是没有意义的
}
char * goo()
{
char * p = "HELLO";// 字符串文字量"HELLO"是静态分配的,其类型为const char[6]
return p; // p是其地址,将其返回是可以的
}
int main(void)
{
goo()[0] = 'h'; // 试图将"HELLO"首字符修改为'h'
return 0;
}
这段代码编译是没问题的,但运行期间会出异常或段错误,原因是 “HELLO”是const char[6]类型的。为了能够及早发现这类错误,我们可以让编译器在编译阶段就诊断出这类问题,只需要对goo()的返回类型使用const进行限定即可。
修改如下:
const char * goo()
{
char * p = "HELLO";
return p;
}
// 在函数内部修改str指向的变量,并返回str
const char * goo(const char * str)
{
char * p = (char *)str;
while(*p) ++(*p), ++p;
return str;
}
char * foo(const char * str)
{
char * p = (char *)str;
while(*p) ++(*p), ++p;
return (char *)str;
}
int main(void)
{
char buf[] = "HELLO"; // 仅希望buf在goo()/foo()内部被修改
goo()[0] = 'h'; // error 编译期间就可以发现问题
goo(buf)[0] = 'h'; // error
foo(buf)[0] = 'h'; // ok buf[0]被修改为'h'
return 0;
}
为了避免写出形似foo()[index] = value或(*foo())++等类似组合时,可以给函数返回类型使用const加以限定。
接着来看返回引用的情况[C++]:
int & foo(int a, int & b)
{
return b += a;
}
int main(void)
{
int x = 10;
foo(10, x); // ok x = 20
foo(10, x) = 100; // ok x = 100
foo(10, x)++; // ok x = 111
return 0;
}
显然,上面有两行代码虽然能完美的运行,但其写法并不是我们希望的。这时,有必要使用const来限定一个函数的返回值。
const int & foo(int a, int & b)
{
return b += a;
}
int main(void)
{
int x = 10;
foo(10, x); // ok x = 20
foo(10, x) = 100; // error
foo(10, x)++; // error
x += 1; // ok const限定的是函数返回的引用,并没有限定x
return 0;
}
由于C++中提供了对运算符的重载,因此不合理的重载可能会发生意想不到的结果。下面我们来看一段代码:
class ABC
{
public:
ABC() : x(0) {}
friend ABC operator+(const ABC & op1, const ABC & op2)
{
ABC a;
a.x = op1.x + op2.x;
return a;
}
friend bool operator==(const ABC & op1, const ABC & op2)
{
return op1.x == op2.x;
}
operator bool()
{
return 0 != x;
}
// ... other
private:
int x;
};
int main()
{
ABC a, b, c;
a = b + c; // ok 这是我们想要的
(a + b) = c; // ok 合法,但这是不正常的
if ((a + b) == c) // ok 我们的本意就是比较
0;
if ((a + b) = c) // ok 本意是比较操作,但此处变成了赋值操作
0;
return 0;
}
为了去除这种畸形的代码,让编译帮我们发现更多的错误,以提高代码健壮性,需要给相关的重载函数添加const限定。
friend const ABC operator+(const ABC & a, const ABC & b) {...}
综上,我们可以总结到,需要使用const来限定其返回值类型的函数,其声明形式一般如下:
const return_type * function(parameters-list);
const return_type & function(parameters-list);
用途六:限定类的成员函数
我们先来看一下一般函数的声明形式:
[qualifier] return_type [*/&] function([parameters-list]);
// [] 表示这部分是可选的
// qualifier 即CV限定符const volatile
那么C++中用来限定成员函数的const被安放在了什么位置呢?
我们拿int来说明:
const int * const foo(const int * const p);
猛一看好像没有什么位置可以再放一个const了。仔细一看,似乎只有函数调用符()与;之间还有一个位置可用。得喽,就把const放这了吧。[这只是我自己臆想的啦]
示例代码:
class A
{
public:
void foo() { x = 1; }
void goo() { }
void hoo() const { } // ok const成员函数没有修改成员变量
void koo() const { x = 1; } // error const成员函数不能修改成员变量
void loo() const { foo(); } // error const成员函数不能调用非const成员函数
void moo() const { goo(); } // error 同上,尽管goo()什么也没有做
void yoo() const { hoo(); } // ok const成员函数只能调用const成员函数
private:
int x;
};
int main(void)
{
A a;
const A b = a;
a.foo();
a.hoo();
b.foo(); // error b是A的const对象,只能调用const成员函数
b.hoo(); // ok
return 0;
}
通过示例,我们可能得到一些const成员函数的使用方法及特征。为什么会有这些特征呢?
我们知道,对象在调用成员时,隐式的传入了this指针,默认情况下,this为class_name * const类型,而使用const限定成员函数时,const实际上是修改了this的类型,此时,this为const class_name * const类型,由于this指向的是当前对象,即当前对象为const对象,其成员变量也就具有了const约束,因此,在const成员函数内部修改成员变量会被视为非法操作。
总结一下const成员函数的作用及注意点:
const成员函数内部不能修改成员变量;
const成员函数只能调用const成员函数,非const成员函数可以调用任意成员函数;
const对象的成员变量具有const约束,参考用途一,const对象只能调用const成员函数;
与const相关的常见面试题
待续。。。。。。