Chapter 8. constants
一、介绍常量
//safecons.cpp
#include <iostream>
using namespace std;
const int i = 100;
const int j = i + 10;
long address = (long)&j;
char buf[j + 10];
int main(void)
{
cout << "type a character & CR: ";
const char c = cin.get();
const char c2 = c + 'a';
cout << c2 << endl;
}
1. 宏,它只存在于预编译期(preprocessing),不占据内存空间,不包含类型信息。
2. 常量,也在编译期被翻译。通常情况下默认为内部链接,并且不会为const分配内存。这
被称为const folding原则。但是这个原则在很多情况下会被打破。
特例有很多,主要一点就是常量的外部引用。比如extern声明会强制建立外部链接,这
要求修饰的const也要提供地址(内存空间所在位置)。
二、常量遭遇指针
// c08: const_pointer.cpp
const int *u; // pointer to const int, init not required
int const *v; // still pointer to const int!
int d = 1;
int * const w = &d; // const pointer
const int * const x = &d; // const pointer to const object
int const * const x2 = &d;
int main(void){}
指针也可以被声明常量,这里带来两个歧义:一个是指针指向的东西被声明常量(pointer
to const);另一个是指针包含的地址本身被定义为常量(const pointer)。
上面的例子就列举了两种定义,还有另外指向常量的常量指针。
1) 指向常量的指针(`const*')
可以不被初始化
2) 常量型指针(`*const')
必须初始化
3) 指向常量的常量指针(`const * const')
必须初始化
// c08: pointer_assignment.cpp
int d = 1;
const int e = 2;
int *u = &d;
//! int *v = &e; // illegal -- a const
int *w = (int*)&e; // legal but bad practice
int main(){}
这里举例说明了指针赋值。
1. 常量不可以再被非常量直接引用(指针和引用均不可以)。
2. 但是他可以被强制转换,转换的安全性需要程序员负责。
3. 不可以在文件域内部对指向常量指针赋值(p2c,cp都不行)
三、常量返回值 return consts by value
// c08: constval.cpp
// return consts by value
// has no meaning for built-in type
int f3() { return 1;}
const int f4() { return 1;}
int main(void)
{
const int j = f3(); // Works fine
int k = f4(); // But this works fine too!
}
原子类型之间的赋值,只是拷贝,勿需考虑常量情况。
// c08: const_return_values.cpp
// const return by value
// result cannot be used as an lvalue
class X {
int i;
public:
X(int ii = 0);
void modify();
};
X::X(int ii)
{
i = ii;
}
void X::modify()
{
i++;
}
X f5()
{
return X();
}
const X f6()
{
return X();
}
void f7(const X& x) // pass non-const reference
{
x.modify();
}
int main()
{
f5() = X(1);
f5().modify();
// compile-time error
//! f6() = X(1);
//! f6().modify();
//! f7(f5());
//! f7(f6()); // 29
}
将返回值定义为常量,这对于原子类型来说没有意义。而在自定义类型中,却有很大不同。
NOTE!
如果函数返回一个常量类型的类对象,那么返回的值将不能成为左值(不可被定义和修改)。
而原子类型本身就不能成为左值,所以返回常量原子类型是无意义的。
f5()返回了非常量型,而f6()返回的是常量型的类对象,因此f6()的赋值和修改都是非法的
(f6不可以作为左值)。
四、传递和返回地址 passing and returning addresses
// c08: const_pointer2.cpp
void t(int*) {}
void u(const int *cip)
{
//! *cip = 2; // illegal -- modifies value
int i = *cip; // OK -- copies value
//! int *ip2 = cip; // illegal: non-const
}
const char *v()
{
// return address of a static array
return "result of function v()";
}
const int * const w()
{
static int i;
return &i;
}
int main()
{
int x = 0;
int *ip = &x;
const int *cip = &x;
// passing addresses
t(ip); // non-const* to non-const*
//! t(cip); // const* to non-const*
u(ip); // non-const* to const*
u(cip); // const* to const*
// returning addresses
//! char *cp = v(); // const* to non-const*, not OK
const char * ccp = v(); // const* to const*
//! int *ip2 = w(); // const* to non-const*, not OK!
const int * const ccip = w(); // const* to const*
const int * cip2 = w();
//! *w() = 1; // modify consts, not OK
}
无论是传递地址,还是返回地址。从const*到non-const*的传递(或赋值)都是被禁止的,
而如果选择将const转换为non-const,则需要确保转换的安全性。
五、常量成员的初始化
// c08: const_init.cpp
#include <iostream>
using namespace std;
class Fred {
const int size;
public:
Fred(int sz);
void print();
};
Fred::Fred(int sz) : size(sz) {}
void Fred::print() { cout << size << endl; }
int main()
{
Fred a(1), b(2), c(3);
a.print(), b.print(), c.print();
}
这里展示了构造方法的初始化列表的用法。形如:
Fred::Fred(int sz) : size(sz) {}
函数列表括弧后的单位就是,常量初始化的地方。唯有这里可以将类中定义的常量成员初始
化,实际上,他们是在进入函数体之前就进行了初始化。
六、const 成员遭遇 static
// c08: string_stack.cpp
// using static const to create a compile-time constant
// inside a class
#include <cstring>
#include <string>
#include <iostream>
using namespace std;
class string_stack {
static const int size = 100; // a static constant
const string *stack[size];
int index;
public:
string_stack();
void push(const string *s);
const string *pop();
};
string_stack::string_stack():index(0)
{
memset(stack, 0, size * sizeof(string*));
}
void string_stack::push(const string *s)
{
if (index < size)
stack[index++] = s;
}
const string *string_stack::pop()
{
if (index > 0) {
const string *rv = stack[--index];
stack[index] = 0;
return rv;
}
return 0;
}
const string icecream[] = {
"pralines & cream",
"fudge ripple",
"jamocha almond fudge",
"wild mountain blackberry",
"raspberry sortbet",
"lemon swirl",
"rocky road",
"deep chocolate fudge"
};
const int icsz = sizeof(icecream) / sizeof(*icecream);
int main()
{
string_stack ss;
for(int i = 0; i < icsz; i++)
ss.push(&icecream[i]);
const string *cp;
while((cp = ss.pop()) != 0)
cout << *cp << endl;
}
这里展示了C++ string的强大与方便。但是更重要的是,解释了compile-time constants的
用法。
static类成员意味着,无论用此类建立多少个对象,static成员实例都只会存在一个。
另外static const的特性要求,static const常量(原子型)必须在定义位置初始化。
从例子中可以看到,push持有const string*参数,pop()则返回const string*,还有
string_stack包含了一个const string*成员。这看来像一个锁链一样,一环套一环。
stack_stack包含const string *stack[],这要求在pop():
* 中间赋值必须用const传递, const string *rv;
* 返回值类型必须是const.
const string *icecream也要求push():
* 持有const参数
三个const约定了字符串以const形式亚栈,而除了push和pop以外,你再也无法更改
成员*stack[]。
七、const 成员方法
// c08: const_member.cpp
class X {
int i;
public:
X(int ii);
int f() const; // const member func
int g();
};
X::X(int ii): i(ii) {}
int X::f() const { return i;}
int X::g() {}
int main()
{
X x1(10);
const X x2(20);
x1.f();
x2.f();
//! x2.g(); // discards qualifiers
}
const成员方法,将会告诉编译器,他可以被常量对象调用。而没有被申明常量的方法,将
不可以被常量对象调用!
const成员方法和const函数(const func)有本质不同:
* const func 是返回一个常量值。
* func const 则是表明此成员方法可以被常量对象调用。他的使用方法是:
为了强调const方法,编译器强迫在const成员方法的声明和定义处都要加上const。
接下来细细观察const成员方法和普通成员方法的不同——
// c08: quoter.cpp
// random quote selection
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
class quoter {
int lastquote;
public:
quoter();
int get_lastquote() const; // const member func
const char *quote(); // member func which returns a const pointer
};
quoter::quoter()
{
lastquote = -1;
srand(time(0));
}
int quoter::get_lastquote() const
{
return lastquote;
}
const char *quoter::quote()
{
static const char *quotes[] = {
"Are we having fun yet?",
"Doctors always know best",
"Is it ... Atomic?",
"Fear is obscene",
"There is scientific evidence "
"to support the idea "
"that life is serious",
"Things that make us happy, make us wise",
};
const int qsize = sizeof quotes / sizeof *quotes;
int qnum = rand() % qsize;
while (lastquote >=0 && qnum == lastquote)
qnum = rand() % qsize;
return quotes[lastquote = qnum];
}
int main()
{
quoter q;
const quoter cq;
cq.get_lastquote();
//! cq.quote(); // 常量型对象无法调用非常量型成员方法
for (int i = 0; i < 20; i++)
cout << q.quote() << endl;
}
这里比较了const对象和普通对象处理的区别,另外揭示了const成员方法的条件:
如果成员方法体内更改了数据成员,那么它就不能声明为const。
比如const char *quoter::quote()更改了成员 lastquote(return语句),它将不能声明
为const;而int quoter::get_lastquote()则没有动,他就可以成为const成员方法,而且
可以被常量对象cq安全调用。
如果我确实声明了const成员方法,我又确实想更改类成员,我该如何是好?
八、bitwise vs. logical const
// c08: castaway.cpp
class Y {
int i;
public:
Y();
void f() const;
};
Y::Y(): i(0) {}
void Y::f() const
{
//! i++;
((Y*)this)->i++;
// Better: use C++ explicit cast syntax:
(const_cast<Y*>(this))->i++;
}
int main()
{
const Y yy;
yy.f();
}
bitwise const意思是说,此对象的每一数据比特都是永久不可变的,这样它的任何一部分
都不可改变;而logical const意思是,虽然从概念上说整个对象不可改变,但是可以从成
员基础上更改它本身。
这其实是编译器给const定义的两面性,一方面编译器小心翼翼地确保const对象是bitwise
的,另一方面,它又提供两个路径来通过_const成员函数_来更改数据成员。
以上就是第一个:转换掉const属性。无论是C还是C++语法,都一样可行。
这样带来的一个问题就是,你无法确认你的更改是否有效。。。
于是提供了第二个方法:
// c08: mutable.cpp
class Z {
private:
int i;
mutable int j;
public:
Z();
void f() const;
};
Z::Z(): i(0), j(0) {}
void Z::f() const
{
//! i++; // error - const mem func
j++;
}
int main()
{
const Z zz;
zz.f();
}
mutable 关键词告诉编译器,被定义为mutable的数据成员,在const成员方法内仍旧可以更
改。相对转型,显然mutable更加优雅一点。
TIC++也提到了只读属性(ROMability),如何指导编译器把const对象放到只读内存中呢?
这个条件比较复杂,任何可以改写数据成员的可能都会阻止只读化:
1) 不包含logical const特性(不能有mutable关键词,也不能有const转型)
2) class或者struct不可以包含任何自定义构造和析构方法
3) 也不可以包含有自定义构造和析构方法的超类和成员对象(子类没有限制)
九、volatile和类
// c08: volatile.cpp
class comm {
const volatile unsigned char byte;
volatile unsigned char flag;
enum { bufsize = 100 };
unsigned char buf[bufsize];
int index;
public:
comm();
void isr() volatile;
char read(int index) const;
};
comm::comm(): index(0), byte(0), flag(0) {}
// only a demo; won't actually work as an interrupt service routine:
void comm::isr() volatile
{
flag = 0;
buf[index++] = byte;
if (index >= bufsize)
index = 0;
}
char comm::read(int index) const
{
if (index < 0 || index >= bufsize)
return 0;
return buf[index];
}
int main()
{
volatile comm port;
port.isr(); // OK
//! port.read(0); // Error, read() not volatile
}
volatile关键字和const是极其相似的修饰词,只不过他们的意义不同。
通常情况下,编译器会告诉我们:我把此数据读入到了寄存器,但我保证不会再去碰它。下
次需要读取此变量,直接从寄存器中读取即可(而不需要到内存中寻找)。
而volatile告诉编译器:这个数据成员的活动,将会超过编译器的理解范围。如果需要读取
此数据,不要尝试从寄存器中取值,因为内存和寄存器可能已经出现了不同步。
同const一样,volatile对象也只能调用volatile成员方法。
十、总结
const一章的例子比其他章节要多,原因是const类型在C++中遇到了多种繁杂的情况,而且
他们也表现出来各自的特性。总结来说,我们探讨了以下情况:
* 原子类型申明为const(这里也讨论了const*和*const)
const声明意味着,这意味着该内存为只读属性。const存在于编译期,通常情况下默认为
内部链接,并且不会为const分配内存,这被称为const folding原则。const类型的局部
标识符必须在定义时候初始化。
const指针包括了指向常量的指针(const*)和常指针(*const),const*可以选择是否初始
化,但是他不可以显性修改指针指向的内存(*x = y illegal);*const不可以修改指针
本身((int*)a = (int*)b illegal)。
* 返回值为const
const返回值将不可作为左值,赋值修改都是非法的。而作为右值时候,const返回值的左
值必须为const,const到non-const的传递和赋值都是非法的。
* 传递参数为const
const到non-const的传递仍旧是非法的。要切记这一规则。
* 成员数据为const
const成员数据不可以在普通成员方法中更改。这里也给出了特例(const_init.cpp):
使用构造方法的初始化列表来完成常量数据成员的初始化。
* 成员数据为static const
static说明所有的成员实例都会共享一个static数据成员。static const额外要求必须在
定义位置实现初始化。
* 成员方法为const
首先,一个const成员方法将永远不会更改类的数据成员。
再次,声明为const的成员方法允许被const对象调用,而普通成员方法是不允许调用的。
* 对象为const
const对象表现出两个特性,一个是表面上他的整体不可改变(bitwise const),另一个
则是通过某些特殊方法实现更改(logical const)。
logical const的实现有两种方法,一个在const成员方法使用强制类型转换
(castaway.cpp),另一个是在类定义中给想要修改的值添加mutable修饰词
(mutable.cpp)。
_
没有评论:
发表评论