第一部分:你可能不知道的 C++(一)
此为《你可能不知道的 C++》的第二部分,讨论 类型 , 内联 & 模板 。
我们不讨论原始类型(int, double, etc.)和结构。原始类型比较简单,而结构其实就是类。
本节包含以下几个方面:
联合
类
指针和引用
常量
转型
联合(union)的元素共用同一块内存空间,联合的大小就是最大元素的大小。联合里所存对象的类型,编译器是不知道的,所以虽然它可以有成员函数,也没有多大意义。
在 Win32 里,特定于消息(message)的数据通过两个 32 位的参数来传递: WPARAM
和 LPARAM
。
LRESULT MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
用户得记住每一种消息所对应的 WPARAM
和 LPARAM
的含义,这样非常不便。下面是 Win32 消息处理函数的典型写法:
{ switch (msg) { case WM_USER: if (lParam == WM_RBUTTONUP) { /*...*/ } break; case WM_COMMAND: switch (LOWORD(wParam)) { case IDM_EXIT: SendMessage(hwnd, WM_CLOSE, 0, 0); break; case IDM_CREATE: create(); break; //... } // ... } }
而 Xlib 通过联合,既保证了类型安全,又方便了使用。每一种事件(event,等价于 Win32 下的消息),都封装在一个结构中,比如 XKeyEvent
, XButtonEvent
,等等,它们各有其自身特定的字段,但是开头几个字段是公共的,叫 XAnyEvent
。Xlib 把这些结构统一在联合 XEvent
中:
typedef union _XEvent { XAnyEvent xany; // 公共字段 XKeyEvent xkey; XButtonEvent xbutton; XMotionEvent xmotion; ... } XEvent;
第一个公共字段为 type
,记录事件类型:
struct XAnyEvent { int type; // ... }
接下来,事件处理的代码清晰易懂:
{ switch (evt.xany.type) { case <按钮事件>: // 通过 evt.xbutton 访问按钮事件的特定字段 break; case <键盘事件>: // 通过 evt.xkey 访问键盘事件的特定字段 break; // ... } }
两相比较,高下自见分晓。
联合最著名最巧妙的应用就是测试大端(bit endian)小端(little endian),Linux 内核就是这么做的:
static union { char c[4]; unsigned long mylong; } endian_test = { { 'l', '?', '?', 'b' } }; #define ENDIANNESS ((char)endian_test.mylong)
用法:
if (ENDIANNESS == 'l') /* little endian */ if (ENDIANNESS == 'b') /* big endian */
第一点:访问控制作用于类而非对象。
怎么理解?看一个例子。
class foo { public: int bar(foo* f) { return a + f->a; } private: int a; };
虽然 a
是私有成员,但在成员函数 bar()
里依然可以访问 f->a
。
反过来考虑,如果访问控制作用于对象级别,那么一切都得是 public
才行,否则像拷贝构造函数这样的操作就没办法实现了。
留一个问题,下面的代码可以通过编译吗?
class B { protected: int a; }; class D : public B { public: int f(B* b) { return a + b->a; // ? } };
第二点:访问控制作用于编译时而非运行时。
还是来看一个例子。
class Node { public: Node() : value(0) { } int getValue() const { return value; } private: int value; };
虽然成员变量 value
是私有的,且没有提供 setValue()
方法,但是只要“知道”它的地址,仍然可以改变它:
Node node; *reinterpret_cast<int*>(&node) = 1; // value被改成了1
我们断定 value
的地址就是 Node
对象的地址,如果 Node
有虚函数或基类, value
的地址就难说了。
成员函数指针可能不单单是一个指针(指向成员函数代码的起始地址),它可能是一个小型的结构,编码了很多额外的信息,比如函数是否虚拟(virtual)、是否多继承,等等。
成员函数指针自身不可提领(dereferenced),必须借助具体的对象才能调用。
假设有一个图形库,基类为 Graphic
:
class Graphic { public: virtual void draw() {} };
成员函数 draw()
的指针记为:
void (Graphic::*draw)() = &Graphic::draw;
注意取址符 &
是不能省略的,这一点跟正常的函数指针不太一样。
可以用 typedef
让类型更清晰一些:
typedef void (Graphic::*draw_t)(); draw_t draw = &Graphic::draw;
要提领成员函数指针,必须借助一个对象,这个对象也就充当了 this
指针的作用。
Graphic g; // 定义一个对象 (g.*draw)(); // 在这个对象上调用函数指针
如果成员函数没有访问对象的成员变量,甚至可以通过 NULL
来提领。
((Graphic*)NULL)->*draw();
但是这种情况非常少见,也没什么意义。不过 Linux 内核的链表就是基于这一点来实现的,宏 list_entry
根据结点的字段反推结点的地址:
#define list_entry(ptr, type, member)/ ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
其中, &((type *)0)->member
即是在空对象上访问字段 member
,再取址从而得到 member
字段相对于结构的地址偏移。
扯远了,回归正题。提领成员函数指针的语法还是挺让人困惑的,可以定义一个宏来辅助:
#define CALL_MEMBER_FN(obj, mfp) ((obj).*(mfp)) CALL_MEMBER_FN(g, draw);
成员函数指针的大小跟一般指针也不一样。再看几个图形库的类:
// 直线 class Line : public Graphic { public: virtual void draw() {} }; class Listener { }; // 垂线 class OrthoLine : public Graphic, public Listener { public: virtual void draw() {} };
在 GCC 和 MSVC 上的测试结果(32位):
GCC | MSVC | |
---|---|---|
sizeof (&Graphic::draw) | 8 | 4 |
sizeof (&Line::draw) | 8 | 4 |
sizeof (&OrthoLine::draw) | 8 | 8 |
在类的继承结构中使用成员函数指针,不可避免的需要转型,直接使用 static_cast
即可。
从子类转到基类:
typedef void (Graphic::*graphic_draw_t)(); graphic_draw_t draw = static_cast<graphic_draw_t>(&Line::draw);
从基类转到子类:
typedef void (Line::*line_draw_t)(); line_draw_t draw = static_cast<line_draw_t>(&Graphic::draw);
在界面框架里,比如 wxWidgets 和 MFC,成员函数指针一般指向事件(消息)处理函数,存放在消息映射表里。
先来读个程序吧,能猜到输出吗?(1987 年第四届 IOCCC,最佳单行程序,David Korn)
main() { printf(&unix["/021%six/012/0"],(unix)["have"]+"fun"-0x60);}
猜不到?好吧,给点提示,下面这些表达式结果都一样:
x[3] *(x+3) *(3+x) 3[x]
还是猜不到?好吧,更多提示: http://www.ioccc.org/1987/korn.hint
给定类型 T
的指针( T
不为 void
):
T* p;
p + 1
就等价于:
static_cast<char*>(p) + sizeof(T) static_cast<uintptr_t>(p) + sizeof(T) static_cast<size_t>(p) + sizeof(T)
对 void*
做加减没有任何意义,且不能编译:
void* p; // ... ++p; // 不能编译! p = p + 1; // 不能编译!
两个指针相减,所得差值可以保存在 ptrdiff_t
或 intptr_t
类型的变量中:
int *p1, *p2; ptrdiff_t d = p2 - p1; // or intptr_t d = ...
ptrdiff_t
和 intptr_t
这两个类型与 size_t
类似,在表达指针差值方面,既正确又可移植。
最后,给定 T
的两个变量:
T a, b;
可以得出结论:
(&a - &b) * sizeof(T) == (ptrdiff_t)((uintptr_t)&a - (uintptr_t)&b)
这段代码能编译吗?
void pump1(int& i) { } void pump2(const int& i) { } long lval = 24; pump1(lval); // ? pump2(lval); // ?
这是 Stanley Lippman 在某次演讲中用过的例子,在此就不详细解释了,动手试试就知道。
C++ 为什么要引入引用?其实大多数人都没想过这个问题,就算想了,也摸不清重点。
我们来看一个例子。
给定一个表示矩阵的类 Matrix
,现在想实现 +
操作符,如果没有引用,那只能这样:
Matrix operator+(Matrix m1, Matrix m2);
我们都知道这会有性能问题,因为参数是“传值”的,每次调用都会有临时对象的构造和销毁。对一个3D程序来说,这种矩阵运算每秒钟可能有上万次,这种不必要的浪费会导致严重的性能问题。
参数使用指针,可以避免性能问题:
Matrix operator+(Matrix* m1, Matrix* m2);
但是指针的语法并不直观(non-intuitive,指针本质上就是一个间接层),也容易出错。
不直观这一点是无法接受的,因为操作符重载背后的思想,就是为了让类对象的使用变得直观。
此外,指针的语法,也不能阻止程序员写出这样的代码:
Matrix d = &a + &b + &c; // Oops!
这种连加操作根本不能编译, &a + &b
返回的是值,无法再与 &c
相加,如果改成返回指针,又有内存管理的问题。
Matrix* operator+(Matrix* m1, Matrix* m2);
正因为此,C++ 之父才决定引入引用。引用解决了对象语法的性能短板,在保留指针的高效、避免对象拷贝开销的同时,提供了对象操作的直观的语法。
const was a useful alternative to macros for representing constants only if global consts were implicitly local to their compilation unit. Only in that case could the compiler easily deduce that their value really didn't change.
(Bjarne Stroustrup,《C++ 的设计和演化》, 3.8)
这里的常量,特指以 const
关键字修饰的变量。
两个编译单元里的常量,即使包含自同一个头文件,也互不干扰。
// const.h const int INT = 1;
// test_1.cc #include "const.h" const void* get_int_address_1() { return &INT; }
// test_2.cc #include "const.h" const void* get_int_address_2() { return &INT; }
// main.cc extern const void* get_int_address_1(); extern const void* get_int_address_2(); int main() { // 地址不一样,说明各有一份 assert(get_int_address_1() != get_int_address_2()); }
编译单元 test_1.cc
和 test_2.cc
都包含了 const.h
里的常量 INT
,但其实它们各有一份自己的 INT
,互不干扰。
如果头文件里定义的常量不是 int
, double
这种原始类型,必须加上关键字 static
,否则多个编译单元在链接(link)时,会报错说有重复定义。
// const.h static const std::string STR = "test";
C++ 提供了四个转型操作符。
static_cast
中的 static
是指 编译时 ,转型失败的话就不能编译。
dynamic_cast
中的 dynamic
是指 运行时 ,转型失败的话会返回 NULL
(转指针时)或引发 std::bad_cast
异常(转引用时)。
reinterpret_cast
意为重新解释,转型时不做任何检查。
const_cast
只是去掉常量性。
下面说说常用的其它两种转型,取自 Google Chrome 的源码。
template<typename To, typename From> inline To implicit_cast(From const &f) { return f; }
用法:
double d = 3.14; int i = 3; std::min(d, implicit_cast<double>(i));
你可能会有两个问题:
能不能之间用 std::min(d, i)
?
为什么不用 static_cast
?
implicit_cast
只在特殊情况下才有必要,即当一个表达式的类型必须被精确控制时,比如说,为了避免重载(overload)。
implicit_cast
相较于其它转型的好处是,读代码的人可以立即就明白,这只是一个简单的隐式转换,不是一个潜在的危险的转型(不完全正确,见下文)。
如下代码:
int t = d; // 警告:可能有数据损失 std::min(t, i);
就等价于:
std::min(implicit_cast<int>(d), i); // 警告:可能有数据损失
可见, implicit_cast
简化了隐式转型的用法。
直接用 static_cast
或 C 风格强转是很危险的,因为编译器不再警告。
std::min(static_cast<int>(d), i); // 没有警告 std::min((int)d, i); // 没有警告
再看一个例子:枚举间的转型。
enum E1 { e1_0, e1_1, e1_2 }; enum E2 { e2_0, e2_1, e2_2 }; E1 e = static_cast<E1>(e2_0);
有些编译器不喜欢枚举到枚举的转型,因此我们先 implicit_cast
到 int
:
E1 e = static_cast<E1>(implicit_cast<int>(e2_0));
down_cast
是在继承结构中往下转型,这也正是 down
的含义,它是用来替代 dynamic_cast
的,没有运行时检查,直接用 static_cast
来做转型,从而提高性能。当然,使用场景也就受了限制,只有当你 100% 确定 From
和 To
的关系时,才能使用,否则后果自负。
template<typename To, typename From> inline To down_cast(From* f) { if (false) { implicit_cast<From*, To>(0); } #if !defined(NDEBUG) && !defined(GOOGLE_PROTOBUF_NO_RTTI) assert(f == NULL || dynamic_cast<To>(f) != NULL); // RTTI: debug mode only! #endif return static_cast<To>(f); }
down_cast
的实现巧妙的使用了 implicit_cast
,让编译器帮助做了类型检查,而 if (false)
条件保证了最终肯定会被编译器优化掉,所以对性能没有任何影响。
一些原则:
如果一个编译单元使用了某个内联(inline),那么它就必须要能够看到这个内联的定义。
模板在实例化(instantiation)时必须要能够看到这个模板的定义。
如果一个编译单元使用了某个模板的(完整)特例化,那么它不必看到这个特例化的定义,只要看到声明就可以了,一如非模板的情况。(如果特例化是在头文件中,它便总是内联的。)
应该尽可能避免使用宏。怎么做?用模板和内联,模板提供泛型(genericity),内联提供效率(efficiency)。
举个例子,宏:
#define MIN(a, b) ((a) < (b) ? (a) : (b))
可以替换为:
template <typename T> inline T min(const T& a, const T& b) { return a < b ? a : b; }
如果你需要拼接符号(tokens)的能力,那么就必须用宏,无可替代:
比如用宏来初始化一个结构体:
struct image_info { std::string file; // 图片的文件名 unsigned char* img2c; // 缺省使用的图片(一块内存) size_t img2c_len; // img2c数组的大小 }; #define _PNG(name) #name ".png", name##_png, sizeof(name##_png)
还有那个 Linux 内核链表的例子,除了宏也不可能:
#define list_entry(ptr, type, member) / ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
模板特例化(template specialization)可以理解成一种静态派(static dispath)发机制。
注意: 本文不会讨论模板偏特化(partial specialization)。
STL 里的模板函数 swap
针对 vector
做了特例化,直接调用 vector
自己的 swap
成员函数,从而避免了拷贝和复制。
template <class T> void swap(T& a, T& b) { T tmp = a; a = b; b = tmp; } template <class T> void swap<vector<T> >(vector<T>& a, vector<T>& b) { a.swap(b); }
模板特例化和模板重载(template overloading)是两回事。
template <class Base, class Exponent> Base pow(Base b, Exponent e); template <> int pow(int b, int e); // 特例化 template <class Base> Base pow(Base b, int); // 重载
举一个不太好的例子, vector
针对 bool
的特例化,达到节省内存的目的:一个 bool
只用一个 bit。
template <typename T> class vector { T* vec_data; // ... }; template <> class vector<bool> { unsigned int *vector_data; // ... };
关于 vector<bool>
的问题,值得另写一篇文章了。
详见 Boost 库的头文件: boost/type_traits/remove_pointer.hpp
template<typename T> struct remove_pointer { typedef T type; }; template<typename T> struct remove_pointer<T*> { typedef T type; }; template<typename T> struct remove_pointer<T* const> { typedef T type; };
用法:
remove_pointer<int>::type i = 1; remove_pointer<int*>::type j = 2;
Google Chromium IPC uses template specialization to define how a data type is read, written and logged in the IPC system.
template <class P> struct ParamTraits { };
template <> struct ParamTraits<bool> { typedef bool param_type; static void Write(Message* m, const param_type& p); static bool Read(const Message* m, void** iter, param_type* r); static void Log(const param_type& p, std::string* l); };
template <> struct ParamTraits<int> { typedef int param_type; static void Write(Message* m, const param_type& p); static bool Read(const Message* m, void** iter, param_type* r); static void Log(const param_type& p, std::string* l); };
template <> struct ParamTraits<std::string> { typedef std::string param_type; static void Write(Message* m, const param_type& p); static bool Read(const Message* m, void** iter, param_type* r); static void Log(const param_type& p, std::string* l); };
第一部分:你可能不知道的 C++(一)